Browse Source

Merge pull request #6753 from nocodb/develop

pull/6754/head 0.202.5
github-actions[bot] 10 months ago committed by GitHub
parent
commit
18730fad74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/publish-api-docs.yml
  2. 1
      packages/nc-gui/assets/style/fonts.css
  3. 15
      packages/nc-gui/components/cell/Json.vue
  4. 7
      packages/nc-gui/components/cell/MultiSelect.vue
  5. 8
      packages/nc-gui/components/cell/SingleSelect.vue
  6. 2
      packages/nc-gui/components/cell/attachment/Carousel.vue
  7. 42
      packages/nc-gui/components/cell/attachment/index.vue
  8. 2
      packages/nc-gui/components/dashboard/Sidebar.vue
  9. 2
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  10. 20
      packages/nc-gui/components/dlg/AirtableImport.vue
  11. 8
      packages/nc-gui/components/dlg/QuickImport.vue
  12. 2
      packages/nc-gui/components/general/JoinCloud.vue
  13. 8
      packages/nc-gui/components/general/ShareProject.vue
  14. 8
      packages/nc-gui/components/general/WorkspaceIcon.vue
  15. 7
      packages/nc-gui/components/nc/Button.vue
  16. 10
      packages/nc-gui/components/nc/Select.vue
  17. 2
      packages/nc-gui/components/shared-view/Gallery.vue
  18. 6
      packages/nc-gui/components/shared-view/Grid.vue
  19. 2
      packages/nc-gui/components/shared-view/Kanban.vue
  20. 2
      packages/nc-gui/components/shared-view/Map.vue
  21. 8
      packages/nc-gui/components/smartsheet/Form.vue
  22. 13
      packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue
  23. 13
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  24. 11
      packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue
  25. 7
      packages/nc-gui/components/smartsheet/details/Fields.vue
  26. 78
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  27. 59
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  28. 24
      packages/nc-gui/components/smartsheet/grid/Table.vue
  29. 2
      packages/nc-gui/components/smartsheet/header/CellIcon.ts
  30. 2
      packages/nc-gui/components/smartsheet/toolbar/CreateSort.vue
  31. 4
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  32. 8
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  33. 3
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  34. 6
      packages/nc-gui/components/smartsheet/toolbar/MappedBy.vue
  35. 16
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  36. 14
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  37. 4
      packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue
  38. 3
      packages/nc-gui/components/tabs/Smartsheet.vue
  39. 5
      packages/nc-gui/composables/useAttachment.ts
  40. 6
      packages/nc-gui/composables/useColumnCreateStore.ts
  41. 4
      packages/nc-gui/composables/useExpandedFormStore.ts
  42. 97
      packages/nc-gui/composables/useGridViewColumn.ts
  43. 10
      packages/nc-gui/composables/useTableNew.ts
  44. 557
      packages/nc-gui/composables/useViewColumns.ts
  45. 2
      packages/nc-gui/composables/useViewFilters.ts
  46. 4
      packages/nc-gui/composables/useViewGroupBy.ts
  47. 18
      packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts
  48. 11
      packages/nc-gui/helpers/parsers/ExcelTemplateAdapter.ts
  49. 11
      packages/nc-gui/helpers/parsers/parserHelpers.ts
  50. 1
      packages/nc-gui/package.json
  51. 2
      packages/nc-gui/pages/index/[typeOrId]/[baseId]/index/index/[viewId]/[[viewTitle]].vue
  52. 3
      packages/nc-gui/store/views.ts
  53. 17
      packages/nc-gui/utils/workerUtils.ts
  54. 9
      packages/nc-gui/windi.config.ts
  55. 2
      packages/noco-docs/.gitignore
  56. 0
      packages/noco-docs/dist/.nojekyll
  57. 18
      packages/noco-docs/dist/0.109.7/FAQs/index.html
  58. 17
      packages/noco-docs/dist/0.109.7/developer-resources/accessing-apis/index.html
  59. 16
      packages/noco-docs/dist/0.109.7/developer-resources/rest-apis/index.html
  60. 16
      packages/noco-docs/dist/0.109.7/developer-resources/sdk/index.html
  61. 17
      packages/noco-docs/dist/0.109.7/developer-resources/upload-via-api/index.html
  62. 19
      packages/noco-docs/dist/0.109.7/developer-resources/webhooks/index.html
  63. 16
      packages/noco-docs/dist/0.109.7/engineering/architecture/index.html
  64. 16
      packages/noco-docs/dist/0.109.7/engineering/builds-and-releases/index.html
  65. 16
      packages/noco-docs/dist/0.109.7/engineering/development-setup/index.html
  66. 16
      packages/noco-docs/dist/0.109.7/engineering/playwright/index.html
  67. 16
      packages/noco-docs/dist/0.109.7/engineering/repository-structure/index.html
  68. 18
      packages/noco-docs/dist/0.109.7/engineering/translation/index.html
  69. 19
      packages/noco-docs/dist/0.109.7/engineering/unit-testing/index.html
  70. 16
      packages/noco-docs/dist/0.109.7/getting-started/demos/index.html
  71. 16
      packages/noco-docs/dist/0.109.7/getting-started/environment-variables/index.html
  72. 20
      packages/noco-docs/dist/0.109.7/getting-started/installation/index.html
  73. 16
      packages/noco-docs/dist/0.109.7/getting-started/upgrading/index.html
  74. 16
      packages/noco-docs/dist/0.109.7/index.html
  75. 18
      packages/noco-docs/dist/0.109.7/setup-and-usages/account-settings/index.html
  76. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/audit/index.html
  77. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/code-snippets/index.html
  78. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/column-operations/index.html
  79. 18
      packages/noco-docs/dist/0.109.7/setup-and-usages/column-types/index.html
  80. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/dashboard/index.html
  81. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/display-value/index.html
  82. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/expanded-form/index.html
  83. 17
      packages/noco-docs/dist/0.109.7/setup-and-usages/formulas/index.html
  84. 18
      packages/noco-docs/dist/0.109.7/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/index.html
  85. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/keyboard-maneuver/index.html
  86. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/languages/index.html
  87. 18
      packages/noco-docs/dist/0.109.7/setup-and-usages/link-to-another-record/index.html
  88. 27
      packages/noco-docs/dist/0.109.7/setup-and-usages/links/index.html
  89. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/lookup/index.html
  90. 17
      packages/noco-docs/dist/0.109.7/setup-and-usages/meta-management/index.html
  91. 17
      packages/noco-docs/dist/0.109.7/setup-and-usages/primary-key/index.html
  92. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/project-settings/index.html
  93. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/rollup/index.html
  94. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/share-base/index.html
  95. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/share-view/index.html
  96. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/sync-schema/index.html
  97. 20
      packages/noco-docs/dist/0.109.7/setup-and-usages/table-operations/index.html
  98. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/team-and-auth/index.html
  99. 21
      packages/noco-docs/dist/0.109.7/setup-and-usages/usage-information/index.html
  100. 16
      packages/noco-docs/dist/0.109.7/setup-and-usages/views/index.html
  101. Some files were not shown because too many files have changed in this diff Show More

2
.github/workflows/publish-api-docs.yml

@ -35,7 +35,7 @@ jobs:
env: env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }} API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with: with:
source_file: 'packages/nocodb/src/schema/swagger.json' source_file: 'packages/nocodb/src/schema/swagger-v2.json'
destination_repo: 'nocodb/noco-apis-doc' destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'data-apis-v2' destination_folder: 'data-apis-v2'
user_email: 'oof1lab@gmail.com' user_email: 'oof1lab@gmail.com'

1
packages/nc-gui/assets/style/fonts.css

@ -2,6 +2,7 @@
@font-face { @font-face {
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Manrope'; font-family: 'Manrope';
font-weight: 450 900;
src: url('./manrope/Manrope-VariableFont_wght.ttf') format('truetype') src: url('./manrope/Manrope-VariableFont_wght.ttf') format('truetype')
} }

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

@ -6,6 +6,7 @@ import {
IsFormInj, IsFormInj,
JsonExpandInj, JsonExpandInj,
ReadonlyInj, ReadonlyInj,
RowHeightInj,
computed, computed,
inject, inject,
ref, ref,
@ -46,6 +47,8 @@ const _isExpanded = inject(JsonExpandInj, ref(false))
const isExpanded = ref(false) const isExpanded = ref(false)
const rowHeight = inject(RowHeightInj, ref(undefined))
const localValue = computed<string | Record<string, any> | undefined>({ const localValue = computed<string | Record<string, any> | undefined>({
get: () => localValueState.value, get: () => localValueState.value,
set: (val: undefined | string | Record<string, any>) => { set: (val: undefined | string | Record<string, any>) => {
@ -140,6 +143,13 @@ useSelectedCellKeyupListener(active, (e) => {
} }
}) })
const inputWrapperRef = ref<HTMLElement | null>(null)
onClickOutside(inputWrapperRef, (e) => {
if ((e.target as HTMLElement)?.closest('.nc-json-action')) return
editEnabled.value = false
})
watch(isExpanded, () => { watch(isExpanded, () => {
_isExpanded.value = isExpanded.value _isExpanded.value = isExpanded.value
}) })
@ -148,7 +158,7 @@ watch(isExpanded, () => {
<template> <template>
<component :is="isExpanded ? NcModal : 'div'" v-model:visible="isExpanded" :closable="false" centered :footer="null"> <component :is="isExpanded ? NcModal : 'div'" v-model:visible="isExpanded" :closable="false" centered :footer="null">
<div v-if="editEnabled && !readonly" class="flex flex-col w-full" @mousedown.stop @mouseup.stop @click.stop> <div v-if="editEnabled && !readonly" class="flex flex-col w-full" @mousedown.stop @mouseup.stop @click.stop>
<div class="flex flex-row justify-between pt-1 pb-2" @mousedown.stop> <div class="flex flex-row justify-between pt-1 pb-2 nc-json-action" @mousedown.stop>
<a-button type="text" size="small" @click="isExpanded = !isExpanded"> <a-button type="text" size="small" @click="isExpanded = !isExpanded">
<CilFullscreenExit v-if="isExpanded" class="h-2.5" /> <CilFullscreenExit v-if="isExpanded" class="h-2.5" />
@ -167,6 +177,7 @@ watch(isExpanded, () => {
</div> </div>
<LazyMonacoEditor <LazyMonacoEditor
ref="inputWrapperRef"
:model-value="localValue || ''" :model-value="localValue || ''"
class="min-w-full w-80" class="min-w-full w-80"
:class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }" :class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }"
@ -182,7 +193,7 @@ watch(isExpanded, () => {
<span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span> <span v-else-if="vModel === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<span v-else>{{ vModel }}</span> <LazyCellClampedText v-else :value="vModel" :lines="rowHeight" />
</component> </component>
</template> </template>

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

@ -43,6 +43,8 @@ const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const { isMobileMode } = useGlobal()
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)! const readOnly = inject(ReadonlyInj)!
@ -383,7 +385,7 @@ const selectedOpts = computed(() => {
:placeholder="isEditColumn ? $t('labels.optional') : ''" :placeholder="isEditColumn ? $t('labels.optional') : ''"
:bordered="false" :bordered="false"
clear-icon clear-icon
show-search :show-search="!isMobileMode"
:show-arrow="editAllowed && !(readOnly || isLockedMode)" :show-arrow="editAllowed && !(readOnly || isLockedMode)"
:open="isOpen && editAllowed" :open="isOpen && editAllowed"
:disabled="readOnly || !editAllowed || isLockedMode" :disabled="readOnly || !editAllowed || isLockedMode"
@ -392,6 +394,9 @@ const selectedOpts = computed(() => {
@search="search" @search="search"
@keydown.stop @keydown.stop
> >
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700 nc-select-expand-btn" />
</template>
<a-select-option <a-select-option
v-for="op of options" v-for="op of options"
:key="op.id || op.title" :key="op.id || op.title"

8
packages/nc-gui/components/cell/SingleSelect.vue

@ -37,6 +37,8 @@ const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const { isMobileMode } = useGlobal()
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)! const readOnly = inject(ReadonlyInj)!
@ -202,6 +204,8 @@ async function addIfMissingAndSave() {
} }
const search = () => { const search = () => {
if (isMobileMode.value) return
searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value
} }
@ -285,7 +289,7 @@ const selectedOpt = computed(() => {
v-else v-else
ref="aselect" ref="aselect"
v-model:value="vModel" v-model:value="vModel"
class="w-full overflow-hidden" class="w-full overflow-hidden xs:min-h-12"
:class="{ 'caret-transparent': !hasEditRoles }" :class="{ 'caret-transparent': !hasEditRoles }"
:placeholder="isEditColumn ? $t('labels.optional') : ''" :placeholder="isEditColumn ? $t('labels.optional') : ''"
:allow-clear="!column.rqd && editAllowed" :allow-clear="!column.rqd && editAllowed"
@ -294,7 +298,7 @@ const selectedOpt = computed(() => {
:disabled="readOnly || !editAllowed || isLockedMode" :disabled="readOnly || !editAllowed || isLockedMode"
:show-arrow="hasEditRoles && !(readOnly || isLockedMode) && active && vModel === null" :show-arrow="hasEditRoles && !(readOnly || isLockedMode) && active && vModel === null"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && active ? 'active' : ''}`" :dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && active ? 'active' : ''}`"
:show-search="isOpen && active" :show-search="!isMobileMode && isOpen && active"
@select="onSelect" @select="onSelect"
@keydown="onKeydown($event)" @keydown="onKeydown($event)"
@search="search" @search="search"

2
packages/nc-gui/components/cell/attachment/Carousel.vue

@ -55,7 +55,7 @@ useEventListener(container, 'click', (e) => {
<template> <template>
<GeneralOverlay v-model="selectedImage" :z-index="1001" class="bg-gray-500 bg-opacity-50"> <GeneralOverlay v-model="selectedImage" :z-index="1001" class="bg-gray-500 bg-opacity-50">
<template v-if="selectedImage"> <template v-if="selectedImage">
<div ref="container" class="overflow-hidden p-12 text-center relative"> <div ref="container" class="overflow-hidden p-12 text-center relative xs:h-screen">
<div class="text-white group absolute top-5 right-5"> <div class="text-white group absolute top-5 right-5">
<component <component
:is="iconMap.closeCircle" :is="iconMap.closeCircle"

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

@ -152,14 +152,18 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => {
const rowHeight = inject(RowHeightInj, ref()) const rowHeight = inject(RowHeightInj, ref())
const open = () => { const open = (e: Event) => {
if (isMobileMode.value) return (isExpandedForm.value = true) e.stopPropagation()
_open() _open()
} }
const openAttachment = (item: any) => { const openAttachment = (item: any) => {
if (isMobileMode.value) return if (isMobileMode.value && !isExpandedForm.value) {
isExpandedForm.value = true
return
}
_openAttachment(item) _openAttachment(item)
} }
@ -169,6 +173,14 @@ const onExpand = () => {
modalVisible.value = true modalVisible.value = true
} }
const onImageClick = (item: any) => {
if (isMobileMode.value && !isExpandedForm.value) return
if (!isMobileMode.value && (isGallery.value || (isKanban.value && !isExpandedForm.value))) return
selectedImage.value = item
}
</script> </script>
<template> <template>
@ -178,7 +190,7 @@ const onExpand = () => {
:style="{ :style="{
height: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`, height: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
}" }"
class="nc-attachment-cell relative flex color-transition flex items-center w-full" class="nc-attachment-cell relative flex color-transition flex items-center w-full xs:(min-h-12 max-h-32)"
:class="{ 'justify-center': !active, 'justify-between': active }" :class="{ 'justify-center': !active, 'justify-between': active }"
> >
<LazyCellAttachmentCarousel /> <LazyCellAttachmentCarousel />
@ -198,26 +210,29 @@ const onExpand = () => {
<div <div
v-if="!isReadonly" v-if="!isReadonly"
:class="{ 'mx-auto px-4': !visibleItems.length }" :class="{ 'sm:(mx-auto px-4) xs:(w-full min-w-8)': !visibleItems.length }"
class="group cursor-pointer py-1 flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)" class="group cursor-pointer py-1 flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
data-testid="attachment-cell-file-picker-button" data-testid="attachment-cell-file-picker-button"
@click.stop="open" @click="open"
> >
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> <component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<NcTooltip placement="bottom"> <NcTooltip placement="bottom" class="xs:w-full">
<template #title <template #title
><span data-rec="true">{{ $t('activity.attachmentDrop') }} </span></template ><span data-rec="true">{{ $t('activity.attachmentDrop') }} </span></template
> >
<div v-if="active || !visibleItems.length || (isForm && visibleItems.length)" class="flex items-center gap-1"> <div
v-if="active || !visibleItems.length || (isForm && visibleItems.length)"
class="flex items-center gap-1 xs:(w-full min-w-12 h-8 justify-center)"
>
<MaterialSymbolsAttachFile <MaterialSymbolsAttachFile
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]" class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]"
/> />
<div <div
v-if="!visibleItems.length" v-if="!visibleItems.length"
data-rec="true" data-rec="true"
class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs" class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs xs:(justify-center rounded-lg text-sm)"
> >
{{ $t('activity.addFiles') }} {{ $t('activity.addFiles') }}
</div> </div>
@ -245,12 +260,7 @@ const onExpand = () => {
<div <div
class="nc-attachment flex items-center flex-col flex-wrap justify-center" class="nc-attachment flex items-center flex-col flex-wrap justify-center"
:class="{ 'ml-2': active }" :class="{ 'ml-2': active }"
@click=" @click="() => onImageClick(item)"
() => {
if (isGallery || isMobileMode || (isKanban && !isExpandedForm)) return
selectedImage = item
}
"
> >
<LazyCellAttachmentImage <LazyCellAttachmentImage
:alt="item.title || `#${i}`" :alt="item.title || `#${i}`"
@ -281,7 +291,7 @@ const onExpand = () => {
<div <div
v-if="active || (isForm && visibleItems.length)" v-if="active || (isForm && visibleItems.length)"
class="h-6 w-5 group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)" class="xs:hidden h-6 w-5 group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
> >
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> <component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />

2
packages/nc-gui/components/dashboard/Sidebar.vue

@ -47,7 +47,7 @@ onUnmounted(() => {
</div> </div>
<div <div
ref="treeViewDom" ref="treeViewDom"
class="flex flex-col nc-scrollbar-dark-md flex-grow xs:(border-transparent pt-2)" class="flex flex-col nc-scrollbar-dark-md flex-grow xs:(border-transparent pt-2 pr-2)"
:class="{ :class="{
'border-t-1': !isSharedBase, 'border-t-1': !isSharedBase,
'border-transparent': !isTreeViewOnScrollTop, 'border-transparent': !isTreeViewOnScrollTop,

2
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -182,7 +182,7 @@ const isTableOpened = computed(() => {
:class="{ '!rotate-180': isExpanded }" :class="{ '!rotate-180': isExpanded }"
/> />
</NcButton> </NcButton>
<div v-else class="min-w-5.75"></div> <div v-else class="sm:min-w-5.75 xs:min-w-7.5 h-2"></div>
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`"> <div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<div <div
class="flex items-center nc-table-icon" class="flex items-center nc-table-icon"

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

@ -144,12 +144,12 @@ async function createOrUpdate() {
} }
} }
async function listenForUpdates() { async function listenForUpdates(id?: string) {
if (listeningForUpdates.value) return if (listeningForUpdates.value) return
listeningForUpdates.value = true listeningForUpdates.value = true
const job = await $api.jobs.status({ syncId: syncSource.value.id }) const job = id ? { id } : await $api.jobs.status({ syncId: syncSource.value.id })
if (!job) { if (!job) {
listeningForUpdates.value = false listeningForUpdates.value = false
@ -226,12 +226,12 @@ async function loadSyncSrc() {
async function sync() { async function sync() {
try { try {
await $fetch(`/api/v1/db/meta/syncs/${syncSource.value.id}/trigger`, { const jobData: any = await $fetch(`/api/v1/db/meta/syncs/${syncSource.value.id}/trigger`, {
baseURL, baseURL,
method: 'POST', method: 'POST',
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },
}) })
listenForUpdates() listenForUpdates(jobData.id)
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -251,6 +251,9 @@ async function abort() {
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },
}) })
step.value = 1 step.value = 1
progress.value = []
goBack.value = false
enableAbort.value = false
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -258,6 +261,13 @@ async function abort() {
}) })
} }
function cancel() {
step.value = 1
progress.value = []
goBack.value = false
enableAbort.value = false
}
function migrateSync(src: any) { function migrateSync(src: any) {
if (!src.details?.options) { if (!src.details?.options) {
src.details.options = { src.details.options = {
@ -456,7 +466,7 @@ onMounted(async () => {
<a-button v-if="showGoToDashboardButton" class="mt-4" size="large" @click="dialogShow = false"> <a-button v-if="showGoToDashboardButton" class="mt-4" size="large" @click="dialogShow = false">
{{ $t('labels.goToDashboard') }} {{ $t('labels.goToDashboard') }}
</a-button> </a-button>
<a-button v-else-if="goBack" class="mt-4 uppercase" size="large" danger @click="step = 1">{{ <a-button v-else-if="goBack" class="mt-4 uppercase" size="large" danger @click="cancel()">{{
$t('general.cancel') $t('general.cancel')
}}</a-button> }}</a-button>
<a-button v-else-if="enableAbort" class="mt-4 uppercase" size="large" danger @click="abort()">{{ <a-button v-else-if="enableAbort" class="mt-4 uppercase" size="large" danger @click="abort()">{{

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

@ -149,10 +149,12 @@ const dialogShow = useVModel(rest, 'modelValue', emit)
if (isWorkerSupport) { if (isWorkerSupport) {
watch( watch(
dialogShow, dialogShow,
(val) => { async (val) => {
if (val) { if (val) {
importWorker = initWorker(importWorkerUrl) importWorker = await initWorker(importWorkerUrl)
} else importWorker?.terminate() } else {
importWorker?.terminate()
}
}, },
{ immediate: true }, { immediate: true },
) )

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

@ -6,7 +6,7 @@ import { iconMap } from '#imports'
<a <a
v-e="['c:navbar:join-cloud']" v-e="['c:navbar:join-cloud']"
class="flex !no-underline" class="flex !no-underline"
href="https://docs.google.com/forms/d/e/1FAIpQLSfKLe8Rcrq0uo2_jM5W1kbVBbzDiQ3IvlP8Iov61FTekVAvzA/viewform?usp=pp_url" href="https://app.nocodb.com/#/signin?utm_source=OSS&utm_medium=OSS&utm_campaign=OSS&utm_content=OSS"
> >
<div <div
class="flex justify-center items-center rounded-l-[3px] w-full cursor-pointer px-2 py-1 !text-current !no-underline text-primary border-1 border-[#cdd1d6] bg-[#EFF2F6] hover:bg-[#e9ebef] m-0" class="flex justify-center items-center rounded-l-[3px] w-full cursor-pointer px-2 py-1 !text-current !no-underline text-primary border-1 border-[#cdd1d6] bg-[#EFF2F6] hover:bg-[#e9ebef] m-0"

8
packages/nc-gui/components/general/ShareProject.vue

@ -92,11 +92,3 @@ const copySharedBase = async () => {
<LazyDlgShareAndCollaborateView :is-view-toolbar="isViewToolbar" /> <LazyDlgShareAndCollaborateView :is-view-toolbar="isViewToolbar" />
</template> </template>
<style lang="scss">
.share-status-tootltip {
.ant-tooltip-inner {
@apply !rounded-md !border-1 !border-gray-200;
}
}
</style>

8
packages/nc-gui/components/general/WorkspaceIcon.vue

@ -8,9 +8,11 @@ const props = defineProps<{
size?: 'small' | 'medium' | 'large' size?: 'small' | 'medium' | 'large'
}>() }>()
const workspaceColor = computed(() => const workspaceColor = computed(() => {
props.workspace ? props.workspace.meta?.color || stringToColor(props.workspace.id!) : undefined, const color = props.workspace ? props.workspace.meta?.color || stringToColor(props.workspace.id!) : undefined
)
return color || '#0A1433'
})
const size = computed(() => props.size || 'medium') const size = computed(() => props.size || 'medium')
</script> </script>

7
packages/nc-gui/components/nc/Button.vue

@ -20,6 +20,7 @@ interface Props {
type?: ButtonType | 'danger' | 'secondary' | undefined type?: ButtonType | 'danger' | 'secondary' | undefined
size?: NcButtonSize size?: NcButtonSize
centered?: boolean centered?: boolean
iconOnly?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -51,10 +52,8 @@ const onFocus = (e: FocusEvent) => {
isFocused.value = false isFocused.value = false
} else { } else {
const relatedTarget = e.relatedTarget as HTMLElement | null const relatedTarget = e.relatedTarget as HTMLElement | null
const focusFromModal =
relatedTarget?.classList?.contains('ant-modal-wrap') || relatedTarget?.classList?.contains('ant-modal-wrap')
isFocused.value = !focusFromModal isFocused.value = !!relatedTarget
} }
isClicked.value = false isClicked.value = false
@ -107,7 +106,7 @@ useEventListener(NcButton, 'mousedown', () => {
<slot v-else name="icon" /> <slot v-else name="icon" />
<div <div
v-if="!(size === 'xxsmall' && loading)" v-if="!(size === 'xxsmall' && loading) && !props.iconOnly"
class="flex flex-row items-center" class="flex flex-row items-center"
:class="{ :class="{
'font-medium': type === 'primary' || type === 'danger', 'font-medium': type === 'primary' || type === 'danger',

10
packages/nc-gui/components/nc/Select.vue

@ -55,6 +55,16 @@ const onChange = (value: string) => {
</template> </template>
<style lang="scss"> <style lang="scss">
.ant-select-item {
@apply !xs:h-13;
}
.ant-select-item-option-content {
@apply !xs:mt-2.5;
}
.ant-select-item-option-state {
@apply !xs:mt-1.75;
}
.nc-select.ant-select { .nc-select.ant-select {
height: fit-content; height: fit-content;
.ant-select-selector { .ant-select-selector {

2
packages/nc-gui/components/shared-view/Gallery.vue

@ -17,6 +17,8 @@ provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters) useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
</script> </script>

6
packages/nc-gui/components/shared-view/Grid.vue

@ -37,7 +37,7 @@ provide(FieldsInj, columns)
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
provide(IsLockedInj, isLocked) provide(IsLockedInj, isLocked)
const { loadGridViewColumns } = useProvideGridViewColumn(sharedView, true) useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
if (signedIn.value) { if (signedIn.value) {
try { try {
@ -55,10 +55,6 @@ watch(
immediate: true, immediate: true,
}, },
) )
onMounted(async () => {
await loadGridViewColumns()
})
</script> </script>
<template> <template>

2
packages/nc-gui/components/shared-view/Kanban.vue

@ -25,6 +25,8 @@ provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters) useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
useProvideKanbanViewStore(meta, sharedView, true) useProvideKanbanViewStore(meta, sharedView, true)

2
packages/nc-gui/components/shared-view/Map.vue

@ -25,6 +25,8 @@ provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters) useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
useProvideMapViewStore(meta, sharedView, true) useProvideMapViewStore(meta, sharedView, true)

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

@ -23,7 +23,7 @@ import {
useI18n, useI18n,
useNuxtApp, useNuxtApp,
useRoles, useRoles,
useViewColumns, useViewColumnsOrThrow,
useViewData, useViewData,
watch, watch,
} from '#imports' } from '#imports'
@ -58,16 +58,14 @@ const isPublic = inject(IsPublicInj, ref(false))
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view) const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view)
const reloadEventHook = createEventHook<boolean | void>() const reloadEventHook = inject(ReloadViewDataHookInj, createEventHook<boolean | void>())
provide(ReloadViewDataHookInj, reloadEventHook)
reloadEventHook.on(async () => { reloadEventHook.on(async () => {
await loadFormView() await loadFormView()
setFormData() setFormData()
}) })
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta, async () => reloadEventHook.trigger()) const { showAll, hideAll, saveOrUpdate } = useViewColumnsOrThrow()
const { syncLTARRefs, row } = useProvideSmartsheetRowStore( const { syncLTARRefs, row } = useProvideSmartsheetRowStore(
meta, meta,

13
packages/nc-gui/components/smartsheet/column/BarcodeOptions.vue

@ -10,13 +10,7 @@ const props = defineProps<{
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj, ref()) const { fields, metaColumnById } = useViewColumnsOrThrow()
const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const { fields, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const vModel = useVModel(props, 'modelValue', emit) const vModel = useVModel(props, 'modelValue', emit)
@ -59,11 +53,12 @@ onMounted(() => {
...vModel.value.meta, ...vModel.value.meta,
} }
vModel.value.fk_barcode_value_column_id = vModel.value.fk_barcode_value_column_id =
(column?.value?.colOptions as Record<string, any>)?.fk_barcode_value_column_id || columnsAllowedAsBarcodeValue.value?.[0] (column?.value?.colOptions as Record<string, any>)?.fk_barcode_value_column_id ||
columnsAllowedAsBarcodeValue.value?.[0]?.value
}) })
watch(columnsAllowedAsBarcodeValue, (newColumnsAllowedAsBarcodeValue) => { watch(columnsAllowedAsBarcodeValue, (newColumnsAllowedAsBarcodeValue) => {
if (vModel.value.fk_barcode_value_column_id == null) { if (vModel.value.fk_barcode_value_column_id === null) {
vModel.value.fk_barcode_value_column_id = newColumnsAllowedAsBarcodeValue?.[0]?.value vModel.value.fk_barcode_value_column_id = newColumnsAllowedAsBarcodeValue?.[0]?.value
} }
}) })

13
packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -2,7 +2,7 @@
import { onMounted } from '@vue/runtime-core' import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { MetaInj, inject, ref, storeToRefs, useBase, useColumnCreateStoreOrThrow, useMetas, useVModel } from '#imports' import { MetaInj, inject, ref, storeToRefs, useBase, useColumnCreateStoreOrThrow, useI18n, useMetas, useVModel } from '#imports'
const props = defineProps<{ const props = defineProps<{
value: any value: any
@ -16,11 +16,10 @@ const meta = inject(MetaInj, ref())
const { t } = useI18n() const { t } = useI18n()
const { appInfo } = useGlobal()
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const baseStore = useBase() const baseStore = useBase()
const { tables } = storeToRefs(baseStore) const { tables } = storeToRefs(baseStore)
const { metas } = useMetas() const { metas } = useMetas()
@ -39,13 +38,7 @@ const refTables = computed(() => {
} }
const _refTables = meta.value.columns const _refTables = meta.value.columns
.filter( .filter((column) => isLinksOrLTAR(column) && !column.system && column.source_id === meta.value?.source_id)
(column) =>
isLinksOrLTAR(column) &&
!column.system &&
column.source_id === meta.value?.source_id &&
(!appInfo.value.ee || vModel.value.fk_relation_column_id === column.id || (column?.colOptions as any)?.type === 'bt'),
)
.map((column) => ({ .map((column) => ({
col: column.colOptions, col: column.colOptions,
column, column,

11
packages/nc-gui/components/smartsheet/column/QrCodeOptions.vue

@ -10,15 +10,9 @@ const props = defineProps<{
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const { t } = useI18n() const { t } = useI18n()
const reloadDataHook = inject(ReloadViewDataHookInj)! const { fields, metaColumnById } = useViewColumnsOrThrow()
const { fields, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const vModel = useVModel(props, 'modelValue', emit) const vModel = useVModel(props, 'modelValue', emit)
@ -40,7 +34,8 @@ const columnsAllowedAsQrValue = computed<SelectProps['options']>(() => {
onMounted(() => { onMounted(() => {
// set default value // set default value
vModel.value.fk_qr_value_column_id = (column?.value?.colOptions as Record<string, any>)?.fk_qr_value_column_id || '' vModel.value.fk_qr_value_column_id =
(column?.value?.colOptions as Record<string, any>)?.fk_qr_value_column_id || columnsAllowedAsQrValue.value?.[0]?.value
}) })
setAdditionalValidations({ setAdditionalValidations({

7
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -45,12 +45,7 @@ const visibilityOps = ref<fieldsVisibilityOps[]>([])
const fieldsListWrapperDomRef = ref<HTMLElement>() const fieldsListWrapperDomRef = ref<HTMLElement>()
const { const { fields: viewFields, toggleFieldVisibility, loadViewColumns, isViewColumnsLoading } = useViewColumnsOrThrow()
fields: viewFields,
toggleFieldVisibility,
loadViewColumns,
isViewColumnsLoading,
} = useViewColumns(view, meta as Ref<TableType | undefined>)
const loading = ref(false) const loading = ref(false)

78
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -4,12 +4,18 @@ import type { AuditType } from 'nocodb-sdk'
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { ref, timeAgo, useExpandedFormStoreOrThrow, useGlobal, useRoles, watch } from '#imports' import { ref, timeAgo, useExpandedFormStoreOrThrow, useGlobal, useRoles, watch } from '#imports'
const props = defineProps<{
isLoading: boolean
}>()
const { loadCommentsAndLogs, commentsAndLogs, saveComment: _saveComment, comment, updateComment } = useExpandedFormStoreOrThrow() const { loadCommentsAndLogs, commentsAndLogs, saveComment: _saveComment, comment, updateComment } = useExpandedFormStoreOrThrow()
const commentsWrapperEl = ref<HTMLDivElement>() const commentsWrapperEl = ref<HTMLDivElement>()
const { user, appInfo } = useGlobal() const { user, appInfo } = useGlobal()
const isExpandedFormLoading = computed(() => props.isLoading)
const tab = ref<'comments' | 'audits'>('comments') const tab = ref<'comments' | 'audits'>('comments')
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
@ -69,7 +75,7 @@ onKeyStroke('Enter', (event) => {
}) })
const comments = computed(() => commentsAndLogs.value.filter((log) => log.op_type === 'COMMENT')) const comments = computed(() => commentsAndLogs.value.filter((log) => log.op_type === 'COMMENT'))
const audits = computed(() => commentsAndLogs.value.filter((log) => log.op_type !== 'COMMENT')) const audits = computed(() => commentsAndLogs.value.filter((log) => log.op_type !== 'COMMENT' && log.details))
function editComment(log: AuditType) { function editComment(log: AuditType) {
editLog.value = log editLog.value = log
@ -90,9 +96,22 @@ function scrollComments() {
if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight
} }
const isSaving = ref(false)
const saveComment = async () => { const saveComment = async () => {
await _saveComment() if (isSaving.value) return
scrollComments()
isSaving.value = true
try {
await _saveComment()
scrollComments()
} catch (e) {
console.error(e)
} finally {
isSaving.value = false
}
} }
watch(commentsWrapperEl, () => { watch(commentsWrapperEl, () => {
@ -157,10 +176,13 @@ const onClickAudit = () => {
<div <div
class="h-[calc(100%-4rem)]" class="h-[calc(100%-4rem)]"
:class="{ :class="{
'pb-2': tab !== 'comments' && !appInfo.ee, 'pb-1': tab !== 'comments' && !appInfo.ee,
}" }"
> >
<div v-if="tab === 'comments'" class="flex flex-col h-full"> <div v-if="isExpandedFormLoading" class="flex flex-col h-full">
<GeneralLoader class="!mt-16" size="xlarge" />
</div>
<div v-else-if="tab === 'comments'" class="flex flex-col h-full">
<div v-if="comments.length === 0" class="flex flex-col my-1 text-center justify-center h-full"> <div v-if="comments.length === 0" class="flex flex-col my-1 text-center justify-center h-full">
<div class="text-center text-3xl text-gray-700"> <div class="text-center text-3xl text-gray-700">
<GeneralIcon icon="commentHere" /> <GeneralIcon icon="commentHere" />
@ -229,15 +251,17 @@ const onClickAudit = () => {
v-e="['a:row-expand:comment:save']" v-e="['a:row-expand:comment:save']"
size="medium" size="medium"
class="!w-8" class="!w-8"
:disabled="!comment.length" :loading="isSaving"
:disabled="!isSaving && !comment.length"
:icon-only="isSaving"
@click="saveComment" @click="saveComment"
> >
<GeneralIcon icon="send" /> <GeneralIcon v-if="!isSaving" icon="send" />
</NcButton> </NcButton>
</div> </div>
</div> </div>
</div> </div>
<div v-if="tab === 'audits'" ref="commentsWrapperEl" class="flex flex-col h-full pl-2 pr-1 pt-2 nc-scrollbar-md space-y-2"> <div v-if="tab === 'audits'" ref="commentsWrapperEl" class="flex flex-col h-full nc-scrollbar-md !overflow-y-auto">
<template v-if="audits.length === 0"> <template v-if="audits.length === 0">
<div class="flex flex-col text-center justify-center h-full"> <div class="flex flex-col text-center justify-center h-full">
<div class="text-center text-3xl text-gray-600"> <div class="text-center text-3xl text-gray-600">
@ -246,25 +270,24 @@ const onClickAudit = () => {
<div class="font-bold text-center my-1 text-gray-600">See changes to this record</div> <div class="font-bold text-center my-1 text-gray-600">See changes to this record</div>
</div> </div>
</template> </template>
<div v-for="log of audits" :key="log.id">
<div v-if="log.details" class="bg-white rounded-xl border-1 gap-3 border-gray-200"> <div v-for="log of audits" :key="log.id" class="nc-audit-item">
<div class="flex flex-col p-4 gap-3"> <div class="flex flex-col p-4 gap-3">
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<GeneralUserIcon size="base" :email="log.user" /> <GeneralUserIcon size="base" :email="log.user" />
<div class="flex flex-col"> <div class="flex flex-col">
<span class="truncate font-bold max-w-50"> <span class="truncate font-bold max-w-50">
{{ log.display_name ?? log.user.split('@')[0].slice(0, 2) ?? 'Shared source' }} {{ log.display_name ?? log.user.split('@')[0].slice(0, 2) ?? 'Shared source' }}
</span> </span>
<div v-if="log.id !== editLog?.id" class="text-xs font-medium text-gray-500"> <div v-if="log.id !== editLog?.id" class="text-xs font-medium text-gray-500">
{{ timeAgo(log.created_at) }} {{ timeAgo(log.created_at) }}
</div>
</div> </div>
</div> </div>
</div> </div>
<div v-dompurify-html="log.details" class="text-sm font-medium"></div>
</div> </div>
<div v-dompurify-html="log.details" class="text-sm font-medium"></div>
</div> </div>
</div> </div>
</div> </div>
@ -276,6 +299,15 @@ const onClickAudit = () => {
.tab { .tab {
@apply max-w-1/2; @apply max-w-1/2;
} }
.nc-audit-item {
@apply border-b-1 gap-3 border-gray-200;
}
.nc-audit-item:last-child {
@apply border-b-0;
}
.tab .tab-title { .tab .tab-title {
@apply min-w-0 flex justify-center gap-2 font-semibold items-center; @apply min-w-0 flex justify-center gap-2 font-semibold items-center;
word-break: 'keep-all'; word-break: 'keep-all';

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

@ -438,7 +438,7 @@ export default {
:class="{ active: isExpanded }" :class="{ active: isExpanded }"
> >
<div class="h-[85vh] xs:(max-h-full) max-h-215 flex flex-col p-6"> <div class="h-[85vh] xs:(max-h-full) max-h-215 flex flex-col p-6">
<div class="flex h-8 flex-shrink-0 w-full items-center nc-expanded-form-header relative mb-4 justify-between"> <div class="flex h-9.5 flex-shrink-0 w-full items-center nc-expanded-form-header relative mb-4 justify-between">
<template v-if="!isMobileMode"> <template v-if="!isMobileMode">
<div class="flex gap-3 w-100"> <div class="flex gap-3 w-100">
<div class="flex gap-2"> <div class="flex gap-2">
@ -484,7 +484,7 @@ export default {
{{ isRecordLinkCopied ? $t('labels.copiedRecordURL') : $t('labels.copyRecordURL') }} {{ isRecordLinkCopied ? $t('labels.copiedRecordURL') : $t('labels.copyRecordURL') }}
</div> </div>
</NcButton> </NcButton>
<NcDropdown v-if="!isNew"> <NcDropdown v-if="!isNew" placement="bottomRight">
<NcButton type="secondary" class="nc-expand-form-more-actions w-10"> <NcButton type="secondary" class="nc-expand-form-more-actions w-10">
<GeneralIcon icon="threeDotVertical" class="text-md text-gray-700" /> <GeneralIcon icon="threeDotVertical" class="text-md text-gray-700" />
</NcButton> </NcButton>
@ -546,7 +546,7 @@ export default {
<template v-else> <template v-else>
<div class="flex flex-row w-full"> <div class="flex flex-row w-full">
<NcButton <NcButton
v-if="props.showNextPrevIcons" v-if="props.showNextPrevIcons && !isFirstRow"
v-e="['c:row-expand:prev']" v-e="['c:row-expand:prev']"
type="secondary" type="secondary"
class="nc-prev-arrow !w-10" class="nc-prev-arrow !w-10"
@ -554,11 +554,12 @@ export default {
> >
<GeneralIcon icon="arrowLeft" class="text-lg text-gray-700" /> <GeneralIcon icon="arrowLeft" class="text-lg text-gray-700" />
</NcButton> </NcButton>
<div v-else class="min-w-10.5"></div>
<div class="flex flex-grow justify-center items-center font-semibold text-lg"> <div class="flex flex-grow justify-center items-center font-semibold text-lg">
<div>{{ meta.title }}</div> <div>{{ meta.title }}</div>
</div> </div>
<NcButton <NcButton
v-if="props.showNextPrevIcons && !props.lastRow" v-if="props.showNextPrevIcons && !islastRow"
v-e="['c:row-expand:next']" v-e="['c:row-expand:next']"
type="secondary" type="secondary"
class="nc-next-arrow !w-10" class="nc-next-arrow !w-10"
@ -566,6 +567,7 @@ export default {
> >
<GeneralIcon icon="arrowRight" class="text-lg text-gray-700" /> <GeneralIcon icon="arrowRight" class="text-lg text-gray-700" />
</NcButton> </NcButton>
<div v-else class="min-w-10.5"></div>
</div> </div>
</template> </template>
</div> </div>
@ -578,7 +580,7 @@ export default {
}" }"
> >
<div <div
class="flex flex-col flex-grow mt-2 h-full max-h-full nc-scrollbar-md !pb-2 items-center w-full bg-white p-4 xs:p-0" class="flex flex-col flex-grow mt-2 h-full max-h-full nc-scrollbar-md pb-6 items-center w-full bg-white p-4 xs:p-0"
> >
<div <div
v-for="(col, i) of fields" v-for="(col, i) of fields"
@ -590,14 +592,14 @@ export default {
:data-testid="`nc-expand-col-${col.title}`" :data-testid="`nc-expand-col-${col.title}`"
> >
<div class="flex items-start flex-row sm:(gap-x-6) xs:(flex-col w-full) nc-expanded-cell min-h-10"> <div class="flex items-start flex-row sm:(gap-x-6) xs:(flex-col w-full) nc-expanded-cell min-h-10">
<div class="w-[12rem] xs:(w-full) mt-0.25 !h-[35px]"> <div class="w-48 xs:(w-full) mt-0.25 !h-[35px]">
<LazySmartsheetHeaderVirtualCell <LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(col)" v-if="isVirtualCol(col)"
class="nc-expanded-cell-header h-full !text-gray-500" class="nc-expanded-cell-header h-full"
:column="col" :column="col"
/> />
<LazySmartsheetHeaderCell v-else class="nc-expanded-cell-header !text-gray-500" :column="col" /> <LazySmartsheetHeaderCell v-else class="nc-expanded-cell-header" :column="col" />
</div> </div>
<template v-if="isLoading"> <template v-if="isLoading">
@ -616,7 +618,7 @@ export default {
<SmartsheetDivDataCell <SmartsheetDivDataCell
v-if="col.title" v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)" :ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="!bg-white rounded-lg !w-[20rem] !xs:w-full border-1 border-gray-200 overflow-hidden px-1 min-h-[35px] flex items-center relative" class="!bg-white rounded-lg !w-[20rem] !xs:w-full border-1 border-gray-200 overflow-hidden px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
> >
<LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="_row.row[col.title]" :row="_row" :column="col" /> <LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="_row.row[col.title]" :row="_row" :column="col" />
@ -633,29 +635,34 @@ export default {
</template> </template>
</div> </div>
</div> </div>
<div v-if="hiddenFields.length > 0" class="flex w-full px-12 items-center py-3"> <div v-if="hiddenFields.length > 0" class="flex w-full sm:px-12 xs:(px-1 mt-2) items-center py-3">
<div class="flex-grow h-px mr-1 bg-gray-100"></div> <div class="flex-grow h-px mr-1 bg-gray-100"></div>
<NcButton type="secondary" size="small" class="flex-shrink-1 !text-sm" @click="toggleHiddenFields"> <NcButton
type="secondary"
:size="isMobileMode ? 'medium' : 'small'"
class="flex-shrink-1 !text-sm"
@click="toggleHiddenFields"
>
{{ showHiddenFields ? `Hide ${hiddenFields.length} hidden` : `Show ${hiddenFields.length} hidden` }} {{ showHiddenFields ? `Hide ${hiddenFields.length} hidden` : `Show ${hiddenFields.length} hidden` }}
{{ hiddenFields.length > 1 ? `fields` : `field` }} {{ hiddenFields.length > 1 ? `fields` : `field` }}
<MdiChevronDown class="ml-1" :class="showHiddenFields ? 'transform rotate-180' : ''" /> <MdiChevronDown class="ml-1" :class="showHiddenFields ? 'transform rotate-180' : ''" />
</NcButton> </NcButton>
<div class="flex-grow h-px ml-1 bg-gray-100"></div> <div class="flex-grow h-px ml-1 bg-gray-100"></div>
</div> </div>
<div v-if="hiddenFields.length > 0 && showHiddenFields" class="mb-3"> <div v-if="hiddenFields.length > 0 && showHiddenFields" class="flex flex-col w-full mb-3 items-center">
<div <div
v-for="(col, i) of hiddenFields" v-for="(col, i) of hiddenFields"
v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)" v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
:key="col.title" :key="col.title"
class="mt-2 py-2" class="sm:(mt-2) py-2 xs:w-full"
:class="`nc-expand-col-${col.title}`" :class="`nc-expand-col-${col.title}`"
:data-testid="`nc-expand-col-${col.title}`" :data-testid="`nc-expand-col-${col.title}`"
> >
<div class="flex flex-row items-start min-h-10"> <div class="sm:gap-x-6 flex sm:flex-row xs:(flex-col) items-start min-h-10">
<div class="w-[12rem] scale-110 !h-[35px] mt-2.5"> <div class="sm:w-48 xs:w-full scale-110 !h-[35px]">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" class="!text-gray-600" /> <LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" class="nc-expanded-cell-header" />
<LazySmartsheetHeaderCell v-else class="!text-gray-600" :column="col" /> <LazySmartsheetHeaderCell v-else class="nc-expanded-cell-header" :column="col" />
</div> </div>
<template v-if="isLoading"> <template v-if="isLoading">
@ -674,7 +681,7 @@ export default {
<LazySmartsheetDivDataCell <LazySmartsheetDivDataCell
v-if="col.title" v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)" :ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="!bg-white rounded-lg !w-[20rem] border-1 overflow-hidden border-gray-200 px-1 min-h-[35px] flex items-center relative" class="!bg-white rounded-lg !w-[20rem] border-1 overflow-hidden border-gray-200 px-1 sm:min-h-[35px] xs:min-h-13 flex items-center relative"
> >
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(col)" v-if="isVirtualCol(col)"
@ -703,7 +710,7 @@ export default {
v-if="isUIAllowed('dataEdit')" v-if="isUIAllowed('dataEdit')"
class="w-full h-16 border-t-1 border-gray-200 bg-white flex items-center justify-end p-3 xs:(p-0 mt-4 border-t-0 gap-x-4 justify-between)" class="w-full h-16 border-t-1 border-gray-200 bg-white flex items-center justify-end p-3 xs:(p-0 mt-4 border-t-0 gap-x-4 justify-between)"
> >
<NcDropdown v-if="!isNew && isMobileMode"> <NcDropdown v-if="!isNew && isMobileMode" placement="bottomRight">
<NcButton type="secondary" class="nc-expand-form-more-actions w-10"> <NcButton type="secondary" class="nc-expand-form-more-actions w-10">
<GeneralIcon icon="threeDotVertical" class="text-md text-gray-700" /> <GeneralIcon icon="threeDotVertical" class="text-md text-gray-700" />
</NcButton> </NcButton>
@ -761,7 +768,7 @@ export default {
class="nc-comments-drawer border-1 relative border-gray-200 w-1/3 max-w-125 bg-gray-50 rounded-xl min-w-0 overflow-hidden h-full xs:hidden" class="nc-comments-drawer border-1 relative border-gray-200 w-1/3 max-w-125 bg-gray-50 rounded-xl min-w-0 overflow-hidden h-full xs:hidden"
:class="{ active: commentsDrawer && isUIAllowed('commentList') }" :class="{ active: commentsDrawer && isUIAllowed('commentList') }"
> >
<SmartsheetExpandedFormComments /> <SmartsheetExpandedFormComments :loading="isLoading" />
</div> </div>
</div> </div>
</div> </div>
@ -811,7 +818,7 @@ export default {
} }
.nc-expanded-cell-header { .nc-expanded-cell-header {
@apply w-full text-gray-700 xs:mb-2; @apply w-full text-gray-500 xs:(text-gray-600 mb-2);
} }
.nc-expanded-cell-header > :nth-child(2) { .nc-expanded-cell-header > :nth-child(2) {
@ -825,3 +832,13 @@ export default {
@apply !p-0; @apply !p-0;
} }
</style> </style>
<style lang="scss" scoped>
:deep(.ant-select-selector) {
@apply !xs:(h-full);
}
:deep(.ant-select-selection-item) {
@apply !xs:(mt-1.75 ml-1);
}
</style>

24
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -28,7 +28,6 @@ import {
provide, provide,
ref, ref,
useEventListener, useEventListener,
useGridViewColumnOrThrow,
useI18n, useI18n,
useMultiSelect, useMultiSelect,
useNuxtApp, useNuxtApp,
@ -36,6 +35,7 @@ import {
useRoute, useRoute,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUndoRedo, useUndoRedo,
useViewColumnsOrThrow,
useViewsStore, useViewsStore,
watch, watch,
} from '#imports' } from '#imports'
@ -123,8 +123,6 @@ const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook()) const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
const { isViewColumnsLoading } = useViewColumns(view, meta, () => reloadViewDataHook.trigger())
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const scrollParent = inject(ScrollParentInj, ref<undefined>()) const scrollParent = inject(ScrollParentInj, ref<undefined>())
@ -141,6 +139,8 @@ const { getMeta } = useMetas()
const { addUndo, clone, defineViewScope } = useUndoRedo() const { addUndo, clone, defineViewScope } = useUndoRedo()
const { isViewColumnsLoading, updateGridViewColumn, gridViewCols, resizingColOldWith } = useViewColumnsOrThrow()
const { const {
predictingNextColumn, predictingNextColumn,
predictedNextColumn, predictedNextColumn,
@ -894,8 +894,6 @@ const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue?
} }
// #Grid Resize // #Grid Resize
const { updateGridViewColumn, gridViewCols, resizingColOldWith } = useGridViewColumnOrThrow()
const onresize = (colID: string | undefined, event: any) => { const onresize = (colID: string | undefined, event: any) => {
if (!colID) return if (!colID) return
updateGridViewColumn(colID, { width: event.detail }) updateGridViewColumn(colID, { width: event.detail })
@ -1211,7 +1209,7 @@ const loaderText = computed(() => {
<div class="table-overlay" :class="{ 'nc-grid-skelton-loader': showSkeleton }"> <div class="table-overlay" :class="{ 'nc-grid-skelton-loader': showSkeleton }">
<table <table
ref="smartTable" ref="smartTable"
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white" class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white relative"
:class="{ :class="{
mobile: isMobileMode, mobile: isMobileMode,
desktop: !isMobileMode, desktop: !isMobileMode,
@ -1554,11 +1552,15 @@ const loaderText = computed(() => {
@mouseup.stop @mouseup.stop
@click="addEmptyRow()" @click="addEmptyRow()"
> >
<td class="text-left pointer sticky left-0 !border-r-0"> <div
<div class="px-2 w-full flex items-center text-gray-500"> class="h-10.5 border-b-1 border-gray-100 bg-white group-hover:bg-gray-50 absolute left-0 bottom-0 px-2 sticky z-40 w-full flex items-center text-gray-500"
<component :is="iconMap.plus" class="text-pint-500 text-base ml-2 text-gray-600 group-hover:text-black" /> >
</div> <component
</td> :is="iconMap.plus"
v-if="!isViewColumnsLoading"
class="text-pint-500 text-base ml-2 mt-0 text-gray-600 group-hover:text-black"
/>
</div>
<td class="!border-gray-100" :colspan="visibleColLength"></td> <td class="!border-gray-100" :colspan="visibleColLength"></td>
</tr> </tr>
</tbody> </tbody>

2
packages/nc-gui/components/smartsheet/header/CellIcon.ts

@ -63,7 +63,7 @@ const renderIcon = (column: ColumnType, abstractType: any) => {
} else if (isYear(column, abstractType)) { } else if (isYear(column, abstractType)) {
return iconMap.calendar return iconMap.calendar
} else if (isTime(column, abstractType)) { } else if (isTime(column, abstractType)) {
return iconMap.calendar return iconMap.clock
} else if (isRating(column)) { } else if (isRating(column)) {
return iconMap.rating return iconMap.rating
} else if (isAttachment(column)) { } else if (isAttachment(column)) {

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

@ -21,7 +21,7 @@ const activeView = inject(ActiveViewInj, ref())
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta) const { showSystemFields, metaColumnById } = useViewColumnsOrThrow(activeView, meta)
const { sorts } = useViewSorts(activeView) const { sorts } = useViewSorts(activeView)

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

@ -2,7 +2,7 @@
import type { SelectProps } from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { ActiveViewInj, MetaInj, computed, inject, ref, resolveComponent, useViewColumns } from '#imports' import { ActiveViewInj, MetaInj, computed, inject, ref, resolveComponent, useViewColumnsOrThrow } from '#imports'
const { modelValue, isSort, allowEmpty, ...restProps } = defineProps<{ const { modelValue, isSort, allowEmpty, ...restProps } = defineProps<{
modelValue?: string modelValue?: string
@ -24,7 +24,7 @@ const localValue = computed({
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta) const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const options = computed<SelectProps['options']>(() => const options = computed<SelectProps['options']>(() =>
( (

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

@ -21,16 +21,12 @@ import {
useNuxtApp, useNuxtApp,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUndoRedo, useUndoRedo,
useViewColumns, useViewColumnsOrThrow,
watch, watch,
} from '#imports' } from '#imports'
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const reloadViewMetaHook = inject(ReloadViewMetaHookInj, undefined)! const reloadViewMetaHook = inject(ReloadViewMetaHookInj, undefined)!
const rootFields = inject(FieldsInj) const rootFields = inject(FieldsInj)
@ -55,7 +51,7 @@ const {
metaColumnById, metaColumnById,
loadViewColumns, loadViewColumns,
toggleFieldVisibility, toggleFieldVisibility,
} = useViewColumns(activeView, meta, () => reloadDataHook.trigger()) } = useViewColumnsOrThrow()
const { eventBus } = useSmartsheetStoreOrThrow() const { eventBus } = useSmartsheetStoreOrThrow()

3
packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue

@ -14,6 +14,7 @@ import {
useMetas, useMetas,
useNuxtApp, useNuxtApp,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useViewColumnsOrThrow,
} from '#imports' } from '#imports'
const groupingUidt = [ const groupingUidt = [
@ -33,7 +34,7 @@ const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref()) const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const { gridViewCols, updateGridViewColumn } = useGridViewColumnOrThrow() const { gridViewCols, updateGridViewColumn } = useViewColumnsOrThrow()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()

6
packages/nc-gui/components/smartsheet/toolbar/MappedBy.vue

@ -12,7 +12,7 @@ import {
iconMap, iconMap,
inject, inject,
ref, ref,
useViewColumns, useViewColumnsOrThrow,
watch, watch,
} from '#imports' } from '#imports'
@ -22,13 +22,11 @@ const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const IsPublic = inject(IsPublicInj, ref(false)) const IsPublic = inject(IsPublicInj, ref(false))
const { fields, loadViewColumns, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger()) const { fields, loadViewColumns, metaColumnById } = useViewColumnsOrThrow()
const { loadMapData, loadMapMeta, updateMapMeta, mapMetaData, geoDataFieldColumn } = useMapViewStoreOrThrow() const { loadMapData, loadMapMeta, updateMapMeta, mapMetaData, geoDataFieldColumn } = useMapViewStoreOrThrow()

16
packages/nc-gui/components/smartsheet/toolbar/SearchData.vue

@ -98,20 +98,8 @@ watch(columns, () => {
<div v-if="!isMobileMode" class="w-16 text-[0.75rem] font-medium text-gray-400 truncate"> <div v-if="!isMobileMode" class="w-16 text-[0.75rem] font-medium text-gray-400 truncate">
{{ displayColumnLabel }} {{ displayColumnLabel }}
</div> </div>
<div <div class="xs:(text-gray-600) group-hover:text-gray-700 sm:(text-gray-400)">
:class="{ <component :is="iconMap.arrowDown" class="text-sm text-inherit" />
'opacity-0 group-hover:opacity-100': !isMobileMode,
'text-gray-700': isMobileMode,
}"
>
<component
:is="iconMap.arrowDown"
class="text-sm"
:class="{
'text-gray-400': !isMobileMode,
'text-gray-600': isMobileMode,
}"
/>
</div> </div>
<a-select <a-select
v-model:value="search.field" v-model:value="search.field"

14
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -28,7 +28,7 @@ const { eventBus } = useSmartsheetStoreOrThrow()
const { sorts, saveOrUpdate, loadSorts, addSort: _addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger()) const { sorts, saveOrUpdate, loadSorts, addSort: _addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger())
const { showSystemFields, metaColumnById } = useViewColumns(view, meta) const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const showCreateSort = ref(false) const showCreateSort = ref(false)
@ -79,14 +79,6 @@ const getColumnUidtByID = (key?: string) => {
return columnByID.value[key]?.uidt || '' return columnByID.value[key]?.uidt || ''
} }
watch(
() => view.value?.id,
(viewId) => {
if (viewId) loadSorts()
},
{ immediate: true },
)
const open = ref(false) const open = ref(false)
useMenuCloseOnEsc(open) useMenuCloseOnEsc(open)
@ -105,6 +97,10 @@ watch(open, () => {
showCreateSort.value = false showCreateSort.value = false
} }
}) })
onMounted(() => {
loadSorts()
})
</script> </script>
<template> <template>

4
packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue

@ -14,7 +14,7 @@ import {
useKanbanViewStoreOrThrow, useKanbanViewStoreOrThrow,
useMenuCloseOnEsc, useMenuCloseOnEsc,
useUndoRedo, useUndoRedo,
useViewColumns, useViewColumnsOrThrow,
watch, watch,
} from '#imports' } from '#imports'
@ -30,7 +30,7 @@ const reloadDataHook = inject(ReloadViewDataHookInj)!
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const { fields, loadViewColumns, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger()) const { fields, loadViewColumns, metaColumnById } = useViewColumnsOrThrow(activeView, meta)
const { kanbanMetaData, loadKanbanMeta, loadKanbanData, updateKanbanMeta, groupingField, groupingFieldColumn } = const { kanbanMetaData, loadKanbanMeta, loadKanbanData, updateKanbanMeta, groupingField, groupingFieldColumn } =
useKanbanViewStoreOrThrow() useKanbanViewStoreOrThrow()

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

@ -63,7 +63,6 @@ const openNewRecordFormHook = createEventHook<void>()
useProvideKanbanViewStore(meta, activeView) useProvideKanbanViewStore(meta, activeView)
useProvideMapViewStore(meta, activeView) useProvideMapViewStore(meta, activeView)
useProvideGridViewColumn(activeView)
// todo: move to store // todo: move to store
provide(MetaInj, meta) provide(MetaInj, meta)
@ -80,6 +79,8 @@ provide(
computed(() => !isUIAllowed('dataEdit')), computed(() => !isUIAllowed('dataEdit')),
) )
useProvideViewColumns(activeView, meta, () => reloadEventHook?.trigger())
const grid = ref() const grid = ref()
const onDrop = async (event: DragEvent) => { const onDrop = async (event: DragEvent) => {

5
packages/nc-gui/composables/useAttachment.ts

@ -28,8 +28,9 @@ const useAttachment = () => {
} catch {} } catch {}
} }
// if no source can be fetched, it could be probably blocked by CORS // if no source can be fetched, it could be probably blocked by CORS
// return original url or built url anyway // return signed url / original url / built url anyway
return item.url || `${appInfo.value.ncSiteUrl}/${item.path}` // which we can extract from the sources array since it's ordered based on priority
return sources[0]
} }
const openAttachment = async (item: Record<string, any>) => { const openAttachment = async (item: Record<string, any>) => {

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

@ -35,7 +35,9 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
tableExplorerColumns?: Ref<ColumnType[] | undefined>, tableExplorerColumns?: Ref<ColumnType[] | undefined>,
) => { ) => {
const baseStore = useBase() const baseStore = useBase()
const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc, isXcdbBase: isXcdbBaseFunc, getBaseType } = baseStore
const { isMysql: isMysqlFunc, isPg: isPgFunc, isMssql: isMssqlFunc, isXcdbBase: isXcdbBaseFunc } = baseStore
const { sqlUis } = storeToRefs(baseStore) const { sqlUis } = storeToRefs(baseStore)
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -60,8 +62,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
isXcdbBaseFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]), isXcdbBaseFunc(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]),
) )
const baseType = computed(() => getBaseType(meta.value?.source_id ? meta.value?.source_id : Object.keys(sqlUis.value)[0]))
const idType = null const idType = null
const additionalValidations = ref<ValidationsObj>({}) const additionalValidations = ref<ValidationsObj>({})

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

@ -141,11 +141,11 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
description: `The following comment has been created: ${comment.value}`, description: `The following comment has been created: ${comment.value}`,
}) })
comment.value = ''
reloadTrigger?.trigger() reloadTrigger?.trigger()
await loadCommentsAndLogs() await loadCommentsAndLogs()
comment.value = ''
} catch (e: any) { } catch (e: any) {
message.error(e.message) message.error(e.message)
} }

97
packages/nc-gui/composables/useGridViewColumn.ts

@ -1,97 +0,0 @@
import type { ColumnType, GridColumnReqType, GridColumnType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { IsPublicInj, computed, inject, ref, useMetas, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
(view: Ref<(ViewType & { columns?: GridColumnType[] }) | undefined>, statePublic = false) => {
const { isUIAllowed } = useRoles()
const { $api } = useNuxtApp()
const { metas } = useMetas()
const { addUndo, defineViewScope } = useUndoRedo()
const gridViewCols = ref<Record<string, GridColumnType>>({})
const resizingColOldWith = ref('200px')
const isPublic = inject(IsPublicInj, ref(statePublic))
const columns = computed<ColumnType[]>(() => metas.value?.[view.value?.fk_model_id as string]?.columns || [])
const loadGridViewColumns = async () => {
if (!view.value?.id && !isPublic.value) return
const colsData: GridColumnType[] =
(isPublic.value ? view.value?.columns : await $api.dbView.gridColumnsList(view.value!.id!)) ?? []
gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
(o, col) => ({
...o,
[col.fk_column_id as string]: col,
}),
{},
)
}
/** when columns changes(create/delete) reload grid columns
* or when view changes reload columns width */
watch(
[() => columns.value?.length, () => view.value?.id],
async (n) => {
if (n[1]) await loadGridViewColumns()
},
{ immediate: true },
)
const updateGridViewColumn = async (id: string, props: Partial<GridColumnReqType>, undo = false) => {
if (!undo) {
const oldProps = Object.keys(props).reduce<Partial<GridColumnReqType>>((o: any, k) => {
if (gridViewCols.value[id][k as keyof GridColumnType]) {
if (k === 'width') o[k] = `${resizingColOldWith.value}px`
else o[k] = gridViewCols.value[id][k as keyof GridColumnType]
}
return o
}, {})
addUndo({
redo: {
fn: (w: Partial<GridColumnReqType>) => updateGridViewColumn(id, w, true),
args: [props],
},
undo: {
fn: (w: Partial<GridColumnReqType>) => updateGridViewColumn(id, w, true),
args: [oldProps],
},
scope: defineViewScope({ view: view.value }),
})
}
// sync with server if allowed
if (!isPublic.value && isUIAllowed('viewFieldEdit') && gridViewCols.value[id]?.id) {
await $api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, {
...props,
})
}
if (gridViewCols.value?.[id]) {
Object.assign(gridViewCols.value[id], {
...gridViewCols.value[id],
...props,
})
} else {
// fallback to reload
await loadGridViewColumns()
}
}
return { loadGridViewColumns, updateGridViewColumn, gridViewCols, resizingColOldWith }
},
'useGridViewColumn',
)
export { useProvideGridViewColumn }
export function useGridViewColumnOrThrow() {
const gridViewColumn = useGridViewColumn()
if (gridViewColumn == null) throw new Error('Please call `useProvideGridViewColumn` on the appropriate parent component')
return gridViewColumn
}

10
packages/nc-gui/composables/useTableNew.ts

@ -56,8 +56,6 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
const tables = computed(() => baseTables.value.get(param.baseId) || []) const tables = computed(() => baseTables.value.get(param.baseId) || [])
const base = computed(() => bases.value.get(param.baseId)) const base = computed(() => bases.value.get(param.baseId))
const { loadViews } = useViewsStore()
const openTable = async (table: TableType) => { const openTable = async (table: TableType) => {
if (!table.base_id) return if (!table.base_id) return
@ -82,12 +80,10 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
baseIdOrBaseId = route.value.params.baseId as string baseIdOrBaseId = route.value.params.baseId as string
} }
await loadViews({
tableId: table.id,
})
const views = viewsByTable.value.get(table.id as string) ?? [] const views = viewsByTable.value.get(table.id as string) ?? []
getMeta(table.id as string, (route.value.params?.viewId as string) !== table.id)
if (openedViewsTab.value !== 'view' && views[0].id) { if (openedViewsTab.value !== 'view' && views[0].id) {
await navigateTo({ await navigateTo({
path: `/${workspaceIdOrType}/${baseIdOrBaseId}/${table?.id}/${views[0].id}/${openedViewsTab.value}`, path: `/${workspaceIdOrType}/${baseIdOrBaseId}/${table?.id}/${views[0].id}/${openedViewsTab.value}`,
@ -98,8 +94,6 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
path: `/${workspaceIdOrType}/${baseIdOrBaseId}/${table?.id}`, path: `/${workspaceIdOrType}/${baseIdOrBaseId}/${table?.id}`,
query: route.value.query, query: route.value.query,
}) })
await getMeta(table.id as string)
} }
const createTable = async () => { const createTable = async () => {

557
packages/nc-gui/composables/useViewColumns.ts

@ -1,296 +1,363 @@
import { ViewTypes, isSystemColumn } from 'nocodb-sdk' import { ViewTypes, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, MapType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnType, GridColumnReqType, GridColumnType, MapType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { IsPublicInj, computed, inject, ref, storeToRefs, useBase, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports' import { computed, ref, storeToRefs, useBase, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
import type { Field } from '#imports' import type { Field } from '#imports'
export function useViewColumns( const [useProvideViewColumns, useViewColumns] = useInjectionState(
view: Ref<ViewType | undefined>, (
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>, view: Ref<ViewType | undefined>,
reloadData?: () => void, meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
) { reloadData?: () => void,
const isPublic = inject(IsPublicInj, ref(false)) isPublic = false,
) => {
const fields = ref<Field[]>()
const fields = ref<Field[]>() const filterQuery = ref('')
const filterQuery = ref('') const { $api, $e } = useNuxtApp()
const { $api, $e } = useNuxtApp() const { isUIAllowed } = useRoles()
const { isUIAllowed } = useRoles() const { isSharedBase } = storeToRefs(useBase())
const { isSharedBase } = storeToRefs(useBase()) const isViewColumnsLoading = ref(false)
const isViewColumnsLoading = ref(false) const { addUndo, defineViewScope } = useUndoRedo()
const { addUndo, defineViewScope } = useUndoRedo() const isLocalMode = computed(
() => isPublic || !isUIAllowed('viewFieldEdit') || !isUIAllowed('viewFieldEdit') || isSharedBase.value,
)
const isLocalMode = computed( const localChanges = ref<Field[]>([])
() => isPublic.value || !isUIAllowed('viewFieldEdit') || !isUIAllowed('viewFieldEdit') || isSharedBase.value,
)
const localChanges = ref<Field[]>([]) const isColumnViewEssential = (column: ColumnType) => {
// TODO: consider at some point ti delegate this via a cleaner design pattern to view specific check logic
// which could be inside of a view specific helper class (and generalized via an interface)
// (on the other hand, the logic complexity is still very low atm - might be overkill)
return view.value?.type === ViewTypes.MAP && (view.value?.view as MapType)?.fk_geo_data_col_id === column.id
}
const isColumnViewEssential = (column: ColumnType) => { const metaColumnById = computed<Record<string, ColumnType>>(() => {
// TODO: consider at some point ti delegate this via a cleaner design pattern to view specific check logic if (!meta.value?.columns) return {}
// which could be inside of a view specific helper class (and generalized via an interface)
// (on the other hand, the logic complexity is still very low atm - might be overkill)
return view.value?.type === ViewTypes.MAP && (view.value?.view as MapType)?.fk_geo_data_col_id === column.id
}
const metaColumnById = computed<Record<string, ColumnType>>(() => { return (meta.value.columns as ColumnType[]).reduce(
if (!meta.value?.columns) return {} (acc, curr) => ({
...acc,
[curr.id!]: curr,
}),
{},
) as Record<string, ColumnType>
})
return (meta.value.columns as ColumnType[]).reduce( const gridViewCols = ref<Record<string, GridColumnType>>({})
(acc, curr) => ({
...acc,
[curr.id!]: curr,
}),
{},
) as Record<string, ColumnType>
})
const loadViewColumns = async () => { const loadViewColumns = async () => {
if (!meta || !view) return if (!meta || !view) return
let order = 1 let order = 1
if (view.value?.id) { if (view.value?.id) {
const data = (isPublic.value ? meta.value?.columns : (await $api.dbViewColumn.list(view.value.id)).list) as any[] const data = (isPublic ? meta.value?.columns : (await $api.dbViewColumn.list(view.value.id)).list) as any[]
const fieldById = data.reduce<Record<string, any>>((acc, curr) => { const fieldById = data.reduce<Record<string, any>>((acc, curr) => {
curr.show = !!curr.show curr.show = !!curr.show
return {
...acc,
[curr.fk_column_id]: curr,
}
}, {})
fields.value = meta.value?.columns
?.map((column: ColumnType) => {
const currentColumnField = fieldById[column.id!] || {}
return { return {
title: column.title, ...acc,
fk_column_id: column.id, [curr.fk_column_id]: curr,
...currentColumnField,
show: currentColumnField.show || isColumnViewEssential(currentColumnField),
order: currentColumnField.order || order++,
system: isSystemColumn(metaColumnById?.value?.[currentColumnField.fk_column_id!]),
isViewEssentialField: isColumnViewEssential(column),
} }
}) }, {})
.sort((a: Field, b: Field) => a.order - b.order)
fields.value = meta.value?.columns
if (isLocalMode.value && fields.value) { ?.map((column: ColumnType) => {
for (const field of localChanges.value) { const currentColumnField = fieldById[column.id!] || {}
const fieldIndex = fields.value.findIndex((f) => f.fk_column_id === field.fk_column_id)
if (fieldIndex !== undefined && fieldIndex > -1) { return {
fields.value[fieldIndex] = field title: column.title,
fields.value = fields.value.sort((a: Field, b: Field) => a.order - b.order) fk_column_id: column.id,
...currentColumnField,
show: currentColumnField.show || isColumnViewEssential(currentColumnField),
order: currentColumnField.order || order++,
system: isSystemColumn(metaColumnById?.value?.[currentColumnField.fk_column_id!]),
isViewEssentialField: isColumnViewEssential(column),
}
})
.sort((a: Field, b: Field) => a.order - b.order)
if (isLocalMode.value && fields.value) {
for (const field of localChanges.value) {
const fieldIndex = fields.value.findIndex((f) => f.fk_column_id === field.fk_column_id)
if (fieldIndex !== undefined && fieldIndex > -1) {
fields.value[fieldIndex] = field
fields.value = fields.value.sort((a: Field, b: Field) => a.order - b.order)
}
} }
} }
const colsData: GridColumnType[] = (isPublic.value ? view.value?.columns : fields.value) ?? []
gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
(o, col) => ({
...o,
[col.fk_column_id as string]: col,
}),
{},
)
} }
} }
}
const showAll = async (ignoreIds?: any) => {
if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({
...field,
show: true,
}))
reloadData?.()
return
}
if (view?.value?.id) { const showAll = async (ignoreIds?: any) => {
if (ignoreIds) { if (isLocalMode.value) {
await $api.dbView.showAllColumn(view.value.id, { fields.value = fields.value?.map((field: Field) => ({
ignoreIds, ...field,
}) show: true,
} else { }))
await $api.dbView.showAllColumn(view.value.id) reloadData?.()
return
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.showAllColumn(view.value.id, {
ignoreIds,
})
} else {
await $api.dbView.showAllColumn(view.value.id)
}
} }
await loadViewColumns()
reloadData?.()
$e('a:fields:show-all')
} }
const hideAll = async (ignoreIds?: any) => {
if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({
...field,
show: !!field.isViewEssentialField,
}))
reloadData?.()
return
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.hideAllColumn(view.value.id, {
ignoreIds,
})
} else {
await $api.dbView.hideAllColumn(view.value.id)
}
}
await loadViewColumns() await loadViewColumns()
reloadData?.()
$e('a:fields:show-all')
}
const hideAll = async (ignoreIds?: any) => {
if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({
...field,
show: !!field.isViewEssentialField,
}))
reloadData?.() reloadData?.()
return $e('a:fields:show-all')
} }
if (view?.value?.id) {
if (ignoreIds) { const saveOrUpdate = async (field: any, index: number) => {
await $api.dbView.hideAllColumn(view.value.id, { if (isLocalMode.value && fields.value) {
ignoreIds, fields.value[index] = field
meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => {
if (column.id === field.fk_column_id) {
return {
...column,
...field,
id: field.fk_column_id,
}
}
return column
}) })
} else {
await $api.dbView.hideAllColumn(view.value.id) localChanges.value.push(field)
} }
}
await loadViewColumns() if (isUIAllowed('viewFieldEdit')) {
reloadData?.() if (field.id && view?.value?.id) {
$e('a:fields:show-all') await $api.dbViewColumn.update(view.value.id, field.id, field)
} } else if (view.value?.id) {
const insertedField = (await $api.dbViewColumn.create(view.value.id, field)) as any
const saveOrUpdate = async (field: any, index: number) => { /** update the field in fields if defined */
if (isLocalMode.value && fields.value) { if (fields.value) fields.value[index] = insertedField
fields.value[index] = field
meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => { return insertedField
if (column.id === field.fk_column_id) {
return {
...column,
...field,
id: field.fk_column_id,
}
} }
return column }
})
localChanges.value.push(field) await loadViewColumns()
reloadData?.()
} }
if (isUIAllowed('viewFieldEdit')) { const showSystemFields = computed({
if (field.id && view?.value?.id) { get() {
await $api.dbViewColumn.update(view.value.id, field.id, field) return (view.value?.show_system_fields as boolean) || false
} else if (view.value?.id) { },
const insertedField = (await $api.dbViewColumn.create(view.value.id, field)) as any set(v: boolean) {
if (view?.value?.id) {
if (!isLocalMode.value) {
$api.dbView
.update(view.value.id, {
show_system_fields: v,
})
.finally(() => {
loadViewColumns()
reloadData?.()
})
}
view.value.show_system_fields = v
}
$e('a:fields:system-fields')
},
})
/** update the field in fields if defined */ const filteredFieldList = computed(() => {
if (fields.value) fields.value[index] = insertedField return (
fields.value?.filter((field: Field) => {
if (metaColumnById?.value?.[field.fk_column_id!]?.pv) return true
return insertedField // hide system columns if not enabled
} if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) {
} return false
}
await loadViewColumns() if (filterQuery.value === '') {
reloadData?.() return true
} } else {
return field.title.toLowerCase().includes(filterQuery.value.toLowerCase())
}
}) || []
)
})
const showSystemFields = computed({ const sortedAndFilteredFields = computed<ColumnType[]>(() => {
get() { return (fields?.value
return (view.value?.show_system_fields as boolean) || false ?.filter((field: Field) => {
}, // hide system columns if not enabled
set(v: boolean) { if (
if (view?.value?.id) { !showSystemFields.value &&
if (!isLocalMode.value) { metaColumnById.value &&
$api.dbView metaColumnById?.value?.[field.fk_column_id!] &&
.update(view.value.id, { isSystemColumn(metaColumnById.value?.[field.fk_column_id!]) &&
show_system_fields: v, !metaColumnById.value?.[field.fk_column_id!]?.pv
}) ) {
.finally(() => { return false
loadViewColumns() }
reloadData?.() return field.show && metaColumnById?.value?.[field.fk_column_id!]
}) })
} ?.sort((a: Field, b: Field) => a.order - b.order)
view.value.show_system_fields = v ?.map((field: Field) => metaColumnById?.value?.[field.fk_column_id!]) || []) as ColumnType[]
} })
$e('a:fields:system-fields')
},
})
const filteredFieldList = computed(() => {
return (
fields.value?.filter((field: Field) => {
if (metaColumnById?.value?.[field.fk_column_id!]?.pv) return true
// hide system columns if not enabled
if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) {
return false
}
if (filterQuery.value === '') { const toggleFieldVisibility = (checked: boolean, field: any) => {
return true const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id)
} else { if (!fieldIndex && fieldIndex !== 0) return
return field.title.toLowerCase().includes(filterQuery.value.toLowerCase()) addUndo({
} undo: {
}) || [] fn: (v: boolean) => {
) field.show = !v
}) saveOrUpdate(field, fieldIndex)
},
const sortedAndFilteredFields = computed<ColumnType[]>(() => { args: [checked],
return (fields?.value
?.filter((field: Field) => {
// hide system columns if not enabled
if (
!showSystemFields.value &&
metaColumnById.value &&
metaColumnById?.value?.[field.fk_column_id!] &&
isSystemColumn(metaColumnById.value?.[field.fk_column_id!]) &&
!metaColumnById.value?.[field.fk_column_id!]?.pv
) {
return false
}
return field.show && metaColumnById?.value?.[field.fk_column_id!]
})
?.sort((a: Field, b: Field) => a.order - b.order)
?.map((field: Field) => metaColumnById?.value?.[field.fk_column_id!]) || []) as ColumnType[]
})
const toggleFieldVisibility = (checked: boolean, field: any) => {
const fieldIndex = fields.value?.findIndex((f) => f.fk_column_id === field.fk_column_id)
if (!fieldIndex && fieldIndex !== 0) return
addUndo({
undo: {
fn: (v: boolean) => {
field.show = !v
saveOrUpdate(field, fieldIndex)
}, },
args: [checked], redo: {
}, fn: (v: boolean) => {
redo: { field.show = v
fn: (v: boolean) => { saveOrUpdate(field, fieldIndex)
field.show = v },
saveOrUpdate(field, fieldIndex) args: [checked],
}, },
args: [checked], scope: defineViewScope({ view: view.value }),
}, })
scope: defineViewScope({ view: view.value }), saveOrUpdate(field, fieldIndex)
}) }
saveOrUpdate(field, fieldIndex)
} // reload view columns when active view changes
// or when columns count changes(delete/add)
// reload view columns when active view changes watch(
// or when columns count changes(delete/add) [() => view?.value?.id, () => meta.value?.columns?.length],
watch( async ([newViewId]) => {
[() => view?.value?.id, () => meta.value?.columns?.length], // reload only if view belongs to current table
async ([newViewId]) => { if (newViewId && view.value?.fk_model_id === meta.value?.id) {
// reload only if view belongs to current table isViewColumnsLoading.value = true
if (newViewId && view.value?.fk_model_id === meta.value?.id) { try {
isViewColumnsLoading.value = true await loadViewColumns()
try { } catch (e) {
await loadViewColumns() console.error(e)
} catch (e) { }
console.error(e) isViewColumnsLoading.value = false
} }
isViewColumnsLoading.value = false },
{ immediate: true },
)
const resizingColOldWith = ref('200px')
const updateGridViewColumn = async (id: string, props: Partial<GridColumnReqType>, undo = false) => {
if (!undo) {
const oldProps = Object.keys(props).reduce<Partial<GridColumnReqType>>((o: any, k) => {
if (gridViewCols.value[id][k as keyof GridColumnType]) {
if (k === 'width') o[k] = `${resizingColOldWith.value}px`
else o[k] = gridViewCols.value[id][k as keyof GridColumnType]
}
return o
}, {})
addUndo({
redo: {
fn: (w: Partial<GridColumnReqType>) => updateGridViewColumn(id, w, true),
args: [props],
},
undo: {
fn: (w: Partial<GridColumnReqType>) => updateGridViewColumn(id, w, true),
args: [oldProps],
},
scope: defineViewScope({ view: view.value }),
})
}
// sync with server if allowed
if (!isPublic.value && isUIAllowed('viewFieldEdit') && gridViewCols.value[id]?.id) {
await $api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, {
...props,
})
}
if (gridViewCols.value?.[id]) {
Object.assign(gridViewCols.value[id], {
...gridViewCols.value[id],
...props,
})
} else {
// fallback to reload
await loadViewColumns()
} }
}, }
{ immediate: true },
) return {
fields,
return { loadViewColumns,
fields, filteredFieldList,
loadViewColumns, filterQuery,
filteredFieldList, showAll,
filterQuery, hideAll,
showAll, saveOrUpdate,
hideAll, sortedAndFilteredFields,
saveOrUpdate, showSystemFields,
sortedAndFilteredFields, metaColumnById,
showSystemFields, toggleFieldVisibility,
metaColumnById, isViewColumnsLoading,
toggleFieldVisibility, updateGridViewColumn,
isViewColumnsLoading, gridViewCols,
} resizingColOldWith,
}
},
'useViewColumnsOrThrow',
)
export { useProvideViewColumns }
export function useViewColumnsOrThrow() {
const viewColumns = useViewColumns()
if (viewColumns == null) throw new Error('Please call `useProvideViewColumns` on the appropriate parent component')
return viewColumns
} }

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

@ -81,7 +81,7 @@ export function useViewFilters(
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta) const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const options = computed<SelectProps['options']>(() => const options = computed<SelectProps['options']>(() =>
meta.value?.columns?.filter((c: ColumnType) => { meta.value?.columns?.filter((c: ColumnType) => {

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

@ -1,6 +1,6 @@
import { type ColumnType, type SelectOptionsType, UITypes, type ViewType } from 'nocodb-sdk' import { type ColumnType, type SelectOptionsType, UITypes, type ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { GROUP_BY_VARS, ref, storeToRefs, useApi, useBase } from '#imports' import { GROUP_BY_VARS, ref, storeToRefs, useApi, useBase, useViewColumnsOrThrow } from '#imports'
import type { Group, GroupNestedIn, Row } from '#imports' import type { Group, GroupNestedIn, Row } from '#imports'
export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: ComputedRef<string | undefined>) => { export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: ComputedRef<string | undefined>) => {
@ -12,7 +12,7 @@ export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: Computed
const meta = inject(MetaInj) const meta = inject(MetaInj)
const { gridViewCols } = useGridViewColumnOrThrow() const { gridViewCols } = useViewColumnsOrThrow()
const groupBy = computed<{ column: ColumnType; sort: string; order?: number }[]>(() => { const groupBy = computed<{ column: ColumnType; sort: string; order?: number }[]>(() => {
const tempGroupBy: { column: ColumnType; sort: string; order?: number }[] = [] const tempGroupBy: { column: ColumnType; sort: string; order?: number }[] = []

18
packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts

@ -85,7 +85,7 @@ export default class CSVTemplateAdapter {
detectInitialUidt(v: string) { detectInitialUidt(v: string) {
if (!isNaN(Number(v)) && !isNaN(parseFloat(v))) return UITypes.Number if (!isNaN(Number(v)) && !isNaN(parseFloat(v))) return UITypes.Number
if (validateDateWithUnknownFormat(v)) return UITypes.DateTime if (validateDateWithUnknownFormat(v)) return UITypes.DateTime
if (['true', 'True', 'false', 'False', '1', '0', 'T', 'F', 'Y', 'N'].includes(v)) return UITypes.Checkbox if (isCheckboxType(v)) return UITypes.Checkbox
return UITypes.SingleLineText return UITypes.SingleLineText
} }
@ -101,18 +101,14 @@ export default class CSVTemplateAdapter {
} else if (colProps.uidt === UITypes.SingleLineText) { } else if (colProps.uidt === UITypes.SingleLineText) {
if (isEmailType(colData)) { if (isEmailType(colData)) {
colProps.uidt = UITypes.Email colProps.uidt = UITypes.Email
} } else if (isUrlType(colData)) {
if (isUrlType(colData)) {
colProps.uidt = UITypes.URL colProps.uidt = UITypes.URL
} else if (isCheckboxType(colData)) {
colProps.uidt = UITypes.Checkbox
} else { } else {
const checkboxType = isCheckboxType(colData) if (data[columnIdx] && columnIdx < this.config.maxRowsToParse) {
if (checkboxType.length === 1) { this.columnValues[columnIdx].push(data[columnIdx])
colProps.uidt = UITypes.Checkbox colProps.uidt = UITypes.SingleSelect
} else {
if (data[columnIdx] && columnIdx < this.config.maxRowsToParse) {
this.columnValues[columnIdx].push(data[columnIdx])
colProps.uidt = UITypes.SingleSelect
}
} }
} }
} else if (colProps.uidt === UITypes.Number) { } else if (colProps.uidt === UITypes.Number) {

11
packages/nc-gui/helpers/parsers/ExcelTemplateAdapter.ts

@ -144,13 +144,9 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
// check for long text // check for long text
if (isMultiLineTextType(rows, col)) { if (isMultiLineTextType(rows, col)) {
column.uidt = UITypes.LongText column.uidt = UITypes.LongText
} } else if (isEmailType(rows, col)) {
if (isEmailType(rows, col)) {
column.uidt = UITypes.Email column.uidt = UITypes.Email
} } else if (isUrlType(rows, col)) {
if (isUrlType(rows, col)) {
column.uidt = UITypes.URL column.uidt = UITypes.URL
} else { } else {
const vals = rows const vals = rows
@ -158,8 +154,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
.map((r: any) => r[col]) .map((r: any) => r[col])
.filter((v: any) => v !== null && v !== undefined && v.toString().trim() !== '') .filter((v: any) => v !== null && v !== undefined && v.toString().trim() !== '')
const checkboxType = isCheckboxType(vals, col) if (isCheckboxType(vals, col)) {
if (checkboxType.length === 1) {
column.uidt = UITypes.Checkbox column.uidt = UITypes.Checkbox
} else { } else {
// Single Select / Multi Select // Single Select / Multi Select

11
packages/nc-gui/helpers/parsers/parserHelpers.ts

@ -29,17 +29,15 @@ export const isCheckboxType: any = (values: [], col?: number) => {
let options = booleanOptions let options = booleanOptions
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
const val = getColVal(values[i], col) const val = getColVal(values[i], col)
if (val === null || val === undefined || val.toString().trim() === '') { if (val === null || val === undefined || val.toString().trim() === '') {
continue continue
} }
options = options.filter((v) => val in v) options = options.filter((v) => val in v)
if (!options.length) { if (!options.length) {
return false return false
} }
} }
return options return true
} }
export const getCheckboxValue = (value: any) => { export const getCheckboxValue = (value: any) => {
@ -142,7 +140,9 @@ export const isEmailType = (colData: [], col?: number) =>
export const isUrlType = (colData: [], col?: number) => export const isUrlType = (colData: [], col?: number) =>
colData.some((r: any) => { colData.some((r: any) => {
const v = getColVal(r, col) const v = getColVal(r, col)
return v && isURL(v) // convert to string since isURL only accepts string
// and cell data value can be number or any other types
return v && isURL(v.toString())
}) })
export const getColumnUIDTAndMetas = (colData: [], defaultType: string) => { export const getColumnUIDTAndMetas = (colData: [], defaultType: string) => {
@ -159,8 +159,7 @@ export const getColumnUIDTAndMetas = (colData: [], defaultType: string) => {
if (isUrlType(colData)) { if (isUrlType(colData)) {
colProps.uidt = UITypes.URL colProps.uidt = UITypes.URL
} else { } else {
const checkboxType = isCheckboxType(colData) if (isCheckboxType(colData)) {
if (checkboxType.length === 1) {
colProps.uidt = UITypes.Checkbox colProps.uidt = UITypes.Checkbox
} else { } else {
Object.assign(colProps, extractMultiOrSingleSelectProps(colData)) Object.assign(colProps, extractMultiOrSingleSelectProps(colData))

1
packages/nc-gui/package.json

@ -48,6 +48,7 @@
"@vueuse/integrations": "^10.2.1", "@vueuse/integrations": "^10.2.1",
"ant-design-vue": "^3.2.11", "ant-design-vue": "^3.2.11",
"chart.js": "^4.3.0", "chart.js": "^4.3.0",
"crossoriginworker": "^1.1.0",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",

2
packages/nc-gui/pages/index/[typeOrId]/[baseId]/index/index/[viewId]/[[viewTitle]].vue

@ -20,7 +20,7 @@ watch(
until(tables) until(tables)
.toMatch((tables) => tables.length > 0) .toMatch((tables) => tables.length > 0)
.then(() => { .then(() => {
getMeta(viewId as string, true) getMeta(viewId as string)
}) })
}, },
{ immediate: true }, { immediate: true },

3
packages/nc-gui/store/views.ts

@ -170,15 +170,12 @@ export const useViewsStore = defineStore('viewsStore', () => {
return return
} }
isViewsLoading.value = true
isViewDataLoading.value = true isViewDataLoading.value = true
try { try {
await loadViews() await loadViews()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally {
isViewsLoading.value = false
} }
}, },
{ immediate: true }, { immediate: true },

17
packages/nc-gui/utils/workerUtils.ts

@ -1,18 +1,11 @@
// Returns a blob:// URL which points import getCrossOriginWorkerURL from 'crossoriginworker'
// to a javascript file which will call
// importScripts with the given URL
export function getWorkerURL(url: string) {
const content = `importScripts( "${url}" );`
return URL.createObjectURL(new Blob([content], { type: 'text/javascript' }))
}
export function initWorker(url: string) { export async function initWorker(url: string) {
let worker: Worker | null = null let worker: Worker | null = null
try { try {
if (/^https?:\/\/'/.test(url)) { if (/^https?:\/\//.test(url)) {
const worker_url = getWorkerURL(url) const workerURL = await getCrossOriginWorkerURL(url)
worker = new Worker(worker_url) worker = new Worker(workerURL)
URL.revokeObjectURL(worker_url)
} else { } else {
worker = new Worker(url, { worker = new Worker(url, {
type: 'module', type: 'module',

9
packages/nc-gui/windi.config.ts

@ -69,6 +69,15 @@ export default defineConfig({
min: '1780px', min: '1780px',
}, },
}, },
fontWeight: {
thin: 150,
extraLight: 250,
light: 350,
normal: 450,
medium: 550,
bold: 650,
black: 750,
},
textColor: { textColor: {
primary: 'rgba(var(--color-primary), var(--tw-text-opacity))', primary: 'rgba(var(--color-primary), var(--tw-text-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-text-opacity))', accent: 'rgba(var(--color-accent), var(--tw-text-opacity))',

2
packages/noco-docs/.gitignore vendored

@ -3,7 +3,7 @@
# Production # Production
/build /build
# /dist /dist
# Generated files # Generated files
.docusaurus .docusaurus

0
packages/noco-docs/dist/.nojekyll vendored

18
packages/noco-docs/dist/0.109.7/FAQs/index.html vendored

File diff suppressed because one or more lines are too long

17
packages/noco-docs/dist/0.109.7/developer-resources/accessing-apis/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/developer-resources/rest-apis/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/developer-resources/sdk/index.html vendored

File diff suppressed because one or more lines are too long

17
packages/noco-docs/dist/0.109.7/developer-resources/upload-via-api/index.html vendored

File diff suppressed because one or more lines are too long

19
packages/noco-docs/dist/0.109.7/developer-resources/webhooks/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/engineering/architecture/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/engineering/builds-and-releases/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/engineering/development-setup/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/engineering/playwright/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/engineering/repository-structure/index.html vendored

File diff suppressed because one or more lines are too long

18
packages/noco-docs/dist/0.109.7/engineering/translation/index.html vendored

File diff suppressed because one or more lines are too long

19
packages/noco-docs/dist/0.109.7/engineering/unit-testing/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/getting-started/demos/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/getting-started/environment-variables/index.html vendored

File diff suppressed because one or more lines are too long

20
packages/noco-docs/dist/0.109.7/getting-started/installation/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/getting-started/upgrading/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/index.html vendored

File diff suppressed because one or more lines are too long

18
packages/noco-docs/dist/0.109.7/setup-and-usages/account-settings/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/audit/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/code-snippets/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/column-operations/index.html vendored

File diff suppressed because one or more lines are too long

18
packages/noco-docs/dist/0.109.7/setup-and-usages/column-types/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/dashboard/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/display-value/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/expanded-form/index.html vendored

File diff suppressed because one or more lines are too long

17
packages/noco-docs/dist/0.109.7/setup-and-usages/formulas/index.html vendored

File diff suppressed because one or more lines are too long

18
packages/noco-docs/dist/0.109.7/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/keyboard-maneuver/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/languages/index.html vendored

File diff suppressed because one or more lines are too long

18
packages/noco-docs/dist/0.109.7/setup-and-usages/link-to-another-record/index.html vendored

File diff suppressed because one or more lines are too long

27
packages/noco-docs/dist/0.109.7/setup-and-usages/links/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/lookup/index.html vendored

File diff suppressed because one or more lines are too long

17
packages/noco-docs/dist/0.109.7/setup-and-usages/meta-management/index.html vendored

File diff suppressed because one or more lines are too long

17
packages/noco-docs/dist/0.109.7/setup-and-usages/primary-key/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/project-settings/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/rollup/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/share-base/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/share-view/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/sync-schema/index.html vendored

File diff suppressed because one or more lines are too long

20
packages/noco-docs/dist/0.109.7/setup-and-usages/table-operations/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/team-and-auth/index.html vendored

File diff suppressed because one or more lines are too long

21
packages/noco-docs/dist/0.109.7/setup-and-usages/usage-information/index.html vendored

File diff suppressed because one or more lines are too long

16
packages/noco-docs/dist/0.109.7/setup-and-usages/views/index.html vendored

File diff suppressed because one or more lines are too long

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

Loading…
Cancel
Save