Browse Source

Merge branch 'develop' into fix/gui-v2-misc

pull/3051/head
Wing-Kam Wong 2 years ago
parent
commit
62de7aaafb
  1. 2
      packages/nc-gui-v2/assets/style-v2.scss
  2. 12
      packages/nc-gui-v2/components.d.ts
  3. 232
      packages/nc-gui-v2/components/cell/MultiSelect.vue
  4. 123
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  5. 2
      packages/nc-gui-v2/components/cell/attachment/Carousel.vue
  6. 49
      packages/nc-gui-v2/components/cell/attachment/index.vue
  7. 52
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  8. 2
      packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue
  9. 6
      packages/nc-gui-v2/components/dlg/AirtableImport.vue
  10. 18
      packages/nc-gui-v2/components/dlg/TableCreate.vue
  11. 6
      packages/nc-gui-v2/components/dlg/TableRename.vue
  12. 110
      packages/nc-gui-v2/components/general/ColorPicker.vue
  13. 10
      packages/nc-gui-v2/components/general/MiniSidebar.vue
  14. 4
      packages/nc-gui-v2/components/monaco/Editor.vue
  15. 29
      packages/nc-gui-v2/components/smartsheet-column/CheckboxOptions.vue
  16. 2
      packages/nc-gui-v2/components/smartsheet-column/CurrencyOptions.vue
  17. 2
      packages/nc-gui-v2/components/smartsheet-column/DurationOptions.vue
  18. 56
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  19. 60
      packages/nc-gui-v2/components/smartsheet-column/FormulaOptions.vue
  20. 44
      packages/nc-gui-v2/components/smartsheet-column/LookupOptions.vue
  21. 31
      packages/nc-gui-v2/components/smartsheet-column/RatingOptions.vue
  22. 18
      packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue
  23. 112
      packages/nc-gui-v2/components/smartsheet-column/SelectOptions.vue
  24. 5
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  25. 55
      packages/nc-gui-v2/components/smartsheet-header/Menu.vue
  26. 12
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  27. 3
      packages/nc-gui-v2/components/smartsheet-header/VirtualCellIcon.vue
  28. 3
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  29. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue
  30. 37
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue
  31. 5
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  32. 1
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  33. 9
      packages/nc-gui-v2/components/smartsheet-toolbar/SharedViewList.vue
  34. 1
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  35. 49
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  36. 6
      packages/nc-gui-v2/components/smartsheet/Gallery.vue
  37. 212
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  38. 28
      packages/nc-gui-v2/components/smartsheet/Row.vue
  39. 93
      packages/nc-gui-v2/components/smartsheet/expanded-form/Comments.vue
  40. 60
      packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue
  41. 139
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  42. 3
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  43. 13
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  44. 4
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  45. 182
      packages/nc-gui-v2/components/smartsheet/sidebar/menu/ApiSnippet.vue
  46. 3
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  47. 28
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  48. 2
      packages/nc-gui-v2/components/virtual-cell/Formula.vue
  49. 69
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  50. 59
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  51. 33
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  52. 81
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  53. 82
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  54. 4
      packages/nc-gui-v2/components/webhook/Test.vue
  55. 1
      packages/nc-gui-v2/composables/index.ts
  56. 7
      packages/nc-gui-v2/composables/useColumnCreateStore.ts
  57. 209
      packages/nc-gui-v2/composables/useExpandedFormStore.ts
  58. 5
      packages/nc-gui-v2/composables/useGlobal/state.ts
  59. 5
      packages/nc-gui-v2/composables/useGlobal/types.ts
  60. 60
      packages/nc-gui-v2/composables/useLTARStore.ts
  61. 105
      packages/nc-gui-v2/composables/useSmartsheetRowStore.ts
  62. 10
      packages/nc-gui-v2/composables/useSmartsheetStore.ts
  63. 51
      packages/nc-gui-v2/composables/useViewData.ts
  64. 10
      packages/nc-gui-v2/layouts/base.vue
  65. 1
      packages/nc-gui-v2/nuxt.config.ts
  66. 962
      packages/nc-gui-v2/package-lock.json
  67. 4
      packages/nc-gui-v2/package.json
  68. 10
      packages/nc-gui-v2/pages/index/user/index.vue
  69. 4
      packages/nc-gui-v2/pages/index/user/index/index.vue
  70. 6
      packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue
  71. 4
      packages/nc-gui-v2/pages/project/index/[id].vue
  72. 6
      packages/nc-gui-v2/pages/project/index/create.vue
  73. 2
      packages/nc-gui-v2/pages/projects/index.vue
  74. 6
      packages/nc-gui-v2/plugins/domPurify.ts
  75. 9
      packages/nc-gui-v2/plugins/state.ts
  76. 1
      packages/nc-gui-v2/tsconfig.json
  77. 11
      packages/nc-gui-v2/utils/dataUtils.ts
  78. 1
      packages/nc-gui-v2/utils/index.ts
  79. 1
      packages/nc-gui/components/project/spreadsheet/components/ExpandedForm.vue
  80. 19
      packages/nocodb-sdk/src/lib/Api.ts
  81. 2
      packages/nocodb/src/lib/models/View.ts
  82. 58
      scripts/sdk/swagger.json

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

@ -75,7 +75,7 @@ html {
// menu item styling
.nc-menu-item {
@apply cursor-pointer text-xs flex items-center gap-2 px-4 py-3 after:(content-[''] absolute top-0 left-0 bottom-0 w-full h-full right-0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
@apply cursor-pointer text-xs flex items-center gap-2 px-4 py-3 relative after:(content-[''] absolute top-0 left-0 bottom-0 w-full h-full right-0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
}
.nc-sidebar-right-item {

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

@ -9,6 +9,7 @@ declare module '@vue/runtime-core' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
ABadgeRibbon: typeof import('ant-design-vue/es')['BadgeRibbon']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACarousel: typeof import('ant-design-vue/es')['Carousel']
@ -23,7 +24,6 @@ declare module '@vue/runtime-core' {
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
@ -47,7 +47,6 @@ declare module '@vue/runtime-core' {
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASkeletonImage: typeof import('ant-design-vue/es')['SkeletonImage']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
@ -56,6 +55,7 @@ declare module '@vue/runtime-core' {
ATableColumn: typeof import('ant-design-vue/es')['TableColumn']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
@ -65,14 +65,12 @@ declare module '@vue/runtime-core' {
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
MaterialSymbolsArrowBackRounded: typeof import('~icons/material-symbols/arrow-back-rounded')['default']
MaterialSymbolsArrowForwardRounded: typeof import('~icons/material-symbols/arrow-forward-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']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsMenu: typeof import('~icons/material-symbols/menu')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
MdiAccountGroup: typeof import('~icons/mdi/account-group')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
MdiApi: typeof import('~icons/mdi/api')['default']
@ -86,9 +84,9 @@ declare module '@vue/runtime-core' {
MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiCloseCircle: typeof import('~icons/mdi/close-circle')['default']
MdiCloseThick: typeof import('~icons/mdi/close-thick')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiDatabase: typeof import('~icons/mdi/database')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
MdiDiscord: typeof import('~icons/mdi/discord')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
@ -103,6 +101,7 @@ declare module '@vue/runtime-core' {
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLink: typeof import('~icons/mdi/link')['default']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
@ -122,6 +121,7 @@ declare module '@vue/runtime-core' {
MdiStar: typeof import('~icons/mdi/star')['default']
MdiStore: typeof import('~icons/mdi/store')['default']
MdiTable: typeof import('~icons/mdi/table')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
MdiTableBorder: typeof import('~icons/mdi/table-border')['default']
MdiText: typeof import('~icons/mdi/text')['default']
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']

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

@ -1,78 +1,196 @@
<script lang="ts" setup>
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk'
import { computed, inject } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
import { ActiveCellInj, ColumnInj } from '~/context'
import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props {
modelValue: string | null
modelValue: string | string[] | undefined
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const column = inject(ColumnInj)
const isForm = inject<boolean>('isForm', false)
const editEnabled = inject(EditModeInj, ref(false))
// const isForm = inject<boolean>('isForm', false)
// const editEnabled = inject(EditModeInj, ref(false))
const active = inject(ActiveCellInj, ref(false))
const options = computed(() => column?.value?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || [])
const selectedIds = ref<string[]>([])
const aselect = ref<typeof AntSelect>()
const isOpen = ref(false)
const localState = computed({
get() {
return modelValue?.match(/(?:[^',]|\\')+(?='?(?:,|$))/g)?.map((v: string) => v.replace(/\\'/g, "'"))
},
set(val?: string[]) {
emit('update:modelValue', val?.filter((v) => options.value.includes(v)).join(','))
const options = computed(() => {
if (column?.value.colOptions) {
const opts = column.value.colOptions
? column.value.colOptions.options.filter((el: SelectOptionType) => el.title !== '') || []
: []
for (const op of opts.filter((el: SelectOptionType) => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')
}
return opts
}
return []
})
const vModel = computed({
get: () => selectedIds.value.map((el) => options.value.find((op: SelectOptionType) => op.id === el).title),
set: (val) => emit('update:modelValue', val.length === 0 ? null : val.join(',')),
})
const selectedTitles = computed(() =>
modelValue
? 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)
if (opa && opb) {
return opa.order - opb.order
}
return 0
})
: modelValue.split(',')
: modelValue
: [],
)
const handleKeys = (e: KeyboardEvent) => {
switch (e.key) {
case 'Escape':
e.preventDefault()
isOpen.value = false
break
case 'Enter':
e.stopPropagation()
break
}
}
const handleClose = (e: MouseEvent) => {
if (aselect.value && !aselect.value.$el.contains(e.target)) {
isOpen.value = false
}
}
onMounted(() => {
selectedIds.value = selectedTitles.value.map((el) => {
return options.value.find((op: SelectOptionType) => op.title === el).id
})
})
useEventListener(document, 'click', handleClose)
watch(
() => modelValue,
(_n, _o) => {
selectedIds.value = selectedTitles.value.map((el) => {
return options.value.find((op: SelectOptionType) => op.title === el).id
})
},
)
watch(isOpen, (n, _o) => {
if (n === false) {
aselect.value.blur()
}
})
</script>
<template>
<!--
<v-select
v-model="localState"
:items="options"
hide-details
:clearable="!column.rqd"
variation="outlined"
multiple
/>
-->
<v-combobox
v-model="localState"
:items="options"
multiple
chips
flat
dense
solo
hide-details
deletable-chips
class="text-center mt-0"
<a-select
ref="aselect"
v-model:value="vModel"
mode="multiple"
class="w-full"
placeholder="Select an option"
:bordered="false"
show-arrow
:show-search="false"
:open="isOpen"
@keydown="handleKeys"
@click="isOpen = !isOpen"
>
<!-- <template #selection="data"> -->
<!-- <v-chip -->
<!-- :key="data.item" -->
<!-- small -->
<!-- class="ma-1 " -->
<!-- :color="colors[setValues.indexOf(data.item) % colors.length]" -->
<!-- @click:close="data.parent.selectItem(data.item)" -->
<!-- > -->
<!-- {{ data.item }} -->
<!-- </v-chip> -->
<!-- </template> -->
<!-- <template #item="{item}"> -->
<!-- <v-chip small :color="colors[setValues.indexOf(item) % colors.length]"> -->
<!-- {{ item }} -->
<!-- </v-chip> -->
<!-- </template> -->
<!-- <template #append> -->
<!-- <v-icon small class="mt-2"> -->
<!-- mdi-menu-down -->
<!-- </v-icon> -->
<!-- </template> -->
</v-combobox>
<a-select-option v-for="op of options" :key="op.id" :value="op.title" @click.stop>
<a-tag class="rounded-tag" :color="op.color">
<span class="text-slate-500">{{ op.title }}</span>
</a-tag>
</a-select-option>
<template #tagRender="{ value: val, onClose }">
<a-tag
v-if="options.find((el: SelectOptionType) => el.title === val)"
class="rounded-tag"
:style="{ display: 'flex', alignItems: 'center' }"
:color="options.find((el: SelectOptionType) => el.title === val).color"
:closable="active && (vModel.length > 1 || !column?.rqd)"
:close-icon="h(MdiCloseCircle, { class: ['ms-close-icon'] })"
@close="onClose"
>
<span class="text-slate-500">{{ val }}</span>
</a-tag>
</template>
</a-select>
</template>
<style scoped></style>
<style scoped>
.ms-close-icon {
color: rgba(0, 0, 0, 0.25);
cursor: pointer;
display: flex;
font-size: 12px;
font-style: normal;
height: 12px;
line-height: 1;
text-align: center;
text-transform: none;
transition: color 0.3s ease, opacity 0.15s ease;
width: 12px;
z-index: 1;
margin-right: -6px;
margin-left: 3px;
}
.ms-close-icon:before {
display: block;
}
.ms-close-icon:hover {
color: rgba(0, 0, 0, 0.45);
}
.rounded-tag {
padding: 0px 12px;
border-radius: 12px;
}
:deep(.ant-tag) {
@apply "rounded-tag";
}
:deep(.ant-tag-close-icon) {
@apply "text-slate-500";
}
</style>
<!--
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-->

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

@ -1,71 +1,100 @@
<script lang="ts" setup>
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk'
import { computed, inject } from '#imports'
import { ColumnInj } from '~/context'
import { ActiveCellInj, ColumnInj } from '~/context'
interface Props {
modelValue: string | null
modelValue: string | undefined
}
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, ref(false))
const active = inject(ActiveCellInj, ref(false))
const aselect = ref<typeof AntSelect>()
const isOpen = ref(false)
const vModel = computed({
get: () => modelValue?.replace(/\\'/g, "'").replace(/^'|'$/g, ''),
set: (val) => emit('update:modelValue', val),
get: () => modelValue,
set: (val) => emit('update:modelValue', val || null),
})
const options = computed(() => column.value.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || [])
</script>
<template>
<v-select v-model="vModel" :items="options" hide-details :clearable="!column.rqd" variation="outlined">
<!-- v-on="parentListeners"
<template #selection="{ item }">
<div
class="d-100"
:class="{
'text-center': !isForm,
}"
>
<v-chip small :color="enumColor.light[options.indexOf(item) % enumColor.light.length]" class="ma-1">
{{ item.text }}
</v-chip>
</div>
</template>
<template #item="{ item }">
<v-chip small :color="enumColor.light[options.indexOf(item) % enumColor.light.length]">
{{ item }}
</v-chip>
</template>
<template #append>
<v-icon small class="mt-1"> mdi-menu-down</v-icon>
</template> -->
</v-select>
</template>
const options = computed(() => {
if (column?.value.colOptions) {
const opts = column.value.colOptions
? column.value.colOptions.options.filter((el: SelectOptionType) => el.title !== '') || []
: []
for (const op of opts.filter((el: SelectOptionType) => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')
}
return opts
}
return []
})
<style scoped lang="scss">
/*:deep {
.v-select {
min-width: 150px;
const handleKeys = (e: KeyboardEvent) => {
switch (e.key) {
case 'Escape':
e.preventDefault()
isOpen.value = false
break
}
}
.v-input__slot {
padding-right: 0 !important;
padding-left: 35px !important;
const handleClose = (e: MouseEvent) => {
if (aselect.value && !aselect.value.$el.contains(e.target)) {
isOpen.value = false
aselect.value.blur()
}
}
.v-input__icon.v-input__icon--clear {
width: 15px !important;
min-width: 13px !important;
useEventListener(document, 'click', handleClose)
.v-icon {
font-size: 13px !important;
}
watch(isOpen, (n, _o) => {
if (n === false) {
aselect.value.blur()
}
}*/
})
</script>
<template>
<a-select
ref="aselect"
v-model:value="vModel"
class="w-full"
:allow-clear="!column.rqd && active"
placeholder="Select an option"
:bordered="false"
:open="isOpen"
@select="isOpen = false"
@keydown="handleKeys"
@click="isOpen = !isOpen"
>
<a-select-option v-for="op of options" :key="op.title" @click.stop>
<a-tag class="rounded-tag" :color="op.color">
<span class="text-slate-500">{{ op.title }}</span>
</a-tag>
</a-select-option>
</a-select>
</template>
<style scoped lang="scss">
.rounded-tag {
padding: 0px 12px;
border-radius: 12px;
}
:deep(.ant-tag) {
@apply "rounded-tag";
}
:deep(.ant-select-clear) {
opacity: 1;
}
</style>
<!--
/**

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

@ -2,7 +2,7 @@
import { onKeyDown } from '@vueuse/core'
import { useAttachmentCell } from './utils'
import { isImage } from '~/utils'
import { computed, onClickOutside, ref, watch } from '#imports'
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'

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

@ -1,39 +1,37 @@
<script setup lang="ts">
import { onKeyDown } from '@vueuse/core'
import { useProvideAttachmentCell } from './utils'
import Modal from './Modal.vue'
import { useSortable } from './sort'
import Modal from './Modal.vue'
import Carousel from './Carousel.vue'
import { onMounted, ref, useDropZone, watch } from '#imports'
import { computed, ref, useDropZone, useSmartsheetStoreOrThrow, watch } from '#imports'
import { isImage, openLink } from '~/utils'
import MaterialSymbolsAttachFile from '~icons/material-symbols/attach-file'
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand'
import MaterialSymbolsFileCopyOutline from '~icons/material-symbols/file-copy-outline'
import MdiReload from '~icons/mdi/reload'
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file'
interface Props {
modelValue: string | Record<string, any>[] | null
rowIndex: number
}
interface Emits {
(event: 'update:modelValue', value: string | Record<string, any>): void
}
const { modelValue } = defineProps<Props>()
const { modelValue, rowIndex } = defineProps<Props>()
const emits = defineEmits<Emits>()
const dropZoneRef = ref<HTMLTableDataCellElement>()
const sortableRef = ref<HTMLDivElement>()
const { cellRefs } = useSmartsheetStoreOrThrow()!
const { column, modalVisible, attachments, visibleItems, onDrop, isLoading, open, FileIcon, selectedImage, isReadonly } =
useProvideAttachmentCell(updateModelValue)
const currentCellRef = computed(() => cellRefs.value.find((cell) => cell.dataset.key === `${rowIndex}${column.value.id}`))
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, isReadonly)
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
const { isOverDropZone } = useDropZone(currentCellRef, onDrop)
/** on new value, reparse our stored attachments */
watch(
@ -56,24 +54,17 @@ onKeyDown('Escape', () => {
modalVisible.value = false
isOverDropZone.value = false
})
/** if possible, on mounted we try to fetch the relevant `td` cell to use as a dropzone */
onMounted(() => {
if (typeof document !== 'undefined') {
dropZoneRef.value = document.querySelector(`td[data-col="${column.value.id}"]`) as HTMLTableDataCellElement
}
})
</script>
<template>
<div class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1">
<Carousel />
<template v-if="!isReadonly && !dragging && dropZoneRef">
<template v-if="!isReadonly && !dragging && !!currentCellRef">
<general-overlay
v-model="isOverDropZone"
inline
:target="`td[data-col='${column.id}']`"
:target="currentCellRef"
class="text-white text-lg ring ring-pink-500 bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl"
>
<MaterialSymbolsFileCopyOutline class="text-pink-500" /> Drop here
@ -100,12 +91,15 @@ onMounted(() => {
</div>
<template v-if="visibleItems.length">
<div ref="sortableRef" :class="{ dragging }" class="flex flex-wrap gap-2 p-1 scrollbar-thin-dull">
<div
ref="sortableRef"
:class="{ dragging }"
class="flex justify-center items-center flex-wrap gap-2 p-1 scrollbar-thin-dull max-h-[150px] overflow-auto"
>
<div
v-for="(item, i) of visibleItems"
:id="item.url"
:key="item.url || item.title"
style="flex: 1 1 50px"
:class="isImage(item.title, item.mimetype) ? '' : 'border-1 rounded'"
class="nc-attachment flex items-center justify-center min-h-[50px]"
>
@ -120,7 +114,7 @@ onMounted(() => {
placeholder
:alt="item.title || `#${i}`"
:src="item.url || item.data"
class="ring-1 ring-gray-300 rounded"
class="ring-1 ring-gray-300 rounded max-h-[50px] max-w-[50px]"
@click="selectedImage = item"
/>
@ -137,10 +131,7 @@ onMounted(() => {
<a-tooltip v-else placement="bottom">
<template #title> View attachments </template>
<MaterialArrowExpandIcon
class="select-none transform group-hover:(text-pink-500 scale-120)"
@click.stop="modalVisible = true"
/>
<MdiArrowExpand class="select-none transform group-hover:(text-pink-500 scale-120)" @click.stop="modalVisible = true" />
</a-tooltip>
</div>
</template>
@ -152,6 +143,10 @@ onMounted(() => {
<style lang="scss">
.nc-cell {
.nc-attachment-cell {
.nc-attachment {
@apply w-[50px] h-[50px] min-h-[50px] min-w-[50px];
}
.ghost,
.ghost > * {
@apply !pointer-events-none;

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

@ -1,27 +1,25 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { useToast } from 'vue-toastification'
import { useProject, useTable, useTabs, watchEffect } from '#imports'
import { useNuxtApp, useRoute } from '#app'
import { computed, useProject, useTable, useTabs, watchEffect } from '#imports'
import { TabType } from '~/composables'
import MdiTable from '~icons/mdi/table'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
import MdiMenuDown from '~icons/mdi/chevron-down'
import MdiPlus from '~icons/mdi/plus-circle-outline'
import MdiDrag from '~icons/mdi/drag-vertical'
import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiDrag from '~icons/mdi/drag-vertical'
import MdiPlus from '~icons/mdi/plus-circle-outline'
const { addTab } = useTabs()
const toast = useToast()
const { $api, $e } = useNuxtApp()
const route = useRoute()
const { tables, loadTables } = useProject(route.params.projectId as string)
const { activeTab } = useTabs()
const { deleteTable } = useTable()
const tablesById = $computed<Record<string, TableType>>(() =>
@ -32,13 +30,11 @@ const tablesById = $computed<Record<string, TableType>>(() =>
)
const showTableList = ref(true)
const tableCreateDlg = ref(false)
let key = $ref(0)
const menuRef = $ref<HTMLLIElement>()
let key = $ref(0)
let sortable: Sortable
// todo: replace with vuedraggable
@ -106,13 +102,11 @@ const icon = (table: TableType) => {
}
const filterQuery = $ref('')
const filteredTables = $computed(() => {
return tables?.value?.filter((table) => !filterQuery || table?.title.toLowerCase()?.includes(filterQuery.toLowerCase()))
})
const contextMenuTarget = reactive<{ type?: 'table' | 'main'; value?: any }>({})
const setMenuContext = (type: 'table' | 'main', value?: any) => {
contextMenuTarget.type = type
contextMenuTarget.value = value
@ -120,24 +114,24 @@ const setMenuContext = (type: 'table' | 'main', value?: any) => {
}
const renameTableDlg = ref(false)
const renameTableMeta = ref()
const showRenameTableDlg = (table: TableType, rightClick = false) => {
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
renameTableMeta.value = table
renameTableDlg.value = true
}
const reloadTables = async () => {
$e('a:table:refresh:navdraw')
await loadTables()
}
const addTableTab = (table: TableType) => {
$e('a:table:open')
addTab({ title: table.title, id: table.id, type: table.type as any })
}
const activeTable = computed(() => {
return [TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.title : null
})
</script>
<template>
@ -186,11 +180,10 @@ const addTableTab = (table: TableType) => {
:key="table.id"
v-t="['a:table:open']"
:class="[
{ hidden: !filteredTables?.includes(table) },
{ hidden: !filteredTables?.includes(table), active: activeTable === table.title },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,
route.params.title && route.params.title.includes(table.title) ? 'bg-blue-500/15' : '',
]"
class="pl-5 pr-3 py-2 text-sm cursor-pointer group"
class="nc-tree-item pl-5 pr-3 py-2 text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
@click="addTableTab(table)"
@ -254,7 +247,7 @@ const addTableTab = (table: TableType) => {
</div>
</template>
<style lang="scss" scoped>
<style scoped>
.nc-treeview-container {
@apply h-[calc(100vh_-_var(--header-height))];
}
@ -299,4 +292,23 @@ const addTableTab = (table: TableType) => {
@apply !bg-primary/25 text-primary;
}
}
.nc-tree-item {
@apply relative cursor-pointer after:(pointer-events-none content-[''] absolute top-0 left-0 w-full h-full right-0 !bg-current transition transition-opactity duration-100 opacity-0);
}
.nc-tree-item svg {
@apply text-gray-500;
}
.nc-tree-item.active {
@apply !text-primary after:(!opacity-5);
svg {
@apply !text-primary;
}
}
.nc-tree-item:hover {
@apply !text-grey after:(!opacity-2);
}
</style>

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

@ -32,7 +32,7 @@ async function loadTableList() {
isLoading = true
// TODO includeM2M
tables = await $api.project.modelVisibilityList(project.value?.id, {
includeM2M: true,
includeM2M: false,
})
} catch (e) {
console.error(e)

6
packages/nc-gui-v2/components/dlg/AirtableImport.vue

@ -25,9 +25,7 @@ const { $state } = useNuxtApp()
const toast = useToast()
const { sqlUi, project, loadTables } = useProject()
const loading = ref(false)
const { project, loadTables } = useProject()
const showGoToDashboardButton = ref(false)
@ -78,7 +76,7 @@ const dialogShow = computed({
const useForm = Form.useForm
const { resetFields, validate, validateInfos } = useForm(syncSource, validators)
const { validateInfos } = useForm(syncSource, validators)
const disableImportButton = computed(() => {
return !syncSource.value.details.apiKey || !syncSource.value.details.syncSourceUrlOrId

18
packages/nc-gui-v2/components/dlg/TableCreate.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { Form } from 'ant-design-vue'
import { useToast } from 'vue-toastification'
import { onMounted, useProject, useTable, useTabs } from '#imports'
import { validateTableName } from '~/utils/validation'
import { TabType } from '~/composables'
@ -15,12 +14,6 @@ const emit = defineEmits(['update:modelValue'])
const dialogShow = useVModel(props, 'modelValue', emit)
const toast = useToast()
const valid = ref(false)
const isIdToggleAllowed = ref(false)
const isAdvanceOptVisible = ref(false)
const { addTab } = useTabs()
@ -38,18 +31,9 @@ const { table, createTable, generateUniqueTitle, tables, project } = useTable(as
dialogShow.value = false
})
const prefix = computed(() => project?.value?.prefix || '')
const validateDuplicateAlias = (v: string) => {
return (tables?.value || []).every((t) => t.title !== (v || '')) || 'Duplicate table alias'
}
const validateLeadingOrTrailingWhiteSpace = (v: string) => {
return !/^\s+|\s+$/.test(v) || 'Leading or trailing whitespace not allowed in table name'
}
const validateDuplicate = (v: string) => {
return (tables?.value || []).every((t) => t.table_name.toLowerCase() !== (v || '').toLowerCase()) || 'Duplicate table name'
}
const inputEl = ref<HTMLInputElement>()
const useForm = Form.useForm
@ -60,7 +44,7 @@ const validators = computed(() => {
table_name: [validateTableName],
}
})
const { resetFields, validate, validateInfos } = useForm(table, validators)
const { validateInfos } = useForm(table, validators)
onMounted(() => {
generateUniqueTitle()

6
packages/nc-gui-v2/components/dlg/TableRename.vue

@ -28,9 +28,7 @@ const dialogShow = computed({
const { updateTab } = useTabs()
const { loadTables } = useProject()
const { project, tables } = useProject()
const prefix = computed(() => project?.value?.prefix || '')
const { tables } = useProject()
const inputEl = $ref<any>()
let loading = $ref(false)
@ -60,7 +58,7 @@ const validators = computed(() => {
],
}
})
const { resetFields, validate, validateInfos } = useForm(formState, validators)
const { validateInfos } = useForm(formState, validators)
watchEffect(() => {
if (tableMeta?.title) formState.title = tableMeta?.title

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

@ -0,0 +1,110 @@
<script lang="ts" setup>
import { Chrome } from '@ckpack/vue-color'
import { enumColor } from '@/utils'
interface Props {
modelValue: string | any
colors?: string[]
rowSize?: number
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,
})
const emit = defineEmits(['update:modelValue'])
const vModel = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val.hex ? val.hex : val || null)
},
})
const picked = ref(props.modelValue || enumColor.light[0])
const selectColor = (color: any) => {
picked.value = color.hex ? color.hex : color
vModel.value = 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
}
watch(picked, (n, _o) => {
if (!props.pickButton) {
vModel.value = n.hex ? n.hex : n
}
})
</script>
<template>
<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)"
:key="`color-${colId}-${i}`"
class="color-selector"
:class="compare(picked, color) ? 'selected' : ''"
:style="{ 'background-color': `${color}` }"
@click="selectColor(color)"
>
{{ compare(picked, color) ? '&#10003;' : '' }}
</button>
</div>
<a-card v-if="props.advanced" class="w-full mt-2" :body-style="{ padding: '0px' }" :bordered="false">
<a-collapse accordion ghost expand-icon-position="right">
<a-collapse-panel key="1" header="Advanced" class="">
<a-button v-if="props.pickButton" class="!bg-primary text-white w-full" @click="selectColor(picked)">
Pick Color
</a-button>
<div class="flex justify-center py-4">
<Chrome v-model="picked" class="!w-full !shadow-none" />
</div>
</a-collapse-panel>
</a-collapse>
</a-card>
</div>
</template>
<style scoped>
.color-picker {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: white;
padding: 10px;
}
.color-picker-row {
display: flex;
flex-direction: row;
}
.color-selector {
position: relative;
height: 32px;
width: 32px;
margin: 10px 5px;
border-radius: 5px;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: white;
}
.color-selector:hover {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
.color-selector.selected {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
</style>

10
packages/nc-gui-v2/components/general/MiniSidebar.vue

@ -1,12 +1,8 @@
<script lang="ts" setup>
import { breakpointsTailwind } from '@vueuse/core'
import { navigateTo } from '#app'
import { computed, useBreakpoints, useGlobal, useProject, useRoute, useSidebar } from '#imports'
import { computed, useGlobal, useProject, useRoute, useSidebar } from '#imports'
/** get current breakpoints (for enabling sidebar) */
const breakpoints = useBreakpoints(breakpointsTailwind)
const { signOut, signedIn, isLoading, user } = useGlobal()
const { signOut, signedIn, user } = useGlobal()
const { isOpen } = useSidebar({ isOpen: true })
@ -33,7 +29,7 @@ const logout = () => {
theme="light"
>
<a-dropdown placement="bottom" :trigger="['click']">
<div class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105">
<div class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon">
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>

4
packages/nc-gui-v2/components/monaco/Editor.vue

@ -12,9 +12,10 @@ interface Props {
lang?: string
validate?: boolean
disableDeepCompare?: boolean
readOnly?: boolean
}
const { hideMinimap, lang = 'json', validate = true, disableDeepCompare = false, modelValue } = defineProps<Props>()
const { hideMinimap, lang = 'json', validate = true, disableDeepCompare = false, modelValue, readOnly } = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
@ -92,6 +93,7 @@ onMounted(() => {
},
tabSize: 2,
automaticLayout: true,
readOnly,
minimap: {
enabled: !hideMinimap,
},

29
packages/nc-gui-v2/components/smartsheet-column/CheckboxOptions.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import { Sketch } from '@ckpack/vue-color'
import { useColumnCreateStoreOrThrow } from '#imports'
import { enumColor, getMdiIcon } from '@/utils'
import { getMdiIcon } from '@/utils'
const { formState } = useColumnCreateStoreOrThrow()
@ -37,9 +36,12 @@ const iconList = [
},
]
const advanced = ref(true)
const picked = ref(formState.value.meta.color || enumColor.light[0])
const picked = computed({
get: () => formState.value.meta.color,
set: (val) => {
formState.value.meta.color = val
},
})
// set default value
formState.value.meta = {
@ -91,17 +93,12 @@ watch(
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-card class="w-full shadow-lg mt-2" body-style="padding: 0px">
<a-collapse v-model:activeKey="advanced" accordion ghost expand-icon-position="right">
<a-collapse-panel key="1" header="Advanced" class="">
<a-button class="!bg-primary text-white w-full" @click="formState.meta.color = picked.hex"> Pick Color </a-button>
<div class="px-7 py-3">
<Sketch v-model="picked" />
</div>
</a-collapse-panel>
</a-collapse>
</a-card>
<a-row class="w-full justify-center">
<GeneralColorPicker
v-model="picked"
:row-size="8"
:colors="['#fcb401', '#faa307', '#f48c06', '#e85d04', '#dc2f02', '#d00000', '#9d0208', '#777']"
/>
</a-row>
</template>

2
packages/nc-gui-v2/components/smartsheet-column/CurrencyOptions.vue

@ -7,7 +7,7 @@ interface Option {
value: string
}
const { formState, validateInfos, setAdditionalValidations, sqlUi, onDataTypeChange, onAlter } = useColumnCreateStoreOrThrow()
const { formState, validateInfos, setAdditionalValidations } = useColumnCreateStoreOrThrow()
const validators = {
'meta.currency_locale': [

2
packages/nc-gui-v2/components/smartsheet-column/DurationOptions.vue

@ -2,7 +2,7 @@
import { useColumnCreateStoreOrThrow } from '#imports'
import { durationOptions } from '@/utils'
const { formState, validateInfos, setAdditionalValidations, sqlUi, onDataTypeChange, onAlter } = useColumnCreateStoreOrThrow()
const { formState } = useColumnCreateStoreOrThrow()
const durationOptionList =
durationOptions.map((o) => ({

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

@ -8,7 +8,7 @@ import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier'
interface Props {
editColumnDropdown: boolean
editColumnDropdown?: boolean
}
const { editColumnDropdown } = defineProps<Props>()
@ -21,20 +21,13 @@ const { getMeta } = useMetas()
const formulaOptionsRef = ref()
const {
formState,
resetFields,
validate,
validateInfos,
onUidtOrIdTypeChange,
onAlter,
addOrUpdate,
generateNewColumnMeta,
isEdit,
} = useColumnCreateStoreOrThrow()
const { formState, validateInfos, onUidtOrIdTypeChange, onAlter, addOrUpdate, generateNewColumnMeta, isEdit } =
useColumnCreateStoreOrThrow()
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const onlyNameUpdateOnEditColumns = [UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup]
const uiTypesOptions = computed<typeof uiTypes>(() => {
return [
...uiTypes.filter((t) => !isEdit.value || !t.virtual),
@ -64,6 +57,11 @@ function onCancel() {
}
}
async function onSubmit() {
await addOrUpdate(reloadMetaAndData)
advancedOptions.value = false
}
// create column meta if it's a new column
watchEffect(() => {
if (!isEdit.value) {
@ -81,6 +79,7 @@ watchEffect(() => {
antInput.value.select()
}, 300)
}
advancedOptions.value = false
})
watch(
@ -93,10 +92,15 @@ watch(
}
},
)
// for cases like formula
if (!formState.value?.column_name) {
formState.value.column_name = formState.value?.title
}
</script>
<template>
<div class="max-w-[450px] min-w-[350px] w-max max-h-[95vh] bg-white shadow p-4 overflow-auto" @click.stop>
<div class="min-w-[350px] w-max max-h-[95vh] bg-white shadow p-4 overflow-auto" @click.stop>
<a-form v-model="formState" name="column-create-or-edit" layout="vertical">
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.column_name">
<a-input
@ -107,7 +111,10 @@ watch(
@input="onAlter(8)"
/>
</a-form-item>
<a-form-item :label="$t('labels.columnType')">
<a-form-item
v-if="!(editColumnDropdown && !!onlyNameUpdateOnEditColumns.find((col) => col === formState.uidt))"
:label="$t('labels.columnType')"
>
<a-select
v-model:value="formState.uidt"
show-search
@ -129,12 +136,15 @@ watch(
<SmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" />
<SmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" />
<SmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" />
<SmartsheetColumnLookupOptions v-if="formState.uidt === UITypes.Lookup" />
<SmartsheetColumnLookupOptions v-if="!editColumnDropdown && formState.uidt === UITypes.Lookup" />
<SmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" />
<SmartsheetColumnRollupOptions v-if="formState.uidt === UITypes.Rollup" />
<SmartsheetColumnLinkedToAnotherRecordOptions v-if="formState.uidt === UITypes.LinkToAnotherRecord" />
<SmartsheetColumnRollupOptions v-if="!editColumnDropdown && formState.uidt === UITypes.Rollup" />
<SmartsheetColumnLinkedToAnotherRecordOptions
v-if="!editColumnDropdown && formState.uidt === UITypes.LinkToAnotherRecord"
/>
<SmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<SmartsheetColumnPercentOptions v-if="formState.uidt === UITypes.Percent" />
<SmartsheetColumnSelectOptions v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect" />
<div
v-if="!isVirtualCol(formState.uidt)"
@ -163,17 +173,7 @@ watch(
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button
html-type="submit"
type="primary"
size="small"
@click="
() => {
addOrUpdate(reloadMetaAndData)
advancedOptions = false
}
"
>
<a-button html-type="submit" type="primary" size="small" @click="onSubmit">
<!-- Save -->
{{ $t('general.save') }}
</a-button>

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

@ -96,6 +96,7 @@ const suggestionsList = computed(() => {
text: c.title,
type: 'column',
icon: getUIDTIcon(c.uidt),
uidt: c.uidt,
})),
...availableBinOps.map((op: string) => ({
text: op,
@ -254,7 +255,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
if (targetFormulaCol && column?.value.id) {
formulaPaths.push({
[column.value.id]: [targetFormulaCol.id],
[column?.value?.id as string]: [targetFormulaCol.id],
})
}
const vertices = formulaPaths.length
@ -518,6 +519,7 @@ function appendText(item: Record<string, any>) {
formState.value.formula_raw = insertAtCursor(formulaRef.value.$el, text, len)
}
autocomplete.value = false
wordToComplete.value = ''
if (item.type === 'function' || item.type === 'op') {
// if function / operator is chosen, display columns only
suggestion.value = suggestionsList.value.filter((f) => f.type === 'column')
@ -612,7 +614,7 @@ onMounted(() => {
<template>
<div class="formula-wrapper">
<a-form-item v-bind="validateInfos.formula_raw" label="Formula">
<a-input
<a-textarea
ref="formulaRef"
v-model:value="formState.formula_raw"
class="mb-2"
@ -622,21 +624,13 @@ onMounted(() => {
@change="handleInputDeb"
/>
</a-form-item>
<div class="text-gray-600 mt-2 prose-sm">
<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
>.
</div>
<a-drawer
v-model:visible="formulaSuggestionDrawer"
:closable="false"
:mask="false"
:mask-closable="false"
placement="right"
width="500px"
class="h-full overflow-auto"
>
<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 }">
<a-list-item
@ -649,39 +643,37 @@ onMounted(() => {
@click.prevent.stop="appendText(item)"
>
<a-list-item-meta>
<template v-if="item.type === 'function'" #description>
{{ item.description }} <br /><br />
Syntax: <br />
{{ item.syntax }} <br /><br />
Examples: <br />
<div v-for="(example, idx) of item.examples" :key="idx">
<div>({{ idx + 1 }}): {{ example }}</div>
</div>
</template>
<template #title>
<div class="flex">
<div class="flex-1">
{{ item.text }}
</div>
<div class="">
{{ getFormulaTypeName(item.type) }}
</div>
<a-col :span="6">
<span class="prose-sm text-gray-600">{{ item.text }}</span>
</a-col>
<a-col :span="18">
<div v-if="item.type === 'function'" class="text-xs text-gray-500">
{{ item.description }} <br /><br />
Syntax: <br />
{{ item.syntax }} <br /><br />
Examples: <br />
<div v-for="(example, idx) of item.examples" :key="idx">
<div>({{ idx + 1 }}): {{ example }}</div>
</div>
</div>
<div v-if="item.type === 'column'" class="float-right mr-5 -mt-2">
<a-badge-ribbon :text="item.uidt" color="gray" />
</div>
</a-col>
</div>
</template>
<template #avatar>
<MdiFunction v-if="item.type === 'function'" class="text-lg" />
<MdiCalculator v-if="item.type === 'op'" class="text-lg" />
<mdi-function v-if="item.type === 'function'" class="text-lg" />
<mdi-calculator v-if="item.type === 'op'" class="text-lg" />
<component :is="item.icon" v-if="item.type === 'column'" class="text-lg" />
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-drawer>
</div>
</div>
</template>

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

@ -1,4 +1,5 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { useColumnCreateStoreOrThrow } from '#imports'
import { MetaInj } from '~/context'
@ -27,17 +28,17 @@ const refTables = $computed(() => {
return []
}
// todo: type issues with ColumnType so we have to cast to any
return (
meta.columns
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions?.type !== 'bt' && !c.system)
.map((c) => ({
col: c.colOptions,
column: c,
...tables.find((t) => t.id === (c.colOptions as any)?.fk_related_model_id),
}))
.filter((table: any) => table.col?.fk_related_model_id === table.id && !table.mm) ?? []
)
return meta.columns
.filter((c: ColumnType) => c.uidt === UITypes.LinkToAnotherRecord && !c.system)
.map<TableType & { col: LinkToAnotherRecordType; column: ColumnType }>((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,
)
})
const columns = $computed(() => {
@ -46,7 +47,7 @@ const columns = $computed(() => {
return []
}
return metas[selectedTable.id].columns.filter((c: any) => !isSystemColumn(c))
return metas[selectedTable.id].columns.filter((c) => !isSystemColumn(c))
})
</script>
@ -54,17 +55,22 @@ const columns = $computed(() => {
<div class="p-4 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.childTable')" v-bind="validateInfos.fk_relation_column_id">
<a-select v-model:value="formState.fk_relation_column_id" size="small" @change="onDataTypeChange">
<a-select-option v-for="(table, index) of refTables" :key="index" :value="table.col.fk_column_id">
<div class="flex flex-row items-center space-x-0.5 h-full">
<div class="font-weight-bold text-[0.7rem]">{{ table.column.title }}</div>
<div class="text-[0.5rem]">({{ relationNames[table.col.type] }} {{ table.title || table.table_name }})</div>
<a-select
v-model:value="formState.fk_relation_column_id"
size="small"
dropdown-class-name="!w-64"
@change="onDataTypeChange"
>
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
<div class="text-[0.65rem] text-gray-600">
{{ relationNames[table.col.type] }} {{ table.title || table.table_name }}
</div>
</div>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_lookup_column_id">
<a-select
v-model:value="formState.fk_lookup_column_id"

31
packages/nc-gui-v2/components/smartsheet-column/RatingOptions.vue

@ -1,9 +1,8 @@
<script setup lang="ts">
import { Sketch } from '@ckpack/vue-color'
import { useColumnCreateStoreOrThrow } from '#imports'
import { enumColor, getMdiIcon } from '@/utils'
import { getMdiIcon } from '@/utils'
const { formState, validateInfos, setAdditionalValidations, sqlUi, onDataTypeChange, onAlter } = useColumnCreateStoreOrThrow()
const { formState } = useColumnCreateStoreOrThrow()
// cater existing v1 cases
const iconList = [
@ -29,9 +28,12 @@ const iconList = [
},
]
const advanced = ref(true)
const picked = ref(formState.value.meta.color || enumColor.light[0])
const picked = computed({
get: () => formState.value.meta.color,
set: (val) => {
formState.value.meta.color = val
},
})
// set default value
formState.value.meta = {
@ -94,17 +96,12 @@ watch(
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-card class="w-full shadow-lg mt-2" body-style="padding: 0px">
<a-collapse v-model:activeKey="advanced" accordion ghost expand-icon-position="right">
<a-collapse-panel key="1" header="Advanced" class="">
<a-button class="!bg-primary text-white w-full" @click="formState.meta.color = picked.hex"> Pick Color </a-button>
<div class="px-7 py-3">
<Sketch v-model="picked" />
</div>
</a-collapse-panel>
</a-collapse>
</a-card>
<a-row class="w-full justify-center">
<GeneralColorPicker
v-model="picked"
:row-size="8"
:colors="['#fcb401', '#faa307', '#f48c06', '#e85d04', '#dc2f02', '#d00000', '#9d0208', '#777']"
/>
</a-row>
</template>

18
packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue

@ -4,7 +4,6 @@ import { inject, useColumnCreateStoreOrThrow, useMetas, useProject } from '#impo
import { MetaInj } from '~/context'
const { formState, validateInfos, onDataTypeChange, setAdditionalValidations } = $(useColumnCreateStoreOrThrow())
const { tables } = $(useProject())
const meta = $(inject(MetaInj)!)
@ -69,11 +68,18 @@ const columns = $computed(() => {
<div class="p-4 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.childTable')" v-bind="validateInfos.fk_relation_column_id">
<a-select v-model:value="formState.fk_relation_column_id" size="small" @change="onDataTypeChange">
<a-select-option v-for="(table, index) of refTables" :key="index" :value="table.col.fk_column_id">
<div class="flex flex-row items-center space-x-0.5">
<div class="font-weight-bold text-xs">{{ table.column.title }}</div>
<div class="text-[0.45rem]">({{ relationNames[table.col.type] }} {{ table.title || table.table_name }})</div>
<a-select
v-model:value="formState.fk_relation_column_id"
size="small"
dropdown-class-name="!w-64"
@change="onDataTypeChange"
>
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
<div class="text-[0.65rem] text-gray-600">
({{ relationNames[table.col.type] }} {{ table.title || table.table_name }})
</div>
</div>
</a-select-option>
</a-select>

112
packages/nc-gui-v2/components/smartsheet-column/SelectOptions.vue

@ -0,0 +1,112 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import { UITypes } from 'nocodb-sdk'
import { useColumnCreateStoreOrThrow } from '#imports'
import { enumColor } from '@/utils'
import MdiDragIcon from '~icons/mdi/drag-vertical'
import MdiArrowDownDropCircle from '~icons/mdi/arrow-down-drop-circle'
import MdiClose from '~icons/mdi/close'
import MdiPlusIcon from '~icons/mdi/plus'
const { formState, setAdditionalValidations } = useColumnCreateStoreOrThrow()
let options = $ref<any[]>([])
const colorMenus = $ref<any>({})
const colors = $ref(enumColor.light)
const inputs = ref()
const validators = {
'colOptions.options': [
{
validator: (_: any, _opt: any) => {
return new Promise<void>((resolve, reject) => {
for (const opt of options) {
if (!opt.title.length) {
return reject(new Error("Select options can't be null"))
}
if (formState.value.uidt === UITypes.MultiSelect && opt.title.includes(',')) {
return reject(new Error("MultiSelect columns can't have commas(',')"))
}
if (options.filter((el) => el.title === opt.title).length !== 1) {
return reject(new Error("Select options can't have duplicates"))
}
}
resolve()
})
},
},
],
}
setAdditionalValidations({
...validators,
})
const getNextColor = () => {
let tempColor = colors[0]
if (options.length && options[options.length - 1].color) {
const lastColor = colors.indexOf(options[options.length - 1].color)
tempColor = colors[(lastColor + 1) % colors.length]
}
return tempColor
}
const addNewOption = () => {
const tempOption = {
title: '',
color: getNextColor(),
}
options.push(tempOption)
}
const removeOption = (index: number) => {
options.splice(index, 1)
}
onMounted(() => {
if (!formState.value.colOptions?.options) {
formState.value.colOptions = {
options: [],
}
}
options = formState.value.colOptions.options
// Support for older options
for (const op of options.filter((el) => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')
}
})
// focus last created input
watch(inputs, () => {
if (inputs.value?.$el) {
inputs.value.$el.focus()
}
})
</script>
<template>
<div class="w-full">
<Draggable :list="options" item-key="id" handle=".nc-child-draggable-icon">
<template #item="{ element, index }">
<div class="flex py-1 align-center">
<MdiDragIcon small class="nc-child-draggable-icon handle" />
<a-dropdown v-model:visible="colorMenus[index]" :trigger="['click']">
<template #overlay>
<GeneralColorPicker v-model="element.color" :pick-button="true" @update:model-value="colorMenus[index] = false" />
</template>
<MdiArrowDownDropCircle :style="{ 'font-size': '1.5em', 'color': element.color }" class="mr-2" />
</a-dropdown>
<a-input ref="inputs" v-model:value="element.title" class="caption" />
<MdiClose class="ml-2" :style="{ color: 'red' }" @click="removeOption(index)" />
</div>
</template>
<template #footer>
<a-button type="dashed" class="w-full caption mt-2" @click="addNewOption()">
<div class="flex align-center"><MdiPlusIcon /><span class="flex-auto">Add option</span></div>
</a-button>
</template>
</Draggable>
</div>
</template>
<style scoped lang="scss"></style>

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

@ -2,7 +2,7 @@
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { inject, toRef } from 'vue'
import { ColumnInj, MetaInj } from '~/context'
import { ColumnInj, IsFormInj, MetaInj } from '~/context'
import { useProvideColumnCreateStore } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean }>()
@ -11,6 +11,7 @@ const hideMenu = toRef(props, 'hideMenu')
provide(ColumnInj, column)
const meta = inject(MetaInj)
const isForm = inject(IsFormInj, false)
// instantiate column update store
useProvideColumnCreateStore(meta as Ref<TableType>, column)
@ -23,7 +24,7 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<template v-if="!hideMenu">
<div class="flex-1" />
<SmartsheetHeaderMenu />
<SmartsheetHeaderMenu v-if="!isForm" />
</template>
</div>
</template>

55
packages/nc-gui-v2/components/smartsheet-header/Menu.vue

@ -61,32 +61,49 @@ function onVisibleChange() {
<a-dropdown v-model:visible="editColumnDropdown" :trigger="['click']" @visible-change="onVisibleChange">
<span />
<template #overlay>
<SmartsheetColumnEditOrAdd :edit-column-dropdown="editColumnDropdown" @click.stop @cancel="editColumnDropdown = false" />
<SmartsheetColumnEditOrAdd
:edit-column-dropdown="editColumnDropdown"
@click.stop
@keydown.stop
@cancel="editColumnDropdown = false"
/>
</template>
</a-dropdown>
<a-dropdown :trigger="['hover']">
<MdiMenuDownIcon class="text-grey nc-ui-dt-dropdown" />
<template #overlay>
<div class="shadow bg-white">
<div class="nc-column-edit nc-menu-item" @click="editColumnDropdown = true">
<MdiEditIcon class="text-primary" />
<!-- Edit -->
{{ $t('general.edit') }}
</div>
<div v-if="!virtual" v-t="['a:column:set-primary']" class="nc-menu-item" @click="setAsPrimaryValue">
<MdiStarIcon class="text-primary" />
<a-menu class="shadow bg-white">
<a-menu-item @click="editColumnDropdown = true">
<div class="nc-column-edit nc-header-menu-item">
<MdiEditIcon class="text-primary" />
<!-- Edit -->
{{ $t('general.edit') }}
</div>
</a-menu-item>
<a-menu-item v-if="!virtual" v-t="['a:column:set-primary']" @click="setAsPrimaryValue">
<div class="nc-column-edit nc-header-menu-item">
<MdiStarIcon class="text-primary" />
<!-- todo : tooltip -->
<!-- Set as Primary value -->
{{ $t('activity.setPrimary') }}
<!-- todo : tooltip -->
<!-- Set as Primary value -->
{{ $t('activity.setPrimary') }}
</div>
<!-- <span class="caption font-weight-bold">Primary value will be shown in place of primary key</span> -->
</div>
<div class="nc-column-delete nc-menu-item" @click="deleteColumn">
<MdiDeleteIcon class="text-error" />
<!-- Delete -->
{{ $t('general.delete') }}
</div>
</div>
</a-menu-item>
<a-menu-item @click="deleteColumn">
<div class="nc-column-delete nc-header-menu-item">
<MdiDeleteIcon class="text-error" />
<!-- Delete -->
{{ $t('general.delete') }}
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<style scoped>
.nc-header-menu-item {
@apply text-xs flex items-center px-1 py-2 gap-1;
}
</style>

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

@ -1,12 +1,9 @@
<script setup lang="ts">
import { substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType, TableType } from 'nocodb-sdk'
import { toRef } from 'vue'
import { $computed } from 'vue/macros'
import type { Ref } from 'vue'
import { useMetas } from '~/composables'
import { ColumnInj, MetaInj } from '~/context'
import { provide, useProvideColumnCreateStore } from '#imports'
import { ColumnInj, IsFormInj, MetaInj } from '~/context'
import { provide, toRef, useMetas, useProvideColumnCreateStore } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean }>()
const column = toRef(props, 'column')
@ -15,8 +12,9 @@ const hideMenu = toRef(props, 'hideMenu')
provide(ColumnInj, column)
const { metas } = useMetas()
const meta = inject(MetaInj)
const isForm = inject(IsFormInj, false)
const { isLookup, isBt, isRollup, isMm, isHm, isFormula, isCount } = useVirtualCell(column)
const { isLookup, isBt, isRollup, isMm, isHm, isFormula } = useVirtualCell(column)
const colOptions = $computed(() => column.value?.colOptions)
const tableTile = $computed(() => meta?.value?.title)
@ -100,7 +98,7 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<template v-if="!hideMenu">
<v-spacer />
<SmartsheetHeaderMenu :virtual="true" />
<SmartsheetHeaderMenu v-if="!isForm" :virtual="true" />
</template>
</div>
</template>

3
packages/nc-gui-v2/components/smartsheet-header/VirtualCellIcon.vue

@ -11,6 +11,7 @@ import FormulaIcon from '~icons/mdi/math-integral'
import RollupIcon from '~icons/mdi/movie-roll'
import CountIcon from '~icons/mdi/counter'
import SpecificDBTypeIcon from '~icons/mdi/database-settings'
import TableColumnPlusBefore from '~icons/mdi/table-column-plus-before'
const props = defineProps<{ columnMeta?: ColumnType }>()
const columnMeta = toRef(props, 'columnMeta')
@ -34,7 +35,7 @@ const icon = computed(() => {
case UITypes.Formula:
return FormulaIcon
case UITypes.Lookup:
return GenericIcon
return TableColumnPlusBefore
case UITypes.Rollup:
return RollupIcon
case UITypes.Count:

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

@ -5,7 +5,7 @@ import FieldListAutoCompleteDropdown from './FieldListAutoCompleteDropdown.vue'
import { useNuxtApp } from '#app'
import { inject, useViewFilters } from '#imports'
import { comparisonOpList } from '~/utils/filterUtils'
import { ActiveViewInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import { ActiveViewInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import MdiDeleteIcon from '~icons/mdi/close-box'
import MdiAddIcon from '~icons/mdi/plus'
@ -16,7 +16,6 @@ const emit = defineEmits(['update:filtersLength'])
const meta = inject(MetaInj)
const activeView = inject(ActiveViewInj)
const reloadDataHook = inject(ReloadViewDataHookInj)
const isLocked = inject(IsLockedInj)
// todo: replace with inject or get from state
const shared = ref(false)

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

@ -1,12 +1,10 @@
<script setup lang="ts">
import { watchEffect } from '@vue/runtime-core'
import type ColumnFilter from './ColumnFilter.vue'
import { useState } from '#app'
import { ActiveViewInj, IsLockedInj } from '~/context'
import MdiFilterIcon from '~icons/mdi/filter-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
const autoApplyFilter = useState('autoApplyFilter', () => false)
const isLocked = inject(IsLockedInj)
const activeView = inject(ActiveViewInj)

37
packages/nc-gui-v2/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { SelectProps } from 'ant-design-vue'
import { isVirtualCol } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { computed } from 'vue'
import { MetaInj } from '~/context'
import VirtualCellIcon from '~/components/smartsheet-header/VirtualCellIcon.vue'
@ -8,9 +9,10 @@ import CellIcon from '~/components/smartsheet-header/CellIcon.vue'
interface Props {
modelValue?: string
isSort?: boolean
}
const { modelValue } = defineProps<Props>()
const { modelValue, isSort } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
@ -48,14 +50,26 @@ const localValue = computed({
},
} */
const options = computed<SelectProps['options']>(() =>
meta?.value?.columns?.map((c) => ({
value: c.id,
label: c.title,
icon: h(isVirtualCol(c) ? VirtualCellIcon : CellIcon, {
columnMeta: c,
}),
c,
})),
meta?.value?.columns
?.filter((c: ColumnType) => {
/** ignore hasmany and manytomany relations if it's using within sort menu */
if (isSort) {
return !(
c.uidt === UITypes.LinkToAnotherRecord && (c.colOptions as LinkToAnotherRecordType).type !== RelationTypes.BELONGS_TO
)
/** ignore vutual fields which are system fields ( mm relation ) */
} else {
return !c.colOptions || !c.system
}
})
.map((c: ColumnType) => ({
value: c.id,
label: c.title,
icon: h(isVirtualCol(c) ? VirtualCellIcon : CellIcon, {
columnMeta: c,
}),
c,
})),
)
const filterOption = (input: string, option: any) => {
@ -75,7 +89,8 @@ const filterOption = (input: string, option: any) => {
>
<a-select-option v-for="option in options" :key="option.value" :value="option.value">
<div class="flex gap-2 text-xs items-center align-center h-full">
<component :is="option.icon" class="min-w-5 !mx-0" /> <span class="min-w-0"> {{ option.label }}</span>
<component :is="option.icon" class="min-w-5 !mx-0" />
<span class="min-w-0"> {{ option.label }}</span>
</div>
</a-select-option>
</a-select>

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

@ -18,11 +18,6 @@ const sharedViewListDlg = ref(false)
// todo : replace with inject
const publicViewId = null
// TODO:: identify based on meta
const isView = ref(false)
const { isUIAllowed } = useUIPermission()
const { project } = useProject()
const { $api } = useNuxtApp()

1
packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue

@ -10,7 +10,6 @@ import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiOpenInNewIcon from '~icons/mdi/open-in-new'
import MdiCopyIcon from '~icons/mdi/content-copy'
const { isUIAllowed } = useUIPermission()
const { view, $api } = useSmartsheetStoreOrThrow()
const { copy } = useClipboard()

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

@ -3,7 +3,6 @@ import { useClipboard } from '@vueuse/core'
import { ViewTypes } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { message } from 'ant-design-vue'
import { useRoute } from '#app'
import { onMounted, useSmartsheetStoreOrThrow } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiVisibilityOnIcon from '~icons/mdi/visibility'
@ -20,18 +19,14 @@ interface SharedViewType {
showPassword?: boolean
}
const { view, $api, meta } = useSmartsheetStoreOrThrow()
const { $api, meta } = useSmartsheetStoreOrThrow()
const { copy } = useClipboard()
const toast = useToast()
const route = useRoute()
const { dashboardUrl } = useDashboard()
let isLoading = $ref(false)
// let activeSharedView = $ref(null)
const sharedViewList = ref<SharedViewType[]>()
const loadSharedViewsList = async () => {
isLoading = true
const list = await $api.dbViewShare.list(meta.value?.id as string)
console.log(unref(sharedViewList))
@ -49,8 +44,6 @@ const loadSharedViewsList = async () => {
// } else {
// activeSharedView = null
// }
isLoading = false
}
onMounted(loadSharedViewsList)

1
packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue

@ -59,6 +59,7 @@ watch(
v-model="sort.fk_column_id"
class="caption nc-sort-field-select"
:columns="columns"
is-sort
@click.stop
@update:model-value="saveOrUpdate(sort, i)"
/>

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

@ -3,28 +3,33 @@ import { UITypes } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import { provide, toRef } from 'vue'
import { computed, useColumn, useDebounceFn, useVModel } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
import { ActiveCellInj, ColumnInj, EditModeInj } from '~/context'
import { NavigateDir } from '~/lib'
interface Props {
column: ColumnType
modelValue: any
editEnabled: boolean
}
interface Emits {
(event: 'update:modelValue', value: any): void
rowIndex: number
active?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'save', 'navigate', 'update:editEnabled'])
const column = toRef(props, 'column')
const active = toRef(props, 'active', false)
provide(ColumnInj, column)
provide(EditModeInj, useVModel(props, 'editEnabled', emit))
provide(ActiveCellInj, active)
let changed = $ref(false)
const syncValue = useDebounceFn(function () {
changed = false
emit('save')
@ -114,7 +119,7 @@ const syncAndNavigate = (dir: NavigateDir) => {
>
<CellTextArea v-if="isTextArea" v-model="vModel" />
<CellCheckbox v-else-if="isBoolean" v-model="vModel" />
<CellAttachment v-else-if="isAttachment" v-model="vModel" />
<CellAttachment v-else-if="isAttachment" v-model="vModel" :row-index="props.rowIndex" />
<CellSingleSelect v-else-if="isSingleSelect" v-model="vModel" />
<CellMultiSelect v-else-if="isMultiSelect" v-model="vModel" />
<CellDatePicker v-else-if="isDate" v-model="vModel" />
@ -136,35 +141,3 @@ const syncAndNavigate = (dir: NavigateDir) => {
<CellText v-else v-model="vModel" />
</div>
</template>
<style scoped>
textarea {
outline: none;
}
div {
width: 100%;
height: 100%;
color: var(--v-textColor-base);
}
.nc-hint {
font-size: 0.61rem;
color: grey;
}
.nc-cell {
@apply relative h-full;
width: inherit;
display: inherit;
}
.nc-locked-overlay {
position: absolute;
z-index: 2;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
</style>

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

@ -11,12 +11,6 @@ interface Attachment {
const meta = inject(MetaInj)
const view = inject(ActiveViewInj)
// todo: get from parent ( inject or use prop )
const isPublicView = false
const selected = reactive<{ row?: number | null; col?: number | null }>({})
const editEnabled = ref(false)
const { loadData, paginationData, formattedData: data, loadGalleryData, galleryData, changePage } = useViewData(meta, view as any)
provide(IsFormInj, false)

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

@ -1,16 +1,20 @@
<script lang="ts" setup>
import { onClickOutside, useEventListener } from '@vueuse/core'
import type { ColumnType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import {
inject,
onClickOutside,
onMounted,
provide,
reactive,
ref,
useEventListener,
useGridViewColumnWidth,
useProvideColumnCreateStore,
useSmartsheetStoreOrThrow,
useViewData,
watch,
} from '#imports'
import type { Row } from '~/composables'
import {
@ -25,23 +29,39 @@ import {
ReloadViewDataHookInj,
} from '~/context'
import { NavigateDir } from '~/lib'
import { enumColor } from '~/utils'
const meta = inject(MetaInj)
const view = inject(ActiveViewInj)
// 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 isLocked = inject(IsLockedInj, false)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
// 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 })
let editEnabled = $ref(false)
const { xWhere, isPkAvail } = useSmartsheetStoreOrThrow()
const { xWhere, isPkAvail, cellRefs } = useSmartsheetStoreOrThrow()
const addColumnDropdown = ref(false)
const contextMenu = ref(false)
const contextMenuTarget = ref(false)
const expandedFormDlg = ref(false)
const expandedFormRow = ref<Row>()
const expandedFormRowState = ref<Record<string, any>>()
const visibleColLength = $computed(() => fields.value?.length)
@ -55,8 +75,11 @@ const {
deleteRow,
deleteSelectedRows,
selectedAllRecords,
loadAggCommentsCount,
} = useViewData(meta, view as any, xWhere)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view as any)
onMounted(loadGridViewColumns)
provide(IsFormInj, false)
@ -64,9 +87,9 @@ provide(IsGridInj, true)
provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
reloadViewDataHook?.on(() => {
loadData()
reloadViewDataHook?.on(async () => {
await loadData()
loadAggCommentsCount()
})
const selectCell = (row: number, col: number) => {
@ -87,6 +110,7 @@ watch(
const onresize = (colID: string, event: any) => {
updateWidth(colID, event.detail)
}
const onXcResizing = (cn: string, event: any) => {
resizingCol.value = cn
resizingColWidth.value = event.detail
@ -228,6 +252,12 @@ useEventListener(document, 'keydown', onKeyDown)
/** On clicking outside of table reset active cell */
const smartTable = ref(null)
onClickOutside(smartTable, () => {
if (selected.col === null) return
const activeCol = fields.value[selected.col]
if (editEnabled && (isVirtualCol(activeCol) || activeCol.uidt === UITypes.JSON)) return
selected.row = null
selected.col = null
})
@ -247,6 +277,12 @@ const onNavigate = (dir: NavigateDir) => {
break
}
}
const expandForm = (row: Row, state: Record<string, any>) => {
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
}
</script>
<template>
@ -255,15 +291,16 @@ const onNavigate = (dir: NavigateDir) => {
<a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']">
<table ref="smartTable" class="xc-row-table nc-grid backgroundColorDefault" @contextmenu.prevent="contextMenu = true">
<thead>
<tr class="group">
<tr>
<th>
<div class="flex align-center w-[80px]">
<div class="flex align-center w-[80px] px-1">
<div class="group-hover:hidden" :class="{ hidden: selectedAllRecords }">#</div>
<div
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
class="group-hover:flex w-full align-center"
>
<a-checkbox v-model:checked="selectedAllRecords" />
<span class="flex-1" />
</div>
</div>
@ -279,6 +316,7 @@ const onNavigate = (dir: NavigateDir) => {
@xcresized="resizingCol = null"
>
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />
</th>
<!-- v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" -->
@ -287,65 +325,85 @@ const onNavigate = (dir: NavigateDir) => {
<div class="h-full w-[60px] flex align-center justify-center">
<MdiPlus class="text-sm" />
</div>
<template #overlay>
<SmartsheetColumnEditOrAdd @click.stop @cancel="addColumnDropdown = false" />
<SmartsheetColumnEditOrAdd @click.stop @keydown.stop @cancel="addColumnDropdown = false" />
</template>
</a-dropdown>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) of data" :key="rowIndex" class="nc-grid-row">
<td key="row-index" class="caption nc-grid-cell group">
<div class="flex items-center w-[80px]">
<div class="group-hover:hidden" :class="{ hidden: row.rowMeta.selected }">{{ rowIndex + 1 }}</div>
<div
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="group-hover:flex w-full items-center justify-between p-1"
<SmartsheetRow v-for="(row, rowIndex) of data" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr class="nc-grid-row group">
<td key="row-index" class="caption nc-grid-cell">
<div class="align-center flex w-[80px]">
<div class="group-hover:hidden" :class="{ hidden: row.rowMeta.selected }">{{ rowIndex + 1 }}</div>
<div
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="group-hover:flex w-full items-center justify-between p-1"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
<span class="flex-1" />
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }"
@click="expandForm(row, state)"
>{{ row.rowMeta.commentCount }}</span
>
<div class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:bg-primary/10">
<MdiArrowExpand
class="select-none transform hover:(text-pink-500 scale-120)"
@click="expandForm(row, state)"
/>
</div>
</div>
</div>
</td>
<td
v-for="(columnObj, colIndex) of fields"
:ref="cellRefs.set"
:key="columnObj.id"
class="cell relative cursor-pointer nc-grid-cell"
:class="{
active: !isPublicView && selected.col === colIndex && selected.row === rowIndex,
}"
:data-key="rowIndex + columnObj.id"
:data-col="columnObj.id"
:data-title="columnObj.title"
@click="selectCell(rowIndex, colIndex)"
@dblclick="makeEditable(row, columnObj)"
@contextmenu="contextMenuTarget = { row: rowIndex, col: colIndex }"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
<div class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:bg-primary/10">
<MdiArrowExpand class="select-none transform hover:(text-pink-500 scale-120)" />
<div class="w-full h-full">
<SmartsheetVirtualCell
v-if="isVirtualCol(columnObj)"
v-model="row.row[columnObj.title]"
:column="columnObj"
:active="selected.col === colIndex && selected.row === rowIndex"
:row="row"
@navigate="onNavigate"
/>
<SmartsheetCell
v-else
v-model="row.row[columnObj.title]"
:column="columnObj"
:edit-enabled="editEnabled && selected.col === colIndex && selected.row === rowIndex"
:row-index="rowIndex"
:active="selected.col === colIndex && selected.row === rowIndex"
@update:edit-enabled="editEnabled = false"
@save="updateOrSaveRow(row, columnObj.title)"
@navigate="onNavigate"
@cancel="editEnabled = false"
/>
</div>
</div>
</div>
</td>
<td
v-for="(columnObj, colIndex) of fields"
:key="rowIndex + columnObj.title"
class="cell pointer nc-grid-cell"
:class="{
active: !isPublicView && selected.col === colIndex && selected.row === rowIndex,
}"
:data-col="columnObj.id"
:data-title="columnObj.title"
@click="selectCell(rowIndex, colIndex)"
@dblclick="makeEditable(row, columnObj)"
@contextmenu="contextMenuTarget = { row: rowIndex, col: colIndex }"
>
<div class="w-full h-full">
<SmartsheetVirtualCell
v-if="isVirtualCol(columnObj)"
v-model="row.row[columnObj.title]"
:column="columnObj"
:active="selected.col === colIndex && selected.row === rowIndex"
:row="row"
@navigate="onNavigate"
/>
<SmartsheetCell
v-else
v-model="row.row[columnObj.title]"
:column="columnObj"
:edit-enabled="editEnabled && selected.col === colIndex && selected.row === rowIndex"
@update:edit-enabled="editEnabled = false"
@save="updateOrSaveRow(row, columnObj.title)"
@navigate="onNavigate"
@cancel="editEnabled = false"
/>
</div>
</td>
</tr>
</td>
</tr>
</template>
</SmartsheetRow>
<tr v-if="!isLocked">
<td
@ -366,19 +424,31 @@ const onNavigate = (dir: NavigateDir) => {
</tbody>
</table>
<template #overlay>
<div class="bg-white shadow" @click="contextMenu = false">
<div v-if="contextMenuTarget" class="nc-menu-item" @click="deleteRow(contextMenuTarget.row)">Delete row</div>
<div class="nc-menu-item" @click="deleteSelectedRows">Delete all selected rows</div>
<div v-if="contextMenuTarget" class="nc-menu-item" @click="clearCell(contextMenuTarget)">Clear cell</div>
<div v-if="contextMenuTarget" class="nc-menu-item" @click="addEmptyRow(contextMenuTarget.row + 1)">
Insert new row
</div>
</div>
<a-menu class="bg-white shadow" @click="contextMenu = false">
<a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)"
><span class="text-xs">Delete row</span></a-menu-item
>
<a-menu-item @click="deleteSelectedRows"><span class="text-xs">Delete all selected rows</span></a-menu-item>
<a-menu-item v-if="contextMenuTarget" @click="clearCell(contextMenuTarget)"
><span class="text-xs">Clear cell</span>
</a-menu-item>
<a-menu-item v-if="contextMenuTarget" @click="addEmptyRow(contextMenuTarget.row + 1)">
<span class="text-xs">Insert new row</span>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<SmartsheetPagination />
<SmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:row="expandedFormRow"
:state="expandedFormRowState"
:meta="meta"
/>
</div>
</template>
@ -394,13 +464,11 @@ const onNavigate = (dir: NavigateDir) => {
min-height: 41px !important;
height: 41px !important;
position: relative;
//padding: 0 5px;
}
& > div {
overflow: hidden;
@apply flex align-center h-auto;
padding: 0 5px;
}
td > div {
overflow: hidden;
@apply flex align-center h-auto px-1;
}
table,

28
packages/nc-gui-v2/components/smartsheet/Row.vue

@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { Row } from '~/composables'
import { useProvideSmartsheetRowStore, useSmartsheetStoreOrThrow } from '#imports'
interface Props {
row: Row
}
const props = defineProps<Props>()
const currentRow = toRef(props, 'row')
const { meta } = useSmartsheetStoreOrThrow()
const { isNew, state, syncLTARRefs } = useProvideSmartsheetRowStore(meta, currentRow)
// on changing isNew(new record insert) status sync LTAR cell values
watch(isNew, async (nextVal, prevVal) => {
if (prevVal && !nextVal) {
await syncLTARRefs(currentRow.value.row)
// update row values without invoking api
currentRow.value.row = { ...currentRow.value.row, ...state.value }
currentRow.value.oldRow = { ...currentRow.value.row, ...state.value }
}
})
</script>
<template>
<slot :state="state" />
</template>

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

@ -0,0 +1,93 @@
<script setup lang="ts">
import { nextTick, useExpandedFormStoreOrThrow } from '#imports'
import { enumColor, timeAgo } from '~/utils'
import MdiAccountIcon from '~icons/mdi/account-circle'
const { loadCommentsAndLogs, commentsAndLogs, isCommentsLoading, commentsOnly, saveComment, isYou, comment } =
useExpandedFormStoreOrThrow()
const commentsWrapperEl = ref<HTMLDivElement>()
await loadCommentsAndLogs()
watch(
commentsAndLogs,
() => {
nextTick(() => {
if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight
})
},
{ immediate: true },
)
</script>
<template>
<div class="h-full d-flex flex-column w-full">
<div ref="commentsWrapperEl" class="flex-grow-1 min-h-[100px] overflow-y-auto scrollbar-thin-primary p-2">
<v-skeleton-loader v-if="isCommentsLoading && !commentsAndLogs" type="list-item-avatar-two-line@8" />
<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 '" />
<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 }}
{{ log.op_type === 'COMMENT' ? 'commented' : log.op_sub_type === 'INSERT' ? 'created' : 'edited' }}
</p>
<p
v-if="log.op_type === 'COMMENT'"
class="caption mb-0 nc-chip w-full min-h-20px"
:style="{ backgroundColor: enumColor.light[2] }"
>
{{ log.description }}
</p>
<p v-else v-dompurify-html="log.details" class="caption mb-0" style="word-break: break-all" />
<p class="time text-right text-[10px] mb-0">
{{ timeAgo(log.created_at) }}
</p>
</div>
</div>
</template>
</div>
<div class="border-1 my-2 w-full ml-6" />
<div class="p-0">
<div class="flex justify-center">
<a-checkbox v-model:checked="commentsOnly" @change="loadCommentsAndLogs"
><span class="text-[11px] text-gray-500">Comments only</span>
</a-checkbox>
</div>
<div class="flex-shrink-1 mt-2 d-flex pl-4">
<a-input
v-model:value="comment"
class="!text-xs"
ghost
:class="{ focus: showborder }"
@focusin="showborder = true"
@focusout="showborder = false"
@keyup.enter.prevent="saveComment"
>
<template #addonBefore>
<div class="flex align-center">
<mdi-account-circle class="text-lg text-pink-300" small @click="saveComment" />
</div>
</template>
<template #suffix>
<mdi-keyboard-return v-if="comment" class="text-sm" small @click="saveComment" />
</template>
</a-input>
</div>
</div>
</div>
</template>
<style scoped>
:deep(.red.lighten-4) {
@apply bg-red-100;
}
:deep(.green.lighten-4) {
@apply bg-green-100;
}
</style>

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

@ -0,0 +1,60 @@
<script lang="ts" setup>
import {
computed,
useExpandedFormStoreOrThrow,
useSmartsheetRowStoreOrThrow,
useSmartsheetStoreOrThrow,
useUIPermission,
} from '#imports'
import MdiDoorOpen from '~icons/mdi/door-open'
import MdiDoorClosed from '~icons/mdi/door-closed'
const emit = defineEmits(['cancel'])
const { meta } = useSmartsheetStoreOrThrow()
const { commentsDrawer, primaryValue, save: _save } = useExpandedFormStoreOrThrow()
const { isNew, syncLTARRefs } = useSmartsheetRowStoreOrThrow()
const { isUIAllowed } = useUIPermission()
const save = async () => {
if (isNew.value) {
const data = await _save()
await syncLTARRefs(data)
} else {
await _save()
}
}
const drawerToggleIcon = computed(() => (commentsDrawer.value ? MdiDoorOpen : MdiDoorClosed))
// todo: accept as a prop / inject
const iconColor = '#1890ff'
</script>
<template>
<div class="flex p-2 align-center gap-2">
<h5 class="text-lg font-weight-medium flex align-center gap-1 mb-0">
<mdi-table-arrow-right :style="{ color: iconColor }" />
<template v-if="meta">
{{ meta.title }}
</template>
<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" class="cursor-pointer select-none" @click="commentsDrawer = !commentsDrawer" />
<a-button size="small" class="!text" @click="emit('cancel')">
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button size="small" :disabled="!isUIAllowed('tableRowUpdate')" type="primary" @click="save">
<!-- Save Row -->
{{ $t('activity.saveRow') }}
</a-button>
</div>
</template>
<style scoped></style>

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

@ -0,0 +1,139 @@
<script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
import Comments from './Comments.vue'
import Header from './Header.vue'
import {
computedInject,
provide,
toRef,
useNuxtApp,
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
row: Row
state?: Record<string, any> | null
meta: TableType
loadRow?: boolean
useMetaFields?: boolean
}
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) => {
if (props.useMetaFields) {
return meta.value.columns ?? []
}
return _fields?.value ?? []
})
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)
provide(IsFormInj, true)
// accept as a prop
// const row: Row = { row: {}, rowMeta: {}, oldRow: {} }
watch(
state,
() => {
if (state.value) {
rowState.value = state.value
} else {
rowState.value = {}
}
},
{ immediate: true },
)
const isExpanded = useVModel(props, 'modelValue', emits)
</script>
<template>
<a-modal v-model:visible="isExpanded" :footer="null" width="min(90vw,1000px)" :body-style="{ padding: 0 }" :closable="false">
<Header @cancel="isExpanded = false" />
<a-card class="!bg-gray-100">
<div class="flex h-full nc-form-wrapper items-stretch">
<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">
<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">
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<SmartsheetCell
v-else
v-model="row.row[col.title]"
:column="col"
:edit-enabled="true"
@update:model-value="changedColumns.add(col.title)"
/>
</div>
</div>
</div>
</div>
<div class="nc-comments-drawer min-w-0 min-h-full max-h-full" :class="{ active: commentsDrawer }">
<div class="h-full">
<Comments v-if="commentsDrawer" />
</div>
</div>
</div>
</a-card>
</a-modal>
</template>
<style scoped lang="scss">
:deep(input, select, textarea) {
@apply !bg-white;
}
:deep(.ant-modal-body) {
@apply !bg-gray-100;
}
.nc-comments-drawer {
@apply w-0 transition-width ease-in-out duration-200;
overflow: hidden;
&.active {
@apply w-[250px] border-left-1;
}
}
.nc-form-wrapper {
max-height: max(calc(90vh - 100px), 600px);
height: max-content !important;
}
</style>

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

@ -12,11 +12,13 @@ const emits = defineEmits<Emits>()
const { $e } = useNuxtApp()
const isView = ref(false)
let showApiSnippet = $ref(false)
const showWebhookDrawer = ref(false)
function onApiSnippet() {
// get API snippet
showApiSnippet = true
$e('a:view:api-snippet')
}
@ -95,6 +97,7 @@ function onOpenModal(type: ViewTypes, title = '') {
</a-tooltip>
</a-menu-item>
<SmartsheetSidebarMenuApiSnippet v-model="showApiSnippet" />
<div class="flex-auto justify-end flex flex-col gap-4 mt-4">
<button
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded !bg-primary text-white transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease"

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

@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { FormType, GalleryType, GridType, KanbanType, ViewTypes } from 'nocodb-sdk'
import type { ViewType, ViewTypes } from 'nocodb-sdk'
import type { SortableEvent } from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue'
import { notification } from 'ant-design-vue'
@ -45,7 +45,7 @@ let isMarked = $ref<string | false>(false)
/** Watch currently active view, so we can mark it in the menu */
watch(activeView, (nextActiveView) => {
const _nextActiveView = nextActiveView as GridType | FormType | KanbanType
const _nextActiveView = nextActiveView as ViewType
if (_nextActiveView && _nextActiveView.id) {
selected.value = [_nextActiveView.id]
@ -66,7 +66,7 @@ function validate(value?: string) {
return 'View name is required'
}
if (views.value.every((v1) => ((v1 as GridType | KanbanType | GalleryType).alias || v1.title) !== value)) {
if (views.value.every((v1) => v1.title !== value)) {
return 'View name should be unique'
}
@ -134,14 +134,13 @@ const initSortable = (el: HTMLElement) => {
onMounted(() => menuRef && initSortable(menuRef.$el))
// todo: fix view type, alias is missing for some reason?
/** Navigate to view by changing url param */
function changeView(view: { id: string; alias?: string; title?: string; type: ViewTypes }) {
router.push({ params: { viewTitle: (view.alias ?? view.title) || '' } })
router.push({ params: { viewTitle: view.title || '' } })
}
/** Rename a view */
async function onRename(view: Record<string, any>) {
async function onRename(view: ViewType) {
const valid = validate(view.title)
if (valid !== true) {
@ -153,7 +152,7 @@ async function onRename(view: Record<string, any>) {
try {
// todo typing issues, order and id do not exist on all members of ViewTypes (Kanban, Gallery, Form, Grid)
await api.dbView.update(view.id, {
await api.dbView.update(view.id!, {
title: view.title,
order: view.order,
})

4
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, useApi, useRoute, useViews, watch } from '#imports'
import { computed, inject, provide, ref, useRoute, useViews, watch } from '#imports'
import { ActiveViewInj, MetaInj, RightSidebarInj, ViewListInj } from '~/context'
const meta = inject(MetaInj, ref())
@ -12,8 +12,6 @@ const activeView = inject(ActiveViewInj, ref())
const { views, loadViews } = useViews(meta)
const { api } = useApi()
const route = useRoute()
provide(ViewListInj, views)

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

@ -0,0 +1,182 @@
<script setup lang="ts">
import HTTPSnippet from 'httpsnippet'
import { useClipboard } from '@vueuse/core'
import { notification } from 'ant-design-vue'
import { ActiveViewInj, MetaInj } from '~/context'
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
interface Props {
modelValue: boolean
}
const { project } = $(useProject())
const { appInfo, token } = $(useGlobal())
const meta = $(inject(MetaInj))
const view = $(inject(ActiveViewInj))
const { xWhere } = useSmartsheetStoreOrThrow()
const { queryParams } = $(useViewData(meta, view as any, xWhere))
const { copy } = useClipboard()
let vModel = $(useVModel(props, 'modelValue', emits))
const langs = [
{
name: 'shell',
clients: ['curl', 'wget'],
},
{
name: 'javascript',
clients: ['axios', 'fetch', 'jquery', 'xhr'],
},
{
name: 'node',
clients: ['axios', 'fetch', 'request', 'native', 'unirest'],
},
{
name: 'nocodb-sdk',
clients: ['javascript', 'node'],
},
{
name: 'php',
},
{
name: 'python',
clients: ['python3', 'requests'],
},
{
name: 'ruby',
},
{
name: 'java',
},
{
name: 'c',
},
]
const selectedClient = $ref<string | undefined>(langs[0].clients && langs[0].clients[0])
const selectedLangName = $ref(langs[0].name)
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 activeLang = $computed(() => langs.find((lang) => lang.name === selectedLangName))
const code = $computed(() => {
if (activeLang?.name === 'nocodb-sdk') {
return `${selectedClient === 'node' ? 'const { Api } require("nocodb-sdk");' : 'import { Api } from "nocodb-sdk";'}
const api = new Api({
baseURL: ${JSON.stringify(apiUrl)},
headers: {
"xc-auth": ${JSON.stringify(token as string)}
}
})
api.dbViewRow.list(
"noco",
${JSON.stringify(project.title)},
${JSON.stringify(meta.title)},
${JSON.stringify(view.title)}, ${JSON.stringify(queryParams, null, 4)}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.error(error);
});
`
}
return snippet.convert(activeLang?.name, selectedClient || (activeLang?.clients && activeLang?.clients[0]), {})
})
const onCopyToClipboard = () => {
copy(code)
notification.info({ message: 'Copied to clipboard' })
}
const afterVisibleChange = (visible: boolean) => {
vModel = visible
}
</script>
<template>
<a-drawer
v-model:visible="vModel"
class="h-full relative"
style="color: red"
placement="right"
size="large"
:closable="false"
@after-visible-change="afterVisibleChange"
>
<div class="flex flex-col w-full h-full">
<a-typography-title :level="4">Code Snippet</a-typography-title>
<a-tabs v-model:activeKey="selectedLangName" class="!h-full">
<a-tab-pane v-for="item in langs" :key="item.name" class="!h-full">
<template #tab>
<div class="capitalize select-none">
{{ item.name }}
</div>
</template>
<monaco-editor
class="h-[60vh] border-1 border-gray-100 py-4 rounded-sm"
:model-value="code"
:read-only="true"
lang="typescript"
:validate="false"
:disable-deep-compare="true"
/>
<div class="flex flex-row w-full justify-end space-x-3 mt-4 uppercase">
<a-button
v-t="[
'c:snippet:copy',
{ client: activeLang?.clients && (selectedClient || activeLang?.clients[0]), lang: activeLang?.name },
]"
type="primary"
@click="onCopyToClipboard"
>Copy to clipboard</a-button
>
<a-select v-if="activeLang" v-model:value="selectedClient" style="width: 6rem">
<a-select-option v-for="(client, i) in activeLang?.clients" :key="i" class="!w-full uppercase" :value="client">
{{ client }}
</a-select-option>
</a-select>
</div>
<div class="absolute bottom-4 flex flex-row justify-center w-[95%]">
<a
v-t="['e:hiring']"
class="px-4 py-2 ! rounded shadow"
href="https://angel.co/company/nocodb"
target="_blank"
@click.stop
>
🚀 We are Hiring! 🚀
</a>
</div>
</a-tab-pane>
</a-tabs>
</div>
</a-drawer>
</template>
<style scoped></style>

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

@ -1,6 +1,5 @@
<script setup lang="ts">
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import SmartsheetGrid from '../smartsheet/Grid.vue'
import { computed, inject, provide, useMetas, useProvideSmartsheetStore, watch, watchEffect } from '#imports'

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

@ -3,7 +3,7 @@ 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 } from '#imports'
import { inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
const column = inject(ColumnInj)
@ -18,20 +18,39 @@ const active = false
const listItemsDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
reloadTrigger.trigger,
)
await loadRelatedTableMeta()
const value = computed(() => {
if (cellValue?.value) {
return cellValue?.value
} else if (isNew.value) {
return state?.value?.[column?.value.title as string]
}
return null
})
const unlinkRef = async (rec: Record<string, any>) => {
if (isNew.value) {
removeLTARRef(rec, column?.value as ColumnType)
} else {
await unlink(rec)
}
}
</script>
<template>
<div class="flex w-full chips-wrapper align-center" :class="{ active }">
<div class="chips d-flex align-center flex-grow">
<template v-if="cellValue">
<ItemChip :item="cellValue" :value="cellValue[relatedTablePrimaryValueProp]" @unlink="unlink(cellValue)" />
<template v-if="value">
<ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" />
</template>
</div>
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
@ -40,8 +59,7 @@ await loadRelatedTableMeta()
@click="listItemsDlg = true"
/>
</div>
<ListItems v-model="listItemsDlg" />
<ListItems v-model="listItemsDlg" @attach-record="listItemsDlg = true" />
</div>
</template>

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

@ -20,7 +20,7 @@ const showEditFormulaWarningMessage = () => {
}, 3000)
}
const result = computed(() => (isPg ? handleTZ(value) : value))
const result = isPg ? handleTZ(value) : value
const urls = computed(() => replaceUrlsWithLink(result.value))
</script>

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

@ -4,8 +4,8 @@ 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 } from '#imports'
import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context'
const column = inject(ColumnInj)!
@ -15,20 +15,32 @@ const row = inject(RowInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj)
const listItemsDlg = ref(false)
const childListDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
reloadTrigger.trigger,
)
await loadRelatedTableMeta()
const localCellValue = computed(() => {
if (cellValue?.value) {
return cellValue?.value
} else if (isNew.value) {
return state?.value?.[column?.value.title as string]
}
return []
})
const cells = computed(() =>
cellValue.value.reduce((acc: any[], curr: any) => {
localCellValue.value.reduce((acc: any[], curr: any) => {
if (!relatedTablePrimaryValueProp.value) return acc
const value = curr[relatedTablePrimaryValueProp.value]
@ -38,26 +50,45 @@ const cells = computed(() =>
return [...acc, { value, item: curr }]
}, [] as any[]),
)
const unlinkRef = async (rec: Record<string, any>) => {
if (isNew.value) {
removeLTARRef(rec, column?.value as ColumnType)
} else {
await unlink(rec)
}
}
</script>
<template>
<div class="flex align-center items-center gap-1 w-full chips-wrapper">
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cellValue">
<ItemChip v-for="(cell, i) of cells" :key="i" :value="cell.value" @unlink="unlink(cell.item)" />
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span>
</template>
</div>
<div class="flex-grow flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
@click="childListDlg = true"
/>
<MdiPlus class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
</div>
<template v-if="!isForm">
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<ItemChip v-for="(cell, i) of cells" :key="i" :item="ch" :value="cell.value" @unlink="unlinkRef(cell.item)" />
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true"
>more...
</span>
</template>
</div>
<div class="flex-grow flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
@click="childListDlg = true"
/>
<MdiPlus class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
</div>
</template>
<ListItems v-model="listItemsDlg" />
<ListChildItems v-model="childListDlg" @attach-record=";(childListDlg = false), (listItemsDlg = true)" />
<ListChildItems
v-model="childListDlg"
@attach-record="
() => {
childListDlg = false
listItemsDlg = true
}
"
/>
</div>
</template>

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

@ -4,8 +4,8 @@ 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 } from '#imports'
import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context'
const column = inject(ColumnInj)!
@ -15,20 +15,33 @@ const cellValue = inject(CellValueInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj)
const listItemsDlg = ref(false)
const childListDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>,
row,
isNew,
reloadTrigger.trigger,
)
await loadRelatedTableMeta()
const localCellValue = computed(() => {
if (cellValue?.value) {
return cellValue?.value
} else if (isNew.value) {
return state?.value?.[column?.value.title as string]
}
return []
})
const cells = computed(() =>
cellValue.value.reduce((acc: any[], curr: any) => {
localCellValue.value.reduce((acc: any[], curr: any) => {
if (!relatedTablePrimaryValueProp.value) return acc
const value = curr[relatedTablePrimaryValueProp.value]
@ -38,27 +51,45 @@ const cells = computed(() =>
return [...acc, { value, item: curr }]
}, [] as any[]),
)
const unlinkRef = async (rec: Record<string, any>) => {
if (isNew.value) {
removeLTARRef(rec, column?.value as ColumnType)
} else {
await unlink(rec)
}
}
</script>
<template>
<div class="flex align-center gap-1 w-full h-full chips-wrapper">
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cellValue">
<ItemChip v-for="(cell, i) of cells" :key="i" :value="cell.value" @unlink="unlink(cell.item)" />
<template v-if="!isForm">
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<ItemChip v-for="(cell, i) of cells" :key="i" :item="ch" :value="cell.value" @unlink="unlinkRef(cell.item)" />
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span>
</template>
</div>
<span v-if="value?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span>
</template>
</div>
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="childListDlg = true" />
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="childListDlg = true" />
<MdiPlus class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
</div>
<MdiPlus class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
</div>
</template>
<ListItems v-model="listItemsDlg" />
<ListChildItems v-model="childListDlg" @attach-record=";(childListDlg = false), (listItemsDlg = true)" />
<ListChildItems
v-model="childListDlg"
@attach-record="
() => {
childListDlg = false
listItemsDlg = true
}
"
/>
</div>
</template>

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

@ -1,23 +1,44 @@
<script setup lang="ts">
import { ActiveCellInj, ReadonlyInj } from '~/context'
import MdiCloseThickIcon from '~icons/mdi/close-thick'
import { useLTARStoreOrThrow } from '#imports'
import { ActiveCellInj, IsFormInj, ReadonlyInj } from '~/context'
interface Props {
value?: string | number | boolean
item?: any
}
const { value } = defineProps<Props>()
const { value, item } = defineProps<Props>()
const emit = defineEmits(['unlink'])
const { relatedTableMeta } = useLTARStoreOrThrow()
const readonly = inject(ReadonlyInj, false)
const active = inject(ActiveCellInj, ref(false))
const isForm = inject(IsFormInj)
const expandedFormDlg = ref(false)
</script>
<template>
<div class="group py-1 px-2 flex align-center gap-1 bg-gray-200/50 hover:bg-gray-200 rounded-[20px]" :class="{ active }">
<div
class="group py-1 px-2 flex align-center gap-1 bg-gray-200/50 hover:bg-gray-200 rounded-[20px]"
:class="{ active }"
@click="expandedFormDlg = true"
>
<span class="name">{{ value }}</span>
<div v-show="active" v-if="!readonly" class="flex align-center">
<MdiCloseThickIcon class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click="emit('unlink')" />
<div v-show="active || isForm" v-if="!readonly" 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"
v-model="expandedFormDlg"
:row="{ row: item }"
:meta="relatedTableMeta"
load-row
use-meta-fields
/>
</div>
</template>

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

@ -1,11 +1,15 @@
<script lang="ts" setup>
import { useLTARStoreOrThrow, useVModel, watch } from '#imports'
import { Modal } from 'ant-design-vue'
import type { ColumnType } from 'nocodb-sdk'
import { useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel, watch } from '#imports'
import { ColumnInj, IsFormInj } from '~/context'
const props = defineProps<{ modelValue?: boolean }>()
const emit = defineEmits(['update:modelValue', 'attachRecord'])
const vModel = useVModel(props, 'modelValue', emit)
const isForm = inject(IsFormInj, false)
const column = inject(ColumnInj)
const {
childrenList,
@ -16,40 +20,66 @@ const {
relatedTablePrimaryValueProp,
unlink,
getRelatedTableRowId,
relatedTableMeta,
} = useLTARStoreOrThrow()
watch(vModel, (nextVal) => {
if (nextVal) {
const { isNew, state, removeLTARRef } = useSmartsheetRowStoreOrThrow()
watch([vModel, isForm], (nextVal) => {
if (nextVal[0] || nextVal[1]) {
loadChildrenList()
}
})
const unlinkRow = async (row: Record<string, any>) => {
await unlink(row)
await loadChildrenList()
if (isNew.value) {
removeLTARRef(row, column?.value as ColumnType)
} else {
await unlink(row)
await loadChildrenList()
}
}
const container = computed(() =>
isForm
? h('div', {
class: 'w-full p-2',
})
: Modal,
)
const expandedFormDlg = ref(false)
const expandedFormRow = ref()
</script>
<template>
<a-modal v-model:visible="vModel" :footer="null" title="Child list">
<component :is="container" v-model:visible="vModel" :footer="null" title="Child list">
<div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col">
<div class="flex mb-4 align-center gap-2">
<div class="flex-1" />
<MdiReload class="cursor-pointer text-gray-500" @click="loadChildrenList" />
<MdiReload v-if="!isForm" class="cursor-pointer text-gray-500" @click="loadChildrenList" />
<a-button type="primary" size="small" @click="emit('attachRecord')">
<a-button type="primary" class="!text-xs" size="small" @click="emit('attachRecord')">
<div class="flex align-center gap-1">
<!-- todo: row is not defined? @click="unlinkRow(row)" -->
<MdiLinkVariantRemove class="text-xs text-white" />
<MdiLinkVariantRemove class="text-xs text-white" @click="unlinkRow(row)" />
Link to '{{ meta.title }}'
</div>
</a-button>
</div>
<template v-if="childrenList?.pageInfo?.totalRows">
<template v-if="(isNew && state?.[column?.title]?.length) || childrenList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0">
<a-card v-for="(row, i) of childrenList?.list ?? []" :key="i" class="ma-2 hover:(!bg-gray-200/50 shadow-md)">
<a-card
v-for="(row, i) of childrenList?.list ?? state?.[column?.title] ?? []"
:key="i"
class="ma-2 hover:(!bg-gray-200/50 shadow-md)"
@click="
() => {
expandedFormRow = row
expandedFormDlg = true
}
"
>
<div class="flex align-center">
<div class="flex-grow overflow-hidden min-w-0">
{{ row[relatedTablePrimaryValueProp]
@ -57,14 +87,20 @@ const unlinkRow = async (row: Record<string, any>) => {
</div>
<div class="flex-1"></div>
<div class="flex gap-2">
<MdiLinkVariantRemove class="text-xs text-grey hover:(!text-red-500) cursor-pointer" @click="unlinkRow(row)" />
<MdiDeleteOutline class="text-xs text-grey hover:(!text-red-500) cursor-pointer" @click="deleteRelatedRow(row)" />
<MdiLinkVariantRemove
class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
@click.stop="unlinkRow(row)"
/>
<MdiDeleteOutline
class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
@click.stop="deleteRelatedRow(row)"
/>
</div>
</div>
</a-card>
</div>
<a-pagination
v-if="childrenList?.pageInfo"
v-if="!isNew && childrenList?.pageInfo"
v-model:current="childrenListPagination.page"
v-model:page-size="childrenListPagination.size"
class="mt-2 mx-auto"
@ -75,7 +111,16 @@ const unlinkRow = async (row: Record<string, any>) => {
</template>
<a-empty v-else class="my-10" />
</div>
</a-modal>
<SmartsheetExpandedForm
v-if="expandedFormDlg && expandedFormRow"
v-model="expandedFormDlg"
:row="{ row: expandedFormRow }"
:meta="relatedTableMeta"
load-row
use-meta-fields
/>
</component>
</template>
<style scoped lang="scss">

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

@ -1,5 +1,8 @@
<script lang="ts" setup>
import { useLTARStoreOrThrow, useVModel, watch } from '#imports'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel } from '#imports'
import { ColumnInj } from '~/context'
const props = defineProps<{ modelValue: boolean }>()
@ -7,6 +10,8 @@ const emit = defineEmits(['update:modelValue', 'addNewRecord'])
const vModel = useVModel(props, 'modelValue', emit)
const column = inject(ColumnInj)
const {
childrenExcludedList,
loadChildrenExcludedList,
@ -14,18 +19,62 @@ const {
relatedTablePrimaryValueProp,
link,
getRelatedTableRowId,
relatedTableMeta,
meta,
row,
} = useLTARStoreOrThrow()
watch(vModel, (nextVal) => {
if (nextVal) {
loadChildrenExcludedList()
}
})
const { addLTARRef, isNew } = useSmartsheetRowStoreOrThrow()
const linkRow = async (row: Record<string, any>) => {
await link(row)
if (isNew.value) {
addLTARRef(row, column?.value as ColumnType)
} else {
await link(row)
}
vModel.value = false
}
watch(vModel, () => {
if (vModel.value) {
loadChildrenExcludedList()
}
})
const expandedFormDlg = ref(false)
/** populate initial state for a new row which is parent/child of current record */
const newRowState = computed(() => {
const colOpt = (column?.value as ColumnType)?.colOptions as LinkToAnotherRecordType
const colInRelatedTable: ColumnType | undefined = relatedTableMeta?.value?.columns?.find((col) => {
if (col.uidt !== UITypes.LinkToAnotherRecord) return false
const colOpt1 = col?.colOptions as LinkToAnotherRecordType
if (colOpt1?.fk_related_model_id !== meta.value.id) return false
if (colOpt.type === RelationTypes.MANY_TO_MANY && colOpt1?.type === RelationTypes.MANY_TO_MANY) {
return (
colOpt.fk_parent_column_id === colOpt1.fk_child_column_id && colOpt.fk_child_column_id === colOpt1.fk_parent_column_id
)
} else {
return (
colOpt.fk_parent_column_id === colOpt1.fk_parent_column_id && colOpt.fk_child_column_id === colOpt1.fk_child_column_id
)
}
})
if (!colInRelatedTable) return {}
const relatedTableColOpt = colInRelatedTable?.colOptions as LinkToAnotherRecordType
if (!relatedTableColOpt) return {}
if (relatedTableColOpt.type === RelationTypes.BELONGS_TO) {
return {
[colInRelatedTable.title as string]: row?.value?.row,
}
} else {
return {
[colInRelatedTable.title as string]: row?.value && [row.value.row],
}
}
})
</script>
<template>
@ -40,19 +89,19 @@ const linkRow = async (row: Record<string, any>) => {
></a-input>
<div class="flex-1" />
<MdiReload class="cursor-pointer text-gray-500" @click="loadChildrenExcludedList" />
<a-button type="primary" size="small" @click="emit('addNewRecord')">Add new record</a-button>
<a-button type="primary" size="small" @click="expandedFormDlg = true">Add new record</a-button>
</div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0">
<a-card
v-for="(row, i) in childrenExcludedList?.list ?? []"
v-for="(refRow, i) in childrenExcludedList?.list ?? []"
:key="i"
class="ma-2 cursor-pointer hover:(!bg-gray-200/50 shadow-md) group"
@click="linkRow(row)"
@click="linkRow(refRow)"
>
{{ row[relatedTablePrimaryValueProp]
{{ refRow[relatedTablePrimaryValueProp]
}}<span class="hidden group-hover:(inline) text-gray-400 text-[11px] ml-1"
>(Primary key : {{ getRelatedTableRowId(row) }})</span
>(Primary key : {{ getRelatedTableRowId(refRow) }})</span
>
</a-card>
</div>
@ -67,6 +116,15 @@ const linkRow = async (row: Record<string, any>) => {
/>
</template>
<a-empty v-else class="my-10" />
<SmartsheetExpandedForm
v-if="expandedFormDlg"
v-model="expandedFormDlg"
:meta="relatedTableMeta"
:row="{ row: {}, oldRow: {}, rowMeta: { new: true } }"
:state="newRowState"
use-meta-fields
/>
</div>
</a-modal>
</template>

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

@ -10,7 +10,7 @@ interface Props {
const { hook } = defineProps<Props>()
const { $state, $api, $e } = useNuxtApp()
const { $api } = useNuxtApp()
const toast = useToast()
@ -23,7 +23,7 @@ const activeKey = ref(0)
watch(
() => hook?.operation,
async (v) => {
async () => {
await loadSampleData()
},
)

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

@ -20,3 +20,4 @@ export * from './useVirtualCell'
export * from './useColumnCreateStore'
export * from './useSmartsheetStore'
export * from './useLTARStore'
export * from './useExpandedFormStore'

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

@ -24,7 +24,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
(meta: Ref<TableType>, column?: Ref<ColumnType>) => {
const { sqlUi } = useProject()
const { $api } = useNuxtApp()
const { getMeta } = useMetas()
const toast = useToast()
const idType = null
@ -196,6 +196,11 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
}
await $api.dbTableColumn.create(meta.value.id as string, formState.value)
/** if LTAR column then force reload related table meta */
if (formState.value.uidt === UITypes.LinkToAnotherRecord && meta.value.id !== formState.value.childId) {
getMeta(formState.value.childId, true).then(() => {})
}
toast.success('Column created')
}
onSuccess()

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

@ -0,0 +1,209 @@
import { UITypes } from 'nocodb-sdk'
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { message, notification } 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'
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 comment = ref('')
const commentsDrawer = ref(false)
const changedColumns = ref(new Set<string>())
const { project } = useProject()
const rowStore = useProvideSmartsheetRowStore(meta, row)
// todo
// const activeView = inject(ActiveViewInj)
// const { updateOrSaveRow, insertRow } = useViewData(meta, activeView as any)
// getters
const primaryValue = computed(() => {
if (row?.value?.row) {
const col = meta?.value?.columns?.find((c) => c.pv)
if (!col) {
return
}
const value = row.value.row?.[col.title as string]
const uidt = col.uidt
if (uidt === UITypes.Date) {
return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD')
} else if (uidt === UITypes.DateTime) {
return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD HH:mm')
} else if (uidt === UITypes.Time) {
let dateTime = dayjs(value)
if (!dateTime.isValid()) {
dateTime = dayjs(value, 'HH:mm:ss')
}
if (!dateTime.isValid()) {
dateTime = dayjs(`1999-01-01 ${value}`)
}
if (!dateTime.isValid()) {
return value
}
return dateTime.format('HH:mm:ss')
}
return value
}
})
// actions
const loadCommentsAndLogs = async () => {
if (!row.value) return
const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!rowId) return
commentsAndLogs.value =
(
await api.utils.commentList({
row_id: rowId,
fk_model_id: meta.value.id as string,
comments_only: commentsOnly.value,
})
)?.reverse?.() || []
}
const isYou = (email: string) => {
return $state.user?.value?.email === email
}
const saveComment = async () => {
try {
if (!row.value || !comment.value) return
const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!rowId) return
await api.utils.commentRow({
fk_model_id: meta.value?.id as string,
row_id: rowId,
// todo: swagger type correction
description: comment.value,
} as any)
comment.value = ''
message.success('Comment added successfully')
await loadCommentsAndLogs()
} catch (e: any) {
message.error(e.message)
}
$e('a:row-expand:comment')
}
const save = async () => {
let data
try {
// todo:
// if (this.presetValues) {
// // cater presetValues
// for (const k in this.presetValues) {
// this.$set(this.changedColumns, k, true);
// }
// }
const updateOrInsertObj = [...changedColumns.value].reduce((obj, col) => {
obj[col] = row.value.row[col]
return obj
}, {} as Record<string, any>)
if (row.value.rowMeta.new) {
data = await $api.dbTableRow.create('noco', project.value.title as string, meta.value.title, updateOrInsertObj)
/* todo:
// save hasmany and manytomany relations from local state
if (this.$refs.virtual && Array.isArray(this.$refs.virtual)) {
for (const vcell of this.$refs.virtual) {
if (vcell.save) {
await vcell.save(this.localState);
}
}
} */
row.value = {
row: data,
rowMeta: {},
oldRow: { ...data },
}
/// todo:
// await this.reload();
} else if (Object.keys(updateOrInsertObj).length) {
const id = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[])
if (!id) {
return message.info("Update not allowed for table which doesn't have primary Key")
}
await $api.dbTableRow.update(NOCO, project.value.title as string, meta.value.title, id, updateOrInsertObj)
for (const key of Object.keys(updateOrInsertObj)) {
// audit
$api.utils
.auditRowUpdate(id, {
fk_model_id: meta.value.id,
column_name: key,
row_id: id,
value: getPlainText(updateOrInsertObj[key]),
prev_value: getPlainText(row.value.oldRow[key]),
})
.then(() => {})
}
} else {
return message.info('No columns to update')
}
// this.$emit('update:oldRow', { ...this.localState });
// this.changedColumns = {};
// this.$emit('input', this.localState);
// this.$emit('update:isNew', false);
notification.success({
message: `${primaryValue.value || 'Row'} updated successfully.`,
// position: 'bottom-right',
})
changedColumns.value = new Set()
} catch (e: any) {
notification.error({ message: `Failed to update row`, description: await extractSdkResponseErrorMsg(e) })
}
$e('a:row-expand:add')
return data
}
return {
...rowStore,
commentsOnly,
loadCommentsAndLogs,
commentsAndLogs,
isCommentsLoading,
commentsError,
saveComment,
comment,
isYou,
commentsDrawer,
row,
primaryValue,
save,
changedColumns,
}
}, 'expanded-form-store')
export { useProvideExpandedFormStore }
export function useExpandedFormStoreOrThrow() {
const expandedFormStore = useExpandedFormStore()
if (expandedFormStore == null) throw new Error('Please call `useExpandedFormStore` on the appropriate parent component')
return expandedFormStore
}
// todo: move to utils
function getPlainText(htmlString: string) {
const div = document.createElement('div')
div.textContent = htmlString || ''
return div.innerHTML
}

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

@ -1,7 +1,7 @@
import { usePreferredLanguages, useStorage } from '@vueuse/core'
import { useJwt } from '@vueuse/integrations/useJwt'
import type { JwtPayload } from 'jwt-decode'
import type { State, StoredState } from './types'
import type { AppInfo, State, StoredState } from './types'
import { computed, ref, toRefs, useCounter, useNuxtApp, useTimestamp } from '#imports'
import type { User } from '~/lib'
@ -71,6 +71,8 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
set: (val) => (storage.value.token = val),
})
const appInfo = ref<AppInfo>({ ncSiteUrl: 'localhost:8080' })
/** reactive token payload */
const { payload } = useJwt<JwtPayload & User>(token)
@ -88,5 +90,6 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
timestamp,
runningRequests,
error,
appInfo,
}
}

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

@ -11,6 +11,10 @@ export interface FeedbackForm {
lastFormPollDate?: string
}
export interface AppInfo {
ncSiteUrl: string
}
export interface StoredState {
token: string | null
user: User | null
@ -27,6 +31,7 @@ export type State = ToRefs<Omit<StoredState, 'token'>> & {
timestamp: Ref<number>
runningRequests: ReturnType<typeof useCounter>
error: Ref<any>
appInfo: Ref<AppInfo>
}
export interface Getters {

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

@ -1,5 +1,5 @@
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import { Modal, notification } from 'ant-design-vue'
import { useInjectionState, useMetas, useProject } from '#imports'
import { NOCO } from '~/lib'
@ -13,7 +13,7 @@ interface DataApiResponse {
/** Store for managing Link to another cells */
const [useProvideLTARStore, useLTARStore] = useInjectionState(
(column: Ref<Required<ColumnType>>, row?: Ref<Row>, reloadData = () => {}) => {
(column: Ref<Required<ColumnType>>, row?: Ref<Row>, isNewRow: ComputedRef<boolean> | Ref<boolean>, reloadData = () => {}) => {
// state
const { metas, getMeta } = useMetas()
const { project } = useProject()
@ -61,28 +61,49 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const relatedTablePrimaryValueProp = computed(() => {
return (relatedTableMeta?.value?.columns?.find((c) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title
})
const relatedTablePrimaryKeyProps = computed(() => {
return relatedTableMeta?.value?.columns?.filter((c) => c.pk)?.map((c) => c.title) ?? []
})
const primaryValueProp = computed(() => {
return (meta?.value?.columns?.find((c: Required<ColumnType>) => c.pv) || relatedTableMeta?.value?.columns?.[0])?.title
})
const loadChildrenExcludedList = async () => {
try {
childrenExcludedList.value = await $api.dbTableRow.nestedChildrenExcludedList(
NOCO,
project.value.id as string,
meta.value.id,
rowId.value,
colOptions.type as 'mm' | 'hm',
column?.value?.title,
// todo: swagger type correction
{
limit: childrenExcludedListPagination.size,
offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
where:
childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
} as any,
)
/** if new row load all records */
if (isNewRow?.value) {
childrenExcludedList.value = await $api.dbTableRow.list(
NOCO,
project.value.id as string,
relatedTableMeta?.value?.id as string,
{
limit: childrenExcludedListPagination.size,
offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
where:
childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
fields: [relatedTablePrimaryValueProp.value, ...relatedTablePrimaryKeyProps.value],
} as any,
)
} else {
childrenExcludedList.value = await $api.dbTableRow.nestedChildrenExcludedList(
NOCO,
project.value.id as string,
meta.value.id,
rowId.value,
colOptions.type as 'mm' | 'hm',
column?.value?.title,
// todo: swagger type correction
{
limit: childrenExcludedListPagination.size,
offset: childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1),
where:
childrenExcludedListPagination.query &&
`(${relatedTablePrimaryValueProp.value},like,${childrenExcludedListPagination.query})`,
} as any,
)
}
} catch (e: any) {
notification.error({
message: 'Failed to load list',
@ -93,6 +114,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const loadChildrenList = async () => {
try {
if (colOptions.type === 'bt') return
childrenList.value = await $api.dbTableRow.nestedList(
NOCO,
project.value.id as string,
@ -198,6 +221,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
column?.value?.title,
getRelatedTableRowId(row) as string,
)
await loadChildrenList()
} catch (e: any) {
notification.error({
message: 'Linking failed',

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

@ -0,0 +1,105 @@
import { notification } 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 { NOCO } from '~/lib'
import { extractPkFromRow, extractSdkResponseErrorMsg } from '~/utils'
const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState((meta: Ref<TableType>, row: Ref<Row>) => {
const { $api } = useNuxtApp()
const { project } = useProject()
const { metas } = useMetas()
// state
const state = ref<Record<string, Record<string, any> | Record<string, any>[] | null>>({})
// getters
const isNew = computed(() => row.value?.rowMeta?.new ?? false)
// actions
const addLTARRef = async (value: Record<string, any>, column: ColumnType) => {
const { isHm, isMm, isBt } = $(useVirtualCell(ref(column)))
if (isHm || isMm) {
state.value[column.title!] = state.value[column.title!] || []
state.value[column.title!]!.push(value)
} else if (isBt) {
state.value[column.title!] = value
}
}
// actions
const removeLTARRef = async (value: Record<string, any>, column: ColumnType) => {
const { isHm, isMm, isBt } = $(useVirtualCell(ref(column)))
if (isHm || isMm) {
state.value[column.title!]?.splice(state.value[column.title!]?.indexOf(value), 1)
} else if (isBt) {
state.value[column.title!] = null
}
}
const linkRecord = async (rowId: string, relatedRowId: string, column: ColumnType, type: RelationTypes) => {
try {
await $api.dbTableRow.nestedAdd(
NOCO,
project.value.title as string,
meta.value.title as string,
rowId,
type,
column.title as string,
relatedRowId,
)
} catch (e: any) {
notification.error({
message: 'Linking failed',
description: await extractSdkResponseErrorMsg(e),
})
}
}
/** sync LTAR relations kept in local state */
const syncLTARRefs = async (row: Record<string, any>) => {
const id = extractPkFromRow(row, meta.value.columns as ColumnType[])
for (const column of meta?.value?.columns ?? []) {
if (column.uidt !== UITypes.LinkToAnotherRecord) continue
const colOptions = column?.colOptions as LinkToAnotherRecordType
const { isHm, isMm, isBt } = $(useVirtualCell(ref(column)))
const relatedTableMeta = metas.value?.[colOptions?.fk_related_model_id as string]
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)
}
} 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,
)
}
}
}
return {
row,
state,
isNew,
// todo: use better name
addLTARRef,
removeLTARRef,
syncLTARRefs,
}
}, 'smartsheet-row-store')
export { useProvideSmartsheetRowStore }
export function useSmartsheetRowStoreOrThrow() {
const smartsheetRowStore = useSmartsheetRowStore()
if (smartsheetRowStore == null) throw new Error('Please call `useSmartsheetRowStore` on the appropriate parent component')
return smartsheetRowStore
}

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

@ -1,15 +1,14 @@
import { computed } from '@vue/reactivity'
import { ViewTypes } from 'nocodb-sdk'
import type { TableType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useNuxtApp } from '#app'
import { useProject } from '#imports'
import { useInjectionState } from '~/composables/useInjectionState'
import { computed, reactive, useInjectionState, useNuxtApp, useProject, useTemplateRefsList } from '#imports'
const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState((view: Ref<ViewType>, meta: Ref<TableType>) => {
const { $api } = useNuxtApp()
const { sqlUi } = useProject()
const cellRefs = useTemplateRefsList<HTMLTableDataCellElement>()
// state
// todo: move to grid view store
const search = reactive({
@ -37,8 +36,6 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState((view:
return where
})
// actions
return {
view,
meta,
@ -50,6 +47,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState((view:
isForm,
isGrid,
isGallery,
cellRefs,
}
}, 'smartsheet-store')

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

@ -1,10 +1,10 @@
import type { Api, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { Api, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { notification } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { useProject } from '#imports'
import { NOCO } from '~/lib'
import { extractSdkResponseErrorMsg } from '~/utils'
import { extractPkFromRow, extractSdkResponseErrorMsg } from '~/utils'
const formatData = (list: Record<string, any>[]) =>
list.map((row) => ({
@ -16,7 +16,10 @@ const formatData = (list: Record<string, any>[]) =>
export interface Row {
row: Record<string, any>
oldRow: Record<string, any>
rowMeta?: any
rowMeta: {
new?: boolean
commentCount?: number
}
}
export function useViewData(
@ -24,8 +27,13 @@ export function useViewData(
viewMeta: Ref<ViewType & { id: string }> | ComputedRef<ViewType & { id: string }> | undefined,
where?: ComputedRef<string | undefined>,
) {
if (!meta) {
throw new Error('Table meta is not available')
}
const formattedData = ref<Row[]>([])
const paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 })
const aggCommentCount = ref<Record<string, number>>({})
const galleryData = ref<GalleryType | undefined>(undefined)
const { project } = useProject()
@ -50,6 +58,38 @@ export function useViewData(
paginationData.value.totalRows = count
}
const queryParams = computed(() => ({
offset: (paginationData.value?.page ?? 0) - 1,
limit: paginationData.value?.pageSize ?? 25,
where: where?.value ?? '',
}))
/** load row comments count */
const loadAggCommentsCount = async () => {
// todo: handle in public api
// if (this.isPublicView) {
// return;
// }
const ids = formattedData.value
?.filter(({ rowMeta: { new: isNew } }) => !isNew)
?.map(({ row }) => {
return extractPkFromRow(row, meta?.value?.columns as ColumnType[])
})
if (!ids?.length) return
aggCommentCount.value = await $api.utils.commentCount({
ids,
fk_model_id: meta.value.id as string,
})
for (const row of formattedData.value) {
const id = extractPkFromRow(row.row, meta?.value?.columns as ColumnType[])
row.rowMeta.commentCount = aggCommentCount.value?.find((c) => c.row_id === id)?.count || 0
}
}
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, {
@ -58,6 +98,8 @@ export function useViewData(
})
formattedData.value = formatData(response.list)
paginationData.value = response.pageInfo
loadAggCommentsCount()
}
const loadGalleryData = async () => {
@ -246,6 +288,7 @@ export function useViewData(
return {
loadData,
paginationData,
queryParams,
formattedData,
insertRow,
updateRowProperty,
@ -258,5 +301,7 @@ export function useViewData(
syncCount,
galleryData,
loadGalleryData,
aggCommentCount,
loadAggCommentsCount,
}
}

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

@ -1,15 +1,9 @@
<script lang="ts" setup>
import { breakpointsTailwind } from '@vueuse/core'
import { navigateTo } from '#app'
import { computed, useBreakpoints, useGlobal, useProject, useRoute } from '#imports'
/** get current breakpoints (for enabling sidebar) */
const breakpoints = useBreakpoints(breakpointsTailwind)
import { computed, useGlobal, useRoute } from '#imports'
const { signOut, signedIn, isLoading, user } = useGlobal()
const { project } = useProject()
const route = useRoute()
const email = computed(() => user.value?.email ?? '---')
@ -83,7 +77,7 @@ const logout = () => {
</template>
</a-layout-header>
<div class="w-full" style="height: calc(100% - var(--header-height))">
<div class="w-full overflow-hidden" style="height: calc(100% - var(--header-height))">
<slot />
</div>
</a-layout>

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

@ -80,6 +80,7 @@ export default defineNuxtConfig({
],
define: {
'process.env.DEBUG': 'false',
'global': {},
},
server: {
watch: {

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

File diff suppressed because it is too large Load Diff

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

@ -26,6 +26,9 @@
"socket.io-client": "^4.5.1",
"sortablejs": "^1.15.0",
"unique-names-generator": "^4.7.1",
"vue-dompurify-html": "^3.0.0",
"url": "^0.11.0",
"util": "^0.12.4",
"vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0",
@ -59,6 +62,7 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"happy-dom": "^6.0.3",
"httpsnippet": "^2.0.0",
"less": "^4.1.3",
"nuxt": "3.0.0-rc.4",
"nuxt-windicss": "^2.5.0",

10
packages/nc-gui-v2/pages/index/user/index.vue

@ -1,13 +1,3 @@
<script setup lang="ts">
import { navigateTo, useNuxtApp, useRoute } from '#app'
import MdiAccountCog from '~icons/mdi/account-cog'
import MdiFolderOutline from '~icons/mdi/folder-outline'
const { $api } = useNuxtApp()
const route = useRoute()
</script>
<template>
<NuxtLayout>
<NuxtPage />

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

@ -6,7 +6,7 @@ import { reactive, ref, useApi } from '#imports'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import MdiKeyChange from '~icons/mdi/key-change'
const { api, isLoading } = useApi()
const { api } = useApi()
const { t } = useI18n()
@ -34,7 +34,7 @@ const formRules = {
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
// Passwords match
{
validator: (_: unknown, v: string) => {
validator: (_: unknown, _v: string) => {
return new Promise((resolve, reject) => {
if (form.password === form.passwordRepeat) return resolve(true)
reject(new Error(t('msg.error.signUpRules.passwdMismatch')))

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

@ -21,12 +21,6 @@ const currentMenu = ref<string[]>(['addORImport'])
provide(TabMetaInj, activeTab)
function onEdit(targetKey: number, action: string) {
if (action !== 'add') {
closeTab(targetKey)
}
}
function openQuickImportDialog(type: string) {
quickImportDialog.value = true
importType.value = type

4
packages/nc-gui-v2/pages/project/index/[id].vue

@ -4,13 +4,13 @@ import type { Form } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { ref } from 'vue'
import { useToast } from 'vue-toastification'
import { navigateTo, useNuxtApp, useRoute } from '#app'
import { navigateTo, useRoute } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { projectTitleValidator } from '~/utils/validation'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
import { nextTick, reactive, useSidebar } from '#imports'
const { api, isLoading } = useApi()
const { api } = useApi()
useSidebar({ hasSidebar: false })

6
packages/nc-gui-v2/pages/project/index/create.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { onMounted, onUpdated } from '@vue/runtime-core'
import { onMounted } from '@vue/runtime-core'
import type { Form } from 'ant-design-vue'
import { useToast } from 'vue-toastification'
import { nextTick, reactive, ref, useApi, useSidebar } from '#imports'
@ -8,10 +8,6 @@ import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { projectTitleValidator } from '~/utils/validation'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
const name = ref('')
const valid = ref(false)
const { $e } = useNuxtApp()
const { api, isLoading } = useApi()

2
packages/nc-gui-v2/pages/projects/index.vue

@ -56,8 +56,6 @@ const deleteProject = (project: ProjectType) => {
},
})
}
const visible = ref(true)
</script>
<template>

6
packages/nc-gui-v2/plugins/domPurify.ts

@ -0,0 +1,6 @@
import VueDOMPurifyHTML from 'vue-dompurify-html'
import { defineNuxtPlugin } from 'nuxt/app'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(VueDOMPurifyHTML)
})

9
packages/nc-gui-v2/plugins/state.ts

@ -13,14 +13,21 @@ import { useDark, useGlobal, watch } from '#imports'
* console.log($state.lang.value) // 'en'
* ```
*/
export default defineNuxtPlugin((nuxtApp) => {
export default defineNuxtPlugin(async (nuxtApp) => {
const state = useGlobal()
const { $api } = useNuxtApp()
const darkMode = useDark()
/** set i18n locale to stored language */
nuxtApp.vueApp.i18n.locale.value = state.lang.value
try {
state.appInfo.value = await $api.utils.appInfo()
} catch (e) {
console.error(e)
}
/** set current dark mode from storage */
watch(
state.darkMode,

1
packages/nc-gui-v2/tsconfig.json

@ -11,7 +11,6 @@
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": [
"@nuxt/types",
"@intlify/vite-plugin-vue-i18n/client",
"vue-i18n",
"unplugin-icons/types/vue",

11
packages/nc-gui-v2/utils/dataUtils.ts

@ -0,0 +1,11 @@
import type { ColumnType } from 'nocodb-sdk'
export const extractPkFromRow = (row: Record<string, any>, columns: ColumnType[]) => {
return (
row &&
columns
?.filter((c) => c.pk)
.map((c) => row?.[c.title as string])
.join('___')
)
}

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

@ -15,3 +15,4 @@ export * from './columnUtils'
export * from './validation'
export * from './viewUtils'
export * from './currencyUtils'
export * from './dataUtils'

1
packages/nc-gui/components/project/spreadsheet/components/ExpandedForm.vue

@ -678,6 +678,7 @@ h5 {
padding: 8px;
border-radius: 8px;
}
</style>
<!--
/**

19
packages/nocodb-sdk/src/lib/Api.ts

@ -122,7 +122,7 @@ export interface TableType {
export interface ViewType {
id?: string;
title?: string;
title: string;
deleted?: boolean;
order?: number;
fk_model_id?: string;
@ -163,6 +163,7 @@ export interface TableReqType {
pinned?: boolean;
deleted?: boolean;
order?: number;
mm?: boolean;
columns?: ColumnType[];
}
@ -234,12 +235,14 @@ export interface ColumnType {
deleted?: boolean;
visible?: boolean;
order?: number;
system?: number | boolean;
meta?: any;
colOptions?:
| LinkToAnotherRecordType
| FormulaType
| RollupType
| LookupType
| SelectOptionsType[]
| SelectOptionsType
| object;
}
@ -300,9 +303,11 @@ export interface FormulaType {
}
export interface SelectOptionsType {
options: SelectOptionType;
}
export interface SelectOptionType {
id?: string;
type?: string;
virtual?: boolean;
fk_column_id?: string;
title?: string;
color?: string;
@ -1515,7 +1520,7 @@ export class Api<
*/
reorder: (
tableId: string,
data: { order?: string },
data: { order?: number },
params: RequestParams = {}
) =>
this.request<void, any>({
@ -1629,7 +1634,7 @@ export class Api<
update: (
viewId: string,
data: {
order?: string;
order?: number;
title?: string;
show_system_fields?: boolean;
lock_type?: 'collaborative' | 'locked' | 'personal';
@ -3106,7 +3111,7 @@ export class Api<
* @response `200` `void` OK
*/
commentRow: (
data: { row_id: string; fk_model_id: string; comment: string },
data: { row_id: string; fk_model_id: string; description?: string },
params: RequestParams = {}
) =>
this.request<void, any>({

2
packages/nocodb/src/lib/models/View.ts

@ -23,7 +23,7 @@ import extractProps from '../meta/helpers/extractProps';
const { v4: uuidv4 } = require('uuid');
export default class View implements ViewType {
id?: string;
title?: string;
title: string;
uuid?: string;
password?: string;
show: boolean;

58
scripts/sdk/swagger.json

@ -1334,7 +1334,7 @@
"type": "object",
"properties": {
"order": {
"type": "string"
"type": "number"
}
}
}
@ -1506,7 +1506,7 @@
"type": "object",
"properties": {
"order": {
"type": "string"
"type": "number"
},
"title": {
"type": "string"
@ -4448,7 +4448,7 @@
"fk_model_id": {
"type": "string"
},
"comment": {
"description": {
"type": "string"
}
},
@ -6011,7 +6011,8 @@
"type": "string"
},
"title": {
"type": "string"
"type": "string",
"required": true
},
"deleted": {
"type": "boolean"
@ -6233,6 +6234,9 @@
"order": {
"type": "number"
},
"mm": {
"type": "boolean"
},
"columns": {
"type": "array",
"items": {
@ -6579,6 +6583,13 @@
"order": {
"type": "number"
},
"system": {
"type": [
"number",
"boolean"
]
},
"meta": {},
"colOptions": {
"oneOf": [
{
@ -6594,10 +6605,7 @@
"$ref": "#/components/schemas/Lookup"
},
{
"type": "array",
"items": {
"$ref": "#/components/schemas/SelectOptions"
}
"$ref": "#/components/schemas/SelectOptions"
},
{
"type": "object"
@ -6810,16 +6818,34 @@
"SelectOptions": {
"title": "SelectOptions",
"type": "object",
"properties": {
"options": {
"type": "array",
"required": true,
"$ref": "#/components/schemas/SelectOption"
}
},
"examples": [
{
"options": [
{
"id": "string",
"fk_column_id": "string",
"title": "string",
"color": "string",
"order": 1
}
]
}
]
},
"SelectOption": {
"title": "SelectOption",
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
},
"virtual": {
"type": "boolean"
},
"fk_column_id": {
"type": "string"
},
@ -6836,12 +6862,10 @@
"examples": [
{
"id": "string",
"type": "string",
"virtual": true,
"fk_column_id": "string",
"title": "string",
"color": "string",
"order": 0
"order": 1
}
]
},

Loading…
Cancel
Save