Browse Source

Merge pull request #3044 from nocodb/chore/type-issues

pull/3056/head
Braks 2 years ago committed by GitHub
parent
commit
2f8f0fcbb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      packages/nc-gui-v2/.eslintrc.js
  2. 27
      packages/nc-gui-v2/components.d.ts
  3. 16
      packages/nc-gui-v2/components/cell/Currency.vue
  4. 9
      packages/nc-gui-v2/components/cell/Decimal.vue
  5. 9
      packages/nc-gui-v2/components/cell/Email.vue
  6. 3
      packages/nc-gui-v2/components/cell/Float.vue
  7. 3
      packages/nc-gui-v2/components/cell/Integer.vue
  8. 55
      packages/nc-gui-v2/components/cell/Json.vue
  9. 28
      packages/nc-gui-v2/components/cell/Rating.vue
  10. 8
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  11. 3
      packages/nc-gui-v2/components/cell/Text.vue
  12. 12
      packages/nc-gui-v2/components/cell/TextArea.vue
  13. 11
      packages/nc-gui-v2/components/cell/Url.vue
  14. 5
      packages/nc-gui-v2/components/cell/attachment/Modal.vue
  15. 14
      packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue
  16. 11
      packages/nc-gui-v2/components/general/Language.vue
  17. 10
      packages/nc-gui-v2/components/smartsheet-column/AdvancedOptions.vue
  18. 2
      packages/nc-gui-v2/components/smartsheet-column/CheckboxOptions.vue
  19. 38
      packages/nc-gui-v2/components/smartsheet-column/FormulaOptions.vue
  20. 7
      packages/nc-gui-v2/components/smartsheet-column/LinkedToAnotherRecordOptions.vue
  21. 21
      packages/nc-gui-v2/components/smartsheet-column/LookupOptions.vue
  22. 28
      packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue
  23. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  24. 54
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  25. 23
      packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue
  26. 11
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  27. 8
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  28. 5
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/DeleteTable.vue
  29. 11
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue
  30. 9
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/Reload.vue
  31. 49
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  32. 25
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  33. 12
      packages/nc-gui-v2/components/virtual-cell/Formula.vue
  34. 40
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  35. 23
      packages/nc-gui-v2/components/virtual-cell/Lookup.vue
  36. 43
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  37. 5
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  38. 23
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  39. 13
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  40. 94
      packages/nc-gui-v2/components/webhook/Editor.vue
  41. 8
      packages/nc-gui-v2/components/webhook/List.vue
  42. 3
      packages/nc-gui-v2/composables/useColumnCreateStore.ts
  43. 4
      packages/nc-gui-v2/composables/useLTARStore.ts
  44. 66
      packages/nc-gui-v2/composables/useTableCreate.ts
  45. 667
      packages/nc-gui-v2/package-lock.json
  46. 2
      packages/nc-gui-v2/package.json
  47. 4
      packages/nc-gui-v2/vue-color-shims.d.ts

5
packages/nc-gui-v2/.eslintrc.js

@ -2,6 +2,11 @@ const baseRules = {
'vue/no-setup-props-destructure': 0, 'vue/no-setup-props-destructure': 0,
'no-console': 0, 'no-console': 0,
'antfu/if-newline': 0, 'antfu/if-newline': 0,
'no-unused-vars': 0,
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
],
'prettier/prettier': ['error', {}, { usePrettierrc: true }], 'prettier/prettier': ['error', {}, { usePrettierrc: true }],
} }

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

@ -61,12 +61,39 @@ declare module '@vue/runtime-core' {
ATooltip: typeof import('ant-design-vue/es')['Tooltip'] ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle'] ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
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']
MaterialSymbolsAttachFile: typeof import('~icons/material-symbols/attach-file')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsMenu: typeof import('~icons/material-symbols/menu')['default'] MaterialSymbolsMenu: typeof import('~icons/material-symbols/menu')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiAt: typeof import('~icons/mdi/at')['default'] MdiAt: typeof import('~icons/mdi/at')['default']
MdiCloseCircle: typeof import('~icons/mdi/close-circle')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default'] MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiDownload: typeof import('~icons/mdi/download')['default']
MdiDrag: typeof import('~icons/mdi/drag')['default']
MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
MdiLogout: typeof import('~icons/mdi/logout')['default'] MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiOpenInNew: typeof import('~icons/mdi/open-in-new')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiReload: typeof import('~icons/mdi/reload')['default'] MdiReload: typeof import('~icons/mdi/reload')['default']
MdiStar: typeof import('~icons/mdi/star')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default'] MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']
MdiXml: typeof import('~icons/mdi/xml')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
} }

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

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref, useVModel } from '#imports' import { computed, inject, ref, useVModel } from '#imports'
import { ColumnInj, EditModeInj } from '~/context' import { ColumnInj, EditModeInj } from '~/context'
@ -10,35 +11,34 @@ const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj) const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(EditModeInj, ref(false))
const root = ref<HTMLInputElement>()
const vModel = useVModel(props, 'modelValue', emit) const vModel = useVModel(props, 'modelValue', emit)
const currencyMeta = computed(() => { const currencyMeta = computed(() => {
return { return {
currency_locale: 'en-US', currency_locale: 'en-US',
currency_code: 'USD', currency_code: 'USD',
...(column?.value?.meta ? column?.value?.meta : {}), ...(column.value.meta ? column.value.meta : {}),
} }
}) })
const currency = computed(() => { const currency = computed(() => {
try { try {
return isNaN(vModel.value) return !vModel.value || isNaN(vModel.value)
? vModel.value ? vModel.value
: new Intl.NumberFormat(currencyMeta?.value?.currency_locale || 'en-US', { : new Intl.NumberFormat(currencyMeta.value.currency_locale || 'en-US', {
style: 'currency', style: 'currency',
currency: currencyMeta?.value?.currency_code || 'USD', currency: currencyMeta.value.currency_code || 'USD',
}).format(vModel.value) }).format(vModel.value)
} catch (e) { } catch (e) {
return vModel.value return vModel.value
} }
}) })
const focus = (el: HTMLInputElement) => el?.focus() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script> </script>
<template> <template>

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

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, ref } from '#imports' import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports'
import { EditModeInj } from '~/context' import { EditModeInj } from '~/context'
interface Props { interface Props {
@ -14,13 +15,11 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const editEnabled = inject<boolean>(EditModeInj) const editEnabled = inject(EditModeInj, ref(false))
const root = ref<HTMLInputElement>()
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
const focus = (el: HTMLInputElement) => el?.focus() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script> </script>
<template> <template>

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

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, useVModel } from '#imports' import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref, useVModel } from '#imports'
import { isEmail } from '~/utils' import { isEmail } from '~/utils'
import { EditModeInj } from '~/context' import { EditModeInj } from '~/context'
@ -19,13 +20,13 @@ const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
const validEmail = computed(() => isEmail(vModel.value)) const validEmail = computed(() => vModel.value && isEmail(vModel.value))
const focus = (el: HTMLInputElement) => el?.focus() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script> </script>
<template> <template>
<input v-if="editEnabled" ref="root" v-model="vModel" class="outline-none prose-sm" @blur="editEnabled = false" /> <input v-if="editEnabled" :ref="focus" 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"> <a v-else-if="validEmail" class="prose-sm underline hover:opacity-75" :href="`mailto:${vModel}`" target="_blank">
{{ vModel }} {{ vModel }}
</a> </a>

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

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports' import { inject, ref, useVModel } from '#imports'
import { EditModeInj } from '~/context' import { EditModeInj } from '~/context'
@ -18,7 +19,7 @@ const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
const focus = (el: HTMLInputElement) => el?.focus() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script> </script>
<template> <template>

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

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports' import { inject, ref, useVModel } from '#imports'
import { EditModeInj } from '~/context' import { EditModeInj } from '~/context'
@ -18,7 +19,7 @@ const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
const focus = (el: HTMLInputElement) => el?.focus() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
function onKeyDown(evt: KeyboardEvent) { function onKeyDown(evt: KeyboardEvent) {
return evt.key === '.' && evt.preventDefault() return evt.key === '.' && evt.preventDefault()

55
packages/nc-gui-v2/components/cell/Json.vue

@ -1,9 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Modal as AModal } from 'ant-design-vue' import { Modal as AModal } from 'ant-design-vue'
import Editor from '~/components/monaco/Editor.vue' import Editor from '~/components/monaco/Editor.vue'
import FullScreenIcon from '~icons/cil/fullscreen' import { computed, inject, ref, useVModel, watch } from '#imports'
import FullScreenExitIcon from '~icons/cil/fullscreen-exit'
import { inject } from '#imports'
import { EditModeInj } from '~/context' import { EditModeInj } from '~/context'
interface Props { interface Props {
@ -20,27 +18,29 @@ const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(EditModeInj, ref(false))
let vModel = $(useVModel(props, 'modelValue', emits)) const vModel = useVModel(props, 'modelValue', emits)
let localValueState = $ref<string | undefined>(undefined) const localValueState = ref<string | undefined>()
let localValue = $(
computed<string | undefined>({ const localValue = computed<string | Record<string, any> | undefined>({
get: () => localValueState, get: () => localValueState.value,
set: (val: undefined | string | Record<string, any>) => { set: (val: undefined | string | Record<string, any>) => {
localValueState = typeof val === 'object' ? JSON.stringify(val, null, 2) : val localValueState.value = typeof val === 'object' ? JSON.stringify(val, null, 2) : val
}, },
}), })
)
let error = $ref<string | undefined>()
let error = $ref<string | undefined>(undefined)
let isExpanded = $ref(false) let isExpanded = $ref(false)
const clear = () => { const clear = () => {
error = undefined error = undefined
isExpanded = false isExpanded = false
editEnabled.value = false editEnabled.value = false
localValue = vModel localValue.value = vModel.value
} }
const formatJson = (json: string) => { const formatJson = (json: string) => {
@ -53,22 +53,26 @@ const formatJson = (json: string) => {
const onSave = () => { const onSave = () => {
isExpanded = false isExpanded = false
editEnabled.value = false editEnabled.value = false
localValue = localValue ? formatJson(localValue) : localValue
vModel = localValue localValue.value = localValue ? formatJson(localValue.value as string) : localValue
vModel.value = localValue.value
} }
watch( watch(
$$(vModel), vModel,
(val) => { (val) => {
localValue = val localValue.value = val
}, },
{ immediate: true }, { immediate: true },
) )
watch($$(localValue), (val) => { watch(localValue, (val) => {
try { try {
JSON.parse(val) JSON.parse(val as string)
error = undefined error = undefined
} catch (e: any) { } catch (e: any) {
error = e error = e
@ -77,7 +81,8 @@ watch($$(localValue), (val) => {
watch(editEnabled, () => { watch(editEnabled, () => {
isExpanded = false isExpanded = false
localValue = vModel
localValue.value = vModel.value
}) })
</script> </script>
@ -86,16 +91,20 @@ watch(editEnabled, () => {
<div v-if="editEnabled" class="flex flex-col w-full"> <div v-if="editEnabled" class="flex flex-col w-full">
<div class="flex flex-row justify-between pt-1 pb-2"> <div class="flex flex-row justify-between pt-1 pb-2">
<a-button type="text" size="small" @click="isExpanded = !isExpanded"> <a-button type="text" size="small" @click="isExpanded = !isExpanded">
<FullScreenExitIcon v-if="isExpanded" class="h-2.5" /> <CilFullscreenExit v-if="isExpanded" class="h-2.5" />
<FullScreenIcon v-else class="h-2.5" />
<CilFullscreen v-else class="h-2.5" />
</a-button> </a-button>
<div class="flex flex-row"> <div class="flex flex-row">
<a-button type="text" size="small" :onclick="clear"><div class="text-xs">Cancel</div></a-button> <a-button type="text" size="small" :onclick="clear"><div class="text-xs">Cancel</div></a-button>
<a-button type="primary" size="small" :disabled="!!error || localValue === vModel"> <a-button type="primary" size="small" :disabled="!!error || localValue === vModel">
<div class="text-xs" :onclick="onSave">Save</div> <div class="text-xs" :onclick="onSave">Save</div>
</a-button> </a-button>
</div> </div>
</div> </div>
<Editor <Editor
:model-value="localValue" :model-value="localValue"
class="min-w-full w-80" class="min-w-full w-80"
@ -104,10 +113,12 @@ watch(editEnabled, () => {
:disable-deep-compare="true" :disable-deep-compare="true"
@update:model-value="localValue = $event" @update:model-value="localValue = $event"
/> />
<span v-if="error" class="text-xs w-full py-1 text-red-500"> <span v-if="error" class="text-xs w-full py-1 text-red-500">
{{ error.toString() }} {{ error.toString() }}
</span> </span>
</div> </div>
<span v-else>{{ vModel }}</span> <span v-else>{{ vModel }}</span>
</component> </component>
</template> </template>

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

@ -1,22 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject } from '#imports' import { computed, inject } from '#imports'
import { ColumnInj } from '~/context' import { ColumnInj } from '~/context'
import MdiStarIcon from '~icons/mdi/star'
import MdiHeartIcon from '~icons/mdi/heart'
import MdiMoonFullIcon from '~icons/mdi/moon-full'
import MdiThumbUpIcon from '~icons/mdi/thumb-up'
import MdiFlagIcon from '~icons/mdi/flag'
interface Props { interface Props {
modelValue?: number | null modelValue?: number | null
readOnly?: boolean readOnly?: boolean
} }
const props = defineProps<Props>() const { modelValue, readOnly } = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const column = inject(ColumnInj) const column = inject(ColumnInj)!
const ratingMeta = computed(() => { const ratingMeta = computed(() => {
return { return {
@ -26,21 +21,24 @@ const ratingMeta = computed(() => {
}, },
color: '#fcb401', color: '#fcb401',
max: 5, max: 5,
...(column?.value?.meta || {}), ...(column.value?.meta || {}),
} }
}) })
const vModel = useVModel(props, 'modelValue', emits) const vModel = computed({
get: () => modelValue ?? NaN,
set: (val) => emits('update:modelValue', val),
})
</script> </script>
<template> <template>
<a-rate v-model:value="vModel" :count="ratingMeta.max" :style="`color: ${ratingMeta.color}`" :disabled="props.readOnly"> <a-rate v-model:value="vModel" :count="ratingMeta.max" :style="`color: ${ratingMeta.color}`" :disabled="readOnly">
<template #character> <template #character>
<MdiStarIcon v-if="ratingMeta.icon.full === 'mdi-star'" class="text-sm" /> <MdiStar v-if="ratingMeta.icon.full === 'mdi-star'" class="text-sm" />
<MdiHeartIcon v-if="ratingMeta.icon.full === 'mdi-heart'" class="text-sm" /> <MdiHeart v-if="ratingMeta.icon.full === 'mdi-heart'" class="text-sm" />
<MdiMoonFullIcon v-if="ratingMeta.icon.full === 'mdi-moon-full'" class="text-sm" /> <MdiMoonFull v-if="ratingMeta.icon.full === 'mdi-moon-full'" class="text-sm" />
<MdiThumbUpIcon v-if="ratingMeta.icon.full === 'mdi-thumb-up'" class="text-sm" /> <MdiThumbUp v-if="ratingMeta.icon.full === 'mdi-thumb-up'" class="text-sm" />
<MdiFlagIcon v-if="ratingMeta.icon.full === 'mdi-flag'" class="text-sm" /> <MdiFlag v-if="ratingMeta.icon.full === 'mdi-flag'" class="text-sm" />
</template> </template>
</a-rate> </a-rate>
</template> </template>

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

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject } from '#imports' import { computed, inject } from '#imports'
import { ColumnInj, EditModeInj } from '~/context' import { ColumnInj } from '~/context'
interface Props { interface Props {
modelValue: string | null modelValue: string | null
@ -10,16 +10,14 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) 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 vModel = computed({ const vModel = computed({
get: () => modelValue?.replace(/\\'/g, "'").replace(/^'|'$/g, ''), get: () => modelValue?.replace(/\\'/g, "'").replace(/^'|'$/g, ''),
set: (val) => emit('update:modelValue', val), set: (val) => emit('update:modelValue', val),
}) })
const options = computed(() => column?.value?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || []) const options = computed(() => column.value.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || [])
</script> </script>
<template> <template>

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

@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports' import { inject, ref, useVModel } from '#imports'
import { EditModeInj } from '~/context' import { EditModeInj } from '~/context'
@ -14,7 +15,7 @@ const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
const focus = (el: HTMLInputElement) => el?.focus() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script> </script>
<template> <template>

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

@ -1,20 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject, ref, useVModel } from '#imports' import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref } from '#imports'
import { EditModeInj } from '~/context' import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: string | null modelValue: string | null
} }
const props = defineProps<Props>() const { modelValue } = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emits) const vModel = computed({
get: () => modelValue ?? '',
set: (value) => emits('update:modelValue', value),
})
const focus = (el: HTMLTextAreaElement) => el?.focus() const focus: VNodeRef = (el) => (el as HTMLTextAreaElement)?.focus()
</script> </script>
<template> <template>

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

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, onMounted, ref } from '#imports' import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref } from '#imports'
import { ColumnInj, EditModeInj } from '~/context' import { ColumnInj, EditModeInj } from '~/context'
import { isValidURL } from '~/utils' import { isValidURL } from '~/utils'
@ -11,14 +12,14 @@ const { modelValue: value } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj) const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(EditModeInj, ref(false))
const vModel = computed({ const vModel = computed({
get: () => value, get: () => value,
set: (val) => { set: (val) => {
if (!column?.value?.meta?.validate || isValidURL(val)) { if (!column.value.meta?.validate || (val && isValidURL(val))) {
emit('update:modelValue', val) emit('update:modelValue', val)
} }
}, },
@ -26,12 +27,12 @@ const vModel = computed({
const isValid = computed(() => value && isValidURL(value)) const isValid = computed(() => value && isValidURL(value))
const focus = (el: HTMLInputElement) => el?.focus() const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</script> </script>
<template> <template>
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none" @blur="editEnabled = false" /> <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> <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> <span v-else>{{ value }}</span>
</template> </template>

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

@ -4,11 +4,6 @@ import { useAttachmentCell } from './utils'
import { useSortable } from './sort' import { useSortable } from './sort'
import { ref, useDropZone, useUIPermission } from '#imports' import { ref, useDropZone, useUIPermission } from '#imports'
import { isImage, openLink } from '~/utils' import { isImage, openLink } from '~/utils'
import MaterialSymbolsAttachFile from '~icons/material-symbols/attach-file'
import MdiCloseCircle from '~icons/mdi/close-circle'
import MdiDownload from '~icons/mdi/download'
import MaterialSymbolsFileCopyOutline from '~icons/material-symbols/file-copy-outline'
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file'
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()

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

@ -1,18 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { viewIcons } from '~/utils/viewUtils' import { viewIcons } from '~/utils'
import { h, useNuxtApp, useProject } from '#imports' import { computed, h, useNuxtApp, useProject } from '#imports'
import MdiReload from '~icons/mdi/reload'
import MdiContentSave from '~icons/mdi/content-save'
import MdiMagnify from '~icons/mdi/magnify'
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { project } = useProject() const { project } = useProject()
const toast = useToast() const toast = useToast()
const roles = $ref<string[]>(['editor', 'commenter', 'viewer']) const roles = $ref<string[]>(['editor', 'commenter', 'viewer'])
let isLoading = $ref(false) let isLoading = $ref(false)
let tables = $ref<any[]>([]) let tables = $ref<any[]>([])
const searchInput = $ref('') const searchInput = $ref('')
const filteredTables = computed(() => const filteredTables = computed(() =>
@ -30,7 +32,7 @@ async function loadTableList() {
isLoading = true isLoading = true
// TODO includeM2M // TODO includeM2M
tables = await $api.project.modelVisibilityList(project.value?.id, { tables = await $api.project.modelVisibilityList(project.value?.id, {
includeM2M: '', includeM2M: true,
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)

11
packages/nc-gui-v2/components/general/Language.vue

@ -1,15 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import MaterialSymbolsTranslate from '~icons/material-symbols/translate' import { Language } from '~/lib'
import { Language } from '~/lib/enums' import { useNuxtApp } from '#imports'
const { $e, $state } = useNuxtApp() const { $e, $state } = useNuxtApp()
const { availableLocales = ['en'], locale } = useI18n() const { availableLocales = ['en'], locale } = useI18n()
const languages = $computed(() => { const languages = $computed(() => availableLocales.sort())
return availableLocales.sort()
})
const isRtlLang = $computed(() => ['fa'].includes($state.lang.value)) const isRtlLang = $computed(() => ['fa'].includes($state.lang.value))
@ -38,10 +36,11 @@ onMounted(() => {
<template #activator="{ props }"> <template #activator="{ props }">
<MaterialSymbolsTranslate class="md:text-xl cursor-pointer nc-menu-translate" @click="props.onClick" /> <MaterialSymbolsTranslate class="md:text-xl cursor-pointer nc-menu-translate" @click="props.onClick" />
</template> </template>
<v-list class="scrollbar min-w-50 max-h-90vh overflow-auto !py-0 dark:(!bg-gray-800 !text-white)"> <v-list class="scrollbar min-w-50 max-h-90vh overflow-auto !py-0 dark:(!bg-gray-800 !text-white)">
<v-list-item <v-list-item
v-for="lang of languages" v-for="lang of languages"
:key="lang.value" :key="lang"
:class="lang === locale ? '!bg-primary/10 text-primary dark:(!bg-gray-700 !text-secondary)' : ''" :class="lang === locale ? '!bg-primary/10 text-primary dark:(!bg-gray-700 !text-secondary)' : ''"
class="!min-h-8 group" class="!min-h-8 group"
:value="lang" :value="lang"

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

@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useColumnCreateStoreOrThrow } from '#imports' import type { UITypes } from 'nocodb-sdk'
import { computed, useColumnCreateStoreOrThrow } from '#imports'
const { formState, validateInfos, setAdditionalValidations, sqlUi, onDataTypeChange, onAlter } = useColumnCreateStoreOrThrow() const { formState, validateInfos, sqlUi, onDataTypeChange, onAlter } = useColumnCreateStoreOrThrow()!
const dataTypes = computed(() => sqlUi?.value?.getDataTypeListForUiType(formState)) // todo: 2nd argument of `getDataTypeListForUiType` is missing!
const dataTypes = computed(() => sqlUi?.value?.getDataTypeListForUiType(formState.value as { uidt: UITypes }, '' as any))
// to avoid type error with checkbox // to avoid type error with checkbox
formState.value.rqd = !!formState.value.rqd formState.value.rqd = !!formState.value.rqd
@ -82,5 +84,3 @@ formState.value.au = !!formState.value.au
</a-form-item> </a-form-item>
</div> </div>
</template> </template>
<style scoped></style>

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

@ -3,7 +3,7 @@ import { Sketch } from '@ckpack/vue-color'
import { useColumnCreateStoreOrThrow } from '#imports' import { useColumnCreateStoreOrThrow } from '#imports'
import { enumColor, getMdiIcon } from '@/utils' import { enumColor, getMdiIcon } from '@/utils'
const { formState, validateInfos, setAdditionalValidations, sqlUi, onDataTypeChange, onAlter } = useColumnCreateStoreOrThrow() const { formState } = useColumnCreateStoreOrThrow()
// cater existing v1 cases // cater existing v1 cases
const iconList = [ const iconList = [

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

@ -4,7 +4,7 @@ import type { ListItem as AntListItem } from 'ant-design-vue'
import jsep from 'jsep' import jsep from 'jsep'
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import { UITypes, jsepCurlyHook } from 'nocodb-sdk' import { UITypes, jsepCurlyHook } from 'nocodb-sdk'
import { useColumnCreateStoreOrThrow, useDebounceFn } from '#imports' import { onMounted, useColumnCreateStoreOrThrow, useDebounceFn } from '#imports'
import { MetaInj } from '~/context' import { MetaInj } from '~/context'
import { import {
NcAutocompleteTree, NcAutocompleteTree,
@ -16,8 +16,6 @@ import {
insertAtCursor, insertAtCursor,
validateDateWithUnknownFormat, validateDateWithUnknownFormat,
} from '@/utils' } from '@/utils'
import MdiFunctionIcon from '~icons/mdi/function'
import MdiOperatorIcon from '~icons/mdi/calculator'
enum JSEPNode { enum JSEPNode {
COMPOUND = 'Compound', COMPOUND = 'Compound',
@ -31,8 +29,7 @@ enum JSEPNode {
ARRAY_EXP = 'ArrayExpression', ARRAY_EXP = 'ArrayExpression',
} }
const { formState, validateInfos, setAdditionalValidations, sqlUi, onDataTypeChange, onAlter, column } = const { formState, validateInfos, setAdditionalValidations, sqlUi, column } = useColumnCreateStoreOrThrow()
useColumnCreateStoreOrThrow()
const meta = inject(MetaInj) const meta = inject(MetaInj)
@ -72,8 +69,6 @@ const wordToComplete = ref<string | undefined>('')
const selected = ref(0) const selected = ref(0)
const tooltip = ref(true)
const sortOrder: Record<string, number> = { const sortOrder: Record<string, number> = {
column: 0, column: 0,
function: 1, function: 1,
@ -95,7 +90,7 @@ const suggestionsList = computed(() => {
...columns.value ...columns.value
.filter( .filter(
(c: Record<string, any>) => (c: Record<string, any>) =>
!column || (column.id !== c.id && !(c.uidt === UITypes.LinkToAnotherRecord && c.system === 1)), !column || (column.value.id !== c.id && !(c.uidt === UITypes.LinkToAnotherRecord && c.system === 1)),
) )
.map((c: any) => ({ .map((c: any) => ({
text: c.title, text: c.title,
@ -230,7 +225,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
} else if (parsedTree.type === JSEPNode.IDENTIFIER) { } else if (parsedTree.type === JSEPNode.IDENTIFIER) {
if ( if (
columns.value columns.value
.filter((c: Record<string, any>) => !column || column.id !== c.id) .filter((c: Record<string, any>) => !column || column.value.id !== c.id)
.every((c: Record<string, any>) => c.title !== parsedTree.name) .every((c: Record<string, any>) => c.title !== parsedTree.name)
) { ) {
errors.add(`Column '${parsedTree.name}' is not available`) errors.add(`Column '${parsedTree.name}' is not available`)
@ -241,7 +236,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
// get all formula columns excluding itself // get all formula columns excluding itself
const formulaPaths = columns.value const formulaPaths = columns.value
.filter((c: Record<string, any>) => c.id !== column?.id && c.uidt === UITypes.Formula) .filter((c: Record<string, any>) => c.id !== column?.value.id && c.uidt === UITypes.Formula)
.reduce((res: Record<string, any>[], c: Record<string, any>) => { .reduce((res: Record<string, any>[], c: Record<string, any>) => {
// in `formula`, get all the target neighbours // in `formula`, get all the target neighbours
// i.e. all column id (e.g. cl_xxxxxxxxxxxxxx) with formula type // i.e. all column id (e.g. cl_xxxxxxxxxxxxxx) with formula type
@ -256,9 +251,10 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
}, []) }, [])
// include target formula column (i.e. the one to be saved if applicable) // include target formula column (i.e. the one to be saved if applicable)
const targetFormulaCol = columns.value.find((c: ColumnType) => c.title === parsedTree.name && c.uidt === UITypes.Formula) const targetFormulaCol = columns.value.find((c: ColumnType) => c.title === parsedTree.name && c.uidt === UITypes.Formula)
if (targetFormulaCol) {
if (targetFormulaCol && column?.value.id) {
formulaPaths.push({ formulaPaths.push({
[column.id]: [targetFormulaCol.id], [column.value.id]: [targetFormulaCol.id],
}) })
} }
const vertices = formulaPaths.length const vertices = formulaPaths.length
@ -267,6 +263,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
const adj = new Map() const adj = new Map()
const inDegrees = new Map() const inDegrees = new Map()
// init adjacency list & indegree // init adjacency list & indegree
for (const [_, v] of Object.entries(formulaPaths)) { for (const [_, v] of Object.entries(formulaPaths)) {
const src = Object.keys(v)[0] const src = Object.keys(v)[0]
const neighbours = v[src] const neighbours = v[src]
@ -346,12 +343,14 @@ function validateAgainstType(parsedTree: any, expectedType: string, func: any, t
} }
} }
} else if (parsedTree.type === JSEPNode.IDENTIFIER) { } else if (parsedTree.type === JSEPNode.IDENTIFIER) {
const col = columns.value.find((c) => c.title === parsedTree.name) as Record<string, any> const col = columns.value.find((c) => c.title === parsedTree.name)
if (col === undefined) { if (col === undefined) {
return return
} }
if (col.uidt === UITypes.Formula) { if (col.uidt === UITypes.Formula) {
const foundType = getRootDataType(jsep(col?.formula_raw)) const foundType = getRootDataType(jsep((col as any).formula_raw))
if (foundType === 'N/A') { if (foundType === 'N/A') {
typeErrors.add(`Not supported to reference column ${col.title}`) typeErrors.add(`Not supported to reference column ${col.title}`)
} else if (expectedType !== foundType) { } else if (expectedType !== foundType) {
@ -594,7 +593,7 @@ function getFormulaTypeName(type: string) {
} }
// set default value // set default value
formState.value.formula_raw = (column?.colOptions as Record<string, any>)?.formula_raw || '' formState.value.formula_raw = (column?.value?.colOptions as Record<string, any>)?.formula_raw || ''
// set additional validations // set additional validations
setAdditionalValidations({ setAdditionalValidations({
@ -659,19 +658,24 @@ onMounted(() => {
<div>({{ idx + 1 }}): {{ example }}</div> <div>({{ idx + 1 }}): {{ example }}</div>
</div> </div>
</template> </template>
<template #title> <template #title>
<div class="flex"> <div class="flex">
<div class="flex-1"> <div class="flex-1">
{{ item.text }} {{ item.text }}
</div> </div>
<div class=""> <div class="">
{{ getFormulaTypeName(item.type) }} {{ getFormulaTypeName(item.type) }}
</div> </div>
</div> </div>
</template> </template>
<template #avatar> <template #avatar>
<MdiFunctionIcon v-if="item.type === 'function'" class="text-lg" /> <MdiFunction v-if="item.type === 'function'" class="text-lg" />
<MdiOperatorIcon v-if="item.type === 'op'" class="text-lg" />
<MdiCalculator v-if="item.type === 'op'" class="text-lg" />
<component :is="item.icon" v-if="item.type === 'column'" class="text-lg" /> <component :is="item.icon" v-if="item.type === 'column'" class="text-lg" />
</template> </template>
</a-list-item-meta> </a-list-item-meta>

7
packages/nc-gui-v2/components/smartsheet-column/LinkedToAnotherRecordOptions.vue

@ -1,14 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import { ModelTypes, MssqlUi, SqliteUi } from 'nocodb-sdk' import { ModelTypes, MssqlUi, SqliteUi } from 'nocodb-sdk'
import { useColumnCreateStoreOrThrow } from '#imports' import { inject, useColumnCreateStoreOrThrow, useProject } from '#imports'
import { MetaInj } from '~/context' import { MetaInj } from '~/context'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline' import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline' import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
const { formState, validateInfos, onDataTypeChange, setAdditionalValidations } = $(useColumnCreateStoreOrThrow()) const { formState, validateInfos, onDataTypeChange, setAdditionalValidations } = $(useColumnCreateStoreOrThrow())
const { tables, sqlUi } = $(useProject()) const { tables, sqlUi } = $(useProject())
const meta: TableType = $(inject(MetaInj)) const meta = $(inject(MetaInj)!)
setAdditionalValidations({ setAdditionalValidations({
childId: [{ required: true, message: 'Required' }], childId: [{ required: true, message: 'Required' }],
@ -103,5 +102,3 @@ const refTables = $computed(() => {
</div> </div>
</div> </div>
</template> </template>
<style scoped></style>

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

@ -5,7 +5,7 @@ import { MetaInj } from '~/context'
const { formState, validateInfos, onDataTypeChange, setAdditionalValidations } = $(useColumnCreateStoreOrThrow()) const { formState, validateInfos, onDataTypeChange, setAdditionalValidations } = $(useColumnCreateStoreOrThrow())
const { tables } = $(useProject()) const { tables } = $(useProject())
const meta = $(inject(MetaInj)) const meta = $(inject(MetaInj)!)
const { metas } = $(useMetas()) const { metas } = $(useMetas())
setAdditionalValidations({ setAdditionalValidations({
@ -27,14 +27,17 @@ const refTables = $computed(() => {
return [] return []
} }
return meta.columns // todo: type issues with ColumnType so we have to cast to any
.filter((c) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== 'bt' && !c.system) return (
meta.columns
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions?.type !== 'bt' && !c.system)
.map((c) => ({ .map((c) => ({
col: c.colOptions, col: c.colOptions,
column: c, column: c,
...tables.find((t) => t.id === c.colOptions.fk_related_model_id), ...tables.find((t) => t.id === (c.colOptions as any)?.fk_related_model_id),
})) }))
.filter((table) => table.col.fk_related_model_id === table.id && !table.mm) .filter((table: any) => table.col?.fk_related_model_id === table.id && !table.mm) ?? []
)
}) })
const columns = $computed(() => { const columns = $computed(() => {
@ -43,7 +46,7 @@ const columns = $computed(() => {
return [] return []
} }
return metas[selectedTable.id].columns.filter((c) => !isSystemColumn(c)) return metas[selectedTable.id].columns.filter((c: any) => !isSystemColumn(c))
}) })
</script> </script>
@ -52,14 +55,16 @@ const columns = $computed(() => {
<div class="w-full flex flex-row space-x-2"> <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-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 v-model:value="formState.fk_relation_column_id" size="small" @change="onDataTypeChange">
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id"> <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="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="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> <div class="text-[0.5rem]">({{ relationNames[table.col.type] }} {{ table.title || table.table_name }})</div>
</div> </div>
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_lookup_column_id"> <a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_lookup_column_id">
<a-select <a-select
v-model:value="formState.fk_lookup_column_id" v-model:value="formState.fk_lookup_column_id"
@ -67,7 +72,7 @@ const columns = $computed(() => {
size="small" size="small"
@change="onDataTypeChange" @change="onDataTypeChange"
> >
<a-select-option v-for="(column, index) in columns" :key="index" :value="column.id"> <a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
{{ column.title }} {{ column.title }}
</a-select-option> </a-select-option>
</a-select> </a-select>

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

@ -1,11 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { useColumnCreateStoreOrThrow } from '#imports' import { inject, useColumnCreateStoreOrThrow, useMetas, useProject } from '#imports'
import { MetaInj } from '~/context' import { MetaInj } from '~/context'
const { formState, validateInfos, onDataTypeChange, setAdditionalValidations } = $(useColumnCreateStoreOrThrow()) const { formState, validateInfos, onDataTypeChange, setAdditionalValidations } = $(useColumnCreateStoreOrThrow())
const { tables } = $(useProject()) const { tables } = $(useProject())
const meta = $(inject(MetaInj))
const meta = $(inject(MetaInj)!)
const { metas } = $(useMetas()) const { metas } = $(useMetas())
setAdditionalValidations({ setAdditionalValidations({
@ -40,22 +43,25 @@ const refTables = $computed(() => {
return [] return []
} }
return meta.columns return (
.filter((c) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== 'bt' && !c.system) meta.columns
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== 'bt' && !c.system)
.map((c) => ({ .map((c) => ({
col: c.colOptions, col: c.colOptions,
column: c, column: c,
...tables.find((t) => t.id === c.colOptions.fk_related_model_id), ...tables.find((t) => t.id === (c.colOptions as any)?.fk_related_model_id),
})) })) ?? []
)
}) })
const columns = $computed(() => { const columns = $computed(() => {
const selectedTable = refTables.find((t) => t.column.id === formState.fk_relation_column_id) const selectedTable = refTables.find((t) => t.column.id === formState.fk_relation_column_id)
if (!selectedTable?.id) { if (!selectedTable?.id) {
return [] return []
} }
return metas[selectedTable.id].columns.filter((c) => !isVirtualCol(c.uidt) && !isSystemColumn(c)) return metas[selectedTable.id].columns.filter((c: any) => !isVirtualCol(c.uidt) && !isSystemColumn(c))
}) })
</script> </script>
@ -64,7 +70,7 @@ const columns = $computed(() => {
<div class="w-full flex flex-row space-x-2"> <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-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 v-model:value="formState.fk_relation_column_id" size="small" @change="onDataTypeChange">
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id"> <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="flex flex-row items-center space-x-0.5">
<div class="font-weight-bold text-xs">{{ table.column.title }}</div> <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> <div class="text-[0.45rem]">({{ relationNames[table.col.type] }} {{ table.title || table.table_name }})</div>
@ -79,7 +85,7 @@ const columns = $computed(() => {
size="small" size="small"
@change="onDataTypeChange" @change="onDataTypeChange"
> >
<a-select-option v-for="(column, index) in columns" :key="index" :value="column.id"> <a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
{{ column.title }} {{ column.title }}
</a-select-option> </a-select-option>
</a-select> </a-select>
@ -87,12 +93,10 @@ const columns = $computed(() => {
</div> </div>
<a-form-item label="Aggregate function" v-bind="validateInfos.rollup_function"> <a-form-item label="Aggregate function" v-bind="validateInfos.rollup_function">
<a-select v-model:value="formState.rollup_function" size="small" @change="onDataTypeChange"> <a-select v-model:value="formState.rollup_function" size="small" @change="onDataTypeChange">
<a-select-option v-for="(func, index) in aggrFunctionsList" :key="index" :value="func.value"> <a-select-option v-for="(func, index) of aggrFunctionsList" :key="index" :value="func.value">
{{ func.text }} {{ func.text }}
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</div> </div>
</template> </template>
<style scoped></style>

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

@ -289,6 +289,6 @@ defineExpose({
} }
:deep(.ant-select-item-option) { :deep(.ant-select-item-option) {
@apply "!min-w-min"; @apply "!min-w-full";
} }
</style> </style>

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

@ -1,21 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject } from 'vue'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context' import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import { useViewColumns } from '#imports' import { computed, inject, useNuxtApp, useViewColumns, watch } from '#imports'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiEyeIcon from '~icons/mdi/eye-off-outline' const meta = inject(MetaInj)!
import MdiDragIcon from '~icons/mdi/drag' const activeView = inject(ActiveViewInj)!
const reloadDataHook = inject(ReloadViewDataHookInj)!
const { fieldsOrder, coverImageField, modelValue } = defineProps<{
coverImageField?: string
fieldsOrder?: string[]
modelValue?: Record<string, boolean>
}>()
const meta = inject(MetaInj)
const activeView = inject(ActiveViewInj)
const reloadDataHook = inject(ReloadViewDataHookInj)
const rootFields = inject(FieldsInj) const rootFields = inject(FieldsInj)
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj)
@ -31,46 +21,45 @@ const {
showAll, showAll,
hideAll, hideAll,
saveOrUpdate, saveOrUpdate,
// sortedFields, } = useViewColumns(activeView, meta, false, () => reloadDataHook.trigger())
} = useViewColumns(activeView, meta, false, () => reloadDataHook?.trigger())
watch( watch(
() => (activeView?.value as any)?.id, () => (activeView.value as any)?.id,
async (newVal, oldVal) => { async (newVal, oldVal) => {
if (newVal !== oldVal && meta?.value) { if (newVal !== oldVal && meta.value) {
await loadViewColumns() await loadViewColumns()
} }
}, },
{ immediate: true }, { immediate: true },
) )
watch( watch(
() => sortedAndFilteredFields.value, sortedAndFilteredFields,
(v) => { (v) => {
if (rootFields) rootFields.value = v || [] if (rootFields) rootFields.value = v || []
}, },
{ immediate: true }, { immediate: true },
) )
const isAnyFieldHidden = computed(() => { const isAnyFieldHidden = computed(() => fields.value?.some((field) => !(!showSystemFields && field.system) && !field.show))
return fields?.value?.some((f) => !(!showSystemFields && f.system) && !f.show)
})
const onMove = (event: { moved: { newIndex: number } }) => { const onMove = (event: { moved: { newIndex: number } }) => {
// todo : sync with server // todo : sync with server
if (!fields?.value) return if (!fields.value) return
if (fields.value.length < 2) return if (fields.value.length < 2) return
if (fields?.value.length - 1 === event.moved.newIndex) { if (fields.value.length - 1 === event.moved.newIndex) {
fields.value[event.moved.newIndex].order = (fields.value[event.moved.newIndex - 1].order || 1) + 1 fields.value[event.moved.newIndex].order = (fields.value[event.moved.newIndex - 1].order || 1) + 1
} else if (event.moved.newIndex === 0) { } else if (event.moved.newIndex === 0) {
fields.value[event.moved.newIndex].order = (fields?.value[1].order || 1) / 2 fields.value[event.moved.newIndex].order = (fields.value[1].order || 1) / 2
} else { } else {
fields.value[event.moved.newIndex].order = fields.value[event.moved.newIndex].order =
((fields?.value[event.moved.newIndex - 1].order || 1) + (fields?.value[event.moved.newIndex + 1].order || 1)) / 2 ((fields.value[event.moved.newIndex - 1].order || 1) + (fields.value[event.moved.newIndex + 1].order || 1)) / 2
// );
} }
saveOrUpdate(fields.value[event.moved.newIndex], event.moved.newIndex) saveOrUpdate(fields.value[event.moved.newIndex], event.moved.newIndex)
$e('a:fields:reorder') $e('a:fields:reorder')
} }
</script> </script>
@ -80,11 +69,12 @@ const onMove = (event: { moved: { newIndex: number } }) => {
<div :class="{ 'nc-badge nc-active-btn': isAnyFieldHidden }"> <div :class="{ 'nc-badge nc-active-btn': isAnyFieldHidden }">
<a-button v-t="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked" size="small"> <a-button v-t="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked" size="small">
<div class="flex align-center gap-1"> <div class="flex align-center gap-1">
<!-- <v-icon small class="mr-1" color="#777"> mdi-eye-off-outline </v-icon> --> <MdiEyeOffOutline class="text-grey" />
<MdiEyeIcon class="text-grey"></MdiEyeIcon>
<!-- Fields --> <!-- Fields -->
<span class="text-xs text-capitalize">{{ $t('objects.fields') }}</span> <span class="text-xs text-capitalize">{{ $t('objects.fields') }}</span>
<MdiMenuDownIcon class="text-grey"></MdiMenuDownIcon>
<MdiMenuDown class="text-grey" />
</div> </div>
</a-button> </a-button>
</div> </div>
@ -101,7 +91,7 @@ const onMove = (event: { moved: { newIndex: number } }) => {
<span class="text-xs">{{ field.title }}</span> <span class="text-xs">{{ field.title }}</span>
</a-checkbox> </a-checkbox>
<div class="flex-1" /> <div class="flex-1" />
<MdiDragIcon class="cursor-move" /> <MdiDrag class="cursor-move" />
</div> </div>
</template> </template>
</Draggable> </Draggable>

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

@ -1,14 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useProvideSmartsheetStore, useSmartsheetStoreOrThrow } from '~/composables/useSmartsheetStore' import { computed, inject, ref, useSmartsheetStoreOrThrow } from '#imports'
import { MetaInj, ReloadViewDataHookInj } from '~/context' import { ReloadViewDataHookInj } from '~/context'
import MdiSearchIcon from '~icons/mdi/magnify'
import MdiMenuDownIcon from '~icons/mdi/menu-down' const reloadData = inject(ReloadViewDataHookInj)!
const reloadData = inject(ReloadViewDataHookInj)
const { search, meta } = useSmartsheetStoreOrThrow() const { search, meta } = useSmartsheetStoreOrThrow()
// todo: where is this value supposed to come from? it's not in the store
const isDropdownOpen = ref(false)
const columns = computed(() => const columns = computed(() =>
meta?.value?.columns?.map((c) => ({ meta.value?.columns?.map((c) => ({
value: c.id, value: c.id,
label: c.title, label: c.title,
})), })),
@ -21,12 +23,13 @@ const columns = computed(() =>
size="small" size="small"
class="max-w-[200px]" class="max-w-[200px]"
placeholder="Filter query" placeholder="Filter query"
@press-enter="reloadData.trigger()" @press-enter="reloadData.trigger(null)"
> >
<template #addonBefore> <template #addonBefore>
<div class="flex align-center relative" @click="isDropdownOpen = true"> <div class="flex align-center relative" @click="isDropdownOpen = true">
<MdiSearchIcon class="text-grey" /> <MdiMagnify class="text-grey" />
<MdiMenuDownIcon class="text-grey" /> <MdiMenuDown class="text-grey" />
<a-select <a-select
v-model:value="search.field" v-model:value="search.field"
size="small" size="small"
@ -39,5 +42,3 @@ const columns = computed(() =>
</template> </template>
</a-input> </a-input>
</template> </template>
<style scoped></style>

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

@ -25,8 +25,6 @@ import {
ReloadViewDataHookInj, ReloadViewDataHookInj,
} from '~/context' } from '~/context'
import { NavigateDir } from '~/lib' import { NavigateDir } from '~/lib'
import MdiArrowExpandIcon from '~icons/mdi/arrow-expand'
import MdiPlusIcon from '~icons/mdi/plus'
const meta = inject(MetaInj) const meta = inject(MetaInj)
const view = inject(ActiveViewInj) const view = inject(ActiveViewInj)
@ -40,7 +38,6 @@ const isView = false
const selected = reactive<{ row: number | null; col: number | null }>({ row: null, col: null }) const selected = reactive<{ row: number | null; col: number | null }>({ row: null, col: null })
let editEnabled = $ref(false) let editEnabled = $ref(false)
const { sqlUi } = useProject()
const { xWhere, isPkAvail } = useSmartsheetStoreOrThrow() const { xWhere, isPkAvail } = useSmartsheetStoreOrThrow()
const addColumnDropdown = ref(false) const addColumnDropdown = ref(false)
const contextMenu = ref(false) const contextMenu = ref(false)
@ -288,7 +285,7 @@ const onNavigate = (dir: NavigateDir) => {
<th v-t="['c:column:add']" @click="addColumnDropdown = true"> <th v-t="['c:column:add']" @click="addColumnDropdown = true">
<a-dropdown v-model:visible="addColumnDropdown" :trigger="['click']"> <a-dropdown v-model:visible="addColumnDropdown" :trigger="['click']">
<div class="h-full w-[60px] flex align-center justify-center"> <div class="h-full w-[60px] flex align-center justify-center">
<MdiPlusIcon class="text-sm" /> <MdiPlus class="text-sm" />
</div> </div>
<template #overlay> <template #overlay>
<SmartsheetColumnEditOrAdd @click.stop @cancel="addColumnDropdown = false" /> <SmartsheetColumnEditOrAdd @click.stop @cancel="addColumnDropdown = false" />
@ -308,7 +305,7 @@ const onNavigate = (dir: NavigateDir) => {
> >
<a-checkbox v-model:checked="row.rowMeta.selected" /> <a-checkbox v-model:checked="row.rowMeta.selected" />
<span class="flex-1" /> <span class="flex-1" />
<MdiArrowExpandIcon class="text-sm text-pink hidden group-hover:inline-block" /> <MdiArrowExpand class="text-sm text-pink hidden group-hover:inline-block" />
</div> </div>
</div> </div>
</td> </td>
@ -357,8 +354,8 @@ const onNavigate = (dir: NavigateDir) => {
@click="addEmptyRow()" @click="addEmptyRow()"
> >
<a-tooltip top left> <a-tooltip top left>
<div class="w-min flex align-center"> <div class="w-full flex align-center">
<MdiPlusIcon class="text-pint-500 text-xs" /> <MdiPlus class="text-pint-500 text-xs" />
<span class="ml-1 caption grey--text"> <span class="ml-1 caption grey--text">
{{ $t('activity.addRow') }} {{ $t('activity.addRow') }}
</span> </span>

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

@ -6,9 +6,9 @@ import { notification } from 'ant-design-vue'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import RenameableMenuItem from './RenameableMenuItem.vue' import RenameableMenuItem from './RenameableMenuItem.vue'
import { inject, onMounted, ref, useApi, useTabs, watch } from '#imports' import { inject, onMounted, ref, useApi, useRouter, watch } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils' import { extractSdkResponseErrorMsg } from '~/utils'
import { ActiveViewInj, MetaInj, ViewListInj } from '~/context' import { ActiveViewInj, ViewListInj } from '~/context'
interface Emits { interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void (event: 'openModal', data: { type: ViewTypes; title?: string }): void
@ -22,10 +22,6 @@ const activeView = inject(ActiveViewInj, ref())
const views = inject<Ref<any[]>>(ViewListInj, ref([])) const views = inject<Ref<any[]>>(ViewListInj, ref([]))
const meta = inject(MetaInj)
const { addTab } = useTabs()
const { api } = useApi() const { api } = useApi()
const router = useRouter() const router = useRouter()

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

@ -1,9 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject, ref, useTable } from '#imports' import { inject, ref, useTable } from '#imports'
import { MetaInj, RightSidebarInj } from '~/context' import { MetaInj, RightSidebarInj } from '~/context'
import MdiDeleteIcon from '~icons/mdi/delete-outline'
const meta = inject(MetaInj) const meta = inject(MetaInj)!
const { deleteTable } = useTable() const { deleteTable } = useTable()
@ -15,7 +14,7 @@ const sidebarOpen = inject(RightSidebarInj, ref(true))
<template #title> {{ $t('activity.deleteTable') }} </template> <template #title> {{ $t('activity.deleteTable') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-red-500 group"> <div class="nc-sidebar-right-item hover:after:bg-red-500 group">
<MdiDeleteIcon class="group-hover:(!text-white)" @click="deleteTable(meta)" /> <MdiDeleteOutline class="group-hover:(!text-white)" @click="deleteTable(meta)" />
</div> </div>
</a-tooltip> </a-tooltip>
</template> </template>

11
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue

@ -1,8 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from '@vue/reactivity'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { useSmartsheetStoreOrThrow } from '~/composables/useSmartsheetStore' import { computed, useSmartsheetStoreOrThrow } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' import { extractSdkResponseErrorMsg } from '~/utils'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline' import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
import MdiAccountIcon from '~icons/mdi/account' import MdiAccountIcon from '~icons/mdi/account'
import MdiAccountGroupIcon from '~icons/mdi/account-group' import MdiAccountGroupIcon from '~icons/mdi/account-group'
@ -25,19 +24,19 @@ function changeLockType(type: LockType) {
return toast.info('Coming soon', { timeout: 3000 }) return toast.info('Coming soon', { timeout: 3000 })
} }
try { try {
view.value.lock_type = type ;(view.value as any).lock_type = type
$api.dbView.update(view.value.id as string, { $api.dbView.update(view.value.id as string, {
lock_type: type, lock_type: type,
}) })
toast.success(`Successfully Switched to ${type} view`, { timeout: 3000 }) toast.success(`Successfully Switched to ${type} view`, { timeout: 3000 })
} catch (e) { } catch (e: any) {
toast.error(extractSdkResponseErrorMsg(e)) toast.error(extractSdkResponseErrorMsg(e))
} }
} }
const Icon = computed(() => { const Icon = computed(() => {
switch (view?.value?.lock_type) { switch ((view.value as any)?.lock_type) {
case LockType.Personal: case LockType.Personal:
return MdiAccountIcon return MdiAccountIcon
case LockType.Locked: case LockType.Locked:

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

@ -1,11 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ReloadViewDataHookInj, RightSidebarInj } from '~/context' import { ReloadViewDataHookInj, RightSidebarInj } from '~/context'
import MdiReloadIcon from '~icons/mdi/reload'
import { inject, ref } from '#imports' import { inject, ref } from '#imports'
const reloadTri = inject(ReloadViewDataHookInj) const reloadHook = inject(ReloadViewDataHookInj)!
const sidebarOpen = inject(RightSidebarInj, ref(true)) const sidebarOpen = inject(RightSidebarInj, ref(true))
const onClick = () => reloadHook.trigger()
</script> </script>
<template> <template>
@ -13,9 +14,7 @@ const sidebarOpen = inject(RightSidebarInj, ref(true))
<template #title> {{ $t('general.reload') }} </template> <template #title> {{ $t('general.reload') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-green-500 group"> <div class="nc-sidebar-right-item hover:after:bg-green-500 group">
<MdiReloadIcon class="group-hover:(!text-white)" @click="reloadTri.trigger()" /> <MdiReload class="group-hover:(!text-white)" @click="onClick" />
</div> </div>
</a-tooltip> </a-tooltip>
</template> </template>
<style scoped></style>

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

@ -1,13 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { useClipboard } from '@vueuse/core' import { onMounted, useClipboard, useNuxtApp, useProject } from '#imports'
import OpenInNewIcon from '~icons/mdi/open-in-new' import { dashboardUrl, extractSdkResponseErrorMsg } from '~/utils'
import { dashboardUrl } from '~/utils/urlUtils'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiReload from '~icons/mdi/reload'
import DownIcon from '~icons/ic/round-keyboard-arrow-down'
import ContentCopyIcon from '~icons/mdi/content-copy'
import MdiXmlIcon from '~icons/mdi/xml'
const toast = useToast() const toast = useToast()
interface ShareBase { interface ShareBase {
@ -22,9 +17,13 @@ enum ShareBaseRole {
} }
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
let base = $ref<null | ShareBase>(null) let base = $ref<null | ShareBase>(null)
const showEditBaseDropdown = $ref(false) const showEditBaseDropdown = $ref(false)
const { project } = useProject() const { project } = useProject()
const { copy } = useClipboard() const { copy } = useClipboard()
const url = $computed(() => (base && base.uuid ? `${dashboardUrl()}#/nc/base/${base.uuid}` : null)) const url = $computed(() => (base && base.uuid ? `${dashboardUrl()}#/nc/base/${base.uuid}` : null))
@ -33,7 +32,8 @@ const loadBase = async () => {
try { try {
if (!project.value.id) return if (!project.value.id) return
const res = await $api.project.sharedBaseGet(project.value.id) // todo: result is missing roles in return-type
const res: any = await $api.project.sharedBaseGet(project.value.id)
base = { base = {
uuid: res.uuid, uuid: res.uuid,
url: res.url, url: res.url,
@ -41,6 +41,7 @@ const loadBase = async () => {
} }
} catch (e: any) { } catch (e: any) {
console.error(e) console.error(e)
toast.error(await extractSdkResponseErrorMsg(e)) toast.error(await extractSdkResponseErrorMsg(e))
} }
} }
@ -49,16 +50,19 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
try { try {
if (!project.value.id) return if (!project.value.id) return
const res = await $api.project.sharedBaseUpdate(project.value.id, { // todo: returns void?
const res: any = await $api.project.sharedBaseUpdate(project.value.id, {
roles: role, roles: role,
}) })
base = res || {} base = res ?? {}
base.role = role base!.role = role
} catch (e: any) { } catch (e: any) {
console.error(e) console.error(e)
toast.error(await extractSdkResponseErrorMsg(e)) toast.error(await extractSdkResponseErrorMsg(e))
} }
$e('a:shared-base:enable', { role }) $e('a:shared-base:enable', { role })
} }
@ -83,10 +87,13 @@ const recreate = async () => {
const sharedBase = await $api.project.sharedBaseCreate(project.value.id, { const sharedBase = await $api.project.sharedBaseCreate(project.value.id, {
roles: base?.role || ShareBaseRole.Viewer, roles: base?.role || ShareBaseRole.Viewer,
}) })
const newBase = sharedBase || {} const newBase = sharedBase || {}
base = { ...newBase, role: base?.role } base = { ...newBase, role: base?.role }
} catch (e: any) { } catch (e: any) {
console.error(e) console.error(e)
toast.error(await extractSdkResponseErrorMsg(e)) toast.error(await extractSdkResponseErrorMsg(e))
} }
@ -96,7 +103,8 @@ const recreate = async () => {
const copyUrl = async () => { const copyUrl = async () => {
if (!url) return if (!url) return
copy(url) await copy(url)
toast.success('Copied shareable base url to clipboard!') toast.success('Copied shareable base url to clipboard!')
$e('c:shared-base:copy-url') $e('c:shared-base:copy-url')
@ -135,7 +143,8 @@ onMounted(() => {
<template> <template>
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="flex flex-row items-center space-x-0.5 pl-2 h-[0.8rem]"> <div class="flex flex-row items-center space-x-0.5 pl-2 h-[0.8rem]">
<OpenInNewIcon /> <MdiOpenInNew />
<div class="text-xs">Shared Base Link</div> <div class="text-xs">Shared Base Link</div>
</div> </div>
<div v-if="base?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm w-full justify-between"> <div v-if="base?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm w-full justify-between">
@ -157,7 +166,7 @@ onMounted(() => {
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="copyUrl"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="copyUrl">
<template #icon> <template #icon>
<ContentCopyIcon class="flex mx-auto text-gray-600" /> <MdiContentCopy class="flex mx-auto text-gray-600" />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -167,7 +176,7 @@ onMounted(() => {
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="navigateToSharedBase"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="navigateToSharedBase">
<template #icon> <template #icon>
<OpenInNewIcon class="flex mx-auto text-gray-600" /> <MdiOpenInNew class="flex mx-auto text-gray-600" />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -177,7 +186,7 @@ onMounted(() => {
</template> </template>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="generateEmbeddableIframe"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5 h-[1rem]" @click="generateEmbeddableIframe">
<template #icon> <template #icon>
<MdiXmlIcon class="flex mx-auto text-gray-600" /> <MdiXml class="flex mx-auto text-gray-600" />
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -190,7 +199,7 @@ onMounted(() => {
<div class="flex flex-row items-center space-x-2"> <div class="flex flex-row items-center space-x-2">
<div v-if="base?.uuid">Anyone with the link</div> <div v-if="base?.uuid">Anyone with the link</div>
<div v-else>Disable shared base</div> <div v-else>Disable shared base</div>
<DownIcon class="h-[1rem]" /> <IcRoundKeyboardArrowDown class="h-[1rem]" />
</div> </div>
</a-button> </a-button>
@ -207,7 +216,7 @@ onMounted(() => {
<a-select v-if="base?.uuid" v-model:value="base.role" class="flex"> <a-select v-if="base?.uuid" v-model:value="base.role" class="flex">
<template #suffixIcon> <template #suffixIcon>
<div class="flex flex-row"> <div class="flex flex-row">
<DownIcon class="text-black -mt-0.5 h-[1rem]" /> <IcRoundKeyboardArrowDown class="text-black -mt-0.5 h-[1rem]" />
</div> </div>
</template> </template>
<a-select-option <a-select-option
@ -225,5 +234,3 @@ onMounted(() => {
</div> </div>
</div> </div>
</template> </template>
<style scoped></style>

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

@ -3,39 +3,44 @@ import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue' import ItemChip from './components/ItemChip.vue'
import ListItems from './components/ListItems.vue' import ListItems from './components/ListItems.vue'
import { useProvideLTARStore } from '#imports' import { inject, ref, useProvideLTARStore } from '#imports'
import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context' import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import MdiExpandIcon from '~icons/mdi/arrow-expand'
const column = inject(ColumnInj) const column = inject(ColumnInj)
const reloadTrigger = inject(ReloadViewDataHookInj)
const cellValue = inject(CellValueInj) const reloadTrigger = inject(ReloadViewDataHookInj)!
const cellValue = inject(CellValueInj, ref<any>(null))
const row = inject(RowInj) const row = inject(RowInj)
const active = false const active = false
const localState = null
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
const { relatedTableMeta, loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore( const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
() => reloadTrigger?.trigger(), reloadTrigger.trigger,
) )
await loadRelatedTableMeta() await loadRelatedTableMeta()
</script> </script>
<template> <template>
<div class="flex w-full chips-wrapper align-center" :class="{ active }"> <div class="flex w-full chips-wrapper align-center" :class="{ active }">
<div class="chips d-flex align-center flex-grow"> <div class="chips d-flex align-center flex-grow">
<template v-if="cellValue || localState"> <template v-if="cellValue">
<ItemChip :item="cellValue" :value="cellValue[relatedTablePrimaryValueProp]" @unlink="unlink(cellValue || localState)" /> <ItemChip :item="cellValue" :value="cellValue[relatedTablePrimaryValueProp]" @unlink="unlink(cellValue)" />
</template> </template>
</div> </div>
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center"> <div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<MdiExpandIcon <MdiArrowExpand
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500)" class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500)"
@click="listItemsDlg = true" @click="listItemsDlg = true"
/> />
</div> </div>
<ListItems v-model="listItemsDlg" /> <ListItems v-model="listItemsDlg" />
</div> </div>
</template> </template>

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

@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, useProject } from '#imports' import { computed, inject, ref, useProject } from '#imports'
import { CellValueInj, ColumnInj } from '~/context' import { CellValueInj, ColumnInj } from '~/context'
import { handleTZ } from '~/utils/dateTimeUtils' import { handleTZ, replaceUrlsWithLink } from '~/utils'
import { replaceUrlsWithLink } from '~/utils/urlUtils'
const column = inject(ColumnInj) // todo: column type doesn't have required property `error` - throws in typecheck
const column: any = inject(ColumnInj)
const value = inject(CellValueInj) const value = inject(CellValueInj)
@ -14,6 +14,7 @@ const showEditFormulaWarning = ref(false)
const showEditFormulaWarningMessage = () => { const showEditFormulaWarningMessage = () => {
showEditFormulaWarning.value = true showEditFormulaWarning.value = true
setTimeout(() => { setTimeout(() => {
showEditFormulaWarning.value = false showEditFormulaWarning.value = false
}, 3000) }, 3000)
@ -32,6 +33,7 @@ const urls = computed(() => replaceUrlsWithLink(result.value))
</template> </template>
<span>ERR!</span> <span>ERR!</span>
</a-tooltip> </a-tooltip>
<div class="pa-2" @dblclick="showEditFormulaWarningMessage"> <div class="pa-2" @dblclick="showEditFormulaWarningMessage">
<div v-if="urls" v-html="urls" /> <div v-if="urls" v-html="urls" />
<div v-else>{{ result }}</div> <div v-else>{{ result }}</div>
@ -42,5 +44,3 @@ const urls = computed(() => replaceUrlsWithLink(result.value))
</div> </div>
</div> </div>
</template> </template>
<style scoped></style>

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

@ -4,41 +4,57 @@ import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue' import ItemChip from './components/ItemChip.vue'
import ListChildItems from './components/ListChildItems.vue' import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue' import ListItems from './components/ListItems.vue'
import { useProvideLTARStore } from '#imports' import { computed, inject, ref, useProvideLTARStore } from '#imports'
import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context' import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import MdiExpandIcon from '~icons/mdi/arrow-expand'
import MdiPlusIcon from '~icons/mdi/plus'
const column = inject(ColumnInj) const column = inject(ColumnInj)!
const cellValue = inject(CellValueInj)
const row = inject(RowInj) const cellValue = inject(CellValueInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)
const row = inject(RowInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)!
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
const { relatedTableMeta, loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore( const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
() => reloadTrigger?.trigger(), reloadTrigger.trigger,
) )
await loadRelatedTableMeta() await loadRelatedTableMeta()
const cells = computed(() =>
cellValue.value.reduce((acc: any[], curr: any) => {
if (!relatedTablePrimaryValueProp.value) return acc
const value = curr[relatedTablePrimaryValueProp.value]
if (!value) return acc
return [...acc, { value, item: curr }]
}, [] as any[]),
)
</script> </script>
<template> <template>
<div class="flex align-center items-center gap-1 w-full chips-wrapper"> <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"> <div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cellValue"> <template v-if="cellValue">
<ItemChip v-for="(ch, i) in cellValue" :key="i" :value="ch[relatedTablePrimaryValueProp]" @unlink="unlink(ch)" /> <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> <span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span>
</template> </template>
</div> </div>
<div class="flex-grow flex justify-end gap-1 min-h-[30px] align-center"> <div class="flex-grow flex justify-end gap-1 min-h-[30px] align-center">
<MdiExpandIcon <MdiArrowExpand
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
@click="childListDlg = true" @click="childListDlg = true"
/> />
<MdiPlusIcon class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" /> <MdiPlus class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
</div> </div>
<ListItems v-model="listItemsDlg" /> <ListItems v-model="listItemsDlg" />
<ListChildItems v-model="childListDlg" @attach-record=";(childListDlg = false), (listItemsDlg = true)" /> <ListChildItems v-model="childListDlg" @attach-record=";(childListDlg = false), (listItemsDlg = true)" />

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

@ -1,26 +1,35 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { useColumn } from '~/composables' import type { Ref } from 'vue'
import { CellValueInj, ColumnInj, MetaInj, ReadonlyInj } from '~/context' import { CellValueInj, ColumnInj, MetaInj, ReadonlyInj } from '~/context'
import { computed, inject, provide, useColumn, useMetas } from '#imports'
const { metas, getMeta } = useMetas() const { metas, getMeta } = useMetas()
provide(ReadonlyInj, true) provide(ReadonlyInj, true)
const column = inject(ColumnInj) as ColumnType & { colOptions: LookupType } const column = inject(ColumnInj)! as Ref<ColumnType & { colOptions: LookupType }>
const meta = inject(MetaInj) const meta = inject(MetaInj)
const value = inject(CellValueInj) const value = inject(CellValueInj)
const arrValue = computed(() => (Array.isArray(value?.value) ? value?.value : [value?.value])) const arrValue = computed(() => (Array.isArray(value?.value) ? value?.value : [value?.value]))
const relationColumn = meta?.value.columns?.find((c) => c.id === column.colOptions.fk_relation_column_id) as ColumnType & { const relationColumn = meta?.value.columns?.find((c) => c.id === column.value.colOptions?.fk_relation_column_id) as ColumnType & {
colOptions: LinkToAnotherRecordType colOptions: LinkToAnotherRecordType
} }
await getMeta(relationColumn.colOptions.fk_related_model_id as string)
const lookupTableMeta = metas?.value[relationColumn.colOptions.fk_related_model_id as string]
const lookupColumn = lookupTableMeta?.columns?.find((c) => c.id === column.colOptions.fk_lookup_column_id) as ColumnType
provide(MetaInj, ref(lookupTableMeta)) await getMeta(relationColumn.colOptions.fk_related_model_id!)
const lookupTableMeta = computed(() => metas.value[relationColumn.colOptions.fk_related_model_id!])
const lookupColumn = computed(
() =>
lookupTableMeta.value.columns?.find(
(c: Record<string, any>) => c.id === column.value.colOptions?.fk_lookup_column_id,
) as ColumnType,
)
provide(MetaInj, lookupTableMeta)
const lookupColumnMetaProps = useColumn(lookupColumn) const lookupColumnMetaProps = useColumn(lookupColumn)
</script> </script>

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

@ -4,43 +4,60 @@ import type { Ref } from 'vue'
import ItemChip from './components/ItemChip.vue' import ItemChip from './components/ItemChip.vue'
import ListChildItems from './components/ListChildItems.vue' import ListChildItems from './components/ListChildItems.vue'
import ListItems from './components/ListItems.vue' import ListItems from './components/ListItems.vue'
import { useProvideLTARStore } from '#imports' import { computed, inject, ref, useProvideLTARStore } from '#imports'
import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context' import { CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context'
import MdiExpandIcon from '~icons/mdi/arrow-expand'
import MdiPlusIcon from '~icons/mdi/plus'
const column = inject(ColumnInj) const column = inject(ColumnInj)!
const row = inject(RowInj)
const cellValue = inject(CellValueInj) const row = inject(RowInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)
const cellValue = inject(CellValueInj)!
const reloadTrigger = inject(ReloadViewDataHookInj)!
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
const { relatedTableMeta, loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore( const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
() => reloadTrigger?.trigger(), reloadTrigger.trigger,
) )
await loadRelatedTableMeta() await loadRelatedTableMeta()
const cells = computed(() =>
cellValue.value.reduce((acc: any[], curr: any) => {
if (!relatedTablePrimaryValueProp.value) return acc
const value = curr[relatedTablePrimaryValueProp.value]
if (!value) return acc
return [...acc, { value, item: curr }]
}, [] as any[]),
)
</script> </script>
<template> <template>
<div class="flex align-center gap-1 w-full h-full chips-wrapper"> <div class="flex align-center gap-1 w-full h-full chips-wrapper">
<!-- <template v-if="!isForm"> -->
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden"> <div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cellValue"> <template v-if="cellValue">
<ItemChip v-for="(ch, i) in cellValue" :key="i" :value="ch[relatedTablePrimaryValueProp]" @unlink="unlink(ch)" /> <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> <span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">more... </span>
</template> </template>
</div> </div>
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center"> <div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<MdiExpandIcon class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="childListDlg = true" /> <MdiArrowExpand class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="childListDlg = true" />
<MdiPlusIcon class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
<MdiPlus class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" />
</div> </div>
<ListItems v-model="listItemsDlg" /> <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> </div>
</template> </template>

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

@ -4,13 +4,12 @@ import MdiCloseThickIcon from '~icons/mdi/close-thick'
interface Props { interface Props {
value?: string | number | boolean value?: string | number | boolean
item?: any
} }
const { value, item } = defineProps<Props>() const { value } = defineProps<Props>()
const emit = defineEmits(['unlink']) const emit = defineEmits(['unlink'])
const readonly = inject(ReadonlyInj, false) const readonly = inject(ReadonlyInj, false)
const active = inject(ActiveCellInj, false) const active = inject(ActiveCellInj, ref(false))
</script> </script>
<template> <template>

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

@ -1,10 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useLTARStoreOrThrow, useVModel } from '#imports' import { useLTARStoreOrThrow, useVModel, watch } from '#imports'
import MdiReloadIcon from '~icons/mdi/reload'
import MdiDeleteIcon from '~icons/mdi/delete-outline'
import MdiUnlinkIcon from '~icons/mdi/link-variant-remove'
const props = defineProps<{ modelValue?: boolean }>() const props = defineProps<{ modelValue?: boolean }>()
const emit = defineEmits(['update:modelValue', 'attachRecord']) const emit = defineEmits(['update:modelValue', 'attachRecord'])
const vModel = useVModel(props, 'modelValue', emit) const vModel = useVModel(props, 'modelValue', emit)
@ -20,14 +18,15 @@ const {
getRelatedTableRowId, getRelatedTableRowId,
} = useLTARStoreOrThrow() } = useLTARStoreOrThrow()
watch(vModel, () => { watch(vModel, (nextVal) => {
if (vModel.value) { if (nextVal) {
loadChildrenList() loadChildrenList()
} }
}) })
const unlinkRow = async (row: Record<string, any>) => { const unlinkRow = async (row: Record<string, any>) => {
await unlink(row) await unlink(row)
await loadChildrenList() await loadChildrenList()
} }
</script> </script>
@ -36,12 +35,14 @@ const unlinkRow = async (row: Record<string, any>) => {
<a-modal v-model:visible="vModel" :footer="null" title="Child list"> <a-modal v-model:visible="vModel" :footer="null" title="Child list">
<div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col"> <div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col">
<div class="flex mb-4 align-center gap-2"> <div class="flex mb-4 align-center gap-2">
<!-- <a-input v-model:value="childrenListPagination.query" class="max-w-[200px]" size="small"></a-input> -->
<div class="flex-1" /> <div class="flex-1" />
<MdiReloadIcon class="cursor-pointer text-gray-500" @click="loadChildrenList" />
<MdiReload class="cursor-pointer text-gray-500" @click="loadChildrenList" />
<a-button type="primary" size="small" @click="emit('attachRecord')"> <a-button type="primary" size="small" @click="emit('attachRecord')">
<div class="flex align-center gap-1"> <div class="flex align-center gap-1">
<MdiUnlinkIcon class="text-xs text-white" @click="unlinkRow(row)" /> <!-- todo: row is not defined? @click="unlinkRow(row)" -->
<MdiLinkVariantRemove class="text-xs text-white" />
Link to '{{ meta.title }}' Link to '{{ meta.title }}'
</div> </div>
</a-button> </a-button>
@ -56,8 +57,8 @@ const unlinkRow = async (row: Record<string, any>) => {
</div> </div>
<div class="flex-1"></div> <div class="flex-1"></div>
<div class="flex gap-2"> <div class="flex gap-2">
<MdiUnlinkIcon class="text-xs text-grey hover:(!text-red-500) cursor-pointer" @click="unlinkRow(row)" /> <MdiLinkVariantRemove class="text-xs text-grey hover:(!text-red-500) cursor-pointer" @click="unlinkRow(row)" />
<MdiDeleteIcon class="text-xs text-grey hover:(!text-red-500) cursor-pointer" @click="deleteRelatedRow(row)" /> <MdiDeleteOutline class="text-xs text-grey hover:(!text-red-500) cursor-pointer" @click="deleteRelatedRow(row)" />
</div> </div>
</div> </div>
</a-card> </a-card>

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

@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useLTARStoreOrThrow, useVModel } from '#imports' import { useLTARStoreOrThrow, useVModel, watch } from '#imports'
import MdiReloadIcon from '~icons/mdi/reload'
const props = defineProps<{ modelValue: boolean }>() const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['update:modelValue', 'addNewRecord'])
const vModel = useVModel(props, 'modelValue', emit) const vModel = useVModel(props, 'modelValue', emit)
@ -16,8 +16,8 @@ const {
getRelatedTableRowId, getRelatedTableRowId,
} = useLTARStoreOrThrow() } = useLTARStoreOrThrow()
watch(vModel, () => { watch(vModel, (nextVal) => {
if (vModel.value) { if (nextVal) {
loadChildrenExcludedList() loadChildrenExcludedList()
} }
}) })
@ -25,7 +25,6 @@ watch(vModel, () => {
const linkRow = async (row: Record<string, any>) => { const linkRow = async (row: Record<string, any>) => {
await link(row) await link(row)
vModel.value = false vModel.value = false
// await loadChildrenExcludedList()
} }
</script> </script>
@ -40,7 +39,7 @@ const linkRow = async (row: Record<string, any>) => {
size="small" size="small"
></a-input> ></a-input>
<div class="flex-1" /> <div class="flex-1" />
<MdiReloadIcon class="cursor-pointer text-gray-500" @click="loadChildrenExcludedList" /> <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="emit('addNewRecord')">Add new record</a-button>
</div> </div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows"> <template v-if="childrenExcludedList?.pageInfo?.totalRows">

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

@ -2,20 +2,8 @@
import { Form } from 'ant-design-vue' import { Form } from 'ant-design-vue'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { MetaInj } from '~/context' import { MetaInj } from '~/context'
import MdiContentSaveIcon from '~icons/mdi/content-save' import { extractSdkResponseErrorMsg, fieldRequiredValidator } from '~/utils'
import MdiLinkIcon from '~icons/mdi/link' import { inject, reactive, useApi, useNuxtApp } from '#imports'
import MdiEmailIcon from '~icons/mdi/email'
import MdiSlackIcon from '~icons/mdi/slack'
import MdiMicrosoftTeamsIcon from '~icons/mdi/microsoft-teams'
import MdiDiscordIcon from '~icons/mdi/discord'
import MdiChatIcon from '~icons/mdi/chat'
import MdiWhatsAppIcon from '~icons/mdi/whatsapp'
import MdiCellPhoneMessageIcon from '~icons/mdi/cellphone-message'
import MdiGestureDoubleTapIcon from '~icons/mdi/gesture-double-tap'
import MdiInformationIcon from '~icons/mdi/information'
import MdiArrowLeftBoldIcon from '~icons/mdi/arrow-left-bold'
import { fieldRequiredValidator } from '~/utils/validation'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
interface Option { interface Option {
label: string label: string
@ -24,7 +12,9 @@ interface Option {
const emit = defineEmits(['backToList', 'editOrAdd']) const emit = defineEmits(['backToList', 'editOrAdd'])
const { $state, $api, $e } = useNuxtApp() const { $e } = useNuxtApp()
const { api, isLoading: loading } = useApi()
const toast = useToast() const toast = useToast()
@ -32,12 +22,12 @@ const meta = inject(MetaInj)
const useForm = Form.useForm const useForm = Form.useForm
const hook = reactive({ const hook = reactive<Record<string, any>>({
id: '', id: '',
title: '', title: '',
event: '', event: '',
operation: '', operation: '',
eventOperation: undefined, eventOperation: '',
notification: { notification: {
type: 'URL', type: 'URL',
payload: { payload: {
@ -45,7 +35,7 @@ const hook = reactive({
body: '{{ json data }}', body: '{{ json data }}',
headers: [{}], headers: [{}],
parameters: [{}], parameters: [{}],
} as any, },
}, },
condition: false, condition: false,
}) })
@ -64,8 +54,6 @@ const discordChannels = ref<Record<string, any>[]>([])
const mattermostChannels = ref<Record<string, any>[]>([]) const mattermostChannels = ref<Record<string, any>[]>([])
const loading = ref(false)
const filters = ref([]) const filters = ref([])
const formInput = ref({ const formInput = ref({
@ -218,7 +206,7 @@ const validators = computed(() => {
}), }),
} }
}) })
const { resetFields, validate, validateInfos } = useForm(hook, validators) const { validate, validateInfos } = useForm(hook, validators)
function onNotTypeChange() { function onNotTypeChange() {
hook.notification.payload = {} as any hook.notification.payload = {} as any
@ -258,7 +246,7 @@ function setHook(newHook: any) {
} }
async function onEventChange() { async function onEventChange() {
const { notification: { payload = {}, type = {} } = {}, ...rest } = hook const { notification: { payload = {}, type = {} } = {} } = hook
Object.assign(hook, { Object.assign(hook, {
...hook, ...hook,
@ -305,7 +293,7 @@ async function onEventChange() {
async function loadPluginList() { async function loadPluginList() {
try { try {
const plugins = (await $api.plugin.list()).list as any const plugins = (await api.plugin.list()).list as any
apps.value = plugins.reduce((o: Record<string, any>[], p: Record<string, any>) => { apps.value = plugins.reduce((o: Record<string, any>[], p: Record<string, any>) => {
p.tags = p.tags ? p.tags.split(',') : [] p.tags = p.tags ? p.tags.split(',') : []
p.parsedInput = p.input && JSON.parse(p.input) p.parsedInput = p.input && JSON.parse(p.input)
@ -327,31 +315,30 @@ async function saveHooks() {
await validate() await validate()
} catch (_: any) { } catch (_: any) {
toast.error('Invalid Form') toast.error('Invalid Form')
loading.value = false loading.value = false
return return
} }
try { try {
let res let res
if (hook.id) { if (hook.id) {
res = await $api.dbTableWebhook.update(hook.id, { res = await api.dbTableWebhook.update(hook.id, {
...hook, ...hook,
notification: { notification: {
...hook.notification, ...hook.notification,
payload: hook.notification.payload, payload: hook.notification.payload,
}, },
} as any) })
} else { } else {
res = await $api.dbTableWebhook.create( res = await api.dbTableWebhook.create(meta!.value.id!, {
meta?.value.id as string,
{
...hook, ...hook,
notification: { notification: {
...hook.notification, ...hook.notification,
payload: hook.notification.payload, payload: hook.notification.payload,
}, },
} as any, } as any)
)
} }
if (!hook.id && res) { if (!hook.id && res) {
@ -371,6 +358,7 @@ async function saveHooks() {
} finally { } finally {
loading.value = false loading.value = false
} }
$e('a:webhook:add', { $e('a:webhook:add', {
operation: hook.operation, operation: hook.operation,
condition: hook.condition, condition: hook.condition,
@ -389,7 +377,9 @@ defineExpose({
watch( watch(
() => hook.eventOperation, () => hook.eventOperation,
(v) => { () => {
if (!hook.eventOperation) return
const [event, operation] = hook.eventOperation.split(' ') const [event, operation] = hook.eventOperation.split(' ')
hook.event = event hook.event = event
hook.operation = operation hook.operation = operation
@ -405,21 +395,21 @@ onMounted(() => {
<div class="mb-4"> <div class="mb-4">
<div class="float-left mt-2"> <div class="float-left mt-2">
<div class="flex items-center"> <div class="flex items-center">
<MdiArrowLeftBoldIcon class="mr-3 text-xl cursor-pointer" @click="emit('backToList')" /> <MdiArrowLeftBold class="mr-3 text-xl cursor-pointer" @click="emit('backToList')" />
<span class="inline text-xl font-bold">{{ meta.title }} : {{ hook.title || 'Webhooks' }} </span> <span class="inline text-xl font-bold">{{ meta.title }} : {{ hook.title || 'Webhooks' }} </span>
</div> </div>
</div> </div>
<div class="float-right mb-5"> <div class="float-right mb-5">
<a-button class="mr-3" size="large" @click="testWebhook"> <a-button class="mr-3" size="large" @click="testWebhook">
<div class="flex items-center"> <div class="flex items-center">
<MdiGestureDoubleTapIcon class="mr-2" /> <MdiGestureDoubleTap class="mr-2" />
<!-- TODO: i18n --> <!-- TODO: i18n -->
Test Webhook Test Webhook
</div> </div>
</a-button> </a-button>
<a-button type="primary" size="large" @click.prevent="saveHooks"> <a-button type="primary" size="large" @click.prevent="saveHooks">
<div class="flex items-center"> <div class="flex items-center">
<MdiContentSaveIcon class="mr-2" /> <MdiContentSave class="mr-2" />
<!-- Save --> <!-- Save -->
{{ $t('general.save') }} {{ $t('general.save') }}
</div> </div>
@ -456,14 +446,22 @@ onMounted(() => {
> >
<a-select-option v-for="(notificationOption, i) in notificationList" :key="i" :value="notificationOption.type"> <a-select-option v-for="(notificationOption, i) in notificationList" :key="i" :value="notificationOption.type">
<div class="flex items-center"> <div class="flex items-center">
<MdiLinkIcon v-if="notificationOption.type === 'URL'" class="mr-2" /> <MdiLink v-if="notificationOption.type === 'URL'" class="mr-2" />
<MdiEmailIcon v-if="notificationOption.type === 'Email'" class="mr-2" />
<MdiSlackIcon v-if="notificationOption.type === 'Slack'" class="mr-2" /> <MdiEmail v-if="notificationOption.type === 'Email'" class="mr-2" />
<MdiMicrosoftTeamsIcon v-if="notificationOption.type === 'Microsoft Teams'" class="mr-2" />
<MdiDiscordIcon v-if="notificationOption.type === 'Discord'" class="mr-2" /> <MdiSlack v-if="notificationOption.type === 'Slack'" class="mr-2" />
<MdiChatIcon v-if="notificationOption.type === 'Mattermost'" class="mr-2" />
<MdiWhatsAppIcon v-if="notificationOption.type === 'Whatsapp Twilio'" class="mr-2" /> <MdiMicrosoftTeams v-if="notificationOption.type === 'Microsoft Teams'" class="mr-2" />
<MdiCellPhoneMessageIcon v-if="notificationOption.type === 'Twilio'" class="mr-2" />
<MdiDiscord v-if="notificationOption.type === 'Discord'" class="mr-2" />
<MdiChat v-if="notificationOption.type === 'Mattermost'" class="mr-2" />
<MdiWhatsapp v-if="notificationOption.type === 'Whatsapp Twilio'" class="mr-2" />
<MdiCellphoneMessage v-if="notificationOption.type === 'Twilio'" class="mr-2" />
{{ notificationOption.type }} {{ notificationOption.type }}
</div> </div>
</a-select-option> </a-select-option>
@ -471,17 +469,20 @@ onMounted(() => {
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-row v-if="hook.notification.type === 'URL'" class="mb-5" type="flex" :gutter="[16, 0]"> <a-row v-if="hook.notification.type === 'URL'" class="mb-5" type="flex" :gutter="[16, 0]">
<a-col :span="6"> <a-col :span="6">
<a-select v-model:value="hook.notification.payload.method" size="large"> <a-select v-model:value="hook.notification.payload.method" size="large">
<a-select-option v-for="(method, i) in methodList" :key="i" :value="method.title">{{ method.title }}</a-select-option> <a-select-option v-for="(method, i) in methodList" :key="i" :value="method.title">{{ method.title }}</a-select-option>
</a-select> </a-select>
</a-col> </a-col>
<a-col :span="18"> <a-col :span="18">
<a-form-item v-bind="validateInfos['notification.payload.path']"> <a-form-item v-bind="validateInfos['notification.payload.path']">
<a-input v-model:value="hook.notification.payload.path" size="large" placeholder="http://example.com" /> <a-input v-model:value="hook.notification.payload.path" size="large" placeholder="http://example.com" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="24"> <a-col :span="24">
<a-tabs v-model:activeKey="urlTabKey" centered> <a-tabs v-model:activeKey="urlTabKey" centered>
<a-tab-pane key="body" tab="Body"> <a-tab-pane key="body" tab="Body">
@ -503,6 +504,7 @@ onMounted(() => {
</a-tabs> </a-tabs>
</a-col> </a-col>
</a-row> </a-row>
<a-row v-if="hook.notification.type === 'Slack'" type="flex"> <a-row v-if="hook.notification.type === 'Slack'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
@ -516,6 +518,7 @@ onMounted(() => {
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-row v-if="hook.notification.type === 'Microsoft Teams'" type="flex"> <a-row v-if="hook.notification.type === 'Microsoft Teams'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
@ -529,6 +532,7 @@ onMounted(() => {
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-row v-if="hook.notification.type === 'Discord'" type="flex"> <a-row v-if="hook.notification.type === 'Discord'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
@ -542,6 +546,7 @@ onMounted(() => {
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-row v-if="hook.notification.type === 'Mattermost'" type="flex"> <a-row v-if="hook.notification.type === 'Mattermost'" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-form-item v-bind="validateInfos['notification.channels']"> <a-form-item v-bind="validateInfos['notification.channels']">
@ -555,6 +560,7 @@ onMounted(() => {
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-row v-if="formInput[hook.notification.type] && hook.notification.payload" type="flex"> <a-row v-if="formInput[hook.notification.type] && hook.notification.payload" type="flex">
<a-col v-for="(input, i) in formInput[hook.notification.type]" :key="i" :span="24"> <a-col v-for="(input, i) in formInput[hook.notification.type]" :key="i" :span="24">
<a-form-item v-if="input.type === 'LongText'" v-bind="validateInfos[`notification.payload.${input.key}`]"> <a-form-item v-if="input.type === 'LongText'" v-bind="validateInfos[`notification.payload.${input.key}`]">
@ -565,6 +571,7 @@ onMounted(() => {
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-row class="mb-5" type="flex"> <a-row class="mb-5" type="flex">
<a-col :span="24"> <a-col :span="24">
<a-card> <a-card>
@ -573,6 +580,7 @@ onMounted(() => {
</a-card> </a-card>
</a-col> </a-col>
</a-row> </a-row>
<a-row> <a-row>
<a-col :span="24"> <a-col :span="24">
<div class="text-gray-600"> <div class="text-gray-600">
@ -582,7 +590,7 @@ onMounted(() => {
<template #title> <template #title>
<span> <strong>data</strong> : Row data <br /> </span> <span> <strong>data</strong> : Row data <br /> </span>
</template> </template>
<MdiInformationIcon class="ml-2" /> <MdiInformation class="ml-2" />
</a-tooltip> </a-tooltip>
<div class="mt-3"> <div class="mt-3">

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

@ -1,9 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { onMounted } from '@vue/runtime-core'
import { MetaInj } from '~/context' import { MetaInj } from '~/context'
import MdiHookIcon from '~icons/mdi/hook' import { inject, onMounted, ref, useNuxtApp } from '#imports'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
const emit = defineEmits(['edit']) const emit = defineEmits(['edit'])
@ -75,7 +73,7 @@ onMounted(() => {
</template> </template>
<template #avatar> <template #avatar>
<div class="mt-4"> <div class="mt-4">
<MdiHookIcon class="text-xl" /> <MdiHook class="text-xl" />
</div> </div>
</template> </template>
</a-list-item-meta> </a-list-item-meta>
@ -84,7 +82,7 @@ onMounted(() => {
<!-- Notify Via --> <!-- Notify Via -->
<div class="mr-2">{{ $t('labels.notifyVia') }} : {{ item?.notification?.type }}</div> <div class="mr-2">{{ $t('labels.notifyVia') }} : {{ item?.notification?.type }}</div>
<div class="float-right pt-2 pr-1"> <div class="float-right pt-2 pr-1">
<MdiDeleteOutlineIcon class="text-xl" @click.stop="deleteHook(item, index)" /> <MdiDeleteOutline class="text-xl" @click.stop="deleteHook(item, index)" />
</div> </div>
</div> </div>
</template> </template>

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

@ -31,7 +31,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
// state // state
// todo: give proper type - ColumnType // todo: give proper type - ColumnType
const formState = ref<Partial<Record<string, any>>>({ const formState = ref<Record<string, any>>({
title: 'title', title: 'title',
uidt: UITypes.SingleLineText, uidt: UITypes.SingleLineText,
...(column?.value || {}), ...(column?.value || {}),
@ -168,6 +168,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
formState.value.altered = formState.value.altered || 2 formState.value.altered = formState.value.altered || 2
} }
// todo: type of onAlter is wrong, the first argument is `CheckboxChangeEvent` not a number.
const onAlter = (val = 2, cdf = false) => { const onAlter = (val = 2, cdf = false) => {
formState.value.altered = formState.value.altered || val formState.value.altered = formState.value.altered || val
if (cdf) formState.value.cdf = formState.value.cdf || null if (cdf) formState.value.cdf = formState.value.cdf || null

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

@ -160,7 +160,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
column?.value?.title, column?.value?.title,
getRelatedTableRowId(row) as string, getRelatedTableRowId(row) as string,
) )
} catch (e) { } catch (e: any) {
notification.error({ notification.error({
message: 'Unlink failed', message: 'Unlink failed',
description: await extractSdkResponseErrorMsg(e), description: await extractSdkResponseErrorMsg(e),
@ -198,7 +198,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
column?.value?.title, column?.value?.title,
getRelatedTableRowId(row) as string, getRelatedTableRowId(row) as string,
) )
} catch (e) { } catch (e: any) {
notification.error({ notification.error({
message: 'Linking failed', message: 'Linking failed',
description: await extractSdkResponseErrorMsg(e), description: await extractSdkResponseErrorMsg(e),

66
packages/nc-gui-v2/composables/useTableCreate.ts

@ -1,66 +0,0 @@
import type { TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { useProject } from './useProject'
import { useNuxtApp } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
export function useTableCreate(onTableCreate?: (tableMeta: TableType) => void) {
const table = reactive<{ title: string; table_name: string; columns: string[] }>({
title: '',
table_name: '',
columns: {
id: true,
title: true,
created_at: true,
updated_at: true,
},
})
const { sqlUi, project, tables } = useProject()
const toast = useToast()
const { $api } = useNuxtApp()
const createTable = async () => {
try {
if (!sqlUi?.value) return
const columns = sqlUi?.value?.getNewTableColumns().filter((col) => {
if (col.column_name === 'id' && table.columns.id_ag) {
Object.assign(col, sqlUi?.value?.getDataTypeForUiType({ uidt: UITypes.ID }, 'AG'))
col.dtxp = sqlUi?.value?.getDefaultLengthForDatatype(col.dt)
col.dtxs = sqlUi?.value?.getDefaultScaleForDatatype(col.dt)
return true
}
return !!table.columns[col.column_name]
})
const tableMeta = await $api.dbTable.create(project?.value?.id as string, {
...table,
columns,
})
onTableCreate?.(tableMeta)
} catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e))
}
}
watch(
() => table.title,
(title) => {
table.table_name = `${project?.value?.prefix || ''}${title}`
},
)
const generateUniqueTitle = () => {
let c = 1
while (tables?.value?.some((t) => t.title === `Sheet${c}`)) {
c++
}
table.title = `Sheet${c}`
}
return { table, createTable, generateUniqueTitle, tables, project }
}

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

File diff suppressed because it is too large Load Diff

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

@ -33,7 +33,7 @@
"xlsx": "^0.17.3" "xlsx": "^0.17.3"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.25.2", "@antfu/eslint-config": "^0.26.0",
"@iconify-json/cil": "^1.1.2", "@iconify-json/cil": "^1.1.2",
"@iconify-json/clarity": "^1.1.4", "@iconify-json/clarity": "^1.1.4",
"@iconify-json/eva": "^1.1.2", "@iconify-json/eva": "^1.1.2",

4
packages/nc-gui-v2/vue-color-shims.d.ts vendored

@ -0,0 +1,4 @@
declare module '@ckpack/vue-color' {
import type { Component } from '@vue/runtime-core'
const Sketch: Component
}
Loading…
Cancel
Save