Browse Source

refactor(nc-gui): use lazy load and cleanup imports

pull/3801/head
braks 2 years ago
parent
commit
4c29fa18ff
  1. 29
      packages/nc-gui/components/api-client/Headers.vue
  2. 25
      packages/nc-gui/components/api-client/Params.vue
  3. 5
      packages/nc-gui/components/cell/Currency.vue
  4. 2
      packages/nc-gui/components/cell/DateTimePicker.vue
  5. 4
      packages/nc-gui/components/cell/Decimal.vue
  6. 14
      packages/nc-gui/components/cell/Duration.vue
  7. 5
      packages/nc-gui/components/cell/Email.vue
  8. 4
      packages/nc-gui/components/cell/Float.vue
  9. 4
      packages/nc-gui/components/cell/Integer.vue
  10. 3
      packages/nc-gui/components/cell/Json.vue
  11. 1
      packages/nc-gui/components/cell/MultiSelect.vue
  12. 61
      packages/nc-gui/components/cell/Percent.vue
  13. 6
      packages/nc-gui/components/cell/PhoneNumber.vue
  14. 3
      packages/nc-gui/components/cell/Rating.vue
  15. 1
      packages/nc-gui/components/cell/Text.vue
  16. 4
      packages/nc-gui/components/cell/TextArea.vue
  17. 4
      packages/nc-gui/components/cell/TimePicker.vue
  18. 7
      packages/nc-gui/components/cell/Url.vue
  19. 6
      packages/nc-gui/components/cell/attachment/index.vue
  20. 5
      packages/nc-gui/components/cell/attachment/utils.ts
  21. 28
      packages/nc-gui/components/dashboard/TreeView.vue
  22. 30
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  23. 9
      packages/nc-gui/components/dashboard/settings/AuditTab.vue
  24. 25
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  25. 8
      packages/nc-gui/components/dashboard/settings/Misc.vue
  26. 31
      packages/nc-gui/components/dashboard/settings/Modal.vue
  27. 26
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  28. 35
      packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue
  29. 15
      packages/nc-gui/components/dlg/AirtableImport.vue
  30. 7
      packages/nc-gui/components/dlg/QuickImport.vue
  31. 11
      packages/nc-gui/components/dlg/TableCreate.vue
  32. 38
      packages/nc-gui/components/dlg/TableRename.vue
  33. 22
      packages/nc-gui/components/dlg/ViewCreate.vue
  34. 21
      packages/nc-gui/components/dlg/ViewDelete.vue
  35. 2
      packages/nc-gui/components/general/ChromeWrapper.vue
  36. 2
      packages/nc-gui/components/general/ColorModeSwitcher.vue
  37. 4
      packages/nc-gui/components/general/ColorPicker.vue
  38. 5
      packages/nc-gui/components/general/FlippingCard.vue
  39. 3
      packages/nc-gui/components/general/FullScreen.vue
  40. 10
      packages/nc-gui/components/general/HelpAndSupport.vue
  41. 3
      packages/nc-gui/components/general/MiniSidebar.vue
  42. 23
      packages/nc-gui/components/general/ReleaseInfo.vue
  43. 2
      packages/nc-gui/components/general/Share.vue
  44. 3
      packages/nc-gui/components/general/ShareBaseButton.vue
  45. 10
      packages/nc-gui/components/general/Social.vue
  46. 9
      packages/nc-gui/components/general/SocialCard.vue
  47. 4
      packages/nc-gui/components/general/Tooltip.vue
  48. 32
      packages/nc-gui/components/general/TruncateText.vue
  49. 2
      packages/nc-gui/components/smartsheet/Form.vue
  50. 2
      packages/nc-gui/components/smartsheet/Gallery.vue
  51. 2
      packages/nc-gui/components/smartsheet/Grid.vue
  52. 2
      packages/nc-gui/components/smartsheet/Row.vue
  53. 2
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  54. 6
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  55. 2
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  56. 2
      packages/nc-gui/components/smartsheet/header/Menu.vue
  57. 3
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  58. 15
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  59. 9
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  60. 2
      packages/nc-gui/components/smartsheet/toolbar/SharedViewList.vue
  61. 6
      packages/nc-gui/components/tabs/Auth.vue
  62. 4
      packages/nc-gui/components/tabs/Smartsheet.vue
  63. 41
      packages/nc-gui/components/tabs/auth/ApiTokenManagement.vue
  64. 19
      packages/nc-gui/components/tabs/auth/UserManagement.vue
  65. 3
      packages/nc-gui/components/tabs/auth/user-management/FeedbackForm.vue
  66. 15
      packages/nc-gui/components/tabs/auth/user-management/ShareBase.vue
  67. 24
      packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue
  68. 8
      packages/nc-gui/components/template/Editor.vue
  69. 21
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  70. 2
      packages/nc-gui/components/virtual-cell/Count.vue
  71. 5
      packages/nc-gui/components/virtual-cell/Formula.vue
  72. 27
      packages/nc-gui/components/virtual-cell/HasMany.vue
  73. 6
      packages/nc-gui/components/virtual-cell/Lookup.vue
  74. 24
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  75. 15
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  76. 21
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  77. 15
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  78. 2
      packages/nc-gui/components/webhook/ChannelMultiSelect.vue
  79. 5
      packages/nc-gui/components/webhook/Drawer.vue
  80. 37
      packages/nc-gui/components/webhook/Editor.vue
  81. 11
      packages/nc-gui/components/webhook/List.vue
  82. 5
      packages/nc-gui/components/webhook/Test.vue
  83. 28
      packages/nc-gui/composables/index.ts
  84. 3
      packages/nc-gui/composables/useApi/index.ts
  85. 4
      packages/nc-gui/composables/useColors.ts
  86. 17
      packages/nc-gui/composables/useColumn.ts
  87. 22
      packages/nc-gui/composables/useColumnCreateStore.ts
  88. 2
      packages/nc-gui/composables/useCopy.ts
  89. 3
      packages/nc-gui/composables/useDashboard.ts
  90. 2
      packages/nc-gui/composables/useDialog/index.ts
  91. 52
      packages/nc-gui/composables/useExpandedFormStore.ts
  92. 14
      packages/nc-gui/composables/useGlobal/actions.ts
  93. 6
      packages/nc-gui/composables/useGlobal/state.ts
  94. 12
      packages/nc-gui/composables/useGridViewColumnWidth.ts
  95. 43
      packages/nc-gui/composables/useLTARStore.ts
  96. 11
      packages/nc-gui/composables/useMetas.ts
  97. 4
      packages/nc-gui/composables/useProject.ts
  98. 2
      packages/nc-gui/composables/useSharedFormViewStore.ts
  99. 13
      packages/nc-gui/composables/useSharedView.ts
  100. 31
      packages/nc-gui/composables/useSidebar/index.ts
  101. Some files were not shown because too many files have changed in this diff Show More

29
packages/nc-gui/components/api-client/Headers.vue

@ -1,18 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import MdiPlusIcon from '~icons/mdi/plus' import { useVModel } from '#imports'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
interface Props { const props = defineProps<{
modelValue: Record<string, any>[] modelValue: any[]
} }>()
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
const headerList = ref([ const headerList = [
'A-IM', 'A-IM',
'Accept', 'Accept',
'Accept-Charset', 'Accept-Charset',
@ -52,11 +49,11 @@ const headerList = ref([
'Dnt', 'Dnt',
'X-Requested-With', 'X-Requested-With',
'X-CSRF-Token', 'X-CSRF-Token',
]) ]
const addHeaderRow = () => vModel.value.push({}) const addHeaderRow = () => vModel.value.push({})
const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1) const deleteHeaderRow = (i: number) => vModel.value.splice(i, 1)
</script> </script>
<template> <template>
@ -67,17 +64,21 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
<th> <th>
<!-- Intended to be empty - For checkbox --> <!-- Intended to be empty - For checkbox -->
</th> </th>
<th> <th>
<div class="text-center font-normal mb-2">Header Name</div> <div class="text-center font-normal mb-2">Header Name</div>
</th> </th>
<th> <th>
<div class="text-center font-normal mb-2">Value</div> <div class="text-center font-normal mb-2">Value</div>
</th> </th>
<th> <th>
<!-- Intended to be empty - For delete button --> <!-- Intended to be empty - For delete button -->
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(headerRow, idx) in vModel" :key="idx"> <tr v-for="(headerRow, idx) in vModel" :key="idx">
<td class="px-2 nc-hook-header-tab-checkbox"> <td class="px-2 nc-hook-header-tab-checkbox">
@ -85,6 +86,7 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
<a-checkbox v-model:checked="headerRow.enabled" /> <a-checkbox v-model:checked="headerRow.enabled" />
</a-form-item> </a-form-item>
</td> </td>
<td class="px-2 w-min-[400px]"> <td class="px-2 w-min-[400px]">
<a-form-item> <a-form-item>
<a-select <a-select
@ -101,22 +103,25 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
</a-select> </a-select>
</a-form-item> </a-form-item>
</td> </td>
<td class="px-2 w-min-[400px]"> <td class="px-2 w-min-[400px]">
<a-form-item> <a-form-item>
<a-input v-model:value="headerRow.value" size="large" placeholder="Value" class="nc-input-hook-header-value" /> <a-input v-model:value="headerRow.value" size="large" placeholder="Value" class="nc-input-hook-header-value" />
</a-form-item> </a-form-item>
</td> </td>
<td class="relative"> <td class="relative">
<div v-if="idx !== 0" class="absolute flex flex-col justify-start mt-2 -right-6 top-0"> <div v-if="idx !== 0" class="absolute flex flex-col justify-start mt-2 -right-6 top-0">
<MdiDeleteOutlineIcon class="cursor-pointer" @click="deleteHeaderRow(idx)" /> <MdiDeleteOutline class="cursor-pointer" @click="deleteHeaderRow(idx)" />
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td :colspan="12" class="text-center"> <td :colspan="12" class="text-center">
<a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addHeaderRow"> <a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addHeaderRow">
<template #icon> <template #icon>
<MdiPlusIcon class="flex mx-auto" /> <MdiPlus class="flex mx-auto" />
</template> </template>
</a-button> </a-button>
</td> </td>

25
packages/nc-gui/components/api-client/Params.vue

@ -1,12 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import MdiPlusIcon from '~icons/mdi/plus' import { useVModel } from '#imports'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
interface Props { const props = defineProps<{
modelValue: Record<string, any>[] modelValue: any[]
} }>()
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
@ -14,7 +11,7 @@ const vModel = useVModel(props, 'modelValue', emits)
const addParamRow = () => vModel.value.push({}) const addParamRow = () => vModel.value.push({})
const deleteParamRow = (idx: number) => vModel.value.splice(idx, 1) const deleteParamRow = (i: number) => vModel.value.splice(i, 1)
</script> </script>
<template> <template>
@ -25,17 +22,21 @@ const deleteParamRow = (idx: number) => vModel.value.splice(idx, 1)
<th> <th>
<!-- Intended to be empty - For checkbox --> <!-- Intended to be empty - For checkbox -->
</th> </th>
<th> <th>
<div class="text-center font-normal mb-2">Param Name</div> <div class="text-center font-normal mb-2">Param Name</div>
</th> </th>
<th> <th>
<div class="text-center font-normal mb-2">Value</div> <div class="text-center font-normal mb-2">Value</div>
</th> </th>
<th> <th>
<!-- Intended to be empty - For delete button --> <!-- Intended to be empty - For delete button -->
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(paramRow, idx) in vModel" :key="idx"> <tr v-for="(paramRow, idx) in vModel" :key="idx">
<td class="px-2"> <td class="px-2">
@ -43,27 +44,31 @@ const deleteParamRow = (idx: number) => vModel.value.splice(idx, 1)
<a-checkbox v-model:checked="paramRow.enabled" /> <a-checkbox v-model:checked="paramRow.enabled" />
</a-form-item> </a-form-item>
</td> </td>
<td class="px-2"> <td class="px-2">
<a-form-item> <a-form-item>
<a-input v-model:value="paramRow.name" size="large" placeholder="Key" /> <a-input v-model:value="paramRow.name" size="large" placeholder="Key" />
</a-form-item> </a-form-item>
</td> </td>
<td class="px-2"> <td class="px-2">
<a-form-item> <a-form-item>
<a-input v-model:value="paramRow.value" size="large" placeholder="Value" /> <a-input v-model:value="paramRow.value" size="large" placeholder="Value" />
</a-form-item> </a-form-item>
</td> </td>
<td class="relative"> <td class="relative">
<div v-if="idx !== 0" class="absolute flex flex-col justify-start mt-2 -right-6 top-0"> <div v-if="idx !== 0" class="absolute flex flex-col justify-start mt-2 -right-6 top-0">
<MdiDeleteOutlineIcon class="cursor-pointer" @click="deleteParamRow(idx)" /> <MdiDeleteOutline class="cursor-pointer" @click="deleteParamRow(idx)" />
</div> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td :colspan="12" class="text-center"> <td :colspan="12" class="text-center">
<a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addParamRow"> <a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addParamRow">
<template #icon> <template #icon>
<MdiPlusIcon class="flex mx-auto" /> <MdiPlus class="flex mx-auto" />
</template> </template>
</a-button> </a-button>
</td> </td>

5
packages/nc-gui/components/cell/Currency.vue

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { ColumnInj, computed, inject, useVModel } from '#imports' import { ColumnInj, EditModeInj, computed, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: number | null | undefined modelValue: number | null | undefined
@ -49,6 +48,8 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
class="w-full h-full border-none outline-none" class="w-full h-full border-none outline-none"
@blur="editEnabled = false" @blur="editEnabled = false"
/> />
<span v-else-if="vModel">{{ currency }}</span> <span v-else-if="vModel">{{ currency }}</span>
<span v-else /> <span v-else />
</template> </template>

2
packages/nc-gui/components/cell/DateTimePicker.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ReadonlyInj } from '#imports' import { ReadonlyInj, inject, ref, useProject, watch } from '#imports'
interface Props { interface Props {
modelValue: string | null | undefined modelValue: string | null | undefined

4
packages/nc-gui/components/cell/Decimal.vue

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { inject, useVModel } from '#imports' import { EditModeInj, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: number | null | string | undefined modelValue: number | null | string | undefined
@ -32,6 +31,7 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
step="0.1" step="0.1"
@blur="editEnabled = false" @blur="editEnabled = false"
/> />
<span v-else class="prose-sm">{{ vModel }}</span> <span v-else class="prose-sm">{{ vModel }}</span>
</template> </template>

14
packages/nc-gui/components/cell/Duration.vue

@ -1,6 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ColumnInj, computed, convertDurationToSeconds, convertMS2Duration, durationOptions, inject, ref } from '#imports' import {
import { EditModeInj } from '~/context' ColumnInj,
EditModeInj,
computed,
convertDurationToSeconds,
convertMS2Duration,
durationOptions,
inject,
ref,
} from '#imports'
interface Props { interface Props {
modelValue: number | string | null | undefined modelValue: number | string | null | undefined
@ -72,7 +80,9 @@ const submitDuration = () => {
@keypress="checkDurationFormat($event)" @keypress="checkDurationFormat($event)"
@keydown.enter="submitDuration" @keydown.enter="submitDuration"
/> />
<span v-else> {{ localState }}</span> <span v-else> {{ localState }}</span>
<div v-if="showWarningMessage" class="duration-warning"> <div v-if="showWarningMessage" class="duration-warning">
<!-- TODO: i18n --> <!-- TODO: i18n -->
Please enter a number Please enter a number

5
packages/nc-gui/components/cell/Email.vue

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

4
packages/nc-gui/components/cell/Float.vue

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { inject, useVModel } from '#imports' import { EditModeInj, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: number | null | undefined modelValue: number | null | undefined
@ -32,6 +31,7 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
step="0.1" step="0.1"
@blur="editEnabled = false" @blur="editEnabled = false"
/> />
<span v-else class="prose-sm">{{ vModel }}</span> <span v-else class="prose-sm">{{ vModel }}</span>
</template> </template>

4
packages/nc-gui/components/cell/Integer.vue

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { inject, useVModel } from '#imports' import { EditModeInj, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: number | null | undefined modelValue: number | null | undefined
@ -36,6 +35,7 @@ function onKeyDown(evt: KeyboardEvent) {
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown="onKeyDown" @keydown="onKeyDown"
/> />
<span v-else class="prose-sm">{{ vModel }}</span> <span v-else class="prose-sm">{{ vModel }}</span>
</template> </template>

3
packages/nc-gui/components/cell/Json.vue

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Modal as AModal } from 'ant-design-vue' import { Modal as AModal, EditModeInj, IsFormInj, ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
import { EditModeInj, IsFormInj, ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
interface Props { interface Props {
modelValue: string | Record<string, any> | undefined modelValue: string | Record<string, any> | undefined

1
packages/nc-gui/components/cell/MultiSelect.vue

@ -143,6 +143,7 @@ watch(isOpen, (n, _o) => {
<span class="text-slate-500">{{ op.title }}</span> <span class="text-slate-500">{{ op.title }}</span>
</a-tag> </a-tag>
</a-select-option> </a-select-option>
<template #tagRender="{ value: val, onClose }"> <template #tagRender="{ value: val, onClose }">
<a-tag <a-tag
v-if="options.find((el) => el.title === val)" v-if="options.find((el) => el.title === val)"

61
packages/nc-gui/components/cell/Percent.vue

@ -1,11 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { EditModeInj, inject } from '#imports' import { EditModeInj, inject, useVModel } from '#imports'
interface Props { interface Props {
modelValue: number | string | null | undefined modelValue: number | string | null | undefined
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj)
@ -15,62 +16,6 @@ const vModel = useVModel(props, 'modelValue', emits)
<template> <template>
<input v-if="editEnabled" v-model="vModel" type="number" /> <input v-if="editEnabled" v-model="vModel" type="number" />
<span v-else>{{ vModel }}</span> <span v-else>{{ vModel }}</span>
</template> </template>
<!-- <script setup lang="ts">
import { ColumnInj, computed, getPercentStep, inject, isValidPercent, renderPercent } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
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()
const isEdited = ref(false)
const percentType = computed(() => column?.value?.meta?.precision || 0)
const percentStep = computed(() => getPercentStep(percentType.value))
const localState = computed({
get: () => {
return renderPercent(modelValue, percentType.value, !isEdited.value)
},
set: (val) => {
if (val === null) val = 0
if (isValidPercent(val, column?.value?.meta?.negative)) {
percent.value = val / 100
}
},
})
function onKeyDown(evt: KeyboardEvent) {
isEdited.value = true
return ['e', 'E', '+', '-'].includes(evt.key) && evt.preventDefault()
}
function onBlur() {
if (isEdited.value) {
emit('update:modelValue', percent.value)
isEdited.value = false
}
}
function onKeyDownEnter() {
if (isEdited.value) {
emit('update:modelValue', percent.value)
isEdited.value = false
}
}
</script>
<template>
<input
v-if="isEdited"
v-model="localState"
type="number"
:step="percentStep"
@keydown="onKeyDown"
@blur="onBlur"
@keydown.enter="onKeyDownEnter"
/>
<input v-if="editEnabled" v-model="localState" type="text" @focus="isEdited = true" />
<span v-else>{{ localState }}</span>
</template> -->

6
packages/nc-gui/components/cell/PhoneNumber.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import Text from './Text.vue' import { useVModel } from '#imports'
interface Props { interface Props {
modelValue: any modelValue: any
@ -17,7 +17,5 @@ const vModel = useVModel(props, 'modelValue', emits)
</script> </script>
<template> <template>
<Text v-model="vModel" /> <LazyCellText v-model="vModel" />
</template> </template>
<style scoped></style>

3
packages/nc-gui/components/cell/Rating.vue

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ColumnInj, computed, inject } from '#imports' import { ColumnInj, EditModeInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue?: number | null | undefined modelValue?: number | null | undefined

1
packages/nc-gui/components/cell/Text.vue

@ -25,5 +25,6 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
class="h-full w-full outline-none bg-transparent" class="h-full w-full outline-none bg-transparent"
@blur="editEnabled = false" @blur="editEnabled = false"
/> />
<span v-else>{{ vModel }}</span> <span v-else>{{ vModel }}</span>
</template> </template>

4
packages/nc-gui/components/cell/TextArea.vue

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject } from '#imports' import { EditModeInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: string | null | undefined modelValue: string | null | undefined
@ -32,5 +31,6 @@ const focus: VNodeRef = (el) => (el as HTMLTextAreaElement)?.focus()
@keydown.alt.enter.stop @keydown.alt.enter.stop
@keydown.shift.enter.stop @keydown.shift.enter.stop
/> />
<span v-else>{{ vModel }}</span> <span v-else>{{ vModel }}</span>
</template> </template>

4
packages/nc-gui/components/cell/TimePicker.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ReadonlyInj, inject, onClickOutside, useProject, watch } from '#imports'
interface Props { interface Props {
modelValue?: string | null | undefined modelValue?: string | null | undefined
@ -16,7 +16,7 @@ const readOnly = inject(ReadonlyInj, false)
let isTimeInvalid = $ref(false) let isTimeInvalid = $ref(false)
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' const dateFormat = isMysql.value ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const localState = $computed({ const localState = $computed({
get() { get() {

7
packages/nc-gui/components/cell/Url.vue

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { message } from 'ant-design-vue'
import { import {
CellUrlDisableOverlayInj, CellUrlDisableOverlayInj,
ColumnInj, ColumnInj,
@ -8,6 +7,7 @@ import {
computed, computed,
inject, inject,
isValidURL, isValidURL,
message,
ref, ref,
useCellUrlConfig, useCellUrlConfig,
useI18n, useI18n,
@ -77,14 +77,19 @@ watch(
<nuxt-link <nuxt-link
v-else-if="isValid && !cellUrlOptions?.overlay" v-else-if="isValid && !cellUrlOptions?.overlay"
no-prefetch
no-rel
class="z-3 text-sm underline hover:opacity-75" class="z-3 text-sm underline hover:opacity-75"
:to="url" :to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'" :target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
> >
{{ value }} {{ value }}
</nuxt-link> </nuxt-link>
<nuxt-link <nuxt-link
v-else-if="isValid && !disableOverlay && cellUrlOptions?.overlay" v-else-if="isValid && !disableOverlay && cellUrlOptions?.overlay"
no-prefetch
no-rel
class="z-3 w-full h-full text-center !no-underline hover:opacity-75" class="z-3 w-full h-full text-center !no-underline hover:opacity-75"
:to="url" :to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'" :target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"

6
packages/nc-gui/components/cell/attachment/index.vue

@ -2,8 +2,6 @@
import { onKeyDown } from '@vueuse/core' import { onKeyDown } from '@vueuse/core'
import { useProvideAttachmentCell } from './utils' import { useProvideAttachmentCell } from './utils'
import { useSortable } from './sort' import { useSortable } from './sort'
import Modal from './Modal.vue'
import Carousel from './Carousel.vue'
import { import {
IsFormInj, IsFormInj,
IsGalleryInj, IsGalleryInj,
@ -130,7 +128,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
ref="attachmentCellRef" ref="attachmentCellRef"
class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1" class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1"
> >
<Carousel /> <LazyCellAttachmentCarousel />
<template v-if="isSharedForm || (!isReadonly && !dragging && !!currentCellRef)"> <template v-if="isSharedForm || (!isReadonly && !dragging && !!currentCellRef)">
<general-overlay <general-overlay
@ -211,7 +209,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
</div> </div>
</template> </template>
<Modal /> <LazyCellAttachmentModal />
</div> </div>
</template> </template>

5
packages/nc-gui/components/cell/attachment/utils.ts

@ -1,5 +1,3 @@
import { message } from 'ant-design-vue'
import FileSaver from 'file-saver'
import { import {
ColumnInj, ColumnInj,
EditModeInj, EditModeInj,
@ -11,6 +9,7 @@ import {
computed, computed,
inject, inject,
isImage, isImage,
message,
ref, ref,
useApi, useApi,
useFileDialog, useFileDialog,
@ -146,7 +145,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
/** download a file */ /** download a file */
async function downloadFile(item: Record<string, any>) { async function downloadFile(item: Record<string, any>) {
FileSaver.saveAs(item.url || item.data, item.title) ;(await import('file-saver')).saveAs(item.url || item.data, item.title)
} }
const FileIcon = (icon: string) => { const FileIcon = (icon: string) => {

28
packages/nc-gui/components/dashboard/TreeView.vue

@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import { Empty } from 'ant-design-vue'
import GithubButton from 'vue-github-button' import GithubButton from 'vue-github-button'
import { import {
Empty,
computed, computed,
inject, inject,
reactive, reactive,
@ -16,15 +16,9 @@ import {
useUIPermission, useUIPermission,
watchEffect, watchEffect,
} from '#imports' } from '#imports'
import DlgAirtableImport from '~/components/dlg/AirtableImport.vue' import { TabType } from '~/lib'
import DlgQuickImport from '~/components/dlg/QuickImport.vue'
import DlgTableCreate from '~/components/dlg/TableCreate.vue'
import DlgTableRename from '~/components/dlg/TableRename.vue'
import { TabType } from '~/composables'
import MdiView from '~icons/mdi/eye-circle-outline' import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large' import MdiTableLarge from '~icons/mdi/table-large'
import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiDrag from '~icons/mdi/drag-vertical'
const { addTab } = useTabs() const { addTab } = useTabs()
@ -49,7 +43,7 @@ const filterQuery = $ref('')
const activeTable = computed(() => ([TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.title : null)) const activeTable = computed(() => ([TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.title : null))
const tablesById = $computed(() => const tablesById = $computed(() =>
tables.value?.reduce((acc: Record<string, TableType>, table) => { tables.value?.reduce<Record<string, TableType>>((acc, table) => {
acc[table.id!] = table acc[table.id!] = table
return acc return acc
@ -150,7 +144,7 @@ function openRenameTableDialog(table: TableType, rightClick = false) {
const isOpen = ref(true) const isOpen = ref(true)
const { close } = useDialog(DlgTableRename, { const { close } = useDialog(() => import('~/components/dlg/TableRename.vue'), {
'modelValue': isOpen, 'modelValue': isOpen,
'tableMeta': table, 'tableMeta': table,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
@ -168,7 +162,7 @@ function openQuickImportDialog(type: string) {
const isOpen = ref(true) const isOpen = ref(true)
const { close } = useDialog(DlgQuickImport, { const { close } = useDialog(() => import('~/components/dlg/QuickImport.vue'), {
'modelValue': isOpen, 'modelValue': isOpen,
'importType': type, 'importType': type,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
@ -186,7 +180,7 @@ function openAirtableImportDialog() {
const isOpen = ref(true) const isOpen = ref(true)
const { close } = useDialog(DlgAirtableImport, { const { close } = useDialog(() => import('~/components/dlg/AirtableImport.vue'), {
'modelValue': isOpen, 'modelValue': isOpen,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -203,7 +197,7 @@ function openTableCreateDialog() {
const isOpen = ref(true) const isOpen = ref(true)
const { close } = useDialog(DlgTableCreate, { const { close } = useDialog(() => import('~/components/dlg/TableCreate.vue'), {
'modelValue': isOpen, 'modelValue': isOpen,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
}) })
@ -320,7 +314,7 @@ function openTableCreateDialog() {
<template #title>{{ table.table_name }}</template> <template #title>{{ table.table_name }}</template>
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)"> <div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto"> <div class="flex w-auto">
<MdiDrag <MdiDragVertical
v-if="isUIAllowed('treeview-drag-n-drop')" v-if="isUIAllowed('treeview-drag-n-drop')"
:class="`nc-child-draggable-icon-${table.title}`" :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" class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
@ -343,7 +337,7 @@ function openTableCreateDialog() {
:trigger="['click']" :trigger="['click']"
@click.stop @click.stop
> >
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" /> <MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay> <template #overlay>
<a-menu class="!py-0 rounded text-sm"> <a-menu class="!py-0 rounded text-sm">
@ -403,11 +397,11 @@ function openTableCreateDialog() {
<a-divider class="!my-0" /> <a-divider class="!my-0" />
<div class="flex items-start flex-col justify-start px-2 py-3 gap-2"> <div class="flex items-start flex-col justify-start px-2 py-3 gap-2">
<GeneralShareBaseButton <LazyGeneralShareBaseButton
class="color-transition py-1.5 px-2 text-primary font-bold cursor-pointer select-none hover:text-accent" class="color-transition py-1.5 px-2 text-primary font-bold cursor-pointer select-none hover:text-accent"
/> />
<GeneralHelpAndSupport class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" /> <LazyGeneralHelpAndSupport class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" />
<GithubButton <GithubButton
class="ml-2 py-1" class="ml-2 py-1"

30
packages/nc-gui/components/dashboard/settings/AppStore.vue

@ -1,18 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { extractSdkResponseErrorMsg, message, onMounted, useI18n, useNuxtApp } from '#imports'
import { useI18n } from 'vue-i18n'
import AppInstall from './app-store/AppInstall.vue'
import MdiEditIcon from '~icons/ic/round-edit'
import MdiCloseCircleIcon from '~icons/mdi/close-circle-outline'
import MdiPlusIcon from '~icons/mdi/plus'
import { extractSdkResponseErrorMsg } from '~/utils'
const { t } = useI18n() const { t } = useI18n()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
let apps = $ref<null | Array<any>>(null) let apps = $ref<null | any[]>(null)
let showPluginUninstallModal = $ref(false) let showPluginUninstallModal = $ref(false)
let showPluginInstallModal = $ref(false) let showPluginInstallModal = $ref(false)
let pluginApp = $ref<any>(null) let pluginApp = $ref<any>(null)
const fetchPluginApps = async () => { const fetchPluginApps = async () => {
@ -66,7 +64,7 @@ const showResetPluginModal = async (app: any) => {
onMounted(async () => { onMounted(async () => {
if (apps === null) { if (apps === null) {
fetchPluginApps() await fetchPluginApps()
} }
}) })
</script> </script>
@ -80,7 +78,7 @@ onMounted(async () => {
:footer="null" :footer="null"
wrap-class-name="nc-modal-plugin-install" wrap-class-name="nc-modal-plugin-install"
> >
<AppInstall <LazyDashboardSettingsAppInstall
v-if="pluginApp && showPluginInstallModal" v-if="pluginApp && showPluginInstallModal"
:id="pluginApp.id" :id="pluginApp.id"
@close="showPluginInstallModal = false" @close="showPluginInstallModal = false"
@ -117,19 +115,21 @@ onMounted(async () => {
<div class="install-btn flex flex-row justify-end space-x-1"> <div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)"> <a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit"> <div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit">
<MdiEditIcon class="pr-0.5" :height="12" /> <IcRoundEdit class="pr-0.5" :height="12" />
Edit Edit
</div> </div>
</a-button> </a-button>
<a-button v-if="app.parsedInput" size="small" outlined @click="showResetPluginModal(app)"> <a-button v-if="app.parsedInput" size="small" outlined @click="showResetPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-reset"> <div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-reset">
<MdiCloseCircleIcon /> <MdiCloseCircleOutline />
<div class="flex ml-0.5">Reset</div> <div class="flex ml-0.5">Reset</div>
</div> </div>
</a-button> </a-button>
<a-button v-else size="small" type="primary" @click="showInstallPluginModal(app)"> <a-button v-else size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install"> <div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<MdiPlusIcon /> <MdiPlus />
Install Install
</div> </div>
</a-button> </a-button>
@ -140,15 +140,19 @@ onMounted(async () => {
<img <img
v-if="app.title !== 'SMTP'" v-if="app.title !== 'SMTP'"
class="avatar" class="avatar"
alt="logo"
:style="{ :style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '', backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}" }"
:src="app.logo" :src="app.logo"
/> />
<div v-else /> <div v-else />
</div> </div>
<div class="flex flex-col flex-1 w-3/5 pl-3"> <div class="flex flex-col flex-1 w-3/5 pl-3">
<a-typography-title :level="5">{{ app.title }}</a-typography-title> <a-typography-title :level="5">{{ app.title }}</a-typography-title>
{{ app.description }} {{ app.description }}
</div> </div>
</div> </div>

9
packages/nc-gui/components/dashboard/settings/AuditTab.vue

@ -1,13 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { Tooltip as ATooltip, Empty } from 'ant-design-vue' import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk' import type { AuditType } from 'nocodb-sdk'
import { timeAgo } from '~/utils/dateTimeUtils' import { h, onMounted, timeAgo, useI18n, useNuxtApp, useProject } from '#imports'
import { h, useNuxtApp, useProject } from '#imports'
import MdiReload from '~icons/mdi/reload'
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { project } = useProject() const { project } = useProject()
const { t } = useI18n() const { t } = useI18n()
let isLoading = $ref(false) let isLoading = $ref(false)
@ -92,9 +91,11 @@ const columns = [
<!-- Reload --> <!-- Reload -->
<div class="flex items-center gap-2 text-gray-600 font-light"> <div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" /> <MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
{{ $t('general.reload') }} {{ $t('general.reload') }}
</div> </div>
</a-button> </a-button>
<a-pagination <a-pagination
v-model:current="currentPage" v-model:current="currentPage"
:page-size="currentLimit" :page-size="currentLimit"

25
packages/nc-gui/components/dashboard/settings/Metadata.vue

@ -1,17 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n' import { Empty, extractSdkResponseErrorMsg, h, message, useI18n, useNuxtApp, useProject } from '#imports'
import { Empty, message } from 'ant-design-vue'
import { h, useNuxtApp, useProject } from '#imports'
import MdiReload from '~icons/mdi/reload'
import MdiDatabaseSync from '~icons/mdi/database-sync'
import { extractSdkResponseErrorMsg } from '~/utils'
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { project, loadTables } = useProject() const { project, loadTables } = useProject()
const { t } = useI18n() const { t } = useI18n()
let isLoading = $ref(false) let isLoading = $ref(false)
let isDifferent = $ref(false) let isDifferent = $ref(false)
let metadiff = $ref<any[]>([]) let metadiff = $ref<any[]>([])
async function loadMetaDiff() { async function loadMetaDiff() {
@ -58,6 +57,7 @@ onMounted(async () => {
}) })
const tableHeaderRenderer = (label: string) => () => h('div', { class: 'text-gray-500' }, label) const tableHeaderRenderer = (label: string) => () => h('div', { class: 'text-gray-500' }, label)
const columns = [ const columns = [
{ {
// Models // Models
@ -90,6 +90,7 @@ const columns = [
</div> </div>
</a-button> </a-button>
</div> </div>
<div class="max-h-600px overflow-y-auto"> <div class="max-h-600px overflow-y-auto">
<a-table <a-table
class="w-full" class="w-full"
@ -105,10 +106,13 @@ const columns = [
:loading="isLoading" :loading="isLoading"
bordered bordered
> >
<template #emptyText> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> </template <template #emptyText>
></a-table> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
</a-table>
</div> </div>
</div> </div>
<div class="flex place-content-center w-2/5"> <div class="flex place-content-center w-2/5">
<!-- Sync Now --> <!-- Sync Now -->
<div v-if="isDifferent"> <div v-if="isDifferent">
@ -119,9 +123,12 @@ const columns = [
</div> </div>
</a-button> </a-button>
</div> </div>
<div v-else> <div v-else>
<!-- Tables metadata is in sync --> <!-- Tables metadata is in sync -->
<span><a-alert :message="$t('msg.info.tablesMetadataInSync')" type="success" show-icon /></span> <span>
<a-alert :message="$t('msg.info.tablesMetadataInSync')" type="success" show-icon />
</span>
</div> </div>
</div> </div>
</div> </div>

8
packages/nc-gui/components/dashboard/settings/Misc.vue

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useGlobal, useProject, watch } from '#imports'
const { includeM2M } = useGlobal() const { includeM2M } = useGlobal()
const { loadTables } = useProject() const { loadTables } = useProject()
@ -10,9 +12,9 @@ watch(includeM2M, async () => await loadTables())
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="flex flex-row items-center w-full mb-4 gap-2"> <div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show M2M Tables --> <!-- Show M2M Tables -->
<a-checkbox v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc">{{ <a-checkbox v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc">
$t('msg.info.showM2mTables') {{ $t('msg.info.showM2mTables') }}
}}</a-checkbox> </a-checkbox>
</div> </div>
</div> </div>
</div> </div>

31
packages/nc-gui/components/dashboard/settings/Modal.vue

@ -1,19 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue' import type { FunctionalComponent, SVGAttributes } from 'vue'
import AuditTab from './AuditTab.vue' import { useI18n, useNuxtApp, useUIPermission, useVModel, watch } from '#imports'
import AppStore from './AppStore.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import Misc from './Misc.vue'
import Erd from './Erd.vue'
import { useNuxtApp } from '#app'
import { useI18n, useUIPermission, useVModel, watch } from '#imports'
import ApiTokenManagement from '~/components/tabs/auth/ApiTokenManagement.vue'
import UserManagement from '~/components/tabs/auth/UserManagement.vue'
import StoreFrontOutline from '~icons/mdi/storefront-outline' import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill' import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple' import MultipleTableIcon from '~icons/mdi/table-multiple'
import NootbookOutline from '~icons/mdi/notebook-outline' import NotebookOutline from '~icons/mdi/notebook-outline'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
@ -59,7 +50,7 @@ const tabsInfo: TabGroup = {
usersManagement: { usersManagement: {
// Users Management // Users Management
title: t('title.userMgmt'), title: t('title.userMgmt'),
body: UserManagement, body: () => import('../../tabs/auth/UserManagement.vue'),
}, },
} }
: {}), : {}),
@ -68,7 +59,7 @@ const tabsInfo: TabGroup = {
apiTokenManagement: { apiTokenManagement: {
// API Tokens Management // API Tokens Management
title: t('title.apiTokenMgmt'), title: t('title.apiTokenMgmt'),
body: ApiTokenManagement, body: () => import('../../tabs/auth/ApiTokenManagement.vue'),
}, },
} }
: {}), : {}),
@ -86,7 +77,7 @@ const tabsInfo: TabGroup = {
subTabs: { subTabs: {
new: { new: {
title: 'Apps', title: 'Apps',
body: AppStore, body: () => import('./AppStore.vue'),
}, },
}, },
onClick: () => { onClick: () => {
@ -103,26 +94,26 @@ const tabsInfo: TabGroup = {
metaData: { metaData: {
// Metadata // Metadata
title: t('title.metadata'), title: t('title.metadata'),
body: Metadata, body: () => import('./Metadata.vue'),
}, },
acl: { acl: {
// UI Access Control // UI Access Control
title: t('title.uiACL'), title: t('title.uiACL'),
body: UIAcl, body: () => import('./UIAcl.vue'),
onClick: () => { onClick: () => {
$e('c:table:ui-acl') $e('c:table:ui-acl')
}, },
}, },
erd: { erd: {
title: t('title.erdView'), title: t('title.erdView'),
body: Erd, body: () => import('./Erd.vue'),
onClick: () => { onClick: () => {
$e('c:settings:erd') $e('c:settings:erd')
}, },
}, },
misc: { misc: {
title: t('general.misc'), title: t('general.misc'),
body: Misc, body: () => import('./Misc.vue'),
}, },
}, },
onClick: () => { onClick: () => {
@ -132,12 +123,12 @@ const tabsInfo: TabGroup = {
audit: { audit: {
// Audit // Audit
title: t('title.audit'), title: t('title.audit'),
icon: NootbookOutline, icon: NotebookOutline,
subTabs: { subTabs: {
audit: { audit: {
// Audit // Audit
title: t('title.audit'), title: t('title.audit'),
body: AuditTab, body: () => import('./AuditTab.vue'),
}, },
}, },
onClick: () => { onClick: () => {

26
packages/nc-gui/components/dashboard/settings/UIAcl.vue

@ -1,8 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { Empty, message } from 'ant-design-vue' import {
import { useI18n } from 'vue-i18n' Empty,
import { extractSdkResponseErrorMsg, viewIcons } from '~/utils' computed,
import { computed, h, useNuxtApp, useProject } from '#imports' extractSdkResponseErrorMsg,
h,
message,
onMounted,
useGlobal,
useI18n,
useNuxtApp,
useProject,
viewIcons,
} from '#imports'
const { t } = useI18n() const { t } = useI18n()
@ -109,12 +118,14 @@ const columns = [
<MdiMagnify /> <MdiMagnify />
</template> </template>
</a-input> </a-input>
<a-button class="self-start nc-acl-reload" @click="loadTableList"> <a-button class="self-start nc-acl-reload" @click="loadTableList">
<div class="flex items-center gap-2 text-gray-600 font-light"> <div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" /> <MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
Reload Reload
</div> </div>
</a-button> </a-button>
<a-button class="self-start nc-acl-save" @click="saveUIAcl"> <a-button class="self-start nc-acl-save" @click="saveUIAcl">
<div class="flex items-center gap-2 text-gray-600 font-light"> <div class="flex items-center gap-2 text-gray-600 font-light">
<MdiContentSave /> <MdiContentSave />
@ -122,6 +133,7 @@ const columns = [
</div> </div>
</a-button> </a-button>
</div> </div>
<div class="max-h-600px overflow-y-auto"> <div class="max-h-600px overflow-y-auto">
<a-table <a-table
class="w-full" class="w-full"
@ -140,14 +152,17 @@ const columns = [
<template #emptyText> <template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template> </template>
<template #bodyCell="{ record, column }"> <template #bodyCell="{ record, column }">
<div v-if="column.name === 'table_name'">{{ record._ptn }}</div> <div v-if="column.name === 'table_name'">{{ record._ptn }}</div>
<div v-if="column.name === 'view_name'"> <div v-if="column.name === 'view_name'">
<div class="flex items-center"> <div class="flex items-center">
<component :is="viewIcons[record.type].icon" :class="`text-${viewIcons[record.type].color} mr-1`" /> <component :is="viewIcons[record.type].icon" :class="`text-${viewIcons[record.type].color} mr-1`" />
{{ record.title }} {{ record.title }}
</div> </div>
</div> </div>
<div v-for="role in roles" :key="role"> <div v-for="role in roles" :key="role">
<div v-if="column.name === role"> <div v-if="column.name === role">
<a-tooltip> <a-tooltip>
@ -157,11 +172,12 @@ const columns = [
> >
<span v-else>Click to hide '{{ record.title }}' for role:{{ role }} in UI dashboard</span> <span v-else>Click to hide '{{ record.title }}' for role:{{ role }} in UI dashboard</span>
</template> </template>
<a-checkbox <a-checkbox
:checked="!record.disabled[role]" :checked="!record.disabled[role]"
:class="`nc-acl-${record.title}-${role}-chkbox`" :class="`nc-acl-${record.title}-${role}-chkbox`"
@change="onRoleCheck(record, role)" @change="onRoleCheck(record, role)"
></a-checkbox> />
</a-tooltip> </a-tooltip>
</div> </div>
</div> </div>

35
packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue

@ -1,19 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue'
import type { PluginType } from 'nocodb-sdk' import type { PluginType } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n' import { extractSdkResponseErrorMsg, message, onMounted, ref, useI18n, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg, ref, useNuxtApp } from '#imports'
interface Props { const { id } = defineProps<{
id: string id: string
} }>()
type Plugin = PluginType & {
formDetails: Record<string, any>
parsedInput: Record<string, any>
}
const { id } = defineProps<Props>()
const emits = defineEmits(['saved', 'close']) const emits = defineEmits(['saved', 'close'])
@ -22,6 +13,11 @@ enum Action {
Test = 'test', Test = 'test',
} }
type Plugin = PluginType & {
formDetails: Record<string, any>
parsedInput: Record<string, any>
}
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const formRef = ref() const formRef = ref()
@ -29,8 +25,11 @@ const formRef = ref()
const { t } = useI18n() const { t } = useI18n()
let plugin = $ref<Plugin | null>(null) let plugin = $ref<Plugin | null>(null)
let pluginFormData = $ref<Record<string, any>>({}) let pluginFormData = $ref<Record<string, any>>({})
let isLoading = $ref(true) let isLoading = $ref(true)
let loadingAction = $ref<null | Action>(null) let loadingAction = $ref<null | Action>(null)
const layout = { const layout = {
@ -153,6 +152,7 @@ onMounted(async () => {
<span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span> <span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span>
</div> </div>
<div class="absolute -right-2 -top-0.5"> <div class="absolute -right-2 -top-0.5">
<a-button type="text" class="!rounded-md mr-1" @click="emits('close')"> <a-button type="text" class="!rounded-md mr-1" @click="emits('close')">
<template #icon> <template #icon>
@ -175,6 +175,7 @@ onMounted(async () => {
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(itemRow, itemIndex) in plugin.parsedInput" :key="itemIndex"> <tr v-for="(itemRow, itemIndex) in plugin.parsedInput" :key="itemIndex">
<td v-for="(columnData, columnIndex) in plugin.formDetails.items" :key="columnIndex" class="px-2"> <td v-for="(columnData, columnIndex) in plugin.formDetails.items" :key="columnIndex" class="px-2">
@ -188,17 +189,21 @@ onMounted(async () => {
v-model:value="itemRow[columnData.key]" v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-textarea <a-textarea
v-else-if="columnData.type === 'LongText'" v-else-if="columnData.type === 'LongText'"
v-model:value="itemRow[columnData.key]" v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-switch <a-switch
v-else-if="columnData.type === 'Checkbox'" v-else-if="columnData.type === 'Checkbox'"
v-model:value="itemRow[columnData.key]" v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-input v-else v-model:value="itemRow[columnData.key]" :placeholder="columnData.placeholder" /> <a-input v-else v-model:value="itemRow[columnData.key]" :placeholder="columnData.placeholder" />
<div <div
v-if="itemIndex !== 0 && columnIndex === plugin.formDetails.items.length - 1" v-if="itemIndex !== 0 && columnIndex === plugin.formDetails.items.length - 1"
class="absolute flex flex-col justify-start mt-2 -right-6 top-0" class="absolute flex flex-col justify-start mt-2 -right-6 top-0"
@ -236,19 +241,23 @@ onMounted(async () => {
v-model:value="pluginFormData[columnData.key]" v-model:value="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-textarea <a-textarea
v-else-if="columnData.type === 'LongText'" v-else-if="columnData.type === 'LongText'"
v-model:value="pluginFormData[columnData.key]" v-model:value="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-switch <a-switch
v-else-if="columnData.type === 'Checkbox'" v-else-if="columnData.type === 'Checkbox'"
v-model:checked="pluginFormData[columnData.key]" v-model:checked="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-input v-else v-model:value="pluginFormData[columnData.key]" :placeholder="columnData.placeholder" /> <a-input v-else v-model:value="pluginFormData[columnData.key]" :placeholder="columnData.placeholder" />
</a-form-item> </a-form-item>
</template> </template>
<div class="flex flex-row space-x-4 justify-center mt-4"> <div class="flex flex-row space-x-4 justify-center mt-4">
<a-button <a-button
v-for="(action, i) in plugin.formDetails.actions" v-for="(action, i) in plugin.formDetails.actions"
@ -265,5 +274,3 @@ onMounted(async () => {
</div> </div>
</template> </template>
</template> </template>
<style scoped lang="scss"></style>

15
packages/nc-gui/components/dlg/AirtableImport.vue

@ -2,11 +2,12 @@
import type { Socket } from 'socket.io-client' import type { Socket } from 'socket.io-client'
import io from 'socket.io-client' import io from 'socket.io-client'
import type { Card as AntCard } from 'ant-design-vue' import type { Card as AntCard } from 'ant-design-vue'
import { Form, message } from 'ant-design-vue'
import { import {
Form,
computed, computed,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
fieldRequiredValidator, fieldRequiredValidator,
message,
nextTick, nextTick,
onBeforeUnmount, onBeforeUnmount,
onMounted, onMounted,
@ -17,11 +18,9 @@ import {
watch, watch,
} from '#imports' } from '#imports'
interface Props { const { modelValue } = defineProps<{
modelValue: boolean modelValue: boolean
} }>()
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -337,9 +336,9 @@ onBeforeUnmount(() => {
<!-- This feature is currently in beta and more information can be found here --> <!-- This feature is currently in beta and more information can be found here -->
<div> <div>
{{ $t('general.betaNote') }} {{ $t('general.betaNote') }}
<a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">{{ <a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">
$t('general.moreInfo') {{ $t('general.moreInfo') }}
}}</a> </a>
. .
</div> </div>
</div> </div>

7
packages/nc-gui/components/dlg/QuickImport.vue

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Form, message } from 'ant-design-vue'
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import type { UploadChangeParam, UploadFile } from 'ant-design-vue' import type { UploadChangeParam, UploadFile } from 'ant-design-vue'
import { import {
ExcelTemplateAdapter, ExcelTemplateAdapter,
ExcelUrlTemplateAdapter, ExcelUrlTemplateAdapter,
Form,
JSONTemplateAdapter, JSONTemplateAdapter,
JSONUrlTemplateAdapter, JSONUrlTemplateAdapter,
computed, computed,
@ -13,6 +13,7 @@ import {
importCsvUrlValidator, importCsvUrlValidator,
importExcelUrlValidator, importExcelUrlValidator,
importUrlValidator, importUrlValidator,
message,
reactive, reactive,
ref, ref,
useI18n, useI18n,
@ -294,7 +295,7 @@ const customReqCbk = (customReqArgs: { file: any; onSuccess: () => void }) => {
<div class="prose-xl font-weight-bold my-5">{{ importMeta.header }}</div> <div class="prose-xl font-weight-bold my-5">{{ importMeta.header }}</div>
<div class="mt-5"> <div class="mt-5">
<TemplateEditor <LazyTemplateEditor
v-if="templateEditorModal" v-if="templateEditorModal"
ref="templateEditorRef" ref="templateEditorRef"
:project-template="templateData" :project-template="templateData"
@ -350,7 +351,7 @@ const customReqCbk = (customReqArgs: { file: any; onSuccess: () => void }) => {
</template> </template>
<div class="pb-3 pt-3"> <div class="pb-3 pt-3">
<MonacoEditor ref="jsonEditorRef" v-model="importState.jsonEditor" class="min-h-60 max-h-80" /> <LazyMonacoEditor ref="jsonEditorRef" v-model="importState.jsonEditor" class="min-h-60 max-h-80" />
</div> </div>
</a-tab-pane> </a-tab-pane>

11
packages/nc-gui/components/dlg/TableCreate.vue

@ -1,13 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Form } from 'ant-design-vue' import { Form, computed, onMounted, ref, useProject, useTable, useTabs, useVModel, validateTableName } from '#imports'
import { computed, onMounted, ref, useProject, useTable, useTabs, useVModel, validateTableName } from '#imports' import { TabType } from '~/lib'
import { TabType } from '~/composables'
interface Props { const props = defineProps<{
modelValue: boolean modelValue: boolean
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])

38
packages/nc-gui/components/dlg/TableRename.vue

@ -1,11 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { watchEffect } from '@vue/runtime-core'
import { Form, message } from 'ant-design-vue'
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n' import {
import { useMetas, useProject, useTabs } from '#imports' Form,
import { extractSdkResponseErrorMsg, validateTableName } from '~/utils' computed,
import { useNuxtApp } from '#app' extractSdkResponseErrorMsg,
message,
nextTick,
reactive,
useI18n,
useMetas,
useNuxtApp,
useProject,
useTabs,
validateTableName,
watchEffect,
} from '#imports'
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean
@ -32,14 +41,19 @@ const dialogShow = computed({
}) })
const { updateTab } = useTabs() const { updateTab } = useTabs()
const { loadTables, tables, project, isMysql, isMssql, isPg } = useProject() const { loadTables, tables, project, isMysql, isMssql, isPg } = useProject()
const inputEl = $ref<any>() const inputEl = $ref<any>()
let loading = $ref(false) let loading = $ref(false)
const useForm = Form.useForm const useForm = Form.useForm
const formState = reactive({ const formState = reactive({
title: '', title: '',
}) })
const validators = computed(() => { const validators = computed(() => {
return { return {
title: [ title: [
@ -83,6 +97,7 @@ const validators = computed(() => {
], ],
} }
}) })
const { validateInfos } = useForm(formState, validators) const { validateInfos } = useForm(formState, validators)
watchEffect(() => { watchEffect(() => {
@ -102,12 +117,15 @@ const renameTable = async () => {
project_id: tableMeta?.project_id, project_id: tableMeta?.project_id,
table_name: formState.title, table_name: formState.title,
}) })
dialogShow.value = false dialogShow.value = false
loadTables()
await loadTables()
updateTab({ id: tableMeta?.id }, { title: formState.title }) updateTab({ id: tableMeta?.id }, { title: formState.title })
// update metas // update metas
setMeta(await $api.dbTable.read(tableMeta?.id as string)) await setMeta(await $api.dbTable.read(tableMeta?.id as string))
// Table renamed successfully // Table renamed successfully
message.success(t('msg.success.tableRenamed')) message.success(t('msg.success.tableRenamed'))
@ -116,6 +134,7 @@ const renameTable = async () => {
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
loading = false loading = false
} }
</script> </script>
@ -131,12 +150,15 @@ const renameTable = async () => {
> >
<template #footer> <template #footer>
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button> <a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" type="primary" :loading="loading" @click="renameTable">{{ $t('general.submit') }}</a-button> <a-button key="submit" type="primary" :loading="loading" @click="renameTable">{{ $t('general.submit') }}</a-button>
</template> </template>
<div class="pl-10 pr-10 pt-5"> <div class="pl-10 pr-10 pt-5">
<a-form :model="formState" name="create-new-table-form"> <a-form :model="formState" name="create-new-table-form">
<!-- hint="Enter table name" --> <!-- hint="Enter table name" -->
<div class="mb-2">{{ $t('msg.info.enterTableName') }}</div> <div class="mb-2">{{ $t('msg.info.enterTableName') }}</div>
<a-form-item v-bind="validateInfos.title"> <a-form-item v-bind="validateInfos.title">
<a-input <a-input
ref="inputEl" ref="inputEl"

22
packages/nc-gui/components/dlg/ViewCreate.vue

@ -1,14 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ComponentPublicInstance } from '@vue/runtime-core' import type { ComponentPublicInstance } from '@vue/runtime-core'
import { message } from 'ant-design-vue'
import type { Form as AntForm } from 'ant-design-vue' import type { Form as AntForm } from 'ant-design-vue'
import { capitalize, inject } from '@vue/runtime-core' import { capitalize } from '@vue/runtime-core'
import type { FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk' import type { FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n' import {
import { MetaInj, ViewListInj } from '~/context' MetaInj,
import { generateUniqueTitle } from '~/utils' ViewListInj,
import { computed, nextTick, reactive, unref, useApi, useVModel, watch } from '#imports' computed,
generateUniqueTitle,
inject,
message,
nextTick,
reactive,
unref,
useApi,
useI18n,
useVModel,
watch,
} from '#imports'
interface Props { interface Props {
modelValue: boolean modelValue: boolean

21
packages/nc-gui/components/dlg/ViewDelete.vue

@ -1,12 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { message } from 'ant-design-vue' import { extractSdkResponseErrorMsg, message, onKeyStroke, useApi, useI18n, useNuxtApp, useVModel } from '#imports'
import { useI18n } from 'vue-i18n'
import { extractSdkResponseErrorMsg } from '~/utils'
import { onKeyStroke, useApi, useNuxtApp, useVModel } from '#imports'
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const { t } = useI18n()
interface Props { interface Props {
modelValue: boolean modelValue: boolean
view?: Record<string, any> view?: Record<string, any>
@ -17,6 +11,12 @@ interface Emits {
(event: 'deleted'): void (event: 'deleted'): void
} }
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const { t } = useI18n()
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
const { api, isLoading } = useApi() const { api, isLoading } = useApi()
@ -55,9 +55,10 @@ async function onDelete() {
<template #footer> <template #footer>
<a-button key="back" @click="vModel = false">{{ $t('general.cancel') }}</a-button> <a-button key="back" @click="vModel = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" danger html-type="submit" :loading="isLoading" @click="onDelete">{{
$t('general.submit') <a-button key="submit" danger html-type="submit" :loading="isLoading" @click="onDelete">
}}</a-button> {{ $t('general.submit') }}
</a-button>
</template> </template>
</a-modal> </a-modal>
</template> </template>

2
packages/nc-gui/components/general/ChromeWrapper.vue

@ -26,5 +26,3 @@ const picked = computed({
<template> <template>
<Chrome v-model="picked" /> <Chrome v-model="picked" />
</template> </template>
<style scoped></style>

2
packages/nc-gui/components/general/ColorModeSwitcher.vue

@ -1,6 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import MaterialSymbolsLightMode from '~icons/material-symbols/light-mode'
import MaterialSymbolsDarkMode from '~icons/material-symbols/dark-mode'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
} }

4
packages/nc-gui/components/general/ColorPicker.vue

@ -57,14 +57,16 @@ watch(picked, (n, _o) => {
{{ compare(picked, color) ? '&#10003;' : '' }} {{ compare(picked, color) ? '&#10003;' : '' }}
</button> </button>
</div> </div>
<a-card v-if="props.advanced" class="w-full mt-2" :body-style="{ padding: '0px' }" :bordered="false"> <a-card v-if="props.advanced" class="w-full mt-2" :body-style="{ padding: '0px' }" :bordered="false">
<a-collapse accordion ghost expand-icon-position="right"> <a-collapse accordion ghost expand-icon-position="right">
<a-collapse-panel key="1" header="Advanced" class=""> <a-collapse-panel key="1" header="Advanced" class="">
<a-button v-if="props.pickButton" class="!bg-primary text-white w-full" @click="selectColor(picked)"> <a-button v-if="props.pickButton" class="!bg-primary text-white w-full" @click="selectColor(picked)">
Pick Color Pick Color
</a-button> </a-button>
<div class="flex justify-center py-4"> <div class="flex justify-center py-4">
<GeneralChromeWrapper v-model="picked" class="!w-full !shadow-none" /> <LazyGeneralChromeWrapper v-model="picked" class="!w-full !shadow-none" />
</div> </div>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>

5
packages/nc-gui/components/general/FlippingCard.vue

@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onBeforeUnmount, onMounted, watch } from '#imports'
type FlipTrigger = 'hover' | 'click' | { duration: number } type FlipTrigger = 'hover' | 'click' | { duration: number }
interface Props { interface Props {
@ -12,7 +14,9 @@ const props = withDefaults(defineProps<Props>(), {
}) })
let flipped = $ref(false) let flipped = $ref(false)
let hovered = $ref(false) let hovered = $ref(false)
let flipTimer = $ref<NodeJS.Timer | null>(null) let flipTimer = $ref<NodeJS.Timer | null>(null)
onMounted(() => { onMounted(() => {
@ -76,6 +80,7 @@ watch($$(flipped), () => {
> >
<slot name="front" /> <slot name="front" />
</div> </div>
<div <div
class="back" class="back"
:style="{ 'pointer-events': flipped ? 'auto' : 'none', 'opacity': !isFlipping ? (flipped ? 100 : 0) : flipped ? 0 : 100 }" :style="{ 'pointer-events': flipped ? 'auto' : 'none', 'opacity': !isFlipping ? (flipped ? 100 : 0) : flipped ? 0 : 100 }"

3
packages/nc-gui/components/general/FullScreen.vue

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useSidebar } from '#imports' import { computed, useSidebar } from '#imports'
const rightSidebar = useSidebar('nc-right-sidebar') const rightSidebar = useSidebar('nc-right-sidebar')
const leftSidebar = useSidebar('nc-left-sidebar') const leftSidebar = useSidebar('nc-left-sidebar')
const isSidebarsOpen = computed({ const isSidebarsOpen = computed({

10
packages/nc-gui/components/general/HelpAndSupport.vue

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useGlobal, useProject } from '#imports' import { ref, useGlobal, useProject, useRoute } from '#imports'
const showDrawer = ref(false) const showDrawer = ref(false)
@ -20,6 +20,7 @@ const openSwaggerLink = () => {
@click="showDrawer = true" @click="showDrawer = true"
> >
<MdiCommentTextOutline class="mr-1" /> <MdiCommentTextOutline class="mr-1" />
<!-- APIs & Support --> <!-- APIs & Support -->
<div>{{ $t('title.APIsAndSupport') }}</div> <div>{{ $t('title.APIsAndSupport') }}</div>
</div> </div>
@ -37,11 +38,13 @@ const openSwaggerLink = () => {
<!-- Help center --> <!-- Help center -->
<a-typography-title :level="4" class="!mb-6 !text-gray-500">{{ $t('title.helpCenter') }}</a-typography-title> <a-typography-title :level="4" class="!mb-6 !text-gray-500">{{ $t('title.helpCenter') }}</a-typography-title>
<GeneralSocialCard class="!w-full nc-social-card"> <LazyGeneralSocialCard class="!w-full nc-social-card">
<template #before> <template #before>
<a-list-item v-if="project"> <a-list-item v-if="project">
<nuxt-link <nuxt-link
v-e="['a:navbar:user:swagger']" v-e="['a:navbar:user:swagger']"
no-prefetch
no-rel
class="!no-underline !text-current py-4 font-semibold" class="!no-underline !text-current py-4 font-semibold"
target="_blank" target="_blank"
@click="openSwaggerLink" @click="openSwaggerLink"
@ -54,7 +57,8 @@ const openSwaggerLink = () => {
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
</template> </template>
</GeneralSocialCard> </LazyGeneralSocialCard>
<div class="min-h-10 w-full" /> <div class="min-h-10 w-full" />
</div> </div>
</a-drawer> </a-drawer>

3
packages/nc-gui/components/general/MiniSidebar.vue

@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { navigateTo } from '#app' import { computed, navigateTo, useGlobal, useProject, useRoute, useSidebar } from '#imports'
import { computed, useGlobal, useProject, useRoute, useSidebar } from '#imports'
const { signOut, signedIn, user, currentVersion } = useGlobal() const { signOut, signedIn, user, currentVersion } = useGlobal()

23
packages/nc-gui/components/general/ReleaseInfo.vue

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { computed, extractSdkResponseErrorMsg, message, onMounted, useGlobal, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg, onMounted } from '#imports'
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -47,22 +46,38 @@ onMounted(async () => await fetchReleaseInfo())
<mdi-menu-down /> <mdi-menu-down />
</div> </div>
</a-button> </a-button>
<template #overlay> <template #overlay>
<div class="mt-1 bg-white shadow-lg !border"> <div class="mt-1 bg-white shadow-lg !border">
<nuxt-link class="!text-primary !no-underline" to="https://github.com/nocodb/nocodb/releases" target="_blank"> <nuxt-link
no-prefetch
no-rel
class="!text-primary !no-underline"
to="https://github.com/nocodb/nocodb/releases"
target="_blank"
>
<div class="nc-menu-item"> <div class="nc-menu-item">
<mdi-script-text-outline /> <mdi-script-text-outline />
{{ latestRelease }} {{ $t('activity.upgrade.releaseNote') }} {{ latestRelease }} {{ $t('activity.upgrade.releaseNote') }}
</div> </div>
</nuxt-link> </nuxt-link>
<nuxt-link class="!text-primary !no-underline" to="https://docs.nocodb.com/getting-started/upgrading" target="_blank">
<nuxt-link
no-prefetch
no-rel
class="!text-primary !no-underline"
to="https://docs.nocodb.com/getting-started/upgrading"
target="_blank"
>
<div class="nc-menu-item"> <div class="nc-menu-item">
<mdi-rocket-launch-outline /> <mdi-rocket-launch-outline />
<!-- How to upgrade? --> <!-- How to upgrade? -->
{{ $t('activity.upgrade.howTo') }} {{ $t('activity.upgrade.howTo') }}
</div> </div>
</nuxt-link> </nuxt-link>
<a-divider class="!m-0" /> <a-divider class="!m-0" />
<div class="nc-menu-item" @click="releaseAlert = false"> <div class="nc-menu-item" @click="releaseAlert = false">
<mdi-close /> <mdi-close />
<!-- Hide menu --> <!-- Hide menu -->

2
packages/nc-gui/components/general/Share.vue

@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from '#imports'
interface Props { interface Props {
url: string url: string
socialMedias: string[] socialMedias: string[]

3
packages/nc-gui/components/general/ShareBaseButton.vue

@ -21,10 +21,11 @@ const { isUIAllowed } = useUIPermission()
> >
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<MdiAccountPlusOutline class="mr-1 nc-share-base" /> <MdiAccountPlusOutline class="mr-1 nc-share-base" />
<div>{{ $t('activity.inviteTeam') }}</div> <div>{{ $t('activity.inviteTeam') }}</div>
</div> </div>
</div> </div>
<TabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" /> <LazyTabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" />
</div> </div>
</template> </template>

10
packages/nc-gui/components/general/Social.vue

@ -1,9 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from 'vue-i18n' import { useI18n } from '#imports'
import MdiDiscord from '~icons/mdi/discord'
import MdiReddit from '~icons/mdi/reddit'
import MdiTwitter from '~icons/mdi/twitter'
import MdiCalendarMonth from '~icons/mdi/calendar-month'
const { locale } = useI18n() const { locale } = useI18n()
@ -24,6 +20,7 @@ const isZhLang = $computed(() => locale.value.startsWith('zh'))
<div v-else class="flex justify-between gap-1 w-full px-2"> <div v-else class="flex justify-between gap-1 w-full px-2">
<MdiDiscord v-e="['e:community:discord']" class="icon text-[#7289DA]" @click="open('https://discord.gg/5RgZmkW')" /> <MdiDiscord v-e="['e:community:discord']" class="icon text-[#7289DA]" @click="open('https://discord.gg/5RgZmkW')" />
<div <div
v-e="['e:community:discourse']" v-e="['e:community:discourse']"
class="icon flex items-center justify-center min-w-[43px]" class="icon flex items-center justify-center min-w-[43px]"
@ -31,8 +28,11 @@ const isZhLang = $computed(() => locale.value.startsWith('zh'))
> >
<div class="discourse" /> <div class="discourse" />
</div> </div>
<MdiReddit v-e="['e:community:reddit']" class="icon text-[#FF4600]" @click="open('https://www.reddit.com/r/NocoDB/')" /> <MdiReddit v-e="['e:community:reddit']" class="icon text-[#FF4600]" @click="open('https://www.reddit.com/r/NocoDB/')" />
<MdiTwitter v-e="['e:community:twitter']" class="icon text-[#1DA1F2]" @click="open('https://twitter.com/NocoDB')" /> <MdiTwitter v-e="['e:community:twitter']" class="icon text-[#1DA1F2]" @click="open('https://twitter.com/NocoDB')" />
<MdiCalendarMonth <MdiCalendarMonth
v-e="['e:community:book-demo']" v-e="['e:community:book-demo']"
class="icon text-green-500" class="icon text-green-500"

9
packages/nc-gui/components/general/SocialCard.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { enumColor as colors } from '#imports' import { enumColor as colors, useGlobal } from '#imports'
const { lang: currentLang } = useGlobal() const { lang: currentLang } = useGlobal()
@ -26,6 +26,7 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:api-docs']" v-e="['e:api-docs']"
@ -42,6 +43,7 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:community:github']" v-e="['e:community:github']"
@ -70,6 +72,7 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:community:book-demo']" v-e="['e:community:book-demo']"
@ -88,6 +91,7 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:community:discord']" v-e="['e:community:discord']"
@ -106,6 +110,7 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:community:twitter']" v-e="['e:community:twitter']"
@ -124,6 +129,7 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:hiring']" v-e="['e:hiring']"
@ -141,6 +147,7 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:community:reddit']" v-e="['e:community:reddit']"

4
packages/nc-gui/components/general/Tooltip.vue

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onKeyStroke } from '@vueuse/core' import { onKeyStroke } from '@vueuse/core'
import { ref, watch } from '#imports'
interface Props { interface Props {
// Key to be pressed on hover to trigger the tooltip // Key to be pressed on hover to trigger the tooltip
@ -7,9 +8,10 @@ interface Props {
wrapperClass?: string wrapperClass?: string
} }
const { modifierKey } = defineProps<Props>() const { modifierKey, wrapperClass } = defineProps<Props>()
const showTooltip = ref(false) const showTooltip = ref(false)
const isMouseOver = ref(false) const isMouseOver = ref(false)
if (modifierKey) { if (modifierKey) {

32
packages/nc-gui/components/general/TruncateText.vue

@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from '#imports'
interface Props { interface Props {
placement?: placement?:
| 'top' | 'top'
@ -16,29 +18,31 @@ interface Props {
length?: number length?: number
} }
const props = withDefaults(defineProps<Props>(), { const { placement = 'bottom', length = 20 } = defineProps<Props>()
placement: 'bottom',
length: 20,
})
const text = ref() const text = ref()
const enableTooltip = computed(() => text?.value?.textContent.length > props.length)
const enableTooltip = computed(() => text.value?.textContent.length > length)
const shortName = computed(() => const shortName = computed(() =>
text?.value?.textContent.length > props.length text.value?.textContent.length > length ? `${text.value?.textContent.substr(0, length - 3)}...` : text.value?.textContent,
? `${text?.value?.textContent.substr(0, props.length - 3)}...`
: text?.value?.textContent,
) )
</script> </script>
<template> <template>
<a-tooltip v-if="enableTooltip" :placement="props.placement"> <a-tooltip v-if="enableTooltip" :placement="placement">
<template #title> <template #title>
<slot></slot> <slot />
</template> </template>
<div>{{ shortName }}</div> <div>{{ shortName }}</div>
</a-tooltip> </a-tooltip>
<div v-else><slot></slot></div>
<div ref="text" class="hidden"><slot></slot></div>
</template>
<style scoped></style> <div v-else>
<slot />
</div>
<div ref="text" class="hidden">
<slot />
</div>
</template>

2
packages/nc-gui/components/smartsheet/Form.vue

@ -25,7 +25,7 @@ import {
useViewData, useViewData,
watch, watch,
} from '#imports' } from '#imports'
import type { Permission } from '~/composables/useUIPermission/rolePermissions' import type { Permission } from '~/lib'
provide(IsFormInj, ref(true)) provide(IsFormInj, ref(true))
provide(IsGalleryInj, ref(false)) provide(IsGalleryInj, ref(false))

2
packages/nc-gui/components/smartsheet/Gallery.vue

@ -25,7 +25,7 @@ import {
useViewData, useViewData,
watch, watch,
} from '#imports' } from '#imports'
import type { Row as RowType } from '~/composables' import type { Row as RowType } from '~/lib'
interface Attachment { interface Attachment {
url: string url: string

2
packages/nc-gui/components/smartsheet/Grid.vue

@ -34,7 +34,7 @@ import {
useViewData, useViewData,
watch, watch,
} from '#imports' } from '#imports'
import type { Row } from '~/composables' import type { Row } from '~/lib'
import { NavigateDir } from '~/lib' import { NavigateDir } from '~/lib'
const { t } = useI18n() const { t } = useI18n()

2
packages/nc-gui/components/smartsheet/Row.vue

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Row } from '~/composables' import type { Row } from '~/lib'
import { import {
ReloadRowDataHookInj, ReloadRowDataHookInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,

2
packages/nc-gui/components/smartsheet/VirtualCell.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import { ActiveCellInj, CellValueInj, ColumnInj, RowInj, provide, toRef, useVirtualCell } from '#imports' import { ActiveCellInj, CellValueInj, ColumnInj, RowInj, provide, toRef, useVirtualCell } from '#imports'
import type { Row } from '~/composables' import type { Row } from '~/lib'
import { NavigateDir } from '~/lib' import { NavigateDir } from '~/lib'
const props = defineProps<{ const props = defineProps<{

6
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -100,7 +100,11 @@ watch(inputs, () => {
overlay-class-name="nc-dropdown-select-color-options" overlay-class-name="nc-dropdown-select-color-options"
> >
<template #overlay> <template #overlay>
<LazyGeneralColorPicker v-model="element.color" :pick-button="true" @update:model-value="colorMenus[index] = false" /> <LazyGeneralColorPicker
v-model="element.color"
:pick-button="true"
@update:model-value="colorMenus[index] = false"
/>
</template> </template>
<MdiArrowDownDropCircle :style="{ 'font-size': '1.5em', 'color': element.color }" class="mr-2" /> <MdiArrowDownDropCircle :style="{ 'font-size': '1.5em', 'color': element.color }" class="mr-2" />
</a-dropdown> </a-dropdown>

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

@ -18,7 +18,7 @@ import {
useVModel, useVModel,
watch, watch,
} from '#imports' } from '#imports'
import type { Row } from '~/composables' import type { Row } from '~/lib'
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean

2
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -1,11 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Modal } from 'ant-design-vue'
import type { LinkToAnotherRecordType } from 'nocodb-sdk' import type { LinkToAnotherRecordType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { import {
ColumnInj, ColumnInj,
IsLockedInj, IsLockedInj,
MetaInj, MetaInj,
Modal,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
inject, inject,
message, message,

3
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -12,7 +12,6 @@ import {
message, message,
onMounted, onMounted,
ref, ref,
resolveComponent,
useApi, useApi,
useDialog, useDialog,
useI18n, useI18n,
@ -189,7 +188,7 @@ async function onRename(view: ViewType) {
function openDeleteDialog(view: Record<string, any>) { function openDeleteDialog(view: Record<string, any>) {
const isOpen = ref(true) const isOpen = ref(true)
const { close } = useDialog(resolveComponent('LazyDlgViewDelete') as any, { const { close } = useDialog(() => import('~/components/dlg/ViewDelete.vue'), {
'modelValue': isOpen, 'modelValue': isOpen,
'view': view, 'view': view,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,

15
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -2,7 +2,7 @@
import type { SelectProps } from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { MetaInj, computed, inject, ref, resolveComponent } from '#imports' import { MetaInj, computed, inject, ref } from '#imports'
const { modelValue, isSort } = defineProps<{ const { modelValue, isSort } = defineProps<{
modelValue?: string modelValue?: string
@ -26,7 +26,7 @@ const options = computed<SelectProps['options']>(() =>
return !( return !(
c.uidt === UITypes.LinkToAnotherRecord && (c.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO c.uidt === UITypes.LinkToAnotherRecord && (c.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO
) )
/** ignore vutual fields which are system fields ( mm relation ) */ /** ignore virtual fields which are system fields ( mm relation ) */
} else { } else {
return !c.colOptions || !c.system return !c.colOptions || !c.system
} }
@ -34,14 +34,9 @@ const options = computed<SelectProps['options']>(() =>
.map((c: ColumnType) => ({ .map((c: ColumnType) => ({
value: c.id, value: c.id,
label: c.title, label: c.title,
icon: h( icon: h(isVirtualCol(c) ? () => import('../header/VirtualCellIcon.vue') : () => import('../header/CellIcon.vue'), {
isVirtualCol(c) columnMeta: c,
? resolveComponent('LazySmartsheetHeaderVirtualCellIcon') }),
: resolveComponent('LazySmartsheetHeaderCellIcon'),
{
columnMeta: c,
},
),
c, c,
})), })),
) )

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

@ -111,12 +111,9 @@ const coverOptions = computed<SelectProps['options']>(() => {
}) })
const getIcon = (c: ColumnType) => const getIcon = (c: ColumnType) =>
h( h(isVirtualCol(c) ? () => import('../header/VirtualCellIcon.vue') : () => import('../header/CellIcon.vue'), {
isVirtualCol(c) ? resolveComponent('LazySmartsheetHeaderVirtualCellIcon') : resolveComponent('LazySmartsheetHeaderCellIcon'), columnMeta: c,
{ })
columnMeta: c,
},
)
</script> </script>
<template> <template>

2
packages/nc-gui/components/smartsheet/toolbar/SharedViewList.vue

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import { Empty } from 'ant-design-vue'
import { import {
Empty,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
message, message,
onMounted, onMounted,

6
packages/nc-gui/components/tabs/Auth.vue

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import UserManagement from './auth/UserManagement.vue'
import ApiTokenManagement from './auth/ApiTokenManagement.vue'
import { useI18n, useUIPermission } from '#imports' import { useI18n, useUIPermission } from '#imports'
interface Tab { interface Tab {
@ -18,13 +16,13 @@ const tabsInfo: Tab[] = [
{ {
title: 'Users Management', title: 'Users Management',
label: t('title.userMgmt'), label: t('title.userMgmt'),
body: () => UserManagement, body: () => import('./auth/UserManagement.vue'),
isUIAllowed: () => isUIAllowed('userMgmtTab'), isUIAllowed: () => isUIAllowed('userMgmtTab'),
}, },
{ {
title: 'API Token Management', title: 'API Token Management',
label: t('title.apiTokenMgmt'), label: t('title.apiTokenMgmt'),
body: () => ApiTokenManagement, body: () => import('./auth/ApiTokenManagement.vue'),
isUIAllowed: () => isUIAllowed('apiTokenTab'), isUIAllowed: () => isUIAllowed('apiTokenTab'),
}, },
] ]

4
packages/nc-gui/components/tabs/Smartsheet.vue

@ -17,9 +17,10 @@ import {
ref, ref,
useMetas, useMetas,
useProvideSmartsheetStore, useProvideSmartsheetStore,
useSidebar,
watch, watch,
} from '#imports' } from '#imports'
import type { TabItem } from '~/composables' import type { TabItem } from '~/lib'
const { activeTab } = defineProps<{ const { activeTab } = defineProps<{
activeTab: TabItem activeTab: TabItem
@ -37,6 +38,7 @@ provide(TabMetaInj, ref(activeTab))
const meta = computed<TableType>(() => metas.value?.[activeTab?.id as string]) const meta = computed<TableType>(() => metas.value?.[activeTab?.id as string])
const reloadEventHook = createEventHook() const reloadEventHook = createEventHook()
const reloadViewMetaEventHook = createEventHook() const reloadViewMetaEventHook = createEventHook()
const openNewRecordFormHook = createEventHook() const openNewRecordFormHook = createEventHook()

41
packages/nc-gui/components/tabs/auth/ApiTokenManagement.vue

@ -1,15 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ApiTokenType } from 'nocodb-sdk' import type { ApiTokenType } from 'nocodb-sdk'
import { message } from 'ant-design-vue' import { extractSdkResponseErrorMsg, message, onMounted, useCopy, useI18n, useNuxtApp, useProject } from '#imports'
import { extractSdkResponseErrorMsg, useCopy, useI18n } from '#imports'
import KebabIcon from '~icons/ic/baseline-more-vert'
import MdiPlusIcon from '~icons/mdi/plus'
import CloseIcon from '~icons/material-symbols/close-rounded'
import ReloadIcon from '~icons/mdi/reload'
import VisibilityOpenIcon from '~icons/material-symbols/visibility'
import VisibilityCloseIcon from '~icons/material-symbols/visibility-off'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import MdiContentCopyIcon from '~icons/mdi/content-copy'
interface ApiToken extends ApiTokenType { interface ApiToken extends ApiTokenType {
show?: boolean show?: boolean
@ -108,13 +99,15 @@ onMounted(() => {
<div class="relative flex flex-col h-full"> <div class="relative flex flex-col h-full">
<a-button type="text" class="!absolute top-0 right-0 rounded-md -mt-2 -mr-3" @click="showNewTokenModal = false"> <a-button type="text" class="!absolute top-0 right-0 rounded-md -mt-2 -mr-3" @click="showNewTokenModal = false">
<template #icon> <template #icon>
<CloseIcon class="flex mx-auto" /> <MaterialSymbolsCloseRounded class="flex mx-auto" />
</template> </template>
</a-button> </a-button>
<!-- Generate Token --> <!-- Generate Token -->
<div class="flex flex-row justify-center w-full -mt-1 mb-3"> <div class="flex flex-row justify-center w-full -mt-1 mb-3">
<a-typography-title :level="5">{{ $t('title.generateToken') }}</a-typography-title> <a-typography-title :level="5">{{ $t('title.generateToken') }}</a-typography-title>
</div> </div>
<!-- Description --> <!-- Description -->
<a-form <a-form
ref="form" ref="form"
@ -137,6 +130,7 @@ onMounted(() => {
</a-form> </a-form>
</div> </div>
</a-modal> </a-modal>
<a-modal <a-modal
v-model:visible="showDeleteTokenModal" v-model:visible="showDeleteTokenModal"
:closable="false" :closable="false"
@ -147,63 +141,73 @@ onMounted(() => {
> >
<div class="flex flex-col h-full"> <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 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"> <div class="flex mt-6 justify-center space-x-2">
<a-button @click="showDeleteTokenModal = false"> {{ $t('general.cancel') }} </a-button> <a-button @click="showDeleteTokenModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="deleteToken()"> {{ $t('general.confirm') }} </a-button> <a-button type="primary" danger @click="deleteToken()"> {{ $t('general.confirm') }} </a-button>
</div> </div>
</div> </div>
</a-modal> </a-modal>
<div class="flex flex-col px-10 mt-6"> <div class="flex flex-col px-10 mt-6">
<div class="flex flex-row justify-end"> <div class="flex flex-row justify-end">
<div class="flex flex-row space-x-1"> <div class="flex flex-row space-x-1">
<a-button size="middle" type="text" @click="loadApiTokens()"> <a-button size="middle" type="text" @click="loadApiTokens()">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1"> <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">{{ $t('general.reload') }}</div> <div class="text-gray-500">{{ $t('general.reload') }}</div>
</div> </div>
</a-button> </a-button>
<!-- Add New Token --> <!-- Add New Token -->
<a-button size="middle" type="primary" ghost @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"> <div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MdiPlusIcon /> <MdiPlus />
<div>{{ $t('activity.newToken') }}</div> <div>{{ $t('activity.newToken') }}</div>
</div> </div>
</a-button> </a-button>
</div> </div>
</div> </div>
<div v-if="tokensInfo" class="w-full flex flex-col mt-2 px-1"> <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 flex-row border-b-1 text-gray-600 text-xs pb-2 pt-2">
<div class="flex w-4/10 pl-2">{{ $t('labels.description') }}</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-4/10 justify-center">{{ $t('labels.token') }}</div>
<div class="flex w-2/10 justify-end pr-2">{{ $t('labels.action') }}</div> <div class="flex w-2/10 justify-end pr-2">{{ $t('labels.action') }}</div>
</div> </div>
<div v-for="(item, index) in tokensInfo" :key="index" class="flex flex-col"> <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"> <div class="flex flex-row border-b-1 items-center px-2 py-2">
<div class="flex flex-row w-4/10 flex-wrap overflow-ellipsis"> <div class="flex flex-row w-4/10 flex-wrap overflow-ellipsis">
{{ item.description }} {{ item.description }}
</div> </div>
<div class="flex w-4/10 justify-center flex-wrap overflow-ellipsis"> <div class="flex w-4/10 justify-center flex-wrap overflow-ellipsis">
<span v-if="item.show">{{ item.token }}</span> <span v-if="item.show">{{ item.token }}</span>
<span v-else>****************************************</span> <span v-else>****************************************</span>
</div> </div>
<div class="flex flex-row w-2/10 justify-end"> <div class="flex flex-row w-2/10 justify-end">
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<span v-if="item.show"> {{ $t('general.hide') }} </span> <span v-if="item.show"> {{ $t('general.hide') }} </span>
<span v-else> {{ $t('general.show') }} </span> <span v-else> {{ $t('general.show') }} </span>
</template> </template>
<a-button type="text" class="!rounded-md" @click="item.show = !item.show"> <a-button type="text" class="!rounded-md" @click="item.show = !item.show">
<template #icon> <template #icon>
<VisibilityCloseIcon v-if="item.show" class="flex mx-auto h-[1.1rem]" /> <MaterialSymbolsVisibilityOff v-if="item.show" class="flex mx-auto h-[1.1rem]" />
<VisibilityOpenIcon v-else class="flex mx-auto h-[1rem]" /> <MaterialSymbolsVisibility v-else class="flex mx-auto h-[1rem]" />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> {{ $t('general.copy') }} </template> <template #title> {{ $t('general.copy') }} </template>
<a-button type="text" class="!rounded-md" @click="copyToken(item.token)"> <a-button type="text" class="!rounded-md" @click="copyToken(item.token)">
<template #icon> <template #icon>
<MdiContentCopyIcon class="flex mx-auto h-[1rem]" /> <MdiContentCopy class="flex mx-auto h-[1rem]" />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -212,15 +216,16 @@ onMounted(() => {
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<a-button type="text" class="!px-0"> <a-button type="text" class="!px-0">
<div class="flex flex-row items-center h-[1.2rem]"> <div class="flex flex-row items-center h-[1.2rem]">
<KebabIcon /> <IcBaselineMoreVert />
</div> </div>
</a-button> </a-button>
</div> </div>
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item> <a-menu-item>
<div class="flex flex-row items-center py-3 h-[1rem]" @click="openDeleteModal(item)"> <div class="flex flex-row items-center py-3 h-[1rem]" @click="openDeleteModal(item)">
<MdiDeleteOutlineIcon class="flex" /> <MdiDeleteOutline class="flex" />
<div class="text-xs pl-2">{{ $t('general.remove') }}</div> <div class="text-xs pl-2">{{ $t('general.remove') }}</div>
</div> </div>
</a-menu-item> </a-menu-item>

19
packages/nc-gui/components/tabs/auth/UserManagement.vue

@ -1,10 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue'
import type { RequestParams } from 'nocodb-sdk' import type { RequestParams } from 'nocodb-sdk'
import UsersModal from './user-management/UsersModal.vue'
import FeedbackForm from './user-management/FeedbackForm.vue'
import { import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
message,
onBeforeMount, onBeforeMount,
ref, ref,
useApi, useApi,
@ -169,13 +167,14 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div> </div>
<div v-else class="flex flex-col w-full px-6"> <div v-else class="flex flex-col w-full px-6">
<UsersModal <LazyTabsAuthUserManagementUsersModal
:key="showUserModal" :key="showUserModal"
:show="showUserModal" :show="showUserModal"
:selected-user="selectedUser" :selected-user="selectedUser"
@closed="showUserModal = false" @closed="showUserModal = false"
@reload="loadUsers()" @reload="loadUsers()"
/> />
<a-modal <a-modal
v-model:visible="showUserDeleteModal" v-model:visible="showUserDeleteModal"
:closable="false" :closable="false"
@ -194,6 +193,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div> </div>
</div> </div>
</a-modal> </a-modal>
<div class="flex flex-row mb-4 mx-4 justify-between pb-2"> <div class="flex flex-row mb-4 mx-4 justify-between pb-2">
<div class="flex w-1/3"> <div class="flex w-1/3">
<a-input v-model:value="searchText" :placeholder="$t('placeholder.filterByEmail')"> <a-input v-model:value="searchText" :placeholder="$t('placeholder.filterByEmail')">
@ -210,6 +210,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="text-gray-500">{{ $t('general.reload') }}</div> <div class="text-gray-500">{{ $t('general.reload') }}</div>
</div> </div>
</a-button> </a-button>
<a-button <a-button
v-if="isUIAllowed('newUser')" v-if="isUIAllowed('newUser')"
v-e="['c:user:invite']" v-e="['c:user:invite']"
@ -226,6 +227,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</a-button> </a-button>
</div> </div>
</div> </div>
<div class="px-5"> <div class="px-5">
<div class="flex flex-row border-b-1 pb-2 px-2"> <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"> <div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
@ -262,17 +264,20 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<template #title> <template #title>
<span>{{ $t('activity.editUser') }}</span> <span>{{ $t('activity.editUser') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md nc-user-edit" @click="onEdit(user)"> <a-button type="text" class="!rounded-md nc-user-edit" @click="onEdit(user)">
<template #icon> <template #icon>
<IcRoundEdit class="flex mx-auto h-[1rem] text-gray-500" /> <IcRoundEdit class="flex mx-auto h-[1rem] text-gray-500" />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<!-- Add user to project --> <!-- Add user to project -->
<a-tooltip v-if="!user.project_id" placement="bottom"> <a-tooltip v-if="!user.project_id" placement="bottom">
<template #title> <template #title>
<span>{{ $t('activity.addUserToProject') }}</span> <span>{{ $t('activity.addUserToProject') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md nc-user-invite" @click="inviteUser(user)"> <a-button type="text" class="!rounded-md nc-user-invite" @click="inviteUser(user)">
<template #icon> <template #icon>
<MdiPlus class="flex mx-auto h-[1.1rem] text-gray-500" /> <MdiPlus class="flex mx-auto h-[1.1rem] text-gray-500" />
@ -285,6 +290,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<template #title> <template #title>
<span>{{ $t('activity.deleteUser') }}</span> <span>{{ $t('activity.deleteUser') }}</span>
</template> </template>
<a-button v-e="['c:user:delete']" type="text" class="!rounded-md nc-user-delete" @click="onDelete(user)"> <a-button v-e="['c:user:delete']" type="text" class="!rounded-md nc-user-delete" @click="onDelete(user)">
<template #icon> <template #icon>
<MdiDeleteOutline class="flex mx-auto h-[1.1rem] text-gray-500" /> <MdiDeleteOutline class="flex mx-auto h-[1.1rem] text-gray-500" />
@ -300,6 +306,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div> </div>
</a-button> </a-button>
</div> </div>
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item> <a-menu-item>
@ -320,6 +327,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</a-dropdown> </a-dropdown>
</div> </div>
</div> </div>
<a-pagination <a-pagination
v-model:current="currentPage" v-model:current="currentPage"
hide-on-single-page hide-on-single-page
@ -329,7 +337,8 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
show-less-items show-less-items
@change="loadUsers" @change="loadUsers"
/> />
<FeedbackForm />
<LazyTabsAuthUserManagementFeedbackForm />
</div> </div>
</div> </div>
</template> </template>

3
packages/nc-gui/components/tabs/auth/user-management/FeedbackForm.vue

@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, useGlobal } from '#imports'
const { feedbackForm } = useGlobal() const { feedbackForm } = useGlobal()
const showForm = ref(false) const showForm = ref(false)
// todo: why this timeout?
setTimeout(() => (showForm.value = true), 60000) setTimeout(() => (showForm.value = true), 60000)
</script> </script>

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

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { extractSdkResponseErrorMsg, message, onMounted, useCopy, useDashboard, useI18n, useNuxtApp, useProject } from '#imports'
import { extractSdkResponseErrorMsg, onMounted, useCopy, useI18n, useNuxtApp, useProject } from '#imports'
interface ShareBase { interface ShareBase {
uuid?: string uuid?: string
@ -143,43 +142,52 @@ onMounted(() => {
<div class="text-xs">{{ $t('activity.shareBase.link') }}</div> <div class="text-xs">{{ $t('activity.shareBase.link') }}</div>
</div> </div>
<div v-if="base?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm w-full justify-between"> <div v-if="base?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm w-full justify-between">
<span class="flex text-xs overflow-x-hidden overflow-ellipsis text-gray-700 pl-2 nc-url">{{ url }}</span> <span class="flex text-xs overflow-x-hidden overflow-ellipsis text-gray-700 pl-2 nc-url">{{ url }}</span>
<div class="flex border-l-1 pt-1 pl-1"> <div class="flex border-l-1 pt-1 pl-1">
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<span>{{ $t('general.reload') }}</span> <span>{{ $t('general.reload') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="recreate"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="recreate">
<template #icon> <template #icon>
<MdiReload class="flex mx-auto text-gray-600" /> <MdiReload class="flex mx-auto text-gray-600" />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<span>{{ $t('activity.copyUrl') }}</span> <span>{{ $t('activity.copyUrl') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="copyUrl"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="copyUrl">
<template #icon> <template #icon>
<MdiContentCopy class="flex mx-auto text-gray-600" /> <MdiContentCopy class="flex mx-auto text-gray-600" />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<span>{{ $t('activity.openTab') }}</span> <span>{{ $t('activity.openTab') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="navigateToSharedBase"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="navigateToSharedBase">
<template #icon> <template #icon>
<MdiOpenInNew class="flex mx-auto text-gray-600" /> <MdiOpenInNew class="flex mx-auto text-gray-600" />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
<span>{{ $t('activity.iFrame') }}</span> <span>{{ $t('activity.iFrame') }}</span>
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="generateEmbeddableIframe"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="generateEmbeddableIframe">
<template #icon> <template #icon>
<MdiXml class="flex mx-auto text-gray-600" /> <MdiXml class="flex mx-auto text-gray-600" />
@ -188,8 +196,10 @@ onMounted(() => {
</a-tooltip> </a-tooltip>
</div> </div>
</div> </div>
<!-- Generate publicly shareable readonly base --> <!-- Generate publicly shareable readonly base -->
<div class="flex text-xs text-gray-500 mt-2 justify-start ml-2">{{ $t('msg.info.generatePublicShareableReadonlyBase') }}</div> <div class="flex text-xs text-gray-500 mt-2 justify-start ml-2">{{ $t('msg.info.generatePublicShareableReadonlyBase') }}</div>
<div class="mt-4 flex flex-row justify-between mx-1"> <div class="mt-4 flex flex-row justify-between mx-1">
<a-dropdown v-model="showEditBaseDropdown" class="flex" overlay-class-name="nc-dropdown-shared-base-toggle"> <a-dropdown v-model="showEditBaseDropdown" class="flex" overlay-class-name="nc-dropdown-shared-base-toggle">
<a-button> <a-button>
@ -221,6 +231,7 @@ onMounted(() => {
<IcRoundKeyboardArrowDown class="text-black -mt-0.5 h-[1rem]" /> <IcRoundKeyboardArrowDown class="text-black -mt-0.5 h-[1rem]" />
</div> </div>
</template> </template>
<a-select-option <a-select-option
v-for="(role, index) in [ShareBaseRole.Editor, ShareBaseRole.Viewer]" v-for="(role, index) in [ShareBaseRole.Editor, ShareBaseRole.Viewer]"
:key="index" :key="index"

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

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Form, message } from 'ant-design-vue'
import ShareBase from './ShareBase.vue'
import { import {
Form,
computed, computed,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
isEmail, isEmail,
message,
onMounted, onMounted,
projectRoleTagColors, projectRoleTagColors,
projectRoles, projectRoles,
@ -36,11 +36,15 @@ const emit = defineEmits(['closed', 'reload'])
const { t } = useI18n() const { t } = useI18n()
const { project } = useProject() const { project } = useProject()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { copy } = useCopy() const { copy } = useCopy()
const { dashboardUrl } = $(useDashboard()) const { dashboardUrl } = $(useDashboard())
const usersData = $ref<Users>({ emails: undefined, role: ProjectRole.Viewer, invitationToken: undefined }) const usersData = $ref<Users>({ emails: undefined, role: ProjectRole.Viewer, invitationToken: undefined })
const formRef = ref() const formRef = ref()
const useForm = Form.useForm const useForm = Form.useForm
@ -144,6 +148,7 @@ const clickInviteMore = () => {
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full"> <div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full">
<a-typography-title class="select-none" :level="4"> {{ $t('activity.share') }}: {{ project.title }} </a-typography-title> <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')"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')">
<template #icon> <template #icon>
<MaterialSymbolsCloseRounded class="flex mx-auto" /> <MaterialSymbolsCloseRounded class="flex mx-auto" />
@ -165,6 +170,7 @@ const clickInviteMore = () => {
<div class="flex pl-2 text-green-700 text-xs"> <div class="flex pl-2 text-green-700 text-xs">
{{ inviteUrl }} {{ inviteUrl }}
</div> </div>
<a-button type="text" class="!rounded-md -mt-0.5" @click="copyUrl"> <a-button type="text" class="!rounded-md -mt-0.5" @click="copyUrl">
<template #icon> <template #icon>
<MdiContentCopy class="flex mx-auto text-green-700 h-[1rem]" /> <MdiContentCopy class="flex mx-auto text-green-700 h-[1rem]" />
@ -178,21 +184,25 @@ const clickInviteMore = () => {
{{ $t('msg.info.userInviteNoSMTP') }} {{ $t('msg.info.userInviteNoSMTP') }}
{{ usersData.invitationToken && usersData.emails }} {{ usersData.invitationToken && usersData.emails }}
</div> </div>
<div class="flex flex-row justify-start mt-4 ml-2"> <div class="flex flex-row justify-start mt-4 ml-2">
<a-button size="small" outlined @click="clickInviteMore"> <a-button size="small" outlined @click="clickInviteMore">
<div class="flex flex-row justify-center items-center space-x-0.5"> <div class="flex flex-row justify-center items-center space-x-0.5">
<MaterialSymbolsSendOutline 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 class="text-xs text-gray-600">{{ $t('activity.inviteMore') }}</div>
</div> </div>
</a-button> </a-button>
</div> </div>
</div> </div>
</template> </template>
<div v-else class="flex flex-col pb-4"> <div v-else class="flex flex-col pb-4">
<div class="flex flex-row items-center pl-2 pb-1 h-[1rem]"> <div class="flex flex-row items-center pl-2 pb-1 h-[1rem]">
<MdiAccountOutline /> <MdiAccountOutline />
<div class="text-xs ml-0.5 mt-0.5">{{ selectedUser ? $t('activity.editUser') : $t('activity.inviteTeam') }}</div> <div class="text-xs ml-0.5 mt-0.5">{{ selectedUser ? $t('activity.editUser') : $t('activity.inviteTeam') }}</div>
</div> </div>
<div class="border-1 py-3 px-4 rounded-md mt-1"> <div class="border-1 py-3 px-4 rounded-md mt-1">
<a-form <a-form
ref="formRef" ref="formRef"
@ -210,6 +220,7 @@ const clickInviteMore = () => {
:rules="[{ required: true, message: 'Please input email' }]" :rules="[{ required: true, message: 'Please input email' }]"
> >
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('datatype.Email') }}:</div> <div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('datatype.Email') }}:</div>
<a-input <a-input
v-model:value="usersData.emails" v-model:value="usersData.emails"
validate-trigger="onBlur" validate-trigger="onBlur"
@ -218,9 +229,11 @@ const clickInviteMore = () => {
/> />
</a-form-item> </a-form-item>
</div> </div>
<div class="flex flex-col w-1/4"> <div class="flex flex-col w-1/4">
<a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]"> <a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]">
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div> <div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div>
<a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role"> <a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role">
<a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role" class="nc-role-option"> <a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role" class="nc-role-option">
<div class="flex flex-row h-full justify-start items-center"> <div class="flex flex-row h-full justify-start items-center">
@ -236,9 +249,11 @@ const clickInviteMore = () => {
</a-form-item> </a-form-item>
</div> </div>
</div> </div>
<div class="flex flex-row justify-center"> <div class="flex flex-row justify-center">
<a-button type="primary" html-type="submit"> <a-button type="primary" html-type="submit">
<div v-if="selectedUser">{{ $t('general.save') }}</div> <div v-if="selectedUser">{{ $t('general.save') }}</div>
<div v-else class="flex flex-row justify-center items-center space-x-1.5"> <div v-else class="flex flex-row justify-center items-center space-x-1.5">
<MaterialSymbolsSendOutline class="flex h-[0.8rem]" /> <MaterialSymbolsSendOutline class="flex h-[0.8rem]" />
<div>{{ $t('activity.invite') }}</div> <div>{{ $t('activity.invite') }}</div>
@ -248,12 +263,11 @@ const clickInviteMore = () => {
</a-form> </a-form>
</div> </div>
</div> </div>
<div class="flex mt-4"> <div class="flex mt-4">
<ShareBase /> <LazyTabsAuthUserManagementShareBase />
</div> </div>
</div> </div>
</div> </div>
</a-modal> </a-modal>
</template> </template>
<style scoped></style>

8
packages/nc-gui/components/template/Editor.vue

@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk' import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { Empty, Form, message } from 'ant-design-vue'
import { srcDestMappingColumns, tableColumns } from './utils' import { srcDestMappingColumns, tableColumns } from './utils'
import { import {
Empty,
Form,
MetaInj, MetaInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,
computed, computed,
@ -12,6 +13,7 @@ import {
fieldRequiredValidator, fieldRequiredValidator,
getUIDTIcon, getUIDTIcon,
inject, inject,
message,
nextTick, nextTick,
onMounted, onMounted,
reactive, reactive,
@ -22,7 +24,7 @@ import {
useTabs, useTabs,
useTemplateRefsList, useTemplateRefsList,
} from '#imports' } from '#imports'
import { TabType } from '~/composables' import { TabType } from '~/lib'
const { quickImportType, projectTemplate, importData, importColumns, importOnly, maxRowsToParse } = defineProps<Props>() const { quickImportType, projectTemplate, importData, importColumns, importOnly, maxRowsToParse } = defineProps<Props>()
@ -546,6 +548,7 @@ function handleEditableTnChange(idx: number) {
available for import available for import
</p> </p>
</a-form> </a-form>
<a-collapse v-if="data.tables && data.tables.length" v-model:activeKey="expansionPanel" class="template-collapse" accordion> <a-collapse v-if="data.tables && data.tables.length" v-model:activeKey="expansionPanel" class="template-collapse" accordion>
<a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx"> <a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx">
<template #header> <template #header>
@ -603,6 +606,7 @@ function handleEditableTnChange(idx: number) {
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-card> </a-card>
<a-card v-else> <a-card v-else>
<a-form :model="data" name="template-editor-form" @keydown.enter="emit('import')"> <a-form :model="data" name="template-editor-form" @keydown.enter="emit('import')">
<p v-if="data.tables && quickImportType === 'excel'" class="text-center"> <p v-if="data.tables && quickImportType === 'excel'" class="text-center">

21
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -5,10 +5,13 @@ import {
ActiveCellInj, ActiveCellInj,
CellValueInj, CellValueInj,
ColumnInj, ColumnInj,
IsFormInj,
IsLockedInj,
ReadonlyInj, ReadonlyInj,
ReloadRowDataHookInj, ReloadRowDataHookInj,
RowInj, RowInj,
defineAsyncComponent, computed,
createEventHook,
inject, inject,
ref, ref,
useProvideLTARStore, useProvideLTARStore,
@ -18,10 +21,6 @@ import {
import MdiArrowExpand from '~icons/mdi/arrow-expand' import MdiArrowExpand from '~icons/mdi/arrow-expand'
import MdiPlus from '~icons/mdi/plus' import MdiPlus from '~icons/mdi/plus'
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
const ListItems = defineAsyncComponent(() => import('./components/ListItems.vue'))
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const reloadRowTrigger = inject(ReloadRowDataHookInj, createEventHook()) const reloadRowTrigger = inject(ReloadRowDataHookInj, createEventHook())
@ -66,7 +65,7 @@ const value = computed(() => {
const unlinkRef = async (rec: Record<string, any>) => { const unlinkRef = async (rec: Record<string, any>) => {
if (isNew.value) { if (isNew.value) {
removeLTARRef(rec, column?.value as ColumnType) await removeLTARRef(rec, column?.value as ColumnType)
} else { } else {
await unlink(rec) await unlink(rec)
} }
@ -77,9 +76,14 @@ const unlinkRef = async (rec: Record<string, any>) => {
<div class="flex w-full chips-wrapper items-center" :class="{ active }"> <div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="chips flex items-center flex-1"> <div class="chips flex items-center flex-1">
<template v-if="value && relatedTablePrimaryValueProp"> <template v-if="value && relatedTablePrimaryValueProp">
<ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" /> <LazyVirtualCellComponentsItemChip
:item="value"
:value="value[relatedTablePrimaryValueProp]"
@unlink="unlinkRef(value)"
/>
</template> </template>
</div> </div>
<div <div
v-if="!readOnly && !isLocked && (isUIAllowed('xcDatatableEditable') || isForm)" v-if="!readOnly && !isLocked && (isUIAllowed('xcDatatableEditable') || isForm)"
class="flex justify-end gap-1 min-h-[30px] items-center" class="flex justify-end gap-1 min-h-[30px] items-center"
@ -90,7 +94,8 @@ const unlinkRef = async (rec: Record<string, any>) => {
@click="listItemsDlg = true" @click="listItemsDlg = true"
/> />
</div> </div>
<ListItems v-model="listItemsDlg" @attach-record="listItemsDlg = true" />
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" @attach-record="listItemsDlg = true" />
</div> </div>
</template> </template>

2
packages/nc-gui/components/virtual-cell/Count.vue

@ -5,5 +5,3 @@
<template> <template>
<span class="prose-sm"></span> <span class="prose-sm"></span>
</template> </template>
<style scoped lang="scss"></style>

5
packages/nc-gui/components/virtual-cell/Formula.vue

@ -20,7 +20,7 @@ const showEditFormulaWarningMessage = () => {
}, 3000) }, 3000)
} }
const result = isPg ? handleTZ(value) : value const result = computed(() => (isPg.value ? handleTZ(value) : value))
const urls = computed(() => replaceUrlsWithLink(result.value)) const urls = computed(() => replaceUrlsWithLink(result.value))
</script> </script>
@ -31,12 +31,15 @@ const urls = computed(() => replaceUrlsWithLink(result.value))
<template #title> <template #title>
<span class="font-bold">{{ column.colOptions.error }}</span> <span class="font-bold">{{ column.colOptions.error }}</span>
</template> </template>
<span>ERR!</span> <span>ERR!</span>
</a-tooltip> </a-tooltip>
<div class="p-2" @dblclick="showEditFormulaWarningMessage"> <div class="p-2" @dblclick="showEditFormulaWarningMessage">
<div v-if="urls" v-html="urls" /> <div v-if="urls" v-html="urls" />
<div v-else>{{ result }}</div> <div v-else>{{ result }}</div>
<div v-if="showEditFormulaWarning" class="text-left text-wrap mt-2 text-[#e65100]"> <div v-if="showEditFormulaWarning" class="text-left text-wrap mt-2 text-[#e65100]">
<!-- TODO: i18n --> <!-- TODO: i18n -->
Warning: Formula fields should be configured in the field menu dropdown. Warning: Formula fields should be configured in the field menu dropdown.

27
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -10,7 +10,6 @@ import {
ReloadRowDataHookInj, ReloadRowDataHookInj,
RowInj, RowInj,
computed, computed,
defineAsyncComponent,
inject, inject,
ref, ref,
useProvideLTARStore, useProvideLTARStore,
@ -18,12 +17,6 @@ import {
useUIPermission, useUIPermission,
} from '#imports' } from '#imports'
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
const ListItems = defineAsyncComponent(() => import('./components/ListItems.vue'))
const ListChildItems = defineAsyncComponent(() => import('./components/ListChildItems.vue'))
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const cellValue = inject(CellValueInj)! const cellValue = inject(CellValueInj)!
@ -52,6 +45,7 @@ const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvid
isNew, isNew,
reloadRowTrigger.trigger, reloadRowTrigger.trigger,
) )
await loadRelatedTableMeta() await loadRelatedTableMeta()
const localCellValue = computed<any[]>(() => { const localCellValue = computed<any[]>(() => {
@ -94,17 +88,26 @@ const onAttachRecord = () => {
<template v-if="!isForm"> <template v-if="!isForm">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden"> <div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells"> <template v-if="cells">
<ItemChip v-for="(cell, i) of cells" :key="i" :item="cell.item" :value="cell.value" @unlink="unlinkRef(cell.item)" /> <LazyVirtualCellComponentsItemChip
v-for="(cell, i) of cells"
:key="i"
:item="cell.item"
:value="cell.value"
@unlink="unlinkRef(cell.item)"
/>
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true"> <span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">
more... more...
</span> </span>
</template> </template>
</div> </div>
<div v-if="!isLocked" class="flex justify-end gap-1 min-h-[30px] items-center"> <div v-if="!isLocked" class="flex justify-end gap-1 min-h-[30px] items-center">
<MdiArrowExpand <MdiArrowExpand
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand" class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click="childListDlg = true" @click="childListDlg = true"
/> />
<MdiPlus <MdiPlus
v-if="!readOnly && isUIAllowed('xcDatatableEditable')" v-if="!readOnly && isUIAllowed('xcDatatableEditable')"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus" class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@ -113,9 +116,13 @@ const onAttachRecord = () => {
</div> </div>
</template> </template>
<ListItems v-model="listItemsDlg" /> <LazyVirtualCellComponentsListItems v-model="listItemsDlg" />
<ListChildItems v-model="childListDlg" :cell-value="localCellValue" @attach-record="onAttachRecord" /> <LazyVirtualCellComponentsListChildItems
v-model="childListDlg"
:cell-value="localCellValue"
@attach-record="onAttachRecord"
/>
</div> </div>
</template> </template>

6
packages/nc-gui/components/virtual-cell/Lookup.vue

@ -56,7 +56,7 @@ const lookupColumnMetaProps = useColumn(lookupColumn)
<template <template
v-if="lookupColumn.uidt === UITypes.LinkToAnotherRecord && lookupColumn.colOptions.type === RelationTypes.BELONGS_TO" v-if="lookupColumn.uidt === UITypes.LinkToAnotherRecord && lookupColumn.colOptions.type === RelationTypes.BELONGS_TO"
> >
<SmartsheetVirtualCell <LazySmartsheetVirtualCell
v-for="(v, i) of arrValue" v-for="(v, i) of arrValue"
:key="i" :key="i"
:edit-enabled="false" :edit-enabled="false"
@ -65,7 +65,7 @@ const lookupColumnMetaProps = useColumn(lookupColumn)
/> />
</template> </template>
<SmartsheetVirtualCell v-else :edit-enabled="false" :model-value="arrValue" :column="lookupColumn" /> <LazySmartsheetVirtualCell v-else :edit-enabled="false" :model-value="arrValue" :column="lookupColumn" />
</div> </div>
<!-- Render normal cell --> <!-- Render normal cell -->
@ -82,7 +82,7 @@ const lookupColumnMetaProps = useColumn(lookupColumn)
), ),
}" }"
> >
<SmartsheetCell :model-value="v" :column="lookupColumn" :edit-enabled="false" :virtual="true" /> <LazySmartsheetCell :model-value="v" :column="lookupColumn" :edit-enabled="false" :virtual="true" />
</div> </div>
</template> </template>
</template> </template>

24
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -10,6 +10,7 @@ import {
ReloadRowDataHookInj, ReloadRowDataHookInj,
RowInj, RowInj,
computed, computed,
createEventHook,
inject, inject,
ref, ref,
useProvideLTARStore, useProvideLTARStore,
@ -17,12 +18,6 @@ import {
useUIPermission, useUIPermission,
} from '#imports' } from '#imports'
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
const ListItems = defineAsyncComponent(() => import('./components/ListItems.vue'))
const ListChildItems = defineAsyncComponent(() => import('./components/ListChildItems.vue'))
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const row = inject(RowInj)! const row = inject(RowInj)!
@ -44,6 +39,7 @@ const childListDlg = ref(false)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore( const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
@ -93,7 +89,13 @@ const onAttachRecord = () => {
<template v-if="!isForm"> <template v-if="!isForm">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden"> <div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells"> <template v-if="cells">
<ItemChip v-for="(cell, i) of cells" :key="i" :item="cell.item" :value="cell.value" @unlink="unlinkRef(cell.item)" /> <LazyVirtualCellComponentsItemChip
v-for="(cell, i) of cells"
:key="i"
:item="cell.item"
:value="cell.value"
@unlink="unlinkRef(cell.item)"
/>
<span v-if="cells?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span> <span v-if="cells?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span>
</template> </template>
@ -113,9 +115,13 @@ const onAttachRecord = () => {
</div> </div>
</template> </template>
<ListItems v-model="listItemsDlg" /> <LazyVirtualCellComponentsListItems v-model="listItemsDlg" />
<ListChildItems v-model="childListDlg" :cell-value="localCellValue" @attach-record="onAttachRecord" /> <LazyVirtualCellComponentsListChildItems
v-model="childListDlg"
:cell-value="localCellValue"
@attach-record="onAttachRecord"
/>
</div> </div>
</template> </template>

15
packages/nc-gui/components/virtual-cell/components/ItemChip.vue

@ -1,14 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import { ActiveCellInj, IsFormInj, IsLockedInj, ReadonlyInj, inject, ref, useLTARStoreOrThrow, useUIPermission } from '#imports'
ActiveCellInj,
IsFormInj,
ReadonlyInj,
defineAsyncComponent,
inject,
ref,
useLTARStoreOrThrow,
useUIPermission,
} from '#imports'
interface Props { interface Props {
value?: string | number | boolean value?: string | number | boolean
@ -19,8 +10,6 @@ const { value, item } = defineProps<Props>()
const emit = defineEmits(['unlink']) const emit = defineEmits(['unlink'])
const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/expanded-form/index.vue'))
const { relatedTableMeta } = useLTARStoreOrThrow()! const { relatedTableMeta } = useLTARStoreOrThrow()!
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
@ -55,7 +44,7 @@ export default {
</div> </div>
<Suspense> <Suspense>
<ExpandedForm <LazySmartsheetExpandedForm
v-if="!readOnly && !isLocked && expandedFormDlg" v-if="!readOnly && !isLocked && expandedFormDlg"
v-model="expandedFormDlg" v-model="expandedFormDlg"
:row="{ row: item, rowMeta: {}, oldRow: { ...item } }" :row="{ row: item, rowMeta: {}, oldRow: { ...item } }"

21
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -1,24 +1,26 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Empty, Modal } from 'ant-design-vue'
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import { import {
ColumnInj, ColumnInj,
Empty,
IsFormInj, IsFormInj,
IsPublicInj,
Modal,
ReadonlyInj, ReadonlyInj,
computed, computed,
h,
inject,
ref,
useLTARStoreOrThrow, useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useVModel, useVModel,
watch, watch,
} from '#imports' } from '#imports'
import { IsPublicInj } from '~/context'
const props = defineProps<{ modelValue?: boolean; cellValue: any }>() const props = defineProps<{ modelValue?: boolean; cellValue: any }>()
const emit = defineEmits(['update:modelValue', 'attachRecord']) const emit = defineEmits(['update:modelValue', 'attachRecord'])
const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/expanded-form/index.vue'))
const vModel = useVModel(props, 'modelValue', emit) const vModel = useVModel(props, 'modelValue', emit)
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
@ -54,7 +56,7 @@ watch(
const unlinkRow = async (row: Record<string, any>) => { const unlinkRow = async (row: Record<string, any>) => {
if (isNew.value) { if (isNew.value) {
removeLTARRef(row, column?.value as ColumnType) await removeLTARRef(row, column?.value as ColumnType)
} else { } else {
await unlink(row) await unlink(row)
await loadChildrenList() await loadChildrenList()
@ -63,12 +65,12 @@ const unlinkRow = async (row: Record<string, any>) => {
const unlinkIfNewRow = async (row: Record<string, any>) => { const unlinkIfNewRow = async (row: Record<string, any>) => {
if (isNew.value) { if (isNew.value) {
removeLTARRef(row, column?.value as ColumnType) await removeLTARRef(row, column?.value as ColumnType)
} }
} }
const container = computed(() => const container = computed(() =>
isForm?.value isForm.value
? h('div', { ? h('div', {
class: 'w-full p-2', class: 'w-full p-2',
}) })
@ -76,6 +78,7 @@ const container = computed(() =>
) )
const expandedFormDlg = ref(false) const expandedFormDlg = ref(false)
const expandedFormRow = ref() const expandedFormRow = ref()
/** reload children list whenever cell value changes and list is visible */ /** reload children list whenever cell value changes and list is visible */
@ -108,6 +111,7 @@ watch(
</div> </div>
</a-button> </a-button>
</div> </div>
<template v-if="(isNew && state?.[column?.title]?.length) || childrenList?.pageInfo?.totalRows"> <template v-if="(isNew && state?.[column?.title]?.length) || childrenList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0 scrollbar-thin-dull px-12 cursor-pointer"> <div class="flex-1 overflow-auto min-h-0 scrollbar-thin-dull px-12 cursor-pointer">
<a-card <a-card
@ -127,6 +131,7 @@ watch(
{{ row[relatedTablePrimaryValueProp] }} {{ row[relatedTablePrimaryValueProp] }}
<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span> <span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div> </div>
<div v-if="!readonly" class="flex gap-2"> <div v-if="!readonly" class="flex gap-2">
<MdiLinkVariantRemove <MdiLinkVariantRemove
class="text-xs text-grey hover:(!text-red-500) cursor-pointer" class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
@ -163,7 +168,7 @@ watch(
</div> </div>
<Suspense> <Suspense>
<ExpandedForm <LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg" v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg" v-model="expandedFormDlg"
:row="{ row: expandedFormRow }" :row="{ row: expandedFormRow }"

15
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -1,11 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { RelationTypes, UITypes } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { Empty } from 'ant-design-vue'
import { import {
ColumnInj, ColumnInj,
Empty,
computed, computed,
defineAsyncComponent,
inject, inject,
ref, ref,
useLTARStoreOrThrow, useLTARStoreOrThrow,
@ -19,8 +18,6 @@ const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits(['update:modelValue', 'addNewRecord']) const emit = defineEmits(['update:modelValue', 'addNewRecord'])
const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/expanded-form/index.vue'))
const vModel = useVModel(props, 'modelValue', emit) const vModel = useVModel(props, 'modelValue', emit)
const column = inject(ColumnInj) const column = inject(ColumnInj)
@ -118,14 +115,18 @@ watch(expandedFormDlg, (nexVal) => {
:placeholder="$t('placeholder.filterQuery')" :placeholder="$t('placeholder.filterQuery')"
class="max-w-[200px]" class="max-w-[200px]"
size="small" size="small"
></a-input> />
<div class="flex-1" /> <div class="flex-1" />
<MdiReload class="cursor-pointer text-gray-500 nc-reload" @click="loadChildrenExcludedList" /> <MdiReload class="cursor-pointer text-gray-500 nc-reload" @click="loadChildrenExcludedList" />
<!-- Add new record --> <!-- Add new record -->
<a-button v-if="!isPublic" type="primary" size="small" @click="expandedFormDlg = true"> <a-button v-if="!isPublic" type="primary" size="small" @click="expandedFormDlg = true">
{{ $t('activity.addNewRecord') }} {{ $t('activity.addNewRecord') }}
</a-button> </a-button>
</div> </div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows"> <template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0 scrollbar-thin-dull px-12"> <div class="flex-1 overflow-auto min-h-0 scrollbar-thin-dull px-12">
<a-card <a-card
@ -140,6 +141,7 @@ watch(expandedFormDlg, (nexVal) => {
</span> </span>
</a-card> </a-card>
</div> </div>
<div class="flex justify-center mt-6"> <div class="flex justify-center mt-6">
<a-pagination <a-pagination
v-if="childrenExcludedList?.pageInfo" v-if="childrenExcludedList?.pageInfo"
@ -152,10 +154,11 @@ watch(expandedFormDlg, (nexVal) => {
/> />
</div> </div>
</template> </template>
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" /> <a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<Suspense> <Suspense>
<ExpandedForm <LazySmartsheetExpandedForm
v-if="expandedFormDlg" v-if="expandedFormDlg"
v-model="expandedFormDlg" v-model="expandedFormDlg"
:meta="relatedTableMeta" :meta="relatedTableMeta"

2
packages/nc-gui/components/webhook/ChannelMultiSelect.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from '@vue/runtime-core' import { onMounted, useVModel, watch } from '#imports'
interface Props { interface Props {
modelValue: Record<string, any>[] modelValue: Record<string, any>[]

5
packages/nc-gui/components/webhook/Drawer.vue

@ -34,8 +34,9 @@ async function editHook(hook: Record<string, any>) {
> >
<a-layout> <a-layout>
<a-layout-content class="px-10 py-5 scrollbar-thin-primary"> <a-layout-content class="px-10 py-5 scrollbar-thin-primary">
<WebhookEditor v-if="editOrAdd" ref="webhookEditorRef" @back-to-list="editOrAdd = false" /> <LazyWebhookEditor v-if="editOrAdd" ref="webhookEditorRef" @back-to-list="editOrAdd = false" />
<WebhookList v-else @edit="editHook" @add="editOrAdd = true" />
<LazyWebhookList v-else @edit="editHook" @add="editOrAdd = true" />
</a-layout-content> </a-layout-content>
<a-layout-footer class="!bg-white border-t flex"> <a-layout-footer class="!bg-white border-t flex">

37
packages/nc-gui/components/webhook/Editor.vue

@ -1,12 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { Form, message } from 'ant-design-vue'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { AuditType } from 'nocodb-sdk' import type { AuditType } from 'nocodb-sdk'
import { import {
Form,
MetaInj, MetaInj,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
fieldRequiredValidator, fieldRequiredValidator,
inject, inject,
message,
onMounted,
reactive, reactive,
useApi, useApi,
useI18n, useI18n,
@ -413,6 +415,7 @@ onMounted(async () => {
<span class="inline text-xl font-bold">{{ meta.title }} : {{ hook.title || 'Webhooks' }} </span> <span class="inline text-xl font-bold">{{ meta.title }} : {{ hook.title || 'Webhooks' }} </span>
</div> </div>
</div> </div>
<div> <div>
<a-button class="mr-3 nc-btn-webhook-test" size="large" @click="testWebhook"> <a-button class="mr-3 nc-btn-webhook-test" size="large" @click="testWebhook">
<div class="flex items-center"> <div class="flex items-center">
@ -421,6 +424,7 @@ onMounted(async () => {
Test Webhook Test Webhook
</div> </div>
</a-button> </a-button>
<a-button class="nc-btn-webhook-save" type="primary" size="large" @click.prevent="saveHooks"> <a-button class="nc-btn-webhook-save" type="primary" size="large" @click.prevent="saveHooks">
<div class="flex items-center"> <div class="flex items-center">
<MdiContentSave class="mr-2" /> <MdiContentSave class="mr-2" />
@ -430,7 +434,9 @@ onMounted(async () => {
</a-button> </a-button>
</div> </div>
</div> </div>
<a-divider /> <a-divider />
<a-form :model="hook" name="create-or-edit-webhook"> <a-form :model="hook" name="create-or-edit-webhook">
<a-form-item> <a-form-item>
<a-row type="flex"> <a-row type="flex">
@ -445,6 +451,7 @@ onMounted(async () => {
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-row type="flex" :gutter="[16, 16]"> <a-row type="flex" :gutter="[16, 16]">
<a-col :span="12"> <a-col :span="12">
<a-form-item v-bind="validateInfos.eventOperation"> <a-form-item v-bind="validateInfos.eventOperation">
@ -461,6 +468,7 @@ onMounted(async () => {
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item v-bind="validateInfos['notification.type']"> <a-form-item v-bind="validateInfos['notification.type']">
<a-select <a-select
@ -523,16 +531,20 @@ onMounted(async () => {
<a-col :span="24"> <a-col :span="24">
<a-tabs v-model:activeKey="urlTabKey" type="card" closeable="false" class="shadow-sm"> <a-tabs v-model:activeKey="urlTabKey" type="card" closeable="false" class="shadow-sm">
<a-tab-pane key="body" tab="Body"> <a-tab-pane key="body" tab="Body">
<MonacoEditor v-model="hook.notification.payload.body" :validate="false" class="min-h-60 max-h-80" /> <LazyMonacoEditor v-model="hook.notification.payload.body" :validate="false" class="min-h-60 max-h-80" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="params" tab="Params" force-render> <a-tab-pane key="params" tab="Params" force-render>
<ApiClientParams v-model="hook.notification.payload.parameters" /> <LazyApiClientParams v-model="hook.notification.payload.parameters" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="headers" tab="Headers" class="nc-tab-headers"> <a-tab-pane key="headers" tab="Headers" class="nc-tab-headers">
<ApiClientHeaders v-model="hook.notification.payload.headers" /> <LazyApiClientHeaders v-model="hook.notification.payload.headers" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="auth" tab="Auth"> <a-tab-pane key="auth" tab="Auth">
<MonacoEditor v-model="hook.notification.payload.auth" class="min-h-60 max-h-80" /> <LazyMonacoEditor v-model="hook.notification.payload.auth" class="min-h-60 max-h-80" />
<span class="text-gray-500 prose-sm p-2"> <span class="text-gray-500 prose-sm p-2">
For more about auth option refer For more about auth option refer
<a class="prose-sm" href="https://github.com/axios/axios#request-config" target="_blank">axios docs</a>. <a class="prose-sm" href="https://github.com/axios/axios#request-config" target="_blank">axios docs</a>.
@ -545,7 +557,7 @@ onMounted(async () => {
<a-row v-if="hook.notification.type === 'Slack'" type="flex"> <a-row v-if="hook.notification.type === 'Slack'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
<WebhookChannelMultiSelect <LazyWebhookChannelMultiSelect
v-if="slackChannels.length > 0" v-if="slackChannels.length > 0"
v-model="hook.notification.payload.channels" v-model="hook.notification.payload.channels"
:selected-channel-list="hook.notification.payload.channels" :selected-channel-list="hook.notification.payload.channels"
@ -559,7 +571,7 @@ onMounted(async () => {
<a-row v-if="hook.notification.type === 'Microsoft Teams'" type="flex"> <a-row v-if="hook.notification.type === 'Microsoft Teams'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
<WebhookChannelMultiSelect <LazyWebhookChannelMultiSelect
v-if="teamsChannels.length > 0" v-if="teamsChannels.length > 0"
v-model="hook.notification.payload.channels" v-model="hook.notification.payload.channels"
:selected-channel-list="hook.notification.payload.channels" :selected-channel-list="hook.notification.payload.channels"
@ -573,7 +585,7 @@ onMounted(async () => {
<a-row v-if="hook.notification.type === 'Discord'" type="flex"> <a-row v-if="hook.notification.type === 'Discord'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
<WebhookChannelMultiSelect <LazyWebhookChannelMultiSelect
v-if="discordChannels.length > 0" v-if="discordChannels.length > 0"
v-model="hook.notification.payload.channels" v-model="hook.notification.payload.channels"
:selected-channel-list="hook.notification.payload.channels" :selected-channel-list="hook.notification.payload.channels"
@ -587,7 +599,7 @@ onMounted(async () => {
<a-row v-if="hook.notification.type === 'Mattermost'" type="flex"> <a-row v-if="hook.notification.type === 'Mattermost'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
<WebhookChannelMultiSelect <LazyWebhookChannelMultiSelect
v-if="mattermostChannels.length > 0" v-if="mattermostChannels.length > 0"
v-model="hook.notification.payload.channels" v-model="hook.notification.payload.channels"
:selected-channel-list="hook.notification.payload.channels" :selected-channel-list="hook.notification.payload.channels"
@ -603,6 +615,7 @@ onMounted(async () => {
<a-form-item v-if="input.type === 'LongText'" v-bind="validateInfos[`notification.payload.${input.key}`]"> <a-form-item v-if="input.type === 'LongText'" v-bind="validateInfos[`notification.payload.${input.key}`]">
<a-textarea v-model:value="hook.notification.payload[input.key]" :placeholder="input.label" size="large" /> <a-textarea v-model:value="hook.notification.payload[input.key]" :placeholder="input.label" size="large" />
</a-form-item> </a-form-item>
<a-form-item v-else v-bind="validateInfos[`notification.payload.${input.key}`]"> <a-form-item v-else v-bind="validateInfos[`notification.payload.${input.key}`]">
<a-input v-model:value="hook.notification.payload[input.key]" :placeholder="input.label" size="large" /> <a-input v-model:value="hook.notification.payload[input.key]" :placeholder="input.label" size="large" />
</a-form-item> </a-form-item>
@ -613,7 +626,8 @@ onMounted(async () => {
<a-col :span="24"> <a-col :span="24">
<a-card> <a-card>
<a-checkbox v-model:checked="hook.condition" class="nc-check-box-hook-condition">On Condition</a-checkbox> <a-checkbox v-model:checked="hook.condition" class="nc-check-box-hook-condition">On Condition</a-checkbox>
<SmartsheetToolbarColumnFilter
<LazySmartsheetToolbarColumnFilter
v-if="hook.condition" v-if="hook.condition"
ref="filterRef" ref="filterRef"
:auto-save="false" :auto-save="false"
@ -644,7 +658,8 @@ onMounted(async () => {
</a> </a>
</div> </div>
</div> </div>
<WebhookTest
<LazyWebhookTest
ref="webhookTestRef" ref="webhookTestRef"
:hook="{ :hook="{
...hook, ...hook,

11
packages/nc-gui/components/webhook/List.vue

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { MetaInj, extractSdkResponseErrorMsg, inject, message, onMounted, ref, useI18n, useNuxtApp } from '#imports'
import { MetaInj, extractSdkResponseErrorMsg, inject, onMounted, ref, useI18n, useNuxtApp } from '#imports'
const emit = defineEmits(['edit', 'add']) const emit = defineEmits(['edit', 'add'])
@ -54,6 +53,7 @@ onMounted(() => {
<div class=""> <div class="">
<div class="mb-2"> <div class="mb-2">
<div class="float-left font-bold text-xl mt-2 mb-4">{{ meta.title }} : Webhooks</div> <div class="float-left font-bold text-xl mt-2 mb-4">{{ meta.title }} : Webhooks</div>
<a-button <a-button
v-e="['c:webhook:add']" v-e="['c:webhook:add']"
class="float-right nc-btn-create-webhook" class="float-right nc-btn-create-webhook"
@ -64,7 +64,9 @@ onMounted(() => {
{{ $t('activity.addWebhook') }} {{ $t('activity.addWebhook') }}
</a-button> </a-button>
</div> </div>
<a-divider /> <a-divider />
<div v-if="hooks.length" class=""> <div v-if="hooks.length" class="">
<a-list item-layout="horizontal" :data-source="hooks" class="cursor-pointer scrollbar-thin-primary"> <a-list item-layout="horizontal" :data-source="hooks" class="cursor-pointer scrollbar-thin-primary">
<template #renderItem="{ item, index }"> <template #renderItem="{ item, index }">
@ -73,21 +75,25 @@ onMounted(() => {
<template #description> <template #description>
<span class="uppercase"> {{ item.event }} {{ item.operation }}</span> <span class="uppercase"> {{ item.event }} {{ item.operation }}</span>
</template> </template>
<template #title> <template #title>
<span class="text-xl normal-case"> <span class="text-xl normal-case">
{{ item.title }} {{ item.title }}
</span> </span>
</template> </template>
<template #avatar> <template #avatar>
<div class="mt-4"> <div class="mt-4">
<MdiHook class="text-xl" /> <MdiHook class="text-xl" />
</div> </div>
</template> </template>
</a-list-item-meta> </a-list-item-meta>
<template #extra> <template #extra>
<div> <div>
<!-- Notify Via --> <!-- Notify Via -->
<div class="mr-2">{{ $t('labels.notifyVia') }} : {{ item?.notification?.type }}</div> <div class="mr-2">{{ $t('labels.notifyVia') }} : {{ item?.notification?.type }}</div>
<div class="float-right pt-2 pr-1"> <div class="float-right pt-2 pr-1">
<MdiDeleteOutline class="text-xl nc-hook-delete-icon" @click.stop="deleteHook(item, index)" /> <MdiDeleteOutline class="text-xl nc-hook-delete-icon" @click.stop="deleteHook(item, index)" />
</div> </div>
@ -97,6 +103,7 @@ onMounted(() => {
</template> </template>
</a-list> </a-list>
</div> </div>
<div v-else class="min-h-[75vh]"> <div v-else class="min-h-[75vh]">
<div class="p-4 bg-gray-100 text-gray-600"> <div class="p-4 bg-gray-100 text-gray-600">
Webhooks list is empty, create new webhook by clicking 'Create webhook' button. Webhooks list is empty, create new webhook by clicking 'Create webhook' button.

5
packages/nc-gui/components/webhook/Test.vue

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { MetaInj, extractSdkResponseErrorMsg, inject, message, onMounted, ref, useI18n, useNuxtApp, watch } from '#imports'
import { MetaInj, extractSdkResponseErrorMsg, onMounted, useI18n } from '#imports'
interface Props { interface Props {
hook: Record<string, any> hook: Record<string, any>
@ -58,7 +57,7 @@ onMounted(async () => {
<template> <template>
<a-collapse v-model:activeKey="activeKey" ghost> <a-collapse v-model:activeKey="activeKey" ghost>
<a-collapse-panel key="1" header="Sample Payload"> <a-collapse-panel key="1" header="Sample Payload">
<MonacoEditor v-model="sampleData" class="min-h-60 max-h-80" /> <LazyMonacoEditor v-model="sampleData" class="min-h-60 max-h-80" />
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</template> </template>

28
packages/nc-gui/composables/index.ts

@ -1,28 +0,0 @@
export * from './useApi'
export * from './useDialog'
export * from './useGlobal'
export * from './useInjectionState'
export * from './useRoles'
export * from './useSidebar'
export * from './useTheme'
export * from './useUIPermission'
export * from './useColors'
export * from './useColumn'
export * from './useGridViewColumnWidth'
export * from './useMetas'
export * from './useProject'
export * from './useTable'
export * from './useTabs'
export * from './useViewColumns'
export * from './useViewData'
export * from './useViewFilters'
export * from './useViews'
export * from './useViewSorts'
export * from './useVirtualCell'
export * from './useColumnCreateStore'
export * from './useSmartsheetStore'
export * from './useLTARStore'
export * from './useExpandedFormStore'
export * from './useSharedFormViewStore'
export * from './useCellUrlConfig'
export * from './useCopy'

3
packages/nc-gui/composables/useApi/index.ts

@ -3,8 +3,7 @@ import { Api } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { CreateApiOptions, UseApiProps, UseApiReturn } from './types' import type { CreateApiOptions, UseApiProps, UseApiReturn } from './types'
import { addAxiosInterceptors } from './interceptors' import { addAxiosInterceptors } from './interceptors'
import { BASE_URL } from '~/lib' import { BASE_URL, createEventHook, extractSdkResponseErrorMsg, ref, unref, useCounter, useGlobal, useNuxtApp } from '#imports'
import { createEventHook, extractSdkResponseErrorMsg, ref, unref, useCounter, useGlobal, useNuxtApp } from '#imports'
export function createApiInstance<SecurityDataType = any>({ baseURL = BASE_URL }: CreateApiOptions = {}): Api<SecurityDataType> { export function createApiInstance<SecurityDataType = any>({ baseURL = BASE_URL }: CreateApiOptions = {}): Api<SecurityDataType> {
const { appInfo } = $(useGlobal()) const { appInfo } = $(useGlobal())

4
packages/nc-gui/composables/useColors.ts

@ -1,7 +1,5 @@
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
import { computed, effectScope, tryOnScopeDispose, unref, watch, watchEffect } from '#build/imports' import { computed, effectScope, theme, tryOnScopeDispose, unref, useNuxtApp, watch, watchEffect } from '#imports'
import { useNuxtApp } from '#app'
import { theme } from '~/utils'
export function useColors(darkMode?: MaybeRef<boolean>) { export function useColors(darkMode?: MaybeRef<boolean>) {
const scope = effectScope() const scope = effectScope()

17
packages/nc-gui/composables/useColumn.ts

@ -1,22 +1,23 @@
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import { SqlUiFactory, UITypes, isVirtualCol } from 'nocodb-sdk' import { SqlUiFactory, UITypes, isVirtualCol } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { useProject } from '#imports' import { computed, useProject } from '#imports'
export function useColumn(column: Ref<ColumnType>) { export function useColumn(column: Ref<ColumnType | undefined>) {
const { project } = useProject() const { project } = useProject()
const uiDatatype: ComputedRef<UITypes> = computed(() => column?.value?.uidt as UITypes) const uiDatatype: ComputedRef<UITypes> = computed(() => column.value?.uidt as UITypes)
const abstractType = computed(() => { const abstractType = computed(() => {
// kludge: CY test hack; column.value is being received NULL during attach cell delete operation // kludge: CY test hack; column.value is being received NULL during attach cell delete operation
return isVirtualCol(column?.value) || !column?.value return (column.value && isVirtualCol(column.value)) || !column.value
? null ? null
: SqlUiFactory.create( : SqlUiFactory.create(
project.value?.bases?.[0]?.type ? { client: project.value.bases[0].type } : { client: 'mysql2' }, project.value?.bases?.[0]?.type ? { client: project.value.bases[0].type } : { client: 'mysql2' },
).getAbstractType(column?.value) ).getAbstractType(column.value)
}) })
const dataTypeLow = computed(() => column?.value?.dt?.toLowerCase()) const dataTypeLow = computed(() => column.value?.dt?.toLowerCase())
const isBoolean = computed(() => abstractType.value === 'boolean') const isBoolean = computed(() => abstractType.value === 'boolean')
const isString = computed(() => uiDatatype.value === UITypes.SingleLineText || abstractType.value === 'string') const isString = computed(() => uiDatatype.value === UITypes.SingleLineText || abstractType.value === 'string')
const isTextArea = computed(() => uiDatatype.value === UITypes.LongText) const isTextArea = computed(() => uiDatatype.value === UITypes.LongText)
@ -60,9 +61,7 @@ export function useColumn(column: Ref<ColumnType>) {
const isManualSaved = computed(() => const isManualSaved = computed(() =>
[UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(uiDatatype.value), [UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(uiDatatype.value),
) )
const isPrimary = computed(() => { const isPrimary = computed(() => column.value?.pv)
return column?.value?.pv
})
return { return {
abstractType, abstractType,

22
packages/nc-gui/composables/useColumnCreateStore.ts

@ -1,10 +1,20 @@
import clone from 'just-clone' import clone from 'just-clone'
import { Form, message } from 'ant-design-vue'
import type { ColumnType, TableType } from 'nocodb-sdk' import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { useI18n } from 'vue-i18n' import {
import { computed, createInjectionState, extractSdkResponseErrorMsg, useNuxtApp } from '#imports' Form,
computed,
createInjectionState,
extractSdkResponseErrorMsg,
message,
ref,
useI18n,
useMetas,
useNuxtApp,
useProject,
watch,
} from '#imports'
const useForm = Form.useForm const useForm = Form.useForm
@ -13,9 +23,13 @@ const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState( const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => { (meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => {
const { sqlUi } = useProject() const { sqlUi } = useProject()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { getMeta } = useMetas() const { getMeta } = useMetas()
const { t } = useI18n() const { t } = useI18n()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const isEdit = computed(() => !!column.value?.id) const isEdit = computed(() => !!column.value?.id)
@ -239,6 +253,8 @@ export { useProvideColumnCreateStore }
export function useColumnCreateStoreOrThrow() { export function useColumnCreateStoreOrThrow() {
const columnCreateStore = useColumnCreateStore() const columnCreateStore = useColumnCreateStore()
if (columnCreateStore == null) throw new Error('Please call `useProvideColumnCreateStore` on the appropriate parent component') if (columnCreateStore == null) throw new Error('Please call `useProvideColumnCreateStore` on the appropriate parent component')
return columnCreateStore return columnCreateStore
} }

2
packages/nc-gui/composables/useCopy.ts

@ -15,7 +15,7 @@ export const useCopy = () => {
const { copy: _copy, isSupported } = useClipboard() const { copy: _copy, isSupported } = useClipboard()
const copy = async (text: string) => { const copy = async (text: string) => {
if (isSupported) { if (isSupported.value) {
await _copy(text) await _copy(text)
} else { } else {
copyFallback(text) copyFallback(text)

3
packages/nc-gui/composables/useDashboard.ts

@ -1,5 +1,8 @@
import { computed, useRoute } from '#imports'
export function useDashboard() { export function useDashboard() {
const route = useRoute() const route = useRoute()
const dashboardUrl = computed(() => { const dashboardUrl = computed(() => {
// todo: test in different scenarios // todo: test in different scenarios
// get base path of app // get base path of app

2
packages/nc-gui/composables/useDialog/index.ts

@ -36,7 +36,7 @@ import { createEventHook, h, ref, toReactive, tryOnScopeDispose, useNuxtApp, wat
* } * }
*/ */
export function useDialog( export function useDialog(
componentOrVNode: DefineComponent<any, any, any> | VNode, componentOrVNode: any,
props: NonNullable<Parameters<typeof h>[1]> = {}, props: NonNullable<Parameters<typeof h>[1]> = {},
mountTarget?: Element | ComponentPublicInstance, mountTarget?: Element | ComponentPublicInstance,
) { ) {

52
packages/nc-gui/composables/useExpandedFormStore.ts

@ -1,50 +1,61 @@
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import type { ColumnType, TableType } from 'nocodb-sdk' import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useI18n } from 'vue-i18n'
import { import {
NOCO, NOCO,
computed,
extractPkFromRow, extractPkFromRow,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
getHTMLEncodedText, getHTMLEncodedText,
message,
ref,
useApi, useApi,
useI18n,
useInjectionState, useInjectionState,
useNuxtApp, useNuxtApp,
useProject, useProject,
useProvideSmartsheetRowStore, useProvideSmartsheetRowStore,
useSharedView,
} from '#imports' } from '#imports'
import type { Row } from '~/composables' import type { Row } from '~/lib'
const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((meta: Ref<TableType>, row: Ref<Row>) => { const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((meta: Ref<TableType>, row: Ref<Row>) => {
const { $e, $state, $api } = useNuxtApp() const { $e, $state, $api } = useNuxtApp()
const { api, isLoading: isCommentsLoading, error: commentsError } = useApi() const { api, isLoading: isCommentsLoading, error: commentsError } = useApi()
const { t } = useI18n() const { t } = useI18n()
const commentsOnly = ref(false) const commentsOnly = ref(false)
const commentsAndLogs = ref<any[]>([]) const commentsAndLogs = ref<any[]>([])
const comment = ref('') const comment = ref('')
const commentsDrawer = ref(false) const commentsDrawer = ref(false)
const changedColumns = ref(new Set<string>()) const changedColumns = ref(new Set<string>())
const { project } = useProject() const { project } = useProject()
const rowStore = useProvideSmartsheetRowStore(meta, row)
const { sharedView } = useSharedView() as Record<string, any>
// todo const rowStore = useProvideSmartsheetRowStore(meta, row)
// const activeView = inject(ActiveViewInj)
// const { updateOrSaveRow, insertRow } = useViewData(meta, activeView as any) const { sharedView } = useSharedView()
// getters // getters
const primaryValue = computed(() => { const primaryValue = computed(() => {
if (row?.value?.row) { if (row?.value?.row) {
const col = meta?.value?.columns?.find((c) => c.pv) const col = meta?.value?.columns?.find((c) => c.pv)
if (!col) { if (!col) {
return return
} }
const value = row.value.row?.[col.title as string] const value = row.value.row?.[col.title as string]
const uidt = col.uidt const uidt = col.uidt
if (uidt === UITypes.Date) { if (uidt === UITypes.Date) {
return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD') return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD')
} else if (uidt === UITypes.DateTime) { } else if (uidt === UITypes.DateTime) {
@ -73,8 +84,11 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
// actions // actions
const loadCommentsAndLogs = async () => { const loadCommentsAndLogs = async () => {
if (!row.value) return if (!row.value) return
const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!rowId) return if (!rowId) return
commentsAndLogs.value = commentsAndLogs.value =
( (
await api.utils.commentList({ await api.utils.commentList({
@ -92,7 +106,9 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const saveComment = async () => { const saveComment = async () => {
try { try {
if (!row.value || !comment.value) return if (!row.value || !comment.value) return
const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!rowId) return if (!rowId) return
await api.utils.commentRow({ await api.utils.commentRow({
@ -114,14 +130,6 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const save = async () => { const save = async () => {
let data let data
try { try {
// todo:
// if (this.presetValues) {
// // cater presetValues
// for (const k in this.presetValues) {
// this.$set(this.changedColumns, k, true);
// }
// }
const updateOrInsertObj = [...changedColumns.value].reduce((obj, col) => { const updateOrInsertObj = [...changedColumns.value].reduce((obj, col) => {
obj[col] = row.value.row[col] obj[col] = row.value.row[col]
return obj return obj
@ -141,7 +149,9 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
if (!id) { if (!id) {
return message.info("Update not allowed for table which doesn't have primary Key") return message.info("Update not allowed for table which doesn't have primary Key")
} }
await $api.dbTableRow.update(NOCO, project.value.title as string, meta.value.title, id, updateOrInsertObj) await $api.dbTableRow.update(NOCO, project.value.title as string, meta.value.title, id, updateOrInsertObj)
for (const key of Object.keys(updateOrInsertObj)) { for (const key of Object.keys(updateOrInsertObj)) {
// audit // audit
$api.utils $api.utils
@ -164,11 +174,6 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
return message.info(t('msg.info.noColumnsToUpdate')) return message.info(t('msg.info.noColumnsToUpdate'))
} }
// this.$emit('update:oldRow', { ...this.localState });
// this.changedColumns = {};
// this.$emit('input', this.localState);
// this.$emit('update:isNew', false);
message.success(`${primaryValue.value || 'Row'} updated successfully.`) message.success(`${primaryValue.value || 'Row'} updated successfully.`)
changedColumns.value = new Set() changedColumns.value = new Set()
@ -182,7 +187,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const loadRow = async (rowId?: string) => { const loadRow = async (rowId?: string) => {
const record = await $api.dbTableRow.read( const record = await $api.dbTableRow.read(
NOCO, NOCO,
(project?.value?.id || sharedView.value.view.project_id) as string, // todo: project_id missing on view type
(project?.value?.id || (sharedView.value?.view as any)?.project_id) as string,
meta.value.title, meta.value.title,
rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]), rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]),
) )
@ -218,6 +224,8 @@ export { useProvideExpandedFormStore }
export function useExpandedFormStoreOrThrow() { export function useExpandedFormStoreOrThrow() {
const expandedFormStore = useExpandedFormStore() const expandedFormStore = useExpandedFormStore()
if (expandedFormStore == null) throw new Error('Please call `useExpandedFormStore` on the appropriate parent component') if (expandedFormStore == null) throw new Error('Please call `useExpandedFormStore` on the appropriate parent component')
return expandedFormStore return expandedFormStore
} }

14
packages/nc-gui/composables/useGlobal/actions.ts

@ -1,14 +1,7 @@
import { message } from 'ant-design-vue'
import { Api } from 'nocodb-sdk'
import type { Actions, State } from './types' import type { Actions, State } from './types'
import { getI18n } from '~/plugins/a.i18n' import { message, useNuxtApp } from '#imports'
export function useGlobalActions(state: State): Actions { export function useGlobalActions(state: State): Actions {
/** detached api instance, will not trigger global loading */
const api = new Api()
const { t } = getI18n().global
/** Sign out by deleting the token from localStorage */ /** Sign out by deleting the token from localStorage */
const signOut: Actions['signOut'] = () => { const signOut: Actions['signOut'] = () => {
state.token.value = null state.token.value = null
@ -32,7 +25,10 @@ export function useGlobalActions(state: State): Actions {
/** manually try to refresh token */ /** manually try to refresh token */
const refreshToken = async () => { const refreshToken = async () => {
api.instance const nuxtApp = useNuxtApp()
const t = nuxtApp.vueApp.i18n.global.t
nuxtApp.$api.instance
.post('/auth/refresh-token', null, { .post('/auth/refresh-token', null, {
withCredentials: true, withCredentials: true,
}) })

6
packages/nc-gui/composables/useGlobal/state.ts

@ -1,9 +1,7 @@
import { usePreferredLanguages, useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { useJwt } from '@vueuse/integrations/useJwt'
import type { JwtPayload } from 'jwt-decode' import type { JwtPayload } from 'jwt-decode'
import type { AppInfo, State, StoredState } from './types' import type { AppInfo, State, StoredState } from './types'
import { BASE_URL } from '~/lib' import { BASE_URL, computed, ref, toRefs, useCounter, useJwt, useNuxtApp, usePreferredLanguages, useTimestamp } from '#imports'
import { computed, ref, toRefs, useCounter, useNuxtApp, useTimestamp } from '#imports'
import type { Language, User } from '~/lib' import type { Language, User } from '~/lib'
export function useGlobalState(storageKey = 'nocodb-gui-v2'): State { export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {

12
packages/nc-gui/composables/useGridViewColumnWidth.ts

@ -1,14 +1,14 @@
import { useStyleTag } from '@vueuse/core'
import type { ColumnType, GridColumnType, GridType, ViewType } from 'nocodb-sdk' import type { ColumnType, GridColumnType, GridType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { useMetas } from './useMetas' import { IsPublicInj, computed, inject, ref, useMetas, useNuxtApp, useStyleTag, useUIPermission, watch } from '#imports'
import { useUIPermission } from './useUIPermission'
import { IsPublicInj } from '~/context'
export function useGridViewColumnWidth(view: Ref<GridType | undefined>) { export function useGridViewColumnWidth(view: Ref<GridType | undefined>) {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('') const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { metas } = useMetas() const { metas } = useMetas()
const gridViewCols = ref<Record<string, GridColumnType>>({}) const gridViewCols = ref<Record<string, GridColumnType>>({})
@ -53,14 +53,14 @@ export function useGridViewColumnWidth(view: Ref<GridType | undefined>) {
/** when columns changes(create/delete) reload grid columns */ /** when columns changes(create/delete) reload grid columns */
watch(columns, loadGridViewColumns) watch(columns, loadGridViewColumns)
const updateWidth = (id: string, width: string) => { const updateWidth = async (id: string, width: string) => {
if (gridViewCols?.value?.[id]) { if (gridViewCols?.value?.[id]) {
gridViewCols.value[id].width = width gridViewCols.value[id].width = width
} }
// sync with server if allowed // sync with server if allowed
if (!isPublic.value && isUIAllowed('gridColUpdate') && gridViewCols.value[id]?.id) { if (!isPublic.value && isUIAllowed('gridColUpdate') && gridViewCols.value[id]?.id) {
$api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, { await $api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, {
width, width,
}) })
} }

43
packages/nc-gui/composables/useLTARStore.ts

@ -1,21 +1,25 @@
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, RequestParams, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, PaginatedType, RequestParams, TableType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { Modal, message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { import {
IsPublicInj, IsPublicInj,
Modal,
NOCO, NOCO,
SharedViewPasswordInj,
computed, computed,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
inject,
message,
reactive, reactive,
ref, ref,
useI18n,
useInjectionState, useInjectionState,
useMetas, useMetas,
useNuxtApp, useNuxtApp,
useProject, useProject,
useSharedView,
watch,
} from '#imports' } from '#imports'
import type { Row } from '~/composables' import type { Row } from '~/lib'
import { SharedViewPasswordInj } from '~/context'
interface DataApiResponse { interface DataApiResponse {
list: Record<string, any> list: Record<string, any>
@ -27,29 +31,37 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
(column: Ref<Required<ColumnType>>, row: Ref<Row>, isNewRow: ComputedRef<boolean> | Ref<boolean>, reloadData = () => {}) => { (column: Ref<Required<ColumnType>>, row: Ref<Row>, isNewRow: ComputedRef<boolean> | Ref<boolean>, reloadData = () => {}) => {
// state // state
const { metas, getMeta } = useMetas() const { metas, getMeta } = useMetas()
const { project } = useProject() const { project } = useProject()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const sharedViewPassword = inject(SharedViewPasswordInj, ref(null)) const sharedViewPassword = inject(SharedViewPasswordInj, ref(null))
const childrenExcludedList = ref<DataApiResponse | undefined>() const childrenExcludedList = ref<DataApiResponse | undefined>()
const childrenList = ref<DataApiResponse | undefined>() const childrenList = ref<DataApiResponse | undefined>()
const childrenExcludedListPagination = reactive({ const childrenExcludedListPagination = reactive({
page: 1, page: 1,
query: '', query: '',
size: 10, size: 10,
}) })
const childrenListPagination = reactive({ const childrenListPagination = reactive({
page: 1, page: 1,
query: '', query: '',
size: 10, size: 10,
}) })
const { t } = useI18n() const { t } = useI18n()
const isPublic: boolean = $(inject(IsPublicInj, ref(false))) const isPublic: boolean = $(inject(IsPublicInj, ref(false)))
const colOptions = $computed(() => column.value?.colOptions as LinkToAnotherRecordType) const colOptions = $computed(() => column.value?.colOptions as LinkToAnotherRecordType)
const { sharedView } = useSharedView() as Record<string, any> const { sharedView } = useSharedView()
const projectId = project.value?.id || sharedView.value?.view?.project_id
const projectId = project.value?.id || (sharedView.value?.view as any)?.project_id
// getters // getters
const meta = computed(() => metas?.value?.[column?.value?.fk_model_id as string]) const meta = computed(() => metas?.value?.[column?.value?.fk_model_id as string])
@ -168,7 +180,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
} else { } else {
childrenList.value = await $api.dbTableRow.nestedList( childrenList.value = await $api.dbTableRow.nestedList(
NOCO, NOCO,
(project?.value?.id || sharedView?.value?.view?.project_id) as string, (project?.value?.id || (sharedView.value?.view as any)?.project_id) as string,
meta.value.id, meta.value.id,
rowId.value, rowId.value,
colOptions.type as 'mm' | 'hm', colOptions.type as 'mm' | 'hm',
@ -251,12 +263,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
} catch (e: any) { } catch (e: any) {
message.error(`${t('msg.error.unlinkFailed')}: ${await extractSdkResponseErrorMsg(e)}`) message.error(`${t('msg.error.unlinkFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
} }
reloadData?.() reloadData?.()
// todo: reload table data and children list
// this.$emit('loadTableData');
// if (this.isForm && this.$refs.childList) {
// this.$refs.childList.loadData();
// }
} }
const link = async (row: Record<string, any>) => { const link = async (row: Record<string, any>) => {
@ -288,16 +296,6 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
message.error(`Linking failed: ${await extractSdkResponseErrorMsg(e)}`) message.error(`Linking failed: ${await extractSdkResponseErrorMsg(e)}`)
} }
// todo: reload table data and child list
// this.pid = pid;
//
// this.newRecordModal = false;
//
// this.$emit('loadTableData');
// if (this.isForm && this.$refs.childList) {
// this.$refs.childList.loadData();
// }
reloadData?.() reloadData?.()
} }
@ -305,6 +303,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
watch(childrenExcludedListPagination, async () => { watch(childrenExcludedListPagination, async () => {
await loadChildrenExcludedList() await loadChildrenExcludedList()
}) })
watch(childrenListPagination, async () => { watch(childrenListPagination, async () => {
await loadChildrenList() await loadChildrenList()
}) })

11
packages/nc-gui/composables/useMetas.ts

@ -1,12 +1,11 @@
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { WatchStopHandle } from 'vue' import type { WatchStopHandle } from 'vue'
import type { TableInfoType, TableType } from 'nocodb-sdk' import type { TableInfoType, TableType } from 'nocodb-sdk'
import { useProject } from './useProject' import { extractSdkResponseErrorMsg, useNuxtApp, useProject, useState, watch } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
import { useNuxtApp, useState } from '#app'
export function useMetas() { export function useMetas() {
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { tables } = useProject() const { tables } = useProject()
const metas = useState<{ [idOrTitle: string]: TableType | any }>('metas', () => ({})) const metas = useState<{ [idOrTitle: string]: TableType | any }>('metas', () => ({}))
@ -26,6 +25,7 @@ export function useMetas() {
} }
} }
// todo: this needs a proper refactor, arbitrary waiting times are usually not a good idea
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => { const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => {
if (!tableIdOrTitle) return null if (!tableIdOrTitle) return null
/** wait until loading is finished if requesting same meta */ /** wait until loading is finished if requesting same meta */
@ -53,11 +53,14 @@ export function useMetas() {
{ immediate: true }, { immediate: true },
) )
}) })
if (metas.value[tableIdOrTitle]) { if (metas.value[tableIdOrTitle]) {
return metas.value[tableIdOrTitle] return metas.value[tableIdOrTitle]
} }
} }
loadingState.value[tableIdOrTitle] = true loadingState.value[tableIdOrTitle] = true
try { try {
if (!force && metas.value[tableIdOrTitle]) { if (!force && metas.value[tableIdOrTitle]) {
return metas.value[tableIdOrTitle] return metas.value[tableIdOrTitle]
@ -89,8 +92,10 @@ export function useMetas() {
const clearAllMeta = () => { const clearAllMeta = () => {
metas.value = {} metas.value = {}
} }
const removeMeta = (idOrTitle: string) => { const removeMeta = (idOrTitle: string) => {
const meta = metas.value[idOrTitle] const meta = metas.value[idOrTitle]
if (meta) { if (meta) {
delete metas.value[meta.id] delete metas.value[meta.id]
delete metas.value[meta.title] delete metas.value[meta.title]

4
packages/nc-gui/composables/useProject.ts

@ -15,8 +15,7 @@ import {
useTheme, useTheme,
watch, watch,
} from '#imports' } from '#imports'
import type { ProjectMetaInfo } from '~/lib' import type { ProjectMetaInfo, ThemeConfig } from '~/lib'
import type { ThemeConfig } from '@/composables/useTheme'
const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => { const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -87,6 +86,7 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
} else if (projectType === 'base') { } else if (projectType === 'base') {
try { try {
const baseData = await api.public.sharedBaseGet(route.params.projectId as string) const baseData = await api.public.sharedBaseGet(route.params.projectId as string)
project.value = await api.project.read(baseData.project_id!) project.value = await api.project.read(baseData.project_id!)
} catch (e: any) { } catch (e: any) {
if (e?.response?.status === 404) { if (e?.response?.status === 404) {

2
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -1,13 +1,13 @@
import useVuelidate from '@vuelidate/core' import useVuelidate from '@vuelidate/core'
import { minLength, required } from '@vuelidate/validators' import { minLength, required } from '@vuelidate/validators'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { message } from 'ant-design-vue'
import type { ColumnType, FormType, LinkToAnotherRecordType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnType, FormType, LinkToAnotherRecordType, TableType, ViewType } from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { import {
SharedViewPasswordInj, SharedViewPasswordInj,
computed, computed,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
message,
provide, provide,
ref, ref,
useApi, useApi,

13
packages/nc-gui/composables/useSharedView.ts

@ -1,19 +1,27 @@
import type { ExportTypes, FilterType, PaginatedType, RequestParams, SortType, TableType, ViewType } from 'nocodb-sdk' import type { ExportTypes, FilterType, PaginatedType, RequestParams, SortType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { useGlobal, useNuxtApp } from '#imports' import { computed, useGlobal, useMetas, useNuxtApp, useState } from '#imports'
export function useSharedView() { export function useSharedView() {
const nestedFilters = useState<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>( const nestedFilters = useState<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>(
'nestedFilters', 'nestedFilters',
() => [], () => [],
) )
const { appInfo } = $(useGlobal()) const { appInfo } = $(useGlobal())
const appInfoDefaultLimit = appInfo.defaultLimit || 25 const appInfoDefaultLimit = appInfo.defaultLimit || 25
const paginationData = useState<PaginatedType>('paginationData', () => ({ page: 1, pageSize: appInfoDefaultLimit })) const paginationData = useState<PaginatedType>('paginationData', () => ({ page: 1, pageSize: appInfoDefaultLimit }))
const sharedView = useState<ViewType | undefined>('sharedView', () => undefined) const sharedView = useState<ViewType | undefined>('sharedView', () => undefined)
const sorts = useState<SortType[]>('sorts', () => []) const sorts = useState<SortType[]>('sorts', () => [])
const password = useState<string | undefined>('password', () => undefined) const password = useState<string | undefined>('password', () => undefined)
const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false) const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false)
const meta = useState<TableType | undefined>('meta', () => undefined) const meta = useState<TableType | undefined>('meta', () => undefined)
const formColumns = computed( const formColumns = computed(
@ -28,6 +36,7 @@ export function useSharedView() {
) )
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { setMeta } = useMetas() const { setMeta } = useMetas()
const loadSharedView = async (viewId: string, localPassword: string | undefined = undefined) => { const loadSharedView = async (viewId: string, localPassword: string | undefined = undefined) => {
@ -49,7 +58,7 @@ export function useSharedView() {
.map((c) => ({ ...c, order: order++ })) .map((c) => ({ ...c, order: order++ }))
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
setMeta(viewMeta.model) await setMeta(viewMeta.model)
const relatedMetas = { ...viewMeta.relatedMetas } const relatedMetas = { ...viewMeta.relatedMetas }
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key])) Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))

31
packages/nc-gui/composables/useSidebar/index.ts

@ -1,5 +1,5 @@
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { MemStorage, onScopeDispose, ref, syncRef, toRefs, watch } from '#imports' import { createSharedComposable, ref, syncRef, toRefs, watch } from '#imports'
interface UseSidebarProps { interface UseSidebarProps {
hasSidebar?: boolean hasSidebar?: boolean
@ -13,10 +13,9 @@ interface UseSidebarProps {
* Requires an id to work, id should correspond to the sidebar state you want to create or fetch * Requires an id to work, id should correspond to the sidebar state you want to create or fetch
* If `useSidebar` was not called before it will create a new state if no state can be found for the specified id * If `useSidebar` was not called before it will create a new state if no state can be found for the specified id
*/ */
const sidebarStorage = new MemStorage()
const createSidebar = (id: string, props: UseSidebarProps = {}) => { const createSidebar = (id: string, props: UseSidebarProps = {}) => {
const isOpen = ref(props.isOpen ?? false) const isOpen = ref(props.isOpen ?? false)
const hasSidebar = ref(props.hasSidebar ?? true) const hasSidebar = ref(props.hasSidebar ?? true)
function toggle(state?: boolean) { function toggle(state?: boolean) {
@ -57,25 +56,19 @@ const createSidebar = (id: string, props: UseSidebarProps = {}) => {
} }
} }
export function useSidebar(id: string, props: UseSidebarProps = {}) { const leftSidebar = createSharedComposable(() => createSidebar('leftSidebar'))
if (!id) throw new Error('useSidebar requires an id')
if (!sidebarStorage.has(id)) { const rightSidebar = createSharedComposable(() => createSidebar('rightSidebar', { useStorage: true }))
const sidebar = createSidebar(id, props)
sidebarStorage.set(id, sidebar) export const useSidebar = (id: string, props: UseSidebarProps = {}) => {
const sidebar = id.includes('left') ? leftSidebar() : rightSidebar()
onScopeDispose(() => { if (props.isOpen !== undefined) sidebar.isOpen.value = props.isOpen
sidebarStorage.remove(id) if (props.hasSidebar !== undefined) sidebar.hasSidebar.value = props.hasSidebar
})
return sidebar return sidebar
} else { }
const sidebar = sidebarStorage.get(id)
if (props.isOpen !== undefined) sidebar.isOpen.value = props.isOpen export const useLeftSidebar = (props: UseSidebarProps = {}) => useSidebar('left', props)
if (props.hasSidebar !== undefined) sidebar.hasSidebar.value = props.hasSidebar
return sidebar export const useRightSidebar = (props: UseSidebarProps = {}) => useSidebar('right', props)
}
}

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

Loading…
Cancel
Save