Browse Source

Merge branch 'develop' into fix/default-limit

pull/3622/head
Wing-Kam Wong 2 years ago
parent
commit
bbc04c2943
  1. 9
      .all-contributorsrc
  2. 1
      README.md
  3. 1
      packages/nc-gui/components.d.ts
  4. 10
      packages/nc-gui/components/cell/Checkbox.vue
  5. 45
      packages/nc-gui/components/cell/Url.vue
  6. 4
      packages/nc-gui/components/cell/attachment/Modal.vue
  7. 4
      packages/nc-gui/components/cell/attachment/index.vue
  8. 10
      packages/nc-gui/components/cell/attachment/utils.ts
  9. 8
      packages/nc-gui/components/dashboard/TreeView.vue
  10. 4
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  11. 2
      packages/nc-gui/components/dashboard/settings/Misc.vue
  12. 2
      packages/nc-gui/components/dashboard/settings/Modal.vue
  13. 48
      packages/nc-gui/components/dlg/AirtableImport.vue
  14. 2
      packages/nc-gui/components/dlg/ViewCreate.vue
  15. 2
      packages/nc-gui/components/general/HelpAndSupport.vue
  16. 8
      packages/nc-gui/components/general/MiniSidebar.vue
  17. 10
      packages/nc-gui/components/general/Social.vue
  18. 16
      packages/nc-gui/components/general/SocialCard.vue
  19. 12
      packages/nc-gui/components/shared-view/Grid.vue
  20. 9
      packages/nc-gui/components/smartsheet-column/EditOrAdd.vue
  21. 15
      packages/nc-gui/components/smartsheet-column/EditOrAddProvider.vue
  22. 12
      packages/nc-gui/components/smartsheet-column/FormulaOptions.vue
  23. 11
      packages/nc-gui/components/smartsheet-column/LinkedToAnotherRecordOptions.vue
  24. 4
      packages/nc-gui/components/smartsheet-column/LookupOptions.vue
  25. 2
      packages/nc-gui/components/smartsheet-column/PercentOptions.vue
  26. 2
      packages/nc-gui/components/smartsheet-column/RatingOptions.vue
  27. 7
      packages/nc-gui/components/smartsheet-column/RollupOptions.vue
  28. 11
      packages/nc-gui/components/smartsheet-header/CellIcon.vue
  29. 2
      packages/nc-gui/components/smartsheet-header/Menu.vue
  30. 4
      packages/nc-gui/components/smartsheet-header/VirtualCell.vue
  31. 7
      packages/nc-gui/components/smartsheet-header/VirtualCellIcon.vue
  32. 2
      packages/nc-gui/components/smartsheet-toolbar/AddRow.vue
  33. 4
      packages/nc-gui/components/smartsheet-toolbar/ColumnFilter.vue
  34. 6
      packages/nc-gui/components/smartsheet-toolbar/ColumnFilterMenu.vue
  35. 2
      packages/nc-gui/components/smartsheet-toolbar/Export.vue
  36. 17
      packages/nc-gui/components/smartsheet-toolbar/ExportSubActions.vue
  37. 31
      packages/nc-gui/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue
  38. 10
      packages/nc-gui/components/smartsheet-toolbar/FieldsMenu.vue
  39. 27
      packages/nc-gui/components/smartsheet-toolbar/MoreActions.vue
  40. 23
      packages/nc-gui/components/smartsheet-toolbar/ShareView.vue
  41. 9
      packages/nc-gui/components/smartsheet-toolbar/SharedViewList.vue
  42. 10
      packages/nc-gui/components/smartsheet-toolbar/SortListMenu.vue
  43. 26
      packages/nc-gui/components/smartsheet-toolbar/ViewActions.vue
  44. 26
      packages/nc-gui/components/smartsheet/ApiSnippet.vue
  45. 31
      packages/nc-gui/components/smartsheet/Form.vue
  46. 29
      packages/nc-gui/components/smartsheet/Gallery.vue
  47. 48
      packages/nc-gui/components/smartsheet/Grid.vue
  48. 2
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  49. 2
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  50. 28
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  51. 24
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  52. 2
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  53. 2
      packages/nc-gui/components/smartsheet/sidebar/toolbar/DeleteTable.vue
  54. 2
      packages/nc-gui/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue
  55. 6
      packages/nc-gui/components/tabs/Auth.vue
  56. 6
      packages/nc-gui/components/tabs/Smartsheet.vue
  57. 7
      packages/nc-gui/components/tabs/auth/ApiTokenManagement.vue
  58. 23
      packages/nc-gui/components/tabs/auth/UserManagement.vue
  59. 6
      packages/nc-gui/components/tabs/auth/user-management/FeedbackForm.vue
  60. 16
      packages/nc-gui/components/tabs/auth/user-management/ShareBase.vue
  61. 18
      packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue
  62. 12
      packages/nc-gui/components/template/Editor.vue
  63. 4
      packages/nc-gui/components/virtual-cell/Formula.vue
  64. 24
      packages/nc-gui/components/virtual-cell/HasMany.vue
  65. 18
      packages/nc-gui/components/virtual-cell/Lookup.vue
  66. 24
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  67. 6
      packages/nc-gui/components/webhook/ChannelMultiSelect.vue
  68. 2
      packages/nc-gui/components/webhook/Drawer.vue
  69. 54
      packages/nc-gui/components/webhook/Editor.vue
  70. 11
      packages/nc-gui/components/webhook/List.vue
  71. 17
      packages/nc-gui/components/webhook/Test.vue
  72. 1
      packages/nc-gui/composables/index.ts
  73. 48
      packages/nc-gui/composables/useCellUrlConfig.ts
  74. 30
      packages/nc-gui/composables/useColumnCreateStore.ts
  75. 3
      packages/nc-gui/composables/useExpandedFormStore.ts
  76. 9
      packages/nc-gui/composables/useGridViewColumnWidth.ts
  77. 18
      packages/nc-gui/composables/useLTARStore.ts
  78. 72
      packages/nc-gui/composables/useLoadingIndicator/index.ts
  79. 7
      packages/nc-gui/composables/useProject.ts
  80. 10
      packages/nc-gui/composables/useSharedFormViewStore.ts
  81. 28
      packages/nc-gui/composables/useSharedView.ts
  82. 17
      packages/nc-gui/composables/useSmartsheetRowStore.ts
  83. 20
      packages/nc-gui/composables/useSmartsheetStore.ts
  84. 15
      packages/nc-gui/composables/useUIPermission/index.ts
  85. 21
      packages/nc-gui/composables/useViewColumns.ts
  86. 31
      packages/nc-gui/composables/useViewData.ts
  87. 19
      packages/nc-gui/composables/useViewFilters.ts
  88. 14
      packages/nc-gui/composables/useViewSorts.ts
  89. 1
      packages/nc-gui/context/index.ts
  90. 8
      packages/nc-gui/layouts/base.vue
  91. 4
      packages/nc-gui/lib/types.ts
  92. 1
      packages/nc-gui/package.json
  93. 68
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  94. 45
      packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue
  95. 2
      packages/nc-gui/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue
  96. 2
      packages/nc-gui/pages/[projectType]/[projectId]/index/index/auth.vue
  97. 4
      packages/nc-gui/pages/[projectType]/form/[viewId].vue
  98. 6
      packages/nc-gui/pages/[projectType]/form/[viewId]/index.vue
  99. 4
      packages/nc-gui/pages/index/index.vue
  100. 4
      packages/nc-gui/pages/index/index/[projectId].vue
  101. Some files were not shown because too many files have changed in this diff Show More

9
.all-contributorsrc

@ -864,6 +864,15 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "dolsem",
"name": "Denis Olsem",
"avatar_url": "https://avatars.githubusercontent.com/u/14323955?v=4",
"profile": "https://github.com/dolsem",
"contributions": [
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

1
README.md

@ -453,6 +453,7 @@ Our mission is to provide the most powerful no-code interface for databases whic
<td align="center"><a href="https://github.com/drsantam"><img src="https://avatars.githubusercontent.com/u/10681456?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Santam Chakraborty</b></sub></a><br /><a href="#translation-drsantam" title="Translation">🌍</a></td> <td align="center"><a href="https://github.com/drsantam"><img src="https://avatars.githubusercontent.com/u/10681456?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Santam Chakraborty</b></sub></a><br /><a href="#translation-drsantam" title="Translation">🌍</a></td>
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=eltociear" title="Code">💻</a></td> <td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=eltociear" title="Code">💻</a></td>
<td align="center"><a href="http://asheerrizvi.com"><img src="https://avatars.githubusercontent.com/u/17976252?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Asheer Rizvi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=asheerrizvi" title="Code">💻</a></td> <td align="center"><a href="http://asheerrizvi.com"><img src="https://avatars.githubusercontent.com/u/17976252?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Asheer Rizvi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=asheerrizvi" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dolsem"><img src="https://avatars.githubusercontent.com/u/14323955?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Denis Olsem</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dolsem" title="Code">💻</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

1
packages/nc-gui/components.d.ts vendored

@ -224,6 +224,7 @@ declare module '@vue/runtime-core' {
MdiViewListOutline: typeof import('~icons/mdi/view-list-outline')['default'] MdiViewListOutline: typeof import('~icons/mdi/view-list-outline')['default']
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default'] MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MdiXml: typeof import('~icons/mdi/xml')['default'] MdiXml: typeof import('~icons/mdi/xml')['default']
MiCircleWarning: typeof import('~icons/mi/circle-warning')['default']
PhFileCsv: typeof import('~icons/ph/file-csv')['default'] PhFileCsv: typeof import('~icons/ph/file-csv')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

10
packages/nc-gui/components/cell/Checkbox.vue

@ -42,9 +42,15 @@ function onClick() {
<template> <template>
<div <div
class="flex" class="flex"
:class="{ 'justify-center': !isForm, 'nc-cell-hover-show': !vModel && !readOnly, 'opacity-0': readOnly && !vModel }" :class="{
'justify-center': !isForm,
'w-full': isForm,
'nc-cell-hover-show': !vModel && !readOnly,
'opacity-0': readOnly && !vModel,
}"
@click="onClick"
> >
<div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel }" @click="onClick"> <div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel }">
<component <component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)" :is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
:style="{ :style="{

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

@ -1,21 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import {
import { ColumnInj, EditModeInj, computed, inject, isValidURL } from '#imports' CellUrlDisableOverlayInj,
import MiCircleWarning from '~icons/mi/circle-warning' ColumnInj,
EditModeInj,
computed,
inject,
isValidURL,
ref,
useCellUrlConfig,
useI18n,
watch,
} from '#imports'
const { modelValue: value } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
interface Props { interface Props {
modelValue?: string | null modelValue?: string | null
} }
const { modelValue: value } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)! const editEnabled = inject(EditModeInj)!
const disableOverlay = inject(CellUrlDisableOverlayInj)
// Used in the logic of when to display error since we are not storing the url if its not valid // Used in the logic of when to display error since we are not storing the url if its not valid
const localState = ref(value) const localState = ref(value)
@ -40,6 +54,8 @@ const url = computed(() => {
return `https://${value}` return `https://${value}`
}) })
const { cellUrlOptions } = useCellUrlConfig(url)
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
watch( watch(
@ -59,7 +75,22 @@ watch(
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none text-sm w-full" @blur="editEnabled = false" /> <input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none text-sm w-full" @blur="editEnabled = false" />
<nuxt-link v-else-if="isValid" class="text-sm underline hover:opacity-75" :to="url" target="_blank">{{ value }} </nuxt-link> <nuxt-link
v-else-if="isValid && !cellUrlOptions?.overlay"
class="z-3 text-sm underline hover:opacity-75"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
>
{{ value }}
</nuxt-link>
<nuxt-link
v-else-if="isValid && !disableOverlay && cellUrlOptions?.overlay"
class="z-3 w-full h-full text-center !no-underline hover:opacity-75"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
>
{{ cellUrlOptions.overlay }}
</nuxt-link>
<span v-else class="w-9/10 overflow-ellipsis overflow-hidden">{{ value }}</span> <span v-else class="w-9/10 overflow-ellipsis overflow-hidden">{{ value }}</span>

4
packages/nc-gui/components/cell/attachment/Modal.vue

@ -9,7 +9,7 @@ const { isUIAllowed } = useUIPermission()
const { const {
open, open,
isLoading, isLoading,
isPublicGrid, isPublic,
isReadonly, isReadonly,
visibleItems, visibleItems,
modalVisible, modalVisible,
@ -68,7 +68,7 @@ function onClick(item: Record<string, any>) {
<template #title> <template #title>
<div class="flex gap-4"> <div class="flex gap-4">
<div <div
v-if="isSharedForm || (!isReadonly && isUIAllowed('tableAttachment') && !isPublicGrid && !isLocked)" v-if="isSharedForm || (!isReadonly && isUIAllowed('tableAttachment') && !isPublic && !isLocked)"
class="nc-attach-file group" class="nc-attach-file group"
@click="open" @click="open"
> >

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

@ -65,7 +65,7 @@ watch(
} else { } else {
nextTick(() => { nextTick(() => {
const nextCell = cellRefs.value.reduceRight((cell, curr) => { const nextCell = cellRefs.value.reduceRight((cell, curr) => {
if (!cell && curr.dataset.key === `${rowIndex}${column.value.id}`) cell = curr if (!cell && curr.dataset.key === `${rowIndex}${column.value!.id}`) cell = curr
return cell return cell
}, undefined as HTMLTableDataCellElement | undefined) }, undefined as HTMLTableDataCellElement | undefined)
@ -118,7 +118,7 @@ onKeyDown('Escape', () => {
watch( watch(
() => storedFiles.value.length || 0, () => storedFiles.value.length || 0,
() => { () => {
rowState.value[column.value.title!] = storedFiles.value rowState.value[column.value!.title!] = storedFiles.value
}, },
) )

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

@ -1,9 +1,9 @@
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import FileSaver from 'file-saver' import FileSaver from 'file-saver'
import { useI18n } from 'vue-i18n'
import { import {
ColumnInj, ColumnInj,
EditModeInj, EditModeInj,
IsFormInj,
IsPublicInj, IsPublicInj,
MetaInj, MetaInj,
NOCO, NOCO,
@ -14,11 +14,11 @@ import {
ref, ref,
useApi, useApi,
useFileDialog, useFileDialog,
useI18n,
useInjectionState, useInjectionState,
useProject, useProject,
watch, watch,
} from '#imports' } from '#imports'
import { IsFormInj } from '~/context'
import MdiPdfBox from '~icons/mdi/pdf-box' import MdiPdfBox from '~icons/mdi/pdf-box'
import MdiFileWordOutline from '~icons/mdi/file-word-outline' import MdiFileWordOutline from '~icons/mdi/file-word-outline'
import MdiFilePowerpointBox from '~icons/mdi/file-powerpoint-box' import MdiFilePowerpointBox from '~icons/mdi/file-powerpoint-box'
@ -40,9 +40,9 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const meta = inject(MetaInj)! const meta = inject(MetaInj, ref())
const column = inject(ColumnInj)! const column = inject(ColumnInj, ref())
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(EditModeInj, ref(false))
@ -119,7 +119,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
try { try {
const data = await api.storage.upload( const data = await api.storage.upload(
{ {
path: [NOCO, project.value.title, meta.value.title, column.value.title].join('/'), path: [NOCO, project.value.title, meta.value?.title, column.value?.title].join('/'),
}, },
{ {
files: file, files: file,

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

@ -104,7 +104,7 @@ const initSortable = (el: Element) => {
// update the item order // update the item order
await $api.dbTable.reorder(item.id as string, { await $api.dbTable.reorder(item.id as string, {
order: item.order as any, order: item.order,
}) })
}, },
animation: 150, animation: 150,
@ -142,7 +142,7 @@ const reloadTables = async () => {
} }
const addTableTab = (table: TableType) => { const addTableTab = (table: TableType) => {
addTab({ title: table.title, id: table.id, type: table.type as any }) addTab({ title: table.title, id: table.id, type: table.type as TabType })
} }
function openRenameTableDialog(table: TableType, rightClick = false) { function openRenameTableDialog(table: TableType, rightClick = false) {
@ -286,7 +286,7 @@ function openTableCreateDialog() {
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b"> <a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a <a
v-t="['e:datasource:import-request']" v-e="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052" href="https://github.com/nocodb/nocodb/issues/2052"
target="_blank" target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)" class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)"
@ -306,7 +306,7 @@ function openTableCreateDialog() {
<div <div
v-for="table of tables" v-for="table of tables"
:key="table.id" :key="table.id"
v-t="['a:table:open']" v-e="['a:table:open']"
:class="[ :class="[
{ hidden: !filteredTables?.includes(table), active: activeTable === table.title }, { hidden: !filteredTables?.includes(table), active: activeTable === table.title },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`, `nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,

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

@ -83,7 +83,7 @@ const columns = [
<div class="flex flex-col w-3/5"> <div class="flex flex-col w-3/5">
<div class="flex flex-row justify-end items-center w-full mb-4"> <div class="flex flex-row justify-end items-center w-full mb-4">
<!-- Reload --> <!-- Reload -->
<a-button v-t="['a:proj-meta:meta-data:reload']" class="self-start nc-btn-metasync-reload" @click="loadMetaDiff"> <a-button v-e="['a:proj-meta:meta-data:reload']" class="self-start nc-btn-metasync-reload" @click="loadMetaDiff">
<div class="flex items-center gap-2 text-gray-600 font-light"> <div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" /> <MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
{{ $t('general.reload') }} {{ $t('general.reload') }}
@ -112,7 +112,7 @@ const columns = [
<div class="flex place-content-center w-2/5"> <div class="flex place-content-center w-2/5">
<!-- Sync Now --> <!-- Sync Now -->
<div v-if="isDifferent"> <div v-if="isDifferent">
<a-button v-t="['a:proj-meta:meta-data:sync']" class="nc-btn-metasync-sync-now" type="primary" @click="syncMetaDiff"> <a-button v-e="['a:proj-meta:meta-data:sync']" class="nc-btn-metasync-sync-now" type="primary" @click="syncMetaDiff">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<MdiDatabaseSync /> <MdiDatabaseSync />
{{ $t('activity.metaSync') }} {{ $t('activity.metaSync') }}

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

@ -10,7 +10,7 @@ watch(includeM2M, async () => await loadTables())
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="flex flex-row items-center w-full mb-4 gap-2"> <div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show M2M Tables --> <!-- Show M2M Tables -->
<a-checkbox v-model:checked="includeM2M" v-t="['c:themes:show-m2m-tables']">{{ <a-checkbox v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']">{{
$t('msg.info.showM2mTables') $t('msg.info.showM2mTables')
}}</a-checkbox> }}</a-checkbox>
</div> </div>

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

@ -208,7 +208,7 @@ watch(
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<component :is="selectedSubTab.body" class="px-2 py-6" /> <component :is="selectedSubTab?.body" class="px-2 py-6" />
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</a-modal> </a-modal>

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

@ -208,9 +208,10 @@ onMounted(async () => {
socket.on('progress', async (d: Record<string, any>) => { socket.on('progress', async (d: Record<string, any>) => {
progress.value.push(d) progress.value.push(d)
// FIXME: this doesn't work
await nextTick(() => { await nextTick(() => {
;(logRef.value?.$el as HTMLDivElement).scrollTo() const container: HTMLDivElement = logRef.value?.$el?.firstElementChild
if (!container) return
container.scrollTop = container.scrollHeight
}) })
if (d.status === 'COMPLETED') { if (d.status === 'COMPLETED') {
@ -251,7 +252,8 @@ onBeforeUnmount(() => {
href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials" href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials"
class="prose-sm underline text-grey text-xs" class="prose-sm underline text-grey text-xs"
target="_blank" target="_blank"
>{{ $t('msg.info.airtable.credentials') }} >
{{ $t('msg.info.airtable.credentials') }}
</a> </a>
</div> </div>
@ -286,30 +288,30 @@ onBeforeUnmount(() => {
<!-- Import Secondary Views --> <!-- Import Secondary Views -->
<div class="my-2"> <div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncViews">{{ <a-checkbox v-model:checked="syncSource.details.options.syncViews">
$t('labels.importSecondaryViews') {{ $t('labels.importSecondaryViews') }}
}}</a-checkbox> </a-checkbox>
</div> </div>
<!-- Import Rollup Columns --> <!-- Import Rollup Columns -->
<div class="my-2"> <div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncRollup">{{ <a-checkbox v-model:checked="syncSource.details.options.syncRollup">
$t('labels.importRollupColumns') {{ $t('labels.importRollupColumns') }}
}}</a-checkbox> </a-checkbox>
</div> </div>
<!-- Import Lookup Columns --> <!-- Import Lookup Columns -->
<div class="my-2"> <div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncLookup">{{ <a-checkbox v-model:checked="syncSource.details.options.syncLookup">
$t('labels.importLookupColumns') {{ $t('labels.importLookupColumns') }}
}}</a-checkbox> </a-checkbox>
</div> </div>
<!-- Import Attachment Columns --> <!-- Import Attachment Columns -->
<div class="my-2"> <div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncAttachment">{{ <a-checkbox v-model:checked="syncSource.details.options.syncAttachment">
$t('labels.importAttachmentColumns') {{ $t('labels.importAttachmentColumns') }}
}}</a-checkbox> </a-checkbox>
</div> </div>
<!-- Import Formula Columns --> <!-- Import Formula Columns -->
@ -317,9 +319,9 @@ onBeforeUnmount(() => {
<template #title> <template #title>
<span>Coming Soon!</span> <span>Coming Soon!</span>
</template> </template>
<a-checkbox v-model:checked="syncSource.details.options.syncFormula" disabled>{{ <a-checkbox v-model:checked="syncSource.details.options.syncFormula" disabled>
$t('labels.importFormulaColumns') {{ $t('labels.importFormulaColumns') }}
}}</a-checkbox> </a-checkbox>
</a-tooltip> </a-tooltip>
</a-form> </a-form>
@ -327,8 +329,8 @@ onBeforeUnmount(() => {
<!-- Questions / Help - Reach out here --> <!-- Questions / Help - Reach out here -->
<div> <div>
<a href="https://github.com/nocodb/nocodb/issues/2052" target="_blank" <a href="https://github.com/nocodb/nocodb/issues/2052" target="_blank">
>{{ $t('general.questions') }} / {{ $t('general.help') }} - {{ $t('general.reachOut') }}</a {{ $t('general.questions') }} / {{ $t('general.help') }} - {{ $t('general.reachOut') }}</a
> >
<br /> <br />
@ -337,8 +339,8 @@ onBeforeUnmount(() => {
{{ $t('general.betaNote') }} {{ $t('general.betaNote') }}
<a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">{{ <a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">{{
$t('general.moreInfo') $t('general.moreInfo')
}}</a }}</a>
>. .
</div> </div>
</div> </div>
</div> </div>
@ -391,7 +393,7 @@ onBeforeUnmount(() => {
<!-- Import --> <!-- Import -->
<a-button <a-button
key="submit" key="submit"
v-t="['c:sync-airtable:save-and-sync']" v-e="['c:sync-airtable:save-and-sync']"
type="primary" type="primary"
class="nc-btn-airtable-import" class="nc-btn-airtable-import"
:disabled="disableImportButton" :disabled="disableImportButton"

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

@ -42,7 +42,7 @@ const { t } = useI18n()
const { isLoading: loading, api } = useApi() const { isLoading: loading, api } = useApi()
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const viewList = inject(ViewListInj) const viewList = inject(ViewListInj)

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

@ -41,7 +41,7 @@ const openSwaggerLink = () => {
<template #before> <template #before>
<a-list-item v-if="project"> <a-list-item v-if="project">
<nuxt-link <nuxt-link
v-t="['a:navbar:user:swagger']" v-e="['a:navbar:user:swagger']"
class="!no-underline !text-current py-4 font-semibold" class="!no-underline !text-current py-4 font-semibold"
target="_blank" target="_blank"
@click="openSwaggerLink" @click="openSwaggerLink"

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

@ -37,7 +37,7 @@ const logout = () => {
<a-menu class="ml-2 !py-0 min-w-32 leading-8 !rounded nc-menu-account"> <a-menu class="ml-2 !py-0 min-w-32 leading-8 !rounded nc-menu-account">
<a-menu-item-group title="User Settings"> <a-menu-item-group title="User Settings">
<a-menu-item key="email" class="!rounded-t"> <a-menu-item key="email" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user"> <nuxt-link v-e="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" /> <MdiAt class="mt-1 group-hover:text-success" />
&nbsp; &nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span> <span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span>
@ -47,7 +47,7 @@ const logout = () => {
<a-menu-divider class="!m-0" /> <a-menu-divider class="!m-0" />
<a-menu-item key="signout" class="!rounded-b"> <a-menu-item key="signout" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout"> <div v-e="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout">
<MdiLogout class="group-hover:(!text-red-500)" />&nbsp; <MdiLogout class="group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout"> <span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }} {{ $t('general.signOut') }}
@ -76,7 +76,7 @@ const logout = () => {
<a-menu-item class="active:(ring ring-accent)"> <a-menu-item class="active:(ring ring-accent)">
<div <div
v-t="['c:project:create:xcdb']" v-e="['c:project:create:xcdb']"
class="group flex items-center gap-2 py-2 hover:text-primary" class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create')" @click="navigateTo('/project/create')"
> >
@ -87,7 +87,7 @@ const logout = () => {
<a-menu-item class="rounded-b active:(ring ring-accent)"> <a-menu-item class="rounded-b active:(ring ring-accent)">
<div <div
v-t="['c:project:create:extdb']" v-e="['c:project:create:extdb']"
class="group flex items-center gap-2 py-2 hover:text-primary" class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create-external')" @click="navigateTo('/project/create-external')"
> >

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

@ -23,18 +23,18 @@ const isZhLang = $computed(() => locale.value.startsWith('zh'))
/> />
<div v-else class="flex justify-between gap-1 w-full px-2"> <div v-else class="flex justify-between gap-1 w-full px-2">
<MdiDiscord v-t="['e:community:discord']" class="icon text-[#7289DA]" @click="open('https://discord.gg/5RgZmkW')" /> <MdiDiscord v-e="['e:community:discord']" class="icon text-[#7289DA]" @click="open('https://discord.gg/5RgZmkW')" />
<div <div
v-t="['e:community:discourse']" v-e="['e:community:discourse']"
class="icon flex items-center justify-center min-w-[43px]" class="icon flex items-center justify-center min-w-[43px]"
@click="open('https://community.nocodb.com/')" @click="open('https://community.nocodb.com/')"
> >
<div class="discourse" /> <div class="discourse" />
</div> </div>
<MdiReddit v-t="['e:community:reddit']" class="icon text-[#FF4600]" @click="open('https://www.reddit.com/r/NocoDB/')" /> <MdiReddit v-e="['e:community:reddit']" class="icon text-[#FF4600]" @click="open('https://www.reddit.com/r/NocoDB/')" />
<MdiTwitter v-t="['e:community:twitter']" class="icon text-[#1DA1F2]" @click="open('https://twitter.com/NocoDB')" /> <MdiTwitter v-e="['e:community:twitter']" class="icon text-[#1DA1F2]" @click="open('https://twitter.com/NocoDB')" />
<MdiCalendarMonth <MdiCalendarMonth
v-t="['e:community:book-demo']" v-e="['e:community:book-demo']"
class="icon text-green-500" class="icon text-green-500"
@click="open('https://calendly.com/nocodb-meeting')" @click="open('https://calendly.com/nocodb-meeting')"
/> />

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

@ -13,7 +13,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-t="['e:docs']" v-e="['e:docs']"
class="text-primary !no-underline !text-current" class="text-primary !no-underline !text-current"
target="_blank" target="_blank"
to="https://docs.nocodb.com/" to="https://docs.nocodb.com/"
@ -26,7 +26,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-t="['e:api-docs']" v-e="['e:api-docs']"
class="text-primary !no-underline !text-current" class="text-primary !no-underline !text-current"
target="_blank" target="_blank"
to="https://apis.nocodb.com/" to="https://apis.nocodb.com/"
@ -40,7 +40,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-t="['e:community:github']" v-e="['e:community:github']"
class="text-primary !no-underline !text-current" class="text-primary !no-underline !text-current"
to="https://github.com/nocodb/nocodb" to="https://github.com/nocodb/nocodb"
target="_blank" target="_blank"
@ -66,7 +66,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-t="['e:community:book-demo']" v-e="['e:community:book-demo']"
class="!no-underline !text-current" class="!no-underline !text-current"
to="https://calendly.com/nocodb-meeting" to="https://calendly.com/nocodb-meeting"
target="_blank" target="_blank"
@ -82,7 +82,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-t="['e:community:discord']" v-e="['e:community:discord']"
class="!no-underline !text-current" class="!no-underline !text-current"
to="https://discord.gg/5RgZmkW" to="https://discord.gg/5RgZmkW"
target="_blank" target="_blank"
@ -98,7 +98,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-t="['e:community:twitter']" v-e="['e:community:twitter']"
class="!no-underline !text-current" class="!no-underline !text-current"
to="https://twitter.com/NocoDB" to="https://twitter.com/NocoDB"
target="_blank" target="_blank"
@ -113,7 +113,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link v-t="['e:hiring']" class="!no-underline !text-current" target="_blank" to="http://careers.nocodb.com"> <nuxt-link v-e="['e:hiring']" class="!no-underline !text-current" target="_blank" to="http://careers.nocodb.com">
<div class="flex items-center text-sm"> <div class="flex items-center text-sm">
<!-- todo: i18n --> <!-- todo: i18n -->
<div class="ml-3"> <div class="ml-3">
@ -124,7 +124,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-t="['e:community:reddit']" v-e="['e:community:reddit']"
class="!no-underline !text-current" class="!no-underline !text-current"
target="_blank" target="_blank"
to="https://www.reddit.com/r/NocoDB/" to="https://www.reddit.com/r/NocoDB/"

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

@ -1,23 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReadonlyInj, ReloadViewDataHookInj } from '#imports'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReadonlyInj, ReloadViewDataHookInj } from '~/context'
const { sharedView, meta, sorts, nestedFilters } = useSharedView() const { sharedView, meta, sorts, nestedFilters } = useSharedView()
const { signedIn } = useGlobal() const { signedIn } = useGlobal()
const { loadProject } = useProject(meta?.value.project_id) const { loadProject } = useProject(meta.value?.project_id)
const reloadEventHook = createEventHook<void>() const reloadEventHook = createEventHook<void>()
provide(ReloadViewDataHookInj, reloadEventHook) provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReadonlyInj, true) provide(ReadonlyInj, true)
provide(MetaInj, meta) provide(MetaInj, meta)
provide(ActiveViewInj, sharedView) provide(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value.columns as any[])) provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta, true, sorts, nestedFilters) useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
if (signedIn.value) { if (signedIn.value) {
try { try {

9
packages/nc-gui/components/smartsheet-column/EditOrAdd.vue

@ -1,10 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useNuxtApp } from '#app' import { IsFormInj, MetaInj, ReloadViewDataHookInj, computed, inject, uiTypes, useMetas, useNuxtApp, watchEffect } from '#imports'
import { computed, inject, useMetas, watchEffect } from '#imports'
import { IsFormInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import { uiTypes } from '~/utils/columnUtils'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline' import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline' import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier' import MdiIdentifierIcon from '~icons/mdi/identifier'
@ -20,7 +17,7 @@ const { t } = useI18n()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
@ -48,7 +45,7 @@ const uiTypesOptions = computed<typeof uiTypes>(() => {
}) })
const reloadMetaAndData = async () => { const reloadMetaAndData = async () => {
await getMeta(meta?.value.id as string, true) await getMeta(meta.value?.id as string, true)
reloadDataTrigger?.trigger() reloadDataTrigger?.trigger()
} }

15
packages/nc-gui/components/smartsheet-column/EditOrAddProvider.vue

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

12
packages/nc-gui/components/smartsheet-column/FormulaOptions.vue

@ -41,7 +41,7 @@ enum JSEPNode {
ARRAY_EXP = 'ArrayExpression', ARRAY_EXP = 'ArrayExpression',
} }
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const columns = computed(() => meta?.value?.columns || []) const columns = computed(() => meta?.value?.columns || [])
@ -102,7 +102,7 @@ const suggestionsList = computed(() => {
if (c.uidt === UITypes.LinkToAnotherRecord && c.system) return false if (c.uidt === UITypes.LinkToAnotherRecord && c.system) return false
// v1 logic? skip the current column // v1 logic? skip the current column
if (!column) return true if (!column) return true
return column.value.id !== c.id return column.value?.id !== c.id
}) })
.map((c: any) => ({ .map((c: any) => ({
text: c.title, text: c.title,
@ -238,7 +238,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
} else if (parsedTree.type === JSEPNode.IDENTIFIER) { } else if (parsedTree.type === JSEPNode.IDENTIFIER) {
if ( if (
columns.value columns.value
.filter((c: Record<string, any>) => !column || column.value.id !== c.id) .filter((c: Record<string, any>) => !column || column.value?.id !== c.id)
.every((c: Record<string, any>) => c.title !== parsedTree.name) .every((c: Record<string, any>) => c.title !== parsedTree.name)
) { ) {
errors.add(`Column '${parsedTree.name}' is not available`) errors.add(`Column '${parsedTree.name}' is not available`)
@ -249,7 +249,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
// get all formula columns excluding itself // get all formula columns excluding itself
const formulaPaths = columns.value const formulaPaths = columns.value
.filter((c: Record<string, any>) => c.id !== column?.value.id && c.uidt === UITypes.Formula) .filter((c: Record<string, any>) => c.id !== column.value?.id && c.uidt === UITypes.Formula)
.reduce((res: Record<string, any>[], c: Record<string, any>) => { .reduce((res: Record<string, any>[], c: Record<string, any>) => {
// in `formula`, get all the target neighbours // in `formula`, get all the target neighbours
// i.e. all column id (e.g. cl_xxxxxxxxxxxxxx) with formula type // i.e. all column id (e.g. cl_xxxxxxxxxxxxxx) with formula type
@ -265,9 +265,9 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
// include target formula column (i.e. the one to be saved if applicable) // include target formula column (i.e. the one to be saved if applicable)
const targetFormulaCol = columns.value.find((c: ColumnType) => c.title === parsedTree.name && c.uidt === UITypes.Formula) const targetFormulaCol = columns.value.find((c: ColumnType) => c.title === parsedTree.name && c.uidt === UITypes.Formula)
if (targetFormulaCol && column?.value.id) { if (targetFormulaCol && column.value?.id) {
formulaPaths.push({ formulaPaths.push({
[column?.value?.id as string]: [targetFormulaCol.id], [column.value?.id as string]: [targetFormulaCol.id],
}) })
} }
const vertices = formulaPaths.length const vertices = formulaPaths.length

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

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ModelTypes, MssqlUi, SqliteUi } from 'nocodb-sdk' import { ModelTypes, MssqlUi, SqliteUi } from 'nocodb-sdk'
import { inject, useProject } from '#imports' import { MetaInj, inject, useProject } from '#imports'
import { MetaInj } from '~/context'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline' import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline' import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
@ -13,7 +12,7 @@ const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj)!) const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
@ -25,10 +24,10 @@ setAdditionalValidations({
const onUpdateDeleteOptions = sqlUi === MssqlUi ? ['NO ACTION'] : ['NO ACTION', 'CASCADE', 'RESTRICT', 'SET NULL', 'SET DEFAULT'] const onUpdateDeleteOptions = sqlUi === MssqlUi ? ['NO ACTION'] : ['NO ACTION', 'CASCADE', 'RESTRICT', 'SET NULL', 'SET DEFAULT']
if (!vModel.value.parentId) vModel.value.parentId = meta.id if (!vModel.value.parentId) vModel.value.parentId = meta?.id
if (!vModel.value.childId) vModel.value.childId = null if (!vModel.value.childId) vModel.value.childId = null
if (!vModel.value.childColumn) vModel.value.childColumn = `${meta.table_name}_id` if (!vModel.value.childColumn) vModel.value.childColumn = `${meta?.table_name}_id`
if (!vModel.value.childTable) vModel.value.childTable = meta.table_name if (!vModel.value.childTable) vModel.value.childTable = meta?.table_name
if (!vModel.value.parentTable) vModel.value.parentTable = vModel.value.rtn || '' if (!vModel.value.parentTable) vModel.value.parentTable = vModel.value.rtn || ''
if (!vModel.value.parentColumn) vModel.value.parentColumn = vModel.value.rcn || '' if (!vModel.value.parentColumn) vModel.value.parentColumn = vModel.value.rcn || ''

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

@ -11,7 +11,7 @@ const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj)!) const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
@ -38,7 +38,7 @@ const refTables = $computed(() => {
return [] return []
} }
return meta.columns return meta?.columns
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && !c.system) ?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && !c.system)
.map((c: ColumnType) => ({ .map((c: ColumnType) => ({
col: c.colOptions, col: c.colOptions,

2
packages/nc-gui/components/smartsheet-column/PercentOptions.vue

@ -1,7 +1,7 @@
<!-- File not in use for now --> <!-- File not in use for now -->
<script setup lang="ts"> <script setup lang="ts">
import { precisions } from '@/utils/percentUtils' import { precisions } from '#imports'
interface Props { interface Props {
value: Record<string, any> value: Record<string, any>

2
packages/nc-gui/components/smartsheet-column/RatingOptions.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { getMdiIcon } from '@/utils' import { getMdiIcon } from '#imports'
interface Props { interface Props {
value: Record<string, any> value: Record<string, any>

7
packages/nc-gui/components/smartsheet-column/RollupOptions.vue

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { inject, useMetas, useProject } from '#imports' import { MetaInj, inject, useMetas, useProject } from '#imports'
import { MetaInj } from '~/context'
interface Props { interface Props {
value: Record<string, any> value: Record<string, any>
@ -11,7 +10,7 @@ const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj)!) const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
@ -51,7 +50,7 @@ const refTables = $computed(() => {
} }
return ( return (
meta.columns meta?.columns
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== 'bt' && !c.system) ?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== 'bt' && !c.system)
.map((c) => ({ .map((c) => ({
col: c.colOptions, col: c.colOptions,

11
packages/nc-gui/components/smartsheet-header/CellIcon.vue

@ -1,14 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { toRef } from 'vue' import { ColumnInj, toRef, useColumn } from '#imports'
import { ColumnInj } from '~/context'
import FilePhoneIcon from '~icons/mdi/file-phone' import FilePhoneIcon from '~icons/mdi/file-phone'
import { useColumn } from '#imports'
import KeyIcon from '~icons/mdi/key-variant' import KeyIcon from '~icons/mdi/key-variant'
import JSONIcon from '~icons/mdi/code-json' import JSONIcon from '~icons/mdi/code-json'
import ClockIcon from '~icons/mdi/clock-time-five' import ClockIcon from '~icons/mdi/clock-time-five'
// import FKIcon from '~icons/mdi/link-variant'
import WebIcon from '~icons/mdi/web' import WebIcon from '~icons/mdi/web'
import TextAreaIcon from '~icons/mdi/card-text-outline' import TextAreaIcon from '~icons/mdi/card-text-outline'
import StringIcon from '~icons/mdi/alpha-a-box-outline' import StringIcon from '~icons/mdi/alpha-a-box-outline'
@ -65,11 +62,7 @@ const icon = computed(() => {
return DecimalIcon return DecimalIcon
} else if (additionalColMeta.isPhoneNumber.value) { } else if (additionalColMeta.isPhoneNumber.value) {
return FilePhoneIcon return FilePhoneIcon
} } else if (additionalColMeta.isURL.value) {
// else if(additionalColMeta.isForeignKey) {
// return FKIcon
// }
else if (additionalColMeta.isURL.value) {
return WebIcon return WebIcon
} else if (additionalColMeta.isCurrency.value) { } else if (additionalColMeta.isCurrency.value) {
return CurrencyIcon return CurrencyIcon

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

@ -10,7 +10,7 @@ const emit = defineEmits(['edit'])
const column = inject(ColumnInj) const column = inject(ColumnInj)
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj)

4
packages/nc-gui/components/smartsheet-header/VirtualCell.vue

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk' import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import { substituteColumnIdWithAliasInFormula } from 'nocodb-sdk' import { substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n'
import { import {
ColumnInj, ColumnInj,
IsFormInj, IsFormInj,
@ -11,6 +10,7 @@ import {
provide, provide,
ref, ref,
toRef, toRef,
useI18n,
useMetas, useMetas,
useUIPermission, useUIPermission,
useVirtualCell, useVirtualCell,
@ -32,7 +32,7 @@ const { metas } = useMetas()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))

7
packages/nc-gui/components/smartsheet-header/VirtualCellIcon.vue

@ -2,8 +2,7 @@
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { toRef } from 'vue' import { ColumnInj, toRef } from '#imports'
import { ColumnInj } from '~/context'
import GenericIcon from '~icons/mdi/square-rounded' import GenericIcon from '~icons/mdi/square-rounded'
import HMIcon from '~icons/mdi/table-arrow-right' import HMIcon from '~icons/mdi/table-arrow-right'
import BTIcon from '~icons/mdi/table-arrow-left' import BTIcon from '~icons/mdi/table-arrow-left'
@ -25,9 +24,9 @@ if (column) {
const { isLookup, isBt, isRollup, isMm, isHm } = useVirtualCell(column as Ref<ColumnType>) const { isLookup, isBt, isRollup, isMm, isHm } = useVirtualCell(column as Ref<ColumnType>)
if (isLookup || isBt || isRollup || isMm || isHm) { if (isLookup || isBt || isRollup || isMm || isHm) {
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
relationColumn = meta?.value.columns?.find((c) => c.id === column.value?.colOptions?.fk_relation_column_id) as ColumnType & { relationColumn = meta.value?.columns?.find((c) => c.id === column.value?.colOptions?.fk_relation_column_id) as ColumnType & {
colOptions: LinkToAnotherRecordType colOptions: LinkToAnotherRecordType
} }
} }

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

@ -14,7 +14,7 @@ const onClick = () => {
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> {{ $t('activity.addRow') }} </template> <template #title> {{ $t('activity.addRow') }} </template>
<div <div
v-t="['c:row:add:grid-top']" v-e="['c:row:add:grid-top']"
:class="{ 'group': !isLocked, 'disabled-ring': isLocked }" :class="{ 'group': !isLocked, 'disabled-ring': isLocked }"
class="nc-add-new-row-btn flex align-center" class="nc-add-new-row-btn flex align-center"
> >

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

@ -33,9 +33,9 @@ const logicalOps = [
{ value: 'or', text: 'OR' }, { value: 'or', text: 'OR' },
] ]
const meta = inject(MetaInj)! const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj)! const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)! const reloadDataHook = inject(ReloadViewDataHookInj)!

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

@ -5,7 +5,7 @@ import { ActiveViewInj, IsLockedInj, IsPublicInj, computed, inject, ref, useGlob
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const activeView = inject(ActiveViewInj) const activeView = inject(ActiveViewInj, ref())
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
@ -53,7 +53,7 @@ const filterAutoSaveLoc = computed({
<template> <template>
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-filter-menu"> <a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-filter-menu">
<div :class="{ 'nc-badge nc-active-btn': filtersLength }"> <div :class="{ 'nc-badge nc-active-btn': filtersLength }">
<a-button v-t="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked"> <a-button v-e="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<MdiFilterOutline /> <MdiFilterOutline />
<!-- Filter --> <!-- Filter -->
@ -80,7 +80,7 @@ const filterAutoSaveLoc = computed({
<div class="flex-1" /> <div class="flex-1" />
<a-button <a-button
v-show="!filterAutoSave" v-show="!filterAutoSave"
v-t="['a:filter:auto-apply']" v-e="['a:filter:auto-apply']"
size="small" size="small"
class="text-xs ml-2" class="text-xs ml-2"
@click="applyChanges" @click="applyChanges"

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

@ -1,6 +1,6 @@
<template> <template>
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu"> <a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu">
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> <a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<MdiDownload class="group-hover:text-accent text-gray-500" /> <MdiDownload class="group-hover:text-accent text-gray-500" />
<span class="text-capitalize !text-sm font-weight-normal">Download</span> <span class="text-capitalize !text-sm font-weight-normal">Download</span>

17
packages/nc-gui/components/smartsheet-toolbar/ExportSubActions.vue

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { RequestParams } from 'nocodb-sdk'
import { ExportTypes } from 'nocodb-sdk' import { ExportTypes } from 'nocodb-sdk'
import FileSaver from 'file-saver' import FileSaver from 'file-saver'
import * as XLSX from 'xlsx' import * as XLSX from 'xlsx'
@ -15,7 +16,7 @@ const { project } = useProject()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const selectedView = inject(ActiveViewInj) const selectedView = inject(ActiveViewInj)
@ -35,8 +36,8 @@ const exportFile = async (exportType: ExportTypes) => {
} else { } else {
res = await $api.dbViewRow.export( res = await $api.dbViewRow.export(
'noco', 'noco',
project?.value.title as string, project.value?.title as string,
meta?.value.title as string, meta.value?.title as string,
selectedView?.value.title as string, selectedView?.value.title as string,
exportType, exportType,
{ {
@ -47,16 +48,16 @@ const exportFile = async (exportType: ExportTypes) => {
sortArrJson: JSON.stringify(sorts.value), sortArrJson: JSON.stringify(sorts.value),
filterArrJson: JSON.stringify(nestedFilters.value), filterArrJson: JSON.stringify(nestedFilters.value),
}, },
} as any, } as RequestParams,
) )
} }
const { data, headers } = res const { data, headers } = res
if (exportType === ExportTypes.EXCEL) { if (exportType === ExportTypes.EXCEL) {
const workbook = XLSX.read(data, { type: 'base64' }) const workbook = XLSX.read(data, { type: 'base64' })
XLSX.writeFile(workbook, `${meta?.value.title}_exported_${c++}.xlsx`) XLSX.writeFile(workbook, `${meta.value?.title}_exported_${c++}.xlsx`)
} else if (exportType === ExportTypes.CSV) { } else if (exportType === ExportTypes.CSV) {
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }) const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
FileSaver.saveAs(blob, `${meta?.value.title}_exported_${c++}.csv`) FileSaver.saveAs(blob, `${meta.value?.title}_exported_${c++}.csv`)
} }
offset = +headers['nc-export-offset'] offset = +headers['nc-export-offset']
if (offset > -1) { if (offset > -1) {
@ -75,14 +76,14 @@ const exportFile = async (exportType: ExportTypes) => {
<template> <template>
<a-menu-item> <a-menu-item>
<div v-t="['a:actions:download-csv']" class="nc-project-menu-item" @click="exportFile(ExportTypes.CSV)"> <div v-e="['a:actions:download-csv']" class="nc-project-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadOutline class="text-gray-500" /> <MdiDownloadOutline class="text-gray-500" />
<!-- Download as CSV --> <!-- Download as CSV -->
{{ $t('activity.downloadCSV') }} {{ $t('activity.downloadCSV') }}
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item> <a-menu-item>
<div v-t="['a:actions:download-excel']" class="nc-project-menu-item" @click="exportFile(ExportTypes.EXCEL)"> <div v-e="['a:actions:download-excel']" class="nc-project-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadOutline class="text-gray-500" /> <MdiDownloadOutline class="text-gray-500" />
<!-- Download as XLSX --> <!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }} {{ $t('activity.downloadExcel') }}

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

@ -2,8 +2,7 @@
import type { SelectProps } from 'ant-design-vue' import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { computed } from 'vue' import { MetaInj, computed } from '#imports'
import { MetaInj } from '~/context'
import VirtualCellIcon from '~/components/smartsheet-header/VirtualCellIcon.vue' import VirtualCellIcon from '~/components/smartsheet-header/VirtualCellIcon.vue'
import CellIcon from '~/components/smartsheet-header/CellIcon.vue' import CellIcon from '~/components/smartsheet-header/CellIcon.vue'
@ -16,39 +15,13 @@ const { modelValue, isSort } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const localValue = computed({ const localValue = computed({
get: () => modelValue, get: () => modelValue,
set: (val) => emit('update:modelValue', val), set: (val) => emit('update:modelValue', val),
}) })
/* export default {
name: 'FieldListAutoCompleteDropdown',
props: {
columns: Array,
value: String,
},
computed: {
localValue: {
set(v) {
this.$emit('input', v)
},
get() {
return this.value
},
},
},
mounted() {
const autocompleteInput = this.$refs.field.$refs.input
autocompleteInput.addEventListener('focus', this.onFocus, true)
},
methods: {
onFocus(e) {
this.$refs.field.isMenuActive = true // open item list
},
},
} */
const options = computed<SelectProps['options']>(() => const options = computed<SelectProps['options']>(() =>
meta?.value?.columns meta?.value?.columns
?.filter((c: ColumnType) => { ?.filter((c: ColumnType) => {

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

@ -20,9 +20,9 @@ import {
import CellIcon from '~/components/smartsheet-header/CellIcon.vue' import CellIcon from '~/components/smartsheet-header/CellIcon.vue'
import VirtualCellIcon from '~/components/smartsheet-header/VirtualCellIcon.vue' import VirtualCellIcon from '~/components/smartsheet-header/VirtualCellIcon.vue'
const meta = inject(MetaInj)! const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj)! const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)! const reloadDataHook = inject(ReloadViewDataHookInj)!
@ -48,7 +48,7 @@ const {
} = useViewColumns(activeView, meta, () => reloadDataHook.trigger()) } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
watch( watch(
() => (activeView.value as any)?.id, () => activeView.value?.id,
async (newVal, oldVal) => { async (newVal, oldVal) => {
if (newVal !== oldVal && meta.value) { if (newVal !== oldVal && meta.value) {
await loadViewColumns() await loadViewColumns()
@ -118,7 +118,7 @@ const getIcon = (c: ColumnType) =>
<template> <template>
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-fields-menu"> <a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-fields-menu">
<div :class="{ 'nc-badge nc-active-btn': isAnyFieldHidden }"> <div :class="{ 'nc-badge nc-active-btn': isAnyFieldHidden }">
<a-button v-t="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked"> <a-button v-e="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<MdiEyeOffOutline /> <MdiEyeOffOutline />
@ -146,7 +146,7 @@ const getIcon = (c: ColumnType) =>
<div v-show="filteredFieldList.includes(field)" :key="field.id" class="px-2 py-1 flex items-center" @click.stop> <div v-show="filteredFieldList.includes(field)" :key="field.id" class="px-2 py-1 flex items-center" @click.stop>
<a-checkbox <a-checkbox
v-model:checked="field.show" v-model:checked="field.show"
v-t="['a:fields:show-hide']" v-e="['a:fields:show-hide']"
class="shrink" class="shrink"
@change="saveOrUpdate(field, index)" @change="saveOrUpdate(field, index)"
> >

27
packages/nc-gui/components/smartsheet-toolbar/MoreActions.vue

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import * as XLSX from 'xlsx' import * as XLSX from 'xlsx'
import type { RequestParams } from 'nocodb-sdk'
import { ExportTypes } from 'nocodb-sdk' import { ExportTypes } from 'nocodb-sdk'
import FileSaver from 'file-saver' import FileSaver from 'file-saver'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@ -29,11 +30,11 @@ const { project } = useProject()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))
const selectedView = inject(ActiveViewInj) const selectedView = inject(ActiveViewInj, ref())
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow() const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
@ -60,8 +61,8 @@ const exportFile = async (exportType: ExportTypes) => {
res = await $api.dbViewRow.export( res = await $api.dbViewRow.export(
'noco', 'noco',
project?.value.title as string, project?.value.title as string,
meta?.value.title as string, meta.value?.title as string,
selectedView?.value.title as string, selectedView.value?.title as string,
exportType, exportType,
{ {
responseType, responseType,
@ -71,16 +72,16 @@ const exportFile = async (exportType: ExportTypes) => {
sortArrJson: JSON.stringify(sorts.value), sortArrJson: JSON.stringify(sorts.value),
filterArrJson: JSON.stringify(nestedFilters.value), filterArrJson: JSON.stringify(nestedFilters.value),
}, },
} as any, } as RequestParams,
) )
} }
const { data, headers } = res const { data, headers } = res
if (exportType === ExportTypes.EXCEL) { if (exportType === ExportTypes.EXCEL) {
const workbook = XLSX.read(data, { type: 'base64' }) const workbook = XLSX.read(data, { type: 'base64' })
XLSX.writeFile(workbook, `${meta?.value.title}_exported_${c++}.xlsx`) XLSX.writeFile(workbook, `${meta.value?.title}_exported_${c++}.xlsx`)
} else if (exportType === ExportTypes.CSV) { } else if (exportType === ExportTypes.CSV) {
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }) const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
FileSaver.saveAs(blob, `${meta?.value.title}_exported_${c++}.csv`) FileSaver.saveAs(blob, `${meta.value?.title}_exported_${c++}.csv`)
} }
offset = +headers['nc-export-offset'] offset = +headers['nc-export-offset']
if (offset > -1) { if (offset > -1) {
@ -100,7 +101,7 @@ const exportFile = async (exportType: ExportTypes) => {
<template> <template>
<div> <div>
<a-dropdown> <a-dropdown>
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> <a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-1 items-center"> <div class="flex gap-1 items-center">
<MdiFlashOutline /> <MdiFlashOutline />
@ -114,13 +115,13 @@ const exportFile = async (exportType: ExportTypes) => {
<template #overlay> <template #overlay>
<div class="bg-gray-50 py-2 shadow-lg !border"> <div class="bg-gray-50 py-2 shadow-lg !border">
<div> <div>
<div v-t="['a:actions:download-csv']" class="nc-menu-item" @click="exportFile(ExportTypes.CSV)"> <div v-e="['a:actions:download-csv']" class="nc-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadOutline class="text-gray-500" /> <MdiDownloadOutline class="text-gray-500" />
<!-- Download as CSV --> <!-- Download as CSV -->
{{ $t('activity.downloadCSV') }} {{ $t('activity.downloadCSV') }}
</div> </div>
<div v-t="['a:actions:download-excel']" class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)"> <div v-e="['a:actions:download-excel']" class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadOutline class="text-gray-500" /> <MdiDownloadOutline class="text-gray-500" />
<!-- Download as XLSX --> <!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }} {{ $t('activity.downloadExcel') }}
@ -128,7 +129,7 @@ const exportFile = async (exportType: ExportTypes) => {
<div <div
v-if="isUIAllowed('csvImport') && !isView && !isPublicView" v-if="isUIAllowed('csvImport') && !isView && !isPublicView"
v-t="['a:actions:upload-csv']" v-e="['a:actions:upload-csv']"
class="nc-menu-item" class="nc-menu-item"
:class="{ disabled: isLocked }" :class="{ disabled: isLocked }"
@click="!isLocked ? (quickImportDialog = true) : {}" @click="!isLocked ? (quickImportDialog = true) : {}"
@ -140,7 +141,7 @@ const exportFile = async (exportType: ExportTypes) => {
<div <div
v-if="isUIAllowed('sharedViewList') && !isView && !isPublicView" v-if="isUIAllowed('sharedViewList') && !isView && !isPublicView"
v-t="['a:actions:shared-view-list']" v-e="['a:actions:shared-view-list']"
class="nc-menu-item" class="nc-menu-item"
@click="sharedViewListDlg = true" @click="sharedViewListDlg = true"
> >
@ -150,7 +151,7 @@ const exportFile = async (exportType: ExportTypes) => {
</div> </div>
<div <div
v-if="isUIAllowed('webhook') && !isView && !isPublicView" v-if="isUIAllowed('webhook') && !isView && !isPublicView"
v-t="['c:actions:webhook']" v-e="['c:actions:webhook']"
class="nc-menu-item" class="nc-menu-item"
@click="showWebhookDrawer = true" @click="showWebhookDrawer = true"
> >

23
packages/nc-gui/components/smartsheet-toolbar/ShareView.vue

@ -1,9 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useClipboard } from '@vueuse/core'
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import {
import { computed, extractSdkResponseErrorMsg, useNuxtApp, useProject, useSmartsheetStoreOrThrow } from '#imports' computed,
extractSdkResponseErrorMsg,
useClipboard,
useI18n,
useNuxtApp,
useProject,
useSmartsheetStoreOrThrow,
} from '#imports'
import MdiOpenInNewIcon from '~icons/mdi/open-in-new' import MdiOpenInNewIcon from '~icons/mdi/open-in-new'
import MdiCopyIcon from '~icons/mdi/content-copy' import MdiCopyIcon from '~icons/mdi/content-copy'
@ -39,7 +45,7 @@ const allowCSVDownload = computed({
}) })
const genShareLink = async () => { const genShareLink = async () => {
shared.value = await $api.dbViewShare.create(view.value.id as string) shared.value = await $api.dbViewShare.create(view.value?.id as string)
shared.value.meta = shared.value.meta =
shared.value.meta && typeof shared.value.meta === 'string' ? JSON.parse(shared.value.meta) : shared.value.meta shared.value.meta && typeof shared.value.meta === 'string' ? JSON.parse(shared.value.meta) : shared.value.meta
passwordProtected.value = shared.value.password !== null && shared.value.password !== '' passwordProtected.value = shared.value.password !== null && shared.value.password !== ''
@ -69,7 +75,7 @@ async function saveAllowCSVDownload() {
const meta = shared.value.meta && typeof shared.value.meta === 'string' ? JSON.parse(shared.value.meta) : shared.value.meta const meta = shared.value.meta && typeof shared.value.meta === 'string' ? JSON.parse(shared.value.meta) : shared.value.meta
await $api.dbViewShare.update(shared.value.id, { await $api.dbViewShare.update(shared.value.id, {
meta, meta,
} as any) })
// Successfully updated // Successfully updated
message.success(t('msg.success.updated')) message.success(t('msg.success.updated'))
} catch (e: any) { } catch (e: any) {
@ -114,7 +120,7 @@ watch(passwordProtected, (value) => {
<div> <div>
<a-button <a-button
v-if="isUIAllowed('share-view') && !isSharedBase" v-if="isUIAllowed('share-view') && !isSharedBase"
v-t="['c:view:share']" v-e="['c:view:share']"
outlined outlined
class="nc-btn-share-view nc-toolbar-btn" class="nc-btn-share-view nc-toolbar-btn"
> >
@ -136,11 +142,10 @@ watch(passwordProtected, (value) => {
> >
<div class="share-link-box nc-share-link-box bg-primary-50"> <div class="share-link-box nc-share-link-box bg-primary-50">
<div class="flex-1 h-min text-xs">{{ sharedViewUrl }}</div> <div class="flex-1 h-min text-xs">{{ sharedViewUrl }}</div>
<!-- <v-spacer /> --> <a v-e="['c:view:share:open-url']" :href="sharedViewUrl" target="_blank">
<a v-t="['c:view:share:open-url']" :href="sharedViewUrl" target="_blank">
<MdiOpenInNewIcon class="text-sm text-gray-500 mt-2" /> <MdiOpenInNewIcon class="text-sm text-gray-500 mt-2" />
</a> </a>
<MdiCopyIcon v-t="['c:view:share:copy-url']" class="text-gray-500 text-sm cursor-pointer" @click="copyLink" /> <MdiCopyIcon v-e="['c:view:share:copy-url']" class="text-gray-500 text-sm cursor-pointer" @click="copyLink" />
</div> </div>
<a-collapse ghost> <a-collapse ghost>

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

@ -1,17 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useClipboard } from '@vueuse/core'
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import { Empty, message } from 'ant-design-vue' import { Empty, message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import { extractSdkResponseErrorMsg, onMounted, useClipboard, useI18n, useSmartsheetStoreOrThrow } from '#imports'
import { onMounted, useSmartsheetStoreOrThrow } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiVisibilityOnIcon from '~icons/mdi/visibility' import MdiVisibilityOnIcon from '~icons/mdi/visibility'
import MdiVisibilityOffIcon from '~icons/mdi/visibility-off' import MdiVisibilityOffIcon from '~icons/mdi/visibility-off'
import MdiCopyIcon from '~icons/mdi/content-copy' import MdiCopyIcon from '~icons/mdi/content-copy'
import MdiDeleteIcon from '~icons/mdi/delete-outline' import MdiDeleteIcon from '~icons/mdi/delete-outline'
const { t } = useI18n()
interface SharedViewType { interface SharedViewType {
password: string password: string
title: string title: string
@ -21,6 +16,8 @@ interface SharedViewType {
showPassword?: boolean showPassword?: boolean
} }
const { t } = useI18n()
const { $api, meta } = useSmartsheetStoreOrThrow() const { $api, meta } = useSmartsheetStoreOrThrow()
const { copy } = useClipboard() const { copy } = useClipboard()

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

@ -14,14 +14,14 @@ import {
watch, watch,
} from '#imports' } from '#imports'
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj) const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const reloadDataHook = inject(ReloadViewDataHookInj) const reloadDataHook = inject(ReloadViewDataHookInj)
const { sorts, saveOrUpdate, loadSorts, addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger()) const { sorts, saveOrUpdate, loadSorts, addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger())
const columns = computed(() => meta?.value?.columns || []) const columns = computed(() => meta.value?.columns || [])
const columnByID = computed(() => const columnByID = computed(() =>
columns.value.reduce((obj, col) => { columns.value.reduce((obj, col) => {
@ -32,7 +32,7 @@ const columnByID = computed(() =>
) )
watch( watch(
() => (view?.value as any)?.id, () => view.value?.id,
() => { () => {
loadSorts() loadSorts()
}, },
@ -43,7 +43,7 @@ watch(
<template> <template>
<a-dropdown offset-y class="" :trigger="['click']" overlay-class-name="nc-dropdown-sort-menu"> <a-dropdown offset-y class="" :trigger="['click']" overlay-class-name="nc-dropdown-sort-menu">
<div :class="{ 'nc-badge nc-active-btn': sorts?.length }"> <div :class="{ 'nc-badge nc-active-btn': sorts?.length }">
<a-button v-t="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked" <a-button v-e="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked"
><div class="flex items-center gap-1"> ><div class="flex items-center gap-1">
<MdiSort /> <MdiSort />
<!-- Sort --> <!-- Sort -->

26
packages/nc-gui/components/smartsheet-toolbar/ViewActions.vue

@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { import {
ActiveViewInj, ActiveViewInj,
IsLockedInj, IsLockedInj,
@ -8,13 +7,14 @@ import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
inject, inject,
ref, ref,
useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission, useUIPermission,
viewIcons,
} from '#imports' } from '#imports'
import { LockType } from '~/lib' import { LockType } from '~/lib'
import { viewIcons } from '~/utils'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline' import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
import MdiAccountIcon from '~icons/mdi/account' import MdiAccountIcon from '~icons/mdi/account'
import MdiAccountGroupIcon from '~icons/mdi/account-group' import MdiAccountGroupIcon from '~icons/mdi/account-group'
@ -44,7 +44,7 @@ const { isUIAllowed } = useUIPermission()
const { isSharedBase } = useProject() const { isSharedBase } = useProject()
const Icon = computed(() => { const Icon = computed(() => {
switch ((selectedView?.value as any)?.lock_type) { switch (selectedView?.value.lock_type) {
case LockType.Personal: case LockType.Personal:
return MdiAccountIcon return MdiAccountIcon
case LockType.Locked: case LockType.Locked:
@ -65,8 +65,8 @@ async function changeLockType(type: LockType) {
return message.info(t('msg.toast.futureRelease')) return message.info(t('msg.toast.futureRelease'))
} }
try { try {
;(selectedView.value as any).lock_type = type selectedView.value.lock_type = type
$api.dbView.update(selectedView.value.id as string, { await $api.dbView.update(selectedView.value.id as string, {
lock_type: type, lock_type: type,
}) })
@ -82,7 +82,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<template> <template>
<div> <div>
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu"> <a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu">
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> <a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<component <component
:is="viewIcons[selectedView?.type].icon" :is="viewIcons[selectedView?.type].icon"
@ -106,7 +106,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0" class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0"
> >
<template #title> <template #title>
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group px-0 !py-0"> <div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group px-0 !py-0">
<SmartsheetToolbarLockType hide-tick :type="selectedView?.lock_type || LockType.Collaborative" /> <SmartsheetToolbarLockType hide-tick :type="selectedView?.lock_type || LockType.Collaborative" />
<MaterialSymbolsChevronRightRounded <MaterialSymbolsChevronRightRounded
@ -130,7 +130,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<a-sub-menu key="download"> <a-sub-menu key="download">
<template #title> <template #title>
<!-- Download --> <!-- Download -->
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group"> <div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiDownload class="group-hover:text-accent text-gray-500" /> <MdiDownload class="group-hover:text-accent text-gray-500" />
{{ $t('general.download') }} {{ $t('general.download') }}
<div class="flex-1" /> <div class="flex-1" />
@ -148,7 +148,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<a-sub-menu key="upload"> <a-sub-menu key="upload">
<!-- Upload --> <!-- Upload -->
<template #title> <template #title>
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group"> <div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiUpload class="group-hover:text-accent text-gray-500" /> <MdiUpload class="group-hover:text-accent text-gray-500" />
{{ $t('general.upload') }} {{ $t('general.upload') }}
<div class="flex-1" /> <div class="flex-1" />
@ -163,7 +163,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<a-menu-item> <a-menu-item>
<div <div
v-if="isUIAllowed('csvImport') && !isView && !isPublicView" v-if="isUIAllowed('csvImport') && !isView && !isPublicView"
v-t="['a:actions:upload-csv']" v-e="['a:actions:upload-csv']"
class="nc-project-menu-item" class="nc-project-menu-item"
:class="{ disabled: isLocked }" :class="{ disabled: isLocked }"
@click="!isLocked ? (quickImportDialog = true) : {}" @click="!isLocked ? (quickImportDialog = true) : {}"
@ -180,7 +180,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<a-menu-item> <a-menu-item>
<div <div
v-if="isUIAllowed('SharedViewList') && !isView && !isPublicView" v-if="isUIAllowed('SharedViewList') && !isView && !isPublicView"
v-t="['a:actions:shared-view-list']" v-e="['a:actions:shared-view-list']"
class="py-2 flex gap-2 items-center" class="py-2 flex gap-2 items-center"
@click="sharedViewListDlg = true" @click="sharedViewListDlg = true"
> >
@ -192,7 +192,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<a-menu-item v-if="!isSqlView"> <a-menu-item v-if="!isSqlView">
<div <div
v-if="isUIAllowed('webhook') && !isView && !isPublicView" v-if="isUIAllowed('webhook') && !isView && !isPublicView"
v-t="['c:actions:webhook']" v-e="['c:actions:webhook']"
class="py-2 flex gap-2 items-center" class="py-2 flex gap-2 items-center"
@click="showWebhookDrawer = true" @click="showWebhookDrawer = true"
> >
@ -203,7 +203,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<a-menu-item> <a-menu-item>
<div <div
v-if="!isSharedBase && !isPublicView" v-if="!isSharedBase && !isPublicView"
v-t="['c:snippet:open']" v-e="['c:snippet:open']"
class="py-2 flex gap-2 items-center" class="py-2 flex gap-2 items-center"
@click="showApiSnippetDrawer = true" @click="showApiSnippetDrawer = true"
> >

26
packages/nc-gui/components/smartsheet/ApiSnippet.vue

@ -1,10 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import HTTPSnippet from 'httpsnippet' import HTTPSnippet from 'httpsnippet'
import { useClipboard } from '@vueuse/core'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import {
import { ActiveViewInj, MetaInj } from '~/context' ActiveViewInj,
import { inject, useGlobal, useProject, useSmartsheetStoreOrThrow, useVModel, useViewData } from '#imports' MetaInj,
inject,
useClipboard,
useGlobal,
useI18n,
useProject,
useSmartsheetStoreOrThrow,
useVModel,
useViewData,
} from '#imports'
const props = defineProps<Props>() const props = defineProps<Props>()
@ -20,13 +28,13 @@ const { project } = $(useProject())
const { appInfo, token } = $(useGlobal()) const { appInfo, token } = $(useGlobal())
const meta = $(inject(MetaInj)!) const meta = $(inject(MetaInj, ref()))
const view = $(inject(ActiveViewInj)!) const view = $(inject(ActiveViewInj, ref()))
const { xWhere } = useSmartsheetStoreOrThrow() const { xWhere } = useSmartsheetStoreOrThrow()
const { queryParams } = $(useViewData($$(meta), view as any, xWhere)) const { queryParams } = $(useViewData($$(meta), $$(view), xWhere))
const { copy } = useClipboard() const { copy } = useClipboard()
@ -169,7 +177,7 @@ watch($$(activeLang), (newLang) => {
</a-select-option> </a-select-option>
</a-select> </a-select>
<a-button <a-button
v-t="[ v-e="[
'c:snippet:copy', 'c:snippet:copy',
{ client: activeLang?.clients && (selectedClient || activeLang?.clients[0]), lang: activeLang?.name }, { client: activeLang?.clients && (selectedClient || activeLang?.clients[0]), lang: activeLang?.name },
]" ]"
@ -181,7 +189,7 @@ watch($$(activeLang), (newLang) => {
<div class="absolute bottom-4 flex flex-row justify-center w-[95%]"> <div class="absolute bottom-4 flex flex-row justify-center w-[95%]">
<a <a
v-t="['e:hiring']" v-e="['e:hiring']"
class="px-4 py-2 ! rounded shadow" class="px-4 py-2 ! rounded shadow"
href="https://angel.co/company/nocodb" href="https://angel.co/company/nocodb"
target="_blank" target="_blank"

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

@ -2,8 +2,6 @@
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { RelationTypes, UITypes, getSystemColumns, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, getSystemColumns, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import type { Permission } from '~/composables/useUIPermission/rolePermissions'
import { import {
ActiveViewInj, ActiveViewInj,
IsFormInj, IsFormInj,
@ -18,12 +16,14 @@ import {
ref, ref,
useDebounceFn, useDebounceFn,
useGlobal, useGlobal,
useI18n,
useNuxtApp, useNuxtApp,
useUIPermission, useUIPermission,
useViewColumns, useViewColumns,
useViewData, useViewData,
watch, watch,
} from '#imports' } from '#imports'
import type { Permission } from '~/composables/useUIPermission/rolePermissions'
provide(IsFormInj, ref(true)) provide(IsFormInj, ref(true))
provide(IsGalleryInj, ref(false)) provide(IsGalleryInj, ref(false))
@ -45,11 +45,11 @@ const secondsRemain = ref(0)
const isEditable = isUIAllowed('editFormView' as Permission) const isEditable = isUIAllowed('editFormView' as Permission)
const meta = inject(MetaInj)! const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj) const view = inject(ActiveViewInj, ref())
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view as any) const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view)
const reloadEventHook = createEventHook<void>() const reloadEventHook = createEventHook<void>()
provide(ReloadViewDataHookInj, reloadEventHook) provide(ReloadViewDataHookInj, reloadEventHook)
@ -59,7 +59,7 @@ reloadEventHook.on(async () => {
setFormData() setFormData()
}) })
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta as any, async () => reloadEventHook.trigger()) const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta, async () => reloadEventHook.trigger())
const { syncLTARRefs, row } = useProvideSmartsheetRowStore( const { syncLTARRefs, row } = useProvideSmartsheetRowStore(
meta, meta,
@ -252,10 +252,9 @@ function setFormData() {
formViewData.value = { formViewData.value = {
...formViewData.value, ...formViewData.value,
submit_another_form: !!(formViewData?.value?.submit_another_form ?? 0), submit_another_form: !!(formViewData.value?.submit_another_form ?? 0),
// todo: show_blank_form missing from FormType show_blank_form: !!(formViewData.value?.show_blank_form ?? 0),
show_blank_form: !!((formViewData?.value as any)?.show_blank_form ?? 0), }
} as any
// email me // email me
let data: Record<string, boolean> = {} let data: Record<string, boolean> = {}
@ -309,7 +308,7 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
function updateEmail() { function updateEmail() {
try { try {
const data = JSON.parse(formViewData.value?.email) || {} const data = formViewData.value?.email ? JSON.parse(formViewData.value?.email) : {}
data[state.user.value?.email as string] = emailMe.value data[state.user.value?.email as string] = emailMe.value
formViewData.value!.email = JSON.stringify(data) formViewData.value!.email = JSON.stringify(data)
checkSMTPStatus() checkSMTPStatus()
@ -338,7 +337,7 @@ const updateColMeta = useDebounceFn(async (col: Record<string, any>) => {
}, 250) }, 250)
watch(submitted, (v) => { watch(submitted, (v) => {
if (v && (formViewData?.value as any)?.show_blank_form) { if (v && formViewData.value?.show_blank_form) {
secondsRemain.value = 5 secondsRemain.value = 5
const intvl = setInterval(() => { const intvl = setInterval(() => {
if (--secondsRemain.value < 0) { if (--secondsRemain.value < 0) {
@ -591,7 +590,7 @@ onMounted(async () => {
<a-switch <a-switch
v-model:checked="element.required" v-model:checked="element.required"
v-t="['a:form-view:field:mark-required']" v-e="['a:form-view:field:mark-required']"
size="small" size="small"
class="ml-2" class="ml-2"
@change="updateColMeta(element)" @change="updateColMeta(element)"
@ -715,7 +714,7 @@ onMounted(async () => {
<!-- Show "Submit Another Form" button --> <!-- Show "Submit Another Form" button -->
<a-switch <a-switch
v-model:checked="formViewData.submit_another_form" v-model:checked="formViewData.submit_another_form"
v-t="[`a:form-view:submit-another-form`]" v-e="[`a:form-view:submit-another-form`]"
size="small" size="small"
class="nc-form-checkbox-submit-another-form" class="nc-form-checkbox-submit-another-form"
@change="updateView" @change="updateView"
@ -727,7 +726,7 @@ onMounted(async () => {
<!-- Show a blank form after 5 seconds --> <!-- Show a blank form after 5 seconds -->
<a-switch <a-switch
v-model:checked="formViewData.show_blank_form" v-model:checked="formViewData.show_blank_form"
v-t="[`a:form-view:show-blank-form`]" v-e="[`a:form-view:show-blank-form`]"
size="small" size="small"
class="nc-form-checkbox-show-blank-form" class="nc-form-checkbox-show-blank-form"
@change="updateView" @change="updateView"
@ -738,7 +737,7 @@ onMounted(async () => {
<div class="my-4"> <div class="my-4">
<a-switch <a-switch
v-model:checked="emailMe" v-model:checked="emailMe"
v-t="[`a:form-view:email-me`]" v-e="[`a:form-view:email-me`]"
size="small" size="small"
class="nc-form-checkbox-send-email" class="nc-form-checkbox-send-email"
@change="onEmailChange" @change="onEmailChange"

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

@ -1,8 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk' import { isVirtualCol } from 'nocodb-sdk'
import { inject, provide, useViewData } from '#imports'
import Row from '~/components/smartsheet/Row.vue'
import type { Row as RowType } from '~/composables'
import { import {
ActiveViewInj, ActiveViewInj,
ChangePageInj, ChangePageInj,
@ -14,15 +11,21 @@ import {
OpenNewRecordFormHookInj, OpenNewRecordFormHookInj,
PaginationDataInj, PaginationDataInj,
ReadonlyInj, ReadonlyInj,
} from '~/context' ReloadViewDataHookInj,
inject,
provide,
useViewData,
} from '#imports'
import Row from '~/components/smartsheet/Row.vue'
import type { Row as RowType } from '~/composables'
import ImageIcon from '~icons/mdi/file-image-box' import ImageIcon from '~icons/mdi/file-image-box'
interface Attachment { interface Attachment {
url: string url: string
} }
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj) const view = inject(ActiveViewInj, ref())
const reloadViewDataHook = inject(ReloadViewDataHookInj) const reloadViewDataHook = inject(ReloadViewDataHookInj)
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook()) const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
@ -38,7 +41,7 @@ const {
galleryData, galleryData,
changePage, changePage,
addEmptyRow, addEmptyRow,
} = useViewData(meta, view as any) } = useViewData(meta, view)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
@ -55,7 +58,7 @@ const fieldsWithoutCover = computed(() => fields.value.filter((f) => f.id !== ga
const coverImageColumn: any = $( const coverImageColumn: any = $(
computed(() => computed(() =>
meta?.value.columnsById meta.value?.columnsById
? meta.value.columnsById[galleryData.value?.fk_cover_image_col_id as keyof typeof meta.value.columnsById] ? meta.value.columnsById[galleryData.value?.fk_cover_image_col_id as keyof typeof meta.value.columnsById]
: {}, : {},
), ),
@ -144,7 +147,7 @@ openNewRecordFormHook?.on(async () => {
<img <img
v-for="(attachment, index) in attachments(record)" v-for="(attachment, index) in attachments(record)"
:key="`carousel-${record.row.id}-${index}`" :key="`carousel-${record.row.id}-${index}`"
class="h-52" class="h-52 object-cover"
:src="attachment.url" :src="attachment.url"
/> />
</a-carousel> </a-carousel>
@ -192,14 +195,17 @@ openNewRecordFormHook?.on(async () => {
grid-auto-rows: 1fr; grid-auto-rows: 1fr;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
} }
:depp(.slick-dots li button) {
:deep(.slick-dots li button) {
background-color: black; background-color: black;
} }
.ant-carousel.gallery-carousel :deep(.slick-dots) { .ant-carousel.gallery-carousel :deep(.slick-dots) {
position: relative; position: relative;
height: auto; height: auto;
bottom: 0px; bottom: 0px;
} }
.ant-carousel.gallery-carousel :deep(.slick-dots li div > div) { .ant-carousel.gallery-carousel :deep(.slick-dots li div > div) {
background: #000; background: #000;
border: 0; border: 0;
@ -215,15 +221,18 @@ openNewRecordFormHook?.on(async () => {
transition: all 0.5s; transition: all 0.5s;
width: 100%; width: 100%;
} }
.ant-carousel.gallery-carousel :deep(.slick-dots li.slick-active div > div) { .ant-carousel.gallery-carousel :deep(.slick-dots li.slick-active div > div) {
opacity: 1; opacity: 1;
} }
.ant-carousel.gallery-carousel :deep(.slick-prev) { .ant-carousel.gallery-carousel :deep(.slick-prev) {
left: 0; left: 0;
height: 100%; height: 100%;
top: 12px; top: 12px;
width: 50%; width: 50%;
} }
.ant-carousel.gallery-carousel :deep(.slick-next) { .ant-carousel.gallery-carousel :deep(.slick-next) {
right: 0; right: 0;
height: 100%; height: 100%;

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

@ -2,9 +2,9 @@
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { import {
ActiveViewInj, ActiveViewInj,
CellUrlDisableOverlayInj,
ChangePageInj, ChangePageInj,
FieldsInj, FieldsInj,
IsFormInj, IsFormInj,
@ -17,7 +17,6 @@ import {
ReadonlyInj, ReadonlyInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,
createEventHook, createEventHook,
enumColor,
inject, inject,
onClickOutside, onClickOutside,
onMounted, onMounted,
@ -26,6 +25,7 @@ import {
ref, ref,
useEventListener, useEventListener,
useGridViewColumnWidth, useGridViewColumnWidth,
useI18n,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission, useUIPermission,
useViewData, useViewData,
@ -36,9 +36,9 @@ import { NavigateDir } from '~/lib'
const { t } = useI18n() const { t } = useI18n()
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj) const view = inject(ActiveViewInj, ref())
// keep a root fields variable and will get modified from // keep a root fields variable and will get modified from
// fields menu and get used in grid and gallery // fields menu and get used in grid and gallery
@ -92,9 +92,10 @@ const {
deleteSelectedRows, deleteSelectedRows,
selectedAllRecords, selectedAllRecords,
removeRowIfNew, removeRowIfNew,
} = useViewData(meta, view as any, xWhere) } = useViewData(meta, view, xWhere)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view as any)
onMounted(loadGridViewColumns) onMounted(loadGridViewColumns)
provide(IsFormInj, ref(false)) provide(IsFormInj, ref(false))
@ -109,6 +110,9 @@ provide(ChangePageInj, changePage)
provide(ReadonlyInj, !hasEditPermission) provide(ReadonlyInj, !hasEditPermission)
const disableUrlOverlay = ref(false)
provide(CellUrlDisableOverlayInj, disableUrlOverlay)
reloadViewDataHook?.on(async () => { reloadViewDataHook?.on(async () => {
await loadData() await loadData()
}) })
@ -133,9 +137,9 @@ const selectCell = (row: number, col: number) => {
} }
watch( watch(
() => (view?.value as any)?.id, () => view.value?.id,
async (n?: string, o?: string) => { async (next, old) => {
if (n && o && n !== o) { if (next && old && next !== old) {
await loadData() await loadData()
} }
}, },
@ -201,6 +205,10 @@ const makeEditable = (row: Row, col: ColumnType) => {
/** handle keypress events */ /** handle keypress events */
const onKeyDown = async (e: KeyboardEvent) => { const onKeyDown = async (e: KeyboardEvent) => {
if (e.key === 'Alt') {
disableUrlOverlay.value = true
return
}
if (selected.row === null || selected.col === null) return if (selected.row === null || selected.col === null) return
/** on tab key press navigate through cells */ /** on tab key press navigate through cells */
switch (e.key) { switch (e.key) {
@ -284,8 +292,14 @@ const onKeyDown = async (e: KeyboardEvent) => {
break break
} }
} }
const onKeyUp = async (e: KeyboardEvent) => {
if (e.key === 'Alt') {
disableUrlOverlay.value = false
}
}
useEventListener(document, 'keydown', onKeyDown) useEventListener(document, 'keydown', onKeyDown)
useEventListener(document, 'keyup', onKeyUp)
/** On clicking outside of table reset active cell */ /** On clicking outside of table reset active cell */
const smartTable = ref(null) const smartTable = ref(null)
@ -346,7 +360,7 @@ onBeforeUnmount(async () => {
/** if existing row check updated cell and invoke update method */ /** if existing row check updated cell and invoke update method */
if (currentRow.rowMeta.changed) { if (currentRow.rowMeta.changed) {
currentRow.rowMeta.changed = false currentRow.rowMeta.changed = false
for (const field of meta?.value.columns ?? []) { for (const field of meta.value?.columns ?? []) {
if (isVirtualCol(field)) continue if (isVirtualCol(field)) continue
if (currentRow.row[field.title!] !== currentRow.oldRow[field.title!]) { if (currentRow.row[field.title!] !== currentRow.oldRow[field.title!]) {
await updateOrSaveRow(currentRow, field.title!) await updateOrSaveRow(currentRow, field.title!)
@ -411,7 +425,7 @@ onBeforeUnmount(async () => {
</th> </th>
<th <th
v-if="!readOnly && !isLocked && isUIAllowed('add-column') && !isSqlView" v-if="!readOnly && !isLocked && isUIAllowed('add-column') && !isSqlView"
v-t="['c:column:add']" v-e="['c:column:add']"
class="cursor-pointer" class="cursor-pointer"
@click.stop="addColumnDropdown = true" @click.stop="addColumnDropdown = true"
> >
@ -472,7 +486,7 @@ onBeforeUnmount(async () => {
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)" class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)"
> >
<MdiArrowExpand <MdiArrowExpand
v-t="['c:row-expand']" v-e="['c:row-expand']"
class="select-none transform hover:(text-accent scale-120) nc-row-expand" class="select-none transform hover:(text-accent scale-120) nc-row-expand"
@click="expandForm(row, state)" @click="expandForm(row, state)"
/> />
@ -530,7 +544,7 @@ onBeforeUnmount(async () => {
<tr v-if="!isView && !isLocked && isUIAllowed('xcDatatableEditable') && !isSqlView"> <tr v-if="!isView && !isLocked && isUIAllowed('xcDatatableEditable') && !isSqlView">
<td <td
v-t="['c:row:add:grid-bottom']" v-e="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1" :colspan="visibleColLength + 1"
class="text-left pointer nc-grid-add-new-cell cursor-pointer" class="text-left pointer nc-grid-add-new-cell cursor-pointer"
@click="addEmptyRow()" @click="addEmptyRow()"
@ -550,14 +564,14 @@ onBeforeUnmount(async () => {
<template v-if="!isLocked && isUIAllowed('xcDatatableEditable')" #overlay> <template v-if="!isLocked && isUIAllowed('xcDatatableEditable')" #overlay>
<a-menu class="shadow !rounded !py-0" @click="contextMenu = false"> <a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
<a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)"> <a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)">
<div v-t="['a:row:delete']" class="nc-project-menu-item"> <div v-e="['a:row:delete']" class="nc-project-menu-item">
<!-- Delete Row --> <!-- Delete Row -->
{{ $t('activity.deleteRow') }} {{ $t('activity.deleteRow') }}
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item @click="deleteSelectedRows"> <a-menu-item @click="deleteSelectedRows">
<div v-t="['a:row:delete-bulk']" class="nc-project-menu-item"> <div v-e="['a:row:delete-bulk']" class="nc-project-menu-item">
<!-- Delete Selected Rows --> <!-- Delete Selected Rows -->
{{ $t('activity.deleteSelectedRow') }} {{ $t('activity.deleteSelectedRow') }}
</div> </div>
@ -565,11 +579,11 @@ onBeforeUnmount(async () => {
<!-- Clear cell --> <!-- Clear cell -->
<a-menu-item v-if="contextMenuTarget" @click="clearCell(contextMenuTarget)"> <a-menu-item v-if="contextMenuTarget" @click="clearCell(contextMenuTarget)">
<div v-t="['a:row:clear']" class="nc-project-menu-item">{{ $t('activity.clearCell') }}</div> <div v-e="['a:row:clear']" class="nc-project-menu-item">{{ $t('activity.clearCell') }}</div>
</a-menu-item> </a-menu-item>
<a-menu-item v-if="contextMenuTarget" @click="addEmptyRow(contextMenuTarget.row + 1)"> <a-menu-item v-if="contextMenuTarget" @click="addEmptyRow(contextMenuTarget.row + 1)">
<div v-t="['a:row:insert']" class="nc-project-menu-item"> <div v-e="['a:row:insert']" class="nc-project-menu-item">
<!-- Insert New Row --> <!-- Insert New Row -->
{{ $t('activity.insertRow') }} {{ $t('activity.insertRow') }}
</div> </div>

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

@ -57,7 +57,7 @@ watch(
<div class="p-0"> <div class="p-0">
<div class="flex justify-center"> <div class="flex justify-center">
<!-- Comments only --> <!-- Comments only -->
<a-checkbox v-model:checked="commentsOnly" v-t="['c:row-expand:comment-only']" @change="loadCommentsAndLogs" <a-checkbox v-model:checked="commentsOnly" v-e="['c:row-expand:comment-only']" @change="loadCommentsAndLogs"
>{{ $t('labels.commentsOnly') }}<span class="text-[11px] text-gray-500"></span> >{{ $t('labels.commentsOnly') }}<span class="text-[11px] text-gray-500"></span>
</a-checkbox> </a-checkbox>
</div> </div>

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

@ -64,7 +64,7 @@ const iconColor = '#1890ff'
</template> </template>
<MdiCommentTextOutline <MdiCommentTextOutline
v-if="isUIAllowed('rowComments') && !isNew" v-if="isUIAllowed('rowComments') && !isNew"
v-t="['c:row-expand:comment-toggle']" v-e="['c:row-expand:comment-toggle']"
class="cursor-pointer select-none nc-toggle-comments text-gray-500" class="cursor-pointer select-none nc-toggle-comments text-gray-500"
@click="commentsDrawer = !commentsDrawer" @click="commentsDrawer = !commentsDrawer"
/> />

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

@ -5,9 +5,7 @@ import type { Menu as AntMenu } from 'ant-design-vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import { useI18n } from 'vue-i18n'
import RenameableMenuItem from './RenameableMenuItem.vue' import RenameableMenuItem from './RenameableMenuItem.vue'
import { useNuxtApp } from '#app'
import { import {
ActiveViewInj, ActiveViewInj,
ViewListInj, ViewListInj,
@ -17,6 +15,8 @@ import {
ref, ref,
useApi, useApi,
useDialog, useDialog,
useI18n,
useNuxtApp,
useRoute, useRoute,
useRouter, useRouter,
viewTypeAlias, viewTypeAlias,
@ -40,7 +40,7 @@ const { $e } = useNuxtApp()
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
const views = inject<Ref<any[]>>(ViewListInj, ref([])) const views = inject<Ref<ViewType[]>>(ViewListInj, ref([]))
const { api } = useApi() const { api } = useApi()
@ -76,7 +76,7 @@ function markItem(id: string) {
} }
/** validate view title */ /** validate view title */
function validate(view: Record<string, any>) { function validate(view: ViewType) {
if (!view.title || view.title.trim().length < 0) { if (!view.title || view.title.trim().length < 0) {
return 'View name is required' return 'View name is required'
} }
@ -110,28 +110,32 @@ async function onSortEnd(evt: SortableEvent) {
const previousEl = children[newIndex - 1] const previousEl = children[newIndex - 1]
const nextEl = children[newIndex + 1] const nextEl = children[newIndex + 1]
const currentItem: Record<string, any> = views.value.find((v) => v.id === evt.item.id) const currentItem = views.value.find((v) => v.id === evt.item.id)
const previousItem: Record<string, any> = previousEl ? views.value.find((v) => v.id === previousEl.id) : {}
const nextItem: Record<string, any> = nextEl ? views.value.find((v) => v.id === nextEl.id) : {} if (!currentItem || !currentItem.id) return
const previousItem = (previousEl ? views.value.find((v) => v.id === previousEl.id) : {}) as ViewType
const nextItem = (nextEl ? views.value.find((v) => v.id === nextEl.id) : {}) as ViewType
let nextOrder: number let nextOrder: number
// set new order value based on the new order of the items // set new order value based on the new order of the items
if (views.value.length - 1 === newIndex) { if (views.value.length - 1 === newIndex) {
nextOrder = parseFloat(previousItem.order) + 1 nextOrder = parseFloat(String(previousItem.order)) + 1
} else if (newIndex === 0) { } else if (newIndex === 0) {
nextOrder = parseFloat(nextItem.order) / 2 nextOrder = parseFloat(String(nextItem.order)) / 2
} else { } else {
nextOrder = (parseFloat(previousItem.order) + parseFloat(nextItem.order)) / 2 nextOrder = (parseFloat(String(previousItem.order)) + parseFloat(String(nextItem.order))) / 2
} }
const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder.toString() : oldIndex.toString() const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder : oldIndex
currentItem.order = _nextOrder currentItem.order = _nextOrder
await api.dbView.update(currentItem.id, { order: _nextOrder }) await api.dbView.update(currentItem.id, { order: _nextOrder })
markItem(currentItem.id) markItem(currentItem.id)
$e('a:view:reorder') $e('a:view:reorder')
} }
@ -168,7 +172,7 @@ async function onRename(view: ViewType) {
try { try {
await api.dbView.update(view.id!, { await api.dbView.update(view.id!, {
title: view.title, title: view.title,
order: String(view.order), order: view.order,
}) })
await router.replace({ await router.replace({

24
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -1,19 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ViewTypes } from 'nocodb-sdk' import type { ViewType, ViewTypes } from 'nocodb-sdk'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { viewIcons } from '~/utils' import type { WritableComputedRef } from '@vue/reactivity'
import { IsLockedInj, onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel } from '#imports' import { IsLockedInj, onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel, viewIcons } from '#imports'
interface Props { interface Props {
view: Record<string, any> view: ViewType
onValidate: (view: Record<string, any>) => boolean | string onValidate: (view: ViewType) => boolean | string
} }
interface Emits { interface Emits {
(event: 'update:view', data: Record<string, any>): void (event: 'update:view', data: Record<string, any>): void
(event: 'changeView', view: Record<string, any>): void (event: 'changeView', view: Record<string, any>): void
(event: 'rename', view: Record<string, any>): void (event: 'rename', view: ViewType): void
(event: 'delete', view: Record<string, any>): void (event: 'delete', view: ViewType): void
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string }): void (event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string }): void
} }
@ -21,7 +21,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const vModel = useVModel(props, 'view', emits) const vModel = useVModel(props, 'view', emits) as WritableComputedRef<any>
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -92,7 +92,7 @@ function focusInput(el: HTMLInputElement) {
/** Duplicate a view */ /** Duplicate a view */
// todo: This is not really a duplication, maybe we need to implement a true duplication? // todo: This is not really a duplication, maybe we need to implement a true duplication?
function onDuplicate() { function onDuplicate() {
emits('openModal', { type: vModel.value.type, title: vModel.value.title, copyViewId: vModel.value.id }) emits('openModal', { type: vModel.value.type!, title: vModel.value.title, copyViewId: vModel.value.id })
$e('c:view:copy', { view: vModel.value.type }) $e('c:view:copy', { view: vModel.value.type })
} }
@ -129,7 +129,7 @@ async function onRename() {
function onCancel() { function onCancel() {
if (!isEditing) return if (!isEditing) return
vModel.value.title = originalTitle vModel.value.title = originalTitle || ''
onStopEdit() onStopEdit()
} }
@ -151,7 +151,7 @@ function onStopEdit() {
@dblclick.stop="isUIAllowed('virtualViewsCreateOrEdit') && onDblClick()" @dblclick.stop="isUIAllowed('virtualViewsCreateOrEdit') && onDblClick()"
@click.stop="onClick" @click.stop="onClick"
> >
<div v-t="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2"> <div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2">
<div class="flex w-auto"> <div class="flex w-auto">
<MdiDrag <MdiDrag
class="nc-drag-icon hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 !cursor-move" class="nc-drag-icon hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 !cursor-move"
@ -161,7 +161,7 @@ function onStopEdit() {
<component <component
:is="viewIcons[vModel.type].icon" :is="viewIcons[vModel.type].icon"
class="nc-view-icon group-hover:hidden" class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[vModel?.type]?.color }" :style="{ color: viewIcons[vModel.type].color }"
/> />
</div> </div>

2
packages/nc-gui/components/smartsheet/sidebar/index.vue

@ -34,7 +34,7 @@ const { $e } = useNuxtApp()
provide(ViewListInj, views) provide(ViewListInj, views)
/** Sidebar visible */ /** Sidebar visible */
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' }) const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar', isOpen: true })
const sidebarCollapsed = computed(() => !isOpen.value) const sidebarCollapsed = computed(() => !isOpen.value)

2
packages/nc-gui/components/smartsheet/sidebar/toolbar/DeleteTable.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { MetaInj, inject, useTable } from '#imports' import { MetaInj, inject, useTable } from '#imports'
const meta = inject(MetaInj)! const meta = inject(MetaInj, ref())
const { deleteTable } = useTable() const { deleteTable } = useTable()

2
packages/nc-gui/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
/** Sidebar visible */ /** Sidebar visible */
const { isOpen, toggle } = useSidebar({ storageKey: 'nc-right-sidebar' }) const { isOpen, toggle } = useSidebar({ storageKey: 'nc-right-sidebar', isOpen: true })
</script> </script>
<template> <template>

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

@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'
import UserManagement from './auth/UserManagement.vue' import UserManagement from './auth/UserManagement.vue'
import ApiTokenManagement from './auth/ApiTokenManagement.vue' import ApiTokenManagement from './auth/ApiTokenManagement.vue'
import { useUIPermission } from '#imports' import { useI18n, useUIPermission } from '#imports'
interface Tab { interface Tab {
title: string title: string
@ -10,6 +9,7 @@ interface Tab {
body: any body: any
isUIAllowed: boolean isUIAllowed: boolean
} }
const { t } = useI18n() const { t } = useI18n()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
@ -29,8 +29,6 @@ const tabsInfo: Tab[] = [
}, },
] ]
// const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
const selectedTabKey = $ref(0) const selectedTabKey = $ref(0)
const selectedTab = $computed(() => tabsInfo[selectedTabKey]) const selectedTab = $computed(() => tabsInfo[selectedTabKey])
</script> </script>

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

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk' import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import SmartsheetGrid from '../smartsheet/Grid.vue' import SmartsheetGrid from '../smartsheet/Grid.vue'
import { import {
ActiveViewInj, ActiveViewInj,
@ -18,7 +17,6 @@ import {
useProvideSmartsheetStore, useProvideSmartsheetStore,
watch, watch,
} from '#imports' } from '#imports'
import type { TabItem } from '~/composables' import type { TabItem } from '~/composables'
const { activeTab } = defineProps<{ const { activeTab } = defineProps<{
@ -39,10 +37,10 @@ const meta = computed<TableType>(() => metas.value?.[activeTab?.id as string])
const reloadEventHook = createEventHook<void>() const reloadEventHook = createEventHook<void>()
const openNewRecordFormHook = createEventHook<void>() const openNewRecordFormHook = createEventHook<void>()
const { isGallery, isGrid, isForm, isLocked } = useProvideSmartsheetStore(activeView as Ref<TableType>, meta) const { isGallery, isGrid, isForm, isLocked } = useProvideSmartsheetStore(activeView, meta)
// provide the sidebar injection state // provide the sidebar injection state
provideSidebar({ storageKey: 'nc-right-sidebar' }) provideSidebar({ storageKey: 'nc-right-sidebar', isOpen: true })
// todo: move to store // todo: move to store
provide(MetaInj, meta) provide(MetaInj, meta)

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

@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ApiTokenType } from 'nocodb-sdk' import type { ApiTokenType } from 'nocodb-sdk'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useClipboard } from '@vueuse/core' import { extractSdkResponseErrorMsg, useClipboard, useI18n } from '#imports'
import { useI18n } from 'vue-i18n'
import KebabIcon from '~icons/ic/baseline-more-vert' import KebabIcon from '~icons/ic/baseline-more-vert'
import MdiPlusIcon from '~icons/mdi/plus' import MdiPlusIcon from '~icons/mdi/plus'
import CloseIcon from '~icons/material-symbols/close-rounded' import CloseIcon from '~icons/material-symbols/close-rounded'
@ -11,13 +10,13 @@ import VisibilityOpenIcon from '~icons/material-symbols/visibility'
import VisibilityCloseIcon from '~icons/material-symbols/visibility-off' import VisibilityCloseIcon from '~icons/material-symbols/visibility-off'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline' import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import MdiContentCopyIcon from '~icons/mdi/content-copy' import MdiContentCopyIcon from '~icons/mdi/content-copy'
import { extractSdkResponseErrorMsg } from '~/utils'
const { t } = useI18n()
interface ApiToken extends ApiTokenType { interface ApiToken extends ApiTokenType {
show?: boolean show?: boolean
} }
const { t } = useI18n()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { project } = $(useProject()) const { project } = $(useProject())

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

@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import type { RequestParams } from 'nocodb-sdk'
import UsersModal from './user-management/UsersModal.vue' import UsersModal from './user-management/UsersModal.vue'
import FeedbackForm from './user-management/FeedbackForm.vue' import FeedbackForm from './user-management/FeedbackForm.vue'
import { import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
onMounted, onBeforeMount,
projectRoleTagColors,
ref, ref,
useApi, useApi,
useClipboard, useClipboard,
useDashboard, useDashboard,
useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
useUIPermission, useUIPermission,
@ -61,7 +61,7 @@ const loadUsers = async (page = currentPage, limit = currentLimit) => {
offset: searchText.value.length === 0 ? (page - 1) * limit : 0, offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
query: searchText.value, query: searchText.value,
}, },
} as any) } as RequestParams)
if (!response.users) return if (!response.users) return
totalRows = response.users.pageInfo.totalRows ?? 0 totalRows = response.users.pageInfo.totalRows ?? 0
@ -150,11 +150,13 @@ const copyInviteUrl = (user: User) => {
$e('c:user:copy-url') $e('c:user:copy-url')
} }
onMounted(() => { onBeforeMount(async () => {
if (!users) { if (!users) {
isLoading = true isLoading = true
loadUsers().finally(() => (isLoading = false)) await loadUsers()
isLoading = false
} }
}) })
@ -162,9 +164,10 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</script> </script>
<template> <template>
<div v-if="isLoading" class="h-full w-full flex flex-row justify-center mt-42"> <div v-if="isLoading" class="h-full w-full flex items-center justify-center">
<a-spin size="large" /> <a-spin size="large" />
</div> </div>
<div v-else class="flex flex-col w-full px-6"> <div v-else class="flex flex-col w-full px-6">
<UsersModal <UsersModal
:key="showUserModal" :key="showUserModal"
@ -201,7 +204,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div> </div>
<div class="flex flex-row space-x-1"> <div class="flex flex-row space-x-1">
<a-button v-t="['a:user:reload']" size="middle" type="text" @click="loadUsers()"> <a-button v-e="['a:user:reload']" size="middle" type="text" @click="loadUsers()">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1"> <div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MdiReload class="text-gray-500" /> <MdiReload class="text-gray-500" />
<div class="text-gray-500">{{ $t('general.reload') }}</div> <div class="text-gray-500">{{ $t('general.reload') }}</div>
@ -209,7 +212,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</a-button> </a-button>
<a-button <a-button
v-if="isUIAllowed('newUser')" v-if="isUIAllowed('newUser')"
v-t="['c:user:invite']" v-e="['c:user:invite']"
size="middle" size="middle"
type="primary" type="primary"
ghost ghost
@ -282,7 +285,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<template #title> <template #title>
<span>{{ $t('activity.deleteUser') }}</span> <span>{{ $t('activity.deleteUser') }}</span>
</template> </template>
<a-button v-t="['c:user:delete']" type="text" class="!rounded-md nc-user-delete" @click="onDelete(user)"> <a-button v-e="['c:user:delete']" type="text" class="!rounded-md nc-user-delete" @click="onDelete(user)">
<template #icon> <template #icon>
<MdiDeleteOutline class="flex mx-auto h-[1.1rem] text-gray-500" /> <MdiDeleteOutline class="flex mx-auto h-[1.1rem] text-gray-500" />
</template> </template>

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

@ -1,14 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import CloseIcon from '~icons/material-symbols/close-rounded'
const { feedbackForm } = useGlobal() const { feedbackForm } = useGlobal()
const showForm = ref(false) const showForm = ref(false)
setTimeout(() => (showForm.value = true), 60000) setTimeout(() => (showForm.value = true), 60000)
</script> </script>
<template> <template>
<div v-if="showForm && feedbackForm && !feedbackForm.isHidden" class="nc-feedback-form-wrapper mt-6"> <div v-if="showForm && feedbackForm && !feedbackForm.isHidden" class="nc-feedback-form-wrapper mt-6">
<CloseIcon class="nc-close-icon" @click="feedbackForm.isHidden = true" /> <MaterialSymbolsCloseRounded class="nc-close-icon" @click="feedbackForm.isHidden = true" />
<iframe :src="feedbackForm.url" width="100%" height="500" frameborder="0" marginheight="0" marginwidth="0">Loading </iframe> <iframe :src="feedbackForm.url" width="100%" height="500" frameborder="0" marginheight="0" marginwidth="0">Loading </iframe>
</div> </div>

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

@ -1,10 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import { extractSdkResponseErrorMsg, onMounted, useClipboard, useI18n, useNuxtApp, useProject } from '#imports'
import { onMounted, useClipboard, useNuxtApp, useProject } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
const { t } = useI18n()
interface ShareBase { interface ShareBase {
uuid?: string uuid?: string
@ -17,6 +13,8 @@ enum ShareBaseRole {
Viewer = 'viewer', Viewer = 'viewer',
} }
const { t } = useI18n()
const { dashboardUrl } = $(useDashboard()) const { dashboardUrl } = $(useDashboard())
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
@ -35,8 +33,7 @@ const loadBase = async () => {
try { try {
if (!project.value.id) return if (!project.value.id) return
// todo: response is missing roles in type const res = await $api.project.sharedBaseGet(project.value.id)
const res = (await $api.project.sharedBaseGet(project.value.id)) as any
base = { base = {
uuid: res.uuid, uuid: res.uuid,
@ -52,10 +49,9 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
try { try {
if (!project.value.id) return if (!project.value.id) return
// todo: return type void? const res = await $api.project.sharedBaseUpdate(project.value.id, {
const res = (await $api.project.sharedBaseUpdate(project.value.id, {
roles: role, roles: role,
})) as any })
base = res ?? {} base = res ?? {}
base!.role = role base!.role = role

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

@ -1,28 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { Form, message } from 'ant-design-vue' import { Form, message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import ShareBase from './ShareBase.vue' import ShareBase from './ShareBase.vue'
import { import {
computed, computed,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
isEmail, isEmail,
onMounted, onMounted,
projectRoleTagColors,
projectRoles, projectRoles,
ref, ref,
useClipboard, useClipboard,
useDashboard, useDashboard,
useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
} from '#imports' } from '#imports'
import type { User } from '~/lib' import type { User } from '~/lib'
import { ProjectRole } from '~/lib' import { ProjectRole } from '~/lib'
const { show, selectedUser } = defineProps<Props>()
const emit = defineEmits(['closed', 'reload'])
const { t } = useI18n()
interface Props { interface Props {
show: boolean show: boolean
selectedUser?: User selectedUser?: User
@ -34,6 +29,12 @@ interface Users {
invitationToken?: string invitationToken?: string
} }
const { show, selectedUser } = defineProps<Props>()
const emit = defineEmits(['closed', 'reload'])
const { t } = useI18n()
const { project } = useProject() const { project } = useProject()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { copy } = useClipboard() const { copy } = useClipboard()
@ -69,7 +70,8 @@ const { validateInfos } = useForm(usersData, validators)
onMounted(() => { onMounted(() => {
if (!usersData.emails && selectedUser?.email) { if (!usersData.emails && selectedUser?.email) {
usersData.emails = selectedUser.email usersData.emails = selectedUser.email
usersData.role = selectedUser.roles // todo: types not matching, probably a bug here?
usersData.role = selectedUser.roles as any
} }
}) })

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

@ -2,7 +2,6 @@
import type { ColumnType, TableType } from 'nocodb-sdk' import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { Empty, Form, message } from 'ant-design-vue' import { Empty, Form, message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { srcDestMappingColumns, tableColumns } from './utils' import { srcDestMappingColumns, tableColumns } from './utils'
import { import {
MetaInj, MetaInj,
@ -17,6 +16,7 @@ import {
onMounted, onMounted,
reactive, reactive,
ref, ref,
useI18n,
useNuxtApp, useNuxtApp,
useProject, useProject,
useTabs, useTabs,
@ -44,7 +44,7 @@ interface Option {
value: string value: string
} }
const meta = inject(MetaInj, ref({} as TableType)) const meta = inject(MetaInj, ref())
const columns = computed(() => meta.value?.columns || []) const columns = computed(() => meta.value?.columns || [])
@ -255,7 +255,7 @@ function fieldsValidation(record: Record<string, any>) {
return true return true
} }
const tableName = meta?.value.title || '' const tableName = meta.value?.title || ''
if (!record.destCn) { if (!record.destCn) {
message.error(`${t('msg.error.columnDescriptionNotFound')} ${record.srcCn}`) message.error(`${t('msg.error.columnDescriptionNotFound')} ${record.srcCn}`)
@ -340,7 +340,7 @@ async function importTemplate() {
try { try {
isImporting.value = true isImporting.value = true
const tableName = meta.value.title const tableName = meta.value?.title
// only one file is allowed currently // only one file is allowed currently
const data = importData[Object.keys(importData)[0]] const data = importData[Object.keys(importData)[0]]
@ -381,7 +381,7 @@ async function importTemplate() {
}, {}), }, {}),
) )
await $api.dbTableRow.bulkCreate('noco', projectName, tableName, batchData) await $api.dbTableRow.bulkCreate('noco', projectName, tableName!, batchData)
importingTip.value = `Importing data to ${projectName}: ${progress}/${total} records` importingTip.value = `Importing data to ${projectName}: ${progress}/${total} records`
@ -440,7 +440,7 @@ async function importTemplate() {
table_name: table.ref_table_name, table_name: table.ref_table_name,
// leave title empty to get a generated one based on ref_table_name // leave title empty to get a generated one based on ref_table_name
title: '', title: '',
columns: table.columns, columns: table.columns || [],
}) })
table.title = tableMeta.title table.title = tableMeta.title

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

@ -1,8 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, computed, handleTZ, inject, ref, replaceUrlsWithLink, useProject } from '#imports' import { CellValueInj, ColumnInj, computed, handleTZ, inject, ref, replaceUrlsWithLink, useProject } from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck // todo: column type doesn't have required property `error` - throws in typecheck
const column: any = inject(ColumnInj) const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }>
const value = inject(CellValueInj) const value = inject(CellValueInj)

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

@ -54,7 +54,7 @@ const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvid
) )
await loadRelatedTableMeta() await loadRelatedTableMeta()
const localCellValue = computed(() => { const localCellValue = computed<any[]>(() => {
if (cellValue?.value) { if (cellValue?.value) {
return cellValue?.value ?? [] return cellValue?.value ?? []
} else if (isNew.value) { } else if (isNew.value) {
@ -64,7 +64,7 @@ const localCellValue = computed(() => {
}) })
const cells = computed(() => const cells = computed(() =>
localCellValue.value.reduce((acc: any[], curr: any) => { localCellValue.value.reduce((acc, curr) => {
if (!relatedTablePrimaryValueProp.value) return acc if (!relatedTablePrimaryValueProp.value) return acc
const value = curr[relatedTablePrimaryValueProp.value] const value = curr[relatedTablePrimaryValueProp.value]
@ -72,16 +72,21 @@ const cells = computed(() =>
if (!value) return acc if (!value) return acc
return [...acc, { value, item: curr }] return [...acc, { value, item: curr }]
}, [] as any[]), }, []),
) )
const unlinkRef = async (rec: Record<string, any>) => { const unlinkRef = async (rec: Record<string, any>) => {
if (isNew.value) { if (isNew.value) {
removeLTARRef(rec, column?.value as ColumnType) await removeLTARRef(rec, column.value)
} else { } else {
await unlink(rec) await unlink(rec)
} }
} }
const onAttachRecord = () => {
childListDlg.value = false
listItemsDlg.value = true
}
</script> </script>
<template> <template>
@ -110,16 +115,7 @@ const unlinkRef = async (rec: Record<string, any>) => {
<ListItems v-model="listItemsDlg" /> <ListItems v-model="listItemsDlg" />
<ListChildItems <ListChildItems v-model="childListDlg" :cell-value="localCellValue" @attach-record="onAttachRecord" />
v-model="childListDlg"
:cell-value="localCellValue"
@attach-record="
() => {
childListDlg = false
listItemsDlg = true
}
"
/>
</div> </div>
</template> </template>

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

@ -2,7 +2,18 @@
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, MetaInj, ReadonlyInj, computed, inject, provide, useColumn, useMetas } from '#imports' import {
CellUrlDisableOverlayInj,
CellValueInj,
ColumnInj,
MetaInj,
ReadonlyInj,
computed,
inject,
provide,
useColumn,
useMetas,
} from '#imports'
const { metas, getMeta } = useMetas() const { metas, getMeta } = useMetas()
@ -10,13 +21,13 @@ provide(ReadonlyInj, true)
const column = inject(ColumnInj)! as Ref<ColumnType & { colOptions: LookupType }> const column = inject(ColumnInj)! as Ref<ColumnType & { colOptions: LookupType }>
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const value = inject(CellValueInj) const value = inject(CellValueInj)
const arrValue = computed(() => (Array.isArray(value?.value) ? value?.value : [value?.value]) ?? []) const arrValue = computed(() => (Array.isArray(value?.value) ? value?.value : [value?.value]) ?? [])
const relationColumn = meta?.value.columns?.find((c) => c.id === column.value.colOptions?.fk_relation_column_id) as ColumnType & { const relationColumn = meta.value?.columns?.find((c) => c.id === column.value.colOptions?.fk_relation_column_id) as ColumnType & {
colOptions: LinkToAnotherRecordType colOptions: LinkToAnotherRecordType
} }
@ -32,6 +43,7 @@ const lookupColumn = computed<any>(
) )
provide(MetaInj, lookupTableMeta) provide(MetaInj, lookupTableMeta)
provide(CellUrlDisableOverlayInj, ref(true))
const lookupColumnMetaProps = useColumn(lookupColumn) const lookupColumnMetaProps = useColumn(lookupColumn)
</script> </script>

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

@ -53,7 +53,7 @@ const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvid
await loadRelatedTableMeta() await loadRelatedTableMeta()
const localCellValue = computed(() => { const localCellValue = computed<any[]>(() => {
if (cellValue?.value) { if (cellValue?.value) {
return cellValue?.value ?? [] return cellValue?.value ?? []
} else if (isNew.value) { } else if (isNew.value) {
@ -63,7 +63,7 @@ const localCellValue = computed(() => {
}) })
const cells = computed(() => const cells = computed(() =>
localCellValue.value.reduce((acc: any[], curr: any) => { localCellValue.value.reduce((acc, curr) => {
if (!relatedTablePrimaryValueProp.value) return acc if (!relatedTablePrimaryValueProp.value) return acc
const value = curr[relatedTablePrimaryValueProp.value] const value = curr[relatedTablePrimaryValueProp.value]
@ -71,16 +71,21 @@ const cells = computed(() =>
if (!value) return acc if (!value) return acc
return [...acc, { value, item: curr }] return [...acc, { value, item: curr }]
}, [] as any[]), }, []),
) )
const unlinkRef = async (rec: Record<string, any>) => { const unlinkRef = async (rec: Record<string, any>) => {
if (isNew.value) { if (isNew.value) {
removeLTARRef(rec, column?.value as ColumnType) await removeLTARRef(rec, column.value)
} else { } else {
await unlink(rec) await unlink(rec)
} }
} }
const onAttachRecord = () => {
childListDlg.value = false
listItemsDlg.value = true
}
</script> </script>
<template> <template>
@ -110,16 +115,7 @@ const unlinkRef = async (rec: Record<string, any>) => {
<ListItems v-model="listItemsDlg" /> <ListItems v-model="listItemsDlg" />
<ListChildItems <ListChildItems v-model="childListDlg" :cell-value="localCellValue" @attach-record="onAttachRecord" />
v-model="childListDlg"
:cell-value="localCellValue"
@attach-record="
() => {
childListDlg = false
listItemsDlg = true
}
"
/>
</div> </div>
</template> </template>

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

@ -59,8 +59,8 @@ onMounted(() => {
<template> <template>
<a-select v-model:value="localChannelValues" mode="multiple" :placeholder="placeholder" max-tag-count="responsive"> <a-select v-model:value="localChannelValues" mode="multiple" :placeholder="placeholder" max-tag-count="responsive">
<a-select-option v-for="channel of availableChannelWithIdxList" :key="channel.idx" :value="channel.idx">{{ <a-select-option v-for="channel of availableChannelWithIdxList" :key="channel.idx" :value="channel.idx">
channel.channel {{ channel.channel }}
}}</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>

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

@ -38,7 +38,7 @@ async function editHook(hook: Record<string, any>) {
<WebhookList v-else @edit="editHook" @add="editOrAdd = true" /> <WebhookList v-else @edit="editHook" @add="editOrAdd = true" />
</a-layout-content> </a-layout-content>
<a-layout-footer class="!bg-white flex"> <a-layout-footer class="!bg-white flex">
<a-button v-t="['e:hiring']" class="mx-auto mb-4" href="https://angel.co/company/nocodb" target="_blank" size="large"> <a-button v-e="['e:hiring']" class="mx-auto mb-4" href="https://angel.co/company/nocodb" target="_blank" size="large">
🚀 {{ $t('labels.weAreHiring') }}! 🚀 🚀 {{ $t('labels.weAreHiring') }}! 🚀
</a-button> </a-button>
</a-layout-footer> </a-layout-footer>

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

@ -1,7 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { Form, message } from 'ant-design-vue' import { Form, message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import type { Ref } from 'vue'
import { MetaInj, extractSdkResponseErrorMsg, fieldRequiredValidator, inject, reactive, useApi, useNuxtApp } from '#imports' import type { AuditType } from 'nocodb-sdk'
import {
MetaInj,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
inject,
reactive,
useApi,
useI18n,
useNuxtApp,
} from '#imports'
const emit = defineEmits(['backToList', 'editOrAdd']) const emit = defineEmits(['backToList', 'editOrAdd'])
@ -11,7 +21,7 @@ const { $e } = useNuxtApp()
const { api, isLoading: loading } = useApi() const { api, isLoading: loading } = useApi()
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const useForm = Form.useForm const useForm = Form.useForm
@ -254,28 +264,28 @@ async function onEventChange() {
hook.notification.payload = payload hook.notification.payload = payload
let channels: Record<string, any>[] | null = null let channels: Ref<Record<string, any>[] | null> = ref(null)
switch (hook.notification.type) { switch (hook.notification.type) {
case 'Slack': case 'Slack':
channels = slackChannels as any channels = slackChannels
break break
case 'Microsoft Teams': case 'Microsoft Teams':
channels = teamsChannels as any channels = teamsChannels
break break
case 'Discord': case 'Discord':
channels = discordChannels as any channels = discordChannels
break break
case 'Mattermost': case 'Mattermost':
channels = mattermostChannels as any channels = mattermostChannels
break break
} }
if (channels) { if (channels) {
hook.notification.payload.webhook_url = hook.notification.payload.webhook_url =
hook.notification.payload.webhook_url && hook.notification.payload.webhook_url &&
hook.notification.payload.webhook_url.map((v: Record<string, any>) => hook.notification.payload.webhook_url.map((v: { webhook_url: string }) =>
channels?.find((s: Record<string, any>) => v.webhook_url === s.webhook_url), channels.value?.find((s) => v.webhook_url === s.webhook_url),
) )
} }
@ -289,13 +299,21 @@ async function onEventChange() {
async function loadPluginList() { async function loadPluginList() {
try { try {
const plugins = (await api.plugin.list()).list as any const plugins = (await api.plugin.list()).list!
apps.value = plugins.reduce((o: Record<string, any>[], p: Record<string, any>) => {
p.tags = p.tags ? p.tags.split(',') : [] apps.value = plugins.reduce((o, p) => {
p.parsedInput = p.input && JSON.parse(p.input) const plugin: { title: string; tags: string[]; parsedInput: Record<string, any> } = {
o[p.title] = p title: '',
tags: [],
parsedInput: {},
...(p as any),
}
plugin.tags = p.tags ? p.tags.split(',') : []
plugin.parsedInput = p.input && JSON.parse(p.input)
o[plugin.title] = plugin
return o return o
}, {}) }, {} as Record<string, any>)
if (hook.event && hook.operation) { if (hook.event && hook.operation) {
hook.eventOperation = `${hook.event} ${hook.operation}` hook.eventOperation = `${hook.event} ${hook.operation}`
@ -330,13 +348,13 @@ async function saveHooks() {
}, },
}) })
} else { } else {
res = await api.dbTableWebhook.create(meta!.value.id!, { res = await api.dbTableWebhook.create(meta.value!.id!, {
...hook, ...hook,
notification: { notification: {
...hook.notification, ...hook.notification,
payload: hook.notification.payload, payload: hook.notification.payload,
}, },
} as any) } as AuditType)
} }
if (!hook.id && res) { if (!hook.id && res) {

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

@ -1,9 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import { MetaInj, extractSdkResponseErrorMsg, inject, onMounted, ref, useI18n, useNuxtApp } from '#imports'
import { MetaInj } from '~/context'
import { inject, onMounted, ref, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
const emit = defineEmits(['edit', 'add']) const emit = defineEmits(['edit', 'add'])
@ -13,11 +10,11 @@ const { $api, $e } = useNuxtApp()
const hooks = ref<Record<string, any>[]>([]) const hooks = ref<Record<string, any>[]>([])
const meta = inject(MetaInj)! const meta = inject(MetaInj, ref())
async function loadHooksList() { async function loadHooksList() {
try { try {
const hookList = (await $api.dbTableWebhook.list(meta?.value.id as string)).list as Record<string, any>[] const hookList = (await $api.dbTableWebhook.list(meta.value?.id as string)).list as Record<string, any>[]
hooks.value = hookList.map((hook) => { hooks.value = hookList.map((hook) => {
hook.notification = hook.notification && JSON.parse(hook.notification) hook.notification = hook.notification && JSON.parse(hook.notification)
return hook return hook
@ -58,7 +55,7 @@ onMounted(() => {
<div class="mb-2"> <div class="mb-2">
<div class="float-left font-bold text-xl mt-2 mb-4">{{ meta.title }} : Webhooks</div> <div class="float-left font-bold text-xl mt-2 mb-4">{{ meta.title }} : Webhooks</div>
<a-button <a-button
v-t="['c:webhook:add']" v-e="['c:webhook:add']"
class="float-right nc-btn-create-webhook" class="float-right nc-btn-create-webhook"
type="primary" type="primary"
size="large" size="large"

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

@ -1,21 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import { MetaInj, extractSdkResponseErrorMsg, onMounted, useI18n } from '#imports'
import { MetaInj } from '~/context'
import { extractSdkResponseErrorMsg } from '~/utils'
const { hook } = defineProps<Props>()
const { t } = useI18n()
interface Props { interface Props {
hook: Record<string, any> hook: Record<string, any>
} }
const { hook } = defineProps<Props>()
const { t } = useI18n()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const meta = inject(MetaInj) const meta = inject(MetaInj, ref())
const sampleData = ref({ const sampleData = ref({
data: {}, data: {},
@ -37,7 +34,7 @@ async function loadSampleData() {
async function testWebhook() { async function testWebhook() {
try { try {
await $api.dbTableWebhook.test(meta?.value.id as string, { await $api.dbTableWebhook.test(meta.value?.id as string, {
hook, hook,
payload: sampleData.value, payload: sampleData.value,
}) })

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

@ -24,3 +24,4 @@ export * from './useSmartsheetStore'
export * from './useLTARStore' export * from './useLTARStore'
export * from './useExpandedFormStore' export * from './useExpandedFormStore'
export * from './useSharedFormViewStore' export * from './useSharedFormViewStore'
export * from './useCellUrlConfig'

48
packages/nc-gui/composables/useCellUrlConfig.ts

@ -0,0 +1,48 @@
import type { MaybeRef } from '@vueuse/core'
import { computed, unref, useRoute } from '#imports'
export interface CellUrlOptions {
behavior?: string
overlay?: string
}
type ParsedRules = [RegExp, CellUrlOptions]
const parseUrlRules = (serialized?: string): ParsedRules[] | undefined => {
if (!serialized) return undefined
try {
return Object.entries(JSON.parse(serialized)).map(([key, value]) => [new RegExp(key), value] as ParsedRules)
} catch (err) {
console.error(err)
return undefined
}
}
export function useCellUrlConfig(url?: MaybeRef<string>) {
const route = useRoute()
const config = $computed(() => ({
behavior: route.query.url_behavior as string | undefined,
overlay: route.query.url_overlay as string | undefined,
rules: parseUrlRules(route.query.url_rules as string),
}))
const options = computed(() => {
const options = { behavior: config.behavior, overlay: config.overlay }
if (config.rules && (!config.behavior || !config.overlay)) {
for (const [regex, value] of config.rules) {
if (unref(url)?.match(regex)) return Object.assign(options, value)
}
}
return options
})
return {
cellUrlConfig: config,
cellUrlOptions: options,
}
}

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

@ -12,14 +12,14 @@ const useForm = Form.useForm
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber] const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState( const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType>, column?: Ref<ColumnType>) => { (meta: Ref<TableType | undefined>, column: Ref<ColumnType | undefined>) => {
const { sqlUi } = useProject() const { sqlUi } = useProject()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { getMeta } = useMetas() const { getMeta } = useMetas()
const { t } = useI18n() const { t } = useI18n()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const isEdit = computed(() => !!column?.value?.id) const isEdit = computed(() => !!column.value?.id)
const idType = null const idType = null
@ -32,7 +32,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const formState = ref<Record<string, any>>({ const formState = ref<Record<string, any>>({
title: 'title', title: 'title',
uidt: UITypes.SingleLineText, uidt: UITypes.SingleLineText,
...clone(column?.value || {}), ...clone(column.value || {}),
}) })
// actions // actions
@ -84,7 +84,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
const onUidtOrIdTypeChange = () => { const onUidtOrIdTypeChange = () => {
const { isCurrency } = useColumn(ref(formState.value as ColumnType)) const { isCurrency } = useColumn(ref(formState.value as ColumnType))
const colProp = sqlUi?.value.getDataTypeForUiType(formState?.value as any, idType as any) const colProp = sqlUi.value.getDataTypeForUiType(formState.value as { uidt: UITypes }, idType ?? undefined)
formState.value = { formState.value = {
...formState.value, ...formState.value,
meta: {}, meta: {},
@ -101,8 +101,8 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
formState.value.dtxs = sqlUi.value.getDefaultScaleForDatatype(formState.value.dt) formState.value.dtxs = sqlUi.value.getDefaultScaleForDatatype(formState.value.dt)
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect] const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect]
if (column && selectTypes.includes(formState.value.uidt) && selectTypes.includes(column?.value?.uidt as UITypes)) { if (column && selectTypes.includes(formState.value.uidt) && selectTypes.includes(column.value?.uidt as UITypes)) {
formState.value.dtxp = column?.value?.dtxp formState.value.dtxp = column.value?.dtxp
} }
if (columnToValidate.includes(formState.value.uidt)) { if (columnToValidate.includes(formState.value.uidt)) {
@ -112,7 +112,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
} }
if (isCurrency.value) { if (isCurrency.value) {
if (column?.value?.uidt === UITypes.Currency) { if (column.value?.uidt === UITypes.Currency) {
formState.value.dtxp = column.value.dtxp formState.value.dtxp = column.value.dtxp
formState.value.dtxs = column.value.dtxs formState.value.dtxs = column.value.dtxs
} else { } else {
@ -140,12 +140,12 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
formState.value.dtx = 'specificType' formState.value.dtx = 'specificType'
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect] const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect]
if (column?.value && selectTypes.includes(formState.value.uidt) && selectTypes.includes(column?.value.uidt as UITypes)) { if (column.value && selectTypes.includes(formState.value.uidt) && selectTypes.includes(column.value.uidt as UITypes)) {
formState.value.dtxp = column?.value.dtxp formState.value.dtxp = column.value.dtxp
} }
if (isCurrency.value) { if (isCurrency.value) {
if (column?.value?.uidt === UITypes.Currency) { if (column.value?.uidt === UITypes.Currency) {
formState.value.dtxp = column.value.dtxp formState.value.dtxp = column.value.dtxp
formState.value.dtxs = column.value.dtxs formState.value.dtxs = column.value.dtxs
} else { } else {
@ -177,10 +177,10 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
} }
try { try {
formState.value.table_name = meta.value.table_name formState.value.table_name = meta.value?.table_name
// formState.value.title = formState.value.column_name // formState.value.title = formState.value.column_name
if (column?.value) { if (column.value) {
await $api.dbTableColumn.update(column?.value?.id as string, formState.value) await $api.dbTableColumn.update(column.value?.id as string, formState.value)
// Column updated // Column updated
message.success(t('msg.success.columnUpdated')) message.success(t('msg.success.columnUpdated'))
} else { } else {
@ -193,10 +193,10 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
// }; // };
// } // }
} }
await $api.dbTableColumn.create(meta.value.id as string, formState.value) await $api.dbTableColumn.create(meta.value?.id as string, formState.value)
/** if LTAR column then force reload related table meta */ /** if LTAR column then force reload related table meta */
if (formState.value.uidt === UITypes.LinkToAnotherRecord && meta.value.id !== formState.value.childId) { if (formState.value.uidt === UITypes.LinkToAnotherRecord && meta.value?.id !== formState.value.childId) {
getMeta(formState.value.childId, true).then(() => {}) getMeta(formState.value.childId, true).then(() => {})
} }

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

@ -94,9 +94,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
await api.utils.commentRow({ await api.utils.commentRow({
fk_model_id: meta.value?.id as string, fk_model_id: meta.value?.id as string,
row_id: rowId, row_id: rowId,
// todo: description missing from argument type
description: comment.value, description: comment.value,
} as any) })
comment.value = '' comment.value = ''

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

@ -1,11 +1,11 @@
import { useStyleTag } from '@vueuse/core' import { useStyleTag } from '@vueuse/core'
import type { ColumnType, GridColumnType, GridType } from 'nocodb-sdk' import type { ColumnType, GridColumnType, GridType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { useMetas } from './useMetas' import { useMetas } from './useMetas'
import { useUIPermission } from './useUIPermission' import { useUIPermission } from './useUIPermission'
import { IsPublicInj } from '~/context' import { IsPublicInj } from '~/context'
export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) | undefined>) { export function useGridViewColumnWidth(view: Ref<GridType | undefined>) {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('') const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -16,7 +16,7 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
const resizingColWidth = ref('200px') const resizingColWidth = ref('200px')
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const columns = computed<ColumnType[]>(() => metas?.value?.[(view?.value as any)?.fk_model_id as string]?.columns) const columns = computed<ColumnType[]>(() => metas.value?.[(view.value as ViewType)?.fk_model_id as string]?.columns || [])
watch( watch(
[gridViewCols, resizingCol, resizingColWidth], [gridViewCols, resizingCol, resizingColWidth],
@ -38,7 +38,8 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
const loadGridViewColumns = async () => { const loadGridViewColumns = async () => {
if (!view.value?.id && !isPublic.value) return if (!view.value?.id && !isPublic.value) return
const colsData: GridColumnType[] = isPublic.value ? columns.value : await $api.dbView.gridColumnsList(view.value.id)
const colsData: GridColumnType[] = (isPublic.value ? columns.value : await $api.dbView.gridColumnsList(view.value!.id!)) ?? []
gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>( gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
(o, col) => ({ (o, col) => ({
...o, ...o,

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

@ -1,4 +1,4 @@
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, TableType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, PaginatedType, RequestParams, TableType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { Modal, message } from 'ant-design-vue' import { Modal, message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@ -46,10 +46,10 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const isPublic: boolean = $(inject(IsPublicInj, ref(false))) const isPublic: boolean = $(inject(IsPublicInj, ref(false)))
const colOptions = $computed(() => column?.value.colOptions as LinkToAnotherRecordType) const colOptions = $computed(() => column.value?.colOptions as LinkToAnotherRecordType)
const { sharedView } = useSharedView() as Record<string, any> const { sharedView } = useSharedView() as Record<string, any>
const projectId = project?.value?.id || sharedView.value?.view?.project_id const projectId = project.value?.id || sharedView.value?.view?.project_id
// getters // getters
const meta = computed(() => metas?.value?.[column?.value?.fk_model_id as string]) const meta = computed(() => metas?.value?.[column?.value?.fk_model_id as string])
@ -73,18 +73,18 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
} }
const loadRelatedTableMeta = async () => { const loadRelatedTableMeta = async () => {
await getMeta(colOptions?.fk_related_model_id as string) await getMeta(colOptions.fk_related_model_id as string)
} }
const relatedTablePrimaryValueProp = computed(() => { const relatedTablePrimaryValueProp = computed(() => {
return (relatedTableMeta?.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title return (relatedTableMeta.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title
}) })
const relatedTablePrimaryKeyProps = computed(() => { const relatedTablePrimaryKeyProps = computed(() => {
return relatedTableMeta?.value?.columns?.filter((c) => c.pk)?.map((c) => c.title) ?? [] return relatedTableMeta.value?.columns?.filter((c) => c.pk)?.map((c) => c.title) ?? []
}) })
const primaryValueProp = computed(() => { const primaryValueProp = computed(() => {
return (meta?.value?.columns?.find((c: Required<ColumnType>) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title return (meta.value?.columns?.find((c: Required<ColumnType>) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title
}) })
const loadChildrenExcludedList = async () => { const loadChildrenExcludedList = async () => {
@ -93,7 +93,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const route = useRoute() const route = useRoute()
childrenExcludedList.value = await $api.public.dataRelationList( childrenExcludedList.value = await $api.public.dataRelationList(
route.params.viewId as string, route.params.viewId as string,
column?.value?.id, column.value.id,
{}, {},
{ {
headers: { headers: {
@ -106,7 +106,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
childrenExcludedListPagination.query && childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`, `(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
fields: [relatedTablePrimaryValueProp.value, ...relatedTablePrimaryKeyProps.value], fields: [relatedTablePrimaryValueProp.value, ...relatedTablePrimaryKeyProps.value],
} as any, } as RequestParams,
}, },
) )

72
packages/nc-gui/composables/useLoadingIndicator/index.ts

@ -0,0 +1,72 @@
import { computed, ref } from '#imports'
interface UseLoadingIndicatorProps {
duration?: number
throttle?: number
}
export function useLoadingIndicator({ duration = 2000, throttle = 200 }: UseLoadingIndicatorProps) {
const progress = ref(0)
const isLoading = ref(false)
const step = computed(() => 10000 / duration)
let _timer: any = null
let _throttle: any = null
function start() {
clear()
progress.value = 0
isLoading.value = true
if (throttle) {
if (process.client) {
_throttle = setTimeout(_startTimer, throttle)
}
} else {
_startTimer()
}
}
function finish() {
progress.value = 100
_hide()
}
function clear() {
clearInterval(_timer)
clearTimeout(_throttle)
_timer = null
_throttle = null
}
function _increase(num: number) {
progress.value = Math.min(100, progress.value + num)
}
function _hide() {
clear()
if (process.client) {
setTimeout(() => {
isLoading.value = false
setTimeout(() => {
progress.value = 0
}, 400)
}, 500)
}
}
function _startTimer() {
if (process.client) {
_timer = setInterval(() => {
_increase(step.value)
}, 100)
}
}
return {
progress,
isLoading,
start,
finish,
clear,
}
}

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

@ -4,7 +4,7 @@ import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import { useNuxtApp, useRoute } from '#app' import { useNuxtApp, useRoute } from '#app'
import type { ProjectMetaInfo } from '~/lib' import type { ProjectMetaInfo } from '~/lib'
import type { ThemeConfig } from '@/composables/useTheme' import type { ThemeConfig } from '@/composables/useTheme'
import { useInjectionState } from '#imports' import { createEventHook, useInjectionState } from '#imports'
const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => { const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
@ -12,6 +12,8 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
const { includeM2M } = useGlobal() const { includeM2M } = useGlobal()
const { setTheme, theme } = useTheme() const { setTheme, theme } = useTheme()
const projectLoadedHook = createEventHook<ProjectType>()
const projectId = computed(() => (_projectId ? unref(_projectId) : (route.params.projectId as string))) const projectId = computed(() => (_projectId ? unref(_projectId) : (route.params.projectId as string)))
const project = ref<ProjectType>({}) const project = ref<ProjectType>({})
const tables = ref<TableType[]>([]) const tables = ref<TableType[]>([])
@ -86,6 +88,8 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
await loadProjectRoles() await loadProjectRoles()
await loadTables() await loadTables()
setTheme(projectMeta.value?.theme) setTheme(projectMeta.value?.theme)
projectLoadedHook.trigger(project.value)
} }
async function updateProject(data: Partial<ProjectType>) { async function updateProject(data: Partial<ProjectType>) {
@ -151,6 +155,7 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
projectMetaInfo, projectMetaInfo,
projectMeta, projectMeta,
saveTheme, saveTheme,
projectLoadedHook: projectLoadedHook.on,
} }
}, 'useProject') }, 'useProject')

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

@ -52,7 +52,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
) )
const loadSharedView = async () => { const loadSharedView = async () => {
try { try {
const viewMeta: Record<string, any> = await api.public.sharedViewMetaGet(sharedViewId, { const viewMeta = await api.public.sharedViewMetaGet(sharedViewId, {
headers: { headers: {
'xc-password': password.value, 'xc-password': password.value,
}, },
@ -90,7 +90,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
for (const column of formColumns.value) { for (const column of formColumns.value) {
if ( if (
!isVirtualCol(column) && !isVirtualCol(column) &&
((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || (column as any).required) ((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required)
) { ) {
obj.localState[column.title!] = { required } obj.localState[column.title!] = { required }
} else if ( } else if (
@ -141,7 +141,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
} }
await api.public.dataCreate( await api.public.dataCreate(
(sharedView.value as any)?.uuid as string, sharedView.value!.uuid!,
{ {
data, data,
...attachment, ...attachment,
@ -156,7 +156,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
submitted.value = true submitted.value = true
progress.value = false progress.value = false
await message.success(sharedFormView.value?.sucess_msg || 'Saved successfully.') await message.success(sharedFormView.value?.success_msg || 'Saved successfully.')
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
await message.error(await extractSdkResponseErrorMsg(e)) await message.error(await extractSdkResponseErrorMsg(e))
@ -166,7 +166,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
/** reset form if show_blank_form is true */ /** reset form if show_blank_form is true */
watch(submitted, (nextVal) => { watch(submitted, (nextVal) => {
if (nextVal && (sharedFormView.value as any)?.show_blank_form) { if (nextVal && sharedFormView.value?.show_blank_form) {
secondsRemain.value = 5 secondsRemain.value = 5
const intvl = setInterval(() => { const intvl = setInterval(() => {
secondsRemain.value = secondsRemain.value - 1 secondsRemain.value = secondsRemain.value - 1

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

@ -1,4 +1,4 @@
import type { ExportTypes, FilterType, PaginatedType, SortType, TableType, ViewType } from 'nocodb-sdk' import type { ExportTypes, FilterType, PaginatedType, RequestParams, SortType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { useGlobal, useNuxtApp } from '#imports' import { useGlobal, useNuxtApp } from '#imports'
@ -10,16 +10,16 @@ export function useSharedView() {
const { appInfo } = $(useGlobal()) const { appInfo } = $(useGlobal())
const appInfoDefaultLimit = appInfo.defaultLimit || 25 const appInfoDefaultLimit = appInfo.defaultLimit || 25
const paginationData = useState<PaginatedType>('paginationData', () => ({ page: 1, pageSize: appInfoDefaultLimit })) const paginationData = useState<PaginatedType>('paginationData', () => ({ page: 1, pageSize: appInfoDefaultLimit }))
const sharedView = useState<ViewType>('sharedView') const sharedView = useState<ViewType | undefined>('sharedView', () => undefined)
const sorts = useState<SortType[]>('sorts', () => []) const sorts = useState<SortType[]>('sorts', () => [])
const password = useState<string | undefined>('password') const password = useState<string | undefined>('password', () => undefined)
const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false) const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false)
const meta = useState<TableType | undefined>('meta', () => undefined)
const meta = useState<TableType>('meta')
const formColumns = computed( const formColumns = computed(
() => () =>
meta.value.columns meta.value?.columns
.filter( ?.filter(
(f: Record<string, any>) => (f: Record<string, any>) =>
f.show && f.uidt !== UITypes.Rollup && f.uidt !== UITypes.Lookup && f.uidt !== UITypes.Formula, f.show && f.uidt !== UITypes.Rollup && f.uidt !== UITypes.Lookup && f.uidt !== UITypes.Formula,
) )
@ -31,7 +31,7 @@ export function useSharedView() {
const { setMeta } = useMetas() const { setMeta } = useMetas()
const loadSharedView = async (viewId: string, localPassword: string | undefined = undefined) => { const loadSharedView = async (viewId: string, localPassword: string | undefined = undefined) => {
const viewMeta = await $api.public.sharedViewMetaGet(viewId, { const viewMeta: Record<string, any> = await $api.public.sharedViewMetaGet(viewId, {
headers: { headers: {
'xc-password': localPassword ?? password.value, 'xc-password': localPassword ?? password.value,
}, },
@ -40,11 +40,11 @@ export function useSharedView() {
allowCSVDownload.value = JSON.parse(viewMeta.meta)?.allowCSVDownload allowCSVDownload.value = JSON.parse(viewMeta.meta)?.allowCSVDownload
if (localPassword) password.value = localPassword if (localPassword) password.value = localPassword
sharedView.value = { ...viewMeta } sharedView.value = { title: '', ...viewMeta }
meta.value = { ...viewMeta.model } meta.value = { ...viewMeta.model }
let order = 1 let order = 1
meta.value.columns = [...viewMeta.model.columns] meta.value!.columns = [...viewMeta.model.columns]
.filter((c) => c.show) .filter((c) => c.show)
.map((c) => ({ ...c, order: order++ })) .map((c) => ({ ...c, order: order++ }))
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
@ -56,11 +56,13 @@ export function useSharedView() {
} }
const fetchSharedViewData = async () => { const fetchSharedViewData = async () => {
if (!sharedView.value) return
const page = paginationData.value.page || 1 const page = paginationData.value.page || 1
const pageSize = paginationData.value.pageSize || appInfoDefaultLimit const pageSize = paginationData.value.pageSize || appInfoDefaultLimit
const { data } = await $api.public.dataList( const { data } = await $api.public.dataList(
sharedView?.value?.uuid, sharedView.value.uuid!,
{ {
offset: (page - 1) * pageSize, offset: (page - 1) * pageSize,
filterArrJson: JSON.stringify(nestedFilters.value), filterArrJson: JSON.stringify(nestedFilters.value),
@ -82,8 +84,8 @@ export function useSharedView() {
type: ExportTypes.EXCEL | ExportTypes.CSV, type: ExportTypes.EXCEL | ExportTypes.CSV,
responseType: 'base64' | 'blob', responseType: 'base64' | 'blob',
) => { ) => {
return await $api.public.csvExport(sharedView.value?.uuid, type, { return await $api.public.csvExport(sharedView.value!.uuid!, type, {
format: responseType as any, format: responseType,
query: { query: {
fields: fields.map((field) => field.title), fields: fields.map((field) => field.title),
offset, offset,
@ -94,7 +96,7 @@ export function useSharedView() {
headers: { headers: {
'xc-password': password.value, 'xc-password': password.value,
}, },
}) } as RequestParams)
} }
return { return {

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

@ -20,7 +20,8 @@ import {
useVirtualCell, useVirtualCell,
} from '#imports' } from '#imports'
const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState((meta: Ref<TableType>, row: MaybeRef<Row>) => { const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
(meta: Ref<TableType | undefined>, row: MaybeRef<Row>) => {
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { t } = useI18n() const { t } = useI18n()
@ -69,7 +70,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
await $api.dbTableRow.nestedAdd( await $api.dbTableRow.nestedAdd(
NOCO, NOCO,
project.value.title as string, project.value.title as string,
meta.value.title as string, meta.value?.title as string,
rowId, rowId,
type as 'mm' | 'hm', type as 'mm' | 'hm',
column.title as string, column.title as string,
@ -82,7 +83,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
/** sync LTAR relations kept in local state */ /** sync LTAR relations kept in local state */
const syncLTARRefs = async (row: Record<string, any>) => { const syncLTARRefs = async (row: Record<string, any>) => {
const id = extractPkFromRow(row, meta.value.columns as ColumnType[]) const id = extractPkFromRow(row, meta.value?.columns as ColumnType[])
for (const column of meta?.value?.columns ?? []) { for (const column of meta?.value?.columns ?? []) {
if (column.uidt !== UITypes.LinkToAnotherRecord) continue if (column.uidt !== UITypes.LinkToAnotherRecord) continue
const colOptions = column?.colOptions as LinkToAnotherRecordType const colOptions = column?.colOptions as LinkToAnotherRecordType
@ -114,9 +115,9 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
const loadRow = async () => { const loadRow = async () => {
const record = await $api.dbTableRow.read( const record = await $api.dbTableRow.read(
NOCO, NOCO,
project?.value?.id as string, project.value?.id as string,
meta.value.title, meta.value?.title as string,
extractPkFromRow(ref(row).value?.row, meta.value.columns as ColumnType[]), extractPkFromRow(ref(row).value?.row, meta.value?.columns as ColumnType[]),
) )
Object.assign(ref(row).value, { Object.assign(ref(row).value, {
row: record, row: record,
@ -136,7 +137,9 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
loadRow, loadRow,
currentRow, currentRow,
} }
}, 'smartsheet-row-store') },
'smartsheet-row-store',
)
export { useProvideSmartsheetRowStore } export { useProvideSmartsheetRowStore }

20
packages/nc-gui/composables/useSmartsheetStore.ts

@ -5,8 +5,8 @@ import { computed, reactive, useInjectionState, useNuxtApp, useProject, useTempl
const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState( const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
( (
view: Ref<ViewType>, view: Ref<ViewType | undefined>,
meta: Ref<TableType>, meta: Ref<TableType | undefined>,
shared = false, shared = false,
initalSorts?: Ref<SortType[]>, initalSorts?: Ref<SortType[]>,
initialFilters?: Ref<FilterType[]>, initialFilters?: Ref<FilterType[]>,
@ -24,15 +24,15 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
}) })
// getters // getters
const isLocked = computed(() => (view?.value as any)?.lock_type === 'locked') const isLocked = computed(() => view.value?.lock_type === 'locked')
const isPkAvail = computed(() => meta?.value?.columns?.some((c) => c.pk)) const isPkAvail = computed(() => meta.value?.columns?.some((c) => c.pk))
const isGrid = computed(() => (view?.value as any)?.type === ViewTypes.GRID) const isGrid = computed(() => view.value?.type === ViewTypes.GRID)
const isForm = computed(() => (view?.value as any)?.type === ViewTypes.FORM) const isForm = computed(() => view.value?.type === ViewTypes.FORM)
const isSharedForm = computed(() => isForm?.value && shared) const isSharedForm = computed(() => isForm.value && shared)
const isGallery = computed(() => (view?.value as any)?.type === ViewTypes.GALLERY) const isGallery = computed(() => view.value?.type === ViewTypes.GALLERY)
const xWhere = computed(() => { const xWhere = computed(() => {
let where let where
const col = meta?.value?.columns?.find(({ id }) => id === search.field) || meta?.value?.columns?.find((v) => v.pv) const col = meta.value?.columns?.find(({ id }) => id === search.field) || meta.value?.columns?.find((v) => v.pv)
if (!col) return if (!col) return
if (!search.query.trim()) return if (!search.query.trim()) return
@ -44,7 +44,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
return where return where
}) })
const isSqlView = computed(() => meta?.value?.type === 'view') const isSqlView = computed(() => meta.value?.type === 'view')
const sorts = initalSorts ?? ref<SortType[]>([]) const sorts = initalSorts ?? ref<SortType[]>([])
const nestedFilters: Ref<FilterType[]> = initialFilters ?? ref<FilterType[]>([]) const nestedFilters: Ref<FilterType[]> = initialFilters ?? ref<FilterType[]>([])

15
packages/nc-gui/composables/useUIPermission/index.ts

@ -1,6 +1,7 @@
import type { Permission } from './rolePermissions' import type { Permission } from './rolePermissions'
import rolePermissions from './rolePermissions' import rolePermissions from './rolePermissions'
import { USER_PROJECT_ROLES, useGlobal, useState } from '#imports' import { USER_PROJECT_ROLES, computed, useGlobal, useState } from '#imports'
import type { Role, Roles } from '~/lib'
export function useUIPermission() { export function useUIPermission() {
const { user, previewAs } = useGlobal() const { user, previewAs } = useGlobal()
@ -8,7 +9,7 @@ export function useUIPermission() {
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({})) const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const baseRoles = computed(() => { const baseRoles = computed(() => {
let userRoles = user.value?.roles || {} let userRoles = typeof user.value?.roles === 'string' ? user.value?.roles : ({ ...(user.value?.roles || {}) } as Roles)
// if string populate key-value paired object // if string populate key-value paired object
if (typeof userRoles === 'string') { if (typeof userRoles === 'string') {
@ -17,20 +18,20 @@ export function useUIPermission() {
return acc return acc
}, {}) }, {})
} }
// merge user role and project specific user roles // merge user role and project specific user roles
const roles = { return {
...userRoles, ...userRoles,
...projectRoles.value, ...projectRoles.value,
} }
return roles
}) })
const isUIAllowed = (permission: Permission | string, skipPreviewAs = false) => { const isUIAllowed = (permission: Permission | string, skipPreviewAs = false) => {
let roles = baseRoles.value let roles = baseRoles.value as Record<string, any>
if (previewAs.value && !skipPreviewAs) { if (previewAs.value && !skipPreviewAs) {
roles = { roles = {
[previewAs.value]: true, [previewAs.value as Role]: true,
} }
} }

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

@ -6,7 +6,11 @@ import { useNuxtApp } from '#app'
import { IsPublicInj } from '#imports' import { IsPublicInj } from '#imports'
import type { Field } from '~/lib' import type { Field } from '~/lib'
export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRef<TableType>, reloadData?: () => void) { export function useViewColumns(
view: Ref<ViewType | undefined>,
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
reloadData?: () => void,
) {
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const fields = ref<Field[]>() const fields = ref<Field[]>()
@ -26,12 +30,12 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
const metaColumnById = computed<Record<string, ColumnType>>(() => { const metaColumnById = computed<Record<string, ColumnType>>(() => {
if (!meta.value?.columns) return {} if (!meta.value?.columns) return {}
return meta.value?.columns?.reduce( return meta.value.columns.reduce(
(acc: ColumnType, curr: ColumnType) => ({ (acc, curr) => ({
...acc, ...acc,
[curr.id!]: curr, [curr.id!]: curr,
}), }),
{} as any, {},
) )
}) })
@ -119,7 +123,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
const saveOrUpdate = async (field: any, index: number) => { const saveOrUpdate = async (field: any, index: number) => {
if (isPublic.value && fields.value) { if (isPublic.value && fields.value) {
fields.value[index] = field fields.value[index] = field
meta.value.columns = meta.value?.columns?.map((column: ColumnType) => { meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => {
if (column.id === field.fk_column_id) { if (column.id === field.fk_column_id) {
return { return {
...column, ...column,
@ -136,7 +140,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
if (isUIAllowed('fieldsSync')) { if (isUIAllowed('fieldsSync')) {
if (field.id && view?.value?.id) { if (field.id && view?.value?.id) {
await $api.dbViewColumn.update(view.value.id, field.id, field) await $api.dbViewColumn.update(view.value.id, field.id, field)
} else if (view?.value?.id) { } else if (view.value?.id) {
const insertedField = (await $api.dbViewColumn.create(view.value.id, field)) as any const insertedField = (await $api.dbViewColumn.create(view.value.id, field)) as any
/** update the field in fields if defined */ /** update the field in fields if defined */
@ -151,8 +155,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
const showSystemFields = computed({ const showSystemFields = computed({
get() { get() {
// todo: show_system_fields missing from ViewType return view.value?.show_system_fields || false
return (view?.value as any)?.show_system_fields || false
}, },
set(v: boolean) { set(v: boolean) {
if (view?.value?.id) { if (view?.value?.id) {
@ -163,7 +166,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
}) })
.finally(() => reloadData?.()) .finally(() => reloadData?.())
} }
;(view.value as any).show_system_fields = v view.value.show_system_fields = v
} }
$e('a:fields:system-fields') $e('a:fields:system-fields')
}, },

31
packages/nc-gui/composables/useViewData.ts

@ -34,8 +34,8 @@ export interface Row {
} }
export function useViewData( export function useViewData(
meta: Ref<TableType> | ComputedRef<TableType> | undefined, meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
viewMeta: Ref<ViewType & { id: string }> | ComputedRef<ViewType & { id: string }> | undefined, viewMeta: Ref<ViewType | undefined> | ComputedRef<(ViewType & { id: string }) | undefined>,
where?: ComputedRef<string | undefined>, where?: ComputedRef<string | undefined>,
) { ) {
if (!meta) { if (!meta) {
@ -50,8 +50,7 @@ export function useViewData(
const aggCommentCount = ref<{ row_id: string; count: number }[]>([]) const aggCommentCount = ref<{ row_id: string; count: number }[]>([])
const galleryData = ref<GalleryType>() const galleryData = ref<GalleryType>()
const formColumnData = ref<FormType>() const formColumnData = ref<FormType>()
// todo: missing properties on FormType (success_msg, show_blank_form, const formViewData = ref<FormType>()
const formViewData = ref<FormType & { success_msg?: string; show_blank_form?: boolean }>()
const formattedData = ref<Row[]>([]) const formattedData = ref<Row[]>([])
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
@ -156,19 +155,19 @@ export function useViewData(
aggCommentCount.value = await $api.utils.commentCount({ aggCommentCount.value = await $api.utils.commentCount({
ids, ids,
fk_model_id: meta?.value.id as string, fk_model_id: meta.value?.id as string,
}) })
for (const row of formattedData.value) { for (const row of formattedData.value) {
const id = extractPkFromRow(row.row, meta?.value?.columns as ColumnType[]) const id = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
row.rowMeta.commentCount = aggCommentCount.value?.find((c: Record<string, any>) => c.row_id === id)?.count || 0 row.rowMeta.commentCount = aggCommentCount.value?.find((c: Record<string, any>) => c.row_id === id)?.count || 0
} }
} }
async function loadData(params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) { async function loadData(params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) {
if ((!project?.value?.id || !meta?.value?.id || !viewMeta?.value?.id) && !isPublic.value) return if ((!project?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic.value) return
const response = !isPublic.value const response = !isPublic.value
? await api.dbViewRow.list('noco', project.value.id!, meta!.value.id!, viewMeta!.value.id, { ? await api.dbViewRow.list('noco', project.value.id!, meta.value!.id!, viewMeta.value!.id!, {
...queryParams.value, ...queryParams.value,
...params, ...params,
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
@ -199,7 +198,7 @@ export function useViewData(
const insertedData = await $api.dbViewRow.create( const insertedData = await $api.dbViewRow.create(
NOCO, NOCO,
project?.value.id as string, project?.value.id as string,
meta?.value.id as string, meta.value?.id as string,
viewMeta?.value?.id as string, viewMeta?.value?.id as string,
insertObj, insertObj,
) )
@ -219,12 +218,12 @@ export function useViewData(
async function updateRowProperty(toUpdate: Row, property: string) { async function updateRowProperty(toUpdate: Row, property: string) {
try { try {
const id = extractPkFromRow(toUpdate.row, meta?.value.columns as ColumnType[]) const id = extractPkFromRow(toUpdate.row, meta.value?.columns as ColumnType[])
const updatedRowData = await $api.dbViewRow.update( const updatedRowData = await $api.dbViewRow.update(
NOCO, NOCO,
project?.value.id as string, project?.value.id as string,
meta?.value.id as string, meta.value?.id as string,
viewMeta?.value?.id as string, viewMeta?.value?.id as string,
id, id,
{ {
@ -238,7 +237,7 @@ export function useViewData(
// audit // audit
$api.utils $api.utils
.auditRowUpdate(id, { .auditRowUpdate(id, {
fk_model_id: meta?.value.id as string, fk_model_id: meta.value?.id as string,
column_name: property, column_name: property,
row_id: id, row_id: id,
value: getHTMLEncodedText(toUpdate.row[property]), value: getHTMLEncodedText(toUpdate.row[property]),
@ -276,8 +275,8 @@ export function useViewData(
const res: any = await $api.dbViewRow.delete( const res: any = await $api.dbViewRow.delete(
'noco', 'noco',
project.value.id as string, project.value.id as string,
meta?.value.id as string, meta.value?.id as string,
viewMeta?.value.id as string, viewMeta.value?.id as string,
id, id,
) )
@ -298,7 +297,7 @@ export function useViewData(
if (!row.rowMeta.new) { if (!row.rowMeta.new) {
const id = meta?.value?.columns const id = meta?.value?.columns
?.filter((c) => c.pk) ?.filter((c) => c.pk)
.map((c) => row.row[c.title as any]) .map((c) => row.row[c.title!])
.join('___') .join('___')
const deleted = await deleteRowById(id as string) const deleted = await deleteRowById(id as string)
@ -365,7 +364,7 @@ export function useViewData(
?.map((c: Record<string, any>) => ({ ?.map((c: Record<string, any>) => ({
...c, ...c,
fk_column_id: c.id, fk_column_id: c.id,
fk_view_id: viewMeta.value.id, fk_view_id: viewMeta.value?.id,
...(fieldById[c.id] ? fieldById[c.id] : {}), ...(fieldById[c.id] ? fieldById[c.id] : {}),
order: (fieldById[c.id] && fieldById[c.id].order) || order++, order: (fieldById[c.id] && fieldById[c.id].order) || order++,
id: fieldById[c.id] && fieldById[c.id].id, id: fieldById[c.id] && fieldById[c.id].id,

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

@ -1,4 +1,4 @@
import type { ViewType } from 'nocodb-sdk' import type { FilterType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { import {
@ -16,7 +16,7 @@ import {
import type { Filter } from '~/lib' import type { Filter } from '~/lib'
export function useViewFilters( export function useViewFilters(
view: Ref<ViewType> | undefined, view: Ref<ViewType | undefined>,
parentId?: string, parentId?: string,
autoApply?: ComputedRef<boolean>, autoApply?: ComputedRef<boolean>,
reloadData?: () => void, reloadData?: () => void,
@ -69,13 +69,14 @@ export function useViewFilters(
if (parentId) { if (parentId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId) filters.value = await $api.dbTableFilter.childrenRead(parentId)
} else { } else {
filters.value = (await $api.dbTableWebhookFilter.read(hookId as string)) as any // todo: return type is incorrect
filters.value = (await $api.dbTableWebhookFilter.read(hookId!)) as unknown as Filter[]
} }
} else { } else {
if (parentId) { if (parentId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId) filters.value = await $api.dbTableFilter.childrenRead(parentId)
} else { } else {
filters.value = await $api.dbTableFilter.read(view?.value?.id as string) filters.value = await $api.dbTableFilter.read(view.value!.id!)
} }
} }
} catch (e: any) { } catch (e: any) {
@ -99,12 +100,12 @@ export function useViewFilters(
filters.value[+i] = (await $api.dbTableWebhookFilter.create(hookId, { filters.value[+i] = (await $api.dbTableWebhookFilter.create(hookId, {
...filter, ...filter,
fk_parent_id: parentId, fk_parent_id: parentId,
})) as any })) as unknown as FilterType
} else { } else {
filters.value[+i] = (await $api.dbTableFilter.create(view?.value?.id as string, { filters.value[+i] = await $api.dbTableFilter.create(view?.value?.id as string, {
...filter, ...filter,
fk_parent_id: parentId, fk_parent_id: parentId,
})) as any })
} }
} }
} }
@ -168,10 +169,10 @@ export function useViewFilters(
}) })
} else { } else {
// todo: return type of dbTableFilter is void? // todo: return type of dbTableFilter is void?
filters.value[i] = (await $api.dbTableFilter.create(view?.value?.id as string, { filters.value[i] = await $api.dbTableFilter.create(view.value.id!, {
...filter, ...filter,
fk_parent_id: parentId, fk_parent_id: parentId,
})) as any })
} }
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)

14
packages/nc-gui/composables/useViewSorts.ts

@ -1,12 +1,9 @@
import type { GalleryType, GridType, KanbanType, SortType } from 'nocodb-sdk' import type { SortType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { IsPublicInj, ReloadViewDataHookInj, extractSdkResponseErrorMsg, useNuxtApp } from '#imports' import { IsPublicInj, ReloadViewDataHookInj, extractSdkResponseErrorMsg, useNuxtApp } from '#imports'
export function useViewSorts( export function useViewSorts(view: Ref<ViewType | undefined>, reloadData?: () => void) {
view: Ref<(GridType | KanbanType | GalleryType) & { id?: string }> | undefined,
reloadData?: () => void,
) {
const { sharedView } = useSharedView() const { sharedView } = useSharedView()
const { sorts } = useSmartsheetStoreOrThrow() const { sorts } = useSmartsheetStoreOrThrow()
@ -21,14 +18,15 @@ export function useViewSorts(
const loadSorts = async () => { const loadSorts = async () => {
if (isPublic.value) { if (isPublic.value) {
const sharedSorts = sharedView.value?.sorts || [] // todo: sorts missing on `ViewType`
const sharedSorts = (sharedView.value as any)?.sorts || []
sorts.value = [...sharedSorts] sorts.value = [...sharedSorts]
return return
} }
try { try {
if (!view?.value) return if (!view?.value) return
sorts.value = ((await $api.dbTableSort.list(view?.value?.id as string)) as any)?.sorts?.list sorts.value = (await $api.dbTableSort.list(view.value!.id!)).sorts?.list || []
} catch (e: any) { } catch (e: any) {
console.error(e) console.error(e)
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
@ -48,7 +46,7 @@ export function useViewSorts(
await $api.dbTableSort.update(sort.id, sort) await $api.dbTableSort.update(sort.id, sort)
$e('sort-updated') $e('sort-updated')
} else { } else {
sorts.value[i] = (await $api.dbTableSort.create(view?.value?.id as string, sort)) as any sorts.value[i] = (await $api.dbTableSort.create(view.value?.id as string, sort)) as unknown as SortType
} }
} }
reloadData?.() reloadData?.()

1
packages/nc-gui/context/index.ts

@ -28,3 +28,4 @@ export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection')
export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection') export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection')
export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injection') export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injection')
export const SharedViewPasswordInj: InjectionKey<Ref<string | null>> = Symbol('shared-view-password-injection') export const SharedViewPasswordInj: InjectionKey<Ref<string | null>> = Symbol('shared-view-password-injection')
export const CellUrlDisableOverlayInj: InjectionKey<Ref<boolean>> = Symbol('cell-url-disable-url')

8
packages/nc-gui/layouts/base.vue

@ -41,7 +41,7 @@ hooks.hook('page:finish', () => {
> >
<div <div
v-if="!route.params.projectType" v-if="!route.params.projectType"
v-t="['c:navbar:home']" v-e="['c:navbar:home']"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon" class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')" @click="navigateTo('/')"
> >
@ -75,7 +75,7 @@ hooks.hook('page:finish', () => {
<template #overlay> <template #overlay>
<a-menu class="!py-0 leading-8 !rounded"> <a-menu class="!py-0 leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t"> <a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user"> <nuxt-link v-e="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-accent" />&nbsp; <MdiAt class="mt-1 group-hover:text-accent" />&nbsp;
<span class="prose group-hover:text-primary"> {{ email }}</span> <span class="prose group-hover:text-primary"> {{ email }}</span>
@ -85,7 +85,7 @@ hooks.hook('page:finish', () => {
<a-menu-divider class="!m-0" /> <a-menu-divider class="!m-0" />
<a-menu-item key="1" class="!rounded-b group"> <a-menu-item key="1" class="!rounded-b group">
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout"> <div v-e="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
<MdiLogout class="group-hover:text-accent" />&nbsp; <MdiLogout class="group-hover:text-accent" />&nbsp;
<span class="prose group-hover:text-primary"> <span class="prose group-hover:text-primary">
@ -102,7 +102,7 @@ hooks.hook('page:finish', () => {
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> Switch language</template> <template #title> Switch language</template>
<GeneralLanguage class="nc-lang-btn" /> <GeneralLanguage v-if="!signedIn" class="nc-lang-btn" />
</a-tooltip> </a-tooltip>
<div class="w-full h-full overflow-hidden"> <div class="w-full h-full overflow-hidden">

4
packages/nc-gui/lib/types.ts

@ -1,6 +1,6 @@
import type { FilterType } from 'nocodb-sdk' import type { FilterType } from 'nocodb-sdk'
import type { I18n } from 'vue-i18n' import type { I18n } from 'vue-i18n'
import type { Language, Role } from './enums' import type { Role } from './enums'
export interface User { export interface User {
id: string id: string
@ -31,7 +31,7 @@ export interface Field {
system?: boolean system?: boolean
} }
export type Roles = Record<Role, boolean> export type Roles = Record<Role, boolean> | string
export type Filter = FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string; readOnly?: boolean } export type Filter = FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string; readOnly?: boolean }

1
packages/nc-gui/package.json

@ -1,5 +1,6 @@
{ {
"private": true, "private": true,
"web-types": "web-types.json",
"scripts": { "scripts": {
"build": "nuxi build", "build": "nuxi build",
"dev": "nuxi dev", "dev": "nuxi dev",

68
packages/nc-gui/pages/[projectType]/[projectId]/index.vue

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import { useI18n } from 'vue-i18n'
import { import {
computed, computed,
definePageMeta, definePageMeta,
@ -14,6 +13,7 @@ import {
ref, ref,
useClipboard, useClipboard,
useGlobal, useGlobal,
useI18n,
useProject, useProject,
useRoute, useRoute,
useTabs, useTabs,
@ -25,15 +25,15 @@ definePageMeta({
hideHeader: true, hideHeader: true,
}) })
const route = useRoute() const { t } = useI18n()
const router = useRouter() const route = useRoute()
const { appInfo, token, signOut, signedIn, user } = useGlobal() const { appInfo, token, signOut, signedIn, user } = useGlobal()
const { project, loadProject, loadTables, isSharedBase, loadProjectMetaInfo, projectMetaInfo, saveTheme } = useProject() const { projectLoadedHook, project, isSharedBase, loadProjectMetaInfo, projectMetaInfo, saveTheme } = useProject()
const { addTab, clearTabs } = useTabs() const { clearTabs, addTab } = useTabs()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
@ -62,27 +62,11 @@ const logout = () => {
navigateTo('/signin') navigateTo('/signin')
} }
onKeyStroke(
'Escape',
() => {
dropdownOpen.value = false
},
{ eventName: 'keydown' },
)
clearTabs()
function toggleDialog(value?: boolean, key?: string) { function toggleDialog(value?: boolean, key?: string) {
dialogOpen.value = value ?? !dialogOpen.value dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key openDialogKey.value = key
} }
await loadProject()
await loadTables()
const { t } = useI18n()
const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color: string) => { const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color: string) => {
switch (mode) { switch (mode) {
case 'swatch': { case 'swatch': {
@ -117,10 +101,6 @@ const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color: st
} }
} }
if (!route.params.type && isUIAllowed('teamAndAuth')) {
addTab({ type: TabType.AUTH, title: t('title.teamAndAuth') })
}
const copyProjectInfo = async () => { const copyProjectInfo = async () => {
try { try {
await loadProjectMetaInfo() await loadProjectMetaInfo()
@ -150,11 +130,21 @@ const copyAuthToken = async () => {
} }
} }
/** If v1 url found navigate to corresponding new url */ onKeyStroke(
const { type, name, view } = route.query 'Escape',
if (type && name) { () => {
router.replace(`/nc/${route.params.projectId}/${type}/${name}${view ? `/${view}` : ''}`) dropdownOpen.value = false
} },
{ eventName: 'keydown' },
)
clearTabs()
projectLoadedHook(() => {
if (!route.params.type && isUIAllowed('teamAndAuth')) {
addTab({ type: TabType.AUTH, title: t('title.teamAndAuth') })
}
})
</script> </script>
<template> <template>
@ -177,7 +167,7 @@ if (type && name) {
> >
<div <div
v-if="isOpen && !isSharedBase" v-if="isOpen && !isSharedBase"
v-t="['c:navbar:home']" v-e="['c:navbar:home']"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105 nc-noco-brand-icon" class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')" @click="navigateTo('/')"
> >
@ -245,7 +235,7 @@ if (type && name) {
<!-- Copy Project Info --> <!-- Copy Project Info -->
<a-menu-item key="copy"> <a-menu-item key="copy">
<div <div
v-t="['c:navbar:user:copy-proj-info']" v-e="['c:navbar:user:copy-proj-info']"
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click.stop="copyProjectInfo" @click.stop="copyProjectInfo"
> >
@ -260,7 +250,7 @@ if (type && name) {
<a-menu-item key="api"> <a-menu-item key="api">
<div <div
v-if="isUIAllowed('apiDocs')" v-if="isUIAllowed('apiDocs')"
v-t="['e:api-docs']" v-e="['e:api-docs']"
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)" @click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)"
> >
@ -271,7 +261,7 @@ if (type && name) {
<!-- Copy Auth Token --> <!-- Copy Auth Token -->
<a-menu-item key="copy"> <a-menu-item key="copy">
<div v-t="['a:navbar:user:copy-auth-token']" class="nc-project-menu-item group" @click.stop="copyAuthToken"> <div v-e="['a:navbar:user:copy-auth-token']" class="nc-project-menu-item group" @click.stop="copyAuthToken">
<MdiScriptTextKeyOutline class="group-hover:text-accent" /> <MdiScriptTextKeyOutline class="group-hover:text-accent" />
{{ $t('activity.account.authToken') }} {{ $t('activity.account.authToken') }}
</div> </div>
@ -283,7 +273,7 @@ if (type && name) {
<a-menu-item key="teamAndSettings"> <a-menu-item key="teamAndSettings">
<div <div
v-if="isUIAllowed('settings')" v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']" v-e="['c:navdraw:project-settings']"
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')" @click="toggleDialog(true, 'teamAndAuth')"
> >
@ -364,7 +354,7 @@ if (type && name) {
<!-- Preview As --> <!-- Preview As -->
<a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as"> <a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as">
<template #title> <template #title>
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group"> <div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiFileEyeOutline class="group-hover:text-accent" /> <MdiFileEyeOutline class="group-hover:text-accent" />
{{ $t('activity.previewAs') }} {{ $t('activity.previewAs') }}
@ -421,7 +411,7 @@ if (type && name) {
<template #expandIcon></template> <template #expandIcon></template>
<a-menu-item key="0" class="!rounded-t"> <a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user"> <nuxt-link v-e="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-accent" />&nbsp; <MdiAt class="mt-1 group-hover:text-accent" />&nbsp;
<span class="prose-sm">{{ email }}</span> <span class="prose-sm">{{ email }}</span>
@ -429,7 +419,7 @@ if (type && name) {
</a-menu-item> </a-menu-item>
<a-menu-item key="1" class="!rounded-b"> <a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout"> <div v-e="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
<MdiLogout class="group-hover:(!text-accent)" />&nbsp; <MdiLogout class="group-hover:(!text-accent)" />&nbsp;
<span class="prose-sm nc-user-menu-signout"> <span class="prose-sm nc-user-menu-signout">
@ -448,7 +438,7 @@ if (type && name) {
class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) group nc-sidebar-add-row flex items-center px-2" class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) group nc-sidebar-add-row flex items-center px-2"
> >
<MdiBackburger <MdiBackburger
v-t="['c:grid:toggle-navdraw']" v-e="['c:grid:toggle-navdraw']"
class="cursor-pointer transform transition-transform duration-500" class="cursor-pointer transform transition-transform duration-500"
:class="{ 'rotate-180': !isOpen }" :class="{ 'rotate-180': !isOpen }"
@click="toggle(!isOpen)" @click="toggle(!isOpen)"

45
packages/nc-gui/pages/[projectType]/[projectId]/index/index.vue

@ -1,15 +1,48 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TabItem } from '~/composables' import type { TabItem } from '~/composables'
import { TabType } from '~/composables' import { TabType } from '~/composables'
import { TabMetaInj, provide, useGlobal, useSidebar, useTabs } from '#imports' import {
TabMetaInj,
onBeforeMount,
provide,
ref,
useGlobal,
useProject,
useRoute,
useRouter,
useSidebar,
useTabs,
} from '#imports'
import MdiAirTableIcon from '~icons/mdi/table-large' import MdiAirTableIcon from '~icons/mdi/table-large'
import MdiView from '~icons/mdi/eye-circle-outline' import MdiView from '~icons/mdi/eye-circle-outline'
import MdiAccountGroup from '~icons/mdi/account-group' import MdiAccountGroup from '~icons/mdi/account-group'
const { project, loadProject, loadTables } = useProject()
const { tabs, activeTabIndex, activeTab, closeTab } = useTabs() const { tabs, activeTabIndex, activeTab, closeTab } = useTabs()
const { isLoading } = useGlobal() const { isLoading } = useGlobal()
const route = useRoute()
const router = useRouter()
const isReady = ref(false)
onBeforeMount(async () => {
if (!Object.keys(project.value).length) await loadProject()
/** If v1 url found navigate to corresponding new url */
const { type, name, view } = route.query
if (type && name) {
await router.replace(`/nc/${route.params.projectId}/${type}/${name}${view ? `/${view}` : ''}`)
}
await loadTables()
isReady.value = true
})
provide(TabMetaInj, activeTab) provide(TabMetaInj, activeTab)
const icon = (tab: TabItem) => { const icon = (tab: TabItem) => {
@ -39,7 +72,7 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) group nc-sidebar-add-row py-2 px-3" class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) group nc-sidebar-add-row py-2 px-3"
> >
<MdiMenu <MdiMenu
v-t="['c:grid:toggle-navdraw']" v-e="['c:grid:toggle-navdraw']"
class="cursor-pointer transform transition-transform duration-500 text-white" class="cursor-pointer transform transition-transform duration-500 text-white"
:class="{ 'rotate-180': !isOpen }" :class="{ 'rotate-180': !isOpen }"
@click="toggle(!isOpen)" @click="toggle(!isOpen)"
@ -71,13 +104,17 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
<div v-show="isLoading" class="flex items-center gap-2 ml-3 text-gray-200"> <div v-show="isLoading" class="flex items-center gap-2 ml-3 text-gray-200">
{{ $t('general.loading') }} {{ $t('general.loading') }}
<MdiLoading :class="{ 'animate-infinite animate-spin': isLoading }" /> <MdiLoading class="animate-infinite animate-spin" />
</div> </div>
</div> </div>
</div> </div>
<div class="w-full min-h-[300px] flex-auto"> <div class="w-full min-h-[300px] flex-auto">
<NuxtPage /> <NuxtPage v-if="isReady" />
<div v-else class="w-full h-full flex justify-center items-center">
<a-spin size="large" />
</div>
</div> </div>
</div> </div>
</div> </div>

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

@ -22,5 +22,3 @@ getMeta(route.params.title as string, true).finally(() => {
</div> </div>
<TabsSmartsheet v-else :key="route.params.title" :active-tab="activeTab" /> <TabsSmartsheet v-else :key="route.params.title" :active-tab="activeTab" />
</template> </template>
<style scoped></style>

2
packages/nc-gui/pages/[projectType]/[projectId]/index/index/auth.vue

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import Auth from '~~/components/tabs/Auth.vue' import Auth from '~/components/tabs/Auth.vue'
</script> </script>
<template> <template>

4
packages/nc-gui/pages/[projectType]/form/[viewId].vue

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk'
import { import {
IsFormInj, IsFormInj,
IsPublicInj, IsPublicInj,
@ -34,7 +32,7 @@ if (!notFound.value) {
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
provide(IsFormInj, ref(true)) provide(IsFormInj, ref(true))
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta as Ref<TableType>, true) useProvideSmartsheetStore(sharedView, meta, true)
} }
</script> </script>

6
packages/nc-gui/pages/[projectType]/form/[viewId]/index.vue

@ -36,11 +36,12 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
<div <div
class="bg-white relative flex flex-col justify-center gap-2 w-full lg:max-w-1/2 max-w-500px m-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)" class="bg-white relative flex flex-col justify-center gap-2 w-full lg:max-w-1/2 max-w-500px m-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
> >
<template v-if="sharedFormView">
<general-noco-icon class="color-transition hover:(ring ring-accent)" :class="[isLoading ? 'animated-bg-gradient' : '']" /> <general-noco-icon class="color-transition hover:(ring ring-accent)" :class="[isLoading ? 'animated-bg-gradient' : '']" />
<h1 class="prose-2xl font-bold self-center my-4">{{ sharedFormView.heading }}</h1> <h1 class="prose-2xl font-bold self-center my-4">{{ sharedFormView?.heading }}</h1>
<h2 v-if="sharedFormView.subheading" class="prose-lg text-gray-500 self-center">{{ sharedFormView.subheading }}</h2> <h2 v-if="sharedFormView?.subheading" class="prose-lg text-gray-500 self-center">{{ sharedFormView.subheading }}</h2>
<a-alert v-if="notFound" type="warning" class="my-4 text-center" message="Not found" /> <a-alert v-if="notFound" type="warning" class="my-4 text-center" message="Not found" />
@ -128,6 +129,7 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
</div> </div>
</div> </div>
</template> </template>
</template>
<a-modal <a-modal
v-model:visible="passwordDlg" v-model:visible="passwordDlg"

4
packages/nc-gui/pages/index/index.vue

@ -22,11 +22,11 @@ const route = useRoute()
<div class="flex flex-1 justify-between gap-6 lg:block"> <div class="flex flex-1 justify-between gap-6 lg:block">
<template v-if="route.name === 'index-index'"> <template v-if="route.name === 'index-index'">
<TransitionGroup name="page" mode="out-in"> <TransitionGroup name="page" mode="out-in">
<div> <div key="social-card">
<GeneralSocialCard /> <GeneralSocialCard />
</div> </div>
<div class="block mt-0 lg:(!mt-6) xl:hidden"> <div key="sponsors" class="block mt-0 lg:(!mt-6) xl:hidden">
<GeneralSponsors /> <GeneralSponsors />
</div> </div>
</TransitionGroup> </TransitionGroup>

4
packages/nc-gui/pages/index/index/[projectId].vue

@ -57,7 +57,7 @@ onMounted(async () => {
setTimeout(() => { setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]') const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.title.length) input.setSelectionRange(0, formState.title?.length)
input.focus() input.focus()
}, 500) }, 500)
@ -95,7 +95,7 @@ onMounted(async () => {
</a-form-item> </a-form-item>
<div class="text-center"> <div class="text-center">
<button v-t="['a:project:edit:rename']" type="submit" class="submit"> <button v-e="['a:project:edit:rename']" type="submit" class="submit">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<MaterialSymbolsRocketLaunchOutline /> <MaterialSymbolsRocketLaunchOutline />
{{ $t('general.edit') }} {{ $t('general.edit') }}

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

Loading…
Cancel
Save