Browse Source

Merge branch 'develop' into fix/gui-v2-table-rename

pull/3117/head
Wing-Kam Wong 2 years ago
parent
commit
a82ef941f9
  1. 64
      packages/nc-gui-v2/assets/style-v2.scss
  2. 25
      packages/nc-gui-v2/components.d.ts
  3. 4
      packages/nc-gui-v2/components/api-client/Headers.vue
  4. 15
      packages/nc-gui-v2/components/cell/DatePicker.vue
  5. 73
      packages/nc-gui-v2/components/cell/MultiSelect.vue
  6. 28
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  7. 15
      packages/nc-gui-v2/components/cell/Url.vue
  8. 15
      packages/nc-gui-v2/components/cell/YearPicker.vue
  9. 6
      packages/nc-gui-v2/components/cell/attachment/Carousel.vue
  10. 21
      packages/nc-gui-v2/components/cell/attachment/Modal.vue
  11. 3
      packages/nc-gui-v2/components/cell/attachment/index.vue
  12. 4
      packages/nc-gui-v2/components/cell/attachment/utils.ts
  13. 9
      packages/nc-gui-v2/components/dashboard/GithubStarButton.vue
  14. 11
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  15. 16
      packages/nc-gui-v2/components/dashboard/settings/Misc.vue
  16. 5
      packages/nc-gui-v2/components/dashboard/settings/Modal.vue
  17. 7
      packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue
  18. 14
      packages/nc-gui-v2/components/dashboard/settings/app-store/AppInstall.vue
  19. 32
      packages/nc-gui-v2/components/general/ColorPicker.vue
  20. 18
      packages/nc-gui-v2/components/general/PreviewAs.vue
  21. 69
      packages/nc-gui-v2/components/general/ReleaseInfo.vue
  22. 33
      packages/nc-gui-v2/components/general/ShareBaseButton.vue
  23. 81
      packages/nc-gui-v2/components/general/SocialCard.vue
  24. 45
      packages/nc-gui-v2/components/general/Sponsors.vue
  25. 50
      packages/nc-gui-v2/components/shared-view/AskPassword.vue
  26. 85
      packages/nc-gui-v2/components/shared-view/Form.vue
  27. 32
      packages/nc-gui-v2/components/shared-view/Grid.vue
  28. 32
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  29. 23
      packages/nc-gui-v2/components/smartsheet-column/FormulaOptions.vue
  30. 17
      packages/nc-gui-v2/components/smartsheet-column/LookupOptions.vue
  31. 8
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  32. 35
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  33. 23
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  34. 5
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue
  35. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  36. 98
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  37. 12
      packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue
  38. 6
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  39. 145
      packages/nc-gui-v2/components/smartsheet/Form.vue
  40. 39
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  41. 22
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  42. 10
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  43. 38
      packages/nc-gui-v2/components/smartsheet/VirtualCell.vue
  44. 8
      packages/nc-gui-v2/components/smartsheet/expanded-form/Comments.vue
  45. 8
      packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue
  46. 47
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  47. 22
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  48. 1
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  49. 5
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  50. 17
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  51. 32
      packages/nc-gui-v2/components/smartsheet/sidebar/menu/ApiSnippet.vue
  52. 28
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/DebugMeta.vue
  53. 22
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/DeleteCache.vue
  54. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/DeleteTable.vue
  55. 32
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ExportCache.vue
  56. 31
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  57. 2
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  58. 9
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  59. 2
      packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue
  60. 55
      packages/nc-gui-v2/components/template/Editor.vue
  61. 29
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  62. 4
      packages/nc-gui-v2/components/virtual-cell/Formula.vue
  63. 27
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  64. 12
      packages/nc-gui-v2/components/virtual-cell/Lookup.vue
  65. 24
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  66. 2
      packages/nc-gui-v2/components/virtual-cell/Rollup.vue
  67. 26
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  68. 21
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  69. 19
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  70. 66
      packages/nc-gui-v2/components/webhook/ChannelMultiSelect.vue
  71. 2
      packages/nc-gui-v2/components/webhook/Drawer.vue
  72. 93
      packages/nc-gui-v2/components/webhook/Editor.vue
  73. 10
      packages/nc-gui-v2/components/webhook/List.vue
  74. 2
      packages/nc-gui-v2/components/webhook/Test.vue
  75. 14
      packages/nc-gui-v2/composables/useColumnCreateStore.ts
  76. 26
      packages/nc-gui-v2/composables/useExpandedFormStore.ts
  77. 20
      packages/nc-gui-v2/composables/useGlobal/state.ts
  78. 6
      packages/nc-gui-v2/composables/useGlobal/types.ts
  79. 8
      packages/nc-gui-v2/composables/useGridViewColumnWidth.ts
  80. 33
      packages/nc-gui-v2/composables/useLTARStore.ts
  81. 10
      packages/nc-gui-v2/composables/useMetas.ts
  82. 31
      packages/nc-gui-v2/composables/useProject.ts
  83. 108
      packages/nc-gui-v2/composables/useSharedView.ts
  84. 16
      packages/nc-gui-v2/composables/useSmartsheetRowStore.ts
  85. 4
      packages/nc-gui-v2/composables/useTabs.ts
  86. 26
      packages/nc-gui-v2/composables/useUIPermission/index.ts
  87. 51
      packages/nc-gui-v2/composables/useViewColumns.ts
  88. 51
      packages/nc-gui-v2/composables/useViewData.ts
  89. 97
      packages/nc-gui-v2/composables/useViewFilters.ts
  90. 45
      packages/nc-gui-v2/composables/useViewSorts.ts
  91. 5
      packages/nc-gui-v2/composables/useViews.ts
  92. 1
      packages/nc-gui-v2/context/index.ts
  93. 3
      packages/nc-gui-v2/httpsnippet-shims.d.ts
  94. 1
      packages/nc-gui-v2/just-clone-shims.d.ts
  95. 8
      packages/nc-gui-v2/layouts/base.vue
  96. 37
      packages/nc-gui-v2/layouts/shared-view.vue
  97. 15
      packages/nc-gui-v2/lib/types.ts
  98. 20
      packages/nc-gui-v2/nuxt.config.ts
  99. 380
      packages/nc-gui-v2/package-lock.json
  100. 6
      packages/nc-gui-v2/package.json
  101. Some files were not shown because too many files have changed in this diff Show More

64
packages/nc-gui-v2/assets/style-v2.scss

@ -31,31 +31,6 @@ nav .v-list {
@apply dark:bg-white
}
.page-enter-active,
.page-leave-active,
.layout-enter-active,
.layout-leave-active {
@apply transition-opacity duration-300 ease-in-out;
}
.page-enter,
.page-leave-active,
.layout-enter,
.layout-leave-active {
@apply opacity-0;
}
.slide-enter-active,
.slide-leave-active {
@apply transition-all duration-200 ease-in-out;
transform: translate(100%, 0);
}
.slide-enter,
.slide-leave-active {
transform: translate(-100%, 0);
}
a {
@apply prose text-primary underline hover:opacity-75 dark:(text-secondary) hover:(opacity-75);
}
@ -153,3 +128,42 @@ html {
background-position: 0% 22%
}
}
.page-enter-active,
.page-leave-active,
.layout-enter-active,
.layout-leave-active {
@apply transition-opacity duration-300 ease-in-out;
}
.page-enter,
.page-leave-active,
.layout-enter,
.layout-leave-active {
@apply opacity-0;
}
.slide-enter-active,
.slide-leave-active {
@apply transition-all duration-200 ease-in-out;
transform: translate(100%, 0);
}
.slide-enter,
.slide-leave-active {
transform: translate(-100%, 0);
}
.glow-enter-active,
.glow-leave-active {
@apply transition-all duration-300 ease-in-out;
}
.glow-enter-active {
@apply ring ring-xl;
}
.glow-enter,
.glow-leave-active {
@apply opacity-0;
}

25
packages/nc-gui-v2/components.d.ts vendored

@ -61,27 +61,31 @@ declare module '@vue/runtime-core' {
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypography: typeof import('ant-design-vue/es')['Typography']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
EvaEmailOutline: typeof import('~icons/eva/email-outline')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
IcRoundSearch: typeof import('~icons/ic/round-search')['default']
MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-rounded')['default']
MaterialSymbolsArrowCircleRightRounded: typeof import('~icons/material-symbols/arrow-circle-right-rounded')['default']
MaterialSymbolsAttachFile: typeof import('~icons/material-symbols/attach-file')['default']
MaterialSymbolsChevronLeftRounded: typeof import('~icons/material-symbols/chevron-left-rounded')['default']
MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default']
MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsRocketLaunchOutline: typeof import('~icons/material-symbols/rocket-launch-outline')['default']
MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
MdiAccountGroup: typeof import('~icons/mdi/account-group')['default']
MdiAccountIcon: typeof import('~icons/mdi/account-icon')['default']
MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default']
MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
@ -90,6 +94,7 @@ declare module '@vue/runtime-core' {
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiAt: typeof import('~icons/mdi/at')['default']
MdiBugOutline: typeof import('~icons/mdi/bug-outline')['default']
MdiCalculator: typeof import('~icons/mdi/calculator')['default']
MdiCalendarMonth: typeof import('~icons/mdi/calendar-month')['default']
MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default']
@ -100,21 +105,29 @@ declare module '@vue/runtime-core' {
MdiClose: typeof import('~icons/mdi/close')['default']
MdiCloseCircle: typeof import('~icons/mdi/close-circle')['default']
MdiCloseThick: typeof import('~icons/mdi/close-thick')['default']
MdiCodeJson: typeof import('~icons/mdi/code-json')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
MdiDelete: typeof import('~icons/mdi/delete')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
MdiDiscord: typeof import('~icons/mdi/discord')['default']
MdiDotsHorizontal: typeof import('~icons/mdi/dots-horizontal')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiDownload: typeof import('~icons/mdi/download')['default']
MdiDownloadOutline: typeof import('~icons/mdi/download-outline')['default']
MdiDrag: typeof import('~icons/mdi/drag')['default']
MdiDragVertical: typeof import('~icons/mdi/drag-vertical')['default']
MdiDramaMasks: typeof import('~icons/mdi/drama-masks')['default']
MdiEditOutline: typeof import('~icons/mdi/edit-outline')['default']
MdiEmail: typeof import('~icons/mdi/email')['default']
MdiEmailArrowRightOutline: typeof import('~icons/mdi/email-arrow-right-outline')['default']
MdiExitToApp: typeof import('~icons/mdi/exit-to-app')['default']
MdiExport: typeof import('~icons/mdi/export')['default']
MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default']
MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default']
MdiFileExcel: typeof import('~icons/mdi/file-excel')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFlashOutline: typeof import('~icons/mdi/flash-outline')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
@ -136,10 +149,13 @@ declare module '@vue/runtime-core' {
MdiNumeric: typeof import('~icons/mdi/numeric')['default']
MdiOpenInNew: typeof import('~icons/mdi/open-in-new')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiPlusBoxOutline: typeof import('~icons/mdi/plus-box-outline')['default']
MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default']
MdiRefresh: typeof import('~icons/mdi/refresh')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']
MdiSearch: typeof import('~icons/mdi/search')['default']
MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiStar: typeof import('~icons/mdi/star')['default']
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
@ -147,10 +163,13 @@ declare module '@vue/runtime-core' {
MdiTable: typeof import('~icons/mdi/table')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
MdiTableBorder: typeof import('~icons/mdi/table-border')['default']
MdiTableLarge: typeof import('~icons/mdi/table-large')['default']
MdiText: typeof import('~icons/mdi/text')['default']
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']
MdiTrashCan: typeof import('~icons/mdi/trash-can')['default']
MdiTwitter: typeof import('~icons/mdi/twitter')['default']
MdiUploadOutline: typeof import('~icons/mdi/upload-outline')['default']
MdiViewListOutline: typeof import('~icons/mdi/view-list-outline')['default']
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MdiXml: typeof import('~icons/mdi/xml')['default']
RouterLink: typeof import('vue-router')['RouterLink']

4
packages/nc-gui-v2/components/api-client/Headers.vue

@ -85,7 +85,7 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
<a-checkbox v-model:checked="headerRow.enabled" />
</a-form-item>
</td>
<td class="px-2">
<td class="px-2 w-min-[400px]">
<a-form-item>
<a-select v-model:value="headerRow.name" size="large" placeholder="Key">
<a-select-option v-for="(header, i) in headerList" :key="i" :value="header">
@ -94,7 +94,7 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
</a-select>
</a-form-item>
</td>
<td class="px-2">
<td class="px-2 w-min-[400px]">
<a-form-item>
<a-input v-model:value="headerRow.value" size="large" placeholder="Value" />
</a-form-item>

15
packages/nc-gui-v2/components/cell/DatePicker.vue

@ -1,19 +1,18 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { ColumnInj } from '#imports'
import { EditModeInj } from '~/context'
import { ColumnInj, EditModeInj, computed, inject, ref, watch } from '#imports'
interface Props {
modelValue: string | null | undefined
modelValue?: string | null
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const columnMeta = inject(ColumnInj, null)
const columnMeta = inject(ColumnInj, null)!
const editEnabled = inject(EditModeInj)
const editEnabled = inject(EditModeInj)!
let isDateInvalid = $ref(false)
@ -43,7 +42,7 @@ const localState = $computed({
}
},
})
const open = ref(false)
const open = ref<boolean>(false)
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
watch(
@ -55,6 +54,8 @@ watch(
},
{ flush: 'post' },
)
const placeholder = computed(() => (isDateInvalid ? 'Invalid date' : editEnabled.value ? 'Select date' : ''))
</script>
<template>
@ -63,7 +64,7 @@ watch(
:bordered="false"
class="!w-full px-1"
:format="dateFormat"
:placeholder="isDateInvalid ? 'Invalid date' : !readOnlyMode ? 'Select date' : ''"
:placeholder="placeholder"
:allow-clear="!editEnabled"
:input-read-only="true"
:dropdown-class-name="randomClass"

73
packages/nc-gui-v2/components/cell/MultiSelect.vue

@ -1,12 +1,23 @@
<script lang="ts" setup>
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk'
import { ActiveCellInj, ColumnInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
import type { SelectOptionsType } from 'nocodb-sdk'
import {
ActiveCellInj,
ColumnInj,
ReadonlyInj,
computed,
h,
inject,
onMounted,
ref,
useEventListener,
useProject,
watch,
} from '#imports'
import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props {
modelValue?: string | string[] | undefined
modelValue?: string | string[]
}
const { modelValue } = defineProps<Props>()
@ -15,11 +26,9 @@ const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const column = inject(ColumnInj)
const column = inject(ColumnInj)!
// const isForm = inject<boolean>('isForm', false)
const editEnabled = inject(EditModeInj)
const readOnly = inject(ReadonlyInj)!
const active = inject(ActiveCellInj, ref(false))
@ -29,12 +38,12 @@ const aselect = ref<typeof AntSelect>()
const isOpen = ref(false)
const options = computed(() => {
const options = computed<SelectOptionsType[]>(() => {
if (column?.value.colOptions) {
const opts = column.value.colOptions
? column.value.colOptions.options.filter((el: SelectOptionType) => el.title !== '') || []
? (column.value.colOptions as any).options.filter((el: any) => el.title !== '') || []
: []
for (const op of opts.filter((el: SelectOptionType) => el.order === null)) {
for (const op of opts.filter((el: any) => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')
}
return opts
@ -43,7 +52,7 @@ const options = computed(() => {
})
const vModel = computed({
get: () => selectedIds.value.map((el) => options.value.find((op: SelectOptionType) => op.id === el)?.title),
get: () => selectedIds.value.map((el) => options.value.find((op) => op.id === el)?.title),
set: (val) => emit('update:modelValue', val.length === 0 ? null : val.join(',')),
})
@ -52,10 +61,10 @@ const selectedTitles = computed(() =>
? typeof modelValue === 'string'
? isMysql
? modelValue.split(',').sort((a, b) => {
const opa = options.value.find((el: SelectOptionType) => el.title === a)
const opb = options.value.find((el: SelectOptionType) => el.title === b)
const opa = options.value.find((el) => el.title === a)
const opb = options.value.find((el) => el.title === b)
if (opa && opb) {
return opa.order - opb.order
return opa.order! - opb.order!
}
return 0
})
@ -83,8 +92,13 @@ const handleClose = (e: MouseEvent) => {
}
onMounted(() => {
selectedIds.value = selectedTitles.value.map((el) => {
return options.value.find((op: SelectOptionType) => op.title === el).id
selectedIds.value = selectedTitles.value.flatMap((el) => {
const item = options.value.find((op) => op.title === el)?.id
if (item) {
return [item]
}
return []
})
})
@ -92,17 +106,20 @@ useEventListener(document, 'click', handleClose)
watch(
() => modelValue,
(_n, _o) => {
selectedIds.value = selectedTitles.value.map((el) => {
return options.value.find((op: SelectOptionType) => op.title === el).id
() => {
selectedIds.value = selectedIds.value = selectedTitles.value.flatMap((el) => {
const item = options.value.find((op) => op.title === el)?.id
if (item) {
return [item]
}
return []
})
},
)
watch(isOpen, (n, _o) => {
if (n === false) {
aselect.value.blur()
}
if (!n) aselect.value?.$el.blur()
})
</script>
@ -112,12 +129,12 @@ watch(isOpen, (n, _o) => {
v-model:value="vModel"
mode="multiple"
class="w-full"
placeholder="Select an option"
:placeholder="!readOnly ? 'Select an option' : ''"
:bordered="false"
show-arrow
:show-arrow="!readOnly"
:show-search="false"
:open="isOpen"
:disabled="!editEnabled"
:disabled="readOnly"
@keydown="handleKeys"
@click="isOpen = !isOpen"
>
@ -128,10 +145,10 @@ watch(isOpen, (n, _o) => {
</a-select-option>
<template #tagRender="{ value: val, onClose }">
<a-tag
v-if="options.find((el: SelectOptionType) => el.title === val)"
v-if="options.find((el) => el.title === val)"
class="rounded-tag"
:style="{ display: 'flex', alignItems: 'center' }"
:color="options.find((el: SelectOptionType) => el.title === val).color"
:color="options.find((el) => el.title === val).color"
:closable="active && (vModel.length > 1 || !column?.rqd)"
:close-icon="h(MdiCloseCircle, { class: ['ms-close-icon'] })"
@close="onClose"

28
packages/nc-gui-v2/components/cell/SingleSelect.vue

@ -1,8 +1,7 @@
<script lang="ts" setup>
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk'
import { ActiveCellInj, ColumnInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
import type { SelectOptionsType } from 'nocodb-sdk'
import { ActiveCellInj, ColumnInj, ReadonlyInj, computed, inject, ref, useEventListener, watch } from '#imports'
interface Props {
modelValue?: string | undefined
@ -12,11 +11,9 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)
const column = inject(ColumnInj)!
// const isForm = inject<boolean>('isForm', false)
const editEnabled = inject(EditModeInj)
const readOnly = inject(ReadonlyInj)!
const active = inject(ActiveCellInj, ref(false))
@ -29,12 +26,13 @@ const vModel = computed({
set: (val) => emit('update:modelValue', val || null),
})
const options = computed(() => {
const options = computed<SelectOptionsType[]>(() => {
if (column?.value.colOptions) {
const opts = column.value.colOptions
? column.value.colOptions.options.filter((el: SelectOptionType) => el.title !== '') || []
? // todo: fix colOptions type, options does not exist as a property
(column.value.colOptions as any).options.filter((el: SelectOptionsType) => el.title !== '') || []
: []
for (const op of opts.filter((el: SelectOptionType) => el.order === null)) {
for (const op of opts.filter((el: any) => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')
}
return opts
@ -61,9 +59,7 @@ const handleClose = (e: MouseEvent) => {
useEventListener(document, 'click', handleClose)
watch(isOpen, (n, _o) => {
if (n === false) {
aselect.value.blur()
}
if (!n) aselect.value?.$el.blur()
})
</script>
@ -73,11 +69,11 @@ watch(isOpen, (n, _o) => {
v-model:value="vModel"
class="w-full"
:allow-clear="!column.rqd && active"
placeholder="Select an option"
:placeholder="!readOnly ? 'Select an option' : ''"
:bordered="false"
:open="isOpen"
:disabled="!editEnabled"
:show-arrow="active || vModel === null"
:disabled="readOnly"
:show-arrow="!readOnly && (active || vModel === null)"
@select="isOpen = false"
@keydown="handleKeys"
@click="isOpen = !isOpen"

15
packages/nc-gui-v2/components/cell/Url.vue

@ -1,10 +1,9 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { ColumnInj, computed, inject, isValidURL } from '#imports'
import { EditModeInj } from '~/context'
import { ColumnInj, EditModeInj, computed, inject, isValidURL } from '#imports'
interface Props {
modelValue: string | null | undefined
modelValue?: string | null
}
const { modelValue: value } = defineProps<Props>()
@ -13,7 +12,7 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)
const editEnabled = inject(EditModeInj)!
const vModel = computed({
get: () => value,
@ -26,10 +25,12 @@ const vModel = computed({
const isValid = computed(() => value && isValidURL(value))
const url = computed<string | null>(() => {
if (!value || !isValidURL(value)) return null
const url = computed(() => {
if (!value || !isValidURL(value)) return ''
/** add url scheme if missing */
if (/^https?:\/\//.test(value)) return value
return `https://${value}`
})
@ -38,7 +39,9 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<template>
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none text-sm" @blur="editEnabled = false" />
<nuxt-link v-else-if="isValid" class="text-sm underline hover:opacity-75" :to="url" target="_blank">{{ value }} </nuxt-link>
<span v-else>{{ value }}</span>
</template>

15
packages/nc-gui-v2/components/cell/YearPicker.vue

@ -1,17 +1,16 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs'
import { EditModeInj } from '~/context'
import { EditModeInj, computed, inject, onClickOutside, ref, watch } from '#imports'
interface Props {
modelValue: number | string | null | undefined
modelValue?: number | string | null
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
const editEnabled = inject(EditModeInj)!
let isYearInvalid = $ref(false)
@ -41,7 +40,7 @@ const localState = $computed({
},
})
const open = ref(false)
const open = ref<boolean>(false)
const randomClass = `picker_${Math.floor(Math.random() * 99999)}`
watch(
@ -53,6 +52,8 @@ watch(
},
{ flush: 'post' },
)
const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : editEnabled.value ? 'Select year' : ''))
</script>
<template>
@ -61,7 +62,7 @@ watch(
picker="year"
:bordered="false"
class="!w-full px-1"
:placeholder="isYearInvalid ? 'Invalid year' : !readOnlyMode ? 'Select year' : ''"
:placeholder="placeholder"
:allow-clear="!editEnabled"
:input-read-only="true"
:open="editEnabled ? false : open"
@ -72,5 +73,3 @@ watch(
<template #suffixIcon></template>
</a-date-picker>
</template>
<style scoped></style>

6
packages/nc-gui-v2/components/cell/attachment/Carousel.vue

@ -1,11 +1,7 @@
<script lang="ts" setup>
import { onKeyDown } from '@vueuse/core'
import { useAttachmentCell } from './utils'
import { isImage } from '~/utils'
import { computed, onClickOutside, ref } from '#imports'
import MaterialSymbolsArrowCircleRightRounded from '~icons/material-symbols/arrow-circle-right-rounded'
import MaterialSymbolsArrowCircleLeftRounded from '~icons/material-symbols/arrow-circle-left-rounded'
import MdiCloseCircle from '~icons/mdi/close-circle'
import { computed, isImage, onClickOutside, ref } from '#imports'
const { selectedImage, visibleItems, downloadFile } = useAttachmentCell()!

21
packages/nc-gui-v2/components/cell/attachment/Modal.vue

@ -2,8 +2,7 @@
import { onKeyDown } from '@vueuse/core'
import { useAttachmentCell } from './utils'
import { useSortable } from './sort'
import { ref, useDropZone, useUIPermission } from '#imports'
import { isImage, openLink } from '~/utils'
import { isImage, openLink, ref, useDropZone, useUIPermission, watch } from '#imports'
const { isUIAllowed } = useUIPermission()
@ -39,6 +38,22 @@ onKeyDown('Escape', () => {
modalVisible.value = false
isOverDropZone.value = false
})
function onClick(item: Record<string, any>) {
selectedImage.value = item
modalVisible.value = false
const stopHandle = watch(selectedImage, (nextImage, _, onCleanup) => {
if (!nextImage) {
setTimeout(() => {
modalVisible.value = true
}, 50)
stopHandle?.()
}
onCleanup(() => stopHandle?.())
})
}
</script>
<template>
@ -103,7 +118,7 @@ onKeyDown('Escape', () => {
v-if="isImage(item.title, item.mimetype)"
:style="{ backgroundImage: `url('${item.url}')` }"
class="w-full h-full bg-contain bg-center bg-no-repeat"
@click.stop="() => (selectedImage = item) && (modalVisible = false)"
@click.stop="onClick(item)"
/>
<component

3
packages/nc-gui-v2/components/cell/attachment/index.vue

@ -4,8 +4,7 @@ import { useProvideAttachmentCell } from './utils'
import { useSortable } from './sort'
import Modal from './Modal.vue'
import Carousel from './Carousel.vue'
import { computed, ref, useDropZone, useSmartsheetStoreOrThrow, watch } from '#imports'
import { isImage, openLink } from '~/utils'
import { computed, isImage, openLink, ref, useDropZone, useSmartsheetStoreOrThrow, watch } from '#imports'
interface Props {
modelValue: string | Record<string, any>[] | null

4
packages/nc-gui-v2/components/cell/attachment/utils.ts

@ -141,7 +141,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
editEnabled,
isLoading,
api,
open,
open: () => open(),
onDrop,
modalVisible,
FileIcon,
@ -151,5 +151,5 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
selectedImage,
}
},
'attachmentCell',
'useAttachmentCell',
)

9
packages/nc-gui-v2/components/dashboard/GithubStarButton.vue

@ -0,0 +1,9 @@
<script setup lang="ts">
import GithubButton from 'vue-github-button'
</script>
<template>
<GithubButton href="https://github.com/nocodb/nocodb" data-icon="octicon-star" data-show-count="true" data-size="large"
>Star</GithubButton
>
</template>

11
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -3,12 +3,13 @@ import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { Empty } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { computed, useProject, useTable, useTabs, watchEffect } from '#imports'
import { computed, useProject, useTable, useTabs, useUIPermission, watchEffect } from '#imports'
import { TabType } from '~/composables'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiDrag from '~icons/mdi/drag-vertical'
import GithubStarButton from '~/components/dashboard/GithubStarButton.vue'
const { addTab } = useTabs()
@ -136,7 +137,7 @@ const activeTable = computed(() => {
<template>
<div class="nc-treeview-container flex flex-col">
<div class="px-6 py-[8.75px] border-b-1 nc-filter-input">
<div class="px-6 py-[9px] border-b-1 nc-filter-input">
<div class="flex items-center bg-gray-50 rounded relative">
<a-input
v-model:value="filterQuery"
@ -266,6 +267,12 @@ const activeTable = computed(() => {
</template>
</a-dropdown>
<a-divider class="mt-0 mb-2" />
<div class="items-center flex justify-center mb-1">
<GithubStarButton />
</div>
<DlgTableCreate v-if="tableCreateDlg" v-model="tableCreateDlg" />
<DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" />
</div>

16
packages/nc-gui-v2/components/dashboard/settings/Misc.vue

@ -0,0 +1,16 @@
<script setup lang="ts">
const { includeM2M } = useGlobal()
const { loadTables } = useProject()
watch(includeM2M, async () => await loadTables())
</script>
<template>
<div class="flex flex-row w-full">
<div class="flex flex-column w-full">
<div class="flex flex-row items-center w-full mb-4 gap-2">
<a-checkbox v-model:checked="includeM2M">Show M2M Tables</a-checkbox>
</div>
</div>
</div>
</template>

5
packages/nc-gui-v2/components/dashboard/settings/Modal.vue

@ -5,6 +5,7 @@ import AuditTab from './AuditTab.vue'
import AppStore from './AppStore.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import Misc from './Misc.vue'
import ApiTokenManagement from '~/components/tabs/auth/ApiTokenManagement.vue'
import UserManagement from '~/components/tabs/auth/UserManagement.vue'
import StoreFrontOutline from '~icons/mdi/storefront-outline'
@ -86,6 +87,10 @@ const tabsInfo: TabGroup = {
title: 'UI Access Control',
body: UIAcl,
},
misc: {
title: 'Misc',
body: Misc,
},
},
},
audit: {

7
packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue

@ -7,6 +7,8 @@ const { $api, $e } = useNuxtApp()
const { project } = useProject()
const { includeM2M } = useGlobal()
const roles = $ref<string[]>(['editor', 'commenter', 'viewer'])
let isLoading = $ref(false)
@ -28,9 +30,10 @@ async function loadTableList() {
if (!project.value?.id) return
isLoading = true
// TODO includeM2M
tables = await $api.project.modelVisibilityList(project.value?.id, {
includeM2M: false,
// FIXME: type
includeM2M: includeM2M.value || '',
})
} catch (e) {
console.error(e)

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

@ -1,11 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import type { PluginType } from 'nocodb-sdk'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import CloseIcon from '~icons/material-symbols/close-rounded'
import MdiPlusIcon from '~icons/mdi/plus'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { extractSdkResponseErrorMsg, ref, useNuxtApp } from '#imports'
interface Props {
id: string
@ -139,7 +135,7 @@ onMounted(async () => {
class="mr-1 flex items-center justify-center"
:class="[plugin.title === 'SES' ? 'p-2 bg-[#242f3e]' : '']"
>
<img :src="`/${plugin.logo}`" class="h-6" />
<img :alt="plugin.title || 'plugin'" :src="`/${plugin.logo}`" class="h-6" />
</div>
<span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span>
@ -147,7 +143,7 @@ onMounted(async () => {
<div class="absolute -right-2 -top-0.5">
<a-button type="text" class="!rounded-md mr-1" @click="emits('close')">
<template #icon>
<CloseIcon class="flex mx-auto" />
<MaterialSymbolsCloseRounded class="flex mx-auto" />
</template>
</a-button>
</div>
@ -194,7 +190,7 @@ onMounted(async () => {
v-if="itemIndex !== 0 && columnIndex === plugin.formDetails.items.length - 1"
class="absolute flex flex-col justify-start mt-2 -right-6 top-0"
>
<MdiDeleteOutlineIcon class="hover:text-red-400 cursor-pointer" @click="deleteFormRow(itemIndex)" />
<MdiDeleteOutline class="hover:text-red-400 cursor-pointer" @click="deleteFormRow(itemIndex)" />
</div>
</a-form-item>
</td>
@ -205,7 +201,7 @@ onMounted(async () => {
<td :colspan="plugin.formDetails.items.length" class="text-center">
<a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addSetting">
<template #icon>
<MdiPlusIcon class="flex mx-auto" />
<MdiPlus class="flex mx-auto" />
</template>
</a-button>
</td>

32
packages/nc-gui-v2/components/general/ColorPicker.vue

@ -1,21 +1,22 @@
<script lang="ts" setup>
import { Chrome } from '@ckpack/vue-color'
import { enumColor } from '@/utils'
import { computed, ref, watch } from '#imports'
interface Props {
modelValue: string | any
modelValue?: string | any
colors?: string[]
rowSize?: number
advanced?: Boolean
pickButton?: Boolean
advanced?: boolean
pickButton?: boolean
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => enumColor.light[0],
colors: () => enumColor.light.concat(enumColor.dark),
rowSize: () => 10,
advanced: () => true,
pickButton: () => false,
rowSize: 10,
advanced: true,
pickButton: false,
})
const emit = defineEmits(['update:modelValue'])
@ -27,23 +28,18 @@ const vModel = computed({
},
})
const picked = ref(props.modelValue || enumColor.light[0])
const picked = ref<string | Record<string, any>>(props.modelValue || enumColor.light[0])
const selectColor = (color: any) => {
picked.value = color.hex ? color.hex : color
vModel.value = color.hex ? color.hex : color
const selectColor = (color: string | Record<string, any>) => {
picked.value = typeof color === 'string' ? color : color.hex ? color.hex : color
vModel.value = typeof color === 'string' ? color : color.hex ? color.hex : color
}
const compare = (colorA: String, colorB: String) => {
if ((typeof colorA === 'string' || colorA instanceof String) && (typeof colorB === 'string' || colorB instanceof String)) {
return colorA.toLowerCase() === colorB.toLowerCase()
}
return false
}
const compare = (colorA: string, colorB: string) => colorA.toLowerCase() === colorB.toLowerCase()
watch(picked, (n, _o) => {
if (!props.pickButton) {
vModel.value = n.hex ? n.hex : n
vModel.value = typeof n === 'string' ? n : n.hex ? n.hex : n
}
})
</script>
@ -52,7 +48,7 @@ watch(picked, (n, _o) => {
<div class="color-picker">
<div v-for="colId in Math.ceil(props.colors.length / props.rowSize)" :key="colId" class="color-picker-row">
<button
v-for="(color, i) in colors.slice((colId - 1) * rowSize, colId * rowSize)"
v-for="(color, i) of colors.slice((colId - 1) * rowSize, colId * rowSize)"
:key="`color-${colId}-${i}`"
class="color-selector"
:class="compare(picked, color) ? 'selected' : ''"

18
packages/nc-gui-v2/components/general/PreviewAs.vue

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { onUnmounted, useEventListener, useGlobal, watch } from '#imports'
import { useState } from '#app'
import { onUnmounted, useEventListener, useGlobal, useState, watch } from '#imports'
import MdiAccountStar from '~icons/mdi/account-star'
import MdiAccountHardHat from '~icons/mdi/account-hard-hat'
import MdiAccountEdit from '~icons/mdi/account-edit'
@ -54,13 +53,14 @@ watch(previewAs, () => window.location.reload())
:style="{ top: position.y, left: position.x }"
>
<MdiDrag style="cursor: move" class="text-white" @mousedown="mouseDown" />
<div class="divider" />
<div class="pointer flex items-center gap-4">
<span>Preview as :</span>
<a-radio-group v-model:value="previewAs" name="radioGroup">
<a-radio v-for="role in roleList" :key="role.title" class="capitalize !text-white" :value="role.title"
<a-radio v-for="role of roleList" :key="role.title" class="capitalize !text-white" :value="role.title"
>{{ role.title }}
</a-radio>
</a-radio-group>
@ -77,8 +77,8 @@ watch(previewAs, () => window.location.reload())
<template v-else>
<template v-for="role of roleList" :key="role.title">
<a-menu-item :class="`pointer nc-preview-${role.title}`" @click="previewAs = role.title">
<div class="p-1 flex gap-2 items-center">
<component :is="roleIcon[role.title]" />
<div class="nc-project-menu-item group">
<component :is="roleIcon[role.title]" class="group-hover:text-pink-500" />
<span class="capitalize" :class="{ 'x-active--text': role.title === previewAs }">{{ role.title }}</span>
</div>
@ -87,8 +87,8 @@ watch(previewAs, () => window.location.reload())
<template v-if="previewAs">
<a-menu-item @click="previewAs = null">
<div class="p-1 flex gap-2 items-center">
<mdi-close />
<div class="nc-project-menu-item group">
<MdiClose class="group-hover:text-pink-500" />
<!-- Reset Preview -->
<span class="text-capitalize text-xs whitespace-nowrap">{{ $t('activity.resetReview') }}</span>
</div>
@ -98,6 +98,10 @@ watch(previewAs, () => window.location.reload())
</template>
<style scoped>
.nc-project-menu-item {
@apply cursor-pointer flex items-center gap-2 py-2 hover:text-primary after:(content-[''] absolute top-0 left-0 bottom-0 right-0 w-full h-full bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
}
.floating-reset-btn {
@apply z-1000 index-100 fixed text-white
@apply flex items-center overflow-hidden whitespace-nowrap gap-4 rounded shadow-md;

69
packages/nc-gui-v2/components/general/ReleaseInfo.vue

@ -0,0 +1,69 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, onMounted } from '#imports'
const { $api } = useNuxtApp()
const { currentVersion, latestRelease, hiddenRelease } = useGlobal()
const releaseAlert = computed(
() =>
currentVersion.value &&
latestRelease.value &&
currentVersion.value !== latestRelease.value &&
latestRelease.value !== hiddenRelease.value,
)
async function fetchReleaseInfo() {
try {
const versionInfo = await $api.utils.appVersion()
if (versionInfo && versionInfo.releaseVersion && versionInfo.currentVersion && !/[^0-9.]/.test(versionInfo.currentVersion)) {
currentVersion.value = versionInfo.currentVersion
latestRelease.value = versionInfo.releaseVersion
} else {
currentVersion.value = null
latestRelease.value = null
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
onMounted(async () => await fetchReleaseInfo())
</script>
<template>
<div v-if="releaseAlert" class="flex items-center">
<a-dropdown :trigger="['click']" placement="bottom">
<a-button class="bg-primary border-none">
<div class="flex gap-1 align-center text-white">
<span class="text-sm font-weight-medium">{{ $t('activity.upgrade.available') }}</span>
<mdi-menu-down />
</div>
</a-button>
<template #overlay>
<div class="mt-1 bg-white shadow-lg !border">
<nuxt-link class="text-primary" to="https://github.com/nocodb/nocodb/releases" target="_blank">
<div class="nc-menu-item">
<mdi-script-text-outline />
{{ latestRelease }} {{ $t('activity.upgrade.releaseNote') }}
</div>
</nuxt-link>
<nuxt-link class="text-primary" to="https://docs.nocodb.com/getting-started/upgrading" target="_blank">
<div class="nc-menu-item">
<mdi-rocket-launch-outline />
<!-- How to upgrade? -->
{{ $t('activity.upgrade.howTo') }}
</div>
</nuxt-link>
<a-divider class="ma-0" />
<div class="nc-menu-item" @click="latestRelease = null">
<mdi-close />
<!-- Hide menu -->
{{ $t('general.hideMenu') }}
</div>
</div>
</template>
</a-dropdown>
</div>
</template>

33
packages/nc-gui-v2/components/general/ShareBaseButton.vue

@ -0,0 +1,33 @@
<script setup lang="ts">
import { useRoute } from '#imports'
const route = useRoute()
const showUserModal = $ref(false)
const { isUIAllowed } = useUIPermission()
</script>
<template>
<div class="flex items-center mr-4">
<a-button
v-if="
isUIAllowed('newUser') &&
route.name !== 'index' &&
route.name !== 'project-index-create' &&
route.name !== 'project-index-create-external' &&
route.name !== 'index-user-index'
"
size="middle"
type="primary"
class="!bg-white !text-primary rounded"
@click="showUserModal = true"
>
<div class="flex items-center space-x-1">
<mdi-account-supervisor-outline class="mr-1" />
<div>{{ $t('activity.share') }}</div>
</div>
</a-button>
<TabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" />
</div>
</template>

81
packages/nc-gui-v2/components/general/SocialCard.vue

@ -0,0 +1,81 @@
<script setup lang="ts">
import { enumColor as colors } from '#imports'
const { lang: currentLang } = useGlobal()
const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</script>
<template>
<a-card :body-style="{ padding: '0' }" class="w-[300px] shadow-sm rounded-lg">
<a-list class="w-full" dense>
<a-list-item>
<nuxt-link class="text-primary" to="https://github.com/nocodb/nocodb" target="_blank">
<div class="flex items-center text-sm">
<mdi-github class="mx-3 text-lg" />
<div v-if="isRtlLang">
<!-- us on Github -->
{{ $t('labels.community.starUs2') }}
<!-- Star -->
{{ $t('labels.community.starUs1') }}
<mdi-star-outline />
</div>
<div v-else class="flex items-center">
<!-- Star -->
{{ $t('labels.community.starUs1') }}
<mdi-star-outline class="mx-1" />
<!-- us on Github -->
{{ $t('labels.community.starUs2') }}
</div>
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link class="text-primary" to="https://calendly.com/nocodb-meeting" target="_blank">
<div class="flex items-center text-sm">
<mdi-calendar-month class="mx-3 text-lg" :color="colors.dark[3 % colors.dark.length]" />
<!-- Book a Free DEMO -->
<div>
{{ $t('labels.community.bookDemo') }}
</div>
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link class="text-primary" to="https://discord.gg/5RgZmkW" target="_blank">
<div class="flex items-center text-sm">
<mdi-discord class="mx-3 text-lg" :color="colors.dark[0 % colors.dark.length]" />
<!-- Get your questions answered -->
<div>
{{ $t('labels.community.getAnswered') }}
</div>
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link class="text-primary" to="https://twitter.com/NocoDB" target="_blank">
<div class="flex items-center text-sm">
<mdi-twitter class="mx-3 text-lg" :color="colors.dark[1 % colors.dark.length]" />
<!-- Follow NocoDB -->
<div>
{{ $t('labels.community.followNocodb') }}
</div>
</div>
</nuxt-link>
</a-list-item>
<a-list-item v-t="['e:hiring']">
<nuxt-link class="text-primary" target="_blank" to="http://careers.nocodb.com">
<div class="flex items-center text-sm">
<div class="ml-3">🚀 <span class="ml-2">We are Hiring!!!</span></div>
</div>
</nuxt-link>
</a-list-item>
</a-list>
</a-card>
</template>
<style scoped>
:deep(.ant-list-item) {
@apply hover:(bg-gray-100 !text-primary);
}
</style>

45
packages/nc-gui-v2/components/general/Sponsors.vue

@ -1,37 +1,38 @@
<script lang="ts" setup>
import MdiHeartsCard from '~icons/mdi/cards-heart'
interface Props {
nav?: boolean
img?: boolean
}
const { nav = false, img = true } = defineProps<Props>()
const { nav = false } = defineProps<Props>()
</script>
<template>
<v-card :rounded="0" class="dark:bg-gray-900" href="https://github.com/sponsors/nocodb" target="_blank">
<v-img v-if="img" src="/ants-leaf-cutter.jpeg" :cover="true" :aspect-ratio="1" :height="nav ? 80 : ''" />
<a-card class="w-[300px] shadow-sm rounded-lg">
<template #cover>
<img class="max-h-[180px] rounded-t-lg" alt="cover" src="/ants-leaf-cutter.jpeg" />
</template>
<v-card-title v-if="!nav" class="pb-2">
<a-card-meta>
<template #title>
<span v-if="!nav" class="text-xl pb-4">
{{ $t('msg.info.sponsor.header') }}
</v-card-title>
</span>
</template>
</a-card-meta>
<v-card-text v-if="!nav" class="pb-0">
<div v-if="!nav" class="py-5 text-sm">
{{ $t('msg.info.sponsor.message') }}
</v-card-text>
</div>
<v-card-actions class="justify-center">
<v-btn color="primary" class="dark:(!text-white)">
<MdiHeartsCard class="text-red-500 mr-2" />
<div class="flex justify-center">
<nuxt-link href="https://github.com/sponsors/nocodb" target="_blank">
<a-button class="!shadow rounded" size="large">
<div class="flex items-center">
<mdi-cards-heart class="text-red-500 mr-2" />
{{ $t('activity.sponsorUs') }}
</v-btn>
</v-card-actions>
</v-card>
</div>
</a-button>
</nuxt-link>
</div>
</a-card>
</template>
<style>
a img {
margin: 0 !important;
}
</style>

50
packages/nc-gui-v2/components/shared-view/AskPassword.vue

@ -0,0 +1,50 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg } from '~/utils'
interface Props {
modelValue: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const route = useRoute()
const { loadSharedView } = useSharedView()
const formState = ref({ password: undefined })
const vModel = useVModel(props, 'modelValue', emit)
const onFinish = async () => {
try {
await loadSharedView(route.params.viewId as string, formState.value.password)
vModel.value = false
} catch (e: any) {
console.error(e)
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>
<template>
<a-modal
v-model:visible="vModel"
:closable="false"
width="28rem"
centered
:footer="null"
:mask-closable="false"
@close="vModel = false"
>
<div class="w-full flex flex-col">
<a-typography-title :level="4">This shared view is protected</a-typography-title>
<a-form ref="formRef" :model="formState" class="mt-2" @finish="onFinish">
<a-form-item name="password" :rules="[{ required: true, message: 'Password is required' }]">
<a-input-password v-model:value="formState.password" placeholder="Enter password" />
</a-form-item>
<a-button type="primary" html-type="submit">Unlock</a-button>
</a-form>
</div>
</a-modal>
</template>
<style scoped lang="scss"></style>

85
packages/nc-gui-v2/components/shared-view/Form.vue

@ -0,0 +1,85 @@
<script setup lang="ts">
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { FieldsInj, MetaInj } from '#imports'
const fields = inject(FieldsInj, ref([]))
const meta = inject(MetaInj)
const { sharedView } = useSharedView()
const formState = ref(fields.value.reduce((a, v) => ({ ...a, [v.title]: undefined }), {}))
function isRequired(_columnObj: Record<string, any>, required = false) {
let columnObj = _columnObj
if (
columnObj.uidt === UITypes.LinkToAnotherRecord &&
columnObj.colOptions &&
columnObj.colOptions.type === RelationTypes.BELONGS_TO
) {
columnObj = fields.value.find((c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id) as Record<
string,
any
>
}
return required || (columnObj && columnObj.rqd && !columnObj.cdf)
}
useSmartsheetStoreOrThrow()
useProvideSmartsheetRowStore(meta, formState)
const formRef = ref()
</script>
<template>
<div class="flex flex-col my-4 space-y-2 mx-32 items-center">
<div class="flex w-2/3 flex-col mt-10">
<div class="flex flex-col items-start px-14 py-8 bg-gray-50 rounded-md w-full">
<a-typography-title class="border-b-1 border-gray-100 w-full pb-3" :level="1">
{{ sharedView.view.heading }}
</a-typography-title>
<a-typography class="pl-1 text-sm">{{ sharedView.view.subheading }}</a-typography>
</div>
<a-form ref="formRef" :model="formState" class="mt-8 pb-12 mb-8 px-3 bg-gray-50 rounded-md">
<div v-for="(field, index) in fields" :key="index" class="flex flex-col mt-4 px-10 pt-6 space-y-2">
<div class="flex">
<SmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)"
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
<SmartsheetHeaderCell
v-else
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
</div>
<a-form-item
v-if="isVirtualCol(field)"
class="ma-0 gap-0 pa-0"
:name="field.title"
:rules="[{ required: field.required, message: `${field.title} is required` }]"
>
<SmartsheetVirtualCell v-model="formState[field.title]" class="nc-input" :column="field" />
</a-form-item>
<a-form-item
v-else
class="ma-0 gap-0 pa-0"
:name="field.title"
:rules="[{ required: field.required, message: `${field.title} is required` }]"
>
<SmartsheetCell v-model="formState[field.title]" class="nc-input" :column="field" :edit-enabled="true" />
</a-form-item>
</div>
</a-form>
</div>
</div>
</template>
<style scoped lang="scss">
.nc-input {
@apply w-full !bg-white rounded px-2 py-2 min-h-[40px] mt-2 mb-2 flex align-center border-solid border-1 border-primary;
}
</style>

32
packages/nc-gui-v2/components/shared-view/Grid.vue

@ -0,0 +1,32 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReadonlyInj, ReloadViewDataHookInj } from '~/context'
const { sharedView, meta, columns } = useSharedView()
const reloadEventHook = createEventHook<void>()
provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReadonlyInj, ref(true))
provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, columns)
provide(IsPublicInj, ref(true))
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta)
</script>
<template>
<div class="nc-container flex flex-col h-full mt-4 px-6">
<SmartsheetToolbar />
<SmartsheetGrid class="px-3" />
</div>
</template>
<style scoped>
.nc-container {
height: calc(100% - var(--header-height));
flex: 1 1 100%;
}
</style>

32
packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue

@ -17,9 +17,9 @@ const { formState, generateNewColumnMeta, addOrUpdate, onAlter, onUidtOrIdTypeCh
const reloadDataTrigger = inject(ReloadViewDataHookInj)
const advancedOptions = ref(false)
const { getMeta } = useMetas()
const formulaOptionsRef = ref()
const editOrAddRef = ref<HTMLElement>()
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
@ -74,35 +74,27 @@ onMounted(() => {
generateNewColumnMeta()
}
if (formState.value.uidt === UITypes.Formula) {
formulaOptionsRef.value.formulaSuggestionDrawer = true
}
// for cases like formula
if (formState.value && !formState.value.column_name) {
formState.value.column_name = formState.value?.title
}
})
onUnmounted(() => {
if (formState.value.uidt === UITypes.Formula) {
// close formula drawer
formulaOptionsRef.value.formulaSuggestionDrawer = false
}
})
const handleClose = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (
e.target &&
target &&
editOrAddRef?.value &&
!editOrAddRef.value.contains(e.target) &&
!e.target.closest('.ant-dropdown') &&
!e.target.closest('.ant-select') &&
!e.target.closest('.ant-select-item')
!editOrAddRef.value.contains(target) &&
!target.closest('.ant-dropdown') &&
!target.closest('.ant-select') &&
!target.closest('.ant-select-item')
) {
emit('cancel')
}
}
useEventListener(document, 'click', handleClose)
</script>
@ -127,11 +119,7 @@ useEventListener(document, 'click', handleClose)
</a-select-option>
</a-select>
</a-form-item>
<SmartsheetColumnFormulaOptions
v-if="formState.uidt === UITypes.Formula"
ref="formulaOptionsRef"
v-model:value="formState"
/>
<SmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<SmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<SmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<SmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />

23
packages/nc-gui-v2/components/smartsheet-column/FormulaOptions.vue

@ -4,9 +4,8 @@ import type { ListItem as AntListItem } from 'ant-design-vue'
import jsep from 'jsep'
import type { ColumnType } from 'nocodb-sdk'
import { UITypes, jsepCurlyHook } from 'nocodb-sdk'
import { onMounted, useDebounceFn } from '#imports'
import { MetaInj } from '~/context'
import {
MetaInj,
NcAutocompleteTree,
formulaList,
formulaTypes,
@ -14,8 +13,11 @@ import {
getUIDTIcon,
getWordUntilCaret,
insertAtCursor,
onMounted,
useColumnCreateStoreOrThrow,
useDebounceFn,
validateDateWithUnknownFormat,
} from '@/utils'
} from '#imports'
interface Props {
value: Record<string, any>
@ -59,8 +61,6 @@ const validators = {
],
}
const formulaSuggestionDrawer = ref(true)
const availableFunctions = formulaList
const availableBinOps = ['+', '-', '*', '/', '>', '<', '==', '<=', '>=', '!=']
@ -597,10 +597,6 @@ setAdditionalValidations({
...validators,
})
defineExpose({
formulaSuggestionDrawer,
})
onMounted(() => {
jsep.plugins.register(jsepCurlyHook)
})
@ -612,7 +608,7 @@ onMounted(() => {
<a-textarea
ref="formulaRef"
v-model:value="vModel.formula_raw"
class="mb-2"
class="mb-2 nc-formula-input"
@keydown.down.prevent="suggestionListDown"
@keydown.up.prevent="suggestionListUp"
@keydown.enter.prevent="selectText"
@ -621,10 +617,11 @@ onMounted(() => {
</a-form-item>
<div class="text-gray-600 mt-2 mb-4 prose-sm">
Hint: Use {} to reference columns, e.g: {column_name}. For more, please check out
<a class="prose-sm" href="https://docs.nocodb.com/setup-and-usages/formulas#available-formula-features" target="_blank"
>Formulas</a
>.
<a class="prose-sm" href="https://docs.nocodb.com/setup-and-usages/formulas#available-formula-features" target="_blank">
Formulas.
</a>
</div>
<div class="h-[250px] overflow-auto scrollbar-thin-primary">
<a-list ref="sugListRef" :data-source="suggestion" :locale="{ emptyText: 'No suggested formula was found' }">
<template #renderItem="{ item, index }">

17
packages/nc-gui-v2/components/smartsheet-column/LookupOptions.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { MetaInj } from '~/context'
import { MetaInj } from '#imports'
interface Props {
value: Record<string, any>
@ -39,25 +39,22 @@ const refTables = $computed(() => {
}
return meta.columns
.filter((c: ColumnType) => c.uidt === UITypes.LinkToAnotherRecord && !c.system)
.map<TableType & { col: LinkToAnotherRecordType; column: ColumnType }>((c: ColumnType) => ({
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && !c.system)
.map((c: ColumnType) => ({
col: c.colOptions,
column: c,
...tables.find((t) => t.id === (c.colOptions as LinkToAnotherRecordType).fk_related_model_id),
}))
.filter(
(table: TableType & { col: LinkToAnotherRecordType; column: ColumnType }) =>
table.col.fk_related_model_id === table.id && !table.mm,
)
.filter((table: any) => table.col.fk_related_model_id === table.id && !table.mm)
})
const columns = $computed(() => {
const selectedTable = refTables.find((t) => t.column.id === vModel.value.fk_relation_column_id)
const selectedTable = refTables?.find((t) => t.column.id === vModel.value.fk_relation_column_id)
if (!selectedTable?.id) {
return []
}
return metas[selectedTable.id].columns.filter((c) => !isSystemColumn(c))
return metas[selectedTable.id].columns.filter((c: any) => !isSystemColumn(c))
})
</script>

8
packages/nc-gui-v2/components/smartsheet-header/Cell.vue

@ -1,13 +1,11 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { inject, toRef } from 'vue'
import { ColumnInj, IsFormInj } from '~/context'
import { ColumnInj, IsFormInj, inject, provide, ref, toRef, useUIPermission } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; required?: boolean; hideMenu?: boolean }>()
const hideMenu = toRef(props, 'hideMenu')
const meta = inject(MetaInj)
const isForm = inject(IsFormInj, ref(false))
const column = toRef(props, 'column')
@ -26,7 +24,7 @@ function onVisibleChange() {
</script>
<template>
<div class="flex align-center w-full text-xs font-weight-regular">
<div class="flex items-center w-full text-xs text-normal">
<SmartsheetHeaderCellIcon v-if="column" />
<span v-if="column" class="name" style="white-space: nowrap" :title="column.title">{{ column.title }}</span>
<span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span>

35
packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue

@ -1,9 +1,19 @@
<script setup lang="ts">
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import { substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { ColumnInj, IsFormInj, MetaInj } from '~/context'
import { provide, toRef, useMetas } from '#imports'
import {
ColumnInj,
IsFormInj,
MetaInj,
computed,
inject,
provide,
ref,
toRef,
useMetas,
useUIPermission,
useVirtualCell,
} from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean; required?: boolean }>()
@ -49,12 +59,10 @@ const relatedTableTitle = $computed(() => relatedTableMeta?.title)
const childColumn = $computed(() => {
if (relatedTableMeta?.columns) {
if (isRollup.value) {
const ch = relatedTableMeta?.columns.find((c: ColumnType) => c.id === (colOptions as RollupType).fk_rollup_column_id)
return ch
return relatedTableMeta?.columns.find((c: ColumnType) => c.id === (colOptions as RollupType).fk_rollup_column_id)
}
if (isLookup.value) {
const ch = relatedTableMeta?.columns.find((c: ColumnType) => c.id === (colOptions as LookupType).fk_lookup_column_id)
return ch
return relatedTableMeta?.columns.find((c: ColumnType) => c.id === (colOptions as LookupType).fk_lookup_column_id)
}
}
return ''
@ -93,11 +101,7 @@ function onVisibleChange() {
</script>
<template>
<div class="d-flex align-center w-full text-xs font-weight-regular">
<!-- <v-tooltip bottom>
<template #activator="{ on }">
todo: bring tooltip
-->
<div class="flex items-center w-full text-xs text-normal">
<SmartsheetHeaderVirtualCellIcon v-if="column" />
<a-tooltip placement="bottom">
@ -106,13 +110,10 @@ function onVisibleChange() {
</template>
<span class="name" style="white-space: nowrap" :title="column.title"> {{ column.title }}</span>
</a-tooltip>
<span v-if="column.rqd || required" class="text-red-500">&nbsp;*</span>
<!-- <span class="caption" v-html="tooltipMsg" /> -->
<span v-if="column.rqd || required" class="text-red-500">&nbsp;*</span>
<!-- </v-tooltip> -->
<template v-if="!hideMenu">
<v-spacer />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" />
</template>

23
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue

@ -8,17 +8,25 @@ import { comparisonOpList } from '~/utils/filterUtils'
import { ActiveViewInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import MdiDeleteIcon from '~icons/mdi/close-box'
import MdiAddIcon from '~icons/mdi/plus'
import type { Filter } from '~/lib/types'
const { nested = false, parentId, autoSave = true } = defineProps<{ nested?: boolean; parentId?: string; autoSave: boolean }>()
const {
nested = false,
parentId,
autoSave = true,
hookId = null,
modelValue,
} = defineProps<{ nested?: boolean; parentId?: string; autoSave: boolean; hookId?: string; modelValue?: Filter[] }>()
const emit = defineEmits(['update:filtersLength'])
const meta = inject(MetaInj)
const activeView = inject(ActiveViewInj)
const reloadDataHook = inject(ReloadViewDataHookInj)
// todo: replace with inject or get from state
const shared = ref(false)
const { $e } = useNuxtApp()
@ -29,6 +37,7 @@ const { filters, deleteFilter, saveOrUpdate, loadFilters, addFilter, addFilterGr
() => {
reloadDataHook?.trigger()
},
modelValue,
)
const filterUpdateCondition = (filter: FilterType, i: number) => {
@ -76,7 +85,7 @@ const types = computed(() => {
watch(
() => (activeView?.value as any)?.id,
(n, o) => {
if (n !== o) loadFilters()
if (n !== o) loadFilters(hookId as string)
},
{ immediate: true },
)
@ -95,11 +104,11 @@ watch(
},
)
const applyChanges = async () => {
await sync()
const applyChanges = async (hookId?: string) => {
await sync(hookId)
for (const nestedFilter of nestedFilters?.value || []) {
if (nestedFilter.parentId) {
await nestedFilter.applyChanges(true)
await nestedFilter.applyChanges(hookId, true)
}
}
}
@ -145,7 +154,7 @@ defineExpose({
<span class="col-span-3" />
<div class="col-span-5">
<SmartsheetToolbarColumnFilter
v-if="filter.id || shared"
v-if="filter.id || filter.children"
ref="nestedFilters"
v-model="filter.children"
:parent-id="filter.id"

5
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue

@ -1,12 +1,13 @@
<script setup lang="ts">
import { watchEffect } from '@vue/runtime-core'
import type ColumnFilter from './ColumnFilter.vue'
import { ActiveViewInj, IsLockedInj } from '~/context'
import { ActiveViewInj, IsLockedInj, IsPublicInj } from '~/context'
import MdiFilterIcon from '~icons/mdi/filter-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
const isLocked = inject(IsLockedInj)
const activeView = inject(ActiveViewInj)
const isPublic = inject(IsPublicInj)
const { filterAutoSave } = useGlobal()
@ -50,7 +51,7 @@ const applyChanges = async () => {
:auto-save="filterAutoSave"
@update:filters-length="filtersLength = $event"
>
<div class="d-flex align-end mt-2 min-h-[30px]" @click.stop>
<div v-if="!isPublic" class="d-flex align-end mt-2 min-h-[30px]" @click.stop>
<a-checkbox id="col-filter-checkbox" v-model:checked="filterAutoSave" class="col-filter-checkbox" hide-details dense>
<span class="text-grey text-xs">
{{ $t('msg.info.filterAutoApply') }}

2
packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue

@ -21,7 +21,7 @@ const {
showAll,
hideAll,
saveOrUpdate,
} = useViewColumns(activeView, meta, false, () => reloadDataHook.trigger())
} = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
watch(
() => (activeView.value as any)?.id,

98
packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue

@ -1,41 +1,41 @@
<script lang="ts" setup>
import * as XLSX from 'xlsx'
import { ExportTypes } from 'nocodb-sdk'
// todo: export types is missing EXCEL
// import { ExportTypes } from 'nocodb-sdk'
import FileSaver from 'file-saver'
import { message } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { useProject } from '#imports'
import { ActiveViewInj, MetaInj } from '~/context'
import { extractSdkResponseErrorMsg } from '~/utils'
import MdiFlashIcon from '~icons/mdi/flash-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiDownloadIcon from '~icons/mdi/download-outline'
import MdiUploadIcon from '~icons/mdi/upload-outline'
import MdiHookIcon from '~icons/mdi/hook'
import MdiViewListIcon from '~icons/mdi/view-list-outline'
import {
ActiveViewInj,
FieldsInj,
IsPublicInj,
MetaInj,
extractSdkResponseErrorMsg,
inject,
ref,
useNuxtApp,
useProject,
useUIPermission,
} from '#imports'
enum ExportTypes {
EXCEL = 'excel',
CSV = 'csv',
}
const sharedViewListDlg = ref(false)
const publicViewId = null
const isPublicView = inject(IsPublicInj, ref(false))
const isView = false
// TODO: pending for shared view
// interface Props {
// publicViewId?: string
// queryParams?: Record<string, any>
// reqPayload?: Record<string, any>
// }
// const { publicViewId, queryParams, reqPayload } = defineProps<Props>()
const { project } = useProject()
const { $api } = useNuxtApp()
const meta = inject(MetaInj)
const fields = inject(FieldsInj, ref([]))
const selectedView = inject(ActiveViewInj)
const showWebhookDrawer = ref(false)
@ -44,35 +44,17 @@ const quickImportDialog = ref(false)
const { isUIAllowed } = useUIPermission()
const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
const exportFile = async (exportType: ExportTypes) => {
let offset = 0
let c = 1
const responseType = exportType === ExportTypes.EXCEL ? 'base64' : 'blob'
try {
while (!isNaN(offset) && offset > -1) {
let res
if (publicViewId) {
// TODO: pending for shared view
// const { data, headers } = await $api.public.csvExport(publicViewId, exportType, {
// format: responseType,
// query: {
// fields:
// queryParams && queryParams.fieldsOrder && queryParams.fieldsOrder.filter((c: number) => queryParams.showFields[c]),
// offset,
// sortArrJson: JSON.stringify(
// reqPayload &&
// reqPayload.sorts &&
// reqPayload.sorts.map(({ fk_column_id, direction }) => ({
// direction,
// fk_column_id,
// })),
// ),
// filterArrJson: JSON.stringify(reqPayload && reqPayload.filters),
// },
// headers: {
// 'xc-password': reqPayload && reqPayload.password,
// },
// } as Record<string, any>)
if (isPublicView.value) {
const { exportFile: sharedViewExportFile } = useSharedView()
res = await sharedViewExportFile(fields.value, offset, exportType, responseType)
} else {
res = await $api.dbViewRow.export(
'noco',
@ -114,52 +96,59 @@ const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
<a-dropdown>
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-1 align-center">
<MdiFlashIcon />
<MdiFlashOutline />
<!-- More -->
<span class="!text-sm font-weight-medium">{{ $t('general.more') }}</span>
<MdiMenuDownIcon class="text-grey" />
<MdiMenuDown class="text-grey" />
</div>
</a-button>
<template #overlay>
<div class="bg-white shadow-lg !border">
<div>
<div v-t="['a:actions:download-csv']" class="nc-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadIcon />
<MdiDownloadOutline />
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</div>
<div v-t="['a:actions:download-excel']" class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadIcon />
<MdiDownloadOutline />
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}
</div>
<div
v-if="isUIAllowed('csvImport') && !isView"
v-if="isUIAllowed('csvImport') && !isView && !isPublicView"
v-t="['a:actions:upload-csv']"
class="nc-menu-item"
@click="quickImportDialog = true"
>
<MdiUploadIcon />
<MdiUploadOutline />
<!-- Upload CSV -->
{{ $t('activity.uploadCSV') }}
</div>
<div
v-if="isUIAllowed('SharedViewList') && !isView"
v-if="isUIAllowed('SharedViewList') && !isView && !isPublicView"
v-t="['a:actions:shared-view-list']"
class="nc-menu-item"
@click="sharedViewListDlg = true"
>
<MdiViewListIcon />
<MdiViewListOutline />
<!-- Shared View List -->
{{ $t('activity.listSharedView') }}
</div>
<div
v-if="isUIAllowed('webhook') && !isView"
v-if="isUIAllowed('webhook') && !isView && !isPublicView"
v-t="['c:actions:webhook']"
class="nc-menu-item"
@click="showWebhookDrawer = true"
>
<MdiHookIcon />
<MdiHook />
{{ $t('objects.webhooks') }}
</div>
</div>
@ -168,6 +157,7 @@ const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
</a-dropdown>
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" import-type="csv" :import-only="true" />
<WebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" />
<a-modal v-model:visible="sharedViewListDlg" title="Shared view list" width="max(900px,60vw)" :footer="null">

12
packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue

@ -15,16 +15,14 @@ const columns = computed(() =>
label: c.title,
})),
)
function onPressEnter() {
reloadData.trigger()
}
</script>
<template>
<a-input
v-model:value="search.query"
size="small"
class="max-w-[200px]"
placeholder="Filter query"
@press-enter="reloadData.trigger(null)"
>
<a-input v-model:value="search.query" size="small" class="max-w-[200px]" placeholder="Filter query" @press-enter="onPressEnter">
<template #addonBefore>
<div class="flex align-center relative" @click="isDropdownOpen = true">
<MdiMagnify class="text-grey" />

6
packages/nc-gui-v2/components/smartsheet/Cell.vue

@ -1,16 +1,14 @@
<script setup lang="ts">
import { UITypes } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import { provide, toRef } from 'vue'
import { computed, useColumn, useDebounceFn, useVModel } from '#imports'
import { ActiveCellInj, ColumnInj, EditModeInj } from '~/context'
import { ActiveCellInj, ColumnInj, EditModeInj, computed, provide, toRef, useColumn, useDebounceFn, useVModel } from '#imports'
import { NavigateDir } from '~/lib'
interface Props {
column: ColumnType
modelValue: any
editEnabled: boolean
rowIndex: number
rowIndex?: number
active?: boolean
}

145
packages/nc-gui-v2/components/smartsheet/Form.vue

@ -3,11 +3,27 @@ import Draggable from 'vuedraggable'
import { RelationTypes, UITypes, getSystemColumns, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import type { Permission } from '~/composables/useUIPermission/rolePermissions'
import { computed, inject, onClickOutside, useDebounceFn } from '#imports'
import { ActiveViewInj, IsFormInj, MetaInj } from '~/context'
import { extractSdkResponseErrorMsg } from '~/utils'
provide(IsFormInj, true)
import {
ActiveViewInj,
IsFormInj,
MetaInj,
computed,
extractSdkResponseErrorMsg,
inject,
onClickOutside,
provide,
reactive,
ref,
useDebounceFn,
useGlobal,
useNuxtApp,
useUIPermission,
useViewColumns,
useViewData,
watch,
} from '#imports'
provide(IsFormInj, ref(true))
// todo: generate hideCols based on default values
const hiddenCols = ['created_at', 'updated_at']
@ -20,7 +36,7 @@ const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const formState = reactive({})
const formState: Record<any, any> = reactive({})
const secondsRemain = ref(0)
@ -39,13 +55,13 @@ const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta as any, fal
const columns = computed(() => meta?.value?.columns || [])
const localColumns = ref<Record<string, any>>([])
const localColumns = ref<Record<string, any>[]>([])
const hiddenColumns = ref<Record<string, any>>([])
const hiddenColumns = ref<Record<string, any>[]>([])
const draggableRef = ref()
const systemFieldsIds = ref<Record<string, any>>([])
const systemFieldsIds = ref<Record<string, any>[]>([])
const showColumnDropdown = ref(false)
@ -76,7 +92,8 @@ async function submitForm() {
return
}
insertRow(formState)
await insertRow(formState)
submitted.value = true
}
@ -199,17 +216,23 @@ function setFormData() {
formViewData.value = {
...formViewData.value,
submit_another_form: !!(formViewData?.value?.submit_another_form ?? 0),
show_blank_form: !!(formViewData?.value?.show_blank_form ?? 0),
}
// todo: show_blank_form missing from FormType
show_blank_form: !!((formViewData?.value as any)?.show_blank_form ?? 0),
} as any
{
// email me
let data: Record<string, boolean> = {}
try {
data = JSON.parse(formViewData.value.email as string) || {}
} catch (e) {}
data = JSON.parse(formViewData.value?.email || '') || {}
} catch (e) {
// noop
}
data[state.user.value?.email as string] = emailMe.value
formViewData.value.email = JSON.stringify(data)
formViewData.value!.email = JSON.stringify(data)
checkSMTPStatus()
}
@ -257,7 +280,7 @@ async function submitCallback() {
const updateColMeta = useDebounceFn(async (col: Record<string, any>) => {
if (col.id) {
try {
$api.dbView.formColumnUpdate(col.id, col)
await $api.dbView.formColumnUpdate(col.id, col)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -265,7 +288,7 @@ const updateColMeta = useDebounceFn(async (col: Record<string, any>) => {
}, 250)
watch(submitted, (v) => {
if (v && formViewData?.value?.show_blank_form) {
if (v && (formViewData?.value as any)?.show_blank_form) {
secondsRemain.value = 5
const intvl = setInterval(() => {
if (--secondsRemain.value < 0) {
@ -314,7 +337,11 @@ onMounted(async () => {
</a-col>
</a-row>
<a-row v-else class="h-full flex">
<a-col v-if="isEditable" :span="8" class="bg-[#f7f7f7] shadow-md pa-5 h-full overflow-auto scrollbar-thin-primary">
<a-col
v-if="isEditable"
:span="8"
class="bg-[#f7f7f7] shadow-md pa-5 h-full overflow-auto scrollbar-thin-primary nc-form-left-drawer"
>
<div class="flex">
<div class="flex flex-row flex-1 text-lg">
<span>
@ -326,7 +353,7 @@ onMounted(async () => {
<div class="cursor-pointer mr-2">
<span
v-if="hiddenColumns.length"
class="mr-2"
class="mr-2 nc-form-add-all"
style="border-bottom: 2px solid rgb(218, 218, 218)"
@click="addAllColumns"
>
@ -335,7 +362,7 @@ onMounted(async () => {
</span>
<span
v-if="localColumns.length"
class="ml-2"
class="ml-2 nc-form-remove-all"
style="border-bottom: 2px solid rgb(218, 218, 218)"
@click="removeAllColumns"
>
@ -377,7 +404,7 @@ onMounted(async () => {
</a-card>
</template>
<template #footer>
<div class="mt-4 border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center">
<div class="mt-4 border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center nc-drag-n-drop-to-hide">
<!-- Drag and drop fields here to hide -->
{{ $t('msg.info.dragDropHide') }}
</div>
@ -406,8 +433,15 @@ onMounted(async () => {
<div class="h-[200px] !bg-[#dbdad7]">
<!-- for future implementation of cover image -->
</div>
<a-card class="h-full ma-0 rounded-b-0 pa-4" body-style="max-width: 700px; margin: 0 auto; margin-top: -200px;">
<a-form ref="formRef" :model="formState">
<a-card
class="h-full ma-0 rounded-b-0 pa-4"
:body-style="{
maxWidth: '700px',
margin: '0 auto',
marginTop: '-200px',
}"
>
<a-form ref="formRef" :model="formState" class="nc-form">
<a-card class="rounded ma-2 py-10 px-5">
<!-- Header -->
<a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0">
@ -422,7 +456,9 @@ onMounted(async () => {
@keydown.enter="updateView"
/>
</a-form-item>
<div v-else class="ml-3 w-full text-bold text-h3">{{ formViewData.heading }}</div>
<!-- Sub Header -->
<a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0">
<a-input
@ -437,7 +473,9 @@ onMounted(async () => {
@click="updateView"
/>
</a-form-item>
<div v-else class="ml-3 mb-5 w-full text-bold text-h3">{{ formViewData.subheading }}</div>
<Draggable
ref="draggableRef"
:list="localColumns"
@ -450,7 +488,11 @@ onMounted(async () => {
@end="drag = false"
>
<template #item="{ element, index }">
<div class="nc-editable item cursor-pointer hover:bg-primary/10 pa-3" @click="activeRow = element.title">
<div
class="nc-editable item cursor-pointer hover:bg-primary/10 pa-3"
:class="`nc-form-drag-${element.title.replaceAll(' ', '')}`"
@click="activeRow = element.title"
>
<div class="flex">
<div class="flex flex-1">
<div class="flex flex-row">
@ -473,50 +515,69 @@ onMounted(async () => {
<mdi-eye-off-outline class="opacity-0 nc-field-remove-icon" @click.stop="hideColumn(index)" />
</div>
</div>
<a-form-item
v-if="isVirtualCol(element)"
class="ma-0 gap-0 pa-0"
:name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]"
>
<SmartsheetVirtualCell v-model="formState[element.title]" class="nc-input" :column="element" />
<SmartsheetVirtualCell
v-model="formState[element.title]"
class="nc-input"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element"
/>
</a-form-item>
<a-form-item
v-else
class="ma-0 gap-0 pa-0"
:name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]"
>
<SmartsheetCell v-model="formState[element.title]" class="nc-input" :column="element" :edit-enabled="true" />
<SmartsheetCell
v-model="formState[element.title]"
class="nc-input"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element"
:edit-enabled="true"
/>
</a-form-item>
<div v-if="activeRow === element.title">
<a-form-item class="my-0 w-1/2">
<a-input
v-model:value="element.label"
size="small"
class="form-meta-input !bg-[#dbdbdb]"
class="form-meta-input !bg-[#dbdbdb] nc-form-input-label"
:placeholder="$t('msg.info.formInput')"
@change="updateColMeta(element)"
>
</a-input>
</a-form-item>
<a-form-item class="mt-2 mb-0 w-1/2">
<a-input
v-model:value="element.description"
size="small"
class="form-meta-input !bg-[#dbdbdb] text-sm"
class="form-meta-input !bg-[#dbdbdb] text-sm nc-form-input-help-text"
:placeholder="$t('msg.info.formHelpText')"
@change="updateColMeta(element)"
/>
</a-form-item>
<div class="items-center flex">
<span class="text-sm text-gray-500 mr-2">{{ $t('general.required') }}</span>
<span class="text-sm text-gray-500 mr-2 nc-form-input-required">{{ $t('general.required') }}</span>
<a-switch v-model:checked="element.required" size="small" class="my-2" @change="updateColMeta(element)" />
</div>
</div>
<span class="text-gray-500">{{ element.description }}</span>
</div>
</template>
<template #footer>
<div
v-if="!localColumns.length"
@ -528,7 +589,11 @@ onMounted(async () => {
</Draggable>
<div class="justify-center flex mt-10">
<a-button class="flex items-center gap-2 !bg-primary text-white rounded" size="large" @click="submitForm">
<a-button
class="flex items-center gap-2 !bg-primary text-white rounded nc-form-submit"
size="large"
@click="submitForm"
>
<!-- Submit -->
{{ $t('general.submit') }}
</a-button>
@ -541,9 +606,16 @@ onMounted(async () => {
<div class="text-gray-500 mt-4 mb-2">
{{ $t('msg.info.afterFormSubmitted') }}
</div>
<!-- Show this message -->
<label class="text-gray-600 text-bold"> {{ $t('msg.info.showMessage') }}: </label>
<a-textarea v-model:value="formViewData.success_msg" rows="3" hide-details @change="updateView" />
<a-textarea
v-model:value="formViewData.success_msg"
:rows="3"
hide-details
class="nc-form-after-submit-msg"
@change="updateView"
/>
<!-- Other options -->
<div class="mt-4">
@ -553,6 +625,7 @@ onMounted(async () => {
v-model:checked="formViewData.submit_another_form"
v-t="[`a:form-view:submit-another-form`]"
size="small"
class="nc-form-checkbox-submit-another-form"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.submitAnotherForm') }}</span>
@ -564,12 +637,20 @@ onMounted(async () => {
v-model:checked="formViewData.show_blank_form"
v-t="[`a:form-view:show-blank-form`]"
size="small"
class="nc-form-checkbox-show-blank-form"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.showBlankForm') }}</span>
</div>
<div class="my-4">
<a-switch v-model:checked="emailMe" v-t="[`a:form-view:email-me`]" size="small" @change="onEmailChange" />
<a-switch
v-model:checked="emailMe"
v-t="[`a:form-view:email-me`]"
size="small"
class="nc-form-checkbox-send-email"
@change="onEmailChange"
/>
<!-- Email me at <email> -->
<span class="ml-4">
{{ $t('msg.info.emailForm') }} <span class="text-bold text-gray-600">{{ state.user.value?.email }}</span>

39
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -23,8 +23,10 @@ import {
IsFormInj,
IsGridInj,
IsLockedInj,
IsPublicInj,
MetaInj,
PaginationDataInj,
ReadonlyInj,
ReloadViewDataHookInj,
} from '~/context'
import { NavigateDir } from '~/lib'
@ -34,10 +36,12 @@ const meta = inject(MetaInj)
const view = inject(ActiveViewInj)
const isPublicView = inject(IsPublicInj, ref(false))
// keep a root fields variable and will get modified from
// fields menu and get used in grid and gallery
const fields = inject(FieldsInj, ref([]))
const readonly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj, false)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
@ -45,8 +49,6 @@ const reloadViewDataHook = inject(ReloadViewDataHookInj)
const { isUIAllowed } = useUIPermission()
// todo: get from parent ( inject or use prop )
const isPublicView = false
const isView = false
const selected = reactive<{ row: number | null; col: number | null }>({ row: null, col: null })
@ -80,7 +82,6 @@ const {
} = useViewData(meta, view as any, xWhere)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view as any)
onMounted(loadGridViewColumns)
provide(IsFormInj, ref(false))
@ -149,7 +150,7 @@ const clearCell = async (ctx: { row: number; col: number }) => {
const { copy } = useClipboard()
const makeEditable = (row: Row, col: ColumnType) => {
if (isPublicView || editEnabled || isView) {
if (isPublicView.value || editEnabled || isView) {
return
}
if (!isPkAvail.value && !row.rowMeta.new) {
@ -319,13 +320,18 @@ const expandForm = (row: Row, state: Record<string, any>) => {
@xcresized="resizingCol = null"
>
<div class="w-full h-full bg-gray-100 flex items-center">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readonly" />
<SmartsheetHeaderCell v-else :column="col" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="readonly" />
</div>
</th>
<!-- v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" -->
<th v-if="isUIAllowed('add-column')" v-t="['c:column:add']" @click.stop="addColumnDropdown = true">
<th
v-if="!readonly && isUIAllowed('add-column')"
v-t="['c:column:add']"
class="cursor-pointer"
@click.stop="addColumnDropdown = true"
>
<a-dropdown v-model:visible="addColumnDropdown" :trigger="['click']">
<div class="h-full w-[60px] flex align-center justify-center">
<MdiPlus class="text-sm" />
@ -350,15 +356,17 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<tr class="nc-grid-row">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1">
<div class="align-center flex min-w-[80px]">
<div class="nc-row-no" :class="{ hidden: row.rowMeta.selected }">{{ rowIndex + 1 }}</div>
<div v-if="!readonly" class="nc-row-no" :class="{ hidden: row.rowMeta.selected }">{{ rowIndex + 1 }}</div>
<div v-else>{{ rowIndex + 1 }}</div>
<div
v-if="!readonly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<div class="nc-expand" :class="{ 'nc-comment': row.rowMeta?.commentCount }">
<div v-if="!readonly" class="nc-expand" :class="{ 'nc-comment': row.rowMeta?.commentCount }">
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
@ -405,7 +413,12 @@ const expandForm = (row: Row, state: Record<string, any>) => {
v-else
v-model="row.row[columnObj.title]"
:column="columnObj"
:edit-enabled="editEnabled && selected.col === colIndex && selected.row === rowIndex"
:edit-enabled="
isUIAllowed('xcDatatableEditable') &&
editEnabled &&
selected.col === colIndex &&
selected.row === rowIndex
"
:row-index="rowIndex"
:active="selected.col === colIndex && selected.row === rowIndex"
@update:edit-enabled="editEnabled = false"
@ -430,8 +443,8 @@ const expandForm = (row: Row, state: Record<string, any>) => {
class="text-left pointer nc-grid-add-new-cell"
@click="addEmptyRow()"
>
<div class="px-2 w-full flex items-center">
<MdiPlus class="text-pint-500 text-xs" />
<div class="px-2 w-full flex items-center text-gray-500">
<MdiPlus class="text-pint-500 text-xs ml-2" />
<span class="ml-1">
{{ $t('activity.addRow') }}

22
packages/nc-gui-v2/components/smartsheet/Pagination.vue

@ -1,15 +1,14 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import { computed, inject } from '#imports'
import { ChangePageInj, PaginationDataInj } from '~/context'
import MdiKeyboardIcon from '~icons/mdi/keyboard-return'
const paginatedData = inject(PaginationDataInj)
const paginatedData = inject(PaginationDataInj)!
const changePage = inject(ChangePageInj)
const changePage = inject(ChangePageInj)!
const count = computed(() => paginatedData?.value?.totalRows ?? Infinity)
const count = computed(() => paginatedData.value?.totalRows ?? Infinity)
const size = computed(() => paginatedData?.value?.pageSize ?? 25)
const size = computed(() => paginatedData.value?.pageSize ?? 25)
const page = computed({
get: () => paginatedData?.value?.page ?? 1,
@ -18,8 +17,8 @@ const page = computed({
</script>
<template>
<div class="flex items-center">
<span v-if="count !== null && count !== Infinity" class="caption ml-2 text-gray-500">
<div class="flex items-center mb-1">
<span v-if="count !== null && count !== Infinity" class="caption ml-5 text-gray-500">
{{ count }} record{{ count !== 1 ? 's' : '' }}
</span>
@ -29,7 +28,7 @@ const page = computed({
v-if="count !== Infinity"
v-model:current="page"
size="small"
class="!text-xs !m-1"
class="!text-xs !m-1 nc-pagination"
:total="count"
:page-size="size"
show-less-items
@ -39,7 +38,7 @@ const page = computed({
<span class="text-xs" style="white-space: nowrap"> Change page:</span>
<a-input :value="page" size="small" class="ml-1 !text-xs" type="number" @keydown.enter="changePage(page)">
<template #suffix>
<MdiKeyboardIcon class="mt-1" @click="changePage(page)" />
<MdiKeyboardReturn class="mt-1" @click="changePage(page)" />
</template>
</a-input>
</div>
@ -50,8 +49,7 @@ const page = computed({
<style scoped>
:deep(.ant-pagination-item a) {
line-height: 21px !important;
@apply text-sm;
@apply text-sm !leading-[21px];
}
:deep(.ant-pagination-item:not(.ant-pagination-item-active) a) {
line-height: 21px !important;

10
packages/nc-gui-v2/components/smartsheet/Toolbar.vue

@ -1,7 +1,9 @@
<script setup lang="ts">
import { useSmartsheetStoreOrThrow } from '~/composables'
import { IsPublicInj, useSmartsheetStoreOrThrow } from '#imports'
const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow()
const { allowCSVDownload } = useSharedView()
const isPublic = inject(IsPublicInj, ref(false))
</script>
<template>
@ -12,11 +14,11 @@ const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow()
<SmartsheetToolbarSortListMenu v-if="isGrid || isGallery" />
<SmartsheetToolbarShareView v-if="isForm || isGrid" />
<SmartsheetToolbarShareView v-if="(isForm || isGrid) && !isPublic" />
<SmartsheetToolbarMoreActions v-if="isGrid" />
<SmartsheetToolbarMoreActions v-if="(isGrid && !isPublic) || (isGrid && isPublic && allowCSVDownload)" />
<div class="flex-1" />
<SmartsheetToolbarSearchData v-if="isGrid || isGallery" class="shrink mr-2" />
<SmartsheetToolbarSearchData v-if="(isGrid || isGallery) && !isPublic" class="shrink mr-2" />
</div>
</template>

38
packages/nc-gui-v2/components/smartsheet/VirtualCell.vue

@ -1,10 +1,27 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { provide, toRef, useVirtualCell } from '#imports'
import { ActiveCellInj, CellValueInj, ColumnInj, RowInj, provide, toRef, useVirtualCell } from '#imports'
import type { Row } from '~/composables'
import { ActiveCellInj, CellValueInj, ColumnInj, RowInj } from '~/context'
import { NavigateDir } from '~/lib'
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'navigate'])
const HasMany = defineAsyncComponent(() => import('../virtual-cell/HasMany.vue'))
const ManyToMany = defineAsyncComponent(() => import('../virtual-cell/ManyToMany.vue'))
const BelongsTo = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))
const Rollup = defineAsyncComponent(() => import('../virtual-cell/HasMany.vue'))
const Formula = defineAsyncComponent(() => import('../virtual-cell/ManyToMany.vue'))
const Count = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))
const Lookup = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))
interface Props {
column: ColumnType
modelValue: any
@ -12,9 +29,6 @@ interface Props {
active?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'navigate'])
const column = toRef(props, 'column')
const active = toRef(props, 'active', false)
const row = toRef(props, 'row')
@ -33,12 +47,12 @@ const { isLookup, isBt, isRollup, isMm, isHm, isFormula, isCount } = useVirtualC
@keydown.stop.enter.exact="emit('navigate', NavigateDir.NEXT)"
@keydown.stop.shift.enter.exact="emit('navigate', NavigateDir.PREV)"
>
<VirtualCellHasMany v-if="isHm" />
<VirtualCellManyToMany v-else-if="isMm" />
<VirtualCellBelongsTo v-else-if="isBt" />
<VirtualCellRollup v-else-if="isRollup" />
<VirtualCellFormula v-else-if="isFormula" />
<VirtualCellCount v-else-if="isCount" />
<VirtualCellLookup v-else-if="isLookup" />
<HasMany v-if="isHm" />
<ManyToMany v-else-if="isMm" />
<BelongsTo v-else-if="isBt" />
<Rollup v-else-if="isRollup" />
<Formula v-else-if="isFormula" />
<Count v-else-if="isCount" />
<Lookup v-else-if="isLookup" />
</div>
</template>

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

@ -1,7 +1,5 @@
<script setup lang="ts">
import { nextTick, useExpandedFormStoreOrThrow } from '#imports'
import { enumColor, timeAgo } from '~/utils'
import MdiAccountIcon from '~icons/mdi/account-circle'
import { enumColor, nextTick, ref, timeAgo, useExpandedFormStoreOrThrow, watch } from '#imports'
const { loadCommentsAndLogs, commentsAndLogs, isCommentsLoading, commentsOnly, saveComment, isYou, comment } =
useExpandedFormStoreOrThrow()
@ -10,6 +8,8 @@ const commentsWrapperEl = ref<HTMLDivElement>()
await loadCommentsAndLogs()
const showborder = ref(false)
watch(
commentsAndLogs,
() => {
@ -28,7 +28,7 @@ watch(
<template v-else>
<div v-for="log of commentsAndLogs" :key="log.id" class="flex gap-1 text-xs">
<MdiAccountIcon class="row-span-2" :class="isYou(log.user) ? 'text-pink-300' : 'text-blue-300 '" />
<MdiAccountCircle class="row-span-2" :class="isYou(log.user) ? 'text-pink-300' : 'text-blue-300 '" />
<div class="flex-grow">
<p class="mb-1 caption edited-text text-[10px] text-gray">
{{ isYou(log.user) ? 'You' : log.user == null ? 'Shared base' : log.user }}

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

@ -42,23 +42,31 @@ const iconColor = '#1890ff'
<template v-if="meta">
{{ meta.title }}
</template>
<!-- todo: table doesn't exist?
<template v-else>
{{ table }}
</template>
-->
<template v-if="primaryValue">: {{ primaryValue }}</template>
</h5>
<div class="flex-grow" />
<mdi-reload class="cursor-pointer select-none" />
<component
:is="drawerToggleIcon"
v-if="isUIAllowed('rowComments')"
class="cursor-pointer select-none"
@click="commentsDrawer = !commentsDrawer"
/>
<a-button class="!text" @click="emit('cancel')">
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button :disabled="!isUIAllowed('tableRowUpdate')" type="primary" @click="save">
<!-- Save Row -->
{{ $t('activity.saveRow') }}

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

@ -1,25 +1,32 @@
<script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import Cell from '../Cell.vue'
import VirtualCell from '../VirtualCell.vue'
import Comments from './Comments.vue'
import Header from './Header.vue'
import {
FieldsInj,
IsFormInj,
MetaInj,
NOCO,
computedInject,
extractPkFromRow,
provide,
ref,
toRef,
useNuxtApp,
useProject,
useProvideExpandedFormStore,
useProvideSmartsheetStore,
useVModel,
watch,
} from '#imports'
import { NOCO } from '~/lib'
import { extractPkFromRow } from '~/utils'
import type { Row } from '~/composables'
import { FieldsInj, IsFormInj, MetaInj } from '~/context'
interface Props {
modelValue: string | null
modelValue?: boolean
row: Row
state?: Record<string, any> | null
meta: TableType
@ -28,9 +35,13 @@ interface Props {
}
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const row = toRef(props, 'row')
const state = toRef(props, 'state')
const meta = toRef(props, 'meta')
const fields = computedInject(FieldsInj, (_fields) => {
@ -45,25 +56,26 @@ provide(MetaInj, meta)
const { commentsDrawer, changedColumns, state: rowState } = useProvideExpandedFormStore(meta, row)
const { $api } = useNuxtApp()
if (props.loadRow) {
const { project } = useProject()
row.value.row = await $api.dbTableRow.read(
NOCO,
project.value.id as string,
meta.value.title,
extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]),
)
row.value.oldRow = { ...row.value.row }
row.value.rowMeta = {}
}
useProvideSmartsheetStore(ref({}) as any, meta)
useProvideSmartsheetStore(ref({}) as Ref<ViewType>, meta)
provide(IsFormInj, ref(true))
// accept as a prop
// const row: Row = { row: {}, rowMeta: {}, oldRow: {} }
watch(
state,
() => {
@ -76,7 +88,15 @@ watch(
{ immediate: true },
)
const isExpanded = useVModel(props, 'modelValue', emits)
const isExpanded = useVModel(props, 'modelValue', emits, {
defaultValue: false,
})
</script>
<script lang="ts">
export default {
name: 'ExpandedForm',
}
</script>
<template>
@ -86,13 +106,14 @@ const isExpanded = useVModel(props, 'modelValue', emits)
<div class="flex h-full nc-form-wrapper items-stretch min-h-[70vh]">
<div class="flex-grow overflow-auto scrollbar-thin-primary">
<div class="w-[500px] mx-auto">
<div v-for="col in fields" :key="col.title" class="mt-2 py-2" :class="`nc-expand-col-${col.title}`">
<div v-for="col of fields" :key="col.title" class="mt-2 py-2" :class="`nc-expand-col-${col.title}`">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />
<div class="!bg-white rounded px-1 min-h-[35px] flex align-center mt-2">
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<SmartsheetCell
<VirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<Cell
v-else
v-model="row.row[col.title]"
:column="col"

22
packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue

@ -41,8 +41,12 @@ function onOpenModal(type: ViewTypes, title = '') {
{{ $t('activity.createView') }}
</h3>
<a-menu-item key="grid" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.GRID)">
<a-tooltip placement="left">
<a-menu-item
key="grid"
class="group !flex !items-center !my-0 !h-[30px] nc-create-3-view"
@click="onOpenModal(ViewTypes.GRID)"
>
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title>
{{ $t('msg.info.addView.grid') }}
</template>
@ -59,8 +63,12 @@ function onOpenModal(type: ViewTypes, title = '') {
</a-tooltip>
</a-menu-item>
<a-menu-item key="gallery" class="group !flex !items-center !-my0 !h-[30px]" @click="onOpenModal(ViewTypes.GALLERY)">
<a-tooltip placement="left">
<a-menu-item
key="gallery"
class="group !flex !items-center !-my0 !h-[30px] nc-create-2-view"
@click="onOpenModal(ViewTypes.GALLERY)"
>
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title>
{{ $t('msg.info.addView.gallery') }}
</template>
@ -80,10 +88,10 @@ function onOpenModal(type: ViewTypes, title = '') {
<a-menu-item
v-if="!isView"
key="form"
class="group !flex !items-center !my-0 !h-[30px]"
class="group !flex !items-center !my-0 !h-[30px] nc-create-1-view"
@click="onOpenModal(ViewTypes.FORM)"
>
<a-tooltip placement="left">
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title>
{{ $t('msg.info.addView.form') }}
</template>
@ -103,7 +111,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<SmartsheetSidebarMenuApiSnippet v-model="showApiSnippet" />
<div class="flex-auto justify-end flex flex-col gap-4 mt-4">
<div class="flex-auto justify-end flex flex-col gap-3 mt-3">
<button
v-if="isUIAllowed('virtualViewsCreateOrEdit')"
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease !text-xs"

1
packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue

@ -183,6 +183,7 @@ function onDeleted() {
:class="[
isMarked === view.id ? 'bg-gray-200' : '',
route.params.viewTitle && route.params.viewTitle.includes(view.title) ? 'active' : '',
`nc-view-item nc-${view.type}-view-item`,
]"
@change-view="changeView"
@open-modal="$emit('openModal', $event)"

5
packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -2,7 +2,7 @@
import type { ViewTypes } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { viewIcons } from '~/utils'
import { onKeyStroke, useDebounceFn, useNuxtApp, useVModel } from '#imports'
import { onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel } from '#imports'
interface Props {
view: Record<string, any>
@ -163,6 +163,7 @@ function onStopEdit() {
</div>
<a-input v-if="isEditing" :ref="focusInput" v-model:value="vModel.title" @blur="onCancel" @keydown="onKeyDown($event)" />
<div v-else>{{ vModel.alias || vModel.title }}</div>
<div class="flex-1" />
@ -183,7 +184,7 @@ function onStopEdit() {
{{ $t('activity.deleteView') }}
</template>
<MdiTrashCan class="hidden group-hover:block text-red-500" @click.stop="onDelete" />
<MdiTrashCan class="hidden group-hover:block text-red-500 nc-view-delete-icon" @click.stop="onDelete" />
</a-tooltip>
</template>
</div>

17
packages/nc-gui-v2/components/smartsheet/sidebar/index.vue

@ -3,7 +3,7 @@ import type { FormType, GalleryType, GridType, KanbanType, ViewTypes } from 'noc
import MenuTop from './MenuTop.vue'
import MenuBottom from './MenuBottom.vue'
import Toolbar from './toolbar/index.vue'
import { computed, inject, provide, ref, useRoute, useRouter, useViews, watch } from '#imports'
import { computed, inject, provide, ref, useElementHover, useRoute, useRouter, useViews, watch } from '#imports'
import { ActiveViewInj, MetaInj, RightSidebarInj, ViewListInj } from '~/context'
const meta = inject(MetaInj, ref())
@ -25,6 +25,9 @@ const sidebarOpen = inject(RightSidebarInj, ref(true))
const sidebarCollapsed = computed(() => !sidebarOpen.value)
/** Sidebar ref */
const sidebar = ref()
/** View type to create from modal */
let viewCreateType = $ref<ViewTypes>()
@ -37,6 +40,8 @@ let selectedViewId = $ref('')
/** is view creation modal open */
let modalOpen = $ref(false)
const isHovered = useElementHover(sidebar)
/** Watch route param and change active view based on `viewTitle` */
watch(
[views, () => route.params.viewTitle],
@ -74,6 +79,7 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
<template>
<a-layout-sider
ref="sidebar"
:collapsed="sidebarCollapsed"
collapsiple
collapsed-width="50"
@ -81,24 +87,27 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
class="relative shadow-md h-full"
theme="light"
>
<a-tooltip placement="left">
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title> Toggle sidebar </template>
<Transition name="glow">
<div
v-show="sidebarCollapsed || isHovered"
class="group color-transition cursor-pointer hover:ring active:ring-pink-500 z-1 flex items-center p-[1px] absolute top-1/2 left-[-1rem] shadow bg-gray-100 rounded-full"
>
<MaterialSymbolsChevronRightRounded
v-if="sidebarOpen"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400 nc-right-sidebar-toggle"
@click="sidebarOpen = false"
/>
<MaterialSymbolsChevronLeftRounded
v-else
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400 nc-right-sidebar-toggle"
@click="sidebarOpen = true"
/>
</div>
</Transition>
</a-tooltip>
<Toolbar v-if="sidebarOpen" class="flex items-center py-3 px-3 justify-between border-b-1" />

32
packages/nc-gui-v2/components/smartsheet/sidebar/menu/ApiSnippet.vue

@ -1,8 +1,9 @@
<script setup lang="ts">
import HTTPSnippet from 'httpsnippet'
// import HTTPSnippet from 'httpsnippet'
import { useClipboard } from '@vueuse/core'
import { message } from 'ant-design-vue'
import { ActiveViewInj, MetaInj } from '~/context'
import { inject, useGlobal, useProject, useSmartsheetStoreOrThrow, useVModel, useViewData } from '#imports'
const props = defineProps<Props>()
@ -13,11 +14,17 @@ interface Props {
}
const { project } = $(useProject())
const { appInfo, token } = $(useGlobal())
const meta = $(inject(MetaInj))
const view = $(inject(ActiveViewInj))
const meta = $(inject(MetaInj)!)
const view = $(inject(ActiveViewInj)!)
const { xWhere } = useSmartsheetStoreOrThrow()
const { queryParams } = $(useViewData(meta, view as any, xWhere))
const { queryParams } = $(useViewData($$(meta), view as any, xWhere))
const { copy } = useClipboard()
let vModel = $(useVModel(props, 'modelValue', emits))
@ -66,20 +73,7 @@ const apiUrl = $computed(
new URL(`/api/v1/db/data/noco/${project.id}/${meta.title}/views/${view.title}`, (appInfo && appInfo.ncSiteUrl) || '/').href,
)
const snippet = $computed(
() =>
new HTTPSnippet({
method: 'GET',
headers: [{ name: 'xc-auth', value: token as string, comment: 'JWT Auth token' }],
url: apiUrl,
queryString: Object.entries(queryParams || {}).map(([name, value]) => {
return {
name,
value: String(value),
}
}),
}),
)
const snippet = $computed(() => '')
const activeLang = $computed(() => langs.find((lang) => lang.name === selectedLangName))
@ -178,5 +172,3 @@ const afterVisibleChange = (visible: boolean) => {
</div>
</a-drawer>
</template>
<style scoped></style>

28
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/DebugMeta.vue

@ -0,0 +1,28 @@
<script setup lang="ts">
const editorOpen = ref(false)
const tabKey = ref()
const { metas } = $(useMetas())
const { tables } = useTable()
const localTables = tables.value.filter((t) => metas[t.id as string])
</script>
<template>
<a-tooltip placement="bottom">
<template #title>
<span> Debug Meta </span>
</template>
<mdi-bug-outline class="cursor-pointer" @click="editorOpen = true" />
</a-tooltip>
<a-modal v-model:visible="editorOpen" :footer="null" width="80%">
<a-tabs v-model:activeKey="tabKey" type="card" closeable="false" class="shadow-sm">
<a-tab-pane v-for="table in localTables" :key="table.id" :tab="table.title">
<MonacoEditor v-model="metas[table.id]" class="h-max-[70vh]" :read-only="true" />
</a-tab-pane>
</a-tabs>
</a-modal>
</template>

22
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/DeleteCache.vue

@ -0,0 +1,22 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
const { api } = useApi()
async function deleteCache() {
try {
await api.utils.cacheDelete()
message.info('Deleted Cache Successfully')
} catch (e: any) {
message.error(e.message)
}
}
</script>
<template>
<a-tooltip placement="bottom">
<template #title>
<span> Delete Cache </span>
</template>
<mdi-delete class="cursor-pointer" @click="deleteCache" />
</a-tooltip>
</template>

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

@ -14,7 +14,7 @@ const sidebarOpen = inject(RightSidebarInj, ref(true))
<template #title> {{ $t('activity.deleteTable') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-red-500 group">
<MdiDeleteOutline class="cursor-pointer group-hover:(!text-white)" @click="deleteTable(meta)" />
<MdiDeleteOutline class="cursor-pointer" @click="deleteTable(meta)" />
</div>
</a-tooltip>
</template>

32
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ExportCache.vue

@ -0,0 +1,32 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import FileSaver from 'file-saver'
const { api } = useApi()
async function exportCache() {
try {
const data = await api.utils.cacheGet()
if (!data) {
message.info('Cache is empty')
return
}
const blob = new Blob([JSON.stringify(data)], {
type: 'text/plain;charset=utf-8',
})
FileSaver.saveAs(blob, 'cache_exported.json')
message.info('Exported Cache Successfully')
} catch (e: any) {
message.error(e.message)
}
}
</script>
<template>
<a-tooltip placement="bottom">
<template #title>
<span> Export Cache </span>
</template>
<mdi-export class="cursor-pointer" @click="exportCache" />
</a-tooltip>
</template>

31
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue

@ -2,14 +2,43 @@
import AddRow from './AddRow.vue'
import LockMenu from './LockMenu.vue'
import Reload from './Reload.vue'
import ExportCache from './ExportCache.vue'
import DeleteCache from './DeleteCache.vue'
import DebugMeta from './DebugMeta.vue'
const { isUIAllowed } = useUIPermission()
const debug = $ref(false)
const clickCount = $ref(0)
</script>
<template>
<div class="flex gap-2">
<div
class="flex gap-2"
@click="
() => {
clickCount = clickCount + 1
debug = clickCount >= 4
}
"
>
<slot name="start" />
<template v-if="debug">
<ExportCache />
<div class="dot" />
<DeleteCache />
<div class="dot" />
<DebugMeta />
<div class="dot" />
</template>
<LockMenu v-if="isUIAllowed('view-type')" />
<div v-if="isUIAllowed('view-type')" class="dot" />

2
packages/nc-gui-v2/components/tabs/Smartsheet.vue

@ -34,7 +34,7 @@ provide(ActiveViewInj, activeView)
provide(IsLockedInj, false)
provide(ReloadViewDataHookInj, reloadEventHook)
provide(FieldsInj, fields)
provide(RightSidebarInj, ref(true))
provide(RightSidebarInj, ref(false))
const { isGallery, isGrid, isForm } = useProvideSmartsheetStore(activeView as Ref<TableType>, meta)

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

@ -32,7 +32,9 @@ const loadBase = async () => {
try {
if (!project.value.id) return
const res = await $api.project.sharedBaseGet(project.value.id)
// todo: response is missing roles in type
const res = (await $api.project.sharedBaseGet(project.value.id)) as any
base = {
uuid: res.uuid,
url: res.url,
@ -47,9 +49,10 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
try {
if (!project.value.id) return
const res = await $api.project.sharedBaseUpdate(project.value.id, {
// todo: return type void?
const res = (await $api.project.sharedBaseUpdate(project.value.id, {
roles: role,
})
})) as any
base = res ?? {}
base!.role = role

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

@ -85,7 +85,6 @@ const saveUser = async () => {
project_id: project.value.id,
projectName: project.value.title,
})
emit('reload')
emit('closed')
} else {
const res = await $api.auth.projectUserAdd(project.value.id, {
@ -96,6 +95,7 @@ const saveUser = async () => {
})
usersData.invitationToken = res.invite_token
}
emit('reload')
message.success('Successfully updated the user details')
} catch (e: any) {
console.error(e)

55
packages/nc-gui-v2/components/template/Editor.vue

@ -3,14 +3,27 @@ import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { Form, message } from 'ant-design-vue'
import { srcDestMappingColumns, tableColumns } from './utils'
import { computed, onMounted } from '#imports'
import { extractSdkResponseErrorMsg, fieldRequiredValidator, getUIDTIcon } from '~/utils'
import { MetaInj, ReloadViewDataHookInj } from '~/context'
import {
MetaInj,
ReloadViewDataHookInj,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
getUIDTIcon,
nextTick,
onMounted,
reactive,
ref,
useProject,
useTabs,
useTemplateRefsList,
} from '#imports'
import { TabType } from '~/composables'
interface Props {
quickImportType: 'csv' | 'excel' | 'json'
projectTemplate: Record<string, any>
importData: Record<string, any>[]
importData: Record<string, any>
importColumns: any[]
importOnly: boolean
maxRowsToParse: number
@ -41,7 +54,7 @@ const expansionPanel = ref<number[]>([])
const editableTn = ref<boolean[]>([])
const inputRefs = ref<HTMLInputElement[]>([])
const inputRefs = useTemplateRefsList<HTMLInputElement>()
const isImporting = ref(false)
@ -74,6 +87,7 @@ const { sqlUi, project, loadTables } = useProject()
onMounted(() => {
parseAndLoadTemplate()
nextTick(() => {
inputRefs.value[0]?.focus()
})
@ -409,7 +423,7 @@ async function importTemplate() {
await loadTables()
addTab({
...tab,
type: 'table',
type: TabType.TABLE,
})
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -476,13 +490,15 @@ onMounted(() => {
</p>
</a-form>
<a-collapse v-if="data.tables && data.tables.length" v-model:activeKey="expansionPanel" class="template-collapse" accordion>
<a-collapse-panel v-for="(table, tableIdx) in data.tables" :key="tableIdx">
<a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx">
<template #header>
<span class="font-weight-bold text-lg flex items-center gap-2">
<mdi-table class="text-primary" />
{{ table.ref_table_name }}
</span>
</template>
<a-table
v-if="srcDestMapping"
class="template-form"
@ -496,10 +512,12 @@ onMounted(() => {
{{ column.name }}
</span>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'source_column'">
<span>{{ record.srcCn }}</span>
</template>
<template v-else-if="column.key === 'destination_column'">
<a-select v-model:value="record.destCn" class="w-52" show-search :filter-option="filterOption">
<a-select-option v-for="(col, i) of columns" :key="i" :value="col.title">
@ -525,13 +543,14 @@ onMounted(() => {
{{ data.tables.length }} sheet{{ data.tables.length > 1 ? 's' : '' }}
available for import
</p>
<a-collapse
v-if="data.tables && data.tables.length"
v-model:activeKey="expansionPanel"
class="template-collapse"
accordion
>
<a-collapse-panel v-for="(table, tableIdx) in data.tables" :key="tableIdx">
<a-collapse-panel v-for="(table, tableIdx) of data.tables" :key="tableIdx">
<template #header>
<a-form-item v-if="editableTn[tableIdx]" v-bind="validateInfos[`tables.${tableIdx}.table_name`]" no-style>
<a-input
@ -539,16 +558,18 @@ onMounted(() => {
class="max-w-xs"
size="large"
hide-details
@click="(e) => e.stopPropagation()"
@click="$event.stopPropagation()"
@blur="setEditableTn(tableIdx, false)"
@keydown.enter="setEditableTn(tableIdx, false)"
/>
</a-form-item>
<span v-else class="font-weight-bold text-lg flex items-center gap-2" @click="setEditableTn(tableIdx, true)">
<mdi-table class="text-primary" />
{{ table.table_name }}
</span>
</template>
<template #extra>
<a-tooltip bottom>
<template #title>
@ -560,7 +581,7 @@ onMounted(() => {
</template>
<a-table
v-if="table.columns.length"
v-if="table.columns && table.columns.length"
class="template-form"
row-class-name="template-form-row"
:data-source="table.columns"
@ -573,11 +594,13 @@ onMounted(() => {
{{ $t('labels.columnName') }}
</span>
</template>
<template v-else-if="column.key === 'uidt'">
<span>
{{ $t('labels.columnType') }}
</span>
</template>
<template v-else-if="column.key === 'dtxp' && hasSelectColumn[tableIdx]">
<span>
<!-- TODO: i18n -->
@ -585,19 +608,14 @@ onMounted(() => {
</span>
</template>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'column_name'">
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.columns.${record.key}.${column.key}`]">
<a-input
:ref="
(el) => {
inputRefs[record.key] = el
}
"
v-model:value="record.column_name"
/>
<a-input :ref="inputRefs.set" v-model:value="record.column_name" />
</a-form-item>
</template>
<template v-else-if="column.key === 'uidt'">
<a-form-item v-bind="validateInfos[`tables.${tableIdx}.columns.${record.key}.${column.key}`]">
<a-select
@ -622,6 +640,7 @@ onMounted(() => {
<!-- TODO: i18n -->
<span>Primary Value</span>
</template>
<div class="flex items-center float-right mr-4">
<mdi-key-star class="text-lg" />
</div>

29
packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue

@ -1,22 +1,35 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListItems from './components/ListItems.vue'
import { inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { ActiveCellInj, CellValueInj, ColumnInj, EditModeInj, ReloadViewDataHookInj, RowInj } from '~/context'
import {
ActiveCellInj,
CellValueInj,
ColumnInj,
EditModeInj,
ReloadViewDataHookInj,
RowInj,
defineAsyncComponent,
inject,
ref,
useProvideLTARStore,
useSmartsheetRowStoreOrThrow,
} from '#imports'
import MdiArrowExpand from '~icons/mdi/arrow-expand'
import MdiPlus from '~icons/mdi/plus'
const column = inject(ColumnInj)
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
const ListItems = defineAsyncComponent(() => import('./components/ListItems.vue'))
const column = inject(ColumnInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)!
const cellValue = inject(CellValueInj, ref<any>(null))
const row = inject(RowInj)
const row = inject(RowInj)!
const active = inject(ActiveCellInj)
const active = inject(ActiveCellInj)!
const editEnabled = inject(EditModeInj)
@ -55,7 +68,7 @@ const unlinkRef = async (rec: Record<string, any>) => {
<template>
<div class="flex w-full chips-wrapper align-center" :class="{ active }">
<div class="chips d-flex align-center flex-grow">
<template v-if="value">
<template v-if="value && relatedTablePrimaryValueProp">
<ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" />
</template>
</div>

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

@ -1,7 +1,5 @@
<script lang="ts" setup>
import { computed, inject, ref, useProject } from '#imports'
import { CellValueInj, ColumnInj } from '~/context'
import { handleTZ, replaceUrlsWithLink } from '~/utils'
import { CellValueInj, ColumnInj, computed, handleTZ, inject, ref, replaceUrlsWithLink, useProject } from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck
const column: any = inject(ColumnInj)

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

@ -1,11 +1,26 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue'
import { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, EditModeInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context'
import {
CellValueInj,
ColumnInj,
EditModeInj,
IsFormInj,
ReloadViewDataHookInj,
RowInj,
computed,
defineAsyncComponent,
inject,
ref,
useProvideLTARStore,
useSmartsheetRowStoreOrThrow,
} from '#imports'
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
const ListItems = defineAsyncComponent(() => import('./components/ListItems.vue'))
const ListChildItems = defineAsyncComponent(() => import('./components/ListChildItems.vue'))
const column = inject(ColumnInj)!
@ -85,7 +100,9 @@ const unlinkRef = async (rec: Record<string, any>) => {
/>
</div>
</template>
<ListItems v-model="listItemsDlg" />
<ListChildItems
v-model="childListDlg"
@attach-record="

12
packages/nc-gui-v2/components/virtual-cell/Lookup.vue

@ -2,17 +2,19 @@
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, MetaInj, ReadonlyInj } from '~/context'
import { computed, inject, provide, useColumn, useMetas } from '#imports'
import { CellValueInj, ColumnInj, MetaInj, ReadonlyInj, computed, inject, provide, useColumn, useMetas } from '#imports'
const { metas, getMeta } = useMetas()
provide(ReadonlyInj, true)
const column = inject(ColumnInj)! as Ref<ColumnType & { colOptions: LookupType }>
const meta = inject(MetaInj)
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 & {
colOptions: LinkToAnotherRecordType
@ -22,7 +24,7 @@ await getMeta(relationColumn.colOptions.fk_related_model_id!)
const lookupTableMeta = computed(() => metas.value[relationColumn.colOptions.fk_related_model_id!])
const lookupColumn = computed(
const lookupColumn = computed<any>(
() =>
lookupTableMeta.value.columns?.find(
(c: Record<string, any>) => c.id === column.value.colOptions?.fk_lookup_column_id,
@ -58,7 +60,7 @@ const lookupColumnMetaProps = useColumn(lookupColumn)
<template v-else>
<!-- For attachment cell avoid adding chip style -->
<div
v-for="(v, i) in arrValue"
v-for="(v, i) of arrValue"
:key="i"
:class="{ 'bg-gray-100 px-2 rounded-full': !lookupColumnMetaProps.isAttachment }"
>

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

@ -1,11 +1,25 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue'
import { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, EditModeInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context'
import {
CellValueInj,
ColumnInj,
EditModeInj,
IsFormInj,
ReloadViewDataHookInj,
RowInj,
computed,
inject,
ref,
useProvideLTARStore,
useSmartsheetRowStoreOrThrow,
} from '#imports'
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
const ListItems = defineAsyncComponent(() => import('./components/ListItems.vue'))
const ListChildItems = defineAsyncComponent(() => import('./components/ListChildItems.vue'))
const column = inject(ColumnInj)!

2
packages/nc-gui-v2/components/virtual-cell/Rollup.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { CellValueInj } from '~/context'
import { CellValueInj, inject } from '#imports'
const value = inject(CellValueInj)
</script>

26
packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { useLTARStoreOrThrow } from '#imports'
import { ActiveCellInj, EditModeInj, IsFormInj } from '~/context'
<script lang="ts" setup>
import { ActiveCellInj, EditModeInj, IsFormInj, defineAsyncComponent, inject, ref, useLTARStoreOrThrow } from '#imports'
interface Props {
value?: string | number | boolean
@ -11,17 +10,25 @@ const { value, item } = defineProps<Props>()
const emit = defineEmits(['unlink'])
const { relatedTableMeta } = useLTARStoreOrThrow()
const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/expanded-form/index.vue'))
const editEnabled = inject(EditModeInj)
const { relatedTableMeta } = useLTARStoreOrThrow()!
const editEnabled = inject(EditModeInj)!
const active = inject(ActiveCellInj, ref(false))
const isForm = inject(IsFormInj)
const isForm = inject(IsFormInj)!
const expandedFormDlg = ref(false)
</script>
<script lang="ts">
export default {
name: 'ItemChip',
}
</script>
<template>
<div
class="group py-1 px-2 flex align-center gap-1 bg-gray-200/50 hover:bg-gray-200 rounded-[20px]"
@ -29,18 +36,21 @@ const expandedFormDlg = ref(false)
@click="expandedFormDlg = true"
>
<span class="name">{{ value }}</span>
<div v-show="active || isForm" v-if="editEnabled" class="flex align-center">
<MdiCloseThick class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click.stop="emit('unlink')" />
</div>
<SmartsheetExpandedForm
v-if="expandedFormDlg && editEnabled"
<Suspense>
<ExpandedForm
v-if="editEnabled"
v-model="expandedFormDlg"
:row="{ row: item }"
:meta="relatedTableMeta"
load-row
use-meta-fields
/>
</Suspense>
</div>
</template>

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

@ -1,12 +1,23 @@
<script lang="ts" setup>
import { Empty, Modal } from 'ant-design-vue'
import type { ColumnType } from 'nocodb-sdk'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel, watch } from '#imports'
import { ColumnInj, EditModeInj, IsFormInj } from '~/context'
import {
ColumnInj,
EditModeInj,
IsFormInj,
computed,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
useVModel,
watch,
} from '#imports'
const props = defineProps<{ modelValue?: boolean }>()
const emit = defineEmits(['update:modelValue', 'attachRecord'])
const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/expanded-form/index.vue'))
const vModel = useVModel(props, 'modelValue', emit)
const isForm = inject(IsFormInj, ref(false))
@ -120,14 +131,16 @@ const expandedFormRow = ref()
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
<SmartsheetExpandedForm
v-if="expandedFormDlg && expandedFormRow"
<Suspense>
<ExpandedForm
v-if="expandedFormRow"
v-model="expandedFormDlg"
:row="{ row: expandedFormRow }"
:meta="relatedTableMeta"
load-row
use-meta-fields
/>
</Suspense>
</component>
</template>

19
packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue

@ -2,13 +2,24 @@
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { Empty } from 'ant-design-vue'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel } from '#imports'
import { ColumnInj } from '~/context'
import {
ColumnInj,
computed,
defineAsyncComponent,
inject,
ref,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
useVModel,
watch,
} from '#imports'
const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits(['update:modelValue', 'addNewRecord'])
const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/expanded-form/index.vue'))
const vModel = useVModel(props, 'modelValue', emit)
const column = inject(ColumnInj)
@ -118,7 +129,8 @@ const newRowState = computed(() => {
</template>
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<SmartsheetExpandedForm
<Suspense>
<ExpandedForm
v-if="expandedFormDlg"
v-model="expandedFormDlg"
:meta="relatedTableMeta"
@ -126,6 +138,7 @@ const newRowState = computed(() => {
:state="newRowState"
use-meta-fields
/>
</Suspense>
</div>
</a-modal>
</template>

66
packages/nc-gui-v2/components/webhook/ChannelMultiSelect.vue

@ -0,0 +1,66 @@
<script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
interface Props {
modelValue: Record<string, any>[]
availableChannelList: Record<string, any>[]
placeholder: string
}
const { availableChannelList, placeholder, ...rest } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const vModel = useVModel(rest, 'modelValue', emit)
// idx of selected channels
const localChannelValues = $ref<number[]>([])
// availableChannelList with idx enriched
let availableChannelWithIdxList = $ref<Record<string, any>[]>()
watch(
() => localChannelValues,
(v) => {
const res = []
for (const channelIdx of v) {
const target = availableChannelWithIdxList.find((availableChannel) => availableChannel.idx === channelIdx)
if (target) {
// push without target.idx
res.push({ webhook_url: target.webhook_url, channel: target.channel })
}
}
vModel.value = res
},
)
onMounted(() => {
if (availableChannelList.length) {
// enrich idx
let idx = 0
availableChannelWithIdxList = availableChannelList.map((channel) => ({
...channel,
idx: idx++,
}))
// build localChannelValues from modelValue
for (const channel of rest.modelValue || []) {
const target = availableChannelWithIdxList.find(
(availableChannelWithIdx) =>
availableChannelWithIdx.webhook_url === channel.webhook_url && availableChannelWithIdx.channel === channel.channel,
)
if (target) {
localChannelValues.push(target.idx)
}
}
}
})
</script>
<template>
<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">{{
channel.channel
}}</a-select-option>
</a-select>
</template>

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

@ -31,8 +31,10 @@ async function editHook(hook: Record<string, any>) {
:body-style="{ background: 'rgba(67, 81, 232, 0.05)', padding: '50px' }"
@keydown.esc="vModel = false"
>
<div>
<WebhookEditor v-if="editOrAdd" ref="webhookEditorRef" @back-to-list="editOrAdd = false" />
<WebhookList v-else @edit="editHook" @add="editOrAdd = true" />
</div>
<div class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center">
<a-button v-t="['e:hiring']" href="https://angel.co/company/nocodb" target="_blank" size="large">
🚀 We are Hiring! 🚀

93
packages/nc-gui-v2/components/webhook/Editor.vue

@ -1,13 +1,6 @@
<script setup lang="ts">
import { Form, message } from 'ant-design-vue'
import { MetaInj } from '~/context'
import { extractSdkResponseErrorMsg, fieldRequiredValidator } from '~/utils'
import { inject, reactive, useApi, useNuxtApp } from '#imports'
interface Option {
label: string
value: string
}
import { MetaInj, extractSdkResponseErrorMsg, fieldRequiredValidator, inject, reactive, useApi, useNuxtApp } from '#imports'
const emit = defineEmits(['backToList', 'editOrAdd'])
@ -51,7 +44,7 @@ const discordChannels = ref<Record<string, any>[]>([])
const mattermostChannels = ref<Record<string, any>[]>([])
const filters = ref([])
const filterRef = ref()
const formInput = ref({
'Email': [
@ -205,21 +198,27 @@ const validators = computed(() => {
})
const { validate, validateInfos } = useForm(hook, validators)
function onNotTypeChange() {
hook.notification.payload = {} as any
function onNotTypeChange(reset = false) {
if (reset) {
hook.notification.payload = {} as Record<string, any>
}
if (hook.notification.type === 'Slack') {
slackChannels.value = (apps && apps?.Slack && apps.Slack.parsedInput) || []
slackChannels.value = (apps.value && apps.value.Slack && apps.Slack.parsedInput) || []
}
if (hook.notification.type === 'Microsoft Teams') {
teamsChannels.value = (apps && apps['Microsoft Teams'] && apps['Microsoft Teams'].parsedInput) || []
teamsChannels.value = (apps.value && apps.value['Microsoft Teams'] && apps.value['Microsoft Teams'].parsedInput) || []
}
if (hook.notification.type === 'Discord') {
discordChannels.value = (apps && apps.Discord && apps.Discord.parsedInput) || []
discordChannels.value = (apps.value && apps.value.Discord && apps.value.Discord.parsedInput) || []
}
if (hook.notification.type === 'Mattermost') {
mattermostChannels.value = (apps && apps.Mattermost && apps.Mattermost.parsedInput) || []
mattermostChannels.value = (apps.value && apps.value.Mattermost && apps.value.Mattermost.parsedInput) || []
}
if (hook.notification.type === 'URL') {
hook.notification.payload.body = '{{ json data }}'
hook.notification.payload.parameters = [{}]
@ -228,10 +227,6 @@ function onNotTypeChange() {
}
}
function filterOption(input: string, option: Option) {
return option.value.toUpperCase().includes(input.toUpperCase())
}
function setHook(newHook: any) {
Object.assign(hook, {
...newHook,
@ -301,6 +296,8 @@ async function loadPluginList() {
if (hook.event && hook.operation) {
hook.eventOperation = `${hook.event} ${hook.operation}`
}
onNotTypeChange()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -342,12 +339,9 @@ async function saveHooks() {
hook.id = res.id
}
// TODO: wait for filter implementation
// if ($refs.filter) {
// await $refs.filter.applyChanges(false, {
// hookId: hook.id,
// });
// }
if (filterRef.value) {
await filterRef.value.applyChanges(hook.id)
}
message.success('Webhook details updated successfully')
} catch (e: any) {
@ -383,8 +377,8 @@ watch(
},
)
onMounted(() => {
loadPluginList()
onMounted(async () => {
await loadPluginList()
})
</script>
@ -439,7 +433,7 @@ onMounted(() => {
v-model:value="hook.notification.type"
size="large"
:placeholder="$t('general.notification')"
@change="onNotTypeChange"
@change="onNotTypeChange(true)"
>
<a-select-option v-for="(notificationOption, i) in notificationList" :key="i" :value="notificationOption.type">
<div class="flex items-center">
@ -505,12 +499,12 @@ onMounted(() => {
<a-row v-if="hook.notification.type === 'Slack'" type="flex">
<a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']">
<a-auto-complete
v-model:value="hook.notification.payload.channels"
size="large"
:options="slackChannels"
<WebhookChannelMultiSelect
v-if="slackChannels.length > 0"
v-model="hook.notification.payload.channels"
:selected-channel-list="hook.notification.payload.channels"
:available-channel-list="slackChannels"
placeholder="Select Slack channels"
:filter-option="filterOption"
/>
</a-form-item>
</a-col>
@ -519,12 +513,12 @@ onMounted(() => {
<a-row v-if="hook.notification.type === 'Microsoft Teams'" type="flex">
<a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']">
<a-auto-complete
v-model:value="hook.notification.payload.channels"
size="large"
:options="teamsChannels"
<WebhookChannelMultiSelect
v-if="teamsChannels.length > 0"
v-model="hook.notification.payload.channels"
:selected-channel-list="hook.notification.payload.channels"
:available-channel-list="teamsChannels"
placeholder="Select Microsoft Teams channels"
:filter-option="filterOption"
/>
</a-form-item>
</a-col>
@ -533,12 +527,12 @@ onMounted(() => {
<a-row v-if="hook.notification.type === 'Discord'" type="flex">
<a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']">
<a-auto-complete
v-model:value="hook.notification.payload.channels"
size="large"
:options="discordChannels"
<WebhookChannelMultiSelect
v-if="discordChannels.length > 0"
v-model="hook.notification.payload.channels"
:selected-channel-list="hook.notification.payload.channels"
:available-channel-list="discordChannels"
placeholder="Select Discord channels"
:filter-option="filterOption"
/>
</a-form-item>
</a-col>
@ -547,12 +541,12 @@ onMounted(() => {
<a-row v-if="hook.notification.type === 'Mattermost'" type="flex">
<a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']">
<a-auto-complete
v-model:value="hook.notification.payload.channels"
size="large"
:options="mattermostChannels"
<WebhookChannelMultiSelect
v-if="mattermostChannels.length > 0"
v-model="hook.notification.payload.channels"
:selected-channel-list="hook.notification.payload.channels"
:available-channel-list="mattermostChannels"
placeholder="Select Mattermost channels"
:filter-option="filterOption"
/>
</a-form-item>
</a-col>
@ -573,7 +567,7 @@ onMounted(() => {
<a-col :span="24">
<a-card>
<a-checkbox v-model:checked="hook.condition">On Condition</a-checkbox>
<SmartsheetToolbarColumnFilter v-if="hook.condition" />
<SmartsheetToolbarColumnFilter v-if="hook.condition" ref="filterRef" :auto-save="false" :hook-id="hook.id" />
</a-card>
</a-col>
</a-row>
@ -601,7 +595,6 @@ onMounted(() => {
ref="webhookTestRef"
:hook="{
...hook,
filters,
notification: {
...hook.notification,
payload: hook.notification.payload,

10
packages/nc-gui-v2/components/webhook/List.vue

@ -4,13 +4,13 @@ import { MetaInj } from '~/context'
import { inject, onMounted, ref, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
const emit = defineEmits(['edit'])
const emit = defineEmits(['edit', 'add'])
const { $api, $e } = useNuxtApp()
const hooks = ref<Record<string, any>[]>([])
const meta = inject(MetaInj)
const meta = inject(MetaInj)!
async function loadHooksList() {
try {
@ -58,7 +58,11 @@ onMounted(() => {
</div>
<a-divider />
<div v-if="hooks.length">
<a-list item-layout="horizontal" :data-source="hooks" class="cursor-pointer pl-5 pr-5 pt-2 pb-2">
<a-list
item-layout="horizontal"
:data-source="hooks"
class="cursor-pointer max-h-[75vh] overflow-y-auto scrollbar-thin-primary"
>
<template #renderItem="{ item, index }">
<a-list-item class="pa-2" @click="emit('edit', item)">
<a-list-item-meta>

2
packages/nc-gui-v2/components/webhook/Test.vue

@ -28,7 +28,7 @@ watch(
async function loadSampleData() {
sampleData.value = {
data: await $api.dbTableWebhook.samplePayloadGet(meta?.value?.id as string, hook?.operation),
data: await $api.dbTableWebhook.samplePayloadGet(meta?.value?.id as string, hook?.operation || 'insert'),
}
}

14
packages/nc-gui-v2/composables/useColumnCreateStore.ts

@ -1,23 +1,13 @@
import { createInjectionState } from '@vueuse/core'
import clone from 'just-clone'
import { Form, message } from 'ant-design-vue'
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useColumn } from './useColumn'
import { computed } from '#imports'
import { useNuxtApp } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { computed, createInjectionState, extractSdkResponseErrorMsg, useNuxtApp } from '#imports'
const useForm = Form.useForm
// enum ColumnAlterType {
// NEW=4,
// EDIT=2,
// RENAME=8,
// DELETE=0,
// }
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
@ -242,6 +232,6 @@ export { useProvideColumnCreateStore }
export function useColumnCreateStoreOrThrow() {
const columnCreateStore = useColumnCreateStore()
if (columnCreateStore == null) throw new Error('Please call `useColumnCreateStore` on the appropriate parent component')
if (columnCreateStore == null) throw new Error('Please call `useProvideColumnCreateStore` on the appropriate parent component')
return columnCreateStore
}

26
packages/nc-gui-v2/composables/useExpandedFormStore.ts

@ -3,19 +3,24 @@ import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import { useApi, useInjectionState, useProject, useProvideSmartsheetRowStore } from '#imports'
import { NOCO } from '~/lib'
import { useNuxtApp } from '#app'
import type { Row } from '~/composables/useViewData'
import { extractPkFromRow, extractSdkResponseErrorMsg } from '~/utils'
import {
NOCO,
extractPkFromRow,
extractSdkResponseErrorMsg,
useApi,
useInjectionState,
useNuxtApp,
useProject,
useProvideSmartsheetRowStore,
} from '#imports'
import type { Row } from '~/composables'
const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((meta: Ref<TableType>, row: Ref<Row>) => {
const { $e, $state, $api } = useNuxtApp()
const { api, isLoading: isCommentsLoading, error: commentsError } = useApi()
// { useGlobalInstance: true },
// state
const commentsOnly = ref(false)
const commentsAndLogs = ref([])
const commentsAndLogs = ref<any[]>([])
const comment = ref('')
const commentsDrawer = ref(false)
const changedColumns = ref(new Set<string>())
@ -84,11 +89,14 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
await api.utils.commentRow({
fk_model_id: meta.value?.id as string,
row_id: rowId,
// todo: description missing from argument type
description: comment.value,
})
} as any)
comment.value = ''
message.success('Comment added successfully')
await loadCommentsAndLogs()
} catch (e: any) {
message.error(e.message)

20
packages/nc-gui-v2/composables/useGlobal/state.ts

@ -58,6 +58,10 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
},
filterAutoSave: true,
previewAs: null,
includeM2M: false,
currentVersion: null,
latestRelease: null,
hiddenRelease: null,
}
/** saves a reactive state, any change to these values will write/delete to localStorage */
@ -72,7 +76,21 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
set: (val) => (storage.value.token = val),
})
const appInfo = ref<AppInfo>({ ncSiteUrl: 'localhost:8080' })
const appInfo = ref<AppInfo>({
ncSiteUrl: 'localhost:8080',
authType: 'jwt',
connectToExternalDB: false,
defaultLimit: 0,
firstUser: true,
githubAuthEnabled: false,
googleAuthEnabled: true,
ncMin: false,
oneClick: false,
projectHasAdmin: false,
teleEnabled: true,
type: 'nocodb',
version: '0.0.0',
})
/** reactive token payload */
const { payload } = useJwt<JwtPayload & User>(token)

6
packages/nc-gui-v2/composables/useGlobal/types.ts

@ -13,7 +13,7 @@ export interface FeedbackForm {
export interface AppInfo {
ncSiteUrl: string
authType: 'jwt' | 'masterKey' | 'none'
authType: 'jwt' | 'none'
connectToExternalDB: boolean
defaultLimit: number
firstUser: boolean
@ -35,6 +35,10 @@ export interface StoredState {
feedbackForm: FeedbackForm
filterAutoSave: boolean
previewAs: string | null
includeM2M: boolean
currentVersion: string | null
latestRelease: string | null
hiddenRelease: string | null
}
export type State = ToRefs<Omit<StoredState, 'token'>> & {

8
packages/nc-gui-v2/composables/useGridViewColumnWidth.ts

@ -3,6 +3,7 @@ import type { ColumnType, GridColumnType, GridType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useMetas } from './useMetas'
import { useUIPermission } from './useUIPermission'
import { IsPublicInj } from '~/context'
export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) | undefined>) {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
@ -13,6 +14,7 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
const gridViewCols = ref<Record<string, GridColumnType>>({})
const resizingCol = ref('')
const resizingColWidth = ref('200px')
const isPublic = inject(IsPublicInj, ref(false))
const columns = computed<ColumnType[]>(() => metas?.value?.[(view?.value as any)?.fk_model_id as string]?.columns)
@ -35,8 +37,8 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
)
const loadGridViewColumns = async () => {
if (!view.value?.id) return
const colsData: GridColumnType[] = await $api.dbView.gridColumnsList(view.value.id)
if (!view.value?.id && !isPublic.value) return
const colsData: GridColumnType[] = isPublic.value ? columns.value : await $api.dbView.gridColumnsList(view.value.id)
gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
(o, col) => ({
...o,
@ -56,7 +58,7 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
}
// sync with server if allowed
if (isUIAllowed('gridColUpdate') && gridViewCols.value[id]?.id) {
if (!isPublic.value && isUIAllowed('gridColUpdate') && gridViewCols.value[id]?.id) {
$api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, {
width,
})

33
packages/nc-gui-v2/composables/useLTARStore.ts

@ -1,10 +1,18 @@
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, TableType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { Modal, message } from 'ant-design-vue'
import { useInjectionState, useMetas, useProject } from '#imports'
import { NOCO } from '~/lib'
import {
NOCO,
computed,
extractSdkResponseErrorMsg,
reactive,
ref,
useInjectionState,
useMetas,
useNuxtApp,
useProject,
} from '#imports'
import type { Row } from '~/composables'
import { extractSdkResponseErrorMsg } from '~/utils'
interface DataApiResponse {
list: Record<string, any>
@ -13,13 +21,13 @@ interface DataApiResponse {
/** Store for managing Link to another cells */
const [useProvideLTARStore, useLTARStore] = useInjectionState(
(column: Ref<Required<ColumnType>>, row?: Ref<Row>, isNewRow: ComputedRef<boolean> | Ref<boolean>, reloadData = () => {}) => {
(column: Ref<Required<ColumnType>>, row: Ref<Row>, isNewRow: ComputedRef<boolean> | Ref<boolean>, reloadData = () => {}) => {
// state
const { metas, getMeta } = useMetas()
const { project } = useProject()
const { $api } = useNuxtApp()
const childrenExcludedList: Ref<DataApiResponse | undefined> = ref()
const childrenList: Ref<DataApiResponse | undefined> = ref()
const childrenExcludedList = ref<DataApiResponse | undefined>()
const childrenList = ref<DataApiResponse | undefined>()
const childrenExcludedListPagination = reactive({
page: 1,
query: '',
@ -95,12 +103,13 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
colOptions.type as 'mm' | 'hm',
column?.value?.title,
{
limit: childrenExcludedListPagination.size,
offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
limit: String(childrenExcludedListPagination.size),
offset: String(childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1)),
// todo: where clause is missing from type
where:
childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
},
} as any,
)
}
} catch (e: any) {
@ -120,10 +129,10 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
colOptions.type as 'mm' | 'hm',
column?.value?.title,
{
limit: childrenListPagination.size,
offset: childrenListPagination.size * (childrenListPagination.page - 1),
limit: String(childrenListPagination.size),
offset: String(childrenListPagination.size * (childrenListPagination.page - 1)),
where: childrenListPagination.query && `(${relatedTablePrimaryValueProp.value},like,${childrenListPagination.query})`,
},
} as any,
)
} catch (e: any) {
message.error(`Failed to load children list: ${await extractSdkResponseErrorMsg(e)}`)

10
packages/nc-gui-v2/composables/useMetas.ts

@ -12,6 +12,14 @@ export function useMetas() {
const metas = useState<{ [idOrTitle: string]: TableType | any }>('metas', () => ({}))
const loadingState = useState<Record<string, boolean>>('metas-loading-state', () => ({}))
const setMeta = async (model: any) => {
metas.value = {
...metas.value,
[model.id!]: model,
[model.title]: model,
}
}
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => {
if (!tableIdOrTitle) return null
/** wait until loading is finished if requesting same meta */
@ -83,5 +91,5 @@ export function useMetas() {
}
}
return { getMeta, clearAllMeta, metas, removeMeta }
return { getMeta, clearAllMeta, metas, removeMeta, setMeta }
}

31
packages/nc-gui-v2/composables/useProject.ts

@ -2,6 +2,7 @@ import { SqlUiFactory } from 'nocodb-sdk'
import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp, useRoute, useState } from '#app'
import type { ProjectMetaInfo } from '~/lib'
import { USER_PROJECT_ROLES } from '~/lib'
export function useProject(projectId?: MaybeRef<string>) {
@ -12,10 +13,18 @@ export function useProject(projectId?: MaybeRef<string>) {
const project = useState<ProjectType>('project')
const tables = useState<TableType[]>('tables', () => [] as TableType[])
const route = useRoute()
const { includeM2M } = useGlobal()
const projectMetaInfo = useState<ProjectMetaInfo | undefined>('projectMetaInfo')
// todo: refactor path param name and variable name
const projectType = $computed(() => route.params.projectType as string)
async function loadProjectMetaInfo(force?: boolean) {
if (!projectMetaInfo.value || force) {
const data = await $api.project.metaGet(project.value.id!, {}, {})
projectMetaInfo.value = data
}
}
async function loadProjectRoles() {
projectRoles.value = {}
@ -27,7 +36,10 @@ export function useProject(projectId?: MaybeRef<string>) {
async function loadTables() {
if (project.value.id) {
const tablesResponse = await $api.dbTable.list(project.value.id)
const tablesResponse = await $api.dbTable.list(project.value.id, {
// FIXME: type
includeM2M: includeM2M.value || '',
})
if (tablesResponse.list) tables.value = tablesResponse.list
}
}
@ -56,5 +68,18 @@ export function useProject(projectId?: MaybeRef<string>) {
)
const isSharedBase = computed(() => projectType === 'base')
return { project, tables, loadProjectRoles, loadProject, loadTables, isMysql, isMssql, isPg, sqlUi, isSharedBase }
return {
project,
tables,
loadProjectRoles,
loadProject,
loadTables,
isMysql,
isMssql,
isPg,
sqlUi,
isSharedBase,
loadProjectMetaInfo,
projectMetaInfo,
}
}

108
packages/nc-gui-v2/composables/useSharedView.ts

@ -0,0 +1,108 @@
import type { ColumnType, ExportTypes, FilterType, PaginatedType, SortType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { useNuxtApp } from '#app'
export function useSharedView() {
const nestedFilters = useState<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>(
'nestedFilters',
() => [],
)
const paginationData = useState<PaginatedType>('paginationData', () => ({ page: 1, pageSize: 25 }))
const sharedView = useState<ViewType>('sharedView')
const sorts = useState<SortType[]>('sorts', () => [])
const password = useState<string | undefined>('password')
const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false)
const meta = ref<TableType>(sharedView.value?.model)
const columns = ref<ColumnType[]>(sharedView.value?.model?.columns)
const formColumns = computed(
() =>
columns.value
.filter(
(f: Record<string, any>) =>
f.show && f.uidt !== UITypes.Rollup && f.uidt !== UITypes.Lookup && f.uidt !== UITypes.Formula,
)
.sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order)
.map((c: Record<string, any>) => ({ ...c, required: !!(c.required || 0) })) ?? [],
)
const { $api } = useNuxtApp()
const { setMeta } = useMetas()
const loadSharedView = async (viewId: string, localPassword: string | undefined = undefined) => {
const viewMeta = await $api.public.sharedViewMetaGet(viewId, {
headers: {
'xc-password': localPassword ?? password.value,
},
})
allowCSVDownload.value = JSON.parse(viewMeta.meta).allowCSVDownload
if (localPassword) password.value = localPassword
sharedView.value = viewMeta
meta.value = viewMeta.model
columns.value = viewMeta.model.columns
setMeta(viewMeta.model)
const relatedMetas = { ...viewMeta.relatedMetas }
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))
}
const fetchSharedViewData = async () => {
const page = paginationData.value.page || 1
const pageSize = paginationData.value.pageSize || 25
const { data } = await $api.public.dataList(
sharedView?.value?.uuid,
{
offset: (page - 1) * pageSize,
filterArrJson: JSON.stringify(nestedFilters.value),
sortArrJson: JSON.stringify(sorts.value),
} as any,
{
headers: {
'xc-password': password.value,
},
},
)
return data
}
const exportFile = async (
fields: any[],
offset: number,
type: ExportTypes.EXCEL | ExportTypes.CSV,
responseType: 'base64' | 'blob',
) => {
return await $api.public.csvExport(sharedView.value?.uuid, type, {
format: responseType as any,
query: {
fields: fields.map((field) => field.title),
offset,
sortArrJson: JSON.stringify(sorts.value),
filterArrJson: JSON.stringify(nestedFilters.value),
},
headers: {
'xc-password': password.value,
},
})
}
return {
sharedView,
loadSharedView,
meta,
columns,
nestedFilters,
fetchSharedViewData,
paginationData,
sorts,
exportFile,
formColumns,
allowCSVDownload,
}
}

16
packages/nc-gui-v2/composables/useSmartsheetRowStore.ts

@ -2,9 +2,8 @@ import { message } from 'ant-design-vue'
import { UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, RelationTypes, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useNuxtApp } from '#app'
import { useInjectionState, useMetas, useProject, useVirtualCell } from '#imports'
import type { Row } from '~/composables/useViewData'
import type { Row } from './useViewData'
import { useInjectionState, useMetas, useNuxtApp, useProject, useVirtualCell } from '#imports'
import { NOCO } from '~/lib'
import { extractPkFromRow, extractSdkResponseErrorMsg } from '~/utils'
@ -47,7 +46,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
project.value.title as string,
meta.value.title as string,
rowId,
type,
type as 'mm' | 'hm',
column.title as string,
relatedRowId,
)
@ -69,14 +68,19 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
if (isHm || isMm) {
const relatedRows = (state.value?.[column.title!] ?? []) as Record<string, any>[]
for (const relatedRow of relatedRows) {
await linkRecord(id, extractPkFromRow(relatedRow, relatedTableMeta.columns as ColumnType[]), column, colOptions.type)
await linkRecord(
id,
extractPkFromRow(relatedRow, relatedTableMeta.columns as ColumnType[]),
column,
colOptions.type as RelationTypes,
)
}
} else if (isBt && state?.value?.[column.title!]) {
await linkRecord(
id,
extractPkFromRow(state.value?.[column.title!] as Record<string, any>, relatedTableMeta.columns as ColumnType[]),
column,
colOptions.type,
colOptions.type as RelationTypes,
)
}
}

4
packages/nc-gui-v2/composables/useTabs.ts

@ -53,8 +53,8 @@ export function useTabs() {
} else if ((route.name as string)?.startsWith('nc-projectId-index-index-auth')) {
return tabs.value.findIndex((t) => t.type === 'auth')
}
return -1
// by default, it's showing Team & Auth
return 0
},
set(index: number) {
if (index === -1) {

26
packages/nc-gui-v2/composables/useUIPermission/index.ts

@ -1,15 +1,15 @@
import type { Permission } from './rolePermissions'
import rolePermissions from './rolePermissions'
import { useState } from '#app'
import { USER_PROJECT_ROLES } from '~/lib/constants'
import { USER_PROJECT_ROLES, useGlobal, useState } from '#imports'
export function useUIPermission() {
const { $state } = useNuxtApp()
const { user, previewAs } = useGlobal()
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const isUIAllowed = (permission: Permission, skipPreviewAs = false) => {
const user = $state.user
let userRoles = user?.value?.roles || {}
const getRoles = (skipPreviewAs = false) => {
let userRoles = user.value?.roles || {}
// if string populate key-value paired object
if (typeof userRoles === 'string') {
userRoles = userRoles.split(',').reduce<Record<string, boolean>>((acc, role) => {
@ -21,19 +21,23 @@ export function useUIPermission() {
// merge user role and project specific user roles
let roles = {
...userRoles,
...(projectRoles?.value || {}),
...projectRoles.value,
}
if ($state.previewAs.value && !skipPreviewAs) {
if (previewAs.value && !skipPreviewAs) {
roles = {
[$state.previewAs.value]: true,
[previewAs.value]: true,
}
}
return Object.entries<boolean>(roles).some(([role, hasRole]) => {
return roles
}
const isUIAllowed = (permission: Permission | string, skipPreviewAs = false) => {
return Object.entries<boolean>(getRoles(skipPreviewAs)).some(([role, hasRole]) => {
const rolePermission = rolePermissions[role as keyof typeof rolePermissions] as '*' | Record<Permission, true>
return hasRole && (rolePermission === '*' || rolePermission?.[permission])
return hasRole && (rolePermission === '*' || rolePermission?.[permission as Permission])
})
}

51
packages/nc-gui-v2/composables/useViewColumns.ts

@ -3,13 +3,10 @@ import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { watch } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import { useNuxtApp } from '#app'
import { IsPublicInj } from '#imports'
export function useViewColumns(
view: Ref<ViewType> | undefined,
meta: ComputedRef<TableType>,
isPublic = false,
reloadData?: () => void,
) {
export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRef<TableType>, reloadData?: () => void) {
const isPublic = inject(IsPublicInj, ref(false))
const fields = ref<
{
order: number
@ -31,7 +28,7 @@ export function useViewColumns(
let order = 1
if (view.value?.id) {
const data = (await $api.dbViewColumn.list(view.value.id)) as any[]
const data = (isPublic.value ? meta.value?.columns : await $api.dbViewColumn.list(view.value.id)) as any[]
const fieldById = data.reduce<Record<string, any>>((acc, curr) => {
curr.show = !!curr.show
@ -54,12 +51,19 @@ export function useViewColumns(
}
})
.sort((a, b) => a.order - b.order)
} else if (isPublic) {
fields.value = meta.value.columns as any
}
}
const showAll = async (ignoreIds?: any) => {
if (isPublic.value) {
fields.value = fields.value?.map((field) => ({
...field,
show: true,
}))
reloadData?.()
return
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.showAllColumn(view.value.id, {
@ -74,6 +78,14 @@ export function useViewColumns(
reloadData?.()
}
const hideAll = async (ignoreIds?: any) => {
if (isPublic.value) {
fields.value = fields.value?.map((field) => ({
...field,
show: false,
}))
reloadData?.()
return
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.hideAllColumn(view.value.id, {
@ -89,6 +101,22 @@ export function useViewColumns(
}
const saveOrUpdate = async (field: any, index: number) => {
if (isPublic && fields.value) {
fields.value[index] = field
meta.value.columns = meta.value?.columns?.map((column) => {
if (column.id === field.fk_column_id) {
return {
...column,
...field,
}
}
return column
})
reloadData?.()
return
}
if (isUIAllowed('fieldsSync')) {
if (field.id && view?.value?.id) {
await $api.dbViewColumn.update(view.value.id, field.id, field)
@ -113,15 +141,18 @@ export function useViewColumns(
const showSystemFields = computed({
get() {
return view?.value?.show_system_fields || false
// todo: show_system_fields missing from ViewType
return (view?.value as any)?.show_system_fields || false
},
set(v: boolean) {
if (view?.value?.id) {
if (!isPublic.value) {
$api.dbView
.update(view.value.id, {
show_system_fields: v,
})
.finally(() => reloadData?.())
}
;(view.value as any).show_system_fields = v
}
},

51
packages/nc-gui-v2/composables/useViewData.ts

@ -2,9 +2,7 @@ import type { Api, ColumnType, FormType, GalleryType, PaginatedType, TableType,
import type { ComputedRef, Ref } from 'vue'
import { message } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { useProject } from '#imports'
import { NOCO } from '~/lib'
import { extractPkFromRow, extractSdkResponseErrorMsg } from '~/utils'
import { IsPublicInj, NOCO, extractPkFromRow, extractSdkResponseErrorMsg, useProject } from '#imports'
const formatData = (list: Record<string, any>[]) =>
list.map((row) => ({
@ -32,16 +30,30 @@ export function useViewData(
throw new Error('Table meta is not available')
}
const formattedData = ref<Row[]>([])
const paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 })
const _paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 })
const aggCommentCount = ref<{ row_id: string; count: number }[]>([])
const galleryData = ref<GalleryType | undefined>(undefined)
const formColumnData = ref<FormType | undefined>(undefined)
const formViewData = ref<FormType | undefined>(undefined)
const galleryData = ref<GalleryType>()
const formColumnData = ref<FormType>()
// todo: missing properties on FormType (success_msg, show_blank_form,
const formViewData = ref<FormType & { success_msg?: string; show_blank_form?: boolean }>()
const formattedData = ref<Row[]>([])
const isPublic = inject(IsPublicInj, ref(false))
const { project } = useProject()
const { fetchSharedViewData, paginationData: sharedPaginationData } = useSharedView()
const { $api } = useNuxtApp()
const paginationData = computed({
get: () => (isPublic.value ? sharedPaginationData.value : _paginationData.value),
set: (value) => {
if (isPublic.value) {
sharedPaginationData.value = value
} else {
_paginationData.value = value
}
},
})
const selectedAllRecords = computed({
get() {
return !!formattedData.value.length && formattedData.value.every((row: Row) => row.rowMeta.selected)
@ -70,9 +82,7 @@ export function useViewData(
/** load row comments count */
const loadAggCommentsCount = async () => {
// todo: handle in public api
// if (this.isPublicView) {
// return;
// }
if (isPublic.value) return
const ids = formattedData.value
?.filter(({ rowMeta: { new: isNew } }) => !isNew)
@ -94,15 +104,18 @@ export function useViewData(
}
const loadData = async (params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) => {
if (!project?.value?.id || !meta?.value?.id || !viewMeta?.value?.id) return
const response = await $api.dbViewRow.list('noco', project.value.id, meta.value.id, viewMeta.value.id, {
if ((!project?.value?.id || !meta?.value?.id || !viewMeta?.value?.id) && !isPublic.value) return
const response = !isPublic.value
? await $api.dbViewRow.list('noco', project.value.id!, meta.value.id!, viewMeta!.value.id, {
...params,
where: where?.value,
})
: await fetchSharedViewData()
formattedData.value = formatData(response.list)
paginationData.value = response.pageInfo
loadAggCommentsCount()
await loadAggCommentsCount()
}
const loadGalleryData = async () => {
@ -133,7 +146,8 @@ export function useViewData(
rowMeta: {},
oldRow: { ...insertedData },
})
syncCount()
await syncCount()
} catch (error: any) {
message.error(await extractSdkResponseErrorMsg(error))
}
@ -241,8 +255,10 @@ export function useViewData(
return
}
}
formattedData.value.splice(rowIndex, 1)
syncCount()
await syncCount()
} catch (e: any) {
message.error(`Failed to delete row: ${await extractSdkResponseErrorMsg(e)}`)
}
@ -272,7 +288,8 @@ export function useViewData(
return message.error(`Failed to delete row: ${await extractSdkResponseErrorMsg(e)}`)
}
}
syncCount()
await syncCount()
}
const loadFormView = async () => {

97
packages/nc-gui-v2/composables/useViewFilters.ts

@ -1,30 +1,69 @@
import type { FilterType, ViewType } from 'nocodb-sdk'
import type { ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { useNuxtApp, useUIPermission } from '#imports'
import { useMetas } from '~/composables/useMetas'
import { IsPublicInj, ReloadViewDataHookInj, useMetas, useNuxtApp, useUIPermission } from '#imports'
import type { Filter } from '~/lib'
export function useViewFilters(
view: Ref<ViewType> | undefined,
parentId?: string,
autoApply?: ComputedRef<boolean>,
reloadData?: () => void,
shared = false,
siblingFilters?: Filter[],
) {
const filters = ref<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>([])
const { nestedFilters } = useSharedView()
const reloadHook = inject(ReloadViewDataHookInj)
const _filters = ref<Filter[]>([])
const isPublic = inject(IsPublicInj, ref(false))
const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const { metas } = useMetas()
const loadFilters = async () => {
const filters = computed({
get: () => (isPublic.value ? siblingFilters || nestedFilters.value : _filters.value),
set: (value) => {
if (isPublic.value) {
if (siblingFilters) {
siblingFilters = value
} else {
nestedFilters.value = value
}
nestedFilters.value = [...nestedFilters.value]
reloadHook?.trigger()
} else {
_filters.value = value
}
},
})
const placeholderFilter: Filter = {
comparison_op: 'eq',
value: '',
status: 'create',
logical_op: 'and',
}
const loadFilters = async (hookId?: string) => {
if (isPublic.value) return
if (hookId) {
if (parentId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId)
} else {
filters.value = (await $api.dbTableWebhookFilter.read(hookId as string)) as any
}
} else {
if (parentId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId)
} else {
filters.value = await $api.dbTableFilter.read(view?.value?.id as string)
}
}
}
const sync = async (_nested = false) => {
const sync = async (hookId?: string, _nested = false) => {
for (const [i, filter] of Object.entries(filters.value)) {
if (filter.status === 'delete') {
await $api.dbTableFilter.delete(filter.id as string)
@ -34,18 +73,26 @@ export function useViewFilters(
fk_parent_id: parentId,
})
} else if (filter.status === 'create') {
if (hookId) {
filters.value[+i] = (await $api.dbTableWebhookFilter.create(hookId, {
...filter,
fk_parent_id: parentId,
})) as any
} else {
filters.value[+i] = (await $api.dbTableFilter.create(view?.value?.id as string, {
...filter,
fk_parent_id: parentId,
})) as any
}
}
}
reloadData?.()
}
const deleteFilter = async (filter: FilterType & { status: string }, i: number) => {
const deleteFilter = async (filter: Filter, i: number) => {
// if shared or sync permission not allowed simply remove it from array
if (shared || !isUIAllowed('filterSync')) {
if (isPublic.value || !isUIAllowed('filterSync')) {
filters.value.splice(i, 1)
reloadData?.()
} else {
@ -66,9 +113,14 @@ export function useViewFilters(
}
}
const saveOrUpdate = async (filter: FilterType & { status?: string }, i: number, force = false) => {
const saveOrUpdate = async (filter: Filter, i: number, force = false) => {
if (isPublic.value) {
filters.value[i] = { ...filter } as any
filters.value = [...filters.value]
return
}
if (!view?.value) return
if (shared || !isUIAllowed('filterSync')) {
if (!isUIAllowed('filterSync')) {
// skip
} else if (!autoApply?.value && !force) {
filter.status = filter.id ? 'update' : 'create'
@ -78,30 +130,29 @@ export function useViewFilters(
fk_parent_id: parentId,
})
} else {
filters.value[i] = await $api.dbTableFilter.create(view?.value?.id as string, {
// todo: return type of dbTableFilter is void?
filters.value[i] = (await $api.dbTableFilter.create(view?.value?.id as string, {
...filter,
fk_parent_id: parentId,
})
})) as any
}
reloadData?.()
}
const addFilter = () => {
filters.value.push({
comparison_op: 'eq',
value: '',
status: 'create',
logical_op: 'and',
})
filters.value.push(placeholderFilter)
}
const addFilterGroup = async (parentId?: string) => {
filters.value.push({
parentId,
const addFilterGroup = async () => {
const child = placeholderFilter
const placeHolderGroupFilter: Filter = {
is_group: true,
status: 'create',
logical_op: 'and',
})
}
if (isPublic.value) placeHolderGroupFilter.children = [child]
filters.value.push(placeHolderGroupFilter)
const index = filters.value.length - 1
await saveOrUpdate(filters.value[index], index, true)
}

45
packages/nc-gui-v2/composables/useViewSorts.ts

@ -1,18 +1,39 @@
import type { GalleryType, GridType, KanbanType, SortType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useNuxtApp } from '#imports'
import { IsPublicInj, ReloadViewDataHookInj, useNuxtApp } from '#imports'
export function useViewSorts(
view: Ref<(GridType | KanbanType | GalleryType) & { id?: string }> | undefined,
reloadData?: () => void,
) {
const sorts = ref<SortType[]>([])
const _sorts = ref<SortType[]>([])
const { sorts: sharedViewSorts, sharedView } = useSharedView()
const reloadHook = inject(ReloadViewDataHookInj)
const isPublic = inject(IsPublicInj, ref(false))
const sorts = computed<SortType[]>({
get: () => (isPublic.value ? sharedViewSorts.value : _sorts.value),
set: (value) => {
if (isPublic.value) {
sharedViewSorts.value = value
} else {
_sorts.value = value
}
reloadHook?.trigger()
},
})
const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const loadSorts = async () => {
if (isPublic.value) {
const sharedSorts = sharedView.value?.sorts || []
sorts.value = [...sharedSorts]
return
}
if (!view?.value) return
sorts.value = ((await $api.dbTableSort.list(view?.value?.id as string)) as any)?.sorts?.list
}
@ -20,6 +41,12 @@ export function useViewSorts(
const saveOrUpdate = async (sort: SortType, i: number) => {
// TODO:
// if (!this.shared && this._isUIAllowed('sortSync')) {
if (isPublic.value) {
sorts.value[i] = sort
sorts.value = [...sorts.value]
return
}
if (isUIAllowed('sortSync')) {
if (sort.id) {
await $api.dbTableSort.update(sort.id, sort)
@ -30,19 +57,23 @@ export function useViewSorts(
reloadData?.()
}
const addSort = () => {
sorts.value.push({
sorts.value = [
...sorts.value,
{
direction: 'asc',
})
},
]
}
const deleteSort = async (sort: SortType, i: number) => {
// TOOD:
// if (!this.shared && sort.id && this._isUIAllowed('sortSync')) {
if (isUIAllowed('sortSync') && sort.id) {
if (isUIAllowed('sortSync') && sort.id && !isPublic.value) {
await $api.dbTableSort.delete(sort.id)
} else {
sorts.value.splice(i, 1)
}
sorts.value.splice(i, 1)
sorts.value = [...sorts.value]
}
return { sorts, loadSorts, addSort, deleteSort, saveOrUpdate }
}

5
packages/nc-gui-v2/composables/useViews.ts

@ -1,16 +1,17 @@
import type { TableType, ViewType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp } from '#app'
import { unref, useNuxtApp, watch } from '#imports'
export function useViews(meta: MaybeRef<TableType | undefined>) {
let views = $ref<ViewType[]>([])
const { $api } = useNuxtApp()
const loadViews = async () => {
const _meta = unref(meta)
if (_meta && _meta.id) {
const response = (await $api.dbView.list(_meta.id)).list
const response = (await $api.dbView.list(_meta.id)).list as ViewType[]
if (response) {
views = response.sort((a, b) => a.order! - b.order!)
}

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

@ -6,6 +6,7 @@ import type { Row } from '~/composables'
import type { TabItem } from '~/composables/useTabs'
export const ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell')
export const IsPublicInj: InjectionKey<Ref<boolean>> = Symbol('is-public')
export const RowInj: InjectionKey<Ref<Row>> = Symbol('row')
export const ColumnInj: InjectionKey<Ref<ColumnType & { meta: any }>> = Symbol('column-injection')
export const MetaInj: InjectionKey<ComputedRef<TableType>> = Symbol('meta-injection')

3
packages/nc-gui-v2/httpsnippet-shims.d.ts vendored

@ -0,0 +1,3 @@
declare module 'httpsnippet' {
export default new ((): any => {})()
}

1
packages/nc-gui-v2/just-clone-shims.d.ts vendored

@ -0,0 +1 @@
declare module 'just-clone'

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

@ -43,7 +43,11 @@ const logout = () => {
<div class="flex-1" />
<a-tooltip placement="left">
<GeneralReleaseInfo />
<GeneralShareBaseButton />
<a-tooltip placement="bottom">
<template #title> Switch language </template>
<div class="flex pr-4 items-center">
@ -83,7 +87,7 @@ const logout = () => {
</a-layout-header>
</Transition>
<a-tooltip>
<a-tooltip placement="bottom">
<template #title> Switch language </template>
<Transition name="layout">

37
packages/nc-gui-v2/layouts/shared-view.vue

@ -0,0 +1,37 @@
<script lang="ts" setup>
import { navigateTo } from '#app'
</script>
<script lang="ts">
export default {
name: 'SharedView',
}
</script>
<template>
<a-layout id="nc-app">
<a-layout class="!flex-col">
<a-layout-header class="flex !bg-primary items-center text-white pl-3 pr-4 shadow-lg">
<div class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105" @click="navigateTo('/')">
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<div class="flex-1" />
</a-layout-header>
<div class="w-full overflow-hidden" style="height: calc(100% - var(--header-height))">
<slot />
</div>
</a-layout>
</a-layout>
</template>
<style lang="scss" scoped>
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
</style>

15
packages/nc-gui-v2/lib/types.ts

@ -1,5 +1,5 @@
import type { FilterType } from 'nocodb-sdk'
import type { Role } from './enums'
export interface User {
id: string
email: string
@ -10,4 +10,17 @@ export interface User {
project_id?: string
}
export interface ProjectMetaInfo {
Node?: string
Arch?: string
Platform?: string
Docker?: boolean
Database?: string
ProjectOnRootDB?: string
RootDB?: string
PackageVersion?: string
}
export type Roles = Record<Role, boolean>
export type Filter = FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string; readOnly?: boolean }

20
packages/nc-gui-v2/nuxt.config.ts

@ -48,6 +48,13 @@ export default defineNuxtConfig({
},
vite: {
// todo: minifiy again
build: {
minify: false,
rollupOptions: {
external: 'httpsnippet',
},
},
css: {
preprocessorOptions: {
less: {
@ -59,6 +66,7 @@ export default defineNuxtConfig({
plugins: [
vueI18n({
include: path.resolve(__dirname, './lang'),
runtimeOnly: false,
}),
Icons({
autoInstall: true,
@ -81,7 +89,6 @@ export default defineNuxtConfig({
],
define: {
'process.env.DEBUG': 'false',
'global': {},
},
server: {
watch: {
@ -91,17 +98,6 @@ export default defineNuxtConfig({
},
experimental: {
reactivityTransform: true,
viteNode: false,
},
typescript: {
typeCheck: true,
strict: true,
tsConfig: {
compilerOptions: {
types: ['@intlify/vite-plugin-vue-i18n/client', 'vue-i18n', 'unplugin-icons/types/vue', 'nuxt-windicss'],
},
},
},
image: {

380
packages/nc-gui-v2/package-lock.json generated

File diff suppressed because it is too large Load Diff

6
packages/nc-gui-v2/package.json

@ -27,9 +27,8 @@
"socket.io-client": "^4.5.1",
"sortablejs": "^1.15.0",
"unique-names-generator": "^4.7.1",
"url": "^0.11.0",
"util": "^0.12.4",
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
"vue-i18n": "^9.1.10",
"vuedraggable": "^4.1.0",
"vuetify": "^3.0.0-alpha.13",
@ -45,13 +44,12 @@
"@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25",
"@iconify-json/ri": "^1.1.3",
"@intlify/vite-plugin-vue-i18n": "^4.0.0",
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@nuxt/image-edge": "^1.0.0-27657146.da85542",
"@types/axios": "^0.14.0",
"@types/file-saver": "^2.0.5",
"@types/papaparse": "^5.3.2",
"@types/sortablejs": "^1.13.0",
"@vitejs/plugin-vue": "^2.3.3",
"@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37",
"@vue/test-utils": "^2.0.2",

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

Loading…
Cancel
Save