Browse Source

Merge pull request #3005 from nocodb/fix/guiv2-common-issues

fix(gui-v2): Bug fixes in grid
pull/3037/head
Pranav C 2 years ago committed by GitHub
parent
commit
de33055f19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      packages/nc-gui-v2/app.vue
  2. 1
      packages/nc-gui-v2/components.d.ts
  3. 2
      packages/nc-gui-v2/components/cell/Checkbox.vue
  4. 14
      packages/nc-gui-v2/components/cell/Currency.vue
  5. 9
      packages/nc-gui-v2/components/cell/DatePicker.vue
  6. 8
      packages/nc-gui-v2/components/cell/DateTimePicker.vue
  7. 12
      packages/nc-gui-v2/components/cell/Decimal.vue
  8. 4
      packages/nc-gui-v2/components/cell/Duration.vue
  9. 2
      packages/nc-gui-v2/components/cell/Email.vue
  10. 3
      packages/nc-gui-v2/components/cell/Float.vue
  11. 1
      packages/nc-gui-v2/components/cell/Integer.vue
  12. 2
      packages/nc-gui-v2/components/cell/JsonEditableCell.vue
  13. 4
      packages/nc-gui-v2/components/cell/MultiSelect.vue
  14. 6
      packages/nc-gui-v2/components/cell/Percent.vue
  15. 4
      packages/nc-gui-v2/components/cell/Rating.vue
  16. 4
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  17. 8
      packages/nc-gui-v2/components/cell/TimePicker.vue
  18. 6
      packages/nc-gui-v2/components/cell/Url.vue
  19. 8
      packages/nc-gui-v2/components/cell/YearPicker.vue
  20. 2
      packages/nc-gui-v2/components/cell/attachment/utils.ts
  21. 9
      packages/nc-gui-v2/components/smartsheet-column/AdvancedOptions.vue
  22. 45
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  23. 6
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  24. 9
      packages/nc-gui-v2/components/smartsheet-header/CellIcon.vue
  25. 6
      packages/nc-gui-v2/components/smartsheet-header/Menu.vue
  26. 206
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  27. 10
      packages/nc-gui-v2/components/smartsheet-header/VirtualCellIcon.vue
  28. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  29. 10
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  30. 97
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  31. 8
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  32. 4
      packages/nc-gui-v2/components/smartsheet/VirtualCell.vue
  33. 8
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  34. 3
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  35. 3
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  36. 3
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  37. 11
      packages/nc-gui-v2/composables/useColumn.ts
  38. 352
      packages/nc-gui-v2/composables/useColumnCreateStore.ts
  39. 18
      packages/nc-gui-v2/composables/useLTARStore.ts
  40. 15
      packages/nc-gui-v2/composables/useSmartsheetStore.ts
  41. 15
      packages/nc-gui-v2/composables/useTabs.ts
  42. 12
      packages/nc-gui-v2/composables/useViewColumns.ts
  43. 2
      packages/nc-gui-v2/composables/useViewData.ts
  44. 27
      packages/nc-gui-v2/composables/useViewFilters.ts
  45. 22
      packages/nc-gui-v2/composables/useVirtualCell.ts
  46. 2
      packages/nc-gui-v2/context/index.ts
  47. 3
      packages/nc-gui-v2/pages/nc/[projectId]/index.vue
  48. 4
      packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue
  49. 5
      scripts/sdk/swagger.json

6
packages/nc-gui-v2/app.vue

@ -30,20 +30,20 @@ const toggleSidebar = () => {
<div class="flex-1" />
<div class="ml-4 flex justify-center flex-1">
<div class="ml-4 flex justify-center shrink">
<div class="flex items-center gap-2 cursor-pointer nc-noco-brand-icon" @click="navigateTo('/')">
<img width="35" src="~/assets/img/icons/512x512-trans.png" />
<span class="prose-xl">NocoDB</span>
</div>
</div>
<div class="flex-1 text-left">
<div v-show="state.isLoading.value" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }}
<mdi-reload :class="{ 'animate-infinite animate-spin': state.isLoading.value }" />
</div>
</div>
<div class="flex-1" />
<div class="flex justify-end gap-4">
<general-language class="mr-3" />

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

@ -66,6 +66,7 @@ declare module '@vue/runtime-core' {
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}

2
packages/nc-gui-v2/components/cell/Checkbox.vue

@ -26,7 +26,7 @@ const checkboxMeta = $computed(() => {
unchecked: 'mdi-checkbox-blank-circle-outline',
},
color: 'primary',
...(column?.meta || {}),
...(column?.value?.meta || {}),
}
})
</script>

14
packages/nc-gui-v2/components/cell/Currency.vue

@ -3,7 +3,7 @@ import { computed, inject, ref, useVModel } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
interface Props {
modelValue: number
modelValue: number | null
}
const props = defineProps<Props>()
@ -22,7 +22,7 @@ const currencyMeta = computed(() => {
return {
currency_locale: 'en-US',
currency_code: 'USD',
...(column && column.meta ? column.meta : {}),
...(column?.value?.meta ? column?.value?.meta : {}),
}
})
const currency = computed(() => {
@ -37,10 +37,18 @@ const currency = computed(() => {
return vModel.value
}
})
const focus = (el: HTMLInputElement) => el?.focus()
</script>
<template>
<input v-if="editEnabled" ref="root" v-model="vModel" />
<input
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="w-full h-full border-none outline-none"
@blur="editEnabled = false"
/>
<span v-else-if="vModel">{{ currency }}</span>
<span v-else />
</template>

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

@ -2,19 +2,18 @@
import dayjs from 'dayjs'
import { ColumnInj, ReadonlyInj } from '~/context'
interface Props {
modelValue: string | null
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
interface Props {
modelValue: string
}
const columnMeta = inject(ColumnInj, null)
const readOnlyMode = inject(ReadonlyInj, false)
let isDateInvalid = $ref(false)
const dateFormat = columnMeta?.meta?.date_format ?? 'YYYY-MM-DD'
const dateFormat = columnMeta?.value?.meta?.date_format ?? 'YYYY-MM-DD'
const localState = $computed({
get() {

8
packages/nc-gui-v2/components/cell/DateTimePicker.vue

@ -2,14 +2,14 @@
import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context'
interface Props {
modelValue: string | null
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
interface Props {
modelValue: string
}
const { isMysql } = useProject()
const readOnlyMode = inject(ReadonlyInj, false)

12
packages/nc-gui-v2/components/cell/Decimal.vue

@ -1,8 +1,9 @@
<script lang="ts" setup>
import { computed, inject, onMounted, ref } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number | null
modelValue: number | null | string
}
interface Emits {
@ -13,25 +14,24 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const editEnabled = inject<boolean>('editEnabled')
const editEnabled = inject<boolean>(EditModeInj)
const root = ref<HTMLInputElement>()
const vModel = useVModel(props, 'modelValue', emits)
onMounted(() => {
root.value?.focus()
})
const focus = (el: HTMLInputElement) => el?.focus()
</script>
<template>
<input
v-if="editEnabled"
ref="root"
:ref="focus"
v-model="vModel"
class="outline-none pa-0 border-none w-full h-full prose-sm"
type="number"
step="0.1"
@blur="editEnabled = false"
/>
<span v-else class="prose-sm">{{ vModel }}</span>
</template>

4
packages/nc-gui-v2/components/cell/Duration.vue

@ -4,7 +4,7 @@ import { ColumnInj } from '~/context'
import { convertDurationToSeconds, convertMS2Duration, durationOptions } from '~/utils'
interface Props {
modelValue: number | string
modelValue: number | string | null
}
const { modelValue } = defineProps<Props>()
@ -16,7 +16,7 @@ const column = inject(ColumnInj)
const showWarningMessage = ref(false)
const durationInMS = ref(0)
const isEdited = ref(false)
const durationType = ref(column?.meta?.duration || 0)
const durationType = ref(column?.value?.meta?.duration || 0)
const durationPlaceholder = computed(() => durationOptions[durationType.value].title)
const localState = computed({

2
packages/nc-gui-v2/components/cell/Email.vue

@ -25,7 +25,7 @@ const focus = (el: HTMLInputElement) => el?.focus()
</script>
<template>
<input v-if="editEnabled" ref="root" v-model="vModel" class="outline-none prose-sm" />
<input v-if="editEnabled" ref="root" v-model="vModel" class="outline-none prose-sm" @blur="editEnabled = false" />
<a v-else-if="validEmail" class="prose-sm underline hover:opacity-75" :href="`mailto:${vModel}`" target="_blank">
{{ vModel }}
</a>

3
packages/nc-gui-v2/components/cell/Float.vue

@ -3,7 +3,7 @@ import { inject, ref, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number
modelValue: number | null
}
interface Emits {
@ -29,6 +29,7 @@ const focus = (el: HTMLInputElement) => el?.focus()
class="outline-none pa-0 border-none w-full h-full prose-sm"
type="number"
step="0.1"
@blur="editEnabled = false"
/>
<span v-else class="prose-sm">{{ vModel }}</span>
</template>

1
packages/nc-gui-v2/components/cell/Integer.vue

@ -32,6 +32,7 @@ function onKeyDown(evt: KeyboardEvent) {
v-model="vModel"
class="outline-none pa-0 border-none w-full h-full prose-sm"
type="number"
@blur="editEnabled = false"
@keydown="onKeyDown"
/>
<span v-else class="prose-sm">{{ vModel }}</span>

2
packages/nc-gui-v2/components/cell/JsonEditableCell.vue

@ -4,7 +4,7 @@ import { computed, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: string | Record<string, any>
modelValue: string | Record<string, any> | null
isForm: boolean
}

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

@ -3,7 +3,7 @@ import { computed, inject } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
interface Props {
modelValue: string
modelValue: string | null
}
const { modelValue } = defineProps<Props>()
@ -14,7 +14,7 @@ const column = inject(ColumnInj)
const isForm = inject<boolean>('isForm', false)
const editEnabled = inject(EditModeInj, ref(false))
const options = computed(() => column?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || [])
const options = computed(() => column?.value?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || [])
const localState = computed({
get() {

6
packages/nc-gui-v2/components/cell/Percent.vue

@ -4,7 +4,7 @@ import { ColumnInj } from '~/context'
import { getPercentStep, isValidPercent, renderPercent } from '@/utils/percentUtils'
interface Props {
modelValue: number | string
modelValue: number | string | null
}
const { modelValue } = defineProps<Props>()
@ -17,7 +17,7 @@ const percent = ref()
const isEdited = ref(false)
const percentType = computed(() => column?.meta?.precision || 0)
const percentType = computed(() => column?.value?.meta?.precision || 0)
const percentStep = computed(() => getPercentStep(percentType.value))
@ -27,7 +27,7 @@ const localState = computed({
},
set: (val) => {
if (val === null) val = 0
if (isValidPercent(val, column?.meta?.negative)) {
if (isValidPercent(val, column?.value?.meta?.negative)) {
percent.value = val / 100
}
},

4
packages/nc-gui-v2/components/cell/Rating.vue

@ -8,7 +8,7 @@ import MdiThumbUpIcon from '~icons/mdi/thumb-up'
import MdiFlagIcon from '~icons/mdi/flag'
interface Props {
modelValue?: number
modelValue?: number | null
readOnly?: boolean
}
@ -26,7 +26,7 @@ const ratingMeta = computed(() => {
},
color: '#fcb401',
max: 5,
...(column?.meta || {}),
...(column?.value?.meta || {}),
}
})

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

@ -3,7 +3,7 @@ import { computed, inject } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
interface Props {
modelValue: string
modelValue: string | null
}
const { modelValue } = defineProps<Props>()
@ -19,7 +19,7 @@ const vModel = computed({
set: (val) => emit('update:modelValue', val),
})
const options = computed(() => column?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || [])
const options = computed(() => column?.value?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || [])
</script>
<template>

8
packages/nc-gui-v2/components/cell/TimePicker.vue

@ -2,14 +2,14 @@
import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context'
interface Props {
modelValue: string | null
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
interface Props {
modelValue: string
}
const { isMysql } = useProject()
const readOnlyMode = inject(ReadonlyInj, false)

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

@ -4,7 +4,7 @@ import { ColumnInj, EditModeInj } from '~/context'
import { isValidURL } from '~/utils'
interface Props {
modelValue: string
modelValue: string | null
}
const { modelValue: value } = defineProps<Props>()
@ -18,7 +18,7 @@ const editEnabled = inject(EditModeInj, ref(false))
const vModel = computed({
get: () => value,
set: (val) => {
if (!(column && column.meta && column.meta.validate) || isValidURL(val)) {
if (!column?.value?.meta?.validate || isValidURL(val)) {
emit('update:modelValue', val)
}
},
@ -30,7 +30,7 @@ const focus = (el: HTMLInputElement) => el?.focus()
</script>
<template>
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none" />
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none" @blur="editEnabled = false" />
<nuxt-link v-else-if="isValid" class="py-2 underline hover:opacity-75" :to="value" target="_blank">{{ value }}</nuxt-link>
<span v-else>{{ value }}</span>
</template>

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

@ -2,14 +2,14 @@
import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context'
interface Props {
modelValue: number | string | null
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
interface Props {
modelValue: number
}
const readOnlyMode = inject(ReadonlyInj, false)
let isYearInvalid = $ref(false)

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

@ -78,7 +78,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
try {
const data = await api.storage.upload(
{
path: [NOCO, project.value.title, meta.value.title, column.title].join('/'),
path: [NOCO, project.value.title, meta.value.title, column.value.title].join('/'),
},
{
files: file,

9
packages/nc-gui-v2/components/smartsheet-column/AdvancedOptions.vue

@ -77,13 +77,8 @@ formState.value.au = !!formState.value.au
<a-input v-model="formState.dtxs" :disabled="!sqlUi.columnEditable(formState)" size="small" @input="onAlter" />
</a-form-item>
<a-form-item :label="$t('placeholder.defaultValue')">
<a-textarea
v-model="formState.cdf"
:help="sqlUi.getDefaultValueForDatatype(formState.dt)"
size="small"
auto-size
@input="onAlter(2, true)"
/>
<a-textarea v-model:value="formState.cdf" size="small" auto-size @input="onAlter(2, true)" />
<span class="text-gray-400 text-xs">{{ sqlUi.getDefaultValueForDatatype(formState.dt) }}</span>
</a-form-item>
</div>
</template>

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

@ -1,10 +1,11 @@
<script lang="ts" setup>
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { computed, inject, useColumnCreateStoreOrThrow, useMetas, watchEffect } from '#imports'
import { MetaInj } from '~/context'
import { MetaInj, ReloadViewDataHookInj } from '~/context'
import { uiTypes } from '~/utils/columnUtils'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier'
interface Props {
editColumnDropdown: boolean
@ -13,11 +14,9 @@ interface Props {
const { editColumnDropdown } = defineProps<Props>()
const emit = defineEmits(['cancel'])
const meta = inject(MetaInj)
const reloadDataTrigger = inject(ReloadViewDataHookInj)
const advancedOptions = ref(false)
const { getMeta } = useMetas()
const formulaOptionsRef = ref()
@ -38,21 +37,23 @@ const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const uiTypesOptions = computed<typeof uiTypes>(() => {
return [
...uiTypes.filter((t) => !isEdit || !t.virtual),
...(!isEdit && meta?.value?.columns?.every((c) => !c.pk)
...uiTypes.filter((t) => !isEdit.value || !t.virtual),
...(!isEdit.value && meta?.value?.columns?.every((c) => !c.pk)
? [
{
name: 'ID',
icon: 'mdi-identifier',
name: UITypes.ID,
icon: MdiIdentifierIcon,
virtual: 0,
},
]
: []),
]
})
const reloadMeta = () => {
const reloadMetaAndData = () => {
emit('cancel')
getMeta(meta?.value.id as string, true)
reloadDataTrigger?.trigger()
}
function onCancel() {
@ -65,7 +66,7 @@ function onCancel() {
// create column meta if it's a new column
watchEffect(() => {
if (!isEdit) {
if (!isEdit.value) {
generateNewColumnMeta()
}
})
@ -107,8 +108,14 @@ watch(
/>
</a-form-item>
<a-form-item :label="$t('labels.columnType')">
<a-select v-model:value="formState.uidt" size="small" class="nc-column-name-input" @change="onUidtOrIdTypeChange">
<a-select-option v-for="opt in uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">
<a-select
v-model:value="formState.uidt"
show-search
size="small"
class="nc-column-name-input"
@change="onUidtOrIdTypeChange"
>
<a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">
<div class="flex gap-1 align-center text-xs">
<component :is="opt.icon" class="text-grey" />
{{ opt.name }}
@ -144,7 +151,7 @@ watch(
v-model:checked="formState.meta.validate"
class="ml-1 mb-1"
>
<span class="text-xs text-gray-600">
<span class="text-[10px] text-gray-600">
{{ `Accept only valid ${formState.uidt}` }}
</span>
</a-checkbox>
@ -156,7 +163,17 @@ watch(
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button html-type="submit" type="primary" size="small" @click="addOrUpdate(reloadMeta), (advancedOptions = false)">
<a-button
html-type="submit"
type="primary"
size="small"
@click="
() => {
addOrUpdate(reloadMetaAndData)
advancedOptions = false
}
"
>
<!-- Save -->
{{ $t('general.save') }}
</a-button>

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

@ -1,12 +1,12 @@
<script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { inject } from 'vue'
import { inject, toRef } from 'vue'
import { ColumnInj, MetaInj } from '~/context'
import { useProvideColumnCreateStore } from '#imports'
const { column } = defineProps<{ column: ColumnType & { meta: any } }>()
const props = defineProps<{ column: ColumnType & { meta: any } }>()
const column = toRef(props, 'column')
provide(ColumnInj, column)
const meta = inject(MetaInj)

9
packages/nc-gui-v2/components/smartsheet-header/CellIcon.vue

@ -1,5 +1,7 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { toRef } from 'vue'
import { ColumnInj } from '~/context'
import FilePhoneIcon from '~icons/mdi/file-phone'
import { useColumn } from '#imports'
@ -24,14 +26,15 @@ import CurrencyIcon from '~icons/mdi/currency-usd-circle-outline'
import PercentIcon from '~icons/mdi/percent-outline'
import DecimalIcon from '~icons/mdi/decimal'
const { columnMeta } = defineProps<{ columnMeta?: ColumnType }>()
const props = defineProps<{ columnMeta?: ColumnType }>()
const columnMeta = toRef(props, 'columnMeta')
const column = inject(ColumnInj, columnMeta)
const additionalColMeta = useColumn(column as ColumnType)
const additionalColMeta = useColumn(column as Ref<ColumnType>)
const icon = computed(() => {
if (column?.pk) {
if (column?.value?.pk) {
return KeyIcon
} else if (additionalColMeta.isJSON) {
return JSONIcon

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

@ -25,13 +25,13 @@ const { getMeta } = useMetas()
const deleteColumn = () =>
Modal.confirm({
title: h('div', ['Do you want to delete ', h('span', { class: 'font-weight-bold' }, [column?.title]), ' column ?']),
title: h('div', ['Do you want to delete ', h('span', { class: 'font-weight-bold' }, [column?.value?.title]), ' column ?']),
okText: t('general.delete'),
okType: 'danger',
cancelText: t('general.cancel'),
async onOk() {
try {
await $api.dbTableColumn.delete(column?.id as string)
await $api.dbTableColumn.delete(column?.value?.id as string)
getMeta(meta?.value?.id as string, true)
} catch (e) {
toast.error(await extractSdkResponseErrorMsg(e))
@ -41,7 +41,7 @@ const deleteColumn = () =>
const setAsPrimaryValue = async () => {
try {
await $api.dbTableColumn.primaryColumnSet(column?.id as string)
await $api.dbTableColumn.primaryColumnSet(column?.value?.id as string)
getMeta(meta?.value?.id as string, true)
toast.success('Successfully updated as primary column')
} catch (e) {

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

@ -1,141 +1,80 @@
<script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk'
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 } from '#imports'
const { column } = defineProps<{ column: ColumnType & { meta: any } }>()
import { provide, useProvideColumnCreateStore } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any } }>()
const column = toRef(props, 'column')
provide(ColumnInj, column)
const { metas } = useMetas()
const meta = inject(MetaInj)
useProvideColumnCreateStore(meta as Ref<TableType>, column)
const { isLookup, isBt, isRollup, isMm, isHm, isFormula, isCount } = useVirtualCell(column)
const colOptions = $computed(() => column.value?.colOptions)
const tableTile = $computed(() => meta?.value?.title)
const relationColumnOptions = $computed<LinkToAnotherRecordType | null>(() => {
if (isMm.value || isHm.value || isBt.value) {
return column.value?.colOptions as LinkToAnotherRecordType
} else if ((column?.value?.colOptions as LookupType | RollupType)?.fk_relation_column_id) {
return meta?.value?.columns?.find(
(c) => c.id === (column?.value?.colOptions as LookupType | RollupType)?.fk_relation_column_id,
)?.colOptions as LinkToAnotherRecordType
}
return null
})
const relatedTableMeta = $computed(
() => relationColumnOptions?.fk_related_model_id && metas.value?.[relationColumnOptions?.fk_related_model_id as string],
)
// import { UITypes } from 'nocodb-sdk'
// import { getUIDTIcon } from '../helpers/uiTypes'
// import EditVirtualColumn from '~/components/project/spreadsheet/components/EditVirtualColumn'
//
// export default {
// name: 'VirtualHeaderCell',
// components: { EditVirtualColumn },
// props: ['column', 'nodes', 'meta', 'isForm', 'isPublicView', 'sqlUi', 'required', 'isLocked', 'isVirtual'],
// data: () => ({
// columnDeleteDialog: false,
// editColumnMenu: false,
// rollupIcon: getUIDTIcon('Rollup'),
// rels: ['bt', 'hm', 'mm']
// }),
// computed: {
// alias() {
// // return this.column.lk ? `${this.column.lk._lcn} <small class="grey--text text--darken-1">(from ${this.column.lk._ltn})</small>` : this.column.title
// return this.column.title
// },
// type() {
// if (this.column?.colOptions?.type) {
// return this.column.colOptions.type
// }
// if (this.column?.colOptions?.formula) {
// return 'formula'
// }
// if (this.column.uidt === UITypes.Lookup) {
// return 'lk'
// }
// if (this.column.uidt === UITypes.Rollup) {
// return 'rl'
// }
// return ''
// },
// relation() {
// if (this.rels.includes(this.type)) {
// return this.column
// } else if (this.column.colOptions?.fk_relation_column_id) {
// return this.meta.columns.find(c => c.id === this.column.colOptions?.fk_relation_column_id)
// }
// return undefined
// },
// relationType() {
// return this.relation?.colOptions?.type
// },
// relationMeta() {
// if (this.rels.includes(this.type)) {
// return this.getMeta(this.column.colOptions.fk_related_model_id)
// } else if (this.relation) {
// return this.getMeta(this.relation.colOptions.fk_related_model_id)
// }
// return undefined
// },
// childColumn() {
// if (this.relationMeta?.columns) {
// if (this.type === 'rl') {
// const ch = this.relationMeta.columns.find(c => c.id === this.column.colOptions.fk_rollup_column_id)
// return ch
// }
// if (this.type === 'lk') {
// const ch = this.relationMeta.columns.find(c => c.id === this.column.colOptions.fk_lookup_column_id)
// return ch
// }
// }
// return ''
// },
// childTable() {
// if (this.relationMeta?.title) {
// return this.relationMeta.title
// }
// return ''
// },
// parentTable() {
// if (this.rels.includes(this.type)) {
// return this.meta.title
// }
// return ''
// },
// parentColumn() {
// if (this.rels.includes(this.type)) {
// return this.column.title
// }
// return ''
// },
// tooltipMsg() {
// if (!this.column) {
// return ''
// }
// if (this.type === 'hm') {
// return `'${this.parentTable}' has many '${this.childTable}'`
// } else if (this.type === 'mm') {
// return `'${this.childTable}' & '${this.parentTable}' have <br>many to many relation`
// } else if (this.type === 'bt') {
// return `'${this.column.title}' belongs to '${this.childTable}'`
// } else if (this.type === 'lk') {
// return `'${this.childColumn.title}' from '${this.childTable}' (${this.childColumn.uidt})`
// } else if (this.type === 'formula') {
// return `Formula - ${this.column.colOptions.formula}`
// } else if (this.type === 'rl') {
// return `'${this.childColumn.title}' of '${this.childTable}' (${this.childColumn.uidt})`
// }
// return ''
// }
// },
// methods: {
// getMeta(id) {
// return this.$store.state.meta.metas[id] || {}
// },
// async deleteColumn() {
// try {
// await this.$api.dbTableColumn.delete(this.column.id)
//
// if (this.column.uidt === UITypes.LinkToAnotherRecord && this.column.colOptions) {
// this.$store.dispatch('meta/ActLoadMeta', { force: true, id: this.column.colOptions.fk_related_model_id }).then(() => {})
// }
//
// this.$emit('saved')
// this.columnDeleteDialog = false
// } catch (e) {
// this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000)
// }
// }
// }
// }
const relatedTableTitle = $computed(() => relatedTableMeta?.title)
const childColumn = $computed(() => {
if (relatedTableMeta?.columns) {
if (isRollup.value) {
const ch = relatedTableMeta?.columns.find((c: ColumnType) => c.id === (colOptions as RollupType).fk_rollup_column_id)
return ch
}
if (isLookup.value) {
const ch = relatedTableMeta?.columns.find((c: ColumnType) => c.id === (colOptions as LookupType).fk_lookup_column_id)
return ch
}
}
return ''
})
const tooltipMsg = computed(() => {
if (!column.value) {
return ''
}
if (isHm.value) {
return `'${tableTile}' has many '${relatedTableTitle}'`
} else if (isMm.value) {
return `'${tableTile}' & '${relatedTableTitle}' have many to many relation`
} else if (isBt.value) {
return `'${column?.value?.title}' belongs to '${relatedTableTitle}'`
} else if (isLookup.value) {
return `'${childColumn.title}' from '${relatedTableTitle}' (${childColumn.uidt})`
} else if (isFormula.value) {
const formula = substituteColumnIdWithAliasInFormula(
(column.value?.colOptions as FormulaType)?.formula,
meta?.value?.columns as ColumnType[],
(column.value?.colOptions as any)?.formula_raw,
)
return `Formula - ${formula}`
} else if (isRollup.value) {
return `'${childColumn.title}' of '${relatedTableTitle}' (${childColumn.uidt})`
}
return ''
})
useProvideColumnCreateStore(meta as Ref<TableType>, column)
</script>
<template>
@ -145,7 +84,12 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
todo: bring tooltip
-->
<SmartsheetHeaderVirtualCellIcon v-if="column" />
<span class="name" style="white-space: nowrap" :title="column.title"> {{ column.title }}</span>
<a-tooltip placement="bottom">
<template #title>
{{ tooltipMsg }}
</template>
<span class="name" style="white-space: nowrap" :title="column.title"> {{ column.title }}</span>
</a-tooltip>
<span v-if="column.rqd" class="error--text text--lighten-1">&nbsp;*</span>
<!-- <span class="caption" v-html="tooltipMsg" /> -->

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

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import { toRef } from 'vue'
import { ColumnInj } from '~/context'
import GenericIcon from '~icons/mdi/square-rounded'
import HMIcon from '~icons/mdi/table-arrow-right'
@ -11,14 +12,15 @@ import RollupIcon from '~icons/mdi/movie-roll'
import CountIcon from '~icons/mdi/counter'
import SpecificDBTypeIcon from '~icons/mdi/database-settings'
const { columnMeta } = defineProps<{ columnMeta?: ColumnType }>()
const props = defineProps<{ columnMeta?: ColumnType }>()
const columnMeta = toRef(props, 'columnMeta')
const column = inject(ColumnInj, columnMeta)
const column = inject(ColumnInj, ref(columnMeta))
const icon = computed(() => {
switch (column?.uidt) {
switch (column?.value?.uidt) {
case UITypes.LinkToAnotherRecord:
switch ((column?.colOptions as LinkToAnotherRecordType)?.type) {
switch ((column?.value?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY:
return MMIcon
case RelationTypes.HAS_MANY:

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

@ -124,7 +124,7 @@ defineExpose({
v-if="!filter.readOnly"
:key="i"
small
class="nc-filter-item-remove-btn text-grey"
class="nc-filter-item-remove-btn cursor-pointer text-grey"
@click.stop="deleteFilter(filter, i)"
/>
<span v-else :key="`${i}dummy`" />

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

@ -16,9 +16,9 @@ interface Emits {
(event: 'update:modelValue', value: any): void
}
const { column, ...props } = defineProps<Props>()
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'save', 'navigate', 'update:editEnabled'])
const column = toRef(props, 'column')
provide(ColumnInj, column)
@ -44,11 +44,11 @@ const isAutoSaved = $computed(() => {
UITypes.AutoNumber,
UITypes.SpecificDBType,
UITypes.Geometry,
].includes(column.uidt as UITypes)
].includes(column?.value?.uidt as UITypes)
})
const isManualSaved = $computed(() => {
return [UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(column.uidt as UITypes)
return [UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(column?.value?.uidt as UITypes)
})
const vModel = computed({
@ -102,7 +102,7 @@ const syncAndNavigate = (dir: NavigateDir) => {
<template>
<div
class="nc-cell"
class="nc-cell w-full-h-full"
@keydown.stop.left
@keydown.stop.right
@keydown.stop.up

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

@ -1,8 +1,10 @@
<script lang="ts" setup>
import { onClickOutside, useEventListener } from '@vueuse/core'
import type { ColumnType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import {
inject,
onKeyStroke,
onMounted,
provide,
useGridViewColumnWidth,
@ -10,10 +12,10 @@ import {
useSmartsheetStoreOrThrow,
useViewData,
} from '#imports'
import type { Row } from '~/composables'
import {
ActiveViewInj,
ChangePageInj,
EditModeInj,
FieldsInj,
IsFormInj,
IsGridInj,
@ -34,11 +36,12 @@ const fields = inject(FieldsInj, ref([]))
const isLocked = inject(IsLockedInj, false)
// 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 { sqlUi } = useProject()
const { xWhere } = useSmartsheetStoreOrThrow()
const { xWhere, isPkAvail } = useSmartsheetStoreOrThrow()
const addColumnDropdown = ref(false)
const contextMenu = ref(false)
const contextMenuTarget = ref(false)
@ -74,12 +77,6 @@ const selectCell = (row: number, col: number) => {
selected.col = col
}
onKeyStroke(['Enter'], (e) => {
if (selected.row !== null && selected.col !== null) {
editEnabled = true
}
})
watch(
() => (view?.value as any)?.id,
async (n?: string, o?: string) => {
@ -103,9 +100,7 @@ defineExpose({
})
// instantiate column create store
// watchEffect(() => {
if (meta) useProvideColumnCreateStore(meta)
// })
// reset context menu target on hide
watch(contextMenu, () => {
@ -127,8 +122,29 @@ const clearCell = async (ctx: { row: number; col: number }) => {
await updateOrSaveRow(rowObj, columnObj.title)
}
const { copy } = useClipboard()
const makeEditable = (row: Row, col: ColumnType) => {
if (isPublicView || editEnabled || isView) {
return
}
if (!isPkAvail.value && !row.rowMeta.new) {
message.info("Update not allowed for table which doesn't have primary Key")
return
}
if (col.ai) {
message.info('Auto Increment field is not editable')
return
}
if (col.pk && !row.rowMeta.new) {
message.info('Editing primary key not supported')
return
}
return (editEnabled = true)
}
/** handle keypress events */
onKeyStroke(['Tab', 'Shift', 'Enter', 'Delete', 'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'], async (e: KeyboardEvent) => {
const onKeyDown = async (e: KeyboardEvent) => {
if (selected.row === null || selected.col === null) return
/** on tab key press navigate through cells */
switch (e.key) {
@ -153,7 +169,7 @@ onKeyStroke(['Tab', 'Shift', 'Enter', 'Delete', 'ArrowDown', 'ArrowUp', 'ArrowLe
/** on enter key press make cell editable */
case 'Enter':
e.preventDefault()
editEnabled = true
makeEditable(data.value[selected.row], fields.value[selected.col])
break
/** on delete key press clear cell */
case 'Delete':
@ -177,26 +193,59 @@ onKeyStroke(['Tab', 'Shift', 'Enter', 'Delete', 'ArrowDown', 'ArrowUp', 'ArrowLe
e.preventDefault()
if (selected.row < data.value.length - 1) selected.row++
break
default:
{
const rowObj = data.value[selected.row]
const columnObj = fields.value[selected.col]
if (e.metaKey || e.ctrlKey) {
switch (e.keyCode) {
// copy - ctrl/cmd +c
case 67:
await copy(rowObj.row[columnObj.title] || '')
break
}
}
if (editEnabled || e.ctrlKey || e.altKey || e.metaKey) {
return
}
/** on letter key press make cell editable and empty */
if (e?.key?.length === 1) {
if (!isPkAvail && !rowObj.rowMeta.new) {
return message.info("Update not allowed for table which doesn't have primary Key")
}
if (makeEditable(rowObj, columnObj)) {
rowObj.row[columnObj.title] = ''
}
// editEnabled = true
}
}
break
}
}
useEventListener(document, 'keydown', onKeyDown)
/** On clicking outside of table reset active cell */
const smartTable = ref(null)
onClickOutside(smartTable, () => {
selected.row = null
selected.col = null
})
const onNavigate = (dir: NavigateDir) => {
if (selected.row === null || selected.col === null) return
switch (dir) {
case NavigateDir.NEXT:
if (selected.col < visibleColLength - 1) {
selected.col++
} else if (selected.row < data.value.length - 1) {
if (selected.row < data.value.length - 1) {
selected.row++
selected.col = 0
}
break
case NavigateDir.PREV:
if (selected.col > 0) {
selected.col--
} else if (selected.row > 0) {
if (selected.row > 0) {
selected.row--
selected.col = visibleColLength - 1
}
break
}
@ -207,7 +256,7 @@ const onNavigate = (dir: NavigateDir) => {
<div class="flex flex-col h-100 min-h-0 w-100">
<div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-primary">
<a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']">
<table class="xc-row-table nc-grid backgroundColorDefault" @contextmenu.prevent="contextMenu = true">
<table ref="smartTable" class="xc-row-table nc-grid backgroundColorDefault" @contextmenu.prevent="contextMenu = true">
<thead>
<tr class="group">
<th>
@ -237,7 +286,7 @@ const onNavigate = (dir: NavigateDir) => {
<!-- v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" -->
<th v-t="['c:column:add']" @click="addColumnDropdown = true">
<a-dropdown v-model:visible="addColumnDropdown" :trigger="['click']">
<div class="h-full w-full flex align-center justify-center">
<div class="h-full w-[60px] flex align-center justify-center">
<MdiPlusIcon class="text-sm" />
</div>
<template #overlay>
@ -272,7 +321,7 @@ const onNavigate = (dir: NavigateDir) => {
:data-col="columnObj.id"
:data-title="columnObj.title"
@click="selectCell(rowIndex, colIndex)"
@dblclick="editEnabled = true"
@dblclick="makeEditable(row, columnObj)"
@contextmenu="contextMenuTarget = { row: rowIndex, col: colIndex }"
>
<div class="w-full h-full">

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

@ -1,3 +1,9 @@
<script setup lang="ts">
import { useSmartsheetStoreOrThrow } from '~/composables'
const { isGrid, isForm } = useSmartsheetStoreOrThrow()
</script>
<template>
<div class="nc-table-toolbar w-full py-1 flex gap-1 items-center" style="z-index: 7">
<SmartsheetToolbarSearchData class="flex-shrink" />
@ -8,7 +14,7 @@
<SmartsheetToolbarSortListMenu />
<SmartsheetToolbarShareView />
<SmartsheetToolbarShareView v-if="isForm || isGrid" />
<SmartsheetToolbarMoreActions />

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

@ -14,12 +14,12 @@ interface Props {
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'navigate'])
const { column, modelValue: value } = props
const column = toRef(props, 'column')
const active = toRef(props, 'active', false)
const row = toRef(props, 'row')
provide(ColumnInj, column)
provide(CellValueInj, value)
provide(ActiveCellInj, active)
provide(RowInj, row)
provide(CellValueInj, toRef(props, 'modelValue'))

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

@ -37,7 +37,7 @@ provide(ReloadViewDataHookInj, reloadEventHook)
provide(FieldsInj, fields)
provide(RightSidebarInj, ref(true))
useProvideSmartsheetStore(activeView as Ref<TableType>, meta)
const { isGallery, isGrid, isForm } = useProvideSmartsheetStore(activeView as Ref<TableType>, meta)
watch(tabMeta, async (newTabMeta, oldTabMeta) => {
if (newTabMeta !== oldTabMeta && newTabMeta?.id) await getMeta(newTabMeta.id)
@ -52,11 +52,11 @@ watch(tabMeta, async (newTabMeta, oldTabMeta) => {
<template v-if="meta">
<div class="flex flex-1 min-h-0">
<div v-if="activeView" class="h-full flex-grow min-w-0 min-h-0">
<SmartsheetGrid v-if="activeView.type === ViewTypes.GRID" :ref="el" />
<SmartsheetGrid v-if="isGrid" :ref="el" />
<SmartsheetGallery v-else-if="activeView.type === ViewTypes.GALLERY" />
<SmartsheetGallery v-else-if="isGallery" />
<SmartsheetForm v-else-if="activeView.type === ViewTypes.FORM" />
<SmartsheetForm v-else-if="isForm" />
</div>
</div>

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

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListItems from './components/ListItems.vue'
import { useProvideLTARStore } from '#imports'
@ -15,7 +16,7 @@ const localState = null
const listItemsDlg = ref(false)
const { relatedTableMeta, loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Required<ColumnType>,
column as Ref<Required<ColumnType>>,
row,
() => reloadTrigger?.trigger(),
)

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

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue'
@ -17,7 +18,7 @@ const listItemsDlg = ref(false)
const childListDlg = ref(false)
const { relatedTableMeta, loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Required<ColumnType>,
column as Ref<Required<ColumnType>>,
row,
() => reloadTrigger?.trigger(),
)

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

@ -1,5 +1,6 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue'
import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue'
@ -17,7 +18,7 @@ const listItemsDlg = ref(false)
const childListDlg = ref(false)
const { relatedTableMeta, loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Required<ColumnType>,
column as Ref<Required<ColumnType>>,
row,
() => reloadTrigger?.trigger(),
)

11
packages/nc-gui-v2/composables/useColumn.ts

@ -1,16 +1,17 @@
import type { ColumnType } from 'nocodb-sdk'
import { SqlUiFactory, UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useProject } from '#imports'
export function useColumn(column: ColumnType) {
export function useColumn(column: Ref<ColumnType>) {
const { project } = useProject()
const uiDatatype: UITypes = (column && column.uidt) as UITypes
const abstractType = isVirtualCol(column)
const uiDatatype: UITypes = column?.value?.uidt as UITypes
const abstractType = isVirtualCol(column?.value)
? null
: SqlUiFactory.create(project.value?.bases?.[0]?.config || { client: 'mysql2' }).getAbstractType(column)
: SqlUiFactory.create(project.value?.bases?.[0]?.config || { client: 'mysql2' }).getAbstractType(column?.value)
const dataTypeLow = column && column.dt && column.dt.toLowerCase()
const dataTypeLow = column?.value?.dt?.toLowerCase()
const isBoolean = abstractType === 'boolean'
const isString = abstractType === 'string'
const isTextArea = uiDatatype === UITypes.LongText

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

@ -20,205 +20,207 @@ const useForm = Form.useForm
const columnToValidate = [UITypes.Email, UITypes.URL, UITypes.PhoneNumber]
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState((meta: Ref<TableType>, column?: ColumnType) => {
const { sqlUi } = useProject()
const { $api } = useNuxtApp()
const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState(
(meta: Ref<TableType>, column?: Ref<ColumnType>) => {
const { sqlUi } = useProject()
const { $api } = useNuxtApp()
const toast = useToast()
const idType = null
// state
// todo: give proper type - ColumnType
const formState = ref<Partial<Record<string, any>>>({
title: 'title',
uidt: UITypes.SingleLineText,
...(column?.value || {}),
// todo: swagger json update - include meta
meta: (column?.value as any)?.meta || {},
})
const additionalValidations = ref<Record<string, any>>({})
const validators = computed(() => {
return {
column_name: [
{
required: true,
message: 'Column name is required',
},
// validation for unique column name
{
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
if (
meta.value?.columns?.some(
(c) =>
c.id !== formState.value.id && // ignore current column
// compare against column_name and title
((value || '').toLowerCase() === (c.column_name || '').toLowerCase() ||
(value || '').toLowerCase() === (c.title || '').toLowerCase()),
)
) {
return reject(new Error('Duplicate column name'))
}
resolve()
})
},
},
],
uidt: [
{
required: true,
message: 'UI Datatype is required',
},
],
...(additionalValidations?.value || {}),
}
})
const toast = useToast()
const { resetFields, validate, validateInfos } = useForm(formState, validators)
const idType = null
const setAdditionalValidations = (validations: Record<string, any>) => {
additionalValidations.value = validations
}
// state
// todo: give proper type - ColumnType
const formState = ref<Partial<Record<string, any>>>({
title: 'title',
uidt: UITypes.SingleLineText,
...(column || {}),
// todo: swagger json update - include meta
meta: (column as any)?.meta || {},
})
// actions
const generateNewColumnMeta = () => {
setAdditionalValidations({})
formState.value = { meta: {}, ...sqlUi.value.getNewColumn((meta.value?.columns?.length || 0) + 1) }
}
const additionalValidations = ref<Record<string, any>>({})
const onUidtOrIdTypeChange = () => {
const { isCurrency } = useColumn(ref(formState.value as ColumnType))
const colProp = sqlUi?.value.getDataTypeForUiType(formState?.value as any, idType as any)
formState.value = {
...formState.value,
meta: {},
rqd: false,
pk: false,
ai: false,
cdf: null,
un: false,
dtx: 'specificType',
...colProp,
}
const validators = computed(() => {
return {
column_name: [
{
required: true,
message: 'Column name is required',
},
// validation for unique column name
{
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
if (
meta.value?.columns?.some(
(c) =>
c.id !== formState.value.id && // ignore current column
// compare against column_name and title
((value || '').toLowerCase() === (c.column_name || '').toLowerCase() ||
(value || '').toLowerCase() === (c.title || '').toLowerCase()),
)
) {
return reject(new Error('Duplicate column name'))
}
resolve()
})
},
},
],
uidt: [
{
required: true,
message: 'UI Datatype is required',
},
],
...(additionalValidations?.value || {}),
}
})
const { resetFields, validate, validateInfos } = useForm(formState, validators)
const setAdditionalValidations = (validations: Record<string, any>) => {
additionalValidations.value = validations
}
// actions
const generateNewColumnMeta = () => {
setAdditionalValidations({})
formState.value = { meta: {}, ...sqlUi.value.getNewColumn((meta.value?.columns?.length || 0) + 1) }
}
const onUidtOrIdTypeChange = () => {
const { isCurrency } = useColumn(formState.value as ColumnType)
const colProp = sqlUi?.value.getDataTypeForUiType(formState?.value as any, idType as any)
formState.value = {
...formState.value,
meta: {},
rqd: false,
pk: false,
ai: false,
cdf: null,
un: false,
dtx: 'specificType',
...colProp,
}
formState.value.dtxp = sqlUi.value.getDefaultLengthForDatatype(formState.value.dt)
formState.value.dtxs = sqlUi.value.getDefaultScaleForDatatype(formState.value.dt)
formState.value.dtxp = sqlUi.value.getDefaultLengthForDatatype(formState.value.dt)
formState.value.dtxs = sqlUi.value.getDefaultScaleForDatatype(formState.value.dt)
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect]
if (column && selectTypes.includes(formState.value.uidt) && selectTypes.includes(column?.value?.uidt as UITypes)) {
formState.value.dtxp = column?.value?.dtxp
}
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect]
if (column && selectTypes.includes(formState.value.uidt) && selectTypes.includes(column.uidt as UITypes)) {
formState.value.dtxp = column.dtxp
}
if (columnToValidate.includes(formState.value.uidt)) {
formState.value.meta = {
validate: formState.value.meta && formState.value.meta.validate,
}
}
if (columnToValidate.includes(formState.value.uidt)) {
formState.value.meta = {
validate: formState.value.meta && formState.value.meta.validate,
if (isCurrency) {
if (column?.value?.uidt === UITypes.Currency) {
formState.value.dtxp = column.value.dtxp
formState.value.dtxs = column.value.dtxs
} else {
formState.value.dtxp = 19
formState.value.dtxs = 2
}
}
formState.value.altered = formState.value.altered || 2
}
if (isCurrency) {
if (column?.uidt === UITypes.Currency) {
formState.value.dtxp = column.dtxp
formState.value.dtxs = column.dtxs
} else {
formState.value.dtxp = 19
formState.value.dtxs = 2
const onDataTypeChange = () => {
const { isCurrency } = useColumn(ref(formState.value as ColumnType))
formState.value.rqd = false
if (formState.value.uidt !== UITypes.ID) {
formState.value.primaryKey = false
}
}
formState.value.ai = false
formState.value.cdf = null
formState.value.un = false
formState.value.dtxp = sqlUi.value.getDefaultLengthForDatatype(formState.value.dt)
formState.value.dtxs = sqlUi.value.getDefaultScaleForDatatype(formState.value.dt)
formState.value.altered = formState.value.altered || 2
}
formState.value.dtx = 'specificType'
const onDataTypeChange = () => {
const { isCurrency } = useColumn(formState.value as ColumnType)
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect]
if (column?.value && selectTypes.includes(formState.value.uidt) && selectTypes.includes(column?.value.uidt as UITypes)) {
formState.value.dtxp = column?.value.dtxp
}
formState.value.rqd = false
if (formState.value.uidt !== UITypes.ID) {
formState.value.primaryKey = false
}
formState.value.ai = false
formState.value.cdf = null
formState.value.un = false
formState.value.dtxp = sqlUi.value.getDefaultLengthForDatatype(formState.value.dt)
formState.value.dtxs = sqlUi.value.getDefaultScaleForDatatype(formState.value.dt)
if (isCurrency) {
if (column?.value?.uidt === UITypes.Currency) {
formState.value.dtxp = column.value.dtxp
formState.value.dtxs = column.value.dtxs
} else {
formState.value.dtxp = 19
formState.value.dtxs = 2
}
}
formState.value.dtx = 'specificType'
// this.$set(formState.value, 'uidt', sqlUi.value.getUIType(formState.value));
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect]
if (column && selectTypes.includes(formState.value.uidt) && selectTypes.includes(column.uidt as UITypes)) {
formState.value.dtxp = column.dtxp
formState.value.altered = formState.value.altered || 2
}
if (isCurrency) {
if (column?.uidt === UITypes.Currency) {
formState.value.dtxp = column.dtxp
formState.value.dtxs = column.dtxs
} else {
formState.value.dtxp = 19
formState.value.dtxs = 2
}
const onAlter = (val = 2, cdf = false) => {
formState.value.altered = formState.value.altered || val
if (cdf) formState.value.cdf = formState.value.cdf || null
}
// this.$set(formState.value, 'uidt', sqlUi.value.getUIType(formState.value));
formState.value.altered = formState.value.altered || 2
}
const onAlter = (val = 2, cdf = false) => {
formState.value.altered = formState.value.altered || val
if (cdf) formState.value.cdf = formState.value.cdf || null
}
const addOrUpdate = async (onSuccess: () => {}) => {
try {
console.log(formState, validators)
if (!(await validate())) return
formState.value.table_name = meta.value.table_name
formState.value.title = formState.value.column_name
if (column) {
await $api.dbTableColumn.update(column.id as string, formState.value)
toast.success('Column updated')
} else {
// todo : set additional meta for auto generated string id
if (formState.value.uidt === UITypes.ID) {
// based on id column type set autogenerated meta prop
// if (isAutoGenId) {
// this.newColumn.meta = {
// ag: 'nc',
// };
// }
const addOrUpdate = async (onSuccess: () => {}) => {
try {
console.log(formState, validators)
if (!(await validate())) return
formState.value.table_name = meta.value.table_name
formState.value.title = formState.value.column_name
if (column?.value) {
await $api.dbTableColumn.update(column?.value?.id as string, formState.value)
toast.success('Column updated')
} else {
// todo : set additional meta for auto generated string id
if (formState.value.uidt === UITypes.ID) {
// based on id column type set autogenerated meta prop
// if (isAutoGenId) {
// this.newColumn.meta = {
// ag: 'nc',
// };
// }
}
await $api.dbTableColumn.create(meta.value.id as string, formState.value)
toast.success('Column created')
}
await $api.dbTableColumn.create(meta.value.id as string, formState.value)
toast.success('Column created')
onSuccess()
} catch (e: any) {
const error = await extractSdkResponseErrorMsg(e)
if (error) toast.error(await extractSdkResponseErrorMsg(e))
}
onSuccess()
} catch (e: any) {
const error = await extractSdkResponseErrorMsg(e)
if (error) toast.error(await extractSdkResponseErrorMsg(e))
}
}
return {
formState,
resetFields,
validate,
validateInfos,
setAdditionalValidations,
onUidtOrIdTypeChange,
sqlUi,
onDataTypeChange,
onAlter,
addOrUpdate,
generateNewColumnMeta,
isEdit: !!column?.id,
column,
}
})
return {
formState,
resetFields,
validate,
validateInfos,
setAdditionalValidations,
onUidtOrIdTypeChange,
sqlUi,
onDataTypeChange,
onAlter,
addOrUpdate,
generateNewColumnMeta,
isEdit: computed(() => !!column?.value?.id),
column,
}
},
)
export { useProvideColumnCreateStore }

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

@ -13,7 +13,7 @@ interface DataApiResponse {
/** Store for managing Link to another cells */
const [useProvideLTARStore, useLTARStore] = useInjectionState(
(column: Required<ColumnType>, row?: Ref<Row>, reloadData = () => {}) => {
(column: Ref<Required<ColumnType>>, row?: Ref<Row>, reloadData = () => {}) => {
// state
const { metas, getMeta } = useMetas()
const { project } = useProject()
@ -31,12 +31,12 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
size: 10,
})
const colOptions = column.colOptions as LinkToAnotherRecordType
const colOptions = $computed(() => column?.value.colOptions as LinkToAnotherRecordType)
// getters
const meta = computed(() => metas?.value?.[column.fk_model_id as string])
const meta = computed(() => metas?.value?.[column?.value?.fk_model_id as string])
const relatedTableMeta = computed<TableType>(() => {
return metas.value?.[(column.colOptions as any)?.fk_related_model_id as string]
return metas.value?.[colOptions?.fk_related_model_id as string]
})
const rowId = computed(() =>
@ -72,8 +72,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
project.value.id as string,
meta.value.id,
rowId.value,
(column.colOptions as LinkToAnotherRecordType).type as 'mm' | 'hm',
column.title,
colOptions.type as 'mm' | 'hm',
column?.value?.title,
// todo: swagger type correction
{
limit: childrenExcludedListPagination.size,
@ -99,7 +99,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
meta.value.id,
rowId.value,
colOptions.type as 'mm' | 'hm',
column.title,
column?.value?.title,
// todo: swagger type correction
{
limit: childrenListPagination.size,
@ -157,7 +157,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
meta.value.title,
rowId.value,
colOptions.type as 'mm' | 'hm',
column.title,
column?.value?.title,
getRelatedTableRowId(row) as string,
)
} catch (e) {
@ -195,7 +195,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
meta.value.title as string,
rowId.value,
colOptions.type as 'mm' | 'hm',
column.title,
column?.value?.title,
getRelatedTableRowId(row) as string,
)
} catch (e) {

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

@ -1,11 +1,12 @@
import { computed } from '@vue/reactivity'
import { createInjectionState } from '@vueuse/core'
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'
const [useProvideSmartsheetStore, useSmartsheetStore] = createInjectionState((view: Ref<ViewType>, meta: Ref<TableType>) => {
const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState((view: Ref<ViewType>, meta: Ref<TableType>) => {
const { $api } = useNuxtApp()
const { sqlUi } = useProject()
@ -18,6 +19,10 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = createInjectionState((vi
// getters
const isLocked = computed(() => (view?.value as any)?.lock_type === 'locked')
const isPkAvail = computed(() => meta?.value?.columns?.some((c) => c.pk))
const isGrid = computed(() => (view?.value as any)?.type === ViewTypes.GRID)
const isForm = computed(() => (view?.value as any)?.type === ViewTypes.FORM)
const isGallery = computed(() => (view?.value as any)?.type === ViewTypes.GALLERY)
const xWhere = computed(() => {
let where
const col = meta?.value?.columns?.find(({ id }) => id === search.field) || meta?.value?.columns?.find((v) => v.pv)
@ -41,8 +46,12 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = createInjectionState((vi
$api,
search,
xWhere,
isPkAvail,
isForm,
isGrid,
isGallery,
}
})
}, 'smartsheet-store')
export { useProvideSmartsheetStore }

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

@ -18,15 +18,15 @@ export interface TabItem {
function getPredicate(key: Partial<TabItem>) {
return (tab: TabItem) =>
(!('id' in key) || tab.id === key.id) &&
(!('title' in key) || tab.title === key.id) &&
(!('type' in key) || tab.type === key.id)
(!('title' in key) || tab.title === key.title) &&
(!('type' in key) || tab.type === key.type)
}
export function useTabs() {
const tabs = useState<TabItem[]>('tabs', () => [])
const route = useRoute()
const router = useRouter()
const { tables } = useProject()
const activeTabIndex: WritableComputedRef<number> = computed({
@ -36,6 +36,8 @@ export function useTabs() {
const id = tables.value?.find((t) => t.title === tab.title)?.id
if (!id) return -1
tab.id = id as string
let index = tabs.value.findIndex((t) => t.id === tab.id)
@ -108,7 +110,14 @@ export function useTabs() {
const updateTab = (key: number | Partial<TabItem>, newTabItemProps: Partial<TabItem>) => {
const tab = typeof key === 'number' ? tabs.value[key] : tabs.value.find(getPredicate(key))
if (tab) {
const isActive = tabs.value.indexOf(tab) === activeTabIndex.value
Object.assign(tab, newTabItemProps)
if (isActive && tab.title)
router.replace({
params: {
title: tab.title,
},
})
}
}

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

@ -99,10 +99,12 @@ export function useViewColumns(
},
set(v) {
if (view?.value?.id) {
$api.dbView.update(view.value.id, {
// todo: update swagger
show_system_fields: v,
} as any)
$api.dbView
.update(view.value.id, {
// todo: update swagger
show_system_fields: v,
} as any)
.finally(() => reloadData?.())
;(view.value as any).show_system_fields = v
}
},
@ -133,7 +135,7 @@ export function useViewColumns(
) {
return false
}
return c.show
return c.show && metaColumnById?.value?.[c.fk_column_id!]
})
?.sort((a, b) => a.order - b.order)
?.map((c) => metaColumnById?.value?.[c.fk_column_id!]) || []) as ColumnType[]

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

@ -85,7 +85,7 @@ export function useViewData(
.map((c) => row[c.title as string])
.join('___') as string
return $api.dbViewRow.update(
return await $api.dbViewRow.update(
NOCO,
project?.value.id as string,
meta?.value.id as string,

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

@ -43,19 +43,24 @@ export function useViewFilters(
}
const deleteFilter = async (filter: FilterType & { status: string }, i: number) => {
// if shared or sync permission not allowed simply remove it from array
if (shared || !isUIAllowed('filterSync')) {
const _filters = unref(filters.value)
_filters.splice(i, 1)
filters.value = _filters
} else if (filter.id) {
if (!autoApply?.value) {
filter.status = 'delete'
filters.value.splice(i, 1)
reloadData?.()
} else {
if (filter.id) {
// if auto-apply disabled mark it as disabled
if (!autoApply?.value) {
filter.status = 'delete'
// if auto-apply enabled invoke delete api and remove from array
} else {
await $api.dbTableFilter.delete(filter.id)
reloadData?.()
filters.value.splice(i, 1)
}
// if not synced yet remove it from array
} else {
await $api.dbTableFilter.delete(filter.id)
const _filters = unref(filters.value)
_filters.splice(i, 1)
filters.value = _filters
reloadData?.()
filters.value.splice(i, 1)
}
}
}

22
packages/nc-gui-v2/composables/useVirtualCell.ts

@ -1,26 +1,28 @@
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { computed } from '#imports'
export function useVirtualCell(column: ColumnType) {
export function useVirtualCell(column: Ref<ColumnType>) {
const isHm = computed(
() =>
column.uidt === UITypes.LinkToAnotherRecord && (<LinkToAnotherRecordType>column.colOptions).type === RelationTypes.HAS_MANY,
column?.value?.uidt === UITypes.LinkToAnotherRecord &&
(<LinkToAnotherRecordType>column?.value?.colOptions).type === RelationTypes.HAS_MANY,
)
const isMm = computed(
() =>
column.uidt === UITypes.LinkToAnotherRecord &&
(<LinkToAnotherRecordType>column.colOptions).type === RelationTypes.MANY_TO_MANY,
column?.value?.uidt === UITypes.LinkToAnotherRecord &&
(<LinkToAnotherRecordType>column?.value?.colOptions).type === RelationTypes.MANY_TO_MANY,
)
const isBt = computed(
() =>
column.uidt === UITypes.LinkToAnotherRecord &&
(<LinkToAnotherRecordType>column.colOptions).type === RelationTypes.BELONGS_TO,
column?.value?.uidt === UITypes.LinkToAnotherRecord &&
(<LinkToAnotherRecordType>column?.value?.colOptions).type === RelationTypes.BELONGS_TO,
)
const isLookup = computed(() => column.uidt === UITypes.Lookup)
const isRollup = computed(() => column.uidt === UITypes.Rollup)
const isFormula = computed(() => column.uidt === UITypes.Formula)
const isCount = computed(() => column.uidt === UITypes.Count)
const isLookup = computed(() => column?.value?.uidt === UITypes.Lookup)
const isRollup = computed(() => column?.value?.uidt === UITypes.Rollup)
const isFormula = computed(() => column?.value?.uidt === UITypes.Formula)
const isCount = computed(() => column?.value?.uidt === UITypes.Count)
return {
isHm,

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

@ -8,7 +8,7 @@ import type { TabItem } from '~/composables/useTabs'
export const EditEnabledInj: InjectionKey<boolean> = Symbol('edit-enabled')
export const ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell')
export const RowInj: InjectionKey<Ref<Row>> = Symbol('row')
export const ColumnInj: InjectionKey<ColumnType & { meta: any }> = Symbol('column-injection')
export const ColumnInj: InjectionKey<Ref<ColumnType & { meta: any }>> = Symbol('column-injection')
export const MetaInj: InjectionKey<ComputedRef<TableType>> = Symbol('meta-injection')
export const TabMetaInj: InjectionKey<ComputedRef<TabItem>> = Symbol('tab-meta-injection')
export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> =

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

@ -4,9 +4,10 @@ import { TabType } from '~/composables'
const route = useRoute()
const { loadProject, loadTables } = useProject(route.params.projectId as string)
const { addTab } = useTabs()
const { addTab, clearTabs } = useTabs()
const { $state } = useNuxtApp()
clearTabs()
if (!route.params.type) {
addTab({ type: TabType.AUTH, title: 'Team & Auth' })
}

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

@ -162,4 +162,8 @@ function openQuickImportDialog(type: string) {
:deep(.ant-menu-submenu::after) {
@apply !border-none;
}
:deep(.ant-tabs-nav-add) {
@apply !hidden;
}
</style>

5
scripts/sdk/swagger.json

@ -6778,7 +6778,7 @@
}
},
"Formula": {
"title": "Lookup",
"title": "Formula",
"type": "object",
"properties": {
"id": {
@ -6796,6 +6796,9 @@
"formula": {
"type": "string"
},
"formula_raw": {
"type": "string"
},
"deleted": {
"type": "string"
},

Loading…
Cancel
Save