Browse Source

Merge branch 'develop' into fix/gui-v2-table-rename

pull/3117/head
Wing-Kam Wong 2 years ago
parent
commit
a8d40bd905
  1. 45
      packages/nc-gui-v2/assets/style-v2.scss
  2. 17
      packages/nc-gui-v2/components.d.ts
  3. 19
      packages/nc-gui-v2/components/cell/Checkbox.vue
  4. 8
      packages/nc-gui-v2/components/cell/Currency.vue
  5. 13
      packages/nc-gui-v2/components/cell/DatePicker.vue
  6. 14
      packages/nc-gui-v2/components/cell/DateTimePicker.vue
  7. 6
      packages/nc-gui-v2/components/cell/Decimal.vue
  8. 15
      packages/nc-gui-v2/components/cell/Duration.vue
  9. 11
      packages/nc-gui-v2/components/cell/Email.vue
  10. 6
      packages/nc-gui-v2/components/cell/Float.vue
  11. 6
      packages/nc-gui-v2/components/cell/Integer.vue
  12. 6
      packages/nc-gui-v2/components/cell/Json.vue
  13. 22
      packages/nc-gui-v2/components/cell/MultiSelect.vue
  14. 12
      packages/nc-gui-v2/components/cell/Percent.vue
  15. 13
      packages/nc-gui-v2/components/cell/Rating.vue
  16. 16
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  17. 6
      packages/nc-gui-v2/components/cell/Text.vue
  18. 6
      packages/nc-gui-v2/components/cell/TextArea.vue
  19. 11
      packages/nc-gui-v2/components/cell/TimePicker.vue
  20. 13
      packages/nc-gui-v2/components/cell/Url.vue
  21. 10
      packages/nc-gui-v2/components/cell/YearPicker.vue
  22. 9
      packages/nc-gui-v2/components/cell/attachment/index.vue
  23. 116
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  24. 2
      packages/nc-gui-v2/components/dashboard/settings/AppStore.vue
  25. 29
      packages/nc-gui-v2/components/dashboard/settings/Modal.vue
  26. 17
      packages/nc-gui-v2/components/dlg/AirtableImport.vue
  27. 10
      packages/nc-gui-v2/components/dlg/QuickImport.vue
  28. 2
      packages/nc-gui-v2/components/general/MiniSidebar.vue
  29. 74
      packages/nc-gui-v2/components/smartsheet-column/AdvancedOptions.vue
  30. 55
      packages/nc-gui-v2/components/smartsheet-column/CheckboxOptions.vue
  31. 29
      packages/nc-gui-v2/components/smartsheet-column/CurrencyOptions.vue
  32. 17
      packages/nc-gui-v2/components/smartsheet-column/DateOptions.vue
  33. 15
      packages/nc-gui-v2/components/smartsheet-column/DurationOptions.vue
  34. 180
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  35. 27
      packages/nc-gui-v2/components/smartsheet-column/EditOrAddProvider.vue
  36. 24
      packages/nc-gui-v2/components/smartsheet-column/FormulaOptions.vue
  37. 64
      packages/nc-gui-v2/components/smartsheet-column/LinkedToAnotherRecordOptions.vue
  38. 38
      packages/nc-gui-v2/components/smartsheet-column/LookupOptions.vue
  39. 25
      packages/nc-gui-v2/components/smartsheet-column/PercentOptions.vue
  40. 59
      packages/nc-gui-v2/components/smartsheet-column/RatingOptions.vue
  41. 41
      packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue
  42. 19
      packages/nc-gui-v2/components/smartsheet-column/SelectOptions.vue
  43. 2
      packages/nc-gui-v2/components/smartsheet-column/SpecificDBTypeOptions.vue
  44. 41
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  45. 38
      packages/nc-gui-v2/components/smartsheet-header/CellIcon.vue
  46. 31
      packages/nc-gui-v2/components/smartsheet-header/Menu.vue
  47. 42
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  48. 6
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  49. 9
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  50. 34
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  51. 4
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  52. 4
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  53. 277
      packages/nc-gui-v2/components/smartsheet/Form.vue
  54. 69
      packages/nc-gui-v2/components/smartsheet/Gallery.vue
  55. 97
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  56. 28
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  57. 18
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  58. 19
      packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue
  59. 6
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  60. 98
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  61. 10
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  62. 8
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  63. 14
      packages/nc-gui-v2/components/smartsheet/sidebar/menu/ApiSnippet.vue
  64. 13
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  65. 46
      packages/nc-gui-v2/components/tabs/Auth.vue
  66. 19
      packages/nc-gui-v2/components/tabs/auth/ApiTokenManagement.vue
  67. 114
      packages/nc-gui-v2/components/tabs/auth/UserManagement.vue
  68. 2
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  69. 42
      packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue
  70. 6
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  71. 10
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  72. 10
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  73. 10
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  74. 16
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  75. 3
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  76. 15
      packages/nc-gui-v2/composables/useApi/index.ts
  77. 3
      packages/nc-gui-v2/composables/useApi/interceptors.ts
  78. 94
      packages/nc-gui-v2/composables/useColumn.ts
  79. 54
      packages/nc-gui-v2/composables/useColumnCreateStore.ts
  80. 12
      packages/nc-gui-v2/composables/useGlobal/types.ts
  81. 58
      packages/nc-gui-v2/composables/useMetas.ts
  82. 32
      packages/nc-gui-v2/composables/useProject.ts
  83. 2
      packages/nc-gui-v2/composables/useTable.ts
  84. 18
      packages/nc-gui-v2/composables/useTabs.ts
  85. 13
      packages/nc-gui-v2/composables/useViewColumns.ts
  86. 21
      packages/nc-gui-v2/composables/useViewSorts.ts
  87. 4
      packages/nc-gui-v2/context/index.ts
  88. 142
      packages/nc-gui-v2/layouts/base.vue
  89. 2
      packages/nc-gui-v2/lib/types.ts
  90. 3
      packages/nc-gui-v2/middleware/auth.global.ts
  91. 1
      packages/nc-gui-v2/package.json
  92. 64
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  93. 44
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue
  94. 0
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue
  95. 0
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/auth.vue
  96. 0
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/index.vue
  97. 73
      packages/nc-gui-v2/pages/forgot-password.vue
  98. 11
      packages/nc-gui-v2/pages/index/index.vue
  99. 96
      packages/nc-gui-v2/pages/signin.vue
  100. 143
      packages/nc-gui-v2/pages/signup.vue
  101. Some files were not shown because too many files have changed in this diff Show More

45
packages/nc-gui-v2/assets/style-v2.scss

@ -108,3 +108,48 @@ html {
.ant-modal-wrap {
@apply !scrollbar-thin-dull;
}
.animated-bg-gradient {
background: linear-gradient(122deg, #6f3381, #81c7d4, #fedfe1, #9ee59e);
background-size: 800% 800%;
-webkit-animation: gradient 4s ease infinite;
-moz-animation: gradient 4s ease infinite;
animation: gradient 4s ease infinite;
}
@-webkit-keyframes gradient {
0% {
background-position: 0% 22%
}
50% {
background-position: 100% 79%
}
100% {
background-position: 0% 22%
}
}
@-moz-keyframes gradient {
0% {
background-position: 0% 22%
}
50% {
background-position: 100% 79%
}
100% {
background-position: 0% 22%
}
}
@keyframes gradient {
0% {
background-position: 0% 22%
}
50% {
background-position: 100% 79%
}
100% {
background-position: 0% 22%
}
}

17
packages/nc-gui-v2/components.d.ts vendored

@ -65,22 +65,33 @@ declare module '@vue/runtime-core' {
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
EvaEmailOutline: typeof import('~icons/eva/email-outline')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
IcRoundSearch: typeof import('~icons/ic/round-search')['default']
MaterialSymbolsAttachFile: typeof import('~icons/material-symbols/attach-file')['default']
MaterialSymbolsChevronLeftRounded: typeof import('~icons/material-symbols/chevron-left-rounded')['default']
MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default']
MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
MdiAccountGroup: typeof import('~icons/mdi/account-group')['default']
MdiAccountIcon: typeof import('~icons/mdi/account-icon')['default']
MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default']
MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
MdiApi: typeof import('~icons/mdi/api')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiAt: typeof import('~icons/mdi/at')['default']
MdiCalculator: typeof import('~icons/mdi/calculator')['default']
MdiCalendarMonth: typeof import('~icons/mdi/calendar-month')['default']
MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default']
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default']
@ -98,13 +109,16 @@ declare module '@vue/runtime-core' {
MdiDownload: typeof import('~icons/mdi/download')['default']
MdiDrag: typeof import('~icons/mdi/drag')['default']
MdiDragVertical: typeof import('~icons/mdi/drag-vertical')['default']
MdiDramaMasks: typeof import('~icons/mdi/drama-masks')['default']
MdiEmail: typeof import('~icons/mdi/email')['default']
MdiEmailArrowRightOutline: typeof import('~icons/mdi/email-arrow-right-outline')['default']
MdiExitToApp: typeof import('~icons/mdi/exit-to-app')['default']
MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiGithub: typeof import('~icons/mdi/github')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default']
@ -112,6 +126,7 @@ declare module '@vue/runtime-core' {
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLink: typeof import('~icons/mdi/link')['default']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
MdiLogin: typeof import('~icons/mdi/login')['default']
MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
@ -127,6 +142,7 @@ declare module '@vue/runtime-core' {
MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiStar: typeof import('~icons/mdi/star')['default']
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
MdiStore: typeof import('~icons/mdi/store')['default']
MdiTable: typeof import('~icons/mdi/table')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
@ -134,6 +150,7 @@ declare module '@vue/runtime-core' {
MdiText: typeof import('~icons/mdi/text')['default']
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']
MdiTrashCan: typeof import('~icons/mdi/trash-can')['default']
MdiTwitter: typeof import('~icons/mdi/twitter')['default']
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MdiXml: typeof import('~icons/mdi/xml')['default']
RouterLink: typeof import('vue-router')['RouterLink']

19
packages/nc-gui-v2/components/cell/Checkbox.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import { inject } from '#imports'
import { ColumnInj, IsFormInj } from '~/context'
import { getMdiIcon } from '@/utils'
import { ColumnInj, IsFormInj, getMdiIcon, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue?: boolean | undefined | number
@ -14,11 +13,15 @@ interface Emits {
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const vModel = $(useVModel(props, 'modelValue', emits))
let vModel = $(useVModel(props, 'modelValue', emits))
const column = inject(ColumnInj)
const isForm = inject(IsFormInj)
const editEnabled = inject(EditModeInj)
const checkboxMeta = $computed(() => {
return {
icon: {
@ -29,11 +32,17 @@ const checkboxMeta = $computed(() => {
...(column?.value?.meta || {}),
}
})
function onClick() {
if (editEnabled) {
vModel = !vModel
}
}
</script>
<template>
<div class="flex" :class="{ 'justify-center': !isForm, 'nc-cell-hover-show': !vModel }">
<div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel }" @click="vModel = !vModel">
<div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel }" @click="onClick">
<component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
:style="{

8
packages/nc-gui-v2/components/cell/Currency.vue

@ -1,10 +1,10 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref, useVModel } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
import { ColumnInj, computed, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number | null
modelValue: number | null | undefined
}
const props = defineProps<Props>()
@ -13,7 +13,7 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj)
const vModel = useVModel(props, 'modelValue', emit)

13
packages/nc-gui-v2/components/cell/DatePicker.vue

@ -1,9 +1,10 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { ColumnInj, ReadonlyInj } from '~/context'
import { ColumnInj } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: string | null
modelValue: string | null | undefined
}
const { modelValue } = defineProps<Props>()
@ -11,9 +12,11 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const columnMeta = inject(ColumnInj, null)
const readOnlyMode = inject(ReadonlyInj, false)
const editEnabled = inject(EditModeInj)
let isDateInvalid = $ref(false)
const dateFormat = columnMeta?.value?.meta?.date_format ?? 'YYYY-MM-DD'
const localState = $computed({
@ -61,10 +64,10 @@ watch(
class="!w-full px-1"
:format="dateFormat"
:placeholder="isDateInvalid ? 'Invalid date' : !readOnlyMode ? 'Select date' : ''"
:allow-clear="!readOnlyMode"
:allow-clear="!editEnabled"
:input-read-only="true"
:dropdown-class-name="randomClass"
:open="readOnlyMode ? false : open"
:open="editEnabled ? false : open"
@click="open = !open"
>
<template #suffixIcon></template>

14
packages/nc-gui-v2/components/cell/DateTimePicker.vue

@ -1,9 +1,9 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context'
import { EditModeInj } from '~/context'
interface Props {
modelValue: string | null
modelValue: string | null | undefined
}
const { modelValue } = defineProps<Props>()
@ -12,9 +12,10 @@ const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const readOnlyMode = inject(ReadonlyInj, false)
const editEnabled = inject(EditModeInj)
let isDateInvalid = $ref(false)
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const localState = $computed({
@ -63,11 +64,12 @@ watch(
:bordered="false"
class="!w-full px-1"
format="YYYY-MM-DD HH:mm"
:placeholder="isDateInvalid ? 'Invalid date' : !readOnlyMode ? 'Select date and time' : ''"
:allow-clear="!readOnlyMode"
:placeholder="isDateInvalid ? 'Invalid date' : !editEnabled ? 'Select date and time' : ''"
:allow-clear="!editEnabled"
:input-read-only="true"
:dropdown-class-name="randomClass"
:open="readOnlyMode ? false : open"
:open="editEnabled ? false : open"
:disabled="!editEnabled"
@click="open = !open"
@ok="open = !open"
>

6
packages/nc-gui-v2/components/cell/Decimal.vue

@ -1,10 +1,10 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports'
import { inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number | null | string
modelValue: number | null | string | undefined
}
interface Emits {
@ -15,7 +15,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj)
const vModel = useVModel(props, 'modelValue', emits)

15
packages/nc-gui-v2/components/cell/Duration.vue

@ -1,10 +1,9 @@
<script setup lang="ts">
import { computed, inject, ref } from '#imports'
import { ColumnInj } from '~/context'
import { convertDurationToSeconds, convertMS2Duration, durationOptions } from '~/utils'
import { ColumnInj, computed, convertDurationToSeconds, convertMS2Duration, durationOptions, inject, ref } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number | string | null
modelValue: number | string | null | undefined
}
const { modelValue } = defineProps<Props>()
@ -13,12 +12,18 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)
const editEnabled = inject(EditModeInj)
const showWarningMessage = ref(false)
const durationInMS = ref(0)
const isEdited = ref(false)
const durationType = ref(column?.value?.meta?.duration || 0)
const durationPlaceholder = computed(() => durationOptions[durationType.value].title)
const localState = computed({
get: () => convertMS2Duration(modelValue, durationType.value),
set: (val) => {
@ -59,6 +64,7 @@ const submitDuration = () => {
<template>
<div class="duration-cell-wrapper">
<input
v-if="editEnabled"
ref="durationInput"
v-model="localState"
:placeholder="durationPlaceholder"
@ -66,6 +72,7 @@ const submitDuration = () => {
@keypress="checkDurationFormat($event)"
@keydown.enter="submitDuration"
/>
<span v-else> {{ localState }}</span>
<div v-if="showWarningMessage" class="duration-warning">
<!-- TODO: i18n -->
Please enter a number

11
packages/nc-gui-v2/components/cell/Email.vue

@ -1,11 +1,10 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref, useVModel } from '#imports'
import { isEmail } from '~/utils'
import { computed, inject, isEmail, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: string | null
modelValue: string | null | undefined
}
interface Emits {
@ -16,7 +15,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj)
const vModel = useVModel(props, 'modelValue', emits)
@ -26,8 +25,8 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script>
<template>
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none prose-sm" @blur="editEnabled = false" />
<a v-else-if="validEmail" class="prose-sm underline hover:opacity-75" :href="`mailto:${vModel}`" target="_blank">
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none text-sm" @blur="editEnabled = false" />
<a v-else-if="validEmail" class="text-sm underline hover:opacity-75" :href="`mailto:${vModel}`" target="_blank">
{{ vModel }}
</a>
<span v-else>{{ vModel }}</span>

6
packages/nc-gui-v2/components/cell/Float.vue

@ -1,10 +1,10 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports'
import { inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number | null
modelValue: number | null | undefined
}
interface Emits {
@ -15,7 +15,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj)
const vModel = useVModel(props, 'modelValue', emits)

6
packages/nc-gui-v2/components/cell/Integer.vue

@ -1,10 +1,10 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports'
import { inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number | null
modelValue: number | null | undefined
}
interface Emits {
@ -15,7 +15,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj)
const vModel = useVModel(props, 'modelValue', emits)

6
packages/nc-gui-v2/components/cell/Json.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import { Modal as AModal } from 'ant-design-vue'
import Editor from '~/components/monaco/Editor.vue'
import { computed, inject, ref, useVModel, watch } from '#imports'
import { ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
@ -18,6 +18,8 @@ const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false))
const readonly = inject(ReadonlyInj)
const vModel = useVModel(props, 'modelValue', emits)
const localValueState = ref<string | undefined>()
@ -88,7 +90,7 @@ watch(editEnabled, () => {
<template>
<component :is="isExpanded ? AModal : 'div'" v-model:visible="isExpanded" :closable="false" centered :footer="null">
<div v-if="editEnabled" class="flex flex-col w-full">
<div v-if="editEnabled && !readonly" class="flex flex-col w-full">
<div class="flex flex-row justify-between pt-1 pb-2">
<a-button type="text" size="small" @click="isExpanded = !isExpanded">
<CilFullscreenExit v-if="isExpanded" class="h-2.5" />

22
packages/nc-gui-v2/components/cell/MultiSelect.vue

@ -1,12 +1,12 @@
<script lang="ts" setup>
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk'
import { computed, inject } from '#imports'
import { ActiveCellInj, ColumnInj } from '~/context'
import { ActiveCellInj, ColumnInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props {
modelValue: string | string[] | undefined
modelValue?: string | string[] | undefined
}
const { modelValue } = defineProps<Props>()
@ -16,12 +16,17 @@ const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const column = inject(ColumnInj)
// const isForm = inject<boolean>('isForm', false)
// const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj)
const active = inject(ActiveCellInj, ref(false))
const selectedIds = ref<string[]>([])
const aselect = ref<typeof AntSelect>()
const isOpen = ref(false)
const options = computed(() => {
@ -38,7 +43,7 @@ const options = computed(() => {
})
const vModel = computed({
get: () => selectedIds.value.map((el) => options.value.find((op: SelectOptionType) => op.id === el).title),
get: () => selectedIds.value.map((el) => options.value.find((op: SelectOptionType) => op.id === el)?.title),
set: (val) => emit('update:modelValue', val.length === 0 ? null : val.join(',')),
})
@ -112,6 +117,7 @@ watch(isOpen, (n, _o) => {
show-arrow
:show-search="false"
:open="isOpen"
:disabled="!editEnabled"
@keydown="handleKeys"
@click="isOpen = !isOpen"
>
@ -130,7 +136,7 @@ watch(isOpen, (n, _o) => {
:close-icon="h(MdiCloseCircle, { class: ['ms-close-icon'] })"
@close="onClose"
>
<span class="text-slate-500">{{ val }}</span>
<span class="w-full text-slate-500">{{ val }}</span>
</a-tag>
</template>
</a-select>
@ -169,6 +175,10 @@ watch(isOpen, (n, _o) => {
:deep(.ant-tag-close-icon) {
@apply "text-slate-500";
}
:deep(.ant-select-selection-overflow-item) {
@apply "flex overflow-hidden";
}
</style>
<!--
/**

12
packages/nc-gui-v2/components/cell/Percent.vue

@ -1,16 +1,17 @@
<script setup lang="ts">
import { computed, inject } from '#imports'
import { ColumnInj } from '~/context'
import { getPercentStep, isValidPercent, renderPercent } from '@/utils/percentUtils'
import { ColumnInj, computed, getPercentStep, inject, isValidPercent, renderPercent } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number | string | null
modelValue: number | string | null | undefined
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
const column = inject(ColumnInj)
const percent = ref()
@ -63,5 +64,6 @@ function onKeyDownEnter() {
@blur="onBlur"
@keydown.enter="onKeyDownEnter"
/>
<input v-else v-model="localState" type="text" @focus="isEdited = true" />
<input v-if="editEnabled" v-model="localState" type="text" @focus="isEdited = true" />
<span v-else>{{ localState }}</span>
</template>

13
packages/nc-gui-v2/components/cell/Rating.vue

@ -1,18 +1,19 @@
<script setup lang="ts">
import { computed, inject } from '#imports'
import { ColumnInj } from '~/context'
import { ColumnInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue?: number | null
readOnly?: boolean
modelValue?: number | null | undefined
}
const { modelValue, readOnly } = defineProps<Props>()
const { modelValue } = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)
const ratingMeta = computed(() => {
return {
icon: {
@ -32,7 +33,7 @@ const vModel = computed({
</script>
<template>
<a-rate v-model:value="vModel" :count="ratingMeta.max" :style="`color: ${ratingMeta.color}`" :disabled="readOnly">
<a-rate v-model:value="vModel" :count="ratingMeta.max" :style="`color: ${ratingMeta.color}`" :disabled="!editEnabled">
<template #character>
<MdiStar v-if="ratingMeta.icon.full === 'mdi-star'" class="text-sm" />
<MdiHeart v-if="ratingMeta.icon.full === 'mdi-heart'" class="text-sm" />

16
packages/nc-gui-v2/components/cell/SingleSelect.vue

@ -1,11 +1,11 @@
<script lang="ts" setup>
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk'
import { computed, inject } from '#imports'
import { ActiveCellInj, ColumnInj } from '~/context'
import { ActiveCellInj, ColumnInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: string | undefined
modelValue?: string | undefined
}
const { modelValue } = defineProps<Props>()
@ -13,11 +13,15 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)
// const isForm = inject<boolean>('isForm', false)
// const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj)
const active = inject(ActiveCellInj, ref(false))
const aselect = ref<typeof AntSelect>()
const isOpen = ref(false)
const vModel = computed({
@ -72,11 +76,13 @@ watch(isOpen, (n, _o) => {
placeholder="Select an option"
:bordered="false"
:open="isOpen"
:disabled="!editEnabled"
:show-arrow="active || vModel === null"
@select="isOpen = false"
@keydown="handleKeys"
@click="isOpen = !isOpen"
>
<a-select-option v-for="op of options" :key="op.title" @click.stop>
<a-select-option v-for="op of options" :key="op.title" :value="op.title" @click.stop>
<a-tag class="rounded-tag" :color="op.color">
<span class="text-slate-500">{{ op.title }}</span>
</a-tag>

6
packages/nc-gui-v2/components/cell/Text.vue

@ -1,17 +1,17 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports'
import { inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: string | null
modelValue: string | null | undefined
}
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj)
const vModel = useVModel(props, 'modelValue', emits)

6
packages/nc-gui-v2/components/cell/TextArea.vue

@ -1,17 +1,17 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref } from '#imports'
import { computed, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: string | null
modelValue: string | null | undefined
}
const { modelValue } = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj)
const vModel = computed({
get: () => modelValue ?? '',

11
packages/nc-gui-v2/components/cell/TimePicker.vue

@ -1,10 +1,10 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context'
import { EditModeInj } from '~/context'
interface Props {
modelValue?: string | null
modelValue?: string | null | undefined
}
const { modelValue } = defineProps<Props>()
@ -13,9 +13,10 @@ const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const readOnlyMode = inject(ReadonlyInj, false)
const editEnabled = inject(EditModeInj)
let isTimeInvalid = $ref(false)
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const localState = $computed({
@ -76,9 +77,9 @@ watch(
format="HH:mm"
class="!w-full px-1"
:placeholder="isTimeInvalid ? 'Invalid time' : !readOnlyMode ? 'Select time' : ''"
:allow-clear="!readOnlyMode"
:allow-clear="!editEnabled"
:input-read-only="true"
:open="readOnlyMode ? false : open"
:open="editEnabled ? false : open"
:popup-class-name="randomClass"
@click="open = !open"
@ok="open = !open"

13
packages/nc-gui-v2/components/cell/Url.vue

@ -1,11 +1,10 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
import { isValidURL } from '~/utils'
import { ColumnInj, computed, inject, isValidURL } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: string | null
modelValue: string | null | undefined
}
const { modelValue: value } = defineProps<Props>()
@ -14,7 +13,7 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj)
const vModel = computed({
get: () => value,
@ -38,8 +37,8 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script>
<template>
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none" @blur="editEnabled = false" />
<nuxt-link v-else-if="isValid" class="py-2 underline hover:opacity-75" :to="url" target="_blank">{{ value }} </nuxt-link>
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none text-sm" @blur="editEnabled = false" />
<nuxt-link v-else-if="isValid" class="text-sm underline hover:opacity-75" :to="url" target="_blank">{{ value }} </nuxt-link>
<span v-else>{{ value }}</span>
</template>

10
packages/nc-gui-v2/components/cell/YearPicker.vue

@ -1,17 +1,17 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number | string | null
modelValue: number | string | null | undefined
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const readOnlyMode = inject(ReadonlyInj, false)
const editEnabled = inject(EditModeInj)
let isYearInvalid = $ref(false)
@ -62,9 +62,9 @@ watch(
:bordered="false"
class="!w-full px-1"
:placeholder="isYearInvalid ? 'Invalid year' : !readOnlyMode ? 'Select year' : ''"
:allow-clear="!readOnlyMode"
:allow-clear="!editEnabled"
:input-read-only="true"
:open="readOnlyMode ? false : open"
:open="editEnabled ? false : open"
:dropdown-class-name="randomClass"
@click="open = !open"
@change="open = !open"

9
packages/nc-gui-v2/components/cell/attachment/index.vue

@ -83,9 +83,9 @@ onKeyDown('Escape', () => {
<template #title> Click or drop a file into cell </template>
<div class="flex items-center gap-2">
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120)" />
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120) text-gray-500 text-[10px]" />
<div v-if="!visibleItems.length" class="group-hover:text-primary">Add file(s)</div>
<div v-if="!visibleItems.length" class="group-hover:text-primary text-gray-500 text-xs">Add file(s)</div>
</div>
</a-tooltip>
</div>
@ -131,7 +131,10 @@ onKeyDown('Escape', () => {
<a-tooltip v-else placement="bottom">
<template #title> View attachments </template>
<MdiArrowExpand class="select-none transform group-hover:(text-pink-500 scale-120)" @click.stop="modalVisible = true" />
<MdiArrowExpand
class="select-none transform group-hover:(text-pink-500 scale-120) text-[10px] text-gray-500"
@click.stop="modalVisible = true"
/>
</a-tooltip>
</div>
</template>

116
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -1,26 +1,27 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { useNuxtApp, useRoute } from '#app'
import { Empty } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { computed, useProject, useTable, useTabs, watchEffect } from '#imports'
import { TabType } from '~/composables'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
import MdiMenuDown from '~icons/mdi/chevron-down'
import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiDrag from '~icons/mdi/drag-vertical'
import MdiPlus from '~icons/mdi/plus-circle-outline'
const { addTab } = useTabs()
const { $api, $e } = useNuxtApp()
const route = useRoute()
const { tables, loadTables, isSharedBase } = useProject()
const { tables, loadTables } = useProject(route.params.projectId as string)
const { activeTab } = useTabs()
const { deleteTable } = useTable()
const { isUIAllowed } = useUIPermission()
const tablesById = $computed<Record<string, TableType>>(() =>
tables?.value?.reduce((acc: Record<string, TableType>, table: TableType) => {
acc[table.id as string] = table
@ -28,8 +29,8 @@ const tablesById = $computed<Record<string, TableType>>(() =>
}, {}),
)
const showTableList = ref(true)
const tableCreateDlg = ref(false)
let key = $ref(0)
const menuRef = $ref<HTMLLIElement>()
@ -135,21 +136,26 @@ const activeTable = computed(() => {
<template>
<div class="nc-treeview-container flex flex-col">
<div class="px-6 py-[11.75px] border-b-1">
<a-input-search
v-model:value="filterQuery"
size="small"
class="nc-filter-input"
:placeholder="$t('placeholder.searchProjectTree')"
/>
<div class="px-6 py-[8.75px] border-b-1 nc-filter-input">
<div class="flex items-center bg-gray-50 rounded relative">
<a-input
v-model:value="filterQuery"
class="nc-filter-input !bg-transparent"
:placeholder="$t('placeholder.searchProjectTree')"
/>
<MdiSearch class="nc-filter-input-icon text-gray-400 mx-3 absolute right-[-4px] top-[7px]" />
</div>
</div>
<a-dropdown :trigger="['contextmenu']">
<div class="p-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull" style="direction: rtl">
<div
class="p-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull"
:class="{ 'mb-[20px]': isSharedBase }"
style="direction: rtl"
>
<div
style="direction: ltr"
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@click="showTableList = !showTableList"
@contextmenu="setMenuContext('main')"
>
<span class="flex-grow text-bold uppercase nc-project-tree text-gray-500 font-weight-bold">
@ -157,24 +163,9 @@ const activeTable = computed(() => {
<template v-if="tables?.length"> ({{ tables.length }}) </template>
</span>
<MdiPlus
v-t="['c:table:create:navdraw']"
class="transform text-gray-500 hover:(text-pink-500 scale-105) nc-btn-tbl-add"
@click.stop="tableCreateDlg = true"
/>
<MdiMenuDown
class="transition-transform !duration-100 text-gray-500 hover:text-pink-500"
:class="{ 'transform rotate-180': showTableList }"
/>
</div>
<div style="direction: ltr" class="flex-1">
<div
v-if="tables.length"
class="transition-height duration-200 overflow-hidden"
:class="{ 'h-100': showTableList, 'h-0': !showTableList }"
>
<div v-if="tables.length" class="transition-height duration-200 overflow-hidden">
<div :key="key" ref="menuRef" class="border-none sortable-list">
<div
v-for="table of tables"
@ -192,24 +183,42 @@ const activeTable = computed(() => {
<div class="flex align-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto">
<MdiDrag
v-if="isUIAllowed('treeview-drag-n-drop')"
:class="`nc-child-draggable-icon-${table.title}`"
class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
@click.stop.prevent
/>
<component :is="icon(table)" class="nc-view-icon group-hover:hidden text-xs group-hover:text-gray-500" />
<component
:is="icon(table)"
class="nc-view-icon text-xs"
:class="{ 'group-hover:hidden group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop') }"
/>
</div>
<div class="nc-tbl-title flex-1">{{ table.title }}</div>
<a-dropdown :trigger="['click']" @click.stop>
<a-dropdown v-if="isUIAllowed('table-rename') || isUIAllowed('table-delete')" :trigger="['click']" @click.stop>
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
<a-menu class="cursor-pointer">
<a-menu-item v-t="" class="!text-xs" @click="showRenameTableDlg(table)"><div>Rename</div></a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-rename')"
v-t="['c:table:rename']"
class="!text-xs"
@click="showRenameTableDlg(table)"
><div>Rename</div></a-menu-item
>
<a-menu-item
v-if="isUIAllowed('table-delete')"
v-t="['c:table:delete']"
class="!text-xs"
@click="deleteTable(table)"
>
Delete</a-menu-item
>
</a-menu>
</template>
</a-dropdown>
@ -217,21 +226,39 @@ const activeTable = computed(() => {
</div>
</div>
</div>
<a-card v-else class="mt-4 mx-4 !bg-gray-50">
<div class="flex flex-col align-center">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<a-button type="primary" @click.stop="tableCreateDlg = true">{{ $t('tooltip.addTable') }}</a-button>
</div>
</a-card>
</div>
</div>
<template #overlay>
<a-menu class="cursor-pointer">
<template v-if="contextMenuTarget.type === 'table'">
<a-menu-item class="!text-xs" @click="showRenameTableDlg(contextMenuTarget.value)">
<a-menu-item
v-if="isUIAllowed('table-rename')"
v-t="['c:table:rename']"
class="!text-xs"
@click="showRenameTableDlg(contextMenuTarget.value)"
>
{{ $t('general.rename') }}
</a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(contextMenuTarget.value)">
<a-menu-item
v-if="isUIAllowed('table-delete')"
v-t="['c:table:delete']"
class="!text-xs"
@click="deleteTable(contextMenuTarget.value)"
>
{{ $t('general.delete') }}
</a-menu-item>
</template>
<template v-else>
<a-menu-item class="!text-xs" @click="reloadTables">
<a-menu-item v-t="['c:table:reload']" class="!text-xs" @click="reloadTables">
{{ $t('general.reload') }}
</a-menu-item>
</template>
@ -244,7 +271,7 @@ const activeTable = computed(() => {
</div>
</template>
<style scoped>
<style scoped lang="scss">
.nc-treeview-container {
@apply h-[calc(100vh_-_var(--header-height))];
}
@ -271,7 +298,7 @@ const activeTable = computed(() => {
@apply !pointer-events-none;
}
&.dragging {
& .dragging {
.nc-icon {
@apply !hidden;
}
@ -300,6 +327,7 @@ const activeTable = computed(() => {
.nc-tree-item.active {
@apply !text-primary font-weight-bold after:(!opacity-20);
svg {
@apply !text-primary;
}
@ -308,4 +336,10 @@ const activeTable = computed(() => {
.nc-tree-item:hover {
@apply !text-grey after:(!opacity-5);
}
:deep(.nc-filter-input) {
.ant-input {
@apply pr-6 !border-0;
}
}
</style>

2
packages/nc-gui-v2/components/dashboard/settings/AppStore.vue

@ -110,7 +110,7 @@ onMounted(async () => {
<div class="flex ml-0.5">Reset</div>
</div>
</a-button>
<a-button v-else size="small" outlined @click="showInstallPluginModal(app)">
<a-button v-else size="small" outlined type="primary" ghost @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<MdiPlusIcon />
Install

29
packages/nc-gui-v2/components/dashboard/settings/Modal.vue

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue'
import { useI18n } from 'vue-i18n'
import AuditTab from './AuditTab.vue'
import AppStore from './AppStore.vue'
import Metadata from './Metadata.vue'
@ -10,7 +11,7 @@ import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple'
import NootbookOutline from '~icons/mdi/notebook-outline'
import { useVModel, watch } from '#imports'
import { useUIPermission, useVModel, watch } from '#imports'
interface Props {
modelValue: boolean
@ -38,19 +39,29 @@ const emits = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emits)
const { isUIAllowed } = useUIPermission()
const { t } = useI18n()
const tabsInfo: TabGroup = {
teamAndAuth: {
title: 'Team and Auth',
icon: TeamFillIcon,
subTabs: {
usersManagement: {
title: 'Users Management',
body: UserManagement,
},
apiTokenManagement: {
title: 'API Token Management',
body: ApiTokenManagement,
},
...(isUIAllowed('userMgmtTab') && {
usersManagement: {
// Users Management
title: t('title.userMgmt'),
body: UserManagement,
},
}),
...(isUIAllowed('apiTokenTab') && {
apiTokenManagement: {
// API Tokens Management
title: t('title.apiTokenMgmt'),
body: ApiTokenManagement,
},
}),
},
},
appStore: {

17
packages/nc-gui-v2/components/dlg/AirtableImport.vue

@ -224,7 +224,13 @@ onBeforeUnmount(() => {
</script>
<template>
<a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" :mask-closable="false" @keydown.esc="dialogShow = false">
<a-modal
v-model:visible="dialogShow"
width="max(30vw, 600px)"
:mask-closable="false"
class="pa-2"
@keydown.esc="dialogShow = false"
>
<template #footer>
<div v-if="step === 1">
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
@ -239,15 +245,14 @@ onBeforeUnmount(() => {
</a-button>
</div>
</template>
<a-typography-title class="ml-5 mt-5" type="secondary" :level="5">QUICK IMPORT - AIRTABLE</a-typography-title>
<span class="ml-5 mt-5 prose-xl font-weight-bold" type="secondary" :level="5">QUICK IMPORT - AIRTABLE</span>
<div class="ml-5 mr-5">
<a-divider />
<div v-if="step === 1">
<div class="mb-4">
<span class="prose-xl font-bold mr-3">Credentials</span>
<span class="mr-3 pt-2 text-gray-500 text-xs">Credentials</span>
<a
href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials"
class="prose-sm underline text-grey"
class="prose-sm underline text-grey text-xs"
target="_blank"
>Where to find this?
</a>
@ -269,7 +274,7 @@ onBeforeUnmount(() => {
size="large"
/>
</a-form-item>
<span class="prose-xl font-bold self-center my-4">Advanced Settings</span>
<span class="prose-lg self-center my-4 text-gray-500">Advanced Settings</span>
<a-divider class="mt-2 mb-5" />
<div class="mt-0 my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncData">Import Data</a-checkbox>

10
packages/nc-gui-v2/components/dlg/QuickImport.vue

@ -175,8 +175,8 @@ async function parseAndExtractData(val: any, name: string) {
}
function rejectDrop(fileList: any[]) {
fileList.map((_) => {
return message.error('Template Generator cannot be found!')
fileList.map((file) => {
return message.error(`Failed to upload file ${file.name}`)
})
}
@ -235,7 +235,7 @@ function getAdapter(name: string, val: any) {
<template>
<a-modal v-model:visible="dialogShow" :width="modalWidth" :mask-closable="false" @keydown.esc="dialogShow = false">
<a-typography-title class="ml-5 mt-5 mb-5" type="secondary" :level="5">{{ importMeta.header }}</a-typography-title>
<span class="prose-xl font-weight-bold ml-5 mt-5 mb-5" type="secondary" :level="5">{{ importMeta.header }}</span>
<template #footer>
<a-button v-if="templateEditorModal" key="back" @click="templateEditorModal = false">Back</a-button>
<a-button v-else key="cancel" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
@ -260,7 +260,7 @@ function getAdapter(name: string, val: any) {
$t('activity.import')
}}</a-button>
</template>
<div class="ml-5 mr-5">
<div class="ml-5 mr-5 mt-5">
<TemplateEditor
v-if="templateEditorModal"
ref="templateEditorRef"
@ -330,7 +330,7 @@ function getAdapter(name: string, val: any) {
<div v-if="!templateEditorModal" class="ml-5 mr-5">
<a-divider />
<div class="mb-4">
<span class="prose-xl font-bold">Advanced Settings</span>
<span class="prose-lg">Advanced Settings</span>
<a-form-item class="mt-4 mb-2" :label="t('msg.info.footMsg')" v-bind="validateInfos.maxRowsToParse">
<a-input-number v-model:value="importState.parserConfig.maxRowsToParse" :min="1" :max="50000" />
</a-form-item>

2
packages/nc-gui-v2/components/general/MiniSidebar.vue

@ -106,7 +106,7 @@ const logout = () => {
<div
:class="[route.name.includes('nc-projectId') ? 'active' : 'pointer-events-none !text-gray-400']"
class="nc-mini-sidebar-item"
@click="navigateTo(`/nc/${route.params.projectId}`)"
@click="navigateTo(`/${route.params.projectType}/${route.params.projectId}`)"
>
<MdiDatabase class="cursor-pointer transform hover:scale-105 text-2xl" />
</div>

74
packages/nc-gui-v2/components/smartsheet-column/AdvancedOptions.vue

@ -1,67 +1,66 @@
<script setup lang="ts">
import type { UITypes } from 'nocodb-sdk'
import { computed, useColumnCreateStoreOrThrow } from '#imports'
import { computed } from '#imports'
const { formState, validateInfos, sqlUi, onDataTypeChange, onAlter } = useColumnCreateStoreOrThrow()!
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const { sqlUi } = useProject()
const { onAlter, onDataTypeChange, validateInfos } = useColumnCreateStoreOrThrow()
// todo: 2nd argument of `getDataTypeListForUiType` is missing!
const dataTypes = computed(() => sqlUi?.value?.getDataTypeListForUiType(formState.value as { uidt: UITypes }, '' as any))
const dataTypes = computed(() => sqlUi?.value?.getDataTypeListForUiType(vModel.value as { uidt: UITypes }, '' as any))
// to avoid type error with checkbox
formState.value.rqd = !!formState.value.rqd
formState.value.pk = !!formState.value.pk
formState.value.un = !!formState.value.un
formState.value.ai = !!formState.value.ai
formState.value.au = !!formState.value.au
vModel.value.rqd = !!vModel.value.rqd
vModel.value.pk = !!vModel.value.pk
vModel.value.un = !!vModel.value.un
vModel.value.ai = !!vModel.value.ai
vModel.value.au = !!vModel.value.au
</script>
<template>
<div class="p-4 border-[2px] radius-1 border-grey w-full">
<div class="p-4 border-[2px] radius-1 border-grey w-full flex flex-col gap-2">
<div class="flex justify-space-between">
<a-form-item label="NN">
<a-checkbox
v-model:checked="formState.rqd"
:disabled="formState.pk || !sqlUi.columnEditable(formState)"
size="small"
v-model:checked="vModel.rqd"
:disabled="vModel.pk || !sqlUi.columnEditable(vModel)"
class="nc-column-checkbox-NN"
@change="onAlter"
/>
</a-form-item>
<a-form-item label="PK">
<a-checkbox
v-model:checked="formState.pk"
:disabled="!sqlUi.columnEditable(formState)"
size="small"
v-model:checked="vModel.pk"
:disabled="!sqlUi.columnEditable(vModel)"
class="nc-column-checkbox-PK"
@change="onAlter"
/>
</a-form-item>
<a-form-item label="AI">
<a-checkbox
v-model:checked="formState.ai"
:disabled="sqlUi.colPropUNDisabled(formState) || !sqlUi.columnEditable(formState)"
size="small"
v-model:checked="vModel.ai"
:disabled="sqlUi.colPropUNDisabled(vModel) || !sqlUi.columnEditable(vModel)"
class="nc-column-checkbox-AI"
@change="onAlter"
/>
</a-form-item>
<a-form-item
label="UN"
:disabled="sqlUi.colPropUNDisabled(formState) || !sqlUi.columnEditable(formState)"
@change="onAlter"
>
<a-checkbox v-model:checked="formState.un" size="small" class="nc-column-checkbox-UN" />
<a-form-item label="UN" :disabled="sqlUi.colPropUNDisabled(vModel) || !sqlUi.columnEditable(vModel)" @change="onAlter">
<a-checkbox v-model:checked="vModel.un" class="nc-column-checkbox-UN" />
</a-form-item>
<a-form-item
label="AU"
:disabled="sqlUi.colPropAuDisabled(formState) || !sqlUi.columnEditable(formState)"
@change="onAlter"
>
<a-checkbox v-model:checked="formState.au" size="small" class="nc-column-checkbox-AU" />
<a-form-item label="AU" :disabled="sqlUi.colPropAuDisabled(vModel) || !sqlUi.columnEditable(vModel)" @change="onAlter">
<a-checkbox v-model:checked="vModel.au" class="nc-column-checkbox-AU" />
</a-form-item>
</div>
<a-form-item :label="$t('labels.databaseType')" v-bind="validateInfos.dt">
<a-select v-model:value="formState.dt" size="small" @change="onDataTypeChange">
<a-select v-model:value="vModel.dt" @change="onDataTypeChange">
<a-select-option v-for="type in dataTypes" :key="type" :value="type">
{{ type }}
</a-select-option>
@ -69,18 +68,17 @@ formState.value.au = !!formState.value.au
</a-form-item>
<a-form-item :label="$t('labels.lengthValue')">
<a-input
v-model:value="formState.dtxp"
:disabled="sqlUi.getDefaultLengthIsDisabled(formState.dt) || !sqlUi.columnEditable(formState)"
size="small"
v-model:value="vModel.dtxp"
:disabled="sqlUi.getDefaultLengthIsDisabled(vModel.dt) || !sqlUi.columnEditable(vModel)"
@input="onAlter"
/>
</a-form-item>
<a-form-item v-if="sqlUi.showScale(formState)" label="Scale">
<a-input v-model="formState.dtxs" :disabled="!sqlUi.columnEditable(formState)" size="small" @input="onAlter" />
<a-form-item v-if="sqlUi.showScale(vModel)" label="Scale">
<a-input v-model="vModel.dtxs" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" />
</a-form-item>
<a-form-item :label="$t('placeholder.defaultValue')">
<a-textarea v-model:value="formState.cdf" size="small" auto-size @input="onAlter(2, true)" />
<span class="text-gray-400 text-xs">{{ sqlUi.getDefaultValueForDatatype(formState.dt) }}</span>
<a-textarea v-model:value="vModel.cdf" auto-size @input="onAlter(2, true)" />
<span class="text-gray-400 text-xs">{{ sqlUi.getDefaultValueForDatatype(vModel.dt) }}</span>
</a-form-item>
</div>
</template>

55
packages/nc-gui-v2/components/smartsheet-column/CheckboxOptions.vue

@ -1,8 +1,13 @@
<script setup lang="ts">
import { useColumnCreateStoreOrThrow } from '#imports'
import { getMdiIcon } from '@/utils'
const { formState } = useColumnCreateStoreOrThrow()
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
// cater existing v1 cases
const iconList = [
@ -37,34 +42,34 @@ const iconList = [
]
const picked = computed({
get: () => formState.value.meta.color,
get: () => vModel.value.meta.color,
set: (val) => {
formState.value.meta.color = val
vModel.value.meta.color = val
},
})
// set default value
formState.value.meta = {
vModel.value.meta = {
icon: {
checked: 'mdi-check-bold',
unchecked: 'mdi-crop-square',
},
color: '#777',
...formState.value.meta,
...vModel.value.meta,
}
// antdv doesn't support object as value
// use iconIdx as value and update back in watch
const iconIdx = iconList.findIndex(
(ele) => ele.checked === formState.value.meta.icon.checked && ele.unchecked === formState.value.meta.icon.unchecked,
(ele) => ele.checked === vModel.value.meta.icon.checked && ele.unchecked === vModel.value.meta.icon.unchecked,
)
formState.value.meta.iconIdx = iconIdx === -1 ? 0 : iconIdx
vModel.value.meta.iconIdx = iconIdx === -1 ? 0 : iconIdx
watch(
() => formState.value.meta.iconIdx,
() => vModel.value.meta.iconIdx,
(v) => {
formState.value.meta.icon = iconList[v]
vModel.value.meta.icon = iconList[v]
},
)
</script>
@ -73,21 +78,23 @@ watch(
<a-row>
<a-col :span="24">
<a-form-item label="Icon">
<a-select v-model:value="formState.meta.iconIdx" size="small" class="w-52">
<a-select v-model:value="vModel.meta.iconIdx" class="w-52">
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i">
<component
:is="getMdiIcon(icon.checked)"
class="mx-1"
:style="{
color: formState.meta.color,
}"
/>
<component
:is="getMdiIcon(icon.unchecked)"
:style="{
color: formState.meta.color,
}"
/>
<div class="flex items-center">
<component
:is="getMdiIcon(icon.checked)"
class="mx-1"
:style="{
color: vModel.meta.color,
}"
/>
<component
:is="getMdiIcon(icon.unchecked)"
:style="{
color: vModel.meta.color,
}"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>

29
packages/nc-gui-v2/components/smartsheet-column/CurrencyOptions.vue

@ -1,14 +1,20 @@
<script setup lang="ts">
import { useColumnCreateStoreOrThrow, useProject } from '#imports'
import { useProject } from '#imports'
import { currencyCodes, currencyLocales, validateCurrencyCode, validateCurrencyLocale } from '@/utils'
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
interface Option {
label: string
value: string
}
const { formState, validateInfos, setAdditionalValidations } = useColumnCreateStoreOrThrow()
const validators = {
'meta.currency_locale': [
{
@ -36,7 +42,8 @@ const validators = {
],
}
// set additional validations
const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow()
setAdditionalValidations({
...validators,
})
@ -47,7 +54,7 @@ const currencyList = currencyCodes || []
const currencyLocaleList = currencyLocales() || []
const isMoney = computed(() => formState.value.dt === 'money')
const isMoney = computed(() => vModel.value.dt === 'money')
const message = computed(() => {
if (isMoney.value && isPg) return "PostgreSQL 'money' type has own currency settings"
@ -59,20 +66,19 @@ function filterOption(input: string, option: Option) {
}
// set default value
formState.value.meta = {
vModel.value.meta = {
currency_locale: 'en-US',
currency_code: 'USD',
...formState.value.meta,
...vModel.value.meta,
}
</script>
<template>
<a-row>
<a-row gutter="8">
<a-col :span="12">
<a-form-item v-bind="validateInfos['meta.currency_locale']" label="Currency Locale">
<a-select
v-model:value="formState.meta.currency_locale"
size="small"
v-model:value="vModel.meta.currency_locale"
class="w-52"
show-search
:filter-option="filterOption"
@ -87,11 +93,10 @@ formState.value.meta = {
<a-col :span="12">
<a-form-item v-bind="validateInfos['meta.currency_code']" label="Currency Code">
<a-select
v-model:value="formState.meta.currency_code"
v-model:value="vModel.meta.currency_code"
class="w-52"
show-search
:filter-option="filterOption"
size="small"
:disabled="isMoney && isPg"
>
<a-select-option v-for="(currencyCode, i) of currencyList" :key="i" :value="currencyCode">

17
packages/nc-gui-v2/components/smartsheet-column/DateOptions.vue

@ -1,18 +1,23 @@
<script setup lang="ts">
import { useColumnCreateStoreOrThrow } from '#imports'
import { dateFormats } from '~/utils'
const { formState } = $(useColumnCreateStoreOrThrow())
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
if (!formState.meta?.date_format) {
if (!formState.meta) formState.meta = {}
formState.meta.date_format = dateFormats[0]
if (!vModel.value.meta?.date_format) {
if (!vModel.value.meta) vModel.value.meta = {}
vModel.value.meta.date_format = dateFormats[0]
}
</script>
<template>
<a-form-item label="Date Format">
<a-select v-model:value="formState.meta.date_format" size="small">
<a-select v-model:value="vModel.meta.date_format">
<a-select-option v-for="(format, i) of dateFormats" :key="i" :value="format">
<div class="flex flex-row items-center">
<div class="text-xs">

15
packages/nc-gui-v2/components/smartsheet-column/DurationOptions.vue

@ -1,8 +1,13 @@
<script setup lang="ts">
import { useColumnCreateStoreOrThrow } from '#imports'
import { durationOptions } from '@/utils'
const { formState } = useColumnCreateStoreOrThrow()
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const durationOptionList =
durationOptions.map((o) => ({
@ -12,9 +17,9 @@ const durationOptionList =
})) || []
// set default value
formState.value.meta = {
vModel.value.meta = {
duration: 0,
...formState.value.meta,
...vModel.value.meta,
}
</script>
@ -25,7 +30,7 @@ formState.value.meta = {
</a-col>
<a-col :span="24">
<a-form-item label="Duration Format">
<a-select v-model:value="formState.meta.duration" size="small" class="w-52">
<a-select v-model:value="vModel.meta.duration" class="w-52">
<a-select-option v-for="(duration, i) of durationOptionList" :key="i" :value="duration.id">
{{ duration.title }}
</a-select-option>

180
packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue

@ -1,28 +1,26 @@
<script lang="ts" setup>
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { computed, inject, useColumnCreateStoreOrThrow, useMetas, watchEffect } from '#imports'
import { computed, inject, useMetas, watchEffect } from '#imports'
import { MetaInj, ReloadViewDataHookInj } from '~/context'
import { uiTypes } from '~/utils/columnUtils'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier'
interface Props {
editColumnDropdown?: boolean
}
const { editColumnDropdown } = defineProps<Props>()
const emit = defineEmits(['submit', 'cancel'])
const emit = defineEmits(['cancel', 'submit'])
const meta = inject(MetaInj)
const { formState, generateNewColumnMeta, addOrUpdate, onAlter, onUidtOrIdTypeChange, validateInfos, isEdit } =
useColumnCreateStoreOrThrow()
const reloadDataTrigger = inject(ReloadViewDataHookInj)
const advancedOptions = ref(false)
const { getMeta } = useMetas()
const formulaOptionsRef = ref()
const { formState, validateInfos, onUidtOrIdTypeChange, onAlter, addOrUpdate, generateNewColumnMeta, isEdit } =
useColumnCreateStoreOrThrow()
const editOrAddRef = ref<HTMLElement>()
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
@ -44,18 +42,8 @@ const uiTypesOptions = computed<typeof uiTypes>(() => {
})
const reloadMetaAndData = async () => {
emit('cancel')
await getMeta(meta?.value.id as string, true)
reloadDataTrigger?.trigger()
emit('submit')
}
function onCancel() {
emit('cancel')
if (formState.value.uidt === UITypes.Formula) {
// close formula drawer
formulaOptionsRef.value.formulaSuggestionDrawer = false
}
}
async function onSubmit() {
@ -65,97 +53,113 @@ async function onSubmit() {
setTimeout(() => {
advancedOptions.value = false
}, 500)
emit('submit')
}
// create column meta if it's a new column
watchEffect(() => {
if (!isEdit.value && formState.value.altered !== 1) {
generateNewColumnMeta()
}
})
// focus and select the column name field
const antInput = ref()
watchEffect(() => {
if (antInput.value && formState.value) {
// todo: replace setTimeout
setTimeout(() => {
antInput.value.focus()
antInput.value.select()
antInput.value?.focus()
antInput.value?.select()
}, 300)
}
advancedOptions.value = false
})
watch(
() => editColumnDropdown,
(v) => {
if (v) {
if (formState.value.uidt === UITypes.Formula) {
formulaOptionsRef.value.formulaSuggestionDrawer = true
}
}
},
)
// for cases like formula
if (!formState.value?.column_name) {
formState.value.column_name = formState.value?.title
onMounted(() => {
if (isEdit.value === false) {
generateNewColumnMeta()
}
if (formState.value.uidt === UITypes.Formula) {
formulaOptionsRef.value.formulaSuggestionDrawer = true
}
// for cases like formula
if (formState.value && !formState.value.column_name) {
formState.value.column_name = formState.value?.title
}
})
onUnmounted(() => {
if (formState.value.uidt === UITypes.Formula) {
// close formula drawer
formulaOptionsRef.value.formulaSuggestionDrawer = false
}
})
const handleClose = (e: MouseEvent) => {
if (
e.target &&
editOrAddRef?.value &&
!editOrAddRef.value.contains(e.target) &&
!e.target.closest('.ant-dropdown') &&
!e.target.closest('.ant-select') &&
!e.target.closest('.ant-select-item')
) {
emit('cancel')
}
}
useEventListener(document, 'click', handleClose)
</script>
<template>
<div class="min-w-[350px] w-max max-h-[95vh] bg-white shadow p-4 overflow-auto" @click.stop>
<a-form v-model="formState" name="column-create-or-edit" layout="vertical">
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title">
<a-input ref="antInput" v-model:value="formState.title" size="small" class="nc-column-name-input" @input="onAlter(8)" />
</a-form-item>
<a-form-item
v-if="!(editColumnDropdown && !!onlyNameUpdateOnEditColumns.find((col) => col === formState.uidt))"
:label="$t('labels.columnType')"
>
<a-select
v-model:value="formState.uidt"
show-search
size="small"
class="nc-column-type-input"
@change="onUidtOrIdTypeChange"
<div ref="editOrAddRef" class="min-w-[400px] max-h-[95vh] bg-white shadow p-6 overflow-auto" @click.stop>
<a-form v-if="formState" v-model="formState" name="column-create-or-edit" layout="vertical">
<div class="flex flex-col gap-2">
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title">
<a-input ref="antInput" v-model:value="formState.title" class="nc-column-name-input" @input="onAlter(8)" />
</a-form-item>
<a-form-item
v-if="!(isEdit && !!onlyNameUpdateOnEditColumns.find((col) => col === formState.uidt))"
:label="$t('labels.columnType')"
>
<a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">
<div class="flex gap-1 align-center text-xs">
<component :is="opt.icon" class="text-grey" />
{{ opt.name }}
</div>
</a-select-option>
</a-select>
</a-form-item>
<SmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" ref="formulaOptionsRef" />
<SmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" />
<SmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" />
<SmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" />
<SmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" />
<SmartsheetColumnLookupOptions v-if="!editColumnDropdown && formState.uidt === UITypes.Lookup" />
<SmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" />
<SmartsheetColumnRollupOptions v-if="!editColumnDropdown && formState.uidt === UITypes.Rollup" />
<SmartsheetColumnLinkedToAnotherRecordOptions
v-if="!editColumnDropdown && formState.uidt === UITypes.LinkToAnotherRecord"
/>
<SmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<SmartsheetColumnPercentOptions v-if="formState.uidt === UITypes.Percent" />
<SmartsheetColumnSelectOptions v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect" />
<a-select v-model:value="formState.uidt" show-search class="nc-column-type-input" @change="onUidtOrIdTypeChange">
<a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">
<div class="flex gap-1 align-center">
<component :is="opt.icon" class="text-grey" />
{{ opt.name }}
</div>
</a-select-option>
</a-select>
</a-form-item>
<SmartsheetColumnFormulaOptions
v-if="formState.uidt === UITypes.Formula"
ref="formulaOptionsRef"
v-model:value="formState"
/>
<SmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<SmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<SmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<SmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
<SmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" />
<SmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<SmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" />
<SmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord"
v-model:value="formState"
/>
<SmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<SmartsheetColumnPercentOptions v-if="formState.uidt === UITypes.Percent" v-model:value="formState" />
<SmartsheetColumnSelectOptions
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"
v-model:value="formState"
/>
</div>
<div
v-if="!isVirtualCol(formState.uidt)"
class="text-xs cursor-pointer text-grey nc-more-options my-2 flex align-center gap-1 justify-end"
class="text-xs cursor-pointer text-grey nc-more-options mb-1 mt-4 flex align-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions"
>
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div>
<div class="overflow-hidden" :class="advancedOptions ? 'h-min' : 'h-0'">
<div class="overflow-hidden" :class="advancedOptions ? 'h-min mb-2' : 'h-0'">
<a-checkbox
v-if="formState.meta && columnToValidate.includes(formState.uidt)"
v-model:checked="formState.meta.validate"
@ -165,15 +169,15 @@ if (!formState.value?.column_name) {
{{ `Accept only valid ${formState.uidt}` }}
</span>
</a-checkbox>
<SmartsheetColumnAdvancedOptions />
<SmartsheetColumnAdvancedOptions v-model:value="formState" />
</div>
<a-form-item>
<div class="flex justify-end gap-1 mt-4">
<a-button html-type="button" size="small" @click="onCancel">
<a-button html-type="button" @click="emit('cancel')">
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button html-type="submit" type="primary" size="small" @click="onSubmit">
<a-button html-type="submit" type="primary" @click.prevent="onSubmit">
<!-- Save -->
{{ $t('general.save') }}
</a-button>

27
packages/nc-gui-v2/components/smartsheet-column/EditOrAddProvider.vue

@ -0,0 +1,27 @@
<script lang="ts" setup>
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { inject } from '#imports'
import { MetaInj } from '~/context'
interface Props {
column?: Ref<ColumnType & { meta: any }>
}
const props = defineProps<Props>()
const emit = defineEmits(['submit', 'cancel'])
const meta = inject(MetaInj)
if (props?.column) {
const column = toRef(props, 'column')
useProvideColumnCreateStore(meta as Ref<TableType>, column)
} else {
useProvideColumnCreateStore(meta as Ref<TableType>)
}
</script>
<template>
<SmartsheetColumnEditOrAdd @submit="emit('submit')" @cancel="emit('cancel')"></SmartsheetColumnEditOrAdd>
</template>

24
packages/nc-gui-v2/components/smartsheet-column/FormulaOptions.vue

@ -4,7 +4,7 @@ import type { ListItem as AntListItem } from 'ant-design-vue'
import jsep from 'jsep'
import type { ColumnType } from 'nocodb-sdk'
import { UITypes, jsepCurlyHook } from 'nocodb-sdk'
import { onMounted, useColumnCreateStoreOrThrow, useDebounceFn } from '#imports'
import { onMounted, useDebounceFn } from '#imports'
import { MetaInj } from '~/context'
import {
NcAutocompleteTree,
@ -17,6 +17,16 @@ import {
validateDateWithUnknownFormat,
} from '@/utils'
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, sqlUi, column } = useColumnCreateStoreOrThrow()
enum JSEPNode {
COMPOUND = 'Compound',
IDENTIFIER = 'Identifier',
@ -29,8 +39,6 @@ enum JSEPNode {
ARRAY_EXP = 'ArrayExpression',
}
const { formState, validateInfos, setAdditionalValidations, sqlUi, column } = useColumnCreateStoreOrThrow()
const meta = inject(MetaInj)
const columns = computed(() => meta?.value?.columns || [])
@ -512,11 +520,11 @@ function appendText(item: Record<string, any>) {
const len = wordToComplete.value?.length || 0
if (item.type === 'function') {
formState.value.formula_raw = insertAtCursor(formulaRef.value.$el, text, len, 1)
vModel.value.formula_raw = insertAtCursor(formulaRef.value.$el, text, len, 1)
} else if (item.type === 'column') {
formState.value.formula_raw = insertAtCursor(formulaRef.value.$el, `{${text}}`, len + +!isCurlyBracketBalanced())
vModel.value.formula_raw = insertAtCursor(formulaRef.value.$el, `{${text}}`, len + +!isCurlyBracketBalanced())
} else {
formState.value.formula_raw = insertAtCursor(formulaRef.value.$el, text, len)
vModel.value.formula_raw = insertAtCursor(formulaRef.value.$el, text, len)
}
autocomplete.value = false
wordToComplete.value = ''
@ -582,7 +590,7 @@ function scrollToSelectedOption() {
}
// set default value
formState.value.formula_raw = (column?.value?.colOptions as Record<string, any>)?.formula_raw || ''
vModel.value.formula_raw = (column?.value?.colOptions as Record<string, any>)?.formula_raw || ''
// set additional validations
setAdditionalValidations({
@ -603,7 +611,7 @@ onMounted(() => {
<a-form-item v-bind="validateInfos.formula_raw" label="Formula">
<a-textarea
ref="formulaRef"
v-model:value="formState.formula_raw"
v-model:value="vModel.formula_raw"
class="mb-2"
@keydown.down.prevent="suggestionListDown"
@keydown.up.prevent="suggestionListUp"

64
packages/nc-gui-v2/components/smartsheet-column/LinkedToAnotherRecordOptions.vue

@ -1,32 +1,42 @@
<script setup lang="ts">
import { ModelTypes, MssqlUi, SqliteUi } from 'nocodb-sdk'
import { inject, useColumnCreateStoreOrThrow, useProject } from '#imports'
import { inject, useProject } from '#imports'
import { MetaInj } from '~/context'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
const { formState, validateInfos, onDataTypeChange, setAdditionalValidations } = $(useColumnCreateStoreOrThrow())
const { tables, sqlUi } = $(useProject())
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj)!)
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
const { tables, sqlUi } = $(useProject())
setAdditionalValidations({
childId: [{ required: true, message: 'Required' }],
})
const onUpdateDeleteOptions = sqlUi === MssqlUi ? ['NO ACTION'] : ['NO ACTION', 'CASCADE', 'RESTRICT', 'SET NULL', 'SET DEFAULT']
if (!formState.parentId) formState.parentId = meta.id
if (!formState.childId) formState.childId = null
if (!formState.childColumn) formState.childColumn = `${meta.table_name}_id`
if (!formState.childTable) formState.childTable = meta.table_name
if (!formState.parentTable) formState.parentTable = formState.rtn || ''
if (!formState.parentColumn) formState.parentColumn = formState.rcn || ''
if (!vModel.value.parentId) vModel.value.parentId = meta.id
if (!vModel.value.childId) vModel.value.childId = null
if (!vModel.value.childColumn) vModel.value.childColumn = `${meta.table_name}_id`
if (!vModel.value.childTable) vModel.value.childTable = meta.table_name
if (!vModel.value.parentTable) vModel.value.parentTable = vModel.value.rtn || ''
if (!vModel.value.parentColumn) vModel.value.parentColumn = vModel.value.rcn || ''
if (!formState.type) formState.type = 'hm'
if (!formState.onUpdate) formState.onUpdate = onUpdateDeleteOptions[0]
if (!formState.onDelete) formState.onDelete = onUpdateDeleteOptions[0]
if (!formState.virtual) formState.virtual = sqlUi === SqliteUi
if (!formState.alias) formState.alias = formState.column_name
if (!vModel.value.type) vModel.value.type = 'hm'
if (!vModel.value.onUpdate) vModel.value.onUpdate = onUpdateDeleteOptions[0]
if (!vModel.value.onDelete) vModel.value.onDelete = onUpdateDeleteOptions[0]
if (!vModel.value.virtual) vModel.value.virtual = sqlUi === SqliteUi
if (!vModel.value.alias) vModel.value.alias = vModel.value.column_name
const advancedOptions = $(ref(false))
@ -41,15 +51,15 @@ const refTables = $computed(() => {
<template>
<div class="w-full flex flex-col mb-2 mt-4">
<div class="border-2 p-4">
<div class="border-2 p-6">
<a-form-item v-bind="validateInfos.type">
<a-radio-group v-model:value="formState.type" name="type" v-bind="validateInfos.type">
<a-radio-group v-model:value="vModel.type" name="type" v-bind="validateInfos.type">
<a-radio value="hm">Has Many</a-radio>
<a-radio value="mm">Many To Many</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item class="flex w-full pb-2 mt-4" :label="$t('labels.childTable')" v-bind="validateInfos.childId">
<a-select v-model:value="formState.childId" size="small" @change="onDataTypeChange">
<a-select v-model:value="vModel.childId" @change="onDataTypeChange">
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.id">
{{ table.title }}
</a-select-option>
@ -65,29 +75,17 @@ const refTables = $computed(() => {
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div>
<div v-if="advancedOptions" class="flex flex-col p-4 border-2 mt-2">
<div v-if="advancedOptions" class="flex flex-col p-6 gap-4 border-2 mt-2">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')">
<a-select
v-model:value="formState.onUpdate"
:disabled="formState.virtual"
name="onUpdate"
size="small"
@change="onDataTypeChange"
>
<a-select v-model:value="vModel.onUpdate" :disabled="vModel.virtual" name="onUpdate" @change="onDataTypeChange">
<a-select-option v-for="(option, index) in onUpdateDeleteOptions" :key="index" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.onDelete')">
<a-select
v-model:value="formState.onDelete"
:disabled="formState.virtual"
name="onDelete"
size="small"
@change="onDataTypeChange"
>
<a-select v-model:value="vModel.onDelete" :disabled="vModel.virtual" name="onDelete" @change="onDataTypeChange">
<a-select-option v-for="(option, index) in onUpdateDeleteOptions" :key="index" :value="option">
{{ option }}
</a-select-option>
@ -96,7 +94,7 @@ const refTables = $computed(() => {
</div>
<div class="flex flex-row">
<a-form-item>
<a-checkbox v-model:checked="formState.virtual" name="virtual" @change="onDataTypeChange">Virtual Relation</a-checkbox>
<a-checkbox v-model:checked="vModel.virtual" name="virtual" @change="onDataTypeChange">Virtual Relation</a-checkbox>
</a-form-item>
</div>
</div>

38
packages/nc-gui-v2/components/smartsheet-column/LookupOptions.vue

@ -1,12 +1,22 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { useColumnCreateStoreOrThrow } from '#imports'
import { MetaInj } from '~/context'
const { formState, validateInfos, onDataTypeChange, setAdditionalValidations } = $(useColumnCreateStoreOrThrow())
const { tables } = $(useProject())
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj)!)
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
const { tables } = $(useProject())
const { metas } = $(useMetas())
setAdditionalValidations({
@ -14,8 +24,8 @@ setAdditionalValidations({
fk_lookup_column_id: [{ required: true, message: 'Required' }],
})
if (!formState.fk_relation_column_id) formState.fk_relation_column_id = null
if (!formState.fk_lookup_column_id) formState.fk_lookup_column_id = null
if (!vModel.value.fk_relation_column_id) vModel.value.fk_relation_column_id = null
if (!vModel.value.fk_lookup_column_id) vModel.value.fk_lookup_column_id = null
const relationNames = {
mm: 'Many To Many',
@ -42,7 +52,7 @@ const refTables = $computed(() => {
})
const columns = $computed(() => {
const selectedTable = refTables.find((t) => t.column.id === formState.fk_relation_column_id)
const selectedTable = refTables.find((t) => t.column.id === vModel.value.fk_relation_column_id)
if (!selectedTable?.id) {
return []
}
@ -52,15 +62,10 @@ const columns = $computed(() => {
</script>
<template>
<div class="p-4 w-full flex flex-col border-2 mb-2 mt-4">
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.childTable')" v-bind="validateInfos.fk_relation_column_id">
<a-select
v-model:value="formState.fk_relation_column_id"
size="small"
dropdown-class-name="!w-64"
@change="onDataTypeChange"
>
<a-select v-model:value="vModel.fk_relation_column_id" dropdown-class-name="!w-64" @change="onDataTypeChange">
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
@ -72,12 +77,7 @@ const columns = $computed(() => {
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_lookup_column_id">
<a-select
v-model:value="formState.fk_lookup_column_id"
name="fk_lookup_column_id"
size="small"
@change="onDataTypeChange"
>
<a-select v-model:value="vModel.fk_lookup_column_id" name="fk_lookup_column_id" @change="onDataTypeChange">
<a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
{{ column.title }}
</a-select-option>

25
packages/nc-gui-v2/components/smartsheet-column/PercentOptions.vue

@ -1,20 +1,25 @@
<script setup lang="ts">
import { useColumnCreateStoreOrThrow } from '#imports'
import { precisions } from '@/utils/percentUtils'
const { formState } = $(useColumnCreateStoreOrThrow())
interface Props {
value: Record<string, any>
}
if (!formState.meta) formState.meta = {}
if (!formState.meta?.precision) formState.meta.precision = precisions[0].id
if (!formState.meta?.negative) formState.meta.negative = false
if (!formState.meta?.default) formState.meta.default = null
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
if (!vModel.value.meta) vModel.value.meta = {}
if (!vModel.value.meta?.negative) vModel.value.meta.negative = false
if (!vModel.value.meta?.default) vModel.value.meta.default = null
if (!vModel.value.meta?.precision) vModel.value.meta.precision = precisions[0].id
</script>
<template>
<div class="flex flex-col mt-2">
<div class="flex flex-col mt-2 gap-2">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" label="Precision">
<a-select v-model:value="formState.meta.precision" size="small">
<a-select v-model:value="vModel.meta.precision">
<a-select-option v-for="(precision, i) of precisions" :key="i" :value="precision.id">
<div class="flex flex-row items-center">
<div class="text-xs">
@ -25,13 +30,13 @@ if (!formState.meta?.default) formState.meta.default = null
</a-select>
</a-form-item>
<a-form-item label="Default Number (%)">
<a-input v-model:value="formState.meta.default" size="small" name="default" type="number" />
<a-input v-model:value="vModel.meta.default" name="default" type="number" />
</a-form-item>
</div>
<div class="flex flex-row mt-2">
<a-form-item>
<div class="flex flex-row space-x-2 items-center">
<a-switch v-model:checked="formState.meta.negative" name="negative" />
<a-switch v-model:checked="vModel.meta.negative" name="negative" />
<div class="text-xs">Allow negative numbers</div>
</div>
</a-form-item>

59
packages/nc-gui-v2/components/smartsheet-column/RatingOptions.vue

@ -1,8 +1,13 @@
<script setup lang="ts">
import { useColumnCreateStoreOrThrow } from '#imports'
import { getMdiIcon } from '@/utils'
const { formState } = useColumnCreateStoreOrThrow()
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
// cater existing v1 cases
const iconList = [
@ -29,14 +34,14 @@ const iconList = [
]
const picked = computed({
get: () => formState.value.meta.color,
get: () => vModel.value.meta.color,
set: (val) => {
formState.value.meta.color = val
vModel.value.meta.color = val
},
})
// set default value
formState.value.meta = {
vModel.value.meta = {
iconIdx: 0,
icon: {
full: 'mdi-star',
@ -44,51 +49,53 @@ formState.value.meta = {
},
color: '#fcb401',
max: 5,
...formState.value.meta,
...vModel.value.meta,
}
// antdv doesn't support object as value
// use iconIdx as value and update back in watch
const iconIdx = iconList.findIndex(
(ele) => ele.full === formState.value.meta.icon.full && ele.empty === formState.value.meta.icon.empty,
(ele) => ele.full === vModel.value.meta.icon.full && ele.empty === vModel.value.meta.icon.empty,
)
formState.value.meta.iconIdx = iconIdx === -1 ? 0 : iconIdx
vModel.value.meta.iconIdx = iconIdx === -1 ? 0 : iconIdx
watch(
() => formState.value.meta.iconIdx,
() => vModel.value.meta.iconIdx,
(v) => {
formState.value.meta.icon = iconList[v]
vModel.value.meta.icon = iconList[v]
},
)
</script>
<template>
<a-row>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="Icon">
<a-select v-model:value="formState.meta.iconIdx" size="small" class="w-52">
<a-select v-model:value="vModel.meta.iconIdx" class="w-52">
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i">
<component
:is="getMdiIcon(icon.full)"
class="mx-1"
:style="{
color: formState.meta.color,
}"
/>
<component
:is="getMdiIcon(icon.empty)"
:style="{
color: formState.meta.color,
}"
/>
<div class="flex items-center">
<component
:is="getMdiIcon(icon.full)"
class="mx-1"
:style="{
color: vModel.meta.color,
}"
/>
<component
:is="getMdiIcon(icon.empty)"
:style="{
color: vModel.meta.color,
}"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="Max">
<a-select v-model:value="formState.meta.max" class="w-52" size="small">
<a-select v-model:value="vModel.meta.max" class="w-52">
<a-select-option v-for="(v, i) in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" :key="i" :value="v">
{{ v }}
</a-select-option>

41
packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue

@ -1,13 +1,22 @@
<script setup lang="ts">
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { inject, useColumnCreateStoreOrThrow, useMetas, useProject } from '#imports'
import { inject, useMetas, useProject } from '#imports'
import { MetaInj } from '~/context'
const { formState, validateInfos, onDataTypeChange, setAdditionalValidations } = $(useColumnCreateStoreOrThrow())
const { tables } = $(useProject())
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj)!)
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
const { tables } = $(useProject())
const { metas } = $(useMetas())
setAdditionalValidations({
@ -33,9 +42,9 @@ const aggrFunctionsList = [
{ text: 'avgDistinct', value: 'avgDistinct' },
]
if (!formState.fk_relation_column_id) formState.fk_relation_column_id = null
if (!formState.fk_rollup_column_id) formState.fk_rollup_column_id = null
if (!formState.rollup_function) formState.rollup_function = null
if (!vModel.value.fk_relation_column_id) vModel.value.fk_relation_column_id = null
if (!vModel.value.fk_rollup_column_id) vModel.value.value.fk_rollup_column_id = null
if (!vModel.value.rollup_function) vModel.value.rollup_function = null
const refTables = $computed(() => {
if (!tables || !tables.length) {
@ -54,7 +63,7 @@ const refTables = $computed(() => {
})
const columns = $computed(() => {
const selectedTable = refTables.find((t) => t.column.id === formState.fk_relation_column_id)
const selectedTable = refTables.find((t) => t.column.id === vModel.value.fk_relation_column_id)
if (!selectedTable?.id) {
return []
@ -65,15 +74,10 @@ const columns = $computed(() => {
</script>
<template>
<div class="p-4 w-full flex flex-col border-2 mb-2 mt-4">
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.childTable')" v-bind="validateInfos.fk_relation_column_id">
<a-select
v-model:value="formState.fk_relation_column_id"
size="small"
dropdown-class-name="!w-64"
@change="onDataTypeChange"
>
<a-select v-model:value="vModel.fk_relation_column_id" dropdown-class-name="!w-64" @change="onDataTypeChange">
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
@ -85,12 +89,7 @@ const columns = $computed(() => {
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_rollup_column_id">
<a-select
v-model:value="formState.fk_rollup_column_id"
name="fk_rollup_column_id"
size="small"
@change="onDataTypeChange"
>
<a-select v-model:value="vModel.fk_rollup_column_id" name="fk_rollup_column_id" @change="onDataTypeChange">
<a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
{{ column.title }}
</a-select-option>
@ -98,7 +97,7 @@ const columns = $computed(() => {
</a-form-item>
</div>
<a-form-item label="Aggregate function" v-bind="validateInfos.rollup_function">
<a-select v-model:value="formState.rollup_function" size="small" @change="onDataTypeChange">
<a-select v-model:value="vModel.rollup_function" @change="onDataTypeChange">
<a-select-option v-for="(func, index) of aggrFunctionsList" :key="index" :value="func.value">
{{ func.text }}
</a-select-option>

19
packages/nc-gui-v2/components/smartsheet-column/SelectOptions.vue

@ -1,14 +1,21 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import { UITypes } from 'nocodb-sdk'
import { useColumnCreateStoreOrThrow } from '#imports'
import { enumColor } from '@/utils'
import MdiDragIcon from '~icons/mdi/drag-vertical'
import MdiArrowDownDropCircle from '~icons/mdi/arrow-down-drop-circle'
import MdiClose from '~icons/mdi/close'
import MdiPlusIcon from '~icons/mdi/plus'
const { formState, setAdditionalValidations } = useColumnCreateStoreOrThrow()
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations } = useColumnCreateStoreOrThrow()
let options = $ref<any[]>([])
const colorMenus = $ref<any>({})
@ -24,7 +31,7 @@ const validators = {
if (!opt.title.length) {
return reject(new Error("Select options can't be null"))
}
if (formState.value.uidt === UITypes.MultiSelect && opt.title.includes(',')) {
if (vModel.value.uidt === UITypes.MultiSelect && opt.title.includes(',')) {
return reject(new Error("MultiSelect columns can't have commas(',')"))
}
if (options.filter((el) => el.title === opt.title).length !== 1) {
@ -64,12 +71,12 @@ const removeOption = (index: number) => {
}
onMounted(() => {
if (!formState.value.colOptions?.options) {
formState.value.colOptions = {
if (!vModel.value.colOptions?.options) {
vModel.value.colOptions = {
options: [],
}
}
options = formState.value.colOptions.options
options = vModel.value.colOptions.options
// Support for older options
for (const op of options.filter((el) => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')

2
packages/nc-gui-v2/components/smartsheet-column/SpecificDBTypeOptions.vue

@ -1,3 +1,3 @@
<template>
<SmartsheetColumnAdvancedOptions class="mt-4" />
<div class="mt-4 mb-2" />
</template>

41
packages/nc-gui-v2/components/smartsheet-header/Cell.vue

@ -1,23 +1,28 @@
<script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { ColumnType } from 'nocodb-sdk'
import { inject, toRef } from 'vue'
import { ColumnInj, IsFormInj, MetaInj } from '~/context'
import { useProvideColumnCreateStore } from '#imports'
import { ColumnInj, IsFormInj } from '~/context'
const props = defineProps<{ column: ColumnType & { meta: any }; required?: boolean; hideMenu?: boolean }>()
const hideMenu = toRef(props, 'hideMenu')
const meta = inject(MetaInj)
const isForm = inject(IsFormInj, ref(false))
const column = toRef(props, 'column')
const { isUIAllowed } = useUIPermission()
provide(ColumnInj, column)
// instantiate column update store
useProvideColumnCreateStore(meta as Ref<TableType>, column)
const editColumnDropdown = ref(false)
function onVisibleChange() {
// only allow to close the EditOrAdd component
// by clicking cancel button
editColumnDropdown.value = true
}
</script>
<template>
@ -28,8 +33,28 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<template v-if="!hideMenu">
<div class="flex-1" />
<SmartsheetHeaderMenu v-if="!isForm" />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" @edit="editColumnDropdown = true" />
</template>
<a-dropdown
v-model:visible="editColumnDropdown"
:trigger="['click']"
placement="bottomRight"
@visible-change="onVisibleChange"
>
<div />
<template #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="editColumnDropdown"
:column="column"
class="w-full"
@submit="editColumnDropdown = false"
@cancel="editColumnDropdown = false"
@click.stop
@keydown.stop
/>
</template>
</a-dropdown>
</div>
</template>

38
packages/nc-gui-v2/components/smartsheet-header/CellIcon.vue

@ -36,47 +36,47 @@ const additionalColMeta = useColumn(column as Ref<ColumnType>)
const icon = computed(() => {
if (column?.value?.pk) {
return KeyIcon
} else if (additionalColMeta.isJSON) {
} else if (additionalColMeta.isJSON.value) {
return JSONIcon
} else if (additionalColMeta.isDate) {
} else if (additionalColMeta.isDate.value) {
return CalendarIcon
} else if (additionalColMeta.isDateTime) {
} else if (additionalColMeta.isDateTime.value) {
return DatetimeIcon
} else if (additionalColMeta.isSet) {
} else if (additionalColMeta.isSet.value) {
return MultiSelectIcon
} else if (additionalColMeta.isSingleSelect) {
} else if (additionalColMeta.isSingleSelect.value) {
return SingleSelectIcon
} else if (additionalColMeta.isBoolean) {
} else if (additionalColMeta.isBoolean.value) {
return BooleanIcon
} else if (additionalColMeta.isTextArea) {
} else if (additionalColMeta.isTextArea.value) {
return TextAreaIcon
} else if (additionalColMeta.isEmail) {
} else if (additionalColMeta.isEmail.value) {
return EmailIcon
} else if (additionalColMeta.isYear) {
} else if (additionalColMeta.isYear.value) {
return CalendarIcon
} else if (additionalColMeta.isTime) {
} else if (additionalColMeta.isTime.value) {
return ClockIcon
} else if (additionalColMeta.isRating) {
} else if (additionalColMeta.isRating.value) {
return RatingIcon
} else if (additionalColMeta.isAttachment) {
} else if (additionalColMeta.isAttachment.value) {
return AttachmentIcon
} else if (additionalColMeta.isDecimal) {
} else if (additionalColMeta.isDecimal.value) {
return DecimalIcon
} else if (additionalColMeta.isPhoneNumber) {
} else if (additionalColMeta.isPhoneNumber.value) {
return FilePhoneIcon
}
// else if(additionalColMeta.isForeignKey) {
// return FKIcon
// }
else if (additionalColMeta.isURL) {
else if (additionalColMeta.isURL.value) {
return WebIcon
} else if (additionalColMeta.isCurrency) {
} else if (additionalColMeta.isCurrency.value) {
return CurrencyIcon
} else if (additionalColMeta.isPercent) {
} else if (additionalColMeta.isPercent.value) {
return PercentIcon
} else if (additionalColMeta.isInt || additionalColMeta.isFloat) {
} else if (additionalColMeta.isInt.value || additionalColMeta.isFloat.value) {
return NumericIcon
} else if (additionalColMeta.isString) {
} else if (additionalColMeta.isString.value) {
return StringIcon
} else {
return GenericIcon

31
packages/nc-gui-v2/components/smartsheet-header/Menu.vue

@ -12,14 +12,13 @@ import MdiDeleteIcon from '~icons/mdi/delete-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
const { virtual = false } = defineProps<{ virtual?: boolean }>()
const editColumnDropdown = ref(false)
const emit = defineEmits(['edit'])
const column = inject(ColumnInj)
const meta = inject(MetaInj)
const { $api } = useNuxtApp()
const { $api, $e } = useNuxtApp()
const { t } = useI18n()
@ -46,43 +45,27 @@ const setAsPrimaryValue = async () => {
await $api.dbTableColumn.primaryColumnSet(column?.value?.id as string)
getMeta(meta?.value?.id as string, true)
message.success('Successfully updated as primary column')
$e('a:column:set-primary')
} catch (e) {
message.error('Failed to update primary column')
}
}
function onVisibleChange() {
// only allow to close the EditOrAdd component
// by clicking cancel button
editColumnDropdown.value = true
}
</script>
<template>
<a-dropdown v-model:visible="editColumnDropdown" :trigger="['click']" @visible-change="onVisibleChange">
<span />
<template #overlay>
<SmartsheetColumnEditOrAdd
:edit-column-dropdown="editColumnDropdown"
@click.stop
@keydown.stop
@cancel="editColumnDropdown = false"
/>
</template>
</a-dropdown>
<a-dropdown :trigger="['hover']">
<a-dropdown placement="bottomRight" :trigger="['hover']">
<MdiMenuDownIcon class="text-grey nc-ui-dt-dropdown" />
<template #overlay>
<a-menu class="shadow bg-white">
<a-menu-item @click="editColumnDropdown = true">
<a-menu-item @click="emit('edit')">
<div class="nc-column-edit nc-header-menu-item">
<MdiEditIcon class="text-primary" />
<!-- Edit -->
{{ $t('general.edit') }}
</div>
</a-menu-item>
<a-menu-item v-if="!virtual" v-t="['a:column:set-primary']" @click="setAsPrimaryValue">
<div class="nc-column-edit nc-header-menu-item">
<a-menu-item v-if="!virtual" @click="setAsPrimaryValue">
<div class="nc-column-set-primary nc-header-menu-item">
<MdiStarIcon class="text-primary" />
<!-- todo : tooltip -->

42
packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue

@ -3,23 +3,32 @@ import { substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { ColumnInj, IsFormInj, MetaInj } from '~/context'
import { provide, toRef, useMetas, useProvideColumnCreateStore } from '#imports'
import { provide, toRef, useMetas } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean; required?: boolean }>()
const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean; required: boolean }>()
const column = toRef(props, 'column')
const hideMenu = toRef(props, 'hideMenu')
const editColumnDropdown = ref(false)
provide(ColumnInj, column)
const { metas } = useMetas()
const { isUIAllowed } = useUIPermission()
const meta = inject(MetaInj)
const isForm = inject(IsFormInj, ref(false))
const { isLookup, isBt, isRollup, isMm, isHm, isFormula } = useVirtualCell(column)
const colOptions = $computed(() => column.value?.colOptions)
const tableTile = $computed(() => meta?.value?.title)
const relationColumnOptions = $computed<LinkToAnotherRecordType | null>(() => {
if (isMm.value || isHm.value || isBt.value) {
return column.value?.colOptions as LinkToAnotherRecordType
@ -76,7 +85,11 @@ const tooltipMsg = computed(() => {
return ''
})
useProvideColumnCreateStore(meta as Ref<TableType>, column)
function onVisibleChange() {
// only allow to close the EditOrAdd component
// by clicking cancel button
editColumnDropdown.value = true
}
</script>
<template>
@ -100,9 +113,28 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<!-- </v-tooltip> -->
<template v-if="!hideMenu">
<v-spacer />
<SmartsheetHeaderMenu v-if="!isForm" :virtual="true" />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" />
</template>
<a-dropdown
v-model:visible="editColumnDropdown"
:trigger="['click']"
placement="bottomRight"
@visible-change="onVisibleChange"
>
<div />
<template #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="editColumnDropdown"
:column="column"
class="w-full"
@submit="editColumnDropdown = false"
@cancel="editColumnDropdown = false"
@click.stop
@keydown.stop
/>
</template>
</a-dropdown>
</div>
</template>

6
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue

@ -112,7 +112,7 @@ defineExpose({
<template>
<div
class="p-6 menu-filter-dropdown bg-gray-50"
class="p-6 menu-filter-dropdown bg-gray-50 !border"
:class="{ 'shadow-xl min-w-[430px] max-w-[630px] max-h-[max(80vh,500px)] overflow-auto': !nested, 'border-1 w-full': nested }"
>
<div v-if="filters && filters.length" class="nc-filter-grid mb-2" @click.stop>
@ -248,7 +248,7 @@ defineExpose({
</div>
<div class="flex gap-2 mb-2 mt-4">
<a-button class="elevation-0 text-capitalize" @click.stop="addFilter">
<a-button class="elevation-0 text-capitalize" type="primary" ghost @click.stop="addFilter">
<div class="flex align-center gap-1">
<!-- <v-icon small color="grey"> mdi-plus </v-icon> -->
<MdiAddIcon />
@ -272,7 +272,7 @@ defineExpose({
<style scoped>
.nc-filter-grid {
display: grid;
grid-template-columns: 18px 70px auto auto auto;
grid-template-columns: 18px 75px auto auto auto;
@apply gap-[12px]
align-items: center;
}

9
packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue

@ -79,7 +79,10 @@ const onMove = (event: { moved: { newIndex: number } }) => {
</a-button>
</div>
<template #overlay>
<div class="p-3 min-w-[280px] bg-gray-50 shadow-lg nc-table-toolbar-menu max-h-[max(80vh,500px)] overflow-auto" @click.stop>
<div
class="p-3 min-w-[280px] bg-gray-50 shadow-lg nc-table-toolbar-menu max-h-[max(80vh,500px)] overflow-auto !border"
@click.stop
>
<div class="p-1" @click.stop>
<a-input v-model:value="filterQuery" size="small" :placeholder="$t('placeholder.searchFields')" />
</div>
@ -104,11 +107,11 @@ const onMove = (event: { moved: { newIndex: number } }) => {
</a-checkbox>
</div>
<div class="p-2 flex gap-2" @click.stop>
<a-button size="small" class="!text-xs text-gray-500 text-capitalize" @click.stop="showAll">
<a-button size="small" class="!text-xs text-gray-500 text-capitalize" @click.stop="showAll()">
<!-- Show All -->
{{ $t('general.showAll') }}
</a-button>
<a-button size="small" class="!text-xs text-gray-500 text-capitalize" @click.stop="hideAll">
<a-button size="small" class="!text-xs text-gray-500 text-capitalize" @click.stop="hideAll()">
<!-- Hide All -->
{{ $t('general.hideAll') }}
</a-button>

34
packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue

@ -18,6 +18,8 @@ const sharedViewListDlg = ref(false)
const publicViewId = null
const isView = false
// TODO: pending for shared view
// interface Props {
@ -40,6 +42,8 @@ const showWebhookDrawer = ref(false)
const quickImportDialog = ref(false)
const { isUIAllowed } = useUIPermission()
const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
let offset = 0
let c = 1
@ -117,32 +121,46 @@ const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
</div>
</a-button>
<template #overlay>
<div class="bg-white shadow-lg">
<div class="bg-white shadow-lg !border">
<div>
<div class="nc-menu-item" @click="exportFile(ExportTypes.CSV)">
<div v-t="['a:actions:download-csv']" class="nc-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadIcon />
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</div>
<div class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<div v-t="['a:actions:download-excel']" class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadIcon />
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}
</div>
<div class="nc-menu-item" @click="quickImportDialog = true">
<div
v-if="isUIAllowed('csvImport') && !isView"
v-t="['a:actions:upload-csv']"
class="nc-menu-item"
@click="quickImportDialog = true"
>
<MdiUploadIcon />
<!-- Upload CSV -->
{{ $t('activity.uploadCSV') }}
</div>
<div class="nc-menu-item" @click="sharedViewListDlg = true">
<div
v-if="isUIAllowed('SharedViewList') && !isView"
v-t="['a:actions:shared-view-list']"
class="nc-menu-item"
@click="sharedViewListDlg = true"
>
<MdiViewListIcon />
<!-- Shared View List -->
{{ $t('activity.listSharedView') }}
</div>
<div class="nc-menu-item" @click="showWebhookDrawer = true">
<div
v-if="isUIAllowed('webhook') && !isView"
v-t="['c:actions:webhook']"
class="nc-menu-item"
@click="showWebhookDrawer = true"
>
<MdiHookIcon />
<!-- todo: i18n -->
Webhook
{{ $t('objects.webhooks') }}
</div>
</div>
</div>

4
packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue

@ -17,6 +17,8 @@ const { $e } = useNuxtApp()
const { dashboardUrl } = useDashboard()
const { isUIAllowed } = useUIPermission()
let showShareModel = $ref(false)
const passwordProtected = $ref(false)
@ -102,7 +104,7 @@ const copyLink = () => {
<template>
<div>
<a-button v-t="['c:view:share']" outlined class="nc-btn-share-view nc-toolbar-btn">
<a-button v-if="isUIAllowed('share-view')" v-t="['c:view:share']" outlined class="nc-btn-share-view nc-toolbar-btn">
<div class="flex align-center gap-1" @click="genShareLink">
<MdiOpenInNewIcon />
<!-- Share View -->

4
packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue

@ -46,7 +46,7 @@ watch(
</a-button>
</div>
<template #overlay>
<div class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto">
<div class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto !border">
<div v-if="sorts?.length" class="sort-grid mb-2" @click.stop>
<template v-for="(sort, i) in sorts || []" :key="i">
<!-- <v-icon :key="`${i}icon`" class="nc-sort-item-remove-btn" small @click.stop="deleteSort(sort)"> mdi-close-box </v-icon> -->
@ -84,7 +84,7 @@ watch(
<!-- </v-select> -->
</template>
</div>
<a-button class="text-capitalize mb-1 mt-4" @click.stop="addSort">
<a-button class="text-capitalize mb-1 mt-4" type="primary" ghost @click.stop="addSort">
<div class="flex gap-1 align-center">
<MdiAddIcon />
<!-- Add Sort Option -->

277
packages/nc-gui-v2/components/smartsheet/Form.vue

@ -30,8 +30,6 @@ const meta = inject(MetaInj)
const view = inject(ActiveViewInj)
if (meta) useProvideColumnCreateStore(meta)
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view as any)
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta as any, false, async () => {
@ -47,8 +45,6 @@ const hiddenColumns = ref<Record<string, any>>([])
const draggableRef = ref()
const editOrAddRef = ref()
const systemFieldsIds = ref<Record<string, any>>([])
const showColumnDropdown = ref(false)
@ -293,10 +289,6 @@ onClickOutside(draggableRef, () => {
activeRow.value = ''
})
onClickOutside(editOrAddRef, () => {
showColumnDropdown.value = false
})
onMounted(async () => {
await loadFormView()
setFormData()
@ -316,7 +308,7 @@ onMounted(async () => {
New form will be loaded after {{ secondsRemain }} seconds
</div>
<div v-if="formViewData.submit_another_form" class="text-center mt-4">
<a-button type="primary" size="large" @click="submitted = false"> Submit Another Form </a-button>
<a-button type="primary" size="large" @click="submitted = false"> Submit Another Form</a-button>
</div>
</div>
</a-col>
@ -390,7 +382,7 @@ onMounted(async () => {
{{ $t('msg.info.dragDropHide') }}
</div>
<a-dropdown v-model:visible="showColumnDropdown" :trigger="['click']">
<a-button type="link" class="w-full caption mt-2" size="large" @click="showColumnDropdown = true">
<a-button type="link" class="w-full caption mt-2" size="large" @click.stop="showColumnDropdown = true">
<div class="flex items-center prose-sm justify-center text-gray-400">
<mdi-plus />
<!-- Add new field to this table -->
@ -398,144 +390,153 @@ onMounted(async () => {
</div>
</a-button>
<template #overlay>
<SmartsheetColumnEditOrAdd ref="editOrAddRef" @submit="submitCallback" @cancel="showColumnDropdown = false" />
<SmartsheetColumnEditOrAddProvider
v-if="showColumnDropdown"
@submit="submitCallback"
@cancel="showColumnDropdown = false"
@click.stop
@keydown.stop
/>
</template>
</a-dropdown>
</template>
</Draggable>
</a-col>
<a-col v-if="formViewData" :span="isEditable ? 16 : 24" class="h-full overflow-auto scrollbar-thin-primary">
<div class="h-[200px]">
<a-card class="h-full !bg-[#dbdad7] ma-0 rounded-b-0">
<a-form ref="formRef" :model="formState">
<a-card class="rounded ma-10">
<!-- Header -->
<a-form-item class="ma-0 gap-0 pa-0">
<a-input
v-model:value="formViewData.heading"
class="w-full text-bold text-h3"
size="large"
hide-details
placeholder="Form Title"
:bordered="false"
@blur="updateView"
@keydown.enter="updateView"
/>
</a-form-item>
<!-- Sub Header -->
<a-form-item>
<a-input
v-model:value="formViewData.subheading"
class="w-full"
size="large"
hide-details
:placeholder="$t('msg.info.formDesc')"
:bordered="false"
@blur="updateView"
@click="updateView"
/>
</a-form-item>
<Draggable
ref="draggableRef"
:list="localColumns"
item-key="fk_column_id"
draggable=".item"
group="form-inputs"
class="h-100"
@change="onMove($event)"
@start="drag = true"
@end="drag = false"
>
<template #item="{ element, index }">
<div class="nc-editable item cursor-pointer hover:bg-primary/10 pa-3" @click="activeRow = element.title">
<div class="flex">
<div class="flex flex-1">
<div class="flex flex-row">
<mdi-drag-vertical class="flex flex-1" />
</div>
<SmartsheetHeaderVirtualCell
v-if="isVirtualCol(element)"
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
/>
<SmartsheetHeaderCell
v-else
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
/>
</div>
<div v-if="isUIAllowed('editFormView') && !isRequired(element, element.required)" class="flex">
<mdi-eye-off-outline class="opacity-0 nc-field-remove-icon" @click.stop="hideColumn(index)" />
<div class="h-[200px] !bg-[#dbdad7]">
<!-- for future implementation of cover image -->
</div>
<a-card class="h-full ma-0 rounded-b-0 pa-4" body-style="max-width: 700px; margin: 0 auto; margin-top: -200px;">
<a-form ref="formRef" :model="formState">
<a-card class="rounded ma-2 py-10 px-5">
<!-- Header -->
<a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0">
<a-input
v-model:value="formViewData.heading"
class="w-full text-bold text-h3"
size="large"
hide-details
placeholder="Form Title"
:bordered="false"
@blur="updateView"
@keydown.enter="updateView"
/>
</a-form-item>
<div v-else class="ml-3 w-full text-bold text-h3">{{ formViewData.heading }}</div>
<!-- Sub Header -->
<a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0">
<a-input
v-model:value="formViewData.subheading"
class="w-full"
size="large"
hide-details
:placeholder="$t('msg.info.formDesc')"
:bordered="false"
:disabled="!isEditable"
@blur="updateView"
@click="updateView"
/>
</a-form-item>
<div v-else class="ml-3 mb-5 w-full text-bold text-h3">{{ formViewData.subheading }}</div>
<Draggable
ref="draggableRef"
:list="localColumns"
item-key="fk_column_id"
draggable=".item"
group="form-inputs"
class="h-100"
@change="onMove($event)"
@start="drag = true"
@end="drag = false"
>
<template #item="{ element, index }">
<div class="nc-editable item cursor-pointer hover:bg-primary/10 pa-3" @click="activeRow = element.title">
<div class="flex">
<div class="flex flex-1">
<div class="flex flex-row">
<mdi-drag-vertical class="flex flex-1" />
</div>
<SmartsheetHeaderVirtualCell
v-if="isVirtualCol(element)"
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
/>
<SmartsheetHeaderCell
v-else
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
/>
</div>
<div v-if="isUIAllowed('editFormView') && !isRequired(element, element.required)" class="flex">
<mdi-eye-off-outline class="opacity-0 nc-field-remove-icon" @click.stop="hideColumn(index)" />
</div>
<a-form-item
v-if="isVirtualCol(element)"
class="ma-0 gap-0 pa-0"
:name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]"
>
<SmartsheetVirtualCell v-model="formState[element.title]" class="nc-input" :column="element" />
</div>
<a-form-item
v-if="isVirtualCol(element)"
class="ma-0 gap-0 pa-0"
:name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]"
>
<SmartsheetVirtualCell v-model="formState[element.title]" class="nc-input" :column="element" />
</a-form-item>
<a-form-item
v-else
class="ma-0 gap-0 pa-0"
:name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]"
>
<SmartsheetCell v-model="formState[element.title]" class="nc-input" :column="element" :edit-enabled="true" />
</a-form-item>
<div v-if="activeRow === element.title">
<a-form-item class="my-0 w-1/2">
<a-input
v-model:value="element.label"
size="small"
class="form-meta-input !bg-[#dbdbdb]"
:placeholder="$t('msg.info.formInput')"
@change="updateColMeta(element)"
>
</a-input>
</a-form-item>
<a-form-item
v-else
class="ma-0 gap-0 pa-0"
:name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]"
>
<SmartsheetCell
v-model="formState[element.title]"
class="nc-input"
:column="element"
:edit-enabled="true"
<a-form-item class="mt-2 mb-0 w-1/2">
<a-input
v-model:value="element.description"
size="small"
class="form-meta-input !bg-[#dbdbdb] text-sm"
:placeholder="$t('msg.info.formHelpText')"
@change="updateColMeta(element)"
/>
</a-form-item>
<div v-if="activeRow === element.title">
<a-form-item class="my-0 w-1/2">
<a-input
v-model:value="element.label"
class="form-meta-input !bg-[#dbdbdb]"
:placeholder="$t('msg.info.formInput')"
@change="updateColMeta(element)"
>
</a-input>
</a-form-item>
<a-form-item class="mt-2 mb-0 w-1/2">
<a-input
v-model:value="element.description"
class="form-meta-input !bg-[#dbdbdb]"
:placeholder="$t('msg.info.formHelpText')"
@change="updateColMeta(element)"
/>
</a-form-item>
<div class="items-center flex">
<span class="text-sm text-gray-500 mr-2">{{ $t('general.required') }}</span>
<a-switch v-model:checked="element.required" class="my-2" @change="updateColMeta(element)" />
</div>
<div class="items-center flex">
<span class="text-sm text-gray-500 mr-2">{{ $t('general.required') }}</span>
<a-switch v-model:checked="element.required" size="small" class="my-2" @change="updateColMeta(element)" />
</div>
<span class="text-gray-500">{{ element.description }}</span>
</div>
</template>
<template #footer>
<div
v-if="!localColumns.length"
class="mt-4 border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center"
>
Drag and drop fields here to add
</div>
</template>
</Draggable>
</a-card>
</a-form>
<div class="justify-center flex mt-5">
<a-button class="flex items-center gap-2 !bg-primary text-white rounded" size="large" @click="submitForm">
<!-- Submit -->
{{ $t('general.submit') }}
</a-button>
</div>
<span class="text-gray-500">{{ element.description }}</span>
</div>
</template>
<template #footer>
<div
v-if="!localColumns.length"
class="mt-4 border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center"
>
Drag and drop fields here to add
</div>
</template>
</Draggable>
<div class="justify-center flex mt-10">
<a-button class="flex items-center gap-2 !bg-primary text-white rounded" size="large" @click="submitForm">
<!-- Submit -->
{{ $t('general.submit') }}
</a-button>
</div>
</a-card>
</a-form>
<div v-if="isEditable" class="mx-10 px-10">
<!-- After form is submitted -->
<div class="text-gray-500 mt-4 mb-2">
{{ $t('msg.info.afterFormSubmitted') }}
@ -551,6 +552,7 @@ onMounted(async () => {
<a-switch
v-model:checked="formViewData.submit_another_form"
v-t="[`a:form-view:submit-another-form`]"
size="small"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.submitAnotherForm') }}</span>
@ -561,20 +563,21 @@ onMounted(async () => {
<a-switch
v-model:checked="formViewData.show_blank_form"
v-t="[`a:form-view:show-blank-form`]"
size="small"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.showBlankForm') }}</span>
</div>
<div class="my-4">
<a-switch v-model:checked="emailMe" v-t="[`a:form-view:email-me`]" @change="onEmailChange" />
<a-switch v-model:checked="emailMe" v-t="[`a:form-view:email-me`]" size="small" @change="onEmailChange" />
<!-- Email me at <email> -->
<span class="ml-4">
{{ $t('msg.info.emailForm') }} <span class="text-bold text-gray-600">{{ state.user.value?.email }}</span>
</span>
</div>
</div>
</a-card>
</div>
</div>
</a-card>
</a-col>
</a-row>
</template>

69
packages/nc-gui-v2/components/smartsheet/Gallery.vue

@ -1,6 +1,7 @@
<script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk'
import { inject, provide, useViewData } from '#imports'
import Row from '~/components/smartsheet/Row.vue'
import { ActiveViewInj, ChangePageInj, FieldsInj, IsFormInj, IsGridInj, MetaInj, PaginationDataInj, ReadonlyInj } from '~/context'
import ImageIcon from '~icons/mdi/file-image-box'
@ -50,45 +51,46 @@ const attachments = (record: any): Array<Attachment> => {
}
</script>
<!-- TODO: Fix scrolling -->
<template>
<div class="flex flex-col h-full min-h-0 w-full">
<div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3">
<div class="flex flex-col h-full w-full">
<div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3 overflow-auto">
<div v-for="(record, recordIndex) in data" :key="recordIndex" class="flex flex-col">
<a-card hoverable class="!rounded-lg h-full">
<template #cover>
<a-carousel v-if="attachments(record).length !== 0" autoplay>
<img
v-for="(attachment, index) in attachments(record)"
:key="index"
class="h-52 rounded-t-lg"
:src="attachment.url"
/>
</a-carousel>
<ImageIcon v-else class="w-full h-48 my-4 text-cool-gray-200" />
</template>
<Row :row="record">
<a-card hoverable class="!rounded-lg h-full">
<template #cover>
<a-carousel v-if="attachments(record).length !== 0" autoplay>
<img
v-for="(attachment, index) in attachments(record)"
:key="index"
class="h-52 rounded-t-lg"
:src="attachment.url"
/>
</a-carousel>
<ImageIcon v-else class="w-full h-48 my-4 text-cool-gray-200" />
</template>
<div
v-for="(col, colIndex) in fields"
:key="colIndex"
class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full"
>
<div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">
<div class="w-full text-gray-600">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
<div
v-for="(col, colIndex) in fields"
:key="colIndex"
class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full"
>
<div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">
<div class="w-full text-gray-600">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
</div>
</div>
</div>
<div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start">
<div v-if="isRowEmpty(record, col)" class="h-3 bg-gray-200 px-5 rounded-lg"></div>
<template v-else>
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="record.row[col.title]" :column="col" />
<SmartsheetCell v-else v-model="record.row[col.title]" :column="col" :edit-enabled="false" />
</template>
<div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start">
<div v-if="isRowEmpty(record, col)" class="h-3 bg-gray-200 px-5 rounded-lg"></div>
<template v-else>
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="record.row[col.title]" :column="col" />
<SmartsheetCell v-else v-model="record.row[col.title]" :column="col" :edit-enabled="false" />
</template>
</div>
</div>
</div>
</a-card>
</a-card>
</Row>
</div>
</div>
<SmartsheetPagination />
@ -97,7 +99,6 @@ const attachments = (record: any): Array<Attachment> => {
<style scoped>
.nc-gallery-container {
height: calc(100vh - 250px);
overflow: auto;
}
</style>

97
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -11,7 +11,6 @@ import {
ref,
useEventListener,
useGridViewColumnWidth,
useProvideColumnCreateStore,
useSmartsheetStoreOrThrow,
useViewData,
watch,
@ -43,6 +42,8 @@ const isLocked = inject(IsLockedInj, false)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
const { isUIAllowed } = useUIPermission()
// todo: get from parent ( inject or use prop )
const isPublicView = false
@ -83,10 +84,15 @@ const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useG
onMounted(loadGridViewColumns)
provide(IsFormInj, ref(false))
provide(IsGridInj, true)
provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage)
provide(ReadonlyInj, !isUIAllowed('xcDatatableEditable'))
reloadViewDataHook?.on(async () => {
await loadData()
loadAggCommentsCount()
@ -120,9 +126,6 @@ defineExpose({
loadData,
})
// instantiate column create store
if (meta) useProvideColumnCreateStore(meta)
// reset context menu target on hide
watch(contextMenu, () => {
if (!contextMenu.value) {
@ -291,9 +294,9 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']">
<table ref="smartTable" class="xc-row-table nc-grid backgroundColorDefault" @contextmenu.prevent="contextMenu = true">
<thead>
<tr class="nc-grid-header">
<tr class="nc-grid-header border-1 bg-gray-100 sticky top[-1px]">
<th>
<div class="w-full h-full bg-gray-100 flex w-[80px] px-1 items-center">
<div class="w-full h-full bg-gray-100 flex min-w-[80px] pl-5 pr-1 items-center">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
<div
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
@ -322,14 +325,20 @@ const expandForm = (row: Row, state: Record<string, any>) => {
</div>
</th>
<!-- v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" -->
<th v-t="['c:column:add']" @click="addColumnDropdown = true">
<th v-if="isUIAllowed('add-column')" v-t="['c:column:add']" @click.stop="addColumnDropdown = true">
<a-dropdown v-model:visible="addColumnDropdown" :trigger="['click']">
<div class="h-full w-[60px] flex align-center justify-center">
<MdiPlus class="text-sm" />
</div>
<template #overlay>
<SmartsheetColumnEditOrAdd @click.stop @keydown.stop @cancel="addColumnDropdown = false" />
<SmartsheetColumnEditOrAddProvider
v-if="addColumnDropdown"
@submit="addColumnDropdown = false"
@cancel="addColumnDropdown = false"
@click.stop
@keydown.stop
/>
</template>
</a-dropdown>
</th>
@ -339,33 +348,30 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<SmartsheetRow v-for="(row, rowIndex) of data" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr class="nc-grid-row">
<td key="row-index" class="caption nc-grid-cell">
<div class="align-center flex w-[80px]">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1">
<div class="align-center flex min-w-[80px]">
<div class="nc-row-no" :class="{ hidden: row.rowMeta.selected }">{{ rowIndex + 1 }}</div>
<div
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
<span class="flex-1" />
<div class="nc-expand">
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
</div>
<span class="flex-1" />
<div class="nc-expand" :class="{ 'nc-comment': row.rowMeta?.commentCount }">
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
@click="expandForm(row, state)"
>
{{ row.rowMeta.commentCount }}
</span>
<div v-else class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:bg-primary/10">
<MdiArrowExpand
class="select-none transform hover:(text-pink-500 scale-120) nc-row-expand"
@click="expandForm(row, state)"
>
{{ row.rowMeta.commentCount }}
</span>
<div
v-else
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:bg-primary/10"
>
<MdiArrowExpand
class="select-none transform hover:(text-pink-500 scale-120)"
@click="expandForm(row, state)"
/>
</div>
/>
</div>
</div>
</div>
@ -413,7 +419,11 @@ const expandForm = (row: Row, state: Record<string, any>) => {
</template>
</SmartsheetRow>
<tr v-if="!isLocked">
<!--
TODO: add relationType !== 'bt' ?
v1: <tr v-if="!isView && !isLocked && !isPublicView && isEditable && relationType !== 'bt'">
-->
<tr v-if="!isView && !isLocked && !isPublicView && isUIAllowed('xcDatatableEditable')">
<td
v-t="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1"
@ -462,10 +472,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<style scoped lang="scss">
.nc-grid-wrapper {
width: 100%;
// todo : proper height calculation
height: calc(100vh - 215px);
overflow: auto;
@apply h-full w-full overflow-auto;
td,
th {
@ -474,7 +481,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
position: relative;
}
td > div {
td:not(:first-child) > div {
overflow: hidden;
@apply flex align-center h-auto px-1;
}
@ -482,10 +489,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
table,
td,
th {
border-right: 1px solid #7f828b33 !important;
border-left: 1px solid #7f828b33 !important;
border-bottom: 1px solid #7f828b33 !important;
border-top: 1px solid #7f828b33 !important;
@apply !border-1;
border-collapse: collapse;
}
@ -511,8 +515,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
}
td.active::before {
background: #0040bc;
opacity: 0.1;
@apply bg-primary/5;
}
}
@ -528,10 +531,15 @@ const expandForm = (row: Row, state: Record<string, any>) => {
.nc-grid-row {
.nc-row-expand-and-checkbox {
@apply w-full items-center justify-between p-1;
@apply w-full items-center justify-between;
}
.nc-expand {
@apply hidden;
&:not(.nc-comment) {
@apply hidden;
}
&.nc-comment {
display: flex;
}
}
&:hover {
@ -550,6 +558,11 @@ const expandForm = (row: Row, state: Record<string, any>) => {
}
.nc-grid-header {
position: sticky;
top: -1px;
@apply z-1;
&:hover {
.nc-no-label {
@apply hidden;

28
packages/nc-gui-v2/components/smartsheet/Pagination.vue

@ -19,7 +19,9 @@ const page = computed({
<template>
<div class="flex items-center">
<span v-if="count !== null && count !== Infinity" class="caption ml-2"> {{ count }} record{{ count !== 1 ? 's' : '' }} </span>
<span v-if="count !== null && count !== Infinity" class="caption ml-2 text-gray-500">
{{ count }} record{{ count !== 1 ? 's' : '' }}
</span>
<div class="flex-1" />
@ -34,21 +36,12 @@ const page = computed({
:show-size-changer="false"
/>
<div v-else class="mx-auto d-flex align-center mt-n1" style="max-width: 250px">
<span class="caption" style="white-space: nowrap"> Change page:</span>
<v-text-field
:value="page"
class="ml-1 caption"
:full-width="false"
outlined
dense
hide-details
type="number"
@keydown.enter="changePage(page)"
>
<template #append>
<span class="text-xs" style="white-space: nowrap"> Change page:</span>
<a-input :value="page" size="small" class="ml-1 !text-xs" type="number" @keydown.enter="changePage(page)">
<template #suffix>
<MdiKeyboardIcon class="mt-1" @click="changePage(page)" />
</template>
</v-text-field>
</a-input>
</div>
<div class="flex-1" />
@ -60,4 +53,11 @@ const page = computed({
line-height: 21px !important;
@apply text-sm;
}
:deep(.ant-pagination-item:not(.ant-pagination-item-active) a) {
line-height: 21px !important;
@apply text-sm text-gray-500;
}
:deep(.ant-pagination-item-link) {
@apply text-gray-500;
}
</style>

18
packages/nc-gui-v2/components/smartsheet/Toolbar.vue

@ -1,24 +1,22 @@
<script setup lang="ts">
import { useSmartsheetStoreOrThrow } from '~/composables'
const { isGrid, isForm } = useSmartsheetStoreOrThrow()
const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow()
</script>
<template>
<div class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[48px] px-2" style="z-index: 7">
<SmartsheetToolbarFieldsMenu v-if="isGrid" :show-system-fields="false" class="ml-1" />
<div class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[48px] px-2 border-b" style="z-index: 7">
<SmartsheetToolbarFieldsMenu v-if="isGrid || isGallery" :show-system-fields="false" class="ml-1" />
<SmartsheetToolbarColumnFilterMenu v-if="isGrid" />
<SmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery" />
<SmartsheetToolbarSortListMenu v-if="isGrid" />
<SmartsheetToolbarSortListMenu v-if="isGrid || isGallery" />
<SmartsheetToolbarShareView v-if="isForm || isGrid" />
<SmartsheetToolbarMoreActions v-if="isGrid" />
<SmartsheetToolbarSearchData v-if="isGrid" class="shrink ml-2" />
<div class="flex-1" />
<SmartsheetToolbarSearchData v-if="isGrid || isGallery" class="shrink mr-2" />
</div>
</template>
@ -26,4 +24,8 @@ const { isGrid, isForm } = useSmartsheetStoreOrThrow()
:deep(.nc-toolbar-btn) {
@apply border-0 !text-xs font-semibold px-2;
}
.nc-table-toolbar {
border-color: #f0f0f0 !important;
}
</style>

19
packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue

@ -10,9 +10,13 @@ import MdiDoorOpen from '~icons/mdi/door-open'
import MdiDoorClosed from '~icons/mdi/door-closed'
const emit = defineEmits(['cancel'])
const { meta } = useSmartsheetStoreOrThrow()
const { commentsDrawer, primaryValue, save: _save } = useExpandedFormStoreOrThrow()
const { isNew, syncLTARRefs } = useSmartsheetRowStoreOrThrow()
const { isUIAllowed } = useUIPermission()
const save = async () => {
@ -31,8 +35,8 @@ const iconColor = '#1890ff'
</script>
<template>
<div class="flex p-2 align-center gap-2">
<h5 class="text-lg font-weight-medium flex align-center gap-1 mb-0">
<div class="flex p-2 align-center gap-2 p-4">
<h5 class="text-lg font-weight-medium flex align-center gap-1 mb-0 min-w-0 overflow-x-hidden truncate">
<mdi-table-arrow-right :style="{ color: iconColor }" />
<template v-if="meta">
@ -45,12 +49,17 @@ const iconColor = '#1890ff'
</h5>
<div class="flex-grow" />
<mdi-reload class="cursor-pointer select-none" />
<component :is="drawerToggleIcon" class="cursor-pointer select-none" @click="commentsDrawer = !commentsDrawer" />
<a-button size="small" class="!text" @click="emit('cancel')">
<component
:is="drawerToggleIcon"
v-if="isUIAllowed('rowComments')"
class="cursor-pointer select-none"
@click="commentsDrawer = !commentsDrawer"
/>
<a-button class="!text" @click="emit('cancel')">
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button size="small" :disabled="!isUIAllowed('tableRowUpdate')" type="primary" @click="save">
<a-button :disabled="!isUIAllowed('tableRowUpdate')" type="primary" @click="save">
<!-- Save Row -->
{{ $t('activity.saveRow') }}
</a-button>

6
packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue

@ -83,14 +83,14 @@ const isExpanded = useVModel(props, 'modelValue', emits)
<a-modal v-model:visible="isExpanded" :footer="null" width="min(90vw,1000px)" :body-style="{ padding: 0 }" :closable="false">
<Header @cancel="isExpanded = false" />
<a-card class="!bg-gray-100">
<div class="flex h-full nc-form-wrapper items-stretch">
<div class="flex h-full nc-form-wrapper items-stretch min-h-[70vh]">
<div class="flex-grow overflow-auto scrollbar-thin-primary">
<div class="w-[500px] mx-auto">
<div v-for="col in fields" :key="col.title" class="mt-2">
<div v-for="col in fields" :key="col.title" class="mt-2 py-2" :class="`nc-expand-col-${col.title}`">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />
<div class="!bg-white rounded px-1 min-h-[35px] flex align-center">
<div class="!bg-white rounded px-1 min-h-[35px] flex align-center mt-2">
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<SmartsheetCell
v-else

98
packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue

@ -11,7 +11,10 @@ const emits = defineEmits<Emits>()
const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const isView = ref(false)
let showApiSnippet = $ref(false)
const showWebhookDrawer = ref(false)
@ -33,67 +36,76 @@ function onOpenModal(type: ViewTypes, title = '') {
<template>
<a-menu :selected-keys="[]" class="flex-1 flex flex-col">
<h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500">
{{ $t('activity.createView') }}
</h3>
<a-menu-item key="grid" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.GRID)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.grid') }}
</template>
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')">
<h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500">
{{ $t('activity.createView') }}
</h3>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GRID].icon" :class="`text-${viewIcons[ViewTypes.GRID].color}`" />
<a-menu-item key="grid" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.GRID)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.grid') }}
</template>
<div>{{ $t('objects.viewType.grid') }}</div>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GRID].icon" :class="`text-${viewIcons[ViewTypes.GRID].color}`" />
<div class="flex-1" />
<div>{{ $t('objects.viewType.grid') }}</div>
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<div class="flex-1" />
<a-menu-item key="gallery" class="group !flex !items-center !-my0 !h-[30px]" @click="onOpenModal(ViewTypes.GALLERY)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.gallery') }}
</template>
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GALLERY].icon" :class="`text-${viewIcons[ViewTypes.GALLERY].color}`" />
<a-menu-item key="gallery" class="group !flex !items-center !-my0 !h-[30px]" @click="onOpenModal(ViewTypes.GALLERY)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.gallery') }}
</template>
<div>{{ $t('objects.viewType.gallery') }}</div>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GALLERY].icon" :class="`text-${viewIcons[ViewTypes.GALLERY].color}`" />
<div class="flex-1" />
<div>{{ $t('objects.viewType.gallery') }}</div>
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<div class="flex-1" />
<a-menu-item v-if="!isView" key="form" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.FORM)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.form') }}
</template>
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item
v-if="!isView"
key="form"
class="group !flex !items-center !my-0 !h-[30px]"
@click="onOpenModal(ViewTypes.FORM)"
>
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.form') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.FORM].icon" :class="`text-${viewIcons[ViewTypes.FORM].color}`" />
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.FORM].icon" :class="`text-${viewIcons[ViewTypes.FORM].color}`" />
<div>{{ $t('objects.viewType.form') }}</div>
<div>{{ $t('objects.viewType.form') }}</div>
<div class="flex-1" />
<div class="flex-1" />
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
</div>
<SmartsheetSidebarMenuApiSnippet v-model="showApiSnippet" />
<div class="flex-auto justify-end flex flex-col gap-4 mt-4">
<button
v-if="isUIAllowed('virtualViewsCreateOrEdit')"
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease !text-xs"
@click="onWebhooks"
>

10
packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -25,6 +25,8 @@ const vModel = useVModel(props, 'view', emits)
const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
/** Is editing the view name enabled */
let isEditing = $ref<boolean>(false)
@ -141,7 +143,11 @@ function onStopEdit() {
</script>
<template>
<a-menu-item class="select-none group !flex !items-center !my-0" @dblclick.stop="onDblClick" @click.stop="onClick">
<a-menu-item
class="select-none group !flex !items-center !my-0"
@dblclick.stop="isUIAllowed('virtualViewsCreateOrEdit') && onDblClick"
@click.stop="onClick"
>
<div v-t="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2">
<div class="flex w-auto">
<MdiDrag
@ -161,7 +167,7 @@ function onStopEdit() {
<div class="flex-1" />
<template v-if="!isEditing">
<template v-if="!isEditing && isUIAllowed('virtualViewsCreateOrEdit')">
<div class="flex items-center gap-1">
<a-tooltip placement="left">
<template #title>

8
packages/nc-gui-v2/components/smartsheet/sidebar/index.vue

@ -12,6 +12,8 @@ const activeView = inject(ActiveViewInj, ref())
const { views, loadViews } = useViews(meta)
const { isUIAllowed } = useUIPermission()
const router = useRouter()
const route = useRoute()
@ -103,7 +105,7 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
<Toolbar v-else class="py-3 px-2 max-w-[50px] flex !flex-col-reverse gap-4 items-center mt-[-1px]">
<template #start>
<a-tooltip placement="left">
<a-tooltip v-if="isUIAllowed('virtualViewsCreateOrEdit')" placement="left">
<template #title> {{ $t('objects.webhooks') }}</template>
<div class="nc-sidebar-right-item hover:after:bg-gray-300">
@ -111,7 +113,7 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
</div>
</a-tooltip>
<div class="dot" />
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="dot" />
<a-tooltip placement="left">
<template #title> Get API Snippet</template>
@ -128,7 +130,7 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
<div v-if="sidebarOpen" class="flex-1 flex flex-col">
<MenuTop @open-modal="openModal" @deleted="loadViews" @sorted="loadViews" />
<a-divider class="my-2" />
<a-divider v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="my-2" />
<MenuBottom @open-modal="openModal" />
</div>

14
packages/nc-gui-v2/components/smartsheet/sidebar/menu/ApiSnippet.vue

@ -128,8 +128,8 @@ const afterVisibleChange = (visible: boolean) => {
:closable="false"
@after-visible-change="afterVisibleChange"
>
<div class="flex flex-col w-full h-full">
<a-typography-title :level="4">Code Snippet</a-typography-title>
<div class="flex flex-col w-full h-full p-4">
<a-typography-title :level="4" class="pb-1">Code Snippet</a-typography-title>
<a-tabs v-model:activeKey="selectedLangName" class="!h-full">
<a-tab-pane v-for="item in langs" :key="item.name" class="!h-full">
<template #tab>
@ -146,6 +146,11 @@ const afterVisibleChange = (visible: boolean) => {
:disable-deep-compare="true"
/>
<div class="flex flex-row w-full justify-end space-x-3 mt-4 uppercase">
<a-select v-if="activeLang" v-model:value="selectedClient" style="width: 6rem">
<a-select-option v-for="(client, i) in activeLang?.clients" :key="i" class="!w-full uppercase" :value="client">
{{ client }}
</a-select-option>
</a-select>
<a-button
v-t="[
'c:snippet:copy',
@ -155,11 +160,6 @@ const afterVisibleChange = (visible: boolean) => {
@click="onCopyToClipboard"
>Copy to clipboard</a-button
>
<a-select v-if="activeLang" v-model:value="selectedClient" style="width: 6rem">
<a-select-option v-for="(client, i) in activeLang?.clients" :key="i" class="!w-full uppercase" :value="client">
{{ client }}
</a-select-option>
</a-select>
</div>
<div class="absolute bottom-4 flex flex-row justify-center w-[95%]">

13
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue

@ -1,27 +1,24 @@
<script lang="ts" setup>
import AddRow from './AddRow.vue'
import DeleteTable from './DeleteTable.vue'
import LockMenu from './LockMenu.vue'
import Reload from './Reload.vue'
const { isUIAllowed } = useUIPermission()
</script>
<template>
<div class="flex gap-2">
<slot name="start" />
<LockMenu />
<LockMenu v-if="isUIAllowed('view-type')" />
<div class="dot" />
<div v-if="isUIAllowed('view-type')" class="dot" />
<Reload />
<div class="dot" />
<AddRow />
<div class="dot" />
<DeleteTable />
<AddRow v-if="isUIAllowed('xcDatatableEditable')" />
<slot name="end" />
</div>

46
packages/nc-gui-v2/components/tabs/Auth.vue

@ -2,42 +2,48 @@
import UserManagement from './auth/UserManagement.vue'
import ApiTokenManagement from './auth/ApiTokenManagement.vue'
interface TabGroup {
[key: string]: {
title: string
body: any
}
interface Tab {
title: string
body: any
}
const tabsInfo: TabGroup = {
usersManagement: {
const tabsInfo: Tab[] = [
{
title: 'Users Management',
body: () => UserManagement,
},
apiTokenManagement: {
{
title: 'API Token Management',
body: () => ApiTokenManagement,
},
}
]
const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
// const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
const selectedTabKeys = $ref<string[]>([firstKeyOfObject(tabsInfo)])
const selectedTab = $computed(() => tabsInfo[selectedTabKeys[0]])
const selectedTabKey = $ref(0)
const selectedTab = $computed(() => tabsInfo[selectedTabKey])
</script>
<template>
<div class="mt-2">
<a-menu v-model:selectedKeys="selectedTabKeys" :open-keys="[]" mode="horizontal">
<a-menu-item v-for="(tab, key) of tabsInfo" :key="key" class="select-none">
<div class="text-xs pb-2.5">
{{ tab.title }}
</div>
</a-menu-item>
</a-menu>
<div>
<a-tabs v-model:active-key="selectedTabKey" :open-keys="[]" mode="horizontal" class="nc-auth-tabs mx-6">
<a-tab-pane v-for="(tab, key) of tabsInfo" :key="key" class="select-none">
<template #tab>
<span>
{{ tab.title }}
</span>
</template>
</a-tab-pane>
</a-tabs>
<div class="mx-4 py-6 mt-2">
<component :is="selectedTab.body()" />
</div>
</div>
</template>
<style scoped>
:deep(.nc-auth-tabs .ant-tabs-nav::before) {
@apply !border-none;
}
</style>

19
packages/nc-gui-v2/components/tabs/auth/ApiTokenManagement.vue

@ -17,12 +17,17 @@ interface ApiToken extends ApiTokenType {
}
const { $api, $e } = useNuxtApp()
const { project } = $(useProject())
const { copy } = useClipboard()
let tokensInfo = $ref<ApiToken[] | undefined>([])
let showNewTokenModal = $ref(false)
let showDeleteTokenModal = $ref(false)
let selectedTokenData = $ref<ApiToken>({})
const loadApiTokens = async () => {
@ -110,8 +115,8 @@ onMounted(() => {
<div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">This action will remove this API Token</div>
<div class="flex mt-6 justify-center space-x-2">
<a-button @click="showDeleteTokenModal = false"> Cancel </a-button>
<a-button type="primary" danger @click="deleteToken()"> Confirm </a-button>
<a-button @click="showDeleteTokenModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="deleteToken()"> {{ $t('general.confirm') }} </a-button>
</div>
</div>
</a-modal>
@ -121,10 +126,10 @@ onMounted(() => {
<a-button size="middle" type="text" @click="loadApiTokens()">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<ReloadIcon class="text-gray-500" />
<div class="text-gray-500">Reload</div>
<div class="text-gray-500">{{ $t('general.reload') }}</div>
</div>
</a-button>
<a-button size="middle" @click="openNewTokenModal">
<a-button size="middle" type="primary" ghost @click="openNewTokenModal">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MdiPlusIcon />
<div>Add New Token</div>
@ -134,9 +139,9 @@ onMounted(() => {
</div>
<div v-if="tokensInfo" class="w-full flex flex-col mt-2 px-1">
<div class="flex flex-row border-b-1 text-gray-600 text-xs pb-2 pt-2">
<div class="flex w-4/10 pl-2">Description</div>
<div class="flex w-4/10 justify-center">Token</div>
<div class="flex w-2/10 justify-end pr-2">Actions</div>
<div class="flex w-4/10 pl-2">{{ $t('labels.description') }}</div>
<div class="flex w-4/10 justify-center">{{ $t('labels.token') }}</div>
<div class="flex w-2/10 justify-end pr-2">{{ $t('labels.action') }}</div>
</div>
<div v-for="(item, index) in tokensInfo" :key="index" class="flex flex-col">
<div class="flex flex-row border-b-1 items-center px-2 py-2">

114
packages/nc-gui-v2/components/tabs/auth/UserManagement.vue

@ -1,38 +1,50 @@
<script setup lang="ts">
import { useClipboard, watchDebounced } from '@vueuse/core'
import { message } from 'ant-design-vue'
import UsersModal from './user-management/UsersModal.vue'
import FeedbackForm from './user-management/FeedbackForm.vue'
import KebabIcon from '~icons/ic/baseline-more-vert'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { projectRoleTagColors } from '~/utils/userUtils'
import MidAccountIcon from '~icons/mdi/account-outline'
import ReloadIcon from '~icons/mdi/reload'
import MdiEditIcon from '~icons/ic/round-edit'
import SearchIcon from '~icons/ic/round-search'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import EmailIcon from '~icons/eva/email-outline'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiContentCopyIcon from '~icons/mdi/content-copy'
import MdiEmailSendIcon from '~icons/mdi/email-arrow-right-outline'
import RolesIcon from '~icons/mdi/drama-masks'
import type { User } from '~/lib/types'
const { $api, $e } = useNuxtApp()
import {
extractSdkResponseErrorMsg,
onMounted,
projectRoleTagColors,
ref,
useApi,
useClipboard,
useDashboard,
useNuxtApp,
useProject,
useUIPermission,
watchDebounced,
} from '#imports'
import type { User } from '~/lib'
const { $e } = useNuxtApp()
const { api } = useApi()
const { project } = useProject()
const { copy } = useClipboard()
const { isUIAllowed } = useUIPermission()
const { dashboardUrl } = $(useDashboard())
let users = $ref<null | User[]>(null)
let selectedUser = $ref<null | User>(null)
let showUserModal = $ref(false)
let showUserDeleteModal = $ref(false)
let isLoading = $ref(false)
let totalRows = $ref(0)
const currentPage = $ref(1)
const currentLimit = $ref(10)
const searchText = ref<string>('')
const loadUsers = async (page = currentPage, limit = currentLimit) => {
@ -40,16 +52,17 @@ const loadUsers = async (page = currentPage, limit = currentLimit) => {
if (!project.value?.id) return
// TODO: Types of api is not correct
const response: any = await $api.auth.projectUserList(project.value?.id, {
const response: any = await api.auth.projectUserList(project.value?.id, {
query: {
limit,
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
query: searchText.value,
},
})
} as any)
if (!response.users) return
totalRows = response.users.pageInfo.totalRows ?? 0
users = response.users.list as User[]
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -60,7 +73,8 @@ const inviteUser = async (user: User) => {
try {
if (!project.value?.id) return
await $api.auth.projectUserAdd(project.value.id, user)
await api.auth.projectUserAdd(project.value.id, user)
message.success('Successfully added user to project')
await loadUsers()
} catch (e: any) {
@ -74,9 +88,12 @@ const deleteUser = async () => {
try {
if (!project.value?.id || !selectedUser?.id) return
await $api.auth.projectUserRemove(project.value.id, selectedUser.id)
await api.auth.projectUserRemove(project.value.id, selectedUser.id)
message.success('Successfully deleted user from project')
await loadUsers()
showUserDeleteModal = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -106,7 +123,8 @@ const resendInvite = async (user: User) => {
if (!project.value?.id) return
try {
await $api.auth.projectUserResendInvite(project.value.id, user.id)
await api.auth.projectUserResendInvite(project.value.id, user.id, null)
message.success('Invite email sent successfully')
await loadUsers()
} catch (e: any) {
@ -119,20 +137,16 @@ const resendInvite = async (user: User) => {
const copyInviteUrl = (user: User) => {
if (!user.invite_token) return
const getInviteUrl = (token: string) => `${dashboardUrl}/user/authentication/signup/${token}`
copy(`${dashboardUrl}/signup/${user.invite_token}`)
copy(getInviteUrl(user.invite_token))
message.success('Invite url copied to clipboard')
}
onMounted(async () => {
onMounted(() => {
if (!users) {
isLoading = true
try {
await loadUsers()
} finally {
isLoading = false
}
loadUsers().finally(() => (isLoading = false))
}
})
@ -162,11 +176,11 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div>
</div>
</a-modal>
<div class="flex flex-row mb-4 mx-4 justify-between">
<div class="flex flex-row mb-4 mx-4 justify-between pb-2">
<div class="flex w-1/3">
<a-input v-model:value="searchText" placeholder="Filter by email">
<template #prefix>
<SearchIcon class="text-gray-400" />
<IcRoundSearch class="text-gray-400" />
</template>
</a-input>
</div>
@ -174,13 +188,13 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="flex flex-row space-x-1">
<a-button size="middle" type="text" @click="loadUsers()">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<ReloadIcon class="text-gray-500" />
<MdiReload class="text-gray-500" />
<div class="text-gray-500">Reload</div>
</div>
</a-button>
<a-button v-if="isUIAllowed('newUser')" size="middle" @click="onInvite">
<a-button v-if="isUIAllowed('newUser')" size="middle" type="primary" ghost @click="onInvite">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MidAccountIcon />
<MdiAccountPlusOutline class="mr-1" />
<div>{{ $t('activity.inviteTeam') }}</div>
</div>
</a-button>
@ -189,12 +203,12 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="px-5">
<div class="flex flex-row border-b-1 pb-2 px-2">
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
<EmailIcon class="flex text-gray-500 -mt-0.5" />
<EvaEmailOutline class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs space-x-1">{{ $t('labels.email') }}</div>
</div>
<div class="flex flex-row justify-center w-1/6 space-x-1 items-center pl-1">
<RolesIcon class="flex text-gray-500 -mt-0.5" />
<MdiDramaMasks class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs">{{ $t('objects.role') }}</div>
</div>
@ -203,12 +217,13 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div>
</div>
<div v-for="(user, index) in users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2">
<div v-for="(user, index) of users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2">
<div class="flex w-4/6 flex-wrap">
{{ user.email }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
<div class="rounded-full px-2 py-1" :style="{ backgroundColor: projectRoleTagColors[user.roles as String] }">
<div class="rounded-full px-2 py-1" :style="{ backgroundColor: projectRoleTagColors[user.roles] }">
{{ user.roles }}
</div>
</div>
@ -219,7 +234,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</template>
<a-button type="text" class="!rounded-md" @click="onEdit(user)">
<template #icon>
<MdiEditIcon class="flex mx-auto h-[1rem] text-gray-500" />
<IcRoundEdit class="flex mx-auto h-[1rem] text-gray-500" />
</template>
</a-button>
</a-tooltip>
@ -229,7 +244,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</template>
<a-button type="text" class="!rounded-md" @click="inviteUser(user)">
<template #icon>
<MdiPlusIcon class="flex mx-auto h-[1.1rem] text-gray-500" />
<MdiPlus class="flex mx-auto h-[1.1rem] text-gray-500" />
</template>
</a-button>
</a-tooltip>
@ -240,7 +255,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</template>
<a-button type="text" class="!rounded-md" @click="onDelete(user)">
<template #icon>
<MdiDeleteOutlineIcon class="flex mx-auto h-[1.1rem] text-gray-500" />
<MdiDeleteOutline class="flex mx-auto h-[1.1rem] text-gray-500" />
</template>
</a-button>
</a-tooltip>
@ -249,7 +264,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="flex flex-row items-center">
<a-button type="text" class="!px-0">
<div class="flex flex-row items-center h-[1.2rem]">
<KebabIcon />
<IcBaselineMoreVert />
</div>
</a-button>
</div>
@ -257,13 +272,13 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<a-menu>
<a-menu-item>
<div class="flex flex-row items-center py-1" @click="resendInvite(user)">
<MdiEmailSendIcon class="flex h-[1rem] text-gray-500" />
<MdiEmailArrowRightOutline class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">Resend invite email</div>
</div>
</a-menu-item>
<a-menu-item>
<div class="flex flex-row items-center py-1" @click="copyInviteUrl(user)">
<MdiContentCopyIcon class="flex h-[1rem] text-gray-500" />
<MdiContentCopy class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyInviteURL') }}</div>
</div>
</a-menu-item>
@ -285,12 +300,3 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div>
</div>
</template>
<style scoped>
.users-table {
/* equally spaced columns in table */
table-layout: fixed;
width: 100%;
}
</style>

2
packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue

@ -26,7 +26,7 @@ const { project } = useProject()
const { copy } = useClipboard()
const url = $computed(() => (base && base.uuid ? `${dashboardUrl}/nc/base/${base.uuid}` : null))
const url = $computed(() => (base && base.uuid ? `${dashboardUrl}/base/${base.uuid}` : null))
const loadBase = async () => {
try {

42
packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue

@ -1,14 +1,20 @@
<script setup lang="ts">
import { Form, message } from 'ant-design-vue'
import { useClipboard } from '@vueuse/core'
import ShareBase from './ShareBase.vue'
import SendIcon from '~icons/material-symbols/send-outline'
import CloseIcon from '~icons/material-symbols/close-rounded'
import MidAccountIcon from '~icons/mdi/account-outline'
import ContentCopyIcon from '~icons/mdi/content-copy'
import type { User } from '~/lib/types'
import { ProjectRole } from '~/lib/enums'
import { extractSdkResponseErrorMsg, isEmail, projectRoleTagColors, projectRoles } from '~/utils'
import {
computed,
extractSdkResponseErrorMsg,
isEmail,
onMounted,
projectRoles,
ref,
useClipboard,
useDashboard,
useNuxtApp,
useProject,
} from '#imports'
import type { User } from '~/lib'
import { ProjectRole } from '~/lib'
interface Props {
show: boolean
@ -22,6 +28,7 @@ interface Users {
}
const { show, selectedUser } = defineProps<Props>()
const emit = defineEmits(['closed', 'reload'])
const { project } = useProject()
@ -96,14 +103,13 @@ const saveUser = async () => {
}
}
const inviteUrl = $computed(() =>
usersData.invitationToken ? `${dashboardUrl}/user/authentication/signup/${usersData.invitationToken}` : null,
)
const inviteUrl = $computed(() => (usersData.invitationToken ? `${dashboardUrl}/signup/${usersData.invitationToken}` : null))
const copyUrl = async () => {
if (!inviteUrl) return
copy(inviteUrl)
await copy(inviteUrl)
message.success('Copied shareable base url to clipboard!')
$e('c:shared-base:copy-url')
@ -124,7 +130,7 @@ const clickInviteMore = () => {
<a-typography-title class="select-none" :level="4"> {{ $t('activity.share') }}: {{ project.title }} </a-typography-title>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')">
<template #icon>
<CloseIcon class="flex mx-auto" />
<MaterialSymbolsCloseRounded class="flex mx-auto" />
</template>
</a-button>
</div>
@ -133,7 +139,7 @@ const clickInviteMore = () => {
<template v-if="usersData.invitationToken">
<div class="flex flex-col mt-1 border-b-1 pb-5">
<div class="flex flex-row items-center pl-1.5 pb-1 h-[1.1rem]">
<MidAccountIcon />
<MdiAccountOutline />
<div class="text-xs ml-0.5 mt-0.5">Copy Invite Token</div>
</div>
@ -145,7 +151,7 @@ const clickInviteMore = () => {
</div>
<a-button type="text" class="!rounded-md -mt-0.5" @click="copyUrl">
<template #icon>
<ContentCopyIcon class="flex mx-auto text-green-700 h-[1rem]" />
<MdiContentCopy class="flex mx-auto text-green-700 h-[1rem]" />
</template>
</a-button>
</div>
@ -159,7 +165,7 @@ const clickInviteMore = () => {
<div class="flex flex-row justify-start mt-4 ml-2">
<a-button size="small" outlined @click="clickInviteMore">
<div class="flex flex-row justify-center items-center space-x-0.5">
<SendIcon class="flex mx-auto text-gray-600 h-[0.8rem]" />
<MaterialSymbolsSendOutline class="flex mx-auto text-gray-600 h-[0.8rem]" />
<div class="text-xs text-gray-600">{{ $t('activity.inviteMore') }}</div>
</div>
</a-button>
@ -168,7 +174,7 @@ const clickInviteMore = () => {
</template>
<div v-else class="flex flex-col pb-4">
<div class="flex flex-row items-center pl-2 pb-1 h-[1rem]">
<MidAccountIcon />
<MdiAccountOutline />
<div class="text-xs ml-0.5 mt-0.5">{{ selectedUser ? 'Edit User' : 'Invite Team' }}</div>
</div>
<div class="border-1 py-3 px-4 rounded-md mt-1">
@ -218,7 +224,7 @@ const clickInviteMore = () => {
<a-button type="primary" html-type="submit">
<div v-if="selectedUser">{{ $t('general.save') }}</div>
<div v-else class="flex flex-row justify-center items-center space-x-1.5">
<SendIcon class="flex h-[0.8rem]" />
<MaterialSymbolsSendOutline class="flex h-[0.8rem]" />
<div>{{ $t('activity.invite') }}</div>
</div>
</a-button>

6
packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue

@ -4,7 +4,7 @@ import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListItems from './components/ListItems.vue'
import { inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { ActiveCellInj, CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import { ActiveCellInj, CellValueInj, ColumnInj, EditModeInj, ReloadViewDataHookInj, RowInj } from '~/context'
import MdiArrowExpand from '~icons/mdi/arrow-expand'
import MdiPlus from '~icons/mdi/plus'
@ -18,6 +18,8 @@ const row = inject(RowInj)
const active = inject(ActiveCellInj)
const editEnabled = inject(EditModeInj)
const listItemsDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
@ -57,7 +59,7 @@ const unlinkRef = async (rec: Record<string, any>) => {
<ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" />
</template>
</div>
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<div v-if="editEnabled" class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<component
:is="addIcon"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500)"

10
packages/nc-gui-v2/components/virtual-cell/HasMany.vue

@ -5,7 +5,7 @@ import ItemChip from './components/ItemChip.vue'
import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue'
import { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context'
import { CellValueInj, ColumnInj, EditModeInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context'
const column = inject(ColumnInj)!
@ -17,6 +17,8 @@ const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj)
const editEnabled = inject(EditModeInj)
const listItemsDlg = ref(false)
const childListDlg = ref(false)
@ -76,7 +78,11 @@ const unlinkRef = async (rec: Record<string, any>) => {
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
@click="childListDlg = true"
/>
<MdiPlus class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
<MdiPlus
v-if="editEnabled"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
@click="listItemsDlg = true"
/>
</div>
</template>
<ListItems v-model="listItemsDlg" />

10
packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue

@ -5,7 +5,7 @@ import ItemChip from './components/ItemChip.vue'
import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue'
import { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context'
import { CellValueInj, ColumnInj, EditModeInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context'
const column = inject(ColumnInj)!
@ -17,6 +17,8 @@ const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj)
const editEnabled = inject(EditModeInj)
const listItemsDlg = ref(false)
const childListDlg = ref(false)
@ -75,7 +77,11 @@ const unlinkRef = async (rec: Record<string, any>) => {
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="childListDlg = true" />
<MdiPlus class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
<MdiPlus
v-if="editEnabled"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
@click="listItemsDlg = true"
/>
</div>
</template>

10
packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useLTARStoreOrThrow } from '#imports'
import { ActiveCellInj, IsFormInj, ReadonlyInj } from '~/context'
import { ActiveCellInj, EditModeInj, IsFormInj } from '~/context'
interface Props {
value?: string | number | boolean
@ -13,8 +13,10 @@ const emit = defineEmits(['unlink'])
const { relatedTableMeta } = useLTARStoreOrThrow()
const readonly = inject(ReadonlyInj, false)
const editEnabled = inject(EditModeInj)
const active = inject(ActiveCellInj, ref(false))
const isForm = inject(IsFormInj)
const expandedFormDlg = ref(false)
@ -27,12 +29,12 @@ const expandedFormDlg = ref(false)
@click="expandedFormDlg = true"
>
<span class="name">{{ value }}</span>
<div v-show="active || isForm" v-if="!readonly" class="flex align-center">
<div v-show="active || isForm" v-if="editEnabled" class="flex align-center">
<MdiCloseThick class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click.stop="emit('unlink')" />
</div>
<SmartsheetExpandedForm
v-if="expandedFormDlg"
v-if="expandedFormDlg && editEnabled"
v-model="expandedFormDlg"
:row="{ row: item }"
:meta="relatedTableMeta"

16
packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue

@ -1,16 +1,20 @@
<script lang="ts" setup>
import { Modal } from 'ant-design-vue'
import { Empty, Modal } from 'ant-design-vue'
import type { ColumnType } from 'nocodb-sdk'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel, watch } from '#imports'
import { ColumnInj, IsFormInj } from '~/context'
import { ColumnInj, EditModeInj, IsFormInj } from '~/context'
const props = defineProps<{ modelValue?: boolean }>()
const emit = defineEmits(['update:modelValue', 'attachRecord'])
const vModel = useVModel(props, 'modelValue', emit)
const isForm = inject(IsFormInj, ref(false))
const column = inject(ColumnInj)
const editEnabled = inject(EditModeInj)
const {
childrenList,
meta,
@ -64,9 +68,9 @@ const expandedFormRow = ref()
<MdiReload v-if="!isForm" class="cursor-pointer text-gray-500" @click="loadChildrenList" />
<a-button type="primary" class="!text-xs" size="small" @click="emit('attachRecord')">
<a-button v-if="editEnabled" type="primary" ghost class="!text-xs" size="small" @click="emit('attachRecord')">
<div class="flex align-center gap-1">
<MdiLinkVariantRemove class="text-xs text-white" @click="unlinkRow(row)" />
<MdiLinkVariantRemove class="text-xs" type="primary" @click="unlinkRow(row)" />
Link to '{{ meta.title }}'
</div>
</a-button>
@ -90,7 +94,7 @@ const expandedFormRow = ref()
}}<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div>
<div class="flex-1"></div>
<div class="flex gap-2">
<div v-if="editEnabled" class="flex gap-2">
<MdiLinkVariantRemove
class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
@click.stop="unlinkRow(row)"
@ -113,7 +117,7 @@ const expandedFormRow = ref()
show-less-items
/>
</template>
<a-empty v-else class="my-10" />
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
<SmartsheetExpandedForm

3
packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue

@ -1,6 +1,7 @@
<script lang="ts" setup>
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { Empty } from 'ant-design-vue'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel } from '#imports'
import { ColumnInj } from '~/context'
@ -115,7 +116,7 @@ const newRowState = computed(() => {
show-less-items
/>
</template>
<a-empty v-else class="my-10" />
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<SmartsheetExpandedForm
v-if="expandedFormDlg"

15
packages/nc-gui-v2/composables/useApi/index.ts

@ -1,9 +1,9 @@
import type { AxiosError, AxiosResponse } from 'axios'
import { Api } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { addAxiosInterceptors } from './interceptors'
import type { CreateApiOptions, UseApiProps, UseApiReturn } from './types'
import { createEventHook, ref, unref, useCounter, useGlobal, useNuxtApp } from '#imports'
import { addAxiosInterceptors } from './interceptors'
import { createEventHook, ref, unref, useCounter, useNuxtApp } from '#imports'
export function createApiInstance<SecurityDataType = any>(options: CreateApiOptions = {}): Api<SecurityDataType> {
return addAxiosInterceptors(
@ -33,8 +33,6 @@ export function useApi<Data = any, RequestConfig = any>({
apiOptions,
axiosConfig,
}: UseApiProps<Data> = {}): UseApiReturn<Data, RequestConfig> {
const state = useGlobal()
/**
* Local state of running requests, do not confuse with global state of running requests
* This state is only counting requests made by this instance of `useApi` and not by other instances.
@ -54,11 +52,10 @@ export function useApi<Data = any, RequestConfig = any>({
const responseHook = createEventHook<AxiosResponse<Data, RequestConfig>>()
/** global api instance */
const $api = useNuxtApp().$api
const nuxtApp = useNuxtApp()
/** api instance - with interceptors for token refresh already bound */
const api = useGlobalInstance && !!$api ? $api : createApiInstance(apiOptions)
const api = useGlobalInstance && !!nuxtApp.$api ? nuxtApp.$api : createApiInstance(apiOptions)
/** set loading to true and increment local and global request counter */
function onRequestStart() {
@ -68,7 +65,7 @@ export function useApi<Data = any, RequestConfig = any>({
inc()
/** global count */
state.runningRequests.inc()
nuxtApp.$state.runningRequests.inc()
}
/** decrement local and global request counter and check if we can stop loading */
@ -76,7 +73,7 @@ export function useApi<Data = any, RequestConfig = any>({
/** local count */
dec()
/** global count */
state.runningRequests.dec()
nuxtApp.$state.runningRequests.dec()
/** try to stop loading */
stopLoading()

3
packages/nc-gui-v2/composables/useApi/interceptors.ts

@ -18,7 +18,8 @@ export function addAxiosInterceptors(api: Api<any>) {
}
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) {
if (route && route.params && route.params.shared_base_id) config.headers['xc-shared-base-id'] = route.params.shared_base_id
if (route && route.params && route.params.projectType === 'base')
config.headers['xc-shared-base-id'] = route.params.projectId
}
return config

94
packages/nc-gui-v2/composables/useColumn.ts

@ -1,55 +1,61 @@
import type { ColumnType } from 'nocodb-sdk'
import { SqlUiFactory, UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import { useProject } from '#imports'
export function useColumn(column: Ref<ColumnType>) {
const { project } = useProject()
const uiDatatype: UITypes = column?.value?.uidt as UITypes
const abstractType = isVirtualCol(column?.value)
? null
: SqlUiFactory.create(project.value?.bases?.[0]?.config || { client: 'mysql2' }).getAbstractType(column?.value)
const uiDatatype: ComputedRef<UITypes> = computed(() => column?.value?.uidt as UITypes)
const abstractType = computed(() =>
isVirtualCol(column?.value)
? null
: SqlUiFactory.create(project.value?.bases?.[0]?.config || { client: 'mysql2' }).getAbstractType(column?.value),
)
const dataTypeLow = column?.value?.dt?.toLowerCase()
const isBoolean = abstractType === 'boolean'
const isString = abstractType === 'string'
const isTextArea = uiDatatype === UITypes.LongText
const isInt = abstractType === 'integer'
const isFloat = abstractType === 'float'
const isDate = abstractType === 'date' || uiDatatype === 'Date'
const isYear = abstractType === 'year' || uiDatatype === 'Year'
const isTime = abstractType === 'time' || uiDatatype === 'Time'
const isDateTime = abstractType === 'datetime' || uiDatatype === 'DateTime'
const isJSON = uiDatatype === 'JSON'
const isEnum = uiDatatype === 'SingleSelect'
const isSingleSelect = uiDatatype === 'SingleSelect'
const isSet = uiDatatype === 'MultiSelect'
const isMultiSelect = uiDatatype === 'MultiSelect'
const isURL = uiDatatype === 'URL'
const isEmail = uiDatatype === UITypes.Email
const isAttachment = uiDatatype === 'Attachment'
const isRating = uiDatatype === UITypes.Rating
const isCurrency = uiDatatype === 'Currency'
const isPhoneNumber = uiDatatype === UITypes.PhoneNumber
const isDecimal = uiDatatype === UITypes.Decimal
const isDuration = uiDatatype === UITypes.Duration
const isPercent = uiDatatype === UITypes.Percent
const isAutoSaved = [
UITypes.SingleLineText,
UITypes.LongText,
UITypes.PhoneNumber,
UITypes.Email,
UITypes.URL,
UITypes.Number,
UITypes.Decimal,
UITypes.Percent,
UITypes.Count,
UITypes.AutoNumber,
UITypes.SpecificDBType,
UITypes.Geometry,
].includes(uiDatatype)
const isManualSaved = [UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(uiDatatype)
const dataTypeLow = computed(() => column?.value?.dt?.toLowerCase())
const isBoolean = computed(() => abstractType.value === 'boolean')
const isString = computed(() => abstractType.value === 'string')
const isTextArea = computed(() => uiDatatype.value === UITypes.LongText)
const isInt = computed(() => abstractType.value === 'integer')
const isFloat = computed(() => abstractType.value === 'float')
const isDate = computed(() => abstractType.value === 'date' || uiDatatype.value === 'Date')
const isYear = computed(() => abstractType.value === 'year' || uiDatatype.value === 'Year')
const isTime = computed(() => abstractType.value === 'time' || uiDatatype.value === 'Time')
const isDateTime = computed(() => abstractType.value === 'datetime' || uiDatatype.value === 'DateTime')
const isJSON = computed(() => uiDatatype.value === 'JSON')
const isEnum = computed(() => uiDatatype.value === 'SingleSelect')
const isSingleSelect = computed(() => uiDatatype.value === 'SingleSelect')
const isSet = computed(() => uiDatatype.value === 'MultiSelect')
const isMultiSelect = computed(() => uiDatatype.value === 'MultiSelect')
const isURL = computed(() => uiDatatype.value === 'URL')
const isEmail = computed(() => uiDatatype.value === UITypes.Email)
const isAttachment = computed(() => uiDatatype.value === 'Attachment')
const isRating = computed(() => uiDatatype.value === UITypes.Rating)
const isCurrency = computed(() => uiDatatype.value === 'Currency')
const isPhoneNumber = computed(() => uiDatatype.value === UITypes.PhoneNumber)
const isDecimal = computed(() => uiDatatype.value === UITypes.Decimal)
const isDuration = computed(() => uiDatatype.value === UITypes.Duration)
const isPercent = computed(() => uiDatatype.value === UITypes.Percent)
const isAutoSaved = computed(() =>
[
UITypes.SingleLineText,
UITypes.LongText,
UITypes.PhoneNumber,
UITypes.Email,
UITypes.URL,
UITypes.Number,
UITypes.Decimal,
UITypes.Percent,
UITypes.Count,
UITypes.AutoNumber,
UITypes.SpecificDBType,
UITypes.Geometry,
].includes(uiDatatype.value),
)
const isManualSaved = computed(() =>
[UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(uiDatatype.value),
)
return {
abstractType,

54
packages/nc-gui-v2/composables/useColumnCreateStore.ts

@ -1,4 +1,5 @@
import { createInjectionState } from '@vueuse/core'
import clone from 'just-clone'
import { Form, message } from 'ant-design-vue'
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
@ -25,18 +26,28 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { $api } = useNuxtApp()
const { getMeta } = useMetas()
const isEdit = computed(() => !!column?.value?.id)
const idType = null
// state
// todo: give proper type - ColumnType
const additionalValidations = ref<Record<string, any>>({})
const setAdditionalValidations = (validations: Record<string, any>) => {
additionalValidations.value = validations
}
const formState = ref<Record<string, any>>({
title: 'title',
uidt: UITypes.SingleLineText,
...(column?.value || {}),
meta: column?.value?.meta || {},
...clone(column?.value || {}),
})
const additionalValidations = ref<Record<string, any>>({})
// actions
const generateNewColumnMeta = () => {
setAdditionalValidations({})
formState.value = { meta: {}, ...sqlUi.value.getNewColumn((meta.value?.columns?.length || 0) + 1) }
formState.value.title = formState.value.column_name
}
const validators = computed(() => {
return {
@ -77,17 +88,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const { resetFields, validate, validateInfos } = useForm(formState, validators)
const setAdditionalValidations = (validations: Record<string, any>) => {
additionalValidations.value = validations
}
// actions
const generateNewColumnMeta = () => {
setAdditionalValidations({})
formState.value = { meta: {}, ...sqlUi.value.getNewColumn((meta.value?.columns?.length || 0) + 1) }
formState.value.title = formState.value.title || formState.value.column_name
}
const onUidtOrIdTypeChange = () => {
const { isCurrency } = useColumn(ref(formState.value as ColumnType))
@ -118,7 +118,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
}
}
if (isCurrency) {
if (isCurrency.value) {
if (column?.value?.uidt === UITypes.Currency) {
formState.value.dtxp = column.value.dtxp
formState.value.dtxs = column.value.dtxs
@ -151,7 +151,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
formState.value.dtxp = column?.value.dtxp
}
if (isCurrency) {
if (isCurrency.value) {
if (column?.value?.uidt === UITypes.Currency) {
formState.value.dtxp = column.value.dtxp
formState.value.dtxs = column.value.dtxs
@ -177,6 +177,8 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
console.log(formState, validators)
if (!(await validate())) return
} catch (e) {
console.log(e)
console.trace()
message.error('Form validation failed')
return
}
@ -220,18 +222,18 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
return {
formState,
generateNewColumnMeta,
addOrUpdate,
onAlter,
onDataTypeChange,
onUidtOrIdTypeChange,
setAdditionalValidations,
resetFields,
validate,
validateInfos,
setAdditionalValidations,
onUidtOrIdTypeChange,
sqlUi,
onDataTypeChange,
onAlter,
addOrUpdate,
generateNewColumnMeta,
isEdit: computed(() => !!column?.value?.id),
isEdit,
column,
sqlUi,
}
},
)

12
packages/nc-gui-v2/composables/useGlobal/types.ts

@ -13,6 +13,18 @@ export interface FeedbackForm {
export interface AppInfo {
ncSiteUrl: string
authType: 'jwt' | 'masterKey' | 'none'
connectToExternalDB: boolean
defaultLimit: number
firstUser: boolean
githubAuthEnabled: boolean
googleAuthEnabled: boolean
ncMin: boolean
oneClick: boolean
projectHasAdmin: boolean
teleEnabled: boolean
type: string
version: string
}
export interface StoredState {

58
packages/nc-gui-v2/composables/useMetas.ts

@ -1,6 +1,8 @@
import { message } from 'ant-design-vue'
import type { WatchStopHandle } from 'vue'
import type { TableInfoType, TableType } from 'nocodb-sdk'
import { useProject } from './useProject'
import { extractSdkResponseErrorMsg } from '~/utils'
import { useNuxtApp, useState } from '#app'
export function useMetas() {
@ -11,53 +13,63 @@ export function useMetas() {
const loadingState = useState<Record<string, boolean>>('metas-loading-state', () => ({}))
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => {
if (!force && metas.value[tableIdOrTitle]) return metas.value[tableIdOrTitle]
const modelId = (tables.value.find((t) => t.title === tableIdOrTitle || t.id === tableIdOrTitle) || {}).id
if (!modelId) {
console.warn(`Table '${tableIdOrTitle}' is not found in the table list`)
return null
}
if (!tableIdOrTitle) return null
/** wait until loading is finished if requesting same meta */
if (!force) {
if (!force && loadingState.value[tableIdOrTitle]) {
await new Promise((resolve) => {
let unwatch: WatchStopHandle
// set maximum 20sec timeout to wait loading meta
const timeout = setTimeout(() => {
unwatch?.()
clearTimeout(timeout)
resolve(null)
}, 20000)
}, 10000)
// watch for loading state change
unwatch = watch(
() => loadingState.value[modelId],
() => !!loadingState.value[tableIdOrTitle],
(isLoading) => {
if (!isLoading) {
clearTimeout(timeout)
resolve(null)
unwatch?.()
resolve(null)
}
},
{ immediate: true },
)
})
if (metas.value[modelId]) return metas.value[modelId]
if (metas.value[tableIdOrTitle]) {
return metas.value[tableIdOrTitle]
}
}
loadingState.value[tableIdOrTitle] = true
try {
if (!force && metas.value[tableIdOrTitle]) {
return metas.value[tableIdOrTitle]
}
loadingState.value[modelId] = true
const modelId = tableIdOrTitle.startsWith('md_') ? tableIdOrTitle : tables.value.find((t) => t.title === tableIdOrTitle)?.id
const model = await $api.dbTable.read(modelId)
metas.value = {
...metas.value,
[model.id!]: model,
[model.title]: model,
}
if (!modelId) {
console.warn(`Table '${tableIdOrTitle}' is not found in the table list`)
return null
}
loadingState.value[modelId] = false
const model = await $api.dbTable.read(modelId)
metas.value = {
...metas.value,
[model.id!]: model,
[model.title]: model,
}
return model
return model
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
delete loadingState.value[tableIdOrTitle]
}
return null
}
const clearAllMeta = () => {

32
packages/nc-gui-v2/composables/useProject.ts

@ -1,17 +1,20 @@
import { SqlUiFactory } from 'nocodb-sdk'
import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp, useState } from '#app'
import { useNuxtApp, useRoute, useState } from '#app'
import { USER_PROJECT_ROLES } from '~/lib'
export function useProject(projectId?: MaybeRef<string>) {
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const { $api } = useNuxtApp()
const _projectId = $computed(() => unref(projectId))
let _projectId = $ref('')
const project = useState<ProjectType>('project')
const tables = useState<TableType[]>('tables', () => [] as TableType[])
const route = useRoute()
// todo: refactor path param name and variable name
const projectType = $computed(() => route.params.projectType as string)
async function loadProjectRoles() {
projectRoles.value = {}
@ -29,18 +32,20 @@ export function useProject(projectId?: MaybeRef<string>) {
}
}
async function loadProject(id: string) {
project.value = await $api.project.read(id)
async function loadProject() {
if (unref(projectId)) {
_projectId = unref(projectId)!
} else if (projectType === 'base') {
const baseData = await $api.public.sharedBaseGet(route.params.projectId as string)
_projectId = baseData.project_id!
} else {
_projectId = route.params.projectId as string
}
project.value = await $api.project.read(_projectId!)
await loadProjectRoles()
await loadTables()
}
watchEffect(async () => {
if (_projectId) {
await loadProject(_projectId)
await loadTables()
}
})
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
const isMysql = computed(() => ['mysql', 'mysql2'].includes(projectBaseType))
@ -49,6 +54,7 @@ export function useProject(projectId?: MaybeRef<string>) {
const sqlUi = computed(
() => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>,
)
const isSharedBase = computed(() => projectType === 'base')
return { project, tables, loadProjectRoles, loadProject, loadTables, isMysql, isMssql, isPg, sqlUi }
return { project, tables, loadProjectRoles, loadProject, loadTables, isMysql, isMssql, isPg, sqlUi, isSharedBase }
}

2
packages/nc-gui-v2/composables/useTable.ts

@ -61,7 +61,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
$e('c:table:delete')
// 'Click Submit to Delete The table'
Modal.confirm({
title: `Click Yes to Delete The table : ${table.title}`,
title: `Do you want to delete the table : ${table.title}`,
okText: 'Yes',
okType: 'danger',
cancelText: 'No',

18
packages/nc-gui-v2/composables/useTabs.ts

@ -29,9 +29,11 @@ export function useTabs() {
const router = useRouter()
const { tables } = useProject()
const projectType = $computed(() => route.params.projectType as string)
const activeTabIndex: WritableComputedRef<number> = computed({
get() {
if ((route.name as string)?.startsWith('nc-projectId-index-index-type-title-viewTitle') && tables.value?.length) {
if ((route.name as string)?.startsWith('projectType-projectId-index-index-type-title-viewTitle') && tables.value?.length) {
const tab: Partial<TabItem> = { type: route.params.type as TabType, title: route.params.title as string }
const id = tables.value?.find((t) => t.title === tab.title)?.id
@ -56,7 +58,7 @@ export function useTabs() {
},
set(index: number) {
if (index === -1) {
navigateTo(`/nc/${route.params.projectId}`)
navigateTo(`/${projectType}/${route.params.projectId}`)
} else {
const tab = tabs.value[index]
@ -91,7 +93,7 @@ export function useTabs() {
let newTabIndex = index - 1
if (newTabIndex < 0 && tabs.value?.length > 1) newTabIndex = index + 1
if (newTabIndex === -1) {
await navigateTo(`/nc/${route.params.projectId}`)
await navigateTo(`/${projectType}/${route.params.projectId}`)
} else {
await navigateToTab(tabs.value?.[newTabIndex])
}
@ -102,11 +104,15 @@ export function useTabs() {
function navigateToTab(tab: TabItem) {
switch (tab.type) {
case TabType.TABLE:
return navigateTo(`/nc/${route.params.projectId}/table/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`)
return navigateTo(
`/${projectType}/${route.params.projectId}/table/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`,
)
case TabType.VIEW:
return navigateTo(`/nc/${route.params.projectId}/view/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`)
return navigateTo(
`/${projectType}/${route.params.projectId}/view/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`,
)
case TabType.AUTH:
return navigateTo(`/nc/${route.params.projectId}/auth`)
return navigateTo(`/${projectType}/${route.params.projectId}/auth`)
}
}

13
packages/nc-gui-v2/composables/useViewColumns.ts

@ -24,6 +24,8 @@ export function useViewColumns(
const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const loadViewColumns = async () => {
if (!meta || !view) return
@ -39,7 +41,6 @@ export function useViewColumns(
[curr.fk_column_id]: curr,
}
}, {})
fields.value = meta.value?.columns
?.map((column) => {
const currentColumnField = fieldById[column.id!] || {}
@ -88,10 +89,12 @@ export function useViewColumns(
}
const saveOrUpdate = async (field: any, index: number) => {
if (field.id && view?.value?.id) {
await $api.dbViewColumn.update(view.value.id, field.id, field)
} else if (view?.value?.id) {
if (fields.value) fields.value[index] = (await $api.dbViewColumn.create(view.value.id, field)) as any
if (isUIAllowed('fieldsSync')) {
if (field.id && view?.value?.id) {
await $api.dbViewColumn.update(view.value.id, field.id, field)
} else if (view?.value?.id) {
if (fields.value) fields.value[index] = (await $api.dbViewColumn.create(view.value.id, field)) as any
}
}
reloadData?.()

21
packages/nc-gui-v2/composables/useViewSorts.ts

@ -10,17 +10,22 @@ export function useViewSorts(
const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const loadSorts = async () => {
if (!view?.value) return
sorts.value = ((await $api.dbTableSort.list(view?.value?.id as string)) as any)?.sorts?.list
}
const saveOrUpdate = async (sort: SortType, i: number) => {
if (!sorts?.value) return
if (sort.id) {
await $api.dbTableSort.update(sort.id, sort)
} else {
sorts.value[i] = (await $api.dbTableSort.create(view?.value?.id as string, sort)) as any
// TODO:
// if (!this.shared && this._isUIAllowed('sortSync')) {
if (isUIAllowed('sortSync')) {
if (sort.id) {
await $api.dbTableSort.update(sort.id, sort)
} else {
sorts.value[i] = (await $api.dbTableSort.create(view?.value?.id as string, sort)) as any
}
}
reloadData?.()
}
@ -31,11 +36,13 @@ export function useViewSorts(
}
const deleteSort = async (sort: SortType, i: number) => {
// TOOD:
// if (!this.shared && sort.id && this._isUIAllowed('sortSync')) {
if (sort.id) {
if (isUIAllowed('sortSync') && sort.id) {
await $api.dbTableSort.delete(sort.id)
} else {
sorts.value.splice(i, 1)
}
sorts.value.splice(i, 1)
}
return { sorts, loadSorts, addSort, deleteSort, saveOrUpdate }
}

4
packages/nc-gui-v2/context/index.ts

@ -5,7 +5,6 @@ import type { useViewData } from '#imports'
import type { Row } from '~/composables'
import type { TabItem } from '~/composables/useTabs'
export const EditEnabledInj: InjectionKey<boolean> = Symbol('edit-enabled')
export const ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell')
export const RowInj: InjectionKey<Ref<Row>> = Symbol('row')
export const ColumnInj: InjectionKey<Ref<ColumnType & { meta: any }>> = Symbol('column-injection')
@ -23,6 +22,5 @@ export const ReadonlyInj: InjectionKey<any> = Symbol('readonly-injection')
export const ReloadViewDataHookInj: InjectionKey<EventHook<void>> = Symbol('reload-view-data-injection')
export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection')
export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection')
export const RightSidebarInj: InjectionKey<Ref<boolean>> = Symbol('right-sidebar-injection')
export const EditModeInj: InjectionKey<ComputedRef<boolean>> = Symbol('edit-mode-injection')
export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injection')

142
packages/nc-gui-v2/layouts/base.vue

@ -4,6 +4,8 @@ import { computed, useGlobal, useRoute } from '#imports'
const { signOut, signedIn, isLoading, user } = useGlobal()
const { isSharedBase } = useProject()
const route = useRoute()
const email = computed(() => user.value?.email ?? '---')
@ -19,65 +21,79 @@ const logout = () => {
<div id="nc-sidebar-left" />
<a-layout class="!flex-col">
<a-layout-header class="flex !bg-primary items-center text-white pl-1 pr-4 shadow-lg">
<div
v-if="route.name === 'index' || route.name === 'project-index-create' || route.name === 'project-index-create-external'"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105"
@click="navigateTo('/')"
>
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<div class="flex justify-center">
<div v-show="isLoading" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
<Transition name="layout">
<a-layout-header v-if="signedIn" class="flex !bg-primary items-center text-white pl-4 pr-5 shadow-lg">
<div
v-if="
route.name === 'index' || route.name === 'project-index-create' || route.name === 'project-index-create-external'
"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')"
>
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
</div>
<div class="flex-1" />
<div class="flex justify-center">
<div v-show="isLoading" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }}
<a-tooltip placement="left">
<template #title> Switch language </template>
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
</div>
</div>
<div class="flex pr-4 items-center">
<GeneralLanguage class="cursor-pointer text-2xl" />
<div class="flex-1" />
<a-tooltip placement="left">
<template #title> Switch language </template>
<div class="flex pr-4 items-center">
<GeneralLanguage class="cursor-pointer text-2xl" />
</div>
</a-tooltip>
<template v-if="signedIn && !isSharedBase">
<a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />
<template #overlay>
<a-menu class="!py-0 nc-user-menu dark:(!bg-gray-800) leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" />&nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout">
<MdiLogout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-layout-header>
</Transition>
<a-tooltip>
<template #title> Switch language </template>
<Transition name="layout">
<div v-if="!signedIn" class="nc-lang-btn">
<GeneralLanguage />
</div>
</a-tooltip>
<template v-if="signedIn">
<a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />
<template #overlay>
<a-menu class="!py-0 nc-user-menu dark:(!bg-gray-800) leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" />&nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout">
<MdiLogout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-layout-header>
<div class="w-full overflow-hidden" style="height: calc(100% - var(--header-height))">
</Transition>
</a-tooltip>
<div class="w-full h-full overflow-hidden">
<slot />
</div>
</a-layout>
@ -92,4 +108,22 @@ const logout = () => {
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
.nc-lang-btn {
@apply color-transition flex items-center justify-center fixed bottom-10 right-10 z-99 w-12 h-12 rounded-full shadow-md shadow-gray-500 p-2 !bg-primary text-white active:(ring ring-pink-500) hover:(ring ring-pink-500);
&::after {
@apply rounded-full absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
content: '';
z-index: -1;
}
&:hover::after {
@apply transform scale-110 ring ring-pink-500;
}
&:active::after {
@apply ring ring-pink-500;
}
}
</style>

2
packages/nc-gui-v2/lib/types.ts

@ -6,6 +6,8 @@ export interface User {
firstname: string | null
lastname: string | null
roles: Roles
invite_token?: string
project_id?: string
}
export type Roles = Record<Role, boolean>

3
packages/nc-gui-v2/middleware/auth.global.ts

@ -23,6 +23,9 @@ import { useGlobal } from '#imports'
export default defineNuxtRouteMiddleware((to, from) => {
const state = useGlobal()
/** if shred base allow without validating */
if (to.params?.projectType === 'base') return
/** if auth is required or unspecified (same as required) and user is not signed in, redirect to signin page */
if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) {
return navigateTo('/signin')

1
packages/nc-gui-v2/package.json

@ -18,6 +18,7 @@
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"jsep": "^1.3.6",
"just-clone": "^6.1.1",
"jwt-decode": "^3.1.2",
"locale-codes": "^1.3.1",
"monaco-editor": "^0.33.0",

64
packages/nc-gui-v2/pages/nc/[projectId]/index.vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue

@ -5,7 +5,7 @@ import { openLink } from '~/utils'
const route = useRoute()
const { project, loadProject, loadTables } = useProject(route.params.projectId as string)
const { project, loadProject, loadTables, isSharedBase } = useProject()
const { addTab, clearTabs } = useTabs()
@ -30,7 +30,7 @@ onKeyStroke(
clearTabs()
if (!route.params.type) {
if (!route.params.type && isUIAllowed('teamAndAuth')) {
addTab({ type: TabType.AUTH, title: 'Team & Auth' })
}
@ -39,7 +39,7 @@ function toggleDialog(value?: boolean, key?: string) {
openDialogKey.value = key
}
await loadProject(route.params.projectId as string)
await loadProject()
await loadTables()
</script>
@ -56,20 +56,37 @@ await loadTables()
collapsible
theme="light"
>
<div style="height: var(--header-height)" class="flex items-center !bg-primary text-white px-1 gap-2">
<div style="height: var(--header-height)" class="flex items-center !bg-primary text-white px-1 pl-5 gap-2">
<div
v-if="isOpen"
v-if="isOpen && !isSharedBase"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105"
@click="navigateTo('/')"
>
<img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<a
v-if="isOpen && isSharedBase"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105"
href="https://github.com/nocodb/nocodb"
target="_blank"
>
<img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</a>
<div v-if="isSharedBase">
<template v-if="isOpen">
<div class="text-xl font-semibold truncate">{{ project.title }}</div>
</template>
<template v-else>
<MdiFolder class="text-primary cursor-pointer transform hover:scale-105 text-2xl" />
</template>
</div>
<a-dropdown v-model:visible="dropdownOpen" :trigger="['click']">
<a-dropdown v-else :trigger="['click']" placement="bottom">
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"
class="group cursor-pointer flex gap-4 items-center"
class="group cursor-pointer flex gap-4 items-center nc-project-menu"
>
<template v-if="isOpen">
<div class="text-xl font-semibold truncate">{{ project.title }}</div>
@ -102,7 +119,7 @@ await loadTables()
<a-menu-item key="copy">
<div class="nc-project-menu-item group">
<MdiContentCopy class="group-hover:text-pink-500" />
<MdiContentCopy class="group-hover:text-pink-500 nc-copy-project-info" />
Copy Project Info
</div>
</a-menu-item>
@ -114,7 +131,7 @@ await loadTables()
class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`)"
>
<MdiApi class="group-hover:text-pink-500" />
<MdiApi class="group-hover:text-pink-500 nc-swagger-api-docs" />
Swagger: Rest APIs
</div>
</a-menu-item>
@ -123,58 +140,58 @@ await loadTables()
<a-menu-item key="teamAndAuth">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
v-if="isUIAllowed('teamAndAuth')"
v-t="['c:navdraw:team-and-auth']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')"
>
<MdiAccountGroup class="group-hover:text-pink-500" />
<MdiAccountGroup class="group-hover:text-pink-500 nc-team-and-auth" />
Team & Auth
</div>
</a-menu-item>
<a-menu-item key="appStore">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
v-if="isUIAllowed('appStore')"
v-t="['c:navdraw:app-store']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'appStore')"
>
<MdiStore class="group-hover:text-pink-500" />
<MdiStore class="group-hover:text-pink-500 nc-app-store" />
App Store
</div>
</a-menu-item>
<a-menu-item key="metaData">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
v-if="isUIAllowed('projectMetadata')"
v-t="['c:navdraw:project-metadata']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'metaData')"
>
<MdiTableBorder class="group-hover:text-pink-500" />
<MdiTableBorder class="group-hover:text-pink-500 nc-meta-data" />
Project Metadata
</div>
</a-menu-item>
<a-menu-item key="audit">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
v-if="isUIAllowed('audit')"
v-t="['c:navdraw:audit']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'audit')"
>
<MdiNotebookCheckOutline class="group-hover:text-pink-500" />
<MdiNotebookCheckOutline class="group-hover:text-pink-500 nc-audit" />
Audit
</div>
</a-menu-item>
<a-menu-divider />
<a-sub-menu key="preview-as">
<a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as" v-t="['c:navdraw:preview-as']">
<template #title>
<div class="nc-project-menu-item group">
<MdiContentCopy class="group-hover:text-pink-500" />
<MdiContentCopy class="group-hover:text-pink-500 nc-project-preview" />
Preview Project As
<div class="flex-1" />
@ -220,7 +237,6 @@ await loadTables()
</template>
<dashboard-settings-modal v-model="dialogOpen" :open-key="openDialogKey" />
<NuxtPage />
<GeneralPreviewAs float />

44
packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue

@ -46,7 +46,7 @@ const icon = (tab: TabItem) => {
<div class="h-full w-full nc-container pt-[9px]">
<div class="h-full w-full flex flex-col">
<div>
<a-tabs v-model:activeKey="activeTabIndex" type="editable-card" @edit="closeTab">
<a-tabs v-model:activeKey="activeTabIndex" class="nc-root-tabs" type="editable-card" @edit="closeTab">
<a-tab-pane v-for="(tab, i) in tabs" :key="i">
<template #tab>
<div class="flex align-center gap-2">
@ -57,7 +57,7 @@ const icon = (tab: TabItem) => {
</a-tab-pane>
<template #leftExtra>
<a-menu v-model:selectedKeys="currentMenu" class="border-0" mode="horizontal">
<a-menu v-if="isUIAllowed('addOrImport')" v-model:selectedKeys="currentMenu" class="border-0" mode="horizontal">
<a-sub-menu key="addORImport">
<template #title>
<div class="text-sm flex items-center gap-2 pt-[8px] pb-3">
@ -144,8 +144,9 @@ const icon = (tab: TabItem) => {
</template>
</a-tabs>
</div>
<NuxtPage />
<div class="w-full min-h-[300px] flex-grow">
<NuxtPage />
</div>
</div>
<DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" />
@ -154,14 +155,29 @@ const icon = (tab: TabItem) => {
</div>
</template>
<style scoped>
<style scoped lang="scss">
.nc-container {
height: calc(100% - var(--header-height));
height: calc(100vh - var(--header-height));
flex: 1 1 100%;
}
:deep(.ant-tabs-nav) {
@apply !mb-0;
:deep(.nc-root-tabs) {
& > .ant-tabs-nav {
@apply !mb-0;
& > .ant-tabs-nav-wrap > .ant-tabs-nav-list {
& > .ant-tabs-nav-add {
@apply !hidden;
}
& > .ant-tabs-tab-active {
@apply font-weight-medium;
}
& > .ant-tabs-tab:not(.ant-tabs-tab-active) {
@apply bg-gray-100 text-gray-500;
}
}
}
}
:deep(.ant-menu-item-selected) {
@ -173,16 +189,4 @@ const icon = (tab: TabItem) => {
:deep(.ant-menu-submenu::after) {
@apply !border-none;
}
:deep(.ant-tabs-nav-add) {
@apply !hidden;
}
:deep(.ant-tabs-tab-active) {
@apply font-weight-medium;
}
:deep(.ant-tabs-tab:not(.ant-tabs-tab-active)) {
@apply bg-gray-100;
}
</style>

0
packages/nc-gui-v2/pages/nc/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue

0
packages/nc-gui-v2/pages/nc/[projectId]/index/index/auth.vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/auth.vue

0
packages/nc-gui-v2/pages/nc/[projectId]/index/index/index.vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/index.vue

73
packages/nc-gui-v2/pages/forgot-password.vue

@ -1,14 +1,7 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { definePageMeta } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { useNuxtApp } from '#app'
import { isEmail } from '~/utils/validation'
import MdiLogin from '~icons/mdi/login'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import ClaritySuccessLine from '~icons/clarity/success-line'
import { definePageMeta, extractSdkResponseErrorMsg, isEmail, reactive, ref, useApi, useI18n } from '#imports'
const { $api } = $(useNuxtApp())
const { api, isLoading } = useApi()
const { t } = useI18n()
@ -18,6 +11,7 @@ definePageMeta({
})
let error = $ref<string | null>(null)
let success = $ref(false)
const formValidator = ref()
@ -43,13 +37,14 @@ const formRules = {
],
}
const resetPassword = async () => {
const valid = formValidator.value.validate()
if (!valid) return
async function resetPassword() {
if (!formValidator.value.validate()) return
resetError()
error = null
try {
await $api.auth.passwordForgot(form)
await api.auth.passwordForgot(form)
success = true
} catch (e: any) {
// todo: errors should not expose what was wrong (i.e. do not show "Password is wrong" messages)
@ -57,10 +52,8 @@ const resetPassword = async () => {
}
}
const resetError = () => {
if (error) {
error = null
}
function resetError() {
if (error) error = null
}
</script>
@ -70,34 +63,41 @@ const resetError = () => {
ref="formValidator"
layout="vertical"
:model="form"
class="forgot-password h-full min-h-[600px] flex justify-center items-center"
class="bg-primary/5 forgot-password h-full min-h-[600px] flex justify-center items-center"
@finish="resetPassword"
>
<div class="h-full w-full flex flex-col flex-wrap justify-center items-center">
<div
class="color-transition bg-white dark:(!bg-gray-900 !text-white) relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<general-noco-icon />
<general-noco-icon
class="color-transition hover:(ring ring-pink-500)"
:class="[isLoading ? 'animated-bg-gradient' : '']"
/>
<div class="self-center flex flex-col justify-center items-center text-center gap-4">
<div class="self-center flex flex-col justify-center items-center text-center gap-2">
<h1 class="prose-2xl font-bold my-4 w-full">{{ $t('title.resetPassword') }}</h1>
<template v-if="!success">
<p class="prose-sm">{{ $t('msg.info.passwordRecovery.message_1') }}</p>
<p class="prose-sm mb-4">{{ $t('msg.info.passwordRecovery.message_2') }}</p>
<div class="prose-sm">{{ $t('msg.info.passwordRecovery.message_1') }}</div>
<div class="prose-sm mb-4">{{ $t('msg.info.passwordRecovery.message_2') }}</div>
</template>
<template v-else>
<p class="prose-sm text-success flex items-center leading-8 gap-2">
<div class="prose-sm text-success flex items-center leading-8 gap-2">
{{ $t('msg.info.passwordRecovery.success') }} <ClaritySuccessLine />
</p>
</div>
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</template>
</div>
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div class="flex items-center gap-2 justify-center"><MaterialSymbolsWarning /> {{ error }}</div>
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div style="flex: 0 0 auto" class="break-words">{{ error }}</div>
</div>
</div>
</Transition>
@ -105,10 +105,11 @@ const resetError = () => {
<a-input v-model:value="form.email" size="large" :placeholder="$t('labels.email')" @focus="resetError" />
</a-form-item>
<div class="self-center flex flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full">
<div class="self-center flex flex-col gap-4 items-center justify-center w-full">
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiLogin /> {{ $t('activity.sendEmail') }}</span>
</button>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
@ -128,7 +129,21 @@ const resetError = () => {
}
.submit {
@apply ml-1 border border-gray-300 rounded-lg p-4 bg-gray-100/50 text-white bg-primary hover:bg-primary/75 dark:(!bg-secondary/75 hover:!bg-secondary/50);
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white bg-primary;
&::after {
@apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
content: '';
z-index: -1;
}
&:hover::after {
@apply transform scale-110 ring ring-pink-500;
}
&:active::after {
@apply ring ring-pink-500;
}
}
}
</style>

11
packages/nc-gui-v2/pages/index/index.vue

@ -15,6 +15,8 @@ const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
const { isUIAllowed } = useUIPermission()
useSidebar({ hasSidebar: true, isOpen: true })
const filterQuery = ref('')
@ -34,6 +36,7 @@ const filteredProjects = computed(
)
const deleteProject = (project: ProjectType) => {
$e('c:project:delete')
Modal.confirm({
title: `Do you want to delete '${project.title}' project?`,
okText: 'Yes',
@ -41,8 +44,8 @@ const deleteProject = (project: ProjectType) => {
cancelText: 'No',
async onOk() {
try {
$e('c:project:delete')
await api.project.delete(project.id as string)
$e('a:project:delete')
return projects.value?.splice(projects.value.indexOf(project), 1)
} catch (e: any) {
return message.error(await extractSdkResponseErrorMsg(e))
@ -78,7 +81,7 @@ onMounted(() => {
></a-input-search>
<div class="flex-grow"></div>
<a-dropdown @click.stop>
<a-dropdown v-if="isUIAllowed('projectCreate', true)" @click.stop>
<a-button class="nc-new-project-menu !shadow">
<div class="flex align-center">
{{ $t('title.newProj') }}
@ -126,7 +129,7 @@ onMounted(() => {
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
>
<!-- Title -->
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text }">
<div class="capitalize !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap nc-project-row" :title="text">
@ -134,7 +137,7 @@ onMounted(() => {
</div>
</template>
</a-table-column>
<!-- Actions -->
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div class="flex align-center">

96
packages/nc-gui-v2/pages/signin.vue

@ -1,14 +1,21 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { RuleObject } from 'ant-design-vue/es/form'
import { definePageMeta, useSidebar } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { navigateTo, useNuxtApp } from '#app'
import { isEmail } from '~/utils/validation'
import MdiLogin from '~icons/mdi/login'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
const { $api, $state } = $(useNuxtApp())
import {
definePageMeta,
extractSdkResponseErrorMsg,
isEmail,
navigateTo,
reactive,
ref,
useApi,
useGlobal,
useI18n,
useSidebar,
} from '#imports'
const { signIn: _signIn } = useGlobal()
const { api, isLoading } = useApi()
const { t } = useI18n()
@ -49,25 +56,25 @@ const formRules: Record<string, RuleObject[]> = {
],
}
const signIn = async () => {
const valid = formValidator.value.validate()
if (!valid) return
error = null
try {
const { token } = await $api.auth.signin(form)
$state.signIn(token!)
await navigateTo('/')
} catch (e: any) {
// todo: errors should not expose what was wrong (i.e. do not show "Password is wrong" messages)
error = await extractSdkResponseErrorMsg(e)
}
async function signIn() {
if (!formValidator.value.validate()) return
resetError()
api.auth
.signin(form)
.then(async ({ token }) => {
_signIn(token!)
await navigateTo('/')
})
.catch(async (err) => {
// todo: errors should not expose what was wrong (i.e. do not show "Password is wrong" messages)
error = await extractSdkResponseErrorMsg(err)
})
}
const resetError = () => {
if (error) {
error = null
}
function resetError() {
if (error) error = null
}
</script>
@ -77,20 +84,26 @@ const resetError = () => {
ref="formValidator"
:model="form"
layout="vertical"
class="signin h-[calc(100%_+_90px)] min-h-[600px] flex justify-center items-center nc-form-signin"
class="bg-primary/5 signin h-full min-h-[600px] flex justify-center items-center nc-form-signin"
@finish="signIn"
>
<div class="h-full w-full flex flex-col flex-wrap items-center pt-[100px]">
<div
class="bg-white dark:(!bg-gray-900 !text-white) relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<general-noco-icon />
<general-noco-icon
class="!rounded-full color-transition hover:(ring ring-pink-500)"
:class="[isLoading ? 'animated-bg-gradient' : '']"
/>
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signIn') }}</h1>
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div class="flex items-center gap-2 justify-center"><MaterialSymbolsWarning /> {{ error }}</div>
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div style="flex: 0 0 auto" class="break-words">{{ error }}</div>
</div>
</div>
</Transition>
@ -108,18 +121,17 @@ const resetError = () => {
/>
</a-form-item>
<div class="hidden md:block self-end mx-8">
<div class="hidden md:block self-end">
<nuxt-link class="prose-sm" to="/forgot-password">
{{ $t('msg.info.signUp.forgotPassword') }}
</nuxt-link>
</div>
<div
class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full"
>
<div class="self-center flex flex-col flex-wrap gap-4 items-center mt-4 justify-center">
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiLogin /> {{ $t('general.signIn') }}</span>
</button>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.dontHaveAccount') }}
<nuxt-link to="/signup">{{ $t('general.signUp') }}</nuxt-link>
@ -155,7 +167,21 @@ const resetError = () => {
}
.submit {
@apply ml-1 border border-gray-300 rounded-lg p-4 bg-gray-100/50 text-white bg-primary hover:bg-primary/75 dark:(!bg-secondary/75 hover:!bg-secondary/50);
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white bg-primary;
&::after {
@apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
content: '';
z-index: -1;
}
&:hover::after {
@apply transform scale-110 ring ring-pink-500;
}
&:active::after {
@apply ring ring-pink-500;
}
}
}
</style>

143
packages/nc-gui-v2/pages/signup.vue

@ -1,143 +0,0 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { navigateTo } from '#app'
import { isEmail } from '~/utils/validation'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
const { $api, $state } = useNuxtApp()
const { t } = useI18n()
definePageMeta({
requiresAuth: false,
})
const formValidator = ref()
let error = $ref<string | null>(null)
const form = reactive({
email: '',
password: '',
})
const formRules = {
email: [
// E-mail is required
{ required: true, message: t('msg.error.signUpRules.emailReqd') },
// E-mail must be valid format
{
validator: (_: unknown, v: string) => {
return new Promise((resolve, reject) => {
if (isEmail(v)) return resolve(true)
reject(new Error(t('msg.error.signUpRules.emailInvalid')))
})
},
message: t('msg.error.signUpRules.emailInvalid'),
},
],
password: [
// Password is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
{ min: 8, message: t('msg.error.signUpRules.passwdLength') },
],
}
const signUp = async () => {
const valid = formValidator.value.validate()
if (!valid) return
error = null
try {
const { token } = await $api.auth.signup(form)
$state.signIn(token!)
await navigateTo('/')
} catch (e: any) {
error = await extractSdkResponseErrorMsg(e)
}
}
const resetError = () => {
if (error) {
error = null
}
}
</script>
<template>
<NuxtLayout>
<a-form
ref="formValidator"
:model="form"
layout="vertical"
class="signup h-[calc(100%_+_90px)] min-h-[600px] flex justify-center items-center nc-form-signup"
@finish="signUp"
>
<div class="h-full w-full flex flex-col flex-wrap pt-[100px]">
<div
class="bg-white dark:(!bg-gray-900 !text-white) relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<general-noco-icon />
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signUp') }}</h1>
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div class="flex items-center gap-2 justify-center"><MaterialSymbolsWarning /> {{ error }}</div>
</div>
</Transition>
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input v-model:value="form.email" size="large" :placeholder="$t('labels.email')" @focus="resetError" />
</a-form-item>
<a-form-item :label="$t('labels.password')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
size="large"
class="password"
:placeholder="$t('labels.password')"
@focus="resetError"
/>
</a-form-item>
<div
class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full"
>
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MaterialSymbolsRocketLaunchOutline /> {{ $t('general.signUp') }}</span>
</button>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</div>
</div>
</div>
</div>
</a-form>
</NuxtLayout>
</template>
<style lang="scss">
.signup {
.ant-input-affix-wrapper,
.ant-input {
@apply dark:(bg-gray-700 !text-white) !appearance-none my-1 border-1 border-solid border-primary/50 rounded;
}
.password {
input {
@apply !border-none;
}
.ant-input-password-icon {
@apply dark:!text-white;
}
}
.submit {
@apply ml-1 border border-gray-300 rounded-lg p-4 bg-gray-100/50 text-white bg-primary hover:bg-primary/75 dark:(!bg-secondary/75 hover:!bg-secondary/50);
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save