Browse Source

Merge pull request #6753 from nocodb/develop

pull/6754/head 0.202.5
github-actions[bot] 9 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:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
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_folder: 'data-apis-v2'
user_email: 'oof1lab@gmail.com'

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

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

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

@ -6,6 +6,7 @@ import {
IsFormInj,
JsonExpandInj,
ReadonlyInj,
RowHeightInj,
computed,
inject,
ref,
@ -46,6 +47,8 @@ const _isExpanded = inject(JsonExpandInj, ref(false))
const isExpanded = ref(false)
const rowHeight = inject(RowHeightInj, ref(undefined))
const localValue = computed<string | Record<string, any> | undefined>({
get: () => localValueState.value,
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, () => {
_isExpanded.value = isExpanded.value
})
@ -148,7 +158,7 @@ watch(isExpanded, () => {
<template>
<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 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">
<CilFullscreenExit v-if="isExpanded" class="h-2.5" />
@ -167,6 +177,7 @@ watch(isExpanded, () => {
</div>
<LazyMonacoEditor
ref="inputWrapperRef"
:model-value="localValue || ''"
class="min-w-full w-80"
: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>{{ vModel }}</span>
<LazyCellClampedText v-else :value="vModel" :lines="rowHeight" />
</component>
</template>

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

@ -43,6 +43,8 @@ const { modelValue, disableOptionCreation } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { isMobileMode } = useGlobal()
const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)!
@ -383,7 +385,7 @@ const selectedOpts = computed(() => {
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:bordered="false"
clear-icon
show-search
:show-search="!isMobileMode"
:show-arrow="editAllowed && !(readOnly || isLockedMode)"
:open="isOpen && editAllowed"
:disabled="readOnly || !editAllowed || isLockedMode"
@ -392,6 +394,9 @@ const selectedOpts = computed(() => {
@search="search"
@keydown.stop
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-700 nc-select-expand-btn" />
</template>
<a-select-option
v-for="op of options"
: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 { isMobileMode } = useGlobal()
const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)!
@ -202,6 +204,8 @@ async function addIfMissingAndSave() {
}
const search = () => {
if (isMobileMode.value) return
searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value
}
@ -285,7 +289,7 @@ const selectedOpt = computed(() => {
v-else
ref="aselect"
v-model:value="vModel"
class="w-full overflow-hidden"
class="w-full overflow-hidden xs:min-h-12"
:class="{ 'caret-transparent': !hasEditRoles }"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:allow-clear="!column.rqd && editAllowed"
@ -294,7 +298,7 @@ const selectedOpt = computed(() => {
:disabled="readOnly || !editAllowed || isLockedMode"
:show-arrow="hasEditRoles && !(readOnly || isLockedMode) && active && vModel === null"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && active ? 'active' : ''}`"
:show-search="isOpen && active"
:show-search="!isMobileMode && isOpen && active"
@select="onSelect"
@keydown="onKeydown($event)"
@search="search"

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

@ -55,7 +55,7 @@ useEventListener(container, 'click', (e) => {
<template>
<GeneralOverlay v-model="selectedImage" :z-index="1001" class="bg-gray-500 bg-opacity-50">
<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">
<component
: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 open = () => {
if (isMobileMode.value) return (isExpandedForm.value = true)
const open = (e: Event) => {
e.stopPropagation()
_open()
}
const openAttachment = (item: any) => {
if (isMobileMode.value) return
if (isMobileMode.value && !isExpandedForm.value) {
isExpandedForm.value = true
return
}
_openAttachment(item)
}
@ -169,6 +173,14 @@ const onExpand = () => {
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>
<template>
@ -178,7 +190,7 @@ const onExpand = () => {
:style="{
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 }"
>
<LazyCellAttachmentCarousel />
@ -198,26 +210,29 @@ const onExpand = () => {
<div
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)"
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 }" />
<NcTooltip placement="bottom">
<NcTooltip placement="bottom" class="xs:w-full">
<template #title
><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
class="transform dark:(!text-white) group-hover:(!text-accent scale-120) text-gray-500 text-[0.75rem]"
/>
<div
v-if="!visibleItems.length"
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') }}
</div>
@ -245,12 +260,7 @@ const onExpand = () => {
<div
class="nc-attachment flex items-center flex-col flex-wrap justify-center"
:class="{ 'ml-2': active }"
@click="
() => {
if (isGallery || isMobileMode || (isKanban && !isExpandedForm)) return
selectedImage = item
}
"
@click="() => onImageClick(item)"
>
<LazyCellAttachmentImage
:alt="item.title || `#${i}`"
@ -281,7 +291,7 @@ const onExpand = () => {
<div
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 }" />

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

@ -47,7 +47,7 @@ onUnmounted(() => {
</div>
<div
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="{
'border-t-1': !isSharedBase,
'border-transparent': !isTreeViewOnScrollTop,

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

@ -182,7 +182,7 @@ const isTableOpened = computed(() => {
:class="{ '!rotate-180': isExpanded }"
/>
</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 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
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) {
listeningForUpdates.value = false
@ -226,12 +226,12 @@ async function loadSyncSrc() {
async function sync() {
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,
method: 'POST',
headers: { 'xc-auth': $state.token.value as string },
})
listenForUpdates()
listenForUpdates(jobData.id)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -251,6 +251,9 @@ async function abort() {
headers: { 'xc-auth': $state.token.value as string },
})
step.value = 1
progress.value = []
goBack.value = false
enableAbort.value = false
} catch (e: any) {
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) {
if (!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">
{{ $t('labels.goToDashboard') }}
</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')
}}</a-button>
<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) {
watch(
dialogShow,
(val) => {
async (val) => {
if (val) {
importWorker = initWorker(importWorkerUrl)
} else importWorker?.terminate()
importWorker = await initWorker(importWorkerUrl)
} else {
importWorker?.terminate()
}
},
{ immediate: true },
)

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

@ -6,7 +6,7 @@ import { iconMap } from '#imports'
<a
v-e="['c:navbar:join-cloud']"
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
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" />
</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'
}>()
const workspaceColor = computed(() =>
props.workspace ? props.workspace.meta?.color || stringToColor(props.workspace.id!) : undefined,
)
const workspaceColor = computed(() => {
const color = props.workspace ? props.workspace.meta?.color || stringToColor(props.workspace.id!) : undefined
return color || '#0A1433'
})
const size = computed(() => props.size || 'medium')
</script>

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

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

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

@ -55,6 +55,16 @@ const onChange = (value: string) => {
</template>
<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 {
height: fit-content;
.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))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
</script>

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

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

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

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

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

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

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

@ -10,13 +10,7 @@ const props = defineProps<{
const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const { fields, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const { fields, metaColumnById } = useViewColumnsOrThrow()
const vModel = useVModel(props, 'modelValue', emit)
@ -59,11 +53,12 @@ onMounted(() => {
...vModel.value.meta,
}
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) => {
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
}
})

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

@ -2,7 +2,7 @@
import { onMounted } from '@vue/runtime-core'
import type { ColumnType, LinkToAnotherRecordType, TableType } 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<{
value: any
@ -16,11 +16,10 @@ const meta = inject(MetaInj, ref())
const { t } = useI18n()
const { appInfo } = useGlobal()
const { setAdditionalValidations, validateInfos, onDataTypeChange, isEdit } = useColumnCreateStoreOrThrow()
const baseStore = useBase()
const { tables } = storeToRefs(baseStore)
const { metas } = useMetas()
@ -39,13 +38,7 @@ const refTables = computed(() => {
}
const _refTables = meta.value.columns
.filter(
(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'),
)
.filter((column) => isLinksOrLTAR(column) && !column.system && column.source_id === meta.value?.source_id)
.map((column) => ({
col: column.colOptions,
column,

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

@ -10,15 +10,9 @@ const props = defineProps<{
const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const { t } = useI18n()
const reloadDataHook = inject(ReloadViewDataHookInj)!
const { fields, metaColumnById } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
const { fields, metaColumnById } = useViewColumnsOrThrow()
const vModel = useVModel(props, 'modelValue', emit)
@ -40,7 +34,8 @@ const columnsAllowedAsQrValue = computed<SelectProps['options']>(() => {
onMounted(() => {
// 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({

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

@ -45,12 +45,7 @@ const visibilityOps = ref<fieldsVisibilityOps[]>([])
const fieldsListWrapperDomRef = ref<HTMLElement>()
const {
fields: viewFields,
toggleFieldVisibility,
loadViewColumns,
isViewColumnsLoading,
} = useViewColumns(view, meta as Ref<TableType | undefined>)
const { fields: viewFields, toggleFieldVisibility, loadViewColumns, isViewColumnsLoading } = useViewColumnsOrThrow()
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 { ref, timeAgo, useExpandedFormStoreOrThrow, useGlobal, useRoles, watch } from '#imports'
const props = defineProps<{
isLoading: boolean
}>()
const { loadCommentsAndLogs, commentsAndLogs, saveComment: _saveComment, comment, updateComment } = useExpandedFormStoreOrThrow()
const commentsWrapperEl = ref<HTMLDivElement>()
const { user, appInfo } = useGlobal()
const isExpandedFormLoading = computed(() => props.isLoading)
const tab = ref<'comments' | 'audits'>('comments')
const { isUIAllowed } = useRoles()
@ -69,7 +75,7 @@ onKeyStroke('Enter', (event) => {
})
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) {
editLog.value = log
@ -90,9 +96,22 @@ function scrollComments() {
if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight
}
const isSaving = ref(false)
const saveComment = async () => {
await _saveComment()
scrollComments()
if (isSaving.value) return
isSaving.value = true
try {
await _saveComment()
scrollComments()
} catch (e) {
console.error(e)
} finally {
isSaving.value = false
}
}
watch(commentsWrapperEl, () => {
@ -157,10 +176,13 @@ const onClickAudit = () => {
<div
class="h-[calc(100%-4rem)]"
: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 class="text-center text-3xl text-gray-700">
<GeneralIcon icon="commentHere" />
@ -229,15 +251,17 @@ const onClickAudit = () => {
v-e="['a:row-expand:comment:save']"
size="medium"
class="!w-8"
:disabled="!comment.length"
:loading="isSaving"
:disabled="!isSaving && !comment.length"
:icon-only="isSaving"
@click="saveComment"
>
<GeneralIcon icon="send" />
<GeneralIcon v-if="!isSaving" icon="send" />
</NcButton>
</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">
<div class="flex flex-col text-center justify-center h-full">
<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>
</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 class="flex flex-col p-4 gap-3">
<div class="flex justify-between">
<div class="flex items-center gap-2">
<GeneralUserIcon size="base" :email="log.user" />
<div class="flex flex-col">
<span class="truncate font-bold max-w-50">
{{ log.display_name ?? log.user.split('@')[0].slice(0, 2) ?? 'Shared source' }}
</span>
<div v-if="log.id !== editLog?.id" class="text-xs font-medium text-gray-500">
{{ timeAgo(log.created_at) }}
</div>
<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 justify-between">
<div class="flex items-center gap-2">
<GeneralUserIcon size="base" :email="log.user" />
<div class="flex flex-col">
<span class="truncate font-bold max-w-50">
{{ log.display_name ?? log.user.split('@')[0].slice(0, 2) ?? 'Shared source' }}
</span>
<div v-if="log.id !== editLog?.id" class="text-xs font-medium text-gray-500">
{{ timeAgo(log.created_at) }}
</div>
</div>
</div>
<div v-dompurify-html="log.details" class="text-sm font-medium"></div>
</div>
<div v-dompurify-html="log.details" class="text-sm font-medium"></div>
</div>
</div>
</div>
@ -276,6 +299,15 @@ const onClickAudit = () => {
.tab {
@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 {
@apply min-w-0 flex justify-center gap-2 font-semibold items-center;
word-break: 'keep-all';

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

@ -438,7 +438,7 @@ export default {
:class="{ active: isExpanded }"
>
<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">
<div class="flex gap-3 w-100">
<div class="flex gap-2">
@ -484,7 +484,7 @@ export default {
{{ isRecordLinkCopied ? $t('labels.copiedRecordURL') : $t('labels.copyRecordURL') }}
</div>
</NcButton>
<NcDropdown v-if="!isNew">
<NcDropdown v-if="!isNew" placement="bottomRight">
<NcButton type="secondary" class="nc-expand-form-more-actions w-10">
<GeneralIcon icon="threeDotVertical" class="text-md text-gray-700" />
</NcButton>
@ -546,7 +546,7 @@ export default {
<template v-else>
<div class="flex flex-row w-full">
<NcButton
v-if="props.showNextPrevIcons"
v-if="props.showNextPrevIcons && !isFirstRow"
v-e="['c:row-expand:prev']"
type="secondary"
class="nc-prev-arrow !w-10"
@ -554,11 +554,12 @@ export default {
>
<GeneralIcon icon="arrowLeft" class="text-lg text-gray-700" />
</NcButton>
<div v-else class="min-w-10.5"></div>
<div class="flex flex-grow justify-center items-center font-semibold text-lg">
<div>{{ meta.title }}</div>
</div>
<NcButton
v-if="props.showNextPrevIcons && !props.lastRow"
v-if="props.showNextPrevIcons && !islastRow"
v-e="['c:row-expand:next']"
type="secondary"
class="nc-next-arrow !w-10"
@ -566,6 +567,7 @@ export default {
>
<GeneralIcon icon="arrowRight" class="text-lg text-gray-700" />
</NcButton>
<div v-else class="min-w-10.5"></div>
</div>
</template>
</div>
@ -578,7 +580,7 @@ export default {
}"
>
<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
v-for="(col, i) of fields"
@ -590,14 +592,14 @@ export default {
: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="w-[12rem] xs:(w-full) mt-0.25 !h-[35px]">
<div class="w-48 xs:(w-full) mt-0.25 !h-[35px]">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(col)"
class="nc-expanded-cell-header h-full !text-gray-500"
class="nc-expanded-cell-header h-full"
: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>
<template v-if="isLoading">
@ -616,7 +618,7 @@ export default {
<SmartsheetDivDataCell
v-if="col.title"
: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" />
@ -633,29 +635,34 @@ export default {
</template>
</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>
<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` }}
{{ hiddenFields.length > 1 ? `fields` : `field` }}
<MdiChevronDown class="ml-1" :class="showHiddenFields ? 'transform rotate-180' : ''" />
</NcButton>
<div class="flex-grow h-px ml-1 bg-gray-100"></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
v-for="(col, i) of hiddenFields"
v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
:key="col.title"
class="mt-2 py-2"
class="sm:(mt-2) py-2 xs:w-full"
:class="`nc-expand-col-${col.title}`"
:data-testid="`nc-expand-col-${col.title}`"
>
<div class="flex flex-row items-start min-h-10">
<div class="w-[12rem] scale-110 !h-[35px] mt-2.5">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" class="!text-gray-600" />
<div class="sm:gap-x-6 flex sm:flex-row xs:(flex-col) items-start min-h-10">
<div class="sm:w-48 xs:w-full scale-110 !h-[35px]">
<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>
<template v-if="isLoading">
@ -674,7 +681,7 @@ export default {
<LazySmartsheetDivDataCell
v-if="col.title"
: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
v-if="isVirtualCol(col)"
@ -703,7 +710,7 @@ export default {
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)"
>
<NcDropdown v-if="!isNew && isMobileMode">
<NcDropdown v-if="!isNew && isMobileMode" placement="bottomRight">
<NcButton type="secondary" class="nc-expand-form-more-actions w-10">
<GeneralIcon icon="threeDotVertical" class="text-md text-gray-700" />
</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="{ active: commentsDrawer && isUIAllowed('commentList') }"
>
<SmartsheetExpandedFormComments />
<SmartsheetExpandedFormComments :loading="isLoading" />
</div>
</div>
</div>
@ -811,7 +818,7 @@ export default {
}
.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) {
@ -825,3 +832,13 @@ export default {
@apply !p-0;
}
</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,
ref,
useEventListener,
useGridViewColumnOrThrow,
useI18n,
useMultiSelect,
useNuxtApp,
@ -36,6 +35,7 @@ import {
useRoute,
useSmartsheetStoreOrThrow,
useUndoRedo,
useViewColumnsOrThrow,
useViewsStore,
watch,
} from '#imports'
@ -123,8 +123,6 @@ const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
const { isViewColumnsLoading } = useViewColumns(view, meta, () => reloadViewDataHook.trigger())
const { isMobileMode } = useGlobal()
const scrollParent = inject(ScrollParentInj, ref<undefined>())
@ -141,6 +139,8 @@ const { getMeta } = useMetas()
const { addUndo, clone, defineViewScope } = useUndoRedo()
const { isViewColumnsLoading, updateGridViewColumn, gridViewCols, resizingColOldWith } = useViewColumnsOrThrow()
const {
predictingNextColumn,
predictedNextColumn,
@ -894,8 +894,6 @@ const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue?
}
// #Grid Resize
const { updateGridViewColumn, gridViewCols, resizingColOldWith } = useGridViewColumnOrThrow()
const onresize = (colID: string | undefined, event: any) => {
if (!colID) return
updateGridViewColumn(colID, { width: event.detail })
@ -1211,7 +1209,7 @@ const loaderText = computed(() => {
<div class="table-overlay" :class="{ 'nc-grid-skelton-loader': showSkeleton }">
<table
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="{
mobile: isMobileMode,
desktop: !isMobileMode,
@ -1554,11 +1552,15 @@ const loaderText = computed(() => {
@mouseup.stop
@click="addEmptyRow()"
>
<td class="text-left pointer sticky left-0 !border-r-0">
<div class="px-2 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>
</td>
<div
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"
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>
</tr>
</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)) {
return iconMap.calendar
} else if (isTime(column, abstractType)) {
return iconMap.calendar
return iconMap.clock
} else if (isRating(column)) {
return iconMap.rating
} 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 { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow(activeView, meta)
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 { ColumnType, LinkToAnotherRecordType } 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<{
modelValue?: string
@ -24,7 +24,7 @@ const localValue = computed({
const activeView = inject(ActiveViewInj, ref())
const { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const options = computed<SelectProps['options']>(() =>
(

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

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

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

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

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

@ -12,7 +12,7 @@ import {
iconMap,
inject,
ref,
useViewColumns,
useViewColumnsOrThrow,
watch,
} from '#imports'
@ -22,13 +22,11 @@ const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const isLocked = inject(IsLockedInj, 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()

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">
{{ displayColumnLabel }}
</div>
<div
:class="{
'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 class="xs:(text-gray-600) group-hover:text-gray-700 sm:(text-gray-400)">
<component :is="iconMap.arrowDown" class="text-sm text-inherit" />
</div>
<a-select
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 { showSystemFields, metaColumnById } = useViewColumns(view, meta)
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const showCreateSort = ref(false)
@ -79,14 +79,6 @@ const getColumnUidtByID = (key?: string) => {
return columnByID.value[key]?.uidt || ''
}
watch(
() => view.value?.id,
(viewId) => {
if (viewId) loadSorts()
},
{ immediate: true },
)
const open = ref(false)
useMenuCloseOnEsc(open)
@ -105,6 +97,10 @@ watch(open, () => {
showCreateSort.value = false
}
})
onMounted(() => {
loadSorts()
})
</script>
<template>

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

@ -14,7 +14,7 @@ import {
useKanbanViewStoreOrThrow,
useMenuCloseOnEsc,
useUndoRedo,
useViewColumns,
useViewColumnsOrThrow,
watch,
} from '#imports'
@ -30,7 +30,7 @@ const reloadDataHook = inject(ReloadViewDataHookInj)!
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 } =
useKanbanViewStoreOrThrow()

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

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

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

@ -28,8 +28,9 @@ const useAttachment = () => {
} catch {}
}
// if no source can be fetched, it could be probably blocked by CORS
// return original url or built url anyway
return item.url || `${appInfo.value.ncSiteUrl}/${item.path}`
// return signed url / original url / built url anyway
// 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>) => {

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

@ -35,7 +35,9 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
tableExplorerColumns?: Ref<ColumnType[] | undefined>,
) => {
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 { $api } = useNuxtApp()
@ -60,8 +62,6 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
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 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}`,
})
comment.value = ''
reloadTrigger?.trigger()
await loadCommentsAndLogs()
comment.value = ''
} catch (e: any) {
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 base = computed(() => bases.value.get(param.baseId))
const { loadViews } = useViewsStore()
const openTable = async (table: TableType) => {
if (!table.base_id) return
@ -82,12 +80,10 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
baseIdOrBaseId = route.value.params.baseId as string
}
await loadViews({
tableId: table.id,
})
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) {
await navigateTo({
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}`,
query: route.value.query,
})
await getMeta(table.id as string)
}
const createTable = async () => {

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

@ -1,296 +1,363 @@
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 { 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'
export function useViewColumns(
view: Ref<ViewType | undefined>,
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
reloadData?: () => void,
) {
const isPublic = inject(IsPublicInj, ref(false))
const [useProvideViewColumns, useViewColumns] = useInjectionState(
(
view: Ref<ViewType | undefined>,
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
reloadData?: () => void,
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(
() => isPublic.value || !isUIAllowed('viewFieldEdit') || !isUIAllowed('viewFieldEdit') || isSharedBase.value,
)
const localChanges = ref<Field[]>([])
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) => {
// 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 metaColumnById = computed<Record<string, ColumnType>>(() => {
if (!meta.value?.columns) return {}
const metaColumnById = computed<Record<string, ColumnType>>(() => {
if (!meta.value?.columns) return {}
return (meta.value.columns as ColumnType[]).reduce(
(acc, curr) => ({
...acc,
[curr.id!]: curr,
}),
{},
) as Record<string, ColumnType>
})
return (meta.value.columns as ColumnType[]).reduce(
(acc, curr) => ({
...acc,
[curr.id!]: curr,
}),
{},
) as Record<string, ColumnType>
})
const gridViewCols = ref<Record<string, GridColumnType>>({})
const loadViewColumns = async () => {
if (!meta || !view) return
const loadViewColumns = async () => {
if (!meta || !view) return
let order = 1
let order = 1
if (view.value?.id) {
const data = (isPublic.value ? meta.value?.columns : (await $api.dbViewColumn.list(view.value.id)).list) as any[]
if (view.value?.id) {
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) => {
curr.show = !!curr.show
return {
...acc,
[curr.fk_column_id]: curr,
}
}, {})
fields.value = meta.value?.columns
?.map((column: ColumnType) => {
const currentColumnField = fieldById[column.id!] || {}
const fieldById = data.reduce<Record<string, any>>((acc, curr) => {
curr.show = !!curr.show
return {
title: column.title,
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),
...acc,
[curr.fk_column_id]: curr,
}
})
.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)
}, {})
fields.value = meta.value?.columns
?.map((column: ColumnType) => {
const currentColumnField = fieldById[column.id!] || {}
return {
title: column.title,
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) {
if (ignoreIds) {
await $api.dbView.showAllColumn(view.value.id, {
ignoreIds,
})
} else {
await $api.dbView.showAllColumn(view.value.id)
const showAll = async (ignoreIds?: any) => {
if (isLocalMode.value) {
fields.value = fields.value?.map((field: Field) => ({
...field,
show: true,
}))
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()
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,
}))
await loadViewColumns()
reloadData?.()
return
$e('a:fields:show-all')
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.hideAllColumn(view.value.id, {
ignoreIds,
const saveOrUpdate = async (field: any, index: number) => {
if (isLocalMode.value && fields.value) {
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()
reloadData?.()
$e('a:fields:show-all')
}
if (isUIAllowed('viewFieldEdit')) {
if (field.id && view?.value?.id) {
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) => {
if (isLocalMode.value && fields.value) {
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,
}
/** update the field in fields if defined */
if (fields.value) fields.value[index] = insertedField
return insertedField
}
return column
})
}
localChanges.value.push(field)
await loadViewColumns()
reloadData?.()
}
if (isUIAllowed('viewFieldEdit')) {
if (field.id && view?.value?.id) {
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 showSystemFields = computed({
get() {
return (view.value?.show_system_fields as boolean) || false
},
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 */
if (fields.value) fields.value[index] = insertedField
const filteredFieldList = computed(() => {
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()
reloadData?.()
}
if (filterQuery.value === '') {
return true
} else {
return field.title.toLowerCase().includes(filterQuery.value.toLowerCase())
}
}) || []
)
})
const showSystemFields = computed({
get() {
return (view.value?.show_system_fields as boolean) || false
},
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')
},
})
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
}
const sortedAndFilteredFields = computed<ColumnType[]>(() => {
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[]
})
if (filterQuery.value === '') {
return true
} else {
return field.title.toLowerCase().includes(filterQuery.value.toLowerCase())
}
}) || []
)
})
const sortedAndFilteredFields = computed<ColumnType[]>(() => {
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)
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],
},
args: [checked],
},
redo: {
fn: (v: boolean) => {
field.show = v
saveOrUpdate(field, fieldIndex)
redo: {
fn: (v: boolean) => {
field.show = v
saveOrUpdate(field, fieldIndex)
},
args: [checked],
},
args: [checked],
},
scope: defineViewScope({ view: view.value }),
})
saveOrUpdate(field, fieldIndex)
}
// reload view columns when active view changes
// or when columns count changes(delete/add)
watch(
[() => view?.value?.id, () => meta.value?.columns?.length],
async ([newViewId]) => {
// reload only if view belongs to current table
if (newViewId && view.value?.fk_model_id === meta.value?.id) {
isViewColumnsLoading.value = true
try {
await loadViewColumns()
} catch (e) {
console.error(e)
scope: defineViewScope({ view: view.value }),
})
saveOrUpdate(field, fieldIndex)
}
// reload view columns when active view changes
// or when columns count changes(delete/add)
watch(
[() => view?.value?.id, () => meta.value?.columns?.length],
async ([newViewId]) => {
// reload only if view belongs to current table
if (newViewId && view.value?.fk_model_id === meta.value?.id) {
isViewColumnsLoading.value = true
try {
await loadViewColumns()
} 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,
loadViewColumns,
filteredFieldList,
filterQuery,
showAll,
hideAll,
saveOrUpdate,
sortedAndFilteredFields,
showSystemFields,
metaColumnById,
toggleFieldVisibility,
isViewColumnsLoading,
}
}
return {
fields,
loadViewColumns,
filteredFieldList,
filterQuery,
showAll,
hideAll,
saveOrUpdate,
sortedAndFilteredFields,
showSystemFields,
metaColumnById,
toggleFieldVisibility,
isViewColumnsLoading,
updateGridViewColumn,
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 { showSystemFields, metaColumnById } = useViewColumns(activeView, meta)
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const options = computed<SelectProps['options']>(() =>
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 { 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'
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 { gridViewCols } = useGridViewColumnOrThrow()
const { gridViewCols } = useViewColumnsOrThrow()
const groupBy = computed<{ 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) {
if (!isNaN(Number(v)) && !isNaN(parseFloat(v))) return UITypes.Number
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
}
@ -101,18 +101,14 @@ export default class CSVTemplateAdapter {
} else if (colProps.uidt === UITypes.SingleLineText) {
if (isEmailType(colData)) {
colProps.uidt = UITypes.Email
}
if (isUrlType(colData)) {
} else if (isUrlType(colData)) {
colProps.uidt = UITypes.URL
} else if (isCheckboxType(colData)) {
colProps.uidt = UITypes.Checkbox
} else {
const checkboxType = isCheckboxType(colData)
if (checkboxType.length === 1) {
colProps.uidt = UITypes.Checkbox
} else {
if (data[columnIdx] && columnIdx < this.config.maxRowsToParse) {
this.columnValues[columnIdx].push(data[columnIdx])
colProps.uidt = UITypes.SingleSelect
}
if (data[columnIdx] && columnIdx < this.config.maxRowsToParse) {
this.columnValues[columnIdx].push(data[columnIdx])
colProps.uidt = UITypes.SingleSelect
}
}
} 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
if (isMultiLineTextType(rows, col)) {
column.uidt = UITypes.LongText
}
if (isEmailType(rows, col)) {
} else if (isEmailType(rows, col)) {
column.uidt = UITypes.Email
}
if (isUrlType(rows, col)) {
} else if (isUrlType(rows, col)) {
column.uidt = UITypes.URL
} else {
const vals = rows
@ -158,8 +154,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
.map((r: any) => r[col])
.filter((v: any) => v !== null && v !== undefined && v.toString().trim() !== '')
const checkboxType = isCheckboxType(vals, col)
if (checkboxType.length === 1) {
if (isCheckboxType(vals, col)) {
column.uidt = UITypes.Checkbox
} else {
// 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
for (let i = 0; i < values.length; i++) {
const val = getColVal(values[i], col)
if (val === null || val === undefined || val.toString().trim() === '') {
continue
}
options = options.filter((v) => val in v)
if (!options.length) {
return false
}
}
return options
return true
}
export const getCheckboxValue = (value: any) => {
@ -142,7 +140,9 @@ export const isEmailType = (colData: [], col?: number) =>
export const isUrlType = (colData: [], col?: number) =>
colData.some((r: any) => {
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) => {
@ -159,8 +159,7 @@ export const getColumnUIDTAndMetas = (colData: [], defaultType: string) => {
if (isUrlType(colData)) {
colProps.uidt = UITypes.URL
} else {
const checkboxType = isCheckboxType(colData)
if (checkboxType.length === 1) {
if (isCheckboxType(colData)) {
colProps.uidt = UITypes.Checkbox
} else {
Object.assign(colProps, extractMultiOrSingleSelectProps(colData))

1
packages/nc-gui/package.json

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

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

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

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

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

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

@ -1,18 +1,11 @@
// Returns a blob:// URL which points
// 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' }))
}
import getCrossOriginWorkerURL from 'crossoriginworker'
export function initWorker(url: string) {
export async function initWorker(url: string) {
let worker: Worker | null = null
try {
if (/^https?:\/\/'/.test(url)) {
const worker_url = getWorkerURL(url)
worker = new Worker(worker_url)
URL.revokeObjectURL(worker_url)
if (/^https?:\/\//.test(url)) {
const workerURL = await getCrossOriginWorkerURL(url)
worker = new Worker(workerURL)
} else {
worker = new Worker(url, {
type: 'module',

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

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

2
packages/noco-docs/.gitignore vendored

@ -3,7 +3,7 @@
# Production
/build
# /dist
/dist
# Generated files
.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