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

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

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

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

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { ColumnInj, computed, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
import { ColumnInj, EditModeInj, computed, inject, useVModel } from '#imports'
interface Props {
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"
@blur="editEnabled = false"
/>
<span v-else-if="vModel">{{ currency }}</span>
<span v-else />
</template>

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,6 +1,5 @@
<script setup lang="ts">
import { Modal as AModal } from 'ant-design-vue'
import { EditModeInj, IsFormInj, ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
import { Modal as AModal, EditModeInj, IsFormInj, ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
interface Props {
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>
</a-tag>
</a-select-option>
<template #tagRender="{ value: val, onClose }">
<a-tag
v-if="options.find((el) => el.title === val)"

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

@ -1,11 +1,12 @@
<script setup lang="ts">
import { EditModeInj, inject } from '#imports'
import { EditModeInj, inject, useVModel } from '#imports'
interface Props {
modelValue: number | string | null | undefined
}
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
@ -15,62 +16,6 @@ const vModel = useVModel(props, 'modelValue', emits)
<template>
<input v-if="editEnabled" v-model="vModel" type="number" />
<span v-else>{{ vModel }}</span>
</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">
import Text from './Text.vue'
import { useVModel } from '#imports'
interface Props {
modelValue: any
@ -17,7 +17,5 @@ const vModel = useVModel(props, 'modelValue', emits)
</script>
<template>
<Text v-model="vModel" />
<LazyCellText v-model="vModel" />
</template>
<style scoped></style>

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

@ -1,6 +1,5 @@
<script setup lang="ts">
import { ColumnInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
import { ColumnInj, EditModeInj, computed, inject } from '#imports'
interface Props {
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"
@blur="editEnabled = false"
/>
<span v-else>{{ vModel }}</span>
</template>

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

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

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs'
import { ReadonlyInj, inject, onClickOutside, useProject, watch } from '#imports'
interface Props {
modelValue?: string | null | undefined
@ -16,7 +16,7 @@ const readOnly = inject(ReadonlyInj, 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({
get() {

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

@ -1,6 +1,5 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { message } from 'ant-design-vue'
import {
CellUrlDisableOverlayInj,
ColumnInj,
@ -8,6 +7,7 @@ import {
computed,
inject,
isValidURL,
message,
ref,
useCellUrlConfig,
useI18n,
@ -77,14 +77,19 @@ watch(
<nuxt-link
v-else-if="isValid && !cellUrlOptions?.overlay"
no-prefetch
no-rel
class="z-3 text-sm underline hover:opacity-75"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
>
{{ value }}
</nuxt-link>
<nuxt-link
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"
:to="url"
: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 { useProvideAttachmentCell } from './utils'
import { useSortable } from './sort'
import Modal from './Modal.vue'
import Carousel from './Carousel.vue'
import {
IsFormInj,
IsGalleryInj,
@ -130,7 +128,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
ref="attachmentCellRef"
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)">
<general-overlay
@ -211,7 +209,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
</div>
</template>
<Modal />
<LazyCellAttachmentModal />
</div>
</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 {
ColumnInj,
EditModeInj,
@ -11,6 +9,7 @@ import {
computed,
inject,
isImage,
message,
ref,
useApi,
useFileDialog,
@ -146,7 +145,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
/** download a file */
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) => {

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

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

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

@ -1,18 +1,16 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
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'
import { extractSdkResponseErrorMsg, message, onMounted, useI18n, useNuxtApp } from '#imports'
const { t } = useI18n()
const { $api, $e } = useNuxtApp()
let apps = $ref<null | Array<any>>(null)
let apps = $ref<null | any[]>(null)
let showPluginUninstallModal = $ref(false)
let showPluginInstallModal = $ref(false)
let pluginApp = $ref<any>(null)
const fetchPluginApps = async () => {
@ -66,7 +64,7 @@ const showResetPluginModal = async (app: any) => {
onMounted(async () => {
if (apps === null) {
fetchPluginApps()
await fetchPluginApps()
}
})
</script>
@ -80,7 +78,7 @@ onMounted(async () => {
:footer="null"
wrap-class-name="nc-modal-plugin-install"
>
<AppInstall
<LazyDashboardSettingsAppInstall
v-if="pluginApp && showPluginInstallModal"
:id="pluginApp.id"
@close="showPluginInstallModal = false"
@ -117,19 +115,21 @@ onMounted(async () => {
<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)">
<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
</div>
</a-button>
<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">
<MdiCloseCircleIcon />
<MdiCloseCircleOutline />
<div class="flex ml-0.5">Reset</div>
</div>
</a-button>
<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">
<MdiPlusIcon />
<MdiPlus />
Install
</div>
</a-button>
@ -140,15 +140,19 @@ onMounted(async () => {
<img
v-if="app.title !== 'SMTP'"
class="avatar"
alt="logo"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<div v-else />
</div>
<div class="flex flex-col flex-1 w-3/5 pl-3">
<a-typography-title :level="5">{{ app.title }}</a-typography-title>
{{ app.description }}
</div>
</div>

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

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

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

@ -1,17 +1,16 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
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'
import { Empty, extractSdkResponseErrorMsg, h, message, useI18n, useNuxtApp, useProject } from '#imports'
const { $api } = useNuxtApp()
const { project, loadTables } = useProject()
const { t } = useI18n()
let isLoading = $ref(false)
let isDifferent = $ref(false)
let metadiff = $ref<any[]>([])
async function loadMetaDiff() {
@ -58,6 +57,7 @@ onMounted(async () => {
})
const tableHeaderRenderer = (label: string) => () => h('div', { class: 'text-gray-500' }, label)
const columns = [
{
// Models
@ -90,6 +90,7 @@ const columns = [
</div>
</a-button>
</div>
<div class="max-h-600px overflow-y-auto">
<a-table
class="w-full"
@ -105,10 +106,13 @@ const columns = [
:loading="isLoading"
bordered
>
<template #emptyText> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> </template
></a-table>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
</a-table>
</div>
</div>
<div class="flex place-content-center w-2/5">
<!-- Sync Now -->
<div v-if="isDifferent">
@ -119,9 +123,12 @@ const columns = [
</div>
</a-button>
</div>
<div v-else>
<!-- 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>

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

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

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

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

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

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

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

@ -1,19 +1,10 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import type { PluginType } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n'
import { extractSdkResponseErrorMsg, ref, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg, message, onMounted, ref, useI18n, useNuxtApp } from '#imports'
interface Props {
const { id } = defineProps<{
id: string
}
type Plugin = PluginType & {
formDetails: Record<string, any>
parsedInput: Record<string, any>
}
const { id } = defineProps<Props>()
}>()
const emits = defineEmits(['saved', 'close'])
@ -22,6 +13,11 @@ enum Action {
Test = 'test',
}
type Plugin = PluginType & {
formDetails: Record<string, any>
parsedInput: Record<string, any>
}
const { $api } = useNuxtApp()
const formRef = ref()
@ -29,8 +25,11 @@ const formRef = ref()
const { t } = useI18n()
let plugin = $ref<Plugin | null>(null)
let pluginFormData = $ref<Record<string, any>>({})
let isLoading = $ref(true)
let loadingAction = $ref<null | Action>(null)
const layout = {
@ -153,6 +152,7 @@ onMounted(async () => {
<span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span>
</div>
<div class="absolute -right-2 -top-0.5">
<a-button type="text" class="!rounded-md mr-1" @click="emits('close')">
<template #icon>
@ -175,6 +175,7 @@ onMounted(async () => {
</th>
</tr>
</thead>
<tbody>
<tr v-for="(itemRow, itemIndex) in plugin.parsedInput" :key="itemIndex">
<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]"
:placeholder="columnData.placeholder"
/>
<a-textarea
v-else-if="columnData.type === 'LongText'"
v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-switch
v-else-if="columnData.type === 'Checkbox'"
v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-input v-else v-model:value="itemRow[columnData.key]" :placeholder="columnData.placeholder" />
<div
v-if="itemIndex !== 0 && columnIndex === plugin.formDetails.items.length - 1"
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]"
:placeholder="columnData.placeholder"
/>
<a-textarea
v-else-if="columnData.type === 'LongText'"
v-model:value="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-switch
v-else-if="columnData.type === 'Checkbox'"
v-model:checked="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-input v-else v-model:value="pluginFormData[columnData.key]" :placeholder="columnData.placeholder" />
</a-form-item>
</template>
<div class="flex flex-row space-x-4 justify-center mt-4">
<a-button
v-for="(action, i) in plugin.formDetails.actions"
@ -265,5 +274,3 @@ onMounted(async () => {
</div>
</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 io from 'socket.io-client'
import type { Card as AntCard } from 'ant-design-vue'
import { Form, message } from 'ant-design-vue'
import {
Form,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
message,
nextTick,
onBeforeUnmount,
onMounted,
@ -17,11 +18,9 @@ import {
watch,
} from '#imports'
interface Props {
const { modelValue } = defineProps<{
modelValue: boolean
}
const { modelValue } = defineProps<Props>()
}>()
const emit = defineEmits(['update:modelValue'])
@ -337,9 +336,9 @@ onBeforeUnmount(() => {
<!-- This feature is currently in beta and more information can be found here -->
<div>
{{ $t('general.betaNote') }}
<a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">{{
$t('general.moreInfo')
}}</a>
<a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">
{{ $t('general.moreInfo') }}
</a>
.
</div>
</div>

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

@ -1,10 +1,10 @@
<script setup lang="ts">
import { Form, message } from 'ant-design-vue'
import type { TableType } from 'nocodb-sdk'
import type { UploadChangeParam, UploadFile } from 'ant-design-vue'
import {
ExcelTemplateAdapter,
ExcelUrlTemplateAdapter,
Form,
JSONTemplateAdapter,
JSONUrlTemplateAdapter,
computed,
@ -13,6 +13,7 @@ import {
importCsvUrlValidator,
importExcelUrlValidator,
importUrlValidator,
message,
reactive,
ref,
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="mt-5">
<TemplateEditor
<LazyTemplateEditor
v-if="templateEditorModal"
ref="templateEditorRef"
:project-template="templateData"
@ -350,7 +351,7 @@ const customReqCbk = (customReqArgs: { file: any; onSuccess: () => void }) => {
</template>
<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>
</a-tab-pane>

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

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

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

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

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

@ -1,14 +1,24 @@
<script setup lang="ts">
import type { ComponentPublicInstance } from '@vue/runtime-core'
import { message } 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 { ViewTypes } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n'
import { MetaInj, ViewListInj } from '~/context'
import { generateUniqueTitle } from '~/utils'
import { computed, nextTick, reactive, unref, useApi, useVModel, watch } from '#imports'
import {
MetaInj,
ViewListInj,
computed,
generateUniqueTitle,
inject,
message,
nextTick,
reactive,
unref,
useApi,
useI18n,
useVModel,
watch,
} from '#imports'
interface Props {
modelValue: boolean

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

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

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

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

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

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

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

@ -57,14 +57,16 @@ watch(picked, (n, _o) => {
{{ compare(picked, color) ? '&#10003;' : '' }}
</button>
</div>
<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-panel key="1" header="Advanced" class="">
<a-button v-if="props.pickButton" class="!bg-primary text-white w-full" @click="selectColor(picked)">
Pick Color
</a-button>
<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>
</a-collapse-panel>
</a-collapse>

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

@ -1,4 +1,6 @@
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, watch } from '#imports'
type FlipTrigger = 'hover' | 'click' | { duration: number }
interface Props {
@ -12,7 +14,9 @@ const props = withDefaults(defineProps<Props>(), {
})
let flipped = $ref(false)
let hovered = $ref(false)
let flipTimer = $ref<NodeJS.Timer | null>(null)
onMounted(() => {
@ -76,6 +80,7 @@ watch($$(flipped), () => {
>
<slot name="front" />
</div>
<div
class="back"
: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">
import { useSidebar } from '#imports'
import { computed, useSidebar } from '#imports'
const rightSidebar = useSidebar('nc-right-sidebar')
const leftSidebar = useSidebar('nc-left-sidebar')
const isSidebarsOpen = computed({

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

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

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

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

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

@ -1,6 +1,5 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, onMounted } from '#imports'
import { computed, extractSdkResponseErrorMsg, message, onMounted, useGlobal, useNuxtApp } from '#imports'
const { $api } = useNuxtApp()
@ -47,22 +46,38 @@ onMounted(async () => await fetchReleaseInfo())
<mdi-menu-down />
</div>
</a-button>
<template #overlay>
<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">
<mdi-script-text-outline />
{{ latestRelease }} {{ $t('activity.upgrade.releaseNote') }}
</div>
</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">
<mdi-rocket-launch-outline />
<!-- How to upgrade? -->
{{ $t('activity.upgrade.howTo') }}
</div>
</nuxt-link>
<a-divider class="!m-0" />
<div class="nc-menu-item" @click="releaseAlert = false">
<mdi-close />
<!-- Hide menu -->

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

@ -1,4 +1,6 @@
<script lang="ts" setup>
import { computed } from '#imports'
interface Props {
url: 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">
<MdiAccountPlusOutline class="mr-1 nc-share-base" />
<div>{{ $t('activity.inviteTeam') }}</div>
</div>
</div>
<TabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" />
<LazyTabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" />
</div>
</template>

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

@ -1,9 +1,5 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
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'
import { useI18n } from '#imports'
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">
<MdiDiscord v-e="['e:community:discord']" class="icon text-[#7289DA]" @click="open('https://discord.gg/5RgZmkW')" />
<div
v-e="['e:community:discourse']"
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>
<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')" />
<MdiCalendarMonth
v-e="['e:community:book-demo']"
class="icon text-green-500"

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

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

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

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

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

@ -1,4 +1,6 @@
<script lang="ts" setup>
import { computed, ref } from '#imports'
interface Props {
placement?:
| 'top'
@ -16,29 +18,31 @@ interface Props {
length?: number
}
const props = withDefaults(defineProps<Props>(), {
placement: 'bottom',
length: 20,
})
const { placement = 'bottom', length = 20 } = defineProps<Props>()
const text = ref()
const enableTooltip = computed(() => text?.value?.textContent.length > props.length)
const enableTooltip = computed(() => text.value?.textContent.length > length)
const shortName = computed(() =>
text?.value?.textContent.length > props.length
? `${text?.value?.textContent.substr(0, props.length - 3)}...`
: text?.value?.textContent,
text.value?.textContent.length > length ? `${text.value?.textContent.substr(0, length - 3)}...` : text.value?.textContent,
)
</script>
<template>
<a-tooltip v-if="enableTooltip" :placement="props.placement">
<a-tooltip v-if="enableTooltip" :placement="placement">
<template #title>
<slot></slot>
<slot />
</template>
<div>{{ shortName }}</div>
</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,
watch,
} from '#imports'
import type { Permission } from '~/composables/useUIPermission/rolePermissions'
import type { Permission } from '~/lib'
provide(IsFormInj, ref(true))
provide(IsGalleryInj, ref(false))

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

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

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

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

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

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

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

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { ActiveCellInj, CellValueInj, ColumnInj, RowInj, provide, toRef, useVirtualCell } from '#imports'
import type { Row } from '~/composables'
import type { Row } from '~/lib'
import { NavigateDir } from '~/lib'
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"
>
<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>
<MdiArrowDownDropCircle :style="{ 'font-size': '1.5em', 'color': element.color }" class="mr-2" />
</a-dropdown>

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

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

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

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

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

@ -12,7 +12,6 @@ import {
message,
onMounted,
ref,
resolveComponent,
useApi,
useDialog,
useI18n,
@ -189,7 +188,7 @@ async function onRename(view: ViewType) {
function openDeleteDialog(view: Record<string, any>) {
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('LazyDlgViewDelete') as any, {
const { close } = useDialog(() => import('~/components/dlg/ViewDelete.vue'), {
'modelValue': isOpen,
'view': view,
'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 { ColumnType, LinkToAnotherRecordType } 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<{
modelValue?: string
@ -26,7 +26,7 @@ const options = computed<SelectProps['options']>(() =>
return !(
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 {
return !c.colOptions || !c.system
}
@ -34,14 +34,9 @@ const options = computed<SelectProps['options']>(() =>
.map((c: ColumnType) => ({
value: c.id,
label: c.title,
icon: h(
isVirtualCol(c)
? resolveComponent('LazySmartsheetHeaderVirtualCellIcon')
: resolveComponent('LazySmartsheetHeaderCellIcon'),
{
columnMeta: c,
},
),
icon: h(isVirtualCol(c) ? () => import('../header/VirtualCellIcon.vue') : () => import('../header/CellIcon.vue'), {
columnMeta: c,
}),
c,
})),
)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,6 +1,5 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, onMounted, useCopy, useI18n, useNuxtApp, useProject } from '#imports'
import { extractSdkResponseErrorMsg, message, onMounted, useCopy, useDashboard, useI18n, useNuxtApp, useProject } from '#imports'
interface ShareBase {
uuid?: string
@ -143,43 +142,52 @@ onMounted(() => {
<div class="text-xs">{{ $t('activity.shareBase.link') }}</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">
<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">
<a-tooltip placement="bottom">
<template #title>
<span>{{ $t('general.reload') }}</span>
</template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="recreate">
<template #icon>
<MdiReload class="flex mx-auto text-gray-600" />
</template>
</a-button>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
<span>{{ $t('activity.copyUrl') }}</span>
</template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="copyUrl">
<template #icon>
<MdiContentCopy class="flex mx-auto text-gray-600" />
</template>
</a-button>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
<span>{{ $t('activity.openTab') }}</span>
</template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="navigateToSharedBase">
<template #icon>
<MdiOpenInNew class="flex mx-auto text-gray-600" />
</template>
</a-button>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
<span>{{ $t('activity.iFrame') }}</span>
</template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="generateEmbeddableIframe">
<template #icon>
<MdiXml class="flex mx-auto text-gray-600" />
@ -188,8 +196,10 @@ onMounted(() => {
</a-tooltip>
</div>
</div>
<!-- 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="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-button>
@ -221,6 +231,7 @@ onMounted(() => {
<IcRoundKeyboardArrowDown class="text-black -mt-0.5 h-[1rem]" />
</div>
</template>
<a-select-option
v-for="(role, index) in [ShareBaseRole.Editor, ShareBaseRole.Viewer]"
:key="index"

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

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

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

@ -1,9 +1,10 @@
<script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { Empty, Form, message } from 'ant-design-vue'
import { srcDestMappingColumns, tableColumns } from './utils'
import {
Empty,
Form,
MetaInj,
ReloadViewDataHookInj,
computed,
@ -12,6 +13,7 @@ import {
fieldRequiredValidator,
getUIDTIcon,
inject,
message,
nextTick,
onMounted,
reactive,
@ -22,7 +24,7 @@ import {
useTabs,
useTemplateRefsList,
} from '#imports'
import { TabType } from '~/composables'
import { TabType } from '~/lib'
const { quickImportType, projectTemplate, importData, importColumns, importOnly, maxRowsToParse } = defineProps<Props>()
@ -546,6 +548,7 @@ function handleEditableTnChange(idx: number) {
available for import
</p>
</a-form>
<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">
<template #header>
@ -603,6 +606,7 @@ function handleEditableTnChange(idx: number) {
</a-collapse-panel>
</a-collapse>
</a-card>
<a-card v-else>
<a-form :model="data" name="template-editor-form" @keydown.enter="emit('import')">
<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,
CellValueInj,
ColumnInj,
IsFormInj,
IsLockedInj,
ReadonlyInj,
ReloadRowDataHookInj,
RowInj,
defineAsyncComponent,
computed,
createEventHook,
inject,
ref,
useProvideLTARStore,
@ -18,10 +21,6 @@ import {
import MdiArrowExpand from '~icons/mdi/arrow-expand'
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 reloadRowTrigger = inject(ReloadRowDataHookInj, createEventHook())
@ -66,7 +65,7 @@ const value = computed(() => {
const unlinkRef = async (rec: Record<string, any>) => {
if (isNew.value) {
removeLTARRef(rec, column?.value as ColumnType)
await removeLTARRef(rec, column?.value as ColumnType)
} else {
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="chips flex items-center flex-1">
<template v-if="value && relatedTablePrimaryValueProp">
<ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" />
<LazyVirtualCellComponentsItemChip
:item="value"
:value="value[relatedTablePrimaryValueProp]"
@unlink="unlinkRef(value)"
/>
</template>
</div>
<div
v-if="!readOnly && !isLocked && (isUIAllowed('xcDatatableEditable') || isForm)"
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"
/>
</div>
<ListItems v-model="listItemsDlg" @attach-record="listItemsDlg = true" />
<LazyVirtualCellComponentsListItems v-model="listItemsDlg" @attach-record="listItemsDlg = true" />
</div>
</template>

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

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

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

@ -20,7 +20,7 @@ const showEditFormulaWarningMessage = () => {
}, 3000)
}
const result = isPg ? handleTZ(value) : value
const result = computed(() => (isPg.value ? handleTZ(value) : value))
const urls = computed(() => replaceUrlsWithLink(result.value))
</script>
@ -31,12 +31,15 @@ const urls = computed(() => replaceUrlsWithLink(result.value))
<template #title>
<span class="font-bold">{{ column.colOptions.error }}</span>
</template>
<span>ERR!</span>
</a-tooltip>
<div class="p-2" @dblclick="showEditFormulaWarningMessage">
<div v-if="urls" v-html="urls" />
<div v-else>{{ result }}</div>
<div v-if="showEditFormulaWarning" class="text-left text-wrap mt-2 text-[#e65100]">
<!-- TODO: i18n -->
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,
RowInj,
computed,
defineAsyncComponent,
inject,
ref,
useProvideLTARStore,
@ -18,12 +17,6 @@ import {
useUIPermission,
} 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 cellValue = inject(CellValueInj)!
@ -52,6 +45,7 @@ const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvid
isNew,
reloadRowTrigger.trigger,
)
await loadRelatedTableMeta()
const localCellValue = computed<any[]>(() => {
@ -94,17 +88,26 @@ const onAttachRecord = () => {
<template v-if="!isForm">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<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">
more...
</span>
</template>
</div>
<div v-if="!isLocked" class="flex justify-end gap-1 min-h-[30px] items-center">
<MdiArrowExpand
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click="childListDlg = true"
/>
<MdiPlus
v-if="!readOnly && isUIAllowed('xcDatatableEditable')"
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>
</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>
</template>

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

@ -56,7 +56,7 @@ const lookupColumnMetaProps = useColumn(lookupColumn)
<template
v-if="lookupColumn.uidt === UITypes.LinkToAnotherRecord && lookupColumn.colOptions.type === RelationTypes.BELONGS_TO"
>
<SmartsheetVirtualCell
<LazySmartsheetVirtualCell
v-for="(v, i) of arrValue"
:key="i"
:edit-enabled="false"
@ -65,7 +65,7 @@ const lookupColumnMetaProps = useColumn(lookupColumn)
/>
</template>
<SmartsheetVirtualCell v-else :edit-enabled="false" :model-value="arrValue" :column="lookupColumn" />
<LazySmartsheetVirtualCell v-else :edit-enabled="false" :model-value="arrValue" :column="lookupColumn" />
</div>
<!-- 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>
</template>
</template>

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

@ -10,6 +10,7 @@ import {
ReloadRowDataHookInj,
RowInj,
computed,
createEventHook,
inject,
ref,
useProvideLTARStore,
@ -17,12 +18,6 @@ import {
useUIPermission,
} 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 row = inject(RowInj)!
@ -44,6 +39,7 @@ const childListDlg = ref(false)
const { isUIAllowed } = useUIPermission()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
@ -93,7 +89,13 @@ const onAttachRecord = () => {
<template v-if="!isForm">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<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>
</template>
@ -113,9 +115,13 @@ const onAttachRecord = () => {
</div>
</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>
</template>

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

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

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

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

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

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

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

@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
import { onMounted, useVModel, watch } from '#imports'
interface Props {
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-content class="px-10 py-5 scrollbar-thin-primary">
<WebhookEditor v-if="editOrAdd" ref="webhookEditorRef" @back-to-list="editOrAdd = false" />
<WebhookList v-else @edit="editHook" @add="editOrAdd = true" />
<LazyWebhookEditor v-if="editOrAdd" ref="webhookEditorRef" @back-to-list="editOrAdd = false" />
<LazyWebhookList v-else @edit="editHook" @add="editOrAdd = true" />
</a-layout-content>
<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">
import { Form, message } from 'ant-design-vue'
import type { Ref } from 'vue'
import type { AuditType } from 'nocodb-sdk'
import {
Form,
MetaInj,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
inject,
message,
onMounted,
reactive,
useApi,
useI18n,
@ -413,6 +415,7 @@ onMounted(async () => {
<span class="inline text-xl font-bold">{{ meta.title }} : {{ hook.title || 'Webhooks' }} </span>
</div>
</div>
<div>
<a-button class="mr-3 nc-btn-webhook-test" size="large" @click="testWebhook">
<div class="flex items-center">
@ -421,6 +424,7 @@ onMounted(async () => {
Test Webhook
</div>
</a-button>
<a-button class="nc-btn-webhook-save" type="primary" size="large" @click.prevent="saveHooks">
<div class="flex items-center">
<MdiContentSave class="mr-2" />
@ -430,7 +434,9 @@ onMounted(async () => {
</a-button>
</div>
</div>
<a-divider />
<a-form :model="hook" name="create-or-edit-webhook">
<a-form-item>
<a-row type="flex">
@ -445,6 +451,7 @@ onMounted(async () => {
</a-form-item>
</a-col>
</a-row>
<a-row type="flex" :gutter="[16, 16]">
<a-col :span="12">
<a-form-item v-bind="validateInfos.eventOperation">
@ -461,6 +468,7 @@ onMounted(async () => {
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-bind="validateInfos['notification.type']">
<a-select
@ -523,16 +531,20 @@ onMounted(async () => {
<a-col :span="24">
<a-tabs v-model:activeKey="urlTabKey" type="card" closeable="false" class="shadow-sm">
<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 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 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 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">
For more about auth option refer
<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-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']">
<WebhookChannelMultiSelect
<LazyWebhookChannelMultiSelect
v-if="slackChannels.length > 0"
v-model="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-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']">
<WebhookChannelMultiSelect
<LazyWebhookChannelMultiSelect
v-if="teamsChannels.length > 0"
v-model="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-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']">
<WebhookChannelMultiSelect
<LazyWebhookChannelMultiSelect
v-if="discordChannels.length > 0"
v-model="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-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']">
<WebhookChannelMultiSelect
<LazyWebhookChannelMultiSelect
v-if="mattermostChannels.length > 0"
v-model="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-textarea v-model:value="hook.notification.payload[input.key]" :placeholder="input.label" size="large" />
</a-form-item>
<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-form-item>
@ -613,7 +626,8 @@ onMounted(async () => {
<a-col :span="24">
<a-card>
<a-checkbox v-model:checked="hook.condition" class="nc-check-box-hook-condition">On Condition</a-checkbox>
<SmartsheetToolbarColumnFilter
<LazySmartsheetToolbarColumnFilter
v-if="hook.condition"
ref="filterRef"
:auto-save="false"
@ -644,7 +658,8 @@ onMounted(async () => {
</a>
</div>
</div>
<WebhookTest
<LazyWebhookTest
ref="webhookTestRef"
:hook="{
...hook,

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

@ -1,6 +1,5 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { MetaInj, extractSdkResponseErrorMsg, inject, onMounted, ref, useI18n, useNuxtApp } from '#imports'
import { MetaInj, extractSdkResponseErrorMsg, inject, message, onMounted, ref, useI18n, useNuxtApp } from '#imports'
const emit = defineEmits(['edit', 'add'])
@ -54,6 +53,7 @@ onMounted(() => {
<div class="">
<div class="mb-2">
<div class="float-left font-bold text-xl mt-2 mb-4">{{ meta.title }} : Webhooks</div>
<a-button
v-e="['c:webhook:add']"
class="float-right nc-btn-create-webhook"
@ -64,7 +64,9 @@ onMounted(() => {
{{ $t('activity.addWebhook') }}
</a-button>
</div>
<a-divider />
<div v-if="hooks.length" class="">
<a-list item-layout="horizontal" :data-source="hooks" class="cursor-pointer scrollbar-thin-primary">
<template #renderItem="{ item, index }">
@ -73,21 +75,25 @@ onMounted(() => {
<template #description>
<span class="uppercase"> {{ item.event }} {{ item.operation }}</span>
</template>
<template #title>
<span class="text-xl normal-case">
{{ item.title }}
</span>
</template>
<template #avatar>
<div class="mt-4">
<MdiHook class="text-xl" />
</div>
</template>
</a-list-item-meta>
<template #extra>
<div>
<!-- Notify Via -->
<div class="mr-2">{{ $t('labels.notifyVia') }} : {{ item?.notification?.type }}</div>
<div class="float-right pt-2 pr-1">
<MdiDeleteOutline class="text-xl nc-hook-delete-icon" @click.stop="deleteHook(item, index)" />
</div>
@ -97,6 +103,7 @@ onMounted(() => {
</template>
</a-list>
</div>
<div v-else class="min-h-[75vh]">
<div class="p-4 bg-gray-100 text-gray-600">
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">
import { message } from 'ant-design-vue'
import { MetaInj, extractSdkResponseErrorMsg, onMounted, useI18n } from '#imports'
import { MetaInj, extractSdkResponseErrorMsg, inject, message, onMounted, ref, useI18n, useNuxtApp, watch } from '#imports'
interface Props {
hook: Record<string, any>
@ -58,7 +57,7 @@ onMounted(async () => {
<template>
<a-collapse v-model:activeKey="activeKey" ghost>
<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>
</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 { CreateApiOptions, UseApiProps, UseApiReturn } from './types'
import { addAxiosInterceptors } from './interceptors'
import { BASE_URL } from '~/lib'
import { createEventHook, extractSdkResponseErrorMsg, ref, unref, useCounter, useGlobal, useNuxtApp } from '#imports'
import { BASE_URL, createEventHook, extractSdkResponseErrorMsg, ref, unref, useCounter, useGlobal, useNuxtApp } from '#imports'
export function createApiInstance<SecurityDataType = any>({ baseURL = BASE_URL }: CreateApiOptions = {}): Api<SecurityDataType> {
const { appInfo } = $(useGlobal())

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

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

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

@ -1,22 +1,23 @@
import type { ColumnType } from 'nocodb-sdk'
import { SqlUiFactory, UITypes, isVirtualCol } from 'nocodb-sdk'
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 uiDatatype: ComputedRef<UITypes> = computed(() => column?.value?.uidt as UITypes)
const uiDatatype: ComputedRef<UITypes> = computed(() => column.value?.uidt as UITypes)
const abstractType = computed(() => {
// 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
: SqlUiFactory.create(
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 isString = computed(() => uiDatatype.value === UITypes.SingleLineText || abstractType.value === 'string')
const isTextArea = computed(() => uiDatatype.value === UITypes.LongText)
@ -60,9 +61,7 @@ export function useColumn(column: Ref<ColumnType>) {
const isManualSaved = computed(() =>
[UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(uiDatatype.value),
)
const isPrimary = computed(() => {
return column?.value?.pv
})
const isPrimary = computed(() => column.value?.pv)
return {
abstractType,

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

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

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

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

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

@ -1,5 +1,8 @@
import { computed, useRoute } from '#imports'
export function useDashboard() {
const route = useRoute()
const dashboardUrl = computed(() => {
// todo: test in different scenarios
// 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(
componentOrVNode: DefineComponent<any, any, any> | VNode,
componentOrVNode: any,
props: NonNullable<Parameters<typeof h>[1]> = {},
mountTarget?: Element | ComponentPublicInstance,
) {

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

@ -1,50 +1,61 @@
import { UITypes } from 'nocodb-sdk'
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import { useI18n } from 'vue-i18n'
import {
NOCO,
computed,
extractPkFromRow,
extractSdkResponseErrorMsg,
getHTMLEncodedText,
message,
ref,
useApi,
useI18n,
useInjectionState,
useNuxtApp,
useProject,
useProvideSmartsheetRowStore,
useSharedView,
} from '#imports'
import type { Row } from '~/composables'
import type { Row } from '~/lib'
const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((meta: Ref<TableType>, row: Ref<Row>) => {
const { $e, $state, $api } = useNuxtApp()
const { api, isLoading: isCommentsLoading, error: commentsError } = useApi()
const { t } = useI18n()
const commentsOnly = ref(false)
const commentsAndLogs = ref<any[]>([])
const comment = ref('')
const commentsDrawer = ref(false)
const changedColumns = ref(new Set<string>())
const { project } = useProject()
const rowStore = useProvideSmartsheetRowStore(meta, row)
const { sharedView } = useSharedView() as Record<string, any>
// todo
// const activeView = inject(ActiveViewInj)
const rowStore = useProvideSmartsheetRowStore(meta, row)
// const { updateOrSaveRow, insertRow } = useViewData(meta, activeView as any)
const { sharedView } = useSharedView()
// getters
const primaryValue = computed(() => {
if (row?.value?.row) {
const col = meta?.value?.columns?.find((c) => c.pv)
if (!col) {
return
}
const value = row.value.row?.[col.title as string]
const uidt = col.uidt
if (uidt === UITypes.Date) {
return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD')
} else if (uidt === UITypes.DateTime) {
@ -73,8 +84,11 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
// actions
const loadCommentsAndLogs = async () => {
if (!row.value) return
const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!rowId) return
commentsAndLogs.value =
(
await api.utils.commentList({
@ -92,7 +106,9 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const saveComment = async () => {
try {
if (!row.value || !comment.value) return
const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!rowId) return
await api.utils.commentRow({
@ -114,14 +130,6 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const save = async () => {
let data
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) => {
obj[col] = row.value.row[col]
return obj
@ -141,7 +149,9 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
if (!id) {
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)
for (const key of Object.keys(updateOrInsertObj)) {
// audit
$api.utils
@ -164,11 +174,6 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
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.`)
changedColumns.value = new Set()
@ -182,7 +187,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const loadRow = async (rowId?: string) => {
const record = await $api.dbTableRow.read(
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,
rowId ?? extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]),
)
@ -218,6 +224,8 @@ export { useProvideExpandedFormStore }
export function useExpandedFormStoreOrThrow() {
const expandedFormStore = useExpandedFormStore()
if (expandedFormStore == null) throw new Error('Please call `useExpandedFormStore` on the appropriate parent component')
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 { getI18n } from '~/plugins/a.i18n'
import { message, useNuxtApp } from '#imports'
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 */
const signOut: Actions['signOut'] = () => {
state.token.value = null
@ -32,7 +25,10 @@ export function useGlobalActions(state: State): Actions {
/** manually try to refresh token */
const refreshToken = async () => {
api.instance
const nuxtApp = useNuxtApp()
const t = nuxtApp.vueApp.i18n.global.t
nuxtApp.$api.instance
.post('/auth/refresh-token', null, {
withCredentials: true,
})

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

@ -1,9 +1,7 @@
import { usePreferredLanguages, useStorage } from '@vueuse/core'
import { useJwt } from '@vueuse/integrations/useJwt'
import { useStorage } from '@vueuse/core'
import type { JwtPayload } from 'jwt-decode'
import type { AppInfo, State, StoredState } from './types'
import { BASE_URL } from '~/lib'
import { computed, ref, toRefs, useCounter, useNuxtApp, useTimestamp } from '#imports'
import { BASE_URL, computed, ref, toRefs, useCounter, useJwt, useNuxtApp, usePreferredLanguages, useTimestamp } from '#imports'
import type { Language, User } from '~/lib'
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 { Ref } from 'vue'
import { useMetas } from './useMetas'
import { useUIPermission } from './useUIPermission'
import { IsPublicInj } from '~/context'
import { IsPublicInj, computed, inject, ref, useMetas, useNuxtApp, useStyleTag, useUIPermission, watch } from '#imports'
export function useGridViewColumnWidth(view: Ref<GridType | undefined>) {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useUIPermission()
const { $api } = useNuxtApp()
const { metas } = useMetas()
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 */
watch(columns, loadGridViewColumns)
const updateWidth = (id: string, width: string) => {
const updateWidth = async (id: string, width: string) => {
if (gridViewCols?.value?.[id]) {
gridViewCols.value[id].width = width
}
// sync with server if allowed
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,
})
}

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

@ -1,21 +1,25 @@
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, RequestParams, TableType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { Modal, message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import {
IsPublicInj,
Modal,
NOCO,
SharedViewPasswordInj,
computed,
extractSdkResponseErrorMsg,
inject,
message,
reactive,
ref,
useI18n,
useInjectionState,
useMetas,
useNuxtApp,
useProject,
useSharedView,
watch,
} from '#imports'
import type { Row } from '~/composables'
import { SharedViewPasswordInj } from '~/context'
import type { Row } from '~/lib'
interface DataApiResponse {
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 = () => {}) => {
// state
const { metas, getMeta } = useMetas()
const { project } = useProject()
const { $api } = useNuxtApp()
const sharedViewPassword = inject(SharedViewPasswordInj, ref(null))
const childrenExcludedList = ref<DataApiResponse | undefined>()
const childrenList = ref<DataApiResponse | undefined>()
const childrenExcludedListPagination = reactive({
page: 1,
query: '',
size: 10,
})
const childrenListPagination = reactive({
page: 1,
query: '',
size: 10,
})
const { t } = useI18n()
const isPublic: boolean = $(inject(IsPublicInj, ref(false)))
const colOptions = $computed(() => column.value?.colOptions as LinkToAnotherRecordType)
const { sharedView } = useSharedView() as Record<string, any>
const projectId = project.value?.id || sharedView.value?.view?.project_id
const { sharedView } = useSharedView()
const projectId = project.value?.id || (sharedView.value?.view as any)?.project_id
// getters
const meta = computed(() => metas?.value?.[column?.value?.fk_model_id as string])
@ -168,7 +180,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
} else {
childrenList.value = await $api.dbTableRow.nestedList(
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,
rowId.value,
colOptions.type as 'mm' | 'hm',
@ -251,12 +263,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
} catch (e: any) {
message.error(`${t('msg.error.unlinkFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
}
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>) => {
@ -288,16 +296,6 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
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?.()
}
@ -305,6 +303,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
watch(childrenExcludedListPagination, async () => {
await loadChildrenExcludedList()
})
watch(childrenListPagination, async () => {
await loadChildrenList()
})

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

@ -1,12 +1,11 @@
import { message } from 'ant-design-vue'
import type { WatchStopHandle } from 'vue'
import type { TableInfoType, TableType } from 'nocodb-sdk'
import { useProject } from './useProject'
import { extractSdkResponseErrorMsg } from '~/utils'
import { useNuxtApp, useState } from '#app'
import { extractSdkResponseErrorMsg, useNuxtApp, useProject, useState, watch } from '#imports'
export function useMetas() {
const { $api } = useNuxtApp()
const { tables } = useProject()
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> => {
if (!tableIdOrTitle) return null
/** wait until loading is finished if requesting same meta */
@ -53,11 +53,14 @@ export function useMetas() {
{ immediate: true },
)
})
if (metas.value[tableIdOrTitle]) {
return metas.value[tableIdOrTitle]
}
}
loadingState.value[tableIdOrTitle] = true
try {
if (!force && metas.value[tableIdOrTitle]) {
return metas.value[tableIdOrTitle]
@ -89,8 +92,10 @@ export function useMetas() {
const clearAllMeta = () => {
metas.value = {}
}
const removeMeta = (idOrTitle: string) => {
const meta = metas.value[idOrTitle]
if (meta) {
delete metas.value[meta.id]
delete metas.value[meta.title]

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

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

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

@ -1,13 +1,13 @@
import useVuelidate from '@vuelidate/core'
import { minLength, required } from '@vuelidate/validators'
import type { Ref } from 'vue'
import { message } from 'ant-design-vue'
import type { ColumnType, FormType, LinkToAnotherRecordType, TableType, ViewType } from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import {
SharedViewPasswordInj,
computed,
extractSdkResponseErrorMsg,
message,
provide,
ref,
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 { UITypes } from 'nocodb-sdk'
import { useGlobal, useNuxtApp } from '#imports'
import { computed, useGlobal, useMetas, useNuxtApp, useState } from '#imports'
export function useSharedView() {
const nestedFilters = useState<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>(
'nestedFilters',
() => [],
)
const { appInfo } = $(useGlobal())
const appInfoDefaultLimit = appInfo.defaultLimit || 25
const paginationData = useState<PaginatedType>('paginationData', () => ({ page: 1, pageSize: appInfoDefaultLimit }))
const sharedView = useState<ViewType | undefined>('sharedView', () => undefined)
const sorts = useState<SortType[]>('sorts', () => [])
const password = useState<string | undefined>('password', () => undefined)
const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false)
const meta = useState<TableType | undefined>('meta', () => undefined)
const formColumns = computed(
@ -28,6 +36,7 @@ export function useSharedView() {
)
const { $api } = useNuxtApp()
const { setMeta } = useMetas()
const loadSharedView = async (viewId: string, localPassword: string | undefined = undefined) => {
@ -49,7 +58,7 @@ export function useSharedView() {
.map((c) => ({ ...c, order: order++ }))
.sort((a, b) => a.order - b.order)
setMeta(viewMeta.model)
await setMeta(viewMeta.model)
const relatedMetas = { ...viewMeta.relatedMetas }
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 { MemStorage, onScopeDispose, ref, syncRef, toRefs, watch } from '#imports'
import { createSharedComposable, ref, syncRef, toRefs, watch } from '#imports'
interface UseSidebarProps {
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
* 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 isOpen = ref(props.isOpen ?? false)
const hasSidebar = ref(props.hasSidebar ?? true)
function toggle(state?: boolean) {
@ -57,25 +56,19 @@ const createSidebar = (id: string, props: UseSidebarProps = {}) => {
}
}
export function useSidebar(id: string, props: UseSidebarProps = {}) {
if (!id) throw new Error('useSidebar requires an id')
const leftSidebar = createSharedComposable(() => createSidebar('leftSidebar'))
if (!sidebarStorage.has(id)) {
const sidebar = createSidebar(id, props)
const rightSidebar = createSharedComposable(() => createSidebar('rightSidebar', { useStorage: true }))
sidebarStorage.set(id, sidebar)
export const useSidebar = (id: string, props: UseSidebarProps = {}) => {
const sidebar = id.includes('left') ? leftSidebar() : rightSidebar()
onScopeDispose(() => {
sidebarStorage.remove(id)
})
if (props.isOpen !== undefined) sidebar.isOpen.value = props.isOpen
if (props.hasSidebar !== undefined) sidebar.hasSidebar.value = props.hasSidebar
return sidebar
} else {
const sidebar = sidebarStorage.get(id)
return sidebar
}
if (props.isOpen !== undefined) sidebar.isOpen.value = props.isOpen
if (props.hasSidebar !== undefined) sidebar.hasSidebar.value = props.hasSidebar
export const useLeftSidebar = (props: UseSidebarProps = {}) => useSidebar('left', props)
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