Browse Source

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

pull/3049/head
Wing-Kam Wong 2 years ago
parent
commit
b4ae0c3a0a
  1. 5
      packages/nc-gui-v2/.eslintrc.js
  2. 97
      packages/nc-gui-v2/app.vue
  3. 8
      packages/nc-gui-v2/assets/style-v2.scss
  4. 28
      packages/nc-gui-v2/components.d.ts
  5. 16
      packages/nc-gui-v2/components/cell/Currency.vue
  6. 9
      packages/nc-gui-v2/components/cell/Decimal.vue
  7. 9
      packages/nc-gui-v2/components/cell/Email.vue
  8. 3
      packages/nc-gui-v2/components/cell/Float.vue
  9. 3
      packages/nc-gui-v2/components/cell/Integer.vue
  10. 59
      packages/nc-gui-v2/components/cell/Json.vue
  11. 28
      packages/nc-gui-v2/components/cell/Rating.vue
  12. 8
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  13. 3
      packages/nc-gui-v2/components/cell/Text.vue
  14. 12
      packages/nc-gui-v2/components/cell/TextArea.vue
  15. 11
      packages/nc-gui-v2/components/cell/Url.vue
  16. 5
      packages/nc-gui-v2/components/cell/attachment/Modal.vue
  17. 9
      packages/nc-gui-v2/components/cell/attachment/index.vue
  18. 137
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  19. 45
      packages/nc-gui-v2/components/dashboard/settings/Modal.vue
  20. 14
      packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue
  21. 13
      packages/nc-gui-v2/components/general/Language.vue
  22. 144
      packages/nc-gui-v2/components/general/MiniSidebar.vue
  23. 20
      packages/nc-gui-v2/components/smartsheet-column/AdvancedOptions.vue
  24. 2
      packages/nc-gui-v2/components/smartsheet-column/CheckboxOptions.vue
  25. 2
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  26. 22
      packages/nc-gui-v2/components/smartsheet-column/FormulaOptions.vue
  27. 7
      packages/nc-gui-v2/components/smartsheet-column/LinkedToAnotherRecordOptions.vue
  28. 29
      packages/nc-gui-v2/components/smartsheet-column/LookupOptions.vue
  29. 34
      packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue
  30. 9
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  31. 2
      packages/nc-gui-v2/components/smartsheet-header/Menu.vue
  32. 10
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  33. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  34. 54
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  35. 23
      packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue
  36. 271
      packages/nc-gui-v2/components/smartsheet/Gallery.vue
  37. 41
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  38. 29
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  39. 13
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  40. 26
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  41. 3
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  42. 25
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  43. 3
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/AddRow.vue
  44. 5
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/DeleteTable.vue
  45. 18
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue
  46. 9
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/Reload.vue
  47. 19
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue
  48. 5
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  49. 2
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  50. 49
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  51. 25
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  52. 12
      packages/nc-gui-v2/components/virtual-cell/Formula.vue
  53. 40
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  54. 23
      packages/nc-gui-v2/components/virtual-cell/Lookup.vue
  55. 43
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  56. 5
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  57. 23
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  58. 13
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  59. 104
      packages/nc-gui-v2/components/webhook/Editor.vue
  60. 8
      packages/nc-gui-v2/components/webhook/List.vue
  61. 1
      packages/nc-gui-v2/composables/index.ts
  62. 3
      packages/nc-gui-v2/composables/useColumnCreateStore.ts
  63. 4
      packages/nc-gui-v2/composables/useGlobal/state.ts
  64. 1
      packages/nc-gui-v2/composables/useGlobal/types.ts
  65. 2
      packages/nc-gui-v2/composables/useInjectionState/index.ts
  66. 4
      packages/nc-gui-v2/composables/useLTARStore.ts
  67. 58
      packages/nc-gui-v2/composables/useSidebar/index.ts
  68. 66
      packages/nc-gui-v2/composables/useTableCreate.ts
  69. 12
      packages/nc-gui-v2/composables/useViewData.ts
  70. 101
      packages/nc-gui-v2/layouts/base.vue
  71. 10
      packages/nc-gui-v2/layouts/default.vue
  72. 4
      packages/nc-gui-v2/nuxt.config.ts
  73. 866
      packages/nc-gui-v2/package-lock.json
  74. 4
      packages/nc-gui-v2/package.json
  75. 36
      packages/nc-gui-v2/pages/index/index.vue
  76. 40
      packages/nc-gui-v2/pages/index/user/index.vue
  77. 106
      packages/nc-gui-v2/pages/index/user/index/index.vue
  78. 219
      packages/nc-gui-v2/pages/nc/[projectId]/index.vue
  79. 21
      packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue
  80. 18
      packages/nc-gui-v2/pages/project/index/[id].vue
  81. 13
      packages/nc-gui-v2/pages/project/index/create-external.vue
  82. 22
      packages/nc-gui-v2/pages/project/index/create.vue
  83. 4
      packages/nc-gui-v2/pages/signin.vue
  84. 9
      packages/nc-gui-v2/plugins/state.ts
  85. 1
      packages/nc-gui-v2/tsconfig.json
  86. 1
      packages/nc-gui-v2/utils/viewUtils.ts
  87. 4
      packages/nc-gui-v2/vue-color-shims.d.ts
  88. 2
      packages/nc-gui-v2/windi.config.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 }],
} }

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

@ -1,96 +1,5 @@
<script lang="ts" setup>
import { navigateTo } from '#app'
import { useGlobal } from '#imports'
const state = useGlobal()
const sidebar = ref<HTMLDivElement>()
const email = computed(() => state.user.value?.email ?? '---')
const signOut = () => {
state.signOut()
navigateTo('/signin')
}
const sidebarCollapsed = computed({
get: () => !state.sidebarOpen.value,
set: (val) => (state.sidebarOpen.value = !val),
})
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value
}
</script>
<template> <template>
<a-layout class="min-h-[100vh]"> <NuxtLayout name="base">
<a-layout-header class="flex !bg-primary items-center text-white px-4 shadow-md"> <NuxtPage />
<material-symbols-menu v-if="state.signedIn.value" class="text-xl cursor-pointer" @click="toggleSidebar" /> </NuxtLayout>
<div class="flex-1" />
<div class="ml-4 flex justify-center shrink">
<div class="flex items-center gap-2 cursor-pointer nc-noco-brand-icon" @click="navigateTo('/')">
<img width="35" src="~/assets/img/icons/512x512-trans.png" />
<span class="prose-xl">NocoDB</span>
</div>
</div>
<div class="flex-1 text-left">
<div v-show="state.isLoading.value" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }}
<mdi-reload :class="{ 'animate-infinite animate-spin': state.isLoading.value }" />
</div>
</div>
<div class="flex justify-end gap-4">
<general-language class="mr-3" />
<template v-if="state.signedIn.value">
<a-dropdown :trigger="['click']">
<mdi-dots-vertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />
<template #overlay>
<a-menu class="!py-0 nc-user-menu min-w-32 dark:(!bg-gray-800) leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" />&nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="signOut">
<mdi-logout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</div>
</a-layout-header>
<a-layout>
<a-layout-sider
v-model:collapsed="sidebarCollapsed"
width="300"
collapsed-width="0"
class="bg-white dark:!bg-gray-800 border-r-1 border-gray-200 dark:!border-gray-600 h-full"
:trigger="null"
collapsible
>
<div id="sidebar" ref="sidebar" class="w-full h-full" />
</a-layout-sider>
<NuxtPage />
</a-layout>
</a-layout>
</template> </template>

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

@ -2,7 +2,7 @@
@import 'ant-design-vue/dist/antd.min.css'; @import 'ant-design-vue/dist/antd.min.css';
:root { :root {
--header-height: 56px; --header-height: 50px;
} }
.ant-layout-header { .ant-layout-header {
@ -18,7 +18,7 @@ main {
} }
main { main {
@apply flex-0 w-full relative scrollbar-thin-primary; @apply flex-0 w-full relative scrollbar-thin-dull;
overflow-x: hidden; overflow-x: hidden;
} }
@ -75,7 +75,7 @@ html {
// menu item styling // menu item styling
.nc-menu-item { .nc-menu-item {
@apply cursor-pointer text-xs flex align-center gap-2 px-4 py-3 relative after:(content-[''] absolute top-0 left-0 w-full h-full right 0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5)); @apply cursor-pointer text-xs flex items-center gap-2 px-4 py-3 after:(content-[''] absolute top-0 left-0 bottom-0 w-full h-full right-0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
} }
.nc-sidebar-right-item { .nc-sidebar-right-item {
@ -106,5 +106,5 @@ html {
} }
.ant-modal-wrap { .ant-modal-wrap {
@apply !scrollbar-thin-primary; @apply !scrollbar-thin-dull;
} }

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

@ -24,6 +24,7 @@ declare module '@vue/runtime-core' {
AEmpty: typeof import('ant-design-vue/es')['Empty'] AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form'] AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem'] AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input'] AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
@ -41,14 +42,12 @@ declare module '@vue/runtime-core' {
AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup'] AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup']
AModal: typeof import('ant-design-vue/es')['Modal'] AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination'] APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
ARadio: typeof import('ant-design-vue/es')['Radio'] ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARate: typeof import('ant-design-vue/es')['Rate'] ARate: typeof import('ant-design-vue/es')['Rate']
ARow: typeof import('ant-design-vue/es')['Row'] ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select'] ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASkeletonImage: typeof import('ant-design-vue/es')['SkeletonImage'] ASkeletonImage: typeof import('ant-design-vue/es')['SkeletonImage']
ASpin: typeof import('ant-design-vue/es')['Spin'] ASpin: typeof import('ant-design-vue/es')['Spin']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
@ -62,7 +61,22 @@ 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']
MaterialSymbolsArrowBackRounded: typeof import('~icons/material-symbols/arrow-back-rounded')['default']
MaterialSymbolsArrowForwardRounded: typeof import('~icons/material-symbols/arrow-forward-rounded')['default']
MaterialSymbolsAttachFile: typeof import('~icons/material-symbols/attach-file')['default']
MaterialSymbolsChevronLeftRounded: typeof import('~icons/material-symbols/chevron-left-rounded')['default']
MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsMenu: typeof import('~icons/material-symbols/menu')['default'] MaterialSymbolsMenu: typeof import('~icons/material-symbols/menu')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MdiAccountGroup: typeof import('~icons/mdi/account-group')['default']
MdiApi: typeof import('~icons/mdi/api')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiAt: typeof import('~icons/mdi/at')['default'] MdiAt: typeof import('~icons/mdi/at')['default']
MdiCalculator: typeof import('~icons/mdi/calculator')['default'] MdiCalculator: typeof import('~icons/mdi/calculator')['default']
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default'] MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
@ -70,7 +84,15 @@ declare module '@vue/runtime-core' {
MdiLogout: typeof import('~icons/mdi/logout')['default'] MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiOperator: typeof import('~icons/mdi/operator')['default'] MdiOperator: typeof import('~icons/mdi/operator')['default']
MdiReload: typeof import('~icons/mdi/reload')['default'] MdiReload: typeof import('~icons/mdi/reload')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default'] MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiStar: typeof import('~icons/mdi/star')['default']
MdiStore: typeof import('~icons/mdi/store')['default']
MdiTableBorder: typeof import('~icons/mdi/table-border')['default']
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']
MdiTrashCan: typeof import('~icons/mdi/trash-can')['default']
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['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()

59
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()

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

@ -60,7 +60,7 @@ onKeyDown('Escape', () => {
/** if possible, on mounted we try to fetch the relevant `td` cell to use as a dropzone */ /** if possible, on mounted we try to fetch the relevant `td` cell to use as a dropzone */
onMounted(() => { onMounted(() => {
if (typeof document !== 'undefined') { if (typeof document !== 'undefined') {
dropZoneRef.value = document.querySelector(`td[data-col="${column.id}"]`) as HTMLTableDataCellElement dropZoneRef.value = document.querySelector(`td[data-col="${column.value.id}"]`) as HTMLTableDataCellElement
} }
}) })
</script> </script>
@ -100,14 +100,14 @@ onMounted(() => {
</div> </div>
<template v-if="visibleItems.length"> <template v-if="visibleItems.length">
<div ref="sortableRef" :class="{ dragging }" class="flex flex-wrap gap-2 p-1 scrollbar-thin-primary"> <div ref="sortableRef" :class="{ dragging }" class="flex flex-wrap gap-2 p-1 scrollbar-thin-dull">
<div <div
v-for="(item, i) of visibleItems" v-for="(item, i) of visibleItems"
:id="item.url" :id="item.url"
:key="item.url || item.title" :key="item.url || item.title"
style="flex: 1 1 50px" style="flex: 1 1 50px"
:class="isImage(item.title, item.mimetype) ? '' : 'border-1 rounded'" :class="isImage(item.title, item.mimetype) ? '' : 'border-1 rounded'"
class="nc-attachment flex items-center justify-center" class="nc-attachment flex items-center justify-center min-h-[50px]"
> >
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> <template #title>
@ -116,9 +116,8 @@ onMounted(() => {
<nuxt-img <nuxt-img
v-if="isImage(item.title, item.mimetype)" v-if="isImage(item.title, item.mimetype)"
quality="75"
placeholder placeholder
width="150"
height="150"
:alt="item.title || `#${i}`" :alt="item.title || `#${i}`"
:src="item.url || item.data" :src="item.url || item.data"
class="ring-1 ring-gray-300 rounded" class="ring-1 ring-gray-300 rounded"

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

@ -2,10 +2,8 @@
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import SettingsModal from './settings/SettingsModal.vue' import { useProject, useTable, useTabs, watchEffect } from '#imports'
import { computed, useProject, useTable, useTabs, useUIPermission, watchEffect } from '#imports'
import { useNuxtApp, useRoute } from '#app' import { useNuxtApp, useRoute } from '#app'
import MdiSettingIcon from '~icons/mdi/cog'
import MdiTable from '~icons/mdi/table' import MdiTable from '~icons/mdi/table'
import MdiView from '~icons/mdi/eye-circle-outline' import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large' import MdiTableLarge from '~icons/mdi/table-large'
@ -13,15 +11,17 @@ import MdiMenuDown from '~icons/mdi/chevron-down'
import MdiPlus from '~icons/mdi/plus-circle-outline' import MdiPlus from '~icons/mdi/plus-circle-outline'
import MdiDrag from '~icons/mdi/drag-vertical' import MdiDrag from '~icons/mdi/drag-vertical'
import MdiMenuIcon from '~icons/mdi/dots-vertical' import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiAPIDocIcon from '~icons/mdi/open-in-new'
const { addTab } = useTabs() const { addTab } = useTabs()
const toast = useToast() const toast = useToast()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const route = useRoute() const route = useRoute()
const { tables, loadTables } = useProject(route.params.projectId as string) const { tables, loadTables } = useProject(route.params.projectId as string)
const { closeTab } = useTabs()
const { deleteTable } = useTable() const { deleteTable } = useTable()
const tablesById = $computed<Record<string, TableType>>(() => const tablesById = $computed<Record<string, TableType>>(() =>
@ -31,12 +31,14 @@ const tablesById = $computed<Record<string, TableType>>(() =>
}, {}), }, {}),
) )
const settingsDlg = ref(false)
const showTableList = ref(true) const showTableList = ref(true)
const tableCreateDlg = ref(false) const tableCreateDlg = ref(false)
const tableDeleteDlg = ref(false)
const menuRef = $ref<HTMLLIElement>() const menuRef = $ref<HTMLLIElement>()
let key = $ref(0) let key = $ref(0)
let sortable: Sortable let sortable: Sortable
// todo: replace with vuedraggable // todo: replace with vuedraggable
@ -103,21 +105,14 @@ const icon = (table: TableType) => {
} }
} }
const apiLink = computed(
() =>
// new URL(
`/api/v1/db/meta/projects/${route.params.projectId}/swagger`,
// todo: get siteUrl
// this.$store.state.project.appInfo && this.$store.state.project.appInfo.ncSiteUrl
// ),
)
const filterQuery = $ref('') const filterQuery = $ref('')
const filteredTables = $computed(() => { const filteredTables = $computed(() => {
return tables?.value?.filter((table) => !filterQuery || table?.title.toLowerCase()?.includes(filterQuery.toLowerCase())) return tables?.value?.filter((table) => !filterQuery || table?.title.toLowerCase()?.includes(filterQuery.toLowerCase()))
}) })
const contextMenuTarget = reactive<{ type?: 'table' | 'main'; value?: any }>({}) const contextMenuTarget = reactive<{ type?: 'table' | 'main'; value?: any }>({})
const setMenuContext = (type: 'table' | 'main', value?: any) => { const setMenuContext = (type: 'table' | 'main', value?: any) => {
contextMenuTarget.type = type contextMenuTarget.type = type
contextMenuTarget.value = value contextMenuTarget.value = value
@ -125,16 +120,20 @@ const setMenuContext = (type: 'table' | 'main', value?: any) => {
} }
const renameTableDlg = ref(false) const renameTableDlg = ref(false)
const renameTableMeta = ref() const renameTableMeta = ref()
const showRenameTableDlg = (table: TableType, rightClick = false) => { const showRenameTableDlg = (table: TableType, rightClick = false) => {
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options') $e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
renameTableMeta.value = table renameTableMeta.value = table
renameTableDlg.value = true renameTableDlg.value = true
} }
const reloadTables = async () => { const reloadTables = async () => {
$e('a:table:refresh:navdraw') $e('a:table:refresh:navdraw')
await loadTables() await loadTables()
} }
const addTableTab = (table: TableType) => { const addTableTab = (table: TableType) => {
$e('a:table:open') $e('a:table:open')
addTab({ title: table.title, id: table.id, type: table.type as any }) addTab({ title: table.title, id: table.id, type: table.type as any })
@ -142,8 +141,8 @@ const addTableTab = (table: TableType) => {
</script> </script>
<template> <template>
<div class="nc-treeview-container flex flex-column"> <div class="nc-treeview-container flex flex-col">
<div class="px-3 py-2"> <div class="px-2 py-[11.75px] border-b-1">
<a-input-search <a-input-search
v-model:value="filterQuery" v-model:value="filterQuery"
size="small" size="small"
@ -153,47 +152,73 @@ const addTableTab = (table: TableType) => {
</div> </div>
<a-dropdown :trigger="['contextmenu']"> <a-dropdown :trigger="['contextmenu']">
<div class="p-1 flex-1 overflow-y-auto flex flex-column scrollbar-thin-primary"> <div class="p-1 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull" style="direction: rtl">
<div <div
style="direction: ltr"
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer" class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@click="showTableList = !showTableList" @click="showTableList = !showTableList"
@contextmenu="setMenuContext('main')" @contextmenu="setMenuContext('main')"
> >
<MdiTable class="mr-1 text-gray-500" /> <MdiTable class="mr-1 text-gray-500" />
<span class="flex-grow text-bold nc-project-tree"
>{{ $t('objects.tables') }} <template v-if="tables?.length">({{ tables.length }})</template></span <span class="flex-grow text-bold nc-project-tree">
> {{ $t('objects.tables') }}
<MdiPlus v-t="['c:table:create:navdraw']" class="text-gray-500 nc-btn-tbl-add" @click.stop="tableCreateDlg = true" />
<template v-if="tables?.length"> ({{ tables.length }}) </template>
</span>
<MdiPlus
v-t="['c:table:create:navdraw']"
class="transform text-gray-500 hover:(text-pink-500 scale-105) nc-btn-tbl-add"
@click.stop="tableCreateDlg = true"
/>
<MdiMenuDown <MdiMenuDown
class="transition-transform !duration-100 text-gray-500" class="transition-transform !duration-100 text-gray-500 hover:text-pink-500"
:class="{ 'transform rotate-180': showTableList }" :class="{ 'transform rotate-180': showTableList }"
/> />
</div> </div>
<div class="flex-1"> <div style="direction: ltr" class="flex-1">
<div class="transition-height duration-200 overflow-hidden" :class="{ 'h-100': showTableList, 'h-0': !showTableList }"> <div class="transition-height duration-200 overflow-hidden" :class="{ 'h-100': showTableList, 'h-0': !showTableList }">
<div :key="key" ref="menuRef" class="border-none sortable-list"> <div :key="key" ref="menuRef" class="border-none sortable-list">
<div <div
v-for="table in tables" v-for="table of tables"
:key="table.id" :key="table.id"
v-t="['a:table:open']" v-t="['a:table:open']"
:class="[{ hidden: !filteredTables?.includes(table) }, `nc-project-tree-tbl nc-project-tree-tbl-${table.title}`]" :class="[
class="!pl-1 py-1 !h-[28px] !my-0 text-sm cursor-pointer group" { hidden: !filteredTables?.includes(table) },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,
route.params.title && route.params.title.includes(table.title) ? 'bg-blue-500/15' : '',
]"
class="pl-5 pr-3 py-2 text-sm cursor-pointer group"
:data-order="table.order" :data-order="table.order"
:data-id="table.id" :data-id="table.id"
@click="addTableTab(table)" @click="addTableTab(table)"
> >
<div class="flex align-center gap-1 h-full" @contextmenu="setMenuContext('table', table)"> <div class="flex align-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<MdiDrag <div class="flex w-auto">
:class="`transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 nc-drag-icon cursor-move nc-child-draggable-icon-${table.title}`" <MdiDrag
/> :class="`nc-child-draggable-icon-${table.title}`"
<component :is="icon(table)" class="text-[10px] text-gray-500" /> class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
@click.stop.prevent
/>
<component
:is="icon(table)"
:class="route.params.title && route.params.title.includes(table.title) ? 'text-pink-500' : 'text-gray-500'"
class="nc-view-icon group-hover:hidden text-xs"
/>
</div>
<div class="nc-tbl-title text-xs flex-1">{{ table.title }}</div>
<span class="nc-tbl-title text-xs flex-1 ml-2">{{ table.title }}</span>
<a-dropdown :trigger="['click']" @click.stop> <a-dropdown :trigger="['click']" @click.stop>
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" /> <MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay> <template #overlay>
<a-menu class="cursor-pointer"> <a-menu class="cursor-pointer">
<a-menu-item v-t="" class="!text-xs" @click="showRenameTableDlg(table)"><div>Rename</div></a-menu-item> <a-menu-item v-t="" class="!text-xs" @click="showRenameTableDlg(table)"><div>Rename</div></a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item> <a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item>
</a-menu> </a-menu>
</template> </template>
@ -223,28 +248,13 @@ const addTableTab = (table: TableType) => {
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
<div class="w-full h-[1px] bg-gray-200" />
<a v-if="isUIAllowed('apiDocs')" v-t="['e:api-docs']" class="nc-treeview-footer-item" :href="apiLink" target="_blank">
<MdiAPIDocIcon class="mr-2" />
<span> {{ $t('title.apiDocs') }}</span>
</a>
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-treeview-footer-item nc-team-settings"
@click="settingsDlg = true"
>
<MdiSettingIcon class="mr-2" />
<span> {{ $t('title.teamAndSettings') }}</span>
</div>
<SettingsModal :show="settingsDlg" @closed="settingsDlg = false" />
<DlgTableCreate v-if="tableCreateDlg" v-model="tableCreateDlg" /> <DlgTableCreate v-if="tableCreateDlg" v-model="tableCreateDlg" />
<DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" /> <DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" />
</div> </div>
</template> </template>
<style scoped> <style lang="scss" scoped>
.nc-treeview-container { .nc-treeview-container {
@apply h-[calc(100vh_-_var(--header-height))]; @apply h-[calc(100vh_-_var(--header-height))];
} }
@ -264,4 +274,29 @@ const addTableTab = (table: TableType) => {
:deep(.ant-input-group-addon:last-child) { :deep(.ant-input-group-addon:last-child) {
@apply top-[-0.5px]; @apply top-[-0.5px];
} }
.nc-treeview-container {
.ghost,
.ghost > * {
@apply !pointer-events-none;
}
&.dragging {
.nc-icon {
@apply !hidden;
}
.nc-view-icon {
@apply !block;
}
}
.ant-menu-item:not(.sortable-chosen) {
@apply color-transition hover:!bg-transparent;
}
.sortable-chosen {
@apply !bg-primary/25 text-primary;
}
}
</style> </style>

45
packages/nc-gui-v2/components/dashboard/settings/SettingsModal.vue → packages/nc-gui-v2/components/dashboard/settings/Modal.vue

@ -10,9 +10,11 @@ import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill' import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple' import MultipleTableIcon from '~icons/mdi/table-multiple'
import NootbookOutline from '~icons/mdi/notebook-outline' import NootbookOutline from '~icons/mdi/notebook-outline'
import { useVModel, watch } from '#imports'
interface Props { interface Props {
show: boolean modelValue: boolean
openKey?: string
} }
interface SubTabGroup { interface SubTabGroup {
@ -30,9 +32,11 @@ interface TabGroup {
} }
} }
const { show } = defineProps<Props>() const props = defineProps<Props>()
const emits = defineEmits(['closed']) const emits = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emits)
const tabsInfo: TabGroup = { const tabsInfo: TabGroup = {
teamAndAuth: { teamAndAuth: {
@ -41,11 +45,11 @@ const tabsInfo: TabGroup = {
subTabs: { subTabs: {
usersManagement: { usersManagement: {
title: 'Users Management', title: 'Users Management',
body: () => UserManagement, body: UserManagement,
}, },
apiTokenManagement: { apiTokenManagement: {
title: 'API Token Management', title: 'API Token Management',
body: () => ApiTokenManagement, body: ApiTokenManagement,
}, },
}, },
}, },
@ -55,7 +59,7 @@ const tabsInfo: TabGroup = {
subTabs: { subTabs: {
new: { new: {
title: 'Apps', title: 'Apps',
body: () => AppStore, body: AppStore,
}, },
}, },
}, },
@ -65,11 +69,11 @@ const tabsInfo: TabGroup = {
subTabs: { subTabs: {
metaData: { metaData: {
title: 'Metadata', title: 'Metadata',
body: () => Metadata, body: Metadata,
}, },
acl: { acl: {
title: 'UI Access Control', title: 'UI Access Control',
body: () => UIAcl, body: UIAcl,
}, },
}, },
}, },
@ -79,7 +83,7 @@ const tabsInfo: TabGroup = {
subTabs: { subTabs: {
audit: { audit: {
title: 'Audit', title: 'Audit',
body: () => AuditTab, body: AuditTab,
}, },
}, },
}, },
@ -88,7 +92,7 @@ const tabsInfo: TabGroup = {
const firstKeyOfObject = (obj: object) => Object.keys(obj)[0] const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
// Array of keys of tabs which are selected. In our case will be only one. // Array of keys of tabs which are selected. In our case will be only one.
const selectedTabKeys = $ref<string[]>([firstKeyOfObject(tabsInfo)]) let selectedTabKeys = $ref<string[]>([firstKeyOfObject(tabsInfo)])
const selectedTab = $computed(() => tabsInfo[selectedTabKeys[0]]) const selectedTab = $computed(() => tabsInfo[selectedTabKeys[0]])
let selectedSubTabKeys = $ref<string[]>([firstKeyOfObject(selectedTab.subTabs)]) let selectedSubTabKeys = $ref<string[]>([firstKeyOfObject(selectedTab.subTabs)])
@ -100,18 +104,27 @@ watch(
selectedSubTabKeys = [firstKeyOfObject(tabsInfo[newTabKey].subTabs)] selectedSubTabKeys = [firstKeyOfObject(tabsInfo[newTabKey].subTabs)]
}, },
) )
watch(
() => props.openKey,
(nextOpenKey) => {
selectedTabKeys = [Object.keys(tabsInfo).find((key) => key === nextOpenKey) || firstKeyOfObject(tabsInfo)]
},
)
</script> </script>
<template> <template>
<a-modal :footer="null" :visible="show" width="max(90vw, 600px)" @cancel="emits('closed')"> <a-modal v-model:visible="vModel" :footer="null" width="max(90vw, 600px)" @cancel="emits('update:modelValue', false)">
<a-typography-title class="ml-4 mb-2 select-none" type="secondary" :level="5">SETTINGS</a-typography-title> <a-typography-title class="ml-4 mb-2 select-none" type="secondary" :level="5">SETTINGS</a-typography-title>
<a-layout class="mt-3 modal-body">
<a-layout class="mt-3 modal-body flex">
<!-- Side tabs --> <!-- Side tabs -->
<a-layout-sider theme="light"> <a-layout-sider theme="light">
<a-menu v-model:selectedKeys="selectedTabKeys" class="h-full" mode="inline" :open-keys="[]"> <a-menu v-model:selected-keys="selectedTabKeys" class="h-full" mode="inline" :open-keys="[]">
<a-menu-item v-for="(tab, key) of tabsInfo" :key="key"> <a-menu-item v-for="(tab, key) of tabsInfo" :key="key">
<div class="flex flex-row items-center space-x-2"> <div class="flex flex-row items-center space-x-2">
<component :is="tab.icon" class="flex" /> <component :is="tab.icon" class="flex" />
<div class="flex select-none"> <div class="flex select-none">
{{ tab.title }} {{ tab.title }}
</div> </div>
@ -121,14 +134,14 @@ watch(
</a-layout-sider> </a-layout-sider>
<!-- Sub Tabs --> <!-- Sub Tabs -->
<a-layout-content class="h-full px-4 scrollbar-thumb-gray-500"> <a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500">
<a-menu v-model:selectedKeys="selectedSubTabKeys" :open-keys="[]" mode="horizontal"> <a-menu v-model:selectedKeys="selectedSubTabKeys" :open-keys="[]" mode="horizontal">
<a-menu-item v-for="(tab, key) of selectedTab.subTabs" :key="key" class="select-none"> <a-menu-item v-for="(tab, key) of selectedTab.subTabs" :key="key" class="select-none">
{{ tab.title }} {{ tab.title }}
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<component :is="selectedSubTab.body()" class="px-2 py-6" /> <component :is="selectedSubTab.body" class="px-2 py-6" />
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</a-modal> </a-modal>
@ -136,6 +149,6 @@ watch(
<style scoped> <style scoped>
.modal-body { .modal-body {
@apply h-[70vh]; @apply min-h-[75vh];
} }
</style> </style>

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)

13
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))
@ -36,12 +34,13 @@ onMounted(() => {
<template> <template>
<v-menu class="select-none"> <v-menu class="select-none">
<template #activator="{ props }"> <template #activator="{ props }">
<MaterialSymbolsTranslate class="md:text-xl cursor-pointer nc-menu-translate" @click="props.onClick" /> <MaterialSymbolsTranslate v-bind="$attrs" 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"

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

@ -0,0 +1,144 @@
<script lang="ts" setup>
import { breakpointsTailwind } from '@vueuse/core'
import { navigateTo } from '#app'
import { computed, useBreakpoints, useGlobal, useProject, useRoute, useSidebar } from '#imports'
/** get current breakpoints (for enabling sidebar) */
const breakpoints = useBreakpoints(breakpointsTailwind)
const { signOut, signedIn, isLoading, user } = useGlobal()
const { isOpen } = useSidebar({ isOpen: true })
const { project } = useProject()
const route = useRoute()
const email = computed(() => user.value?.email ?? '---')
const logout = () => {
signOut()
navigateTo('/signin')
}
</script>
<template>
<a-layout-sider
:collapsed="isOpen"
width="50"
collapsed-width="0"
class="nc-mini-sidebar !bg-primary h-full"
:trigger="null"
collapsible
theme="light"
>
<a-dropdown placement="bottom" :trigger="['click']">
<div class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105">
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<template v-if="signedIn" #overlay>
<a-menu class="ml-2 !py-0 min-w-32 leading-8 !rounded">
<a-menu-item-group title="User Settings">
<a-menu-item key="email" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" />
&nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item key="signout" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout">
<MdiLogout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu-item-group>
</a-menu>
</template>
</a-dropdown>
<div id="sidebar" ref="sidebar" class="text-white flex-auto flex flex-col items-center w-full">
<a-dropdown :trigger="['contextmenu']" placement="right">
<div :class="[route.name === 'index' ? 'active' : '']" class="nc-mini-sidebar-item" @click="navigateTo('/')">
<MdiFolder class="cursor-pointer transform hover:scale-105 text-2xl" />
</div>
<template #overlay>
<a-menu class="mt-6 select-none !py-0 min-w-32 leading-8 !rounded">
<a-menu-item-group>
<template #title>
<span class="cursor-pointer prose-sm text-gray-500 hover:text-primary" @click="navigateTo('/')">
{{ $t('objects.projects') }}
</span>
</template>
<a-menu-item class="active:(ring ring-pink-500)">
<div
v-t="['c:project:create:xcdb']"
class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create')"
>
<MdiPlus class="text-lg group-hover:text-pink-500" />
{{ $t('activity.createProject') }}
</div>
</a-menu-item>
<a-menu-item class="rounded-b active:(ring ring-pink-500)">
<div
v-t="['c:project:create:extdb']"
class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create-external')"
>
<MdiDatabaseOutline class="text-lg group-hover:text-pink-500" />
<div v-html="$t('activity.createProjectExtended.extDB')" />
</div>
</a-menu-item>
</a-menu-item-group>
</a-menu>
</template>
</a-dropdown>
<a-tooltip placement="right">
<template v-if="project" #title>{{ project.title }}</template>
<div
:class="[route.name.includes('nc-projectId') ? 'active' : 'pointer-events-none !text-gray-400']"
class="nc-mini-sidebar-item"
@click="navigateTo(`/nc/${route.params.projectId}`)"
>
<MdiDatabase class="cursor-pointer transform hover:scale-105 text-2xl" />
</div>
</a-tooltip>
</div>
</a-layout-sider>
</template>
<style lang="scss" scoped>
.nc-mini-sidebar {
:deep(.ant-layout-sider-children) {
@apply flex flex-col items-center;
}
.nc-mini-sidebar-item {
@apply flex w-full justify-center items-center h-12 group p-2;
&.active {
@apply bg-pink-500 border-t-1 border-b-1;
}
}
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
</style>

20
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
@ -21,7 +23,7 @@ formState.value.au = !!formState.value.au
v-model:checked="formState.rqd" v-model:checked="formState.rqd"
:disabled="formState.pk || !sqlUi.columnEditable(formState)" :disabled="formState.pk || !sqlUi.columnEditable(formState)"
size="small" size="small"
class="nc-column-name-input" class="nc-column-checkbox-NN"
@change="onAlter" @change="onAlter"
/> />
</a-form-item> </a-form-item>
@ -30,7 +32,7 @@ formState.value.au = !!formState.value.au
v-model:checked="formState.pk" v-model:checked="formState.pk"
:disabled="!sqlUi.columnEditable(formState)" :disabled="!sqlUi.columnEditable(formState)"
size="small" size="small"
class="nc-column-name-input" class="nc-column-checkbox-PK"
@change="onAlter" @change="onAlter"
/> />
</a-form-item> </a-form-item>
@ -39,7 +41,7 @@ formState.value.au = !!formState.value.au
v-model:checked="formState.ai" v-model:checked="formState.ai"
:disabled="sqlUi.colPropUNDisabled(formState) || !sqlUi.columnEditable(formState)" :disabled="sqlUi.colPropUNDisabled(formState) || !sqlUi.columnEditable(formState)"
size="small" size="small"
class="nc-column-name-input" class="nc-column-checkbox-AI"
@change="onAlter" @change="onAlter"
/> />
</a-form-item> </a-form-item>
@ -48,14 +50,14 @@ formState.value.au = !!formState.value.au
:disabled="sqlUi.colPropUNDisabled(formState) || !sqlUi.columnEditable(formState)" :disabled="sqlUi.colPropUNDisabled(formState) || !sqlUi.columnEditable(formState)"
@change="onAlter" @change="onAlter"
> >
<a-checkbox v-model:checked="formState.un" size="small" class="nc-column-name-input" /> <a-checkbox v-model:checked="formState.un" size="small" class="nc-column-checkbox-UN" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
label="AU" label="AU"
:disabled="sqlUi.colPropAuDisabled(formState) || !sqlUi.columnEditable(formState)" :disabled="sqlUi.colPropAuDisabled(formState) || !sqlUi.columnEditable(formState)"
@change="onAlter" @change="onAlter"
> >
<a-checkbox v-model:checked="formState.au" size="small" class="nc-column-name-input" /> <a-checkbox v-model:checked="formState.au" size="small" class="nc-column-checkbox-AU" />
</a-form-item> </a-form-item>
</div> </div>
<a-form-item :label="$t('labels.databaseType')" v-bind="validateInfos.dt"> <a-form-item :label="$t('labels.databaseType')" v-bind="validateInfos.dt">
@ -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 = [

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

@ -117,7 +117,7 @@ if (!formState.value?.column_name) {
v-model:value="formState.uidt" v-model:value="formState.uidt"
show-search show-search
size="small" size="small"
class="nc-column-name-input" class="nc-column-type-input"
@change="onUidtOrIdTypeChange" @change="onUidtOrIdTypeChange"
> >
<a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt"> <a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">

22
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,
@ -29,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)
@ -70,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,
@ -229,7 +226,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`)
@ -240,7 +237,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
@ -255,7 +252,8 @@ 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?.value?.id as string]: [targetFormulaCol.id], [column?.value?.id as string]: [targetFormulaCol.id],
}) })
@ -266,6 +264,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]
@ -345,12 +344,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) {
@ -662,6 +663,7 @@ onMounted(() => {
</a-col> </a-col>
</div> </div>
</template> </template>
<template #avatar> <template #avatar>
<mdi-function v-if="item.type === 'function'" class="text-lg" /> <mdi-function v-if="item.type === 'function'" class="text-lg" />
<mdi-calculator v-if="item.type === 'op'" class="text-lg" /> <mdi-calculator v-if="item.type === 'op'" class="text-lg" />

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>

29
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 (
.map((c) => ({ meta.columns
col: c.colOptions, ?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions?.type !== 'bt' && !c.system)
column: c, .map((c) => ({
...tables.find((t) => t.id === c.colOptions.fk_related_model_id), col: c.colOptions,
})) column: c,
.filter((table) => table.col.fk_related_model_id === table.id && !table.mm) ...tables.find((t) => t.id === (c.colOptions as any)?.fk_related_model_id),
}))
.filter((table: any) => table.col?.fk_related_model_id === table.id && !table.mm) ?? []
)
}) })
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>

34
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
.map((c) => ({ ?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== 'bt' && !c.system)
col: c.colOptions, .map((c) => ({
column: c, col: c.colOptions,
...tables.find((t) => t.id === c.colOptions.fk_related_model_id), column: c,
})) ...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>

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

@ -5,8 +5,9 @@ import { inject, toRef } from 'vue'
import { ColumnInj, MetaInj } from '~/context' import { ColumnInj, MetaInj } from '~/context'
import { useProvideColumnCreateStore } from '#imports' import { useProvideColumnCreateStore } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any } }>() const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean }>()
const column = toRef(props, 'column') const column = toRef(props, 'column')
const hideMenu = toRef(props, 'hideMenu')
provide(ColumnInj, column) provide(ColumnInj, column)
const meta = inject(MetaInj) const meta = inject(MetaInj)
@ -20,8 +21,10 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<SmartsheetHeaderCellIcon v-if="column" /> <SmartsheetHeaderCellIcon v-if="column" />
<span v-if="column" class="name" style="white-space: nowrap" :title="column.title">{{ column.title }}</span> <span v-if="column" class="name" style="white-space: nowrap" :title="column.title">{{ column.title }}</span>
<div class="flex-1" /> <template v-if="!hideMenu">
<SmartsheetHeaderMenu /> <div class="flex-1" />
<SmartsheetHeaderMenu />
</template>
</div> </div>
</template> </template>

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

@ -65,7 +65,7 @@ function onVisibleChange() {
</template> </template>
</a-dropdown> </a-dropdown>
<a-dropdown :trigger="['hover']"> <a-dropdown :trigger="['hover']">
<MdiMenuDownIcon class="text-grey" /> <MdiMenuDownIcon class="text-grey nc-ui-dt-dropdown" />
<template #overlay> <template #overlay>
<div class="shadow bg-white"> <div class="shadow bg-white">
<div class="nc-column-edit nc-menu-item" @click="editColumnDropdown = true"> <div class="nc-column-edit nc-menu-item" @click="editColumnDropdown = true">

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

@ -8,8 +8,10 @@ import { useMetas } from '~/composables'
import { ColumnInj, MetaInj } from '~/context' import { ColumnInj, MetaInj } from '~/context'
import { provide, useProvideColumnCreateStore } from '#imports' import { provide, useProvideColumnCreateStore } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any } }>() const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean }>()
const column = toRef(props, 'column') const column = toRef(props, 'column')
const hideMenu = toRef(props, 'hideMenu')
provide(ColumnInj, column) provide(ColumnInj, column)
const { metas } = useMetas() const { metas } = useMetas()
const meta = inject(MetaInj) const meta = inject(MetaInj)
@ -95,9 +97,11 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<!-- <span class="caption" v-html="tooltipMsg" /> --> <!-- <span class="caption" v-html="tooltipMsg" /> -->
<!-- </v-tooltip> --> <!-- </v-tooltip> -->
<v-spacer /> <template v-if="!hideMenu">
<v-spacer />
<SmartsheetHeaderMenu :virtual="true" /> <SmartsheetHeaderMenu :virtual="true" />
</template>
</div> </div>
</template> </template>

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>

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

@ -1,7 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk' import { isVirtualCol } from 'nocodb-sdk'
import { inject, provide, useViewData } from '#imports' import { inject, provide, useViewData } from '#imports'
import { ActiveViewInj, ChangePageInj, IsFormInj, IsGridInj, MetaInj, PaginationDataInj, ReadonlyInj } from '~/context' import { ActiveViewInj, ChangePageInj, FieldsInj, IsFormInj, IsGridInj, MetaInj, PaginationDataInj, ReadonlyInj } from '~/context'
import ImageIcon from '~icons/mdi/file-image-box'
interface Attachment {
url: string
}
const meta = inject(MetaInj) const meta = inject(MetaInj)
const view = inject(ActiveViewInj) const view = inject(ActiveViewInj)
@ -12,7 +17,7 @@ const isPublicView = false
const selected = reactive<{ row?: number | null; col?: number | null }>({}) const selected = reactive<{ row?: number | null; col?: number | null }>({})
const editEnabled = ref(false) const editEnabled = ref(false)
const { loadData, paginationData, formattedData: data, updateRowProperty, changePage } = useViewData(meta, view as any) const { loadData, paginationData, formattedData: data, loadGalleryData, galleryData, changePage } = useViewData(meta, view as any)
provide(IsFormInj, false) provide(IsFormInj, false)
provide(IsGridInj, false) provide(IsGridInj, false)
@ -20,227 +25,85 @@ provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage) provide(ChangePageInj, changePage)
provide(ReadonlyInj, true) provide(ReadonlyInj, true)
const fields = inject(FieldsInj, ref([]))
const coverImageColumn = $(computed(() => fields.value.find((col) => col.id === galleryData.value?.fk_cover_image_col_id)))
watch( watch(
[meta, view], [meta, view],
async () => { async () => {
if (meta?.value && view?.value) { if (meta?.value && view?.value) {
await loadData() await loadData()
await loadGalleryData()
} }
}, },
{ immediate: true }, { immediate: true },
) )
const isRowEmpty = (record: any, col: any) => {
const val = record.row[col.title]
if (!val) return true
return Array.isArray(val) && val.length === 0
}
const attachments = (record: any): Array<Attachment> => {
try {
return JSON.parse(record.row[coverImageColumn?.title]) ?? []
} catch (e) {
return []
}
}
</script> </script>
<!-- TODO: Fix scrolling -->
<template> <template>
<v-container fluid class="nc-gallery-container"> <div class="flex flex-col h-full min-h-0 w-full">
<!-- <v-row class="align-stretch"> <div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3">
<v-col v-for="({ row }, rowIndex) in data" :key="rowIndex" <div v-for="(record, recordIndex) in data" :key="recordIndex" class="flex flex-col">
md="4" <a-card hoverable class="!rounded-lg h-full">
lg="3" <template #cover>
sm="6" <a-carousel v-if="attachments(record).length !== 0" autoplay>
xs="12"> --> <img
v-for="(attachment, index) in attachments(record)"
<!-- <v-hover v-slot="{hover}"> --> :key="index"
class="h-52 rounded-t-lg"
<div class="flex flex-wrap gap-4 justify-center"> :src="attachment.url"
<div v-for="({ row }, rowIndex) in data" :key="rowIndex" class="md:w-[300px] lg:w-[400px] xl:w-[500px]"> />
<v-card class="h-100"> </a-carousel>
<!-- :elevation="hover ? 4 : 1" <ImageIcon v-else class="w-full h-48 my-4 text-cool-gray-200" />
:ripple="!isLocked" </template>
@click="!isLocked && $emit('expandForm', {row,rowIndex,rowMeta})"
> --> <div
<!-- <v-carousel v-for="(col, colIndex) in fields"
v-if="attachmentColumn" :key="colIndex"
:continuous="false" class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full"
:cycle="true" >
:show-arrows="false" <div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">
hide-delimiter-background <div class="w-full text-gray-600">
delimiter-icon="mdi-minus" <SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
height="200" <SmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
>
<v-carousel-item
v-for="(cover, i) in getCovers(row)"
:key="i"
>
<v-img
height="200"
:src="cover.url"
:alt="cover.title"
/>
</v-carousel-item>
</v-carousel> -->
<!-- <v-card-title -->
<!-- class="text-capitalize" -->
<!-- v-text="row[primaryValueColumn]" -->
<!-- /> -->
<v-card-text>
<v-container fluid>
<!-- <v-row class="">
<v-col v-for="col in meta.columns" :key="col.title"
class="col-12"> -->
<div v-for="col in meta.columns" :key="col.title" class="my-8">
<!-- <v-col v-for="col in fields" v-show="showFields[col.title]" :key="col.title" class="col-12 mt-1 mb-2"> -->
<!--
todo:header cell
-->
<label :for="`data-table-form-${col.title}`" class="body-2 text-capitalize caption grey--text">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />
<!-- <virtual-header-cell
v-if="isVirtualCol(col)"
:column="col"
:nodes="nodes"
:is-form="true"
:meta="meta"
/>
<header-cell
v-else
:is-form="true"
:value="col.title"
:column="col"
/> -->
</label>
<div class="mt-2">
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row[col.title]" :edit-enabled="false" :column="col" />
<SmartsheetCell
v-else
v-model="row[col.title]"
:edit-enabled="false"
:column="col"
@update:model-value="updateRowProperty(row, col.title)"
/>
</div>
<!-- <virtual-cell
v-if="isVirtualCol(col)"
ref="virtual"
:column="col"
:row="row"
:nodes="nodes"
:meta="meta"
/>
<table-cell
v-else
:value="row[col.title]"
:column="col"
:sql-ui="sqlUi"
:is-locked="isLocked"
class="xc-input body-2"
:meta="meta"
/> -->
<!-- </v-col> -->
<!-- </v-row> -->
</div> </div>
</v-container> </div>
</v-card-text>
</v-card> <div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start">
<div v-if="isRowEmpty(record, col)" class="h-3 bg-gray-200 px-5 rounded-lg"></div>
<template v-else>
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="record.row[col.title]" :column="col" />
<SmartsheetCell v-else v-model="record.row[col.title]" :column="col" :edit-enabled="false" />
</template>
</div>
</div>
</a-card>
</div> </div>
</div> </div>
<SmartsheetPagination />
<!-- </v-hover> --> </div>
<!-- </v-col>
</v-row> -->
</v-container>
</template> </template>
<!--
<script>
import { isVirtualCol } from "nocodb-sdk";
import VirtualHeaderCell from "../components/VirtualHeaderCell";
import HeaderCell from "../components/HeaderCell";
import VirtualCell from "../components/VirtualCell";
import TableCell from "../components/Cell";
export default {
name: "GalleryView",
components: {
TableCell,
VirtualCell,
HeaderCell,
VirtualHeaderCell
},
props: [
"nodes",
"table",
"showFields",
"availableColumns",
"meta",
"data",
"primaryValueColumn",
"showSystemFields",
"sqlUi",
"coverImageField",
"viewId",
"isLocked"
],
data() {
return {
galleryView: {}
};
},
computed: {
attachmentColumn() {
return this.coverImageField && this.meta && this.meta.columns && this.meta.columns.find(c => c.id === this.coverImageField);
},
fields() {
if (this.availableColumns) {
return this.availableColumns;
}
const hideCols = ["created_at", "updated_at"];
if (this.showSystemFields) {
return this.meta.columns || [];
} else {
return this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.title) &&
!((this.meta.v || []).some(v => v.bt && v.bt.title === c.title))
) || [];
}
}
},
watch: {
async coverImageField(v) {
if (this.galleryView && v !== this.galleryView.fk_cover_image_col_id) {
(await this.$api.dbView.galleryUpdate(this.viewId, {
...this.galleryView,
fk_cover_image_col_id: v
}));
}
}
},
created() {
this.loadView();
},
methods: {
isVirtualCol,
async loadView() {
this.galleryView = (await this.$api.dbView.galleryRead(this.viewId));
this.$emit("update:coverImageField", this.galleryView.fk_cover_image_col_id);
},
getCovers(row) {
if (this.attachmentColumn &&
row[this.attachmentColumn.title] && row[this.attachmentColumn.title][0] &&
row[this.attachmentColumn.title]) {
try {
return JSON.parse(row[this.attachmentColumn.title]);
} catch (e) {
}
}
return [{ url: "https://via.placeholder.com/700?text=No%20image%20found" }];
}
}
};
</script>
<style scoped>
</style>
-->
<style scoped> <style scoped>
.nc-gallery-container { .nc-gallery-container {
height: calc(100vh - 160px); height: calc(100vh - 250px);
overflow: auto; overflow: auto;
} }
</style> </style>

41
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)
@ -254,7 +251,7 @@ const onNavigate = (dir: NavigateDir) => {
<template> <template>
<div class="flex flex-col h-100 min-h-0 w-100"> <div class="flex flex-col h-100 min-h-0 w-100">
<div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-primary"> <div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull">
<a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']"> <a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']">
<table ref="smartTable" class="xc-row-table nc-grid backgroundColorDefault" @contextmenu.prevent="contextMenu = true"> <table ref="smartTable" class="xc-row-table nc-grid backgroundColorDefault" @contextmenu.prevent="contextMenu = true">
<thead> <thead>
@ -276,6 +273,7 @@ const onNavigate = (dir: NavigateDir) => {
:key="col.title" :key="col.title"
v-xc-ver-resize v-xc-ver-resize
:data-col="col.id" :data-col="col.id"
:data-title="col.title"
@xcresize="onresize(col.id, $event)" @xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)" @xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null" @xcresized="resizingCol = null"
@ -287,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" />
@ -297,22 +295,23 @@ const onNavigate = (dir: NavigateDir) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(row, rowIndex) in data" :key="rowIndex" class="nc-grid-row group"> <tr v-for="(row, rowIndex) of data" :key="rowIndex" class="nc-grid-row">
<td key="row-index" class="caption nc-grid-cell"> <td key="row-index" class="caption nc-grid-cell group">
<div class="align-center flex w-[80px]"> <div class="flex items-center w-[80px]">
<div class="group-hover:hidden" :class="{ hidden: row.rowMeta.selected }">{{ rowIndex + 1 }}</div> <div class="group-hover:hidden" :class="{ hidden: row.rowMeta.selected }">{{ rowIndex + 1 }}</div>
<div <div
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }" :class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="group-hover:flex w-full align-center" class="group-hover:flex w-full items-center justify-between p-1"
> >
<a-checkbox v-model:checked="row.rowMeta.selected" /> <a-checkbox v-model:checked="row.rowMeta.selected" />
<span class="flex-1" /> <div class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:bg-primary/10">
<MdiArrowExpandIcon class="text-sm text-pink hidden group-hover:inline-block" /> <MdiArrowExpand class="select-none transform hover:(text-pink-500 scale-120)" />
</div>
</div> </div>
</div> </div>
</td> </td>
<td <td
v-for="(columnObj, colIndex) in fields" v-for="(columnObj, colIndex) of fields"
:key="rowIndex + columnObj.title" :key="rowIndex + columnObj.title"
class="cell pointer nc-grid-cell" class="cell pointer nc-grid-cell"
:class="{ :class="{
@ -355,17 +354,13 @@ const onNavigate = (dir: NavigateDir) => {
class="text-left pointer nc-grid-add-new-cell" class="text-left pointer nc-grid-add-new-cell"
@click="addEmptyRow()" @click="addEmptyRow()"
> >
<a-tooltip top left> <div class="px-2 w-full flex items-center">
<div class="w-min flex align-center"> <MdiPlus class="text-pint-500 text-xs" />
<MdiPlusIcon class="text-pint-500 text-xs" />
<span class="ml-1 caption grey--text"> <span class="ml-1">
{{ $t('activity.addRow') }} {{ $t('activity.addRow') }}
</span> </span>
</div> </div>
<template #title>
<span class="caption"> Add new row</span>
</template>
</a-tooltip>
</td> </td>
</tr> </tr>
</tbody> </tbody>

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

@ -15,37 +15,10 @@ const page = computed({
get: () => paginatedData?.value?.page ?? 1, get: () => paginatedData?.value?.page ?? 1,
set: (p) => changePage?.(p), set: (p) => changePage?.(p),
}) })
/*
export default {
name: 'Pagination',
props: {
count: [Number, String],
value: [Number, String],
size: [Number, String],
},
data: () => ({
page: 1,
}),
watch: {
value(v) {
this.page = v
},
count(c) {
const page = Math.max(1, Math.min(this.page, Math.ceil(c / this.size)))
if (this.value !== page) {
this.$emit('input', page)
}
},
},
mounted() {
this.page = this.value
},
}
*/
</script> </script>
<template> <template>
<div class="flex items-center mb-2"> <div class="flex items-center">
<span v-if="count !== null && count !== Infinity" class="caption ml-2"> {{ count }} record{{ count !== 1 ? 's' : '' }} </span> <span v-if="count !== null && count !== Infinity" class="caption ml-2"> {{ count }} record{{ count !== 1 ? 's' : '' }} </span>
<div class="flex-1" /> <div class="flex-1" />

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

@ -2,11 +2,6 @@
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import { ref, useNuxtApp } from '#imports' import { ref, useNuxtApp } from '#imports'
import { viewIcons } from '~/utils' import { viewIcons } from '~/utils'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiXml from '~icons/mdi/xml'
import MdiHook from '~icons/mdi/hook'
import MdiHeartsCard from '~icons/mdi/cards-heart'
import MdiShieldLockOutline from '~icons/mdi/shield-lock-outline'
interface Emits { interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void (event: 'openModal', data: { type: ViewTypes; title?: string }): void
@ -53,7 +48,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<div class="flex-1" /> <div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" /> <MdiPlus class="group-hover:text-primary" />
</div> </div>
</a-tooltip> </a-tooltip>
</a-menu-item> </a-menu-item>
@ -71,7 +66,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<div class="flex-1" /> <div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" /> <MdiPlus class="group-hover:text-primary" />
</div> </div>
</a-tooltip> </a-tooltip>
</a-menu-item> </a-menu-item>
@ -89,7 +84,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<div class="flex-1" /> <div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" /> <MdiPlus class="group-hover:text-primary" />
</div> </div>
</a-tooltip> </a-tooltip>
</a-menu-item> </a-menu-item>
@ -137,7 +132,7 @@ function onOpenModal(type: ViewTypes, title = '') {
class="group flex items-center gap-2 w-full mx-3 px-4 py-2 rounded-l !bg-primary text-white transform translate-x-4 hover:(translate-x-0 shadow-lg !opacity-100) transition duration-150 ease" class="group flex items-center gap-2 w-full mx-3 px-4 py-2 rounded-l !bg-primary text-white transform translate-x-4 hover:(translate-x-0 shadow-lg !opacity-100) transition duration-150 ease"
@click.stop @click.stop
> >
<MdiHeartsCard class="text-red-500" /> <MdiCardsHeart class="text-red-500" />
{{ $t('activity.sponsorUs') }} {{ $t('activity.sponsorUs') }}
</a> </a>
</template> </template>

26
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, useRoute, 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,14 +22,12 @@ 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()
const route = useRoute()
/** Selected view(s) for menu */ /** Selected view(s) for menu */
const selected = ref<string[]>([]) const selected = ref<string[]>([])
@ -123,7 +121,6 @@ async function onSortEnd(evt: SortableEvent) {
let sortable: Sortable let sortable: Sortable
// todo: replace with vuedraggable
const initSortable = (el: HTMLElement) => { const initSortable = (el: HTMLElement) => {
if (sortable) sortable.destroy() if (sortable) sortable.destroy()
@ -197,7 +194,10 @@ function onDeleted() {
:key="view.id" :key="view.id"
:view="view" :view="view"
class="transition-all ease-in duration-300" class="transition-all ease-in duration-300"
:class="[isMarked === view.id ? 'bg-gray-200' : '']" :class="[
isMarked === view.id ? 'bg-gray-200' : '',
route.params.viewTitle && route.params.viewTitle.includes(view.title) ? 'active' : '',
]"
@change-view="changeView" @change-view="changeView"
@open-modal="$emit('openModal', $event)" @open-modal="$emit('openModal', $event)"
@delete="onDelete" @delete="onDelete"
@ -210,7 +210,7 @@ function onDeleted() {
<style lang="scss"> <style lang="scss">
.nc-views-menu { .nc-views-menu {
@apply flex-1 max-h-[20vh] overflow-y-scroll scrollbar-thin-primary; @apply flex-1 max-h-[30vh] overflow-y-scroll scrollbar-thin-dull;
.ghost, .ghost,
.ghost > * { .ghost > * {
@ -234,5 +234,13 @@ function onDeleted() {
.sortable-chosen { .sortable-chosen {
@apply !bg-primary/25 text-primary; @apply !bg-primary/25 text-primary;
} }
.active {
@apply bg-blue-500/15;
.nc-icon {
@apply !text-pink-500;
}
}
} }
</style> </style>

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

@ -2,9 +2,6 @@
import type { ViewTypes } from 'nocodb-sdk' import type { ViewTypes } from 'nocodb-sdk'
import { viewIcons } from '~/utils' import { viewIcons } from '~/utils'
import { useDebounceFn, useNuxtApp, useVModel } from '#imports' import { useDebounceFn, useNuxtApp, useVModel } from '#imports'
import MdiTrashCan from '~icons/mdi/trash-can'
import MdiContentCopy from '~icons/mdi/content-copy'
import MdiDrag from '~icons/mdi/drag-vertical'
interface Props { interface Props {
view: Record<string, any> view: Record<string, any>

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

@ -5,8 +5,6 @@ import MenuBottom from './MenuBottom.vue'
import Toolbar from './toolbar/index.vue' import Toolbar from './toolbar/index.vue'
import { computed, inject, provide, ref, useApi, useRoute, useViews, watch } from '#imports' import { computed, inject, provide, ref, useApi, useRoute, useViews, watch } from '#imports'
import { ActiveViewInj, MetaInj, RightSidebarInj, ViewListInj } from '~/context' import { ActiveViewInj, MetaInj, RightSidebarInj, ViewListInj } from '~/context'
import MdiXml from '~icons/mdi/xml'
import MdiHook from '~icons/mdi/hook'
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
@ -73,10 +71,29 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
collapsiple collapsiple
collapsed-width="50" collapsed-width="50"
width="250" width="250"
class="shadow !mt-[-9px]" class="relative shadow-md h-full"
style="height: calc(100% + 9px)"
theme="light" theme="light"
> >
<a-tooltip placement="left">
<template #title> Toggle sidebar </template>
<div
class="group color-transition cursor-pointer hover:ring active:ring-pink-500 z-1 flex items-center p-[1px] absolute top-1/2 left-[-1rem] shadow bg-gray-100 rounded-full"
>
<MaterialSymbolsChevronRightRounded
v-if="sidebarOpen"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
@click="sidebarOpen = false"
/>
<MaterialSymbolsChevronLeftRounded
v-else
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
@click="sidebarOpen = true"
/>
</div>
</a-tooltip>
<Toolbar v-if="sidebarOpen" class="flex items-center py-3 px-3 justify-between border-b-1" /> <Toolbar v-if="sidebarOpen" class="flex items-center py-3 px-3 justify-between border-b-1" />
<Toolbar v-else class="py-3 px-2 max-w-[50px] flex !flex-col-reverse gap-4 items-center mt-[-1px]"> <Toolbar v-else class="py-3 px-2 max-w-[50px] flex !flex-col-reverse gap-4 items-center mt-[-1px]">

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

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import MdiAddIcon from '~icons/mdi/plus-outline'
import { inject, ref } from '#imports' import { inject, ref } from '#imports'
import { RightSidebarInj } from '~/context' import { RightSidebarInj } from '~/context'
@ -13,7 +12,7 @@ const sidebarOpen = inject(RightSidebarInj, ref(true))
<template #title> {{ $t('activity.addRow') }} </template> <template #title> {{ $t('activity.addRow') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-primary/75 group"> <div class="nc-sidebar-right-item hover:after:bg-primary/75 group">
<MdiAddIcon class="group-hover:(!text-white)" @click="emits('addRow')" /> <MdiPlusOutline class="group-hover:(!text-white)" @click="emits('addRow')" />
</div> </div>
</a-tooltip> </a-tooltip>
</template> </template>

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>

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

@ -1,12 +1,10 @@
<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'
import MdiCheckIcon from '~icons/mdi/check-bold'
enum LockType { enum LockType {
Personal = 'personal', Personal = 'personal',
@ -25,19 +23,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:
@ -60,7 +58,7 @@ const Icon = computed(() => {
<div> <div>
<div class="nc-menu-item" @click="changeLockType(LockType.Collaborative)"> <div class="nc-menu-item" @click="changeLockType(LockType.Collaborative)">
<div> <div>
<MdiCheckIcon v-if="!view?.lock_type || view?.lock_type === LockType.Collaborative" /> <MdiCheck v-if="!view?.lock_type || view?.lock_type === LockType.Collaborative" />
<span v-else /> <span v-else />
<div> <div>
<MdiAccountGroupIcon /> <MdiAccountGroupIcon />
@ -71,7 +69,7 @@ const Icon = computed(() => {
</div> </div>
<div class="nc-menu-item" @click="changeLockType(LockType.Locked)"> <div class="nc-menu-item" @click="changeLockType(LockType.Locked)">
<div> <div>
<MdiCheckIcon v-if="view.lock_type === LockType.Locked" /> <MdiCheck v-if="view.lock_type === LockType.Locked" />
<span v-else /> <span v-else />
<div> <div>
<MdiLockOutlineIcon /> <MdiLockOutlineIcon />
@ -82,7 +80,7 @@ const Icon = computed(() => {
</div> </div>
<div class="nc-menu-item" @click="changeLockType(LockType.Personal)"> <div class="nc-menu-item" @click="changeLockType(LockType.Personal)">
<div> <div>
<MdiCheckIcon v-if="view.lock_type === LockType.Personal" /> <MdiCheck v-if="view.lock_type === LockType.Personal" />
<span v-else /> <span v-else />
<div> <div>
<MdiAccountIcon /> <MdiAccountIcon />

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>

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

@ -1,19 +0,0 @@
<script setup lang="ts">
import MdiUnfoldMoreVertical from '~icons/mdi/unfold-more-vertical'
import MdiUnfoldLessVertical from '~icons/mdi/unfold-less-vertical'
import { inject, ref } from '#imports'
import { RightSidebarInj } from '~/context'
const sidebarOpen = inject(RightSidebarInj, ref(false))
</script>
<template>
<a-tooltip :placement="sidebarOpen ? 'bottomRight' : 'left'">
<template #title> {{ $t('tooltip.toggleNavDraw') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-pink-500 group">
<MdiUnfoldLessVertical v-if="sidebarOpen" class="group-hover:(!text-white)" @click="sidebarOpen = false" />
<MdiUnfoldMoreVertical v-else class="group-hover:(!text-white)" @click="sidebarOpen = true" />
</div>
</a-tooltip>
</template>

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

@ -3,7 +3,6 @@ import AddRow from './AddRow.vue'
import DeleteTable from './DeleteTable.vue' import DeleteTable from './DeleteTable.vue'
import LockMenu from './LockMenu.vue' import LockMenu from './LockMenu.vue'
import Reload from './Reload.vue' import Reload from './Reload.vue'
import ToggleDrawer from './ToggleDrawer.vue'
</script> </script>
<template> <template>
@ -24,10 +23,6 @@ import ToggleDrawer from './ToggleDrawer.vue'
<DeleteTable /> <DeleteTable />
<div class="dot" />
<ToggleDrawer />
<slot name="end" /> <slot name="end" />
</div> </div>
</template> </template>

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

@ -60,7 +60,7 @@ watch(tabMeta, async (newTabMeta, oldTabMeta) => {
</div> </div>
</div> </div>
<teleport to="#sidebar-right"> <teleport to="#content">
<SmartsheetSidebar /> <SmartsheetSidebar />
</teleport> </teleport>
</template> </template>

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">

104
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,
{ notification: {
...hook, ...hook.notification,
notification: { payload: hook.notification.payload,
...hook.notification, },
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>

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

@ -1,6 +1,7 @@
export * from './useApi' export * from './useApi'
export * from './useGlobal' export * from './useGlobal'
export * from './useInjectionState' export * from './useInjectionState'
export * from './useSidebar'
export * from './useUIPermission' export * from './useUIPermission'
export * from './useAttachment' export * from './useAttachment'
export * from './useColors' export * from './useColors'

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/useGlobal/state.ts

@ -74,9 +74,6 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
/** reactive token payload */ /** reactive token payload */
const { payload } = useJwt<JwtPayload & User>(token) const { payload } = useJwt<JwtPayload & User>(token)
/** is sidebar open */
const sidebarOpen = ref(false)
/** currently running requests */ /** currently running requests */
const runningRequests = useCounter() const runningRequests = useCounter()
@ -88,7 +85,6 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
storage, storage,
token, token,
jwtPayload: payload, jwtPayload: payload,
sidebarOpen,
timestamp, timestamp,
runningRequests, runningRequests,
error, error,

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

@ -24,7 +24,6 @@ export type State = ToRefs<Omit<StoredState, 'token'>> & {
storage: Ref<StoredState> storage: Ref<StoredState>
token: WritableComputedRef<StoredState['token']> token: WritableComputedRef<StoredState['token']>
jwtPayload: ComputedRef<(JwtPayload & User) | null> jwtPayload: ComputedRef<(JwtPayload & User) | null>
sidebarOpen: Ref<boolean>
timestamp: Ref<number> timestamp: Ref<number>
runningRequests: ReturnType<typeof useCounter> runningRequests: ReturnType<typeof useCounter>
error: Ref<any> error: Ref<any>

2
packages/nc-gui-v2/composables/useInjectionState/index.ts

@ -14,7 +14,7 @@ export function useInjectionState<Arguments extends any[], Return>(
return providedState return providedState
} }
const useInjectedState = () => inject(key) const useInjectedState = () => inject(key, undefined)
return [useProvidingState, useInjectedState] return [useProvidingState, useInjectedState]
} }

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),

58
packages/nc-gui-v2/composables/useSidebar/index.ts

@ -0,0 +1,58 @@
import { useInjectionState, useToggle, watch } from '#imports'
interface UseSidebarProps {
hasSidebar?: boolean
isOpen?: boolean
}
/**
* Injection state for sidebars
*
* Use `provideSidebar` to provide the injection state on current component level (will affect all children injections)
* Use `useSidebar` to use the injection state on current component level
*
* If `provideSidebar` is not called explicitly, `useSidebar` will trigger the provider if no injection state can be found
*/
const [setup, use] = useInjectionState((props: UseSidebarProps = {}) => {
const [isOpen, toggle] = useToggle(props.isOpen ?? false)
const [hasSidebar, toggleHasSidebar] = useToggle(props.hasSidebar ?? true)
watch(
hasSidebar,
(nextHasSidebar) => {
if (!nextHasSidebar) toggle(false)
},
{ immediate: true },
)
watch(
isOpen,
(nextIsOpen) => {
if (nextIsOpen && !hasSidebar.value) toggleHasSidebar(true)
},
{ immediate: true },
)
return {
isOpen,
toggle,
hasSidebar,
toggleHasSidebar,
}
}, 'useSidebar')
export const provideSidebar = setup
export function useSidebar(props: UseSidebarProps = {}) {
const state = use()
if (!state) {
return setup(props)
} else {
// set state if props were passed
if (typeof props.isOpen !== 'undefined') state.isOpen.value = props.isOpen
if (typeof props.hasSidebar !== 'undefined') state.hasSidebar.value = props.hasSidebar
}
return state
}

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 }
}

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

@ -1,4 +1,4 @@
import type { Api, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { Api, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { notification } from 'ant-design-vue' import { notification } from 'ant-design-vue'
import { useNuxtApp } from '#app' import { useNuxtApp } from '#app'
@ -26,6 +26,7 @@ export function useViewData(
) { ) {
const formattedData = ref<Row[]>([]) const formattedData = ref<Row[]>([])
const paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 }) const paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 })
const galleryData = ref<GalleryType | undefined>(undefined)
const { project } = useProject() const { project } = useProject()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -58,6 +59,13 @@ export function useViewData(
formattedData.value = formatData(response.list) formattedData.value = formatData(response.list)
paginationData.value = response.pageInfo paginationData.value = response.pageInfo
} }
const loadGalleryData = async () => {
if (!viewMeta?.value?.id) return
galleryData.value = await $api.dbView.galleryRead(viewMeta.value.id)
}
const insertRow = async (row: Record<string, any>, rowIndex = formattedData.value?.length) => { const insertRow = async (row: Record<string, any>, rowIndex = formattedData.value?.length) => {
try { try {
const insertObj = meta?.value?.columns?.reduce((o: any, col) => { const insertObj = meta?.value?.columns?.reduce((o: any, col) => {
@ -248,5 +256,7 @@ export function useViewData(
updateOrSaveRow, updateOrSaveRow,
selectedAllRecords, selectedAllRecords,
syncCount, syncCount,
galleryData,
loadGalleryData,
} }
} }

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

@ -0,0 +1,101 @@
<script lang="ts" setup>
import { breakpointsTailwind } from '@vueuse/core'
import { navigateTo } from '#app'
import { computed, useBreakpoints, useGlobal, useProject, useRoute } from '#imports'
/** get current breakpoints (for enabling sidebar) */
const breakpoints = useBreakpoints(breakpointsTailwind)
const { signOut, signedIn, isLoading, user } = useGlobal()
const { project } = useProject()
const route = useRoute()
const email = computed(() => user.value?.email ?? '---')
const logout = () => {
signOut()
navigateTo('/signin')
}
</script>
<template>
<a-layout id="nc-app" has-sider>
<div id="nc-sidebar-left" />
<a-layout class="!flex-col">
<a-layout-header class="flex !bg-primary items-center text-white pl-1 pr-4 shadow-lg">
<div
v-if="route.name === 'index'"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105"
@click="navigateTo('/')"
>
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<div class="flex justify-center">
<div v-show="isLoading" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
</div>
</div>
<div class="flex-1" />
<a-tooltip placement="left">
<template #title> Switch language </template>
<div class="flex pr-4 items-center">
<GeneralLanguage class="cursor-pointer text-2xl" />
</div>
</a-tooltip>
<template v-if="signedIn">
<a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />
<template #overlay>
<a-menu class="!py-0 nc-user-menu dark:(!bg-gray-800) leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" />&nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span>
</nuxt-link>
</a-menu-item>
<a-menu-divider class="!m-0" />
<a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout">
<MdiLogout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-layout-header>
<div class="w-full" style="height: calc(100% - var(--header-height))">
<slot />
</div>
</a-layout>
</a-layout>
</template>
<style lang="scss" scoped>
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
</style>

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

@ -18,11 +18,13 @@ export default {
</script> </script>
<template> <template>
<a-layout-content class="pl-2 pt-2"> <div class="w-full h-full">
<teleport v-if="$slots.sidebar" to="#sidebar"> <teleport v-if="$slots.sidebar" to="#nc-sidebar-left">
<slot name="sidebar" /> <slot name="sidebar" />
</teleport> </teleport>
<slot /> <a-layout-content>
</a-layout-content> <slot />
</a-layout-content>
</div>
</template> </template>

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

@ -101,4 +101,8 @@ export default defineNuxtConfig({
}, },
}, },
}, },
image: {
dir: 'assets/',
},
}) })

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

File diff suppressed because it is too large Load Diff

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

@ -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",
@ -61,7 +61,7 @@
"happy-dom": "^6.0.3", "happy-dom": "^6.0.3",
"less": "^4.1.3", "less": "^4.1.3",
"nuxt": "3.0.0-rc.4", "nuxt": "3.0.0-rc.4",
"nuxt-windicss": "^2.4.2", "nuxt-windicss": "^2.5.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"sass": "^1.53.0", "sass": "^1.53.0",
"unplugin-icons": "^0.14.7", "unplugin-icons": "^0.14.7",

36
packages/nc-gui-v2/pages/index/index.vue

@ -3,7 +3,7 @@ import { Modal } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk' import type { ProjectType } from 'nocodb-sdk'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { navigateTo } from '#app' import { navigateTo } from '#app'
import { computed, onMounted } from '#imports' import { computed, onMounted, ref, useApi, useNuxtApp, useSidebar } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils' import { extractSdkResponseErrorMsg } from '~/utils'
import MdiDeleteOutline from '~icons/mdi/delete-outline' import MdiDeleteOutline from '~icons/mdi/delete-outline'
import MdiEditOutline from '~icons/mdi/edit-outline' import MdiEditOutline from '~icons/mdi/edit-outline'
@ -12,30 +12,31 @@ import MdiMenuDown from '~icons/mdi/menu-down'
import MdiPlus from '~icons/mdi/plus' import MdiPlus from '~icons/mdi/plus'
import MdiDatabaseOutline from '~icons/mdi/database-outline' import MdiDatabaseOutline from '~icons/mdi/database-outline'
const { $api, $state, $e } = useNuxtApp() const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
useSidebar({ hasSidebar: true, isOpen: true })
const toast = useToast() const toast = useToast()
const filterQuery = ref('') const filterQuery = ref('')
const loading = ref(true)
const projects = ref<ProjectType[]>() const projects = ref<ProjectType[]>()
const loadProjects = async () => { const loadProjects = async () => {
loading.value = true const response = await api.project.list({})
const response = await $api.project.list({})
projects.value = response.list projects.value = response.list
loading.value = false
} }
const filteredProjects = computed(() => { const filteredProjects = computed(
return ( () =>
projects.value?.filter( projects.value?.filter(
(project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()), (project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()),
) ?? [] ) ?? [],
) )
})
const deleteProject = (project: ProjectType) => { const deleteProject = (project: ProjectType) => {
$e('c:project:delete')
Modal.confirm({ Modal.confirm({
title: `Do you want to delete '${project.title}' project?`, title: `Do you want to delete '${project.title}' project?`,
okText: 'Yes', okText: 'Yes',
@ -45,9 +46,9 @@ const deleteProject = (project: ProjectType) => {
try { try {
$e('c:project:delete') $e('c:project:delete')
await $api.project.delete(project.id as string) await $api.project.delete(project.id as string)
projects.value?.splice(projects.value.indexOf(project), 1) return projects.value?.splice(projects.value.indexOf(project), 1)
} catch (e) { } catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e)) return toast.error(await extractSdkResponseErrorMsg(e))
} }
}, },
}) })
@ -56,9 +57,6 @@ const deleteProject = (project: ProjectType) => {
onMounted(() => { onMounted(() => {
loadProjects() loadProjects()
}) })
// hide sidebar
$state.sidebarOpen.value = false
</script> </script>
<template> <template>
@ -114,7 +112,7 @@ $state.sidebarOpen.value = false
</a-dropdown> </a-dropdown>
</div> </div>
<div v-if="loading"> <div v-if="isLoading">
<a-skeleton /> <a-skeleton />
</div> </div>

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

@ -6,50 +6,10 @@ import MdiFolderOutline from '~icons/mdi/folder-outline'
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const route = useRoute() const route = useRoute()
const navDrawerOptions = [
{
title: 'My NocoDB',
route: '/',
icon: MdiFolderOutline,
},
{
title: 'Settings',
route: '/user',
icon: MdiAccountCog,
},
]
const selectedKey = computed(() => [navDrawerOptions.findIndex((opt) => opt.route === route.path)])
</script> </script>
<template> <template>
<NuxtLayout> <NuxtLayout>
<template #sidebar>
<div class="flex flex-col h-full">
<a-menu :selected-keys="selectedKey" class="pr-4 dark:bg-gray-800 dark:text-white flex-1 border-0">
<a-menu-item
v-for="(option, index) in navDrawerOptions"
:key="index"
class="!rounded-r-lg"
@click="navigateTo(option.route)"
>
<div class="flex items-center gap-4">
<component :is="option.icon" />
<span class="font-semibold">
{{ option.title }}
</span>
</div>
</a-menu-item>
</a-menu>
<general-social />
<general-sponsors :nav="true" />
</div>
</template>
<NuxtPage /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
</template> </template>

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

@ -1,13 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' import { extractSdkResponseErrorMsg } from '~/utils'
import { useNuxtApp } from '#app' import { reactive, ref, useApi } from '#imports'
import { reactive, ref } from '#imports'
import MaterialSymbolsWarning from '~icons/material-symbols/warning' import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import MdiKeyChange from '~icons/mdi/key-change' import MdiKeyChange from '~icons/mdi/key-change'
const { $api, $state } = useNuxtApp() const { api, isLoading } = useApi()
const { t } = useI18n() const { t } = useI18n()
@ -52,7 +51,7 @@ const passwordChange = async () => {
error = null error = null
try { try {
const { msg } = await $api.auth.passwordChange({ const { msg } = await api.auth.passwordChange({
currentPassword: form.currentPassword, currentPassword: form.currentPassword,
newPassword: form.password, newPassword: form.password,
}) })
@ -70,60 +69,61 @@ const resetError = () => {
</script> </script>
<template> <template>
<a-form ref="formValidator" layout="vertical" :model="form" class="change-password h-full w-full" @finish="passwordChange"> <div class="mt-4 w-1/2 mx-auto">
<div <a-form ref="formValidator" layout="vertical" :model="form" class="change-password" @finish="passwordChange">
class="md:relative flex flex-col gap-2 w-full h-full p-8 lg:(max-w-1/2)" <div class="md:relative flex flex-col gap-2 w-full h-full p-8 w-full">
:class="{ 'mx-auto': !$state.sidebarOpen.value }" <h1 class="prose-2xl font-bold mb-4">{{ $t('activity.changePwd') }}</h1>
>
<h1 class="prose-2xl font-bold mb-4">{{ $t('activity.changePwd') }}</h1> <Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<Transition name="layout"> <div class="flex items-center gap-2 justify-center"><MaterialSymbolsWarning /> {{ error }}</div>
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1"> </div>
<div class="flex items-center gap-2 justify-center"><MaterialSymbolsWarning /> {{ error }}</div> </Transition>
<a-form-item :label="$t('placeholder.password.current')" name="currentPassword" :rules="formRules.currentPassword">
<a-input-password
v-model:value="form.currentPassword"
size="large"
class="password"
:placeholder="$t('placeholder.password.current')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.new')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
size="large"
class="password"
:placeholder="$t('placeholder.password.new')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.confirm')" name="passwordRepeat" :rules="formRules.passwordRepeat">
<a-input-password
v-model:value="form.passwordRepeat"
size="large"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@focus="resetError"
/>
</a-form-item>
<div class="flex flex-wrap gap-4 items-center mt-4 md:justify-between w-full">
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiKeyChange /> {{ $t('activity.changePwd') }}</span>
</button>
</div> </div>
</Transition>
<a-form-item :label="$t('placeholder.password.current')" name="currentPassword" :rules="formRules.currentPassword">
<a-input-password
v-model:value="form.currentPassword"
size="large"
class="password"
:placeholder="$t('placeholder.password.current')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.new')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
size="large"
class="password"
:placeholder="$t('placeholder.password.new')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.confirm')" name="passwordRepeat" :rules="formRules.passwordRepeat">
<a-input-password
v-model:value="form.passwordRepeat"
size="large"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@focus="resetError"
/>
</a-form-item>
<div class="flex flex-wrap gap-4 items-center mt-4 md:justify-between w-full">
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiKeyChange /> {{ $t('activity.changePwd') }}</span>
</button>
</div> </div>
</div> </a-form>
</a-form> </div>
</template> </template>
<style lang="scss"> <style lang="scss">
.change-password { .change-password {
@apply border-1 shadow-md rounded;
.ant-input-affix-wrapper, .ant-input-affix-wrapper,
.ant-input { .ant-input {
@apply dark:(!bg-gray-700 !text-white) !appearance-none my-1 border-1 border-solid border-primary/50 rounded; @apply dark:(!bg-gray-700 !text-white) !appearance-none my-1 border-1 border-solid border-primary/50 rounded;

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

@ -1,29 +1,234 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTabs } from '#imports' import { navigateTo, provideSidebar, ref, useProject, useRoute, useSidebar, useTabs, useUIPermission } from '#imports'
import { TabType } from '~/composables' import { TabType } from '~/composables'
import { openLink } from '~/utils'
const route = useRoute() const route = useRoute()
const { loadProject, loadTables } = useProject(route.params.projectId as string)
const { project, loadProject, loadTables } = useProject(route.params.projectId as string)
const { addTab, clearTabs } = useTabs() const { addTab, clearTabs } = useTabs()
const { $state } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
// set old sidebar state
useSidebar({ isOpen: true })
// create a new sidebar state
const { isOpen, toggle } = provideSidebar({ isOpen: true })
const dialogOpen = ref(false)
const openDialogKey = ref<string>()
clearTabs() clearTabs()
if (!route.params.type) { if (!route.params.type) {
addTab({ type: TabType.AUTH, title: 'Team & Auth' }) addTab({ type: TabType.AUTH, title: 'Team & Auth' })
} }
function toggleDialog(value?: boolean, key?: string) {
dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key
}
await loadProject(route.params.projectId as string) await loadProject(route.params.projectId as string)
await loadTables()
$state.sidebarOpen.value = true await loadTables()
</script> </script>
<template> <template>
<NuxtLayout> <NuxtLayout id="content" class="flex">
<template #sidebar> <template #sidebar>
<DashboardTreeView /> <a-layout-sider
:collapsed="!isOpen"
width="250"
collapsed-width="50"
class="relative shadow-md h-full z-1"
:trigger="null"
collapsible
theme="light"
>
<div style="height: var(--header-height)" class="flex items-center !bg-primary text-white px-1 gap-2">
<div
v-if="isOpen"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105"
@click="navigateTo('/')"
>
<img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<a-dropdown :trigger="['click']">
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"
class="group cursor-pointer flex gap-4 items-center"
>
<template v-if="isOpen">
<div class="text-xl font-semibold truncate">{{ project.title }}</div>
<MdiChevronDown class="min-w-[28.5px] group-hover:text-pink-500 text-2xl" />
</template>
<template v-else>
<MdiFolder class="text-primary cursor-pointer transform hover:scale-105 text-2xl" />
</template>
</div>
<template #overlay>
<a-menu class="ml-6 !w-[300px] !text-sm !p-0 !rounded">
<a-menu-item-group>
<template #title>
<div class="group select-none flex items-center gap-4 py-1">
<MdiFolder class="group-hover:text-pink-500 text-xl" />
<div class="flex flex-col">
<div class="text-lg group-hover:(!text-primary) font-semibold truncate">{{ project.title }}</div>
<div class="text-xs group-hover:text-pink-500 truncate font-italic">{{ project.id }}</div>
</div>
</div>
</template>
<a-menu-item key="copy">
<div class="nc-project-menu-item group">
<MdiContentCopy class="group-hover:text-pink-500" />
Copy Project Info
</div>
</a-menu-item>
<a-menu-item key="api">
<div
v-if="isUIAllowed('apiDocs')"
v-t="['e:api-docs']"
class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`)"
>
<MdiApi class="group-hover:text-pink-500" />
Swagger: Rest APIs
</div>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="teamAndAuth">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')"
>
<MdiAccountGroup class="group-hover:text-pink-500" />
Team & Auth
</div>
</a-menu-item>
<a-menu-item key="appStore">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'appStore')"
>
<MdiStore class="group-hover:text-pink-500" />
App Store
</div>
</a-menu-item>
<a-menu-item key="metaData">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'metaData')"
>
<MdiTableBorder class="group-hover:text-pink-500" />
Project Metadata
</div>
</a-menu-item>
<a-menu-item key="audit">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'audit')"
>
<MdiNotebookCheckOutline class="group-hover:text-pink-500" />
Audit
</div>
</a-menu-item>
<a-menu-divider />
<a-sub-menu key="preview-as">
<template #title>
<div class="nc-project-menu-item group">
<MdiContentCopy class="group-hover:text-pink-500" />
Preview Project As
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-menu-item> Foo </a-menu-item>
</a-sub-menu>
</a-menu-item-group>
</a-menu>
</template>
</a-dropdown>
</div>
<a-tooltip placement="right">
<template #title> Toggle table list </template>
<div
class="group color-transition cursor-pointer hover:ring active:ring-pink-500 z-1 flex items-center absolute top-1/2 right-[-0.75rem] shadow bg-gray-100 rounded-full"
>
<MaterialSymbolsChevronLeftRounded
v-if="isOpen"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
@click="toggle(false)"
/>
<MaterialSymbolsChevronRightRounded
v-else
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400"
@click="toggle(true)"
/>
</div>
</a-tooltip>
<DashboardTreeView v-show="isOpen" />
</a-layout-sider>
</template> </template>
<dashboard-settings-modal v-model="dialogOpen" :open-key="openDialogKey" />
<NuxtPage /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
</template> </template>
<style lang="scss" scoped>
.nc-project-menu-item {
@apply cursor-pointer flex items-center gap-2 py-2 hover:text-primary after:(content-[''] absolute top-0 left-0 bottom-0 right-0 w-full h-full bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500);
}
</style>

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

@ -34,9 +34,9 @@ function openQuickImportDialog(type: string) {
</script> </script>
<template> <template>
<div class="flex w-full h-full"> <div class="h-full w-full nc-container pt-[9px]">
<div class="nc-container flex flex-col"> <div class="h-full w-full flex flex-col">
<div> <div class="px-2">
<a-tabs v-model:activeKey="activeTabIndex" type="editable-card" @edit="closeTab"> <a-tabs v-model:activeKey="activeTabIndex" type="editable-card" @edit="closeTab">
<a-tab-pane v-for="(tab, i) in tabs" :key="i" :tab="tab.title" /> <a-tab-pane v-for="(tab, i) in tabs" :key="i" :tab="tab.title" />
@ -129,23 +129,18 @@ function openQuickImportDialog(type: string) {
</a-tabs> </a-tabs>
</div> </div>
<div class="flex-1 min-h-0"> <NuxtPage class="px-4 pt-2" />
<NuxtPage />
</div>
<DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" />
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" :import-type="importType" />
<DlgAirtableImport v-if="airtableImportDialog" v-model="airtableImportDialog" />
</div> </div>
<div id="sidebar-right" class="h-full" /> <DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" />
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" :import-type="importType" />
<DlgAirtableImport v-if="airtableImportDialog" v-model="airtableImportDialog" />
</div> </div>
</template> </template>
<style scoped> <style scoped>
.nc-container { .nc-container {
height: calc(100vh - var(--header-height) - 8px); height: calc(100% - var(--header-height));
@apply overflow-hidden;
flex: 1 1 100%; flex: 1 1 100%;
} }

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

@ -8,11 +8,14 @@ import { navigateTo, useNuxtApp, useRoute } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { projectTitleValidator } from '~/utils/validation' import { projectTitleValidator } from '~/utils/validation'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline' import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
import { nextTick, reactive, useSidebar } from '#imports'
const loading = ref(false) const { api, isLoading } = useApi()
useSidebar({ hasSidebar: false })
const { $api, $state } = useNuxtApp()
const toast = useToast() const toast = useToast()
const route = useRoute() const route = useRoute()
const nameValidationRules = [ const nameValidationRules = [
@ -35,27 +38,24 @@ const getProject = async () => {
toast.error(await extractSdkResponseErrorMsg(e)) toast.error(await extractSdkResponseErrorMsg(e))
} }
} }
const renameProject = async () => { const renameProject = async () => {
loading.value = true
try { try {
await $api.project.update(route.params.id as string, formState) await api.project.update(route.params.id as string, formState)
navigateTo(`/nc/${route.params.id}`) navigateTo(`/nc/${route.params.id}`)
} catch (e: any) { } catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e)) toast.error(await extractSdkResponseErrorMsg(e))
} }
loading.value = false
} }
const form = ref<typeof Form>() const form = ref<typeof Form>()
// hide sidebar
$state.sidebarOpen.value = false
// select and focus title field on load // select and focus title field on load
onMounted(async () => { onMounted(async () => {
await getProject() await getProject()
nextTick(() => {
await nextTick(() => {
// todo: replace setTimeout and follow better approach // todo: replace setTimeout and follow better approach
setTimeout(() => { setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]') const input = form.value?.$el?.querySelector('input[type=text]')

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

@ -3,7 +3,7 @@ import { onMounted } from '@vue/runtime-core'
import { Form, Modal } from 'ant-design-vue' import { Form, Modal } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { ref } from '#imports' import { computed, ref, useSidebar, watch } from '#imports'
import { navigateTo, useNuxtApp } from '#app' import { navigateTo, useNuxtApp } from '#app'
import { ClientType } from '~/lib' import { ClientType } from '~/lib'
import type { ProjectCreateForm } from '~/utils' import type { ProjectCreateForm } from '~/utils'
@ -23,8 +23,12 @@ const useForm = Form.useForm
const loading = ref(false) const loading = ref(false)
const testSuccess = ref(false) const testSuccess = ref(false)
const { $api, $e, $state } = useNuxtApp() const { $api, $e } = useNuxtApp()
useSidebar({ hasSidebar: false })
const toast = useToast() const toast = useToast()
const { t } = useI18n() const { t } = useI18n()
const formState = $ref<ProjectCreateForm>({ const formState = $ref<ProjectCreateForm>({
@ -66,7 +70,7 @@ const validators = computed(() => {
} }
}) })
const { resetFields, validate, validateInfos } = useForm(formState, validators) const { validate, validateInfos } = useForm(formState, validators)
const onClientChange = () => { const onClientChange = () => {
formState.dataSource = { ...getDefaultConnectionConfig(formState.dataSource.client) } formState.dataSource = { ...getDefaultConnectionConfig(formState.dataSource.client) }
@ -192,9 +196,6 @@ const testConnection = async () => {
} }
} }
// hide sidebar
$state.sidebarOpen.value = false
// reset test status on config change // reset test status on config change
watch( watch(
() => formState.dataSource, () => formState.dataSource,

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

@ -2,17 +2,22 @@
import { onMounted, onUpdated } from '@vue/runtime-core' import { onMounted, onUpdated } from '@vue/runtime-core'
import type { Form } from 'ant-design-vue' import type { Form } from 'ant-design-vue'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import { nextTick, ref } from '#imports' import { nextTick, reactive, ref, useApi, useSidebar } from '#imports'
import { navigateTo, useNuxtApp } from '#app' import { navigateTo, useNuxtApp } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { projectTitleValidator } from '~/utils/validation' import { projectTitleValidator } from '~/utils/validation'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline' import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
const name = ref('') const name = ref('')
const loading = ref(false)
const valid = ref(false) const valid = ref(false)
const { $api, $state, $e } = useNuxtApp() const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
useSidebar({ hasSidebar: false })
const toast = useToast() const toast = useToast()
const nameValidationRules = [ const nameValidationRules = [
@ -29,9 +34,8 @@ const formState = reactive({
const createProject = async () => { const createProject = async () => {
$e('a:project:create:xcdb') $e('a:project:create:xcdb')
loading.value = true
try { try {
const result = await $api.project.create({ const result = await api.project.create({
title: formState.title, title: formState.title,
}) })
@ -39,17 +43,13 @@ const createProject = async () => {
} catch (e: any) { } catch (e: any) {
toast.error(await extractSdkResponseErrorMsg(e)) toast.error(await extractSdkResponseErrorMsg(e))
} }
loading.value = false
} }
const form = ref<typeof Form>() const form = ref<typeof Form>()
// hide sidebar
$state.sidebarOpen.value = false
// select and focus title field on load // select and focus title field on load
onMounted(async () => { onMounted(async () => {
nextTick(() => { await nextTick(() => {
// todo: replace setTimeout and follow better approach // todo: replace setTimeout and follow better approach
setTimeout(() => { setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]') const input = form.value?.$el?.querySelector('input[type=text]')
@ -61,7 +61,7 @@ onMounted(async () => {
</script> </script>
<template> <template>
<a-card class="w-[500px] mx-auto !mt-100px shadow-md"> <a-card :loading="isLoading" class="w-[500px] mx-auto !mt-100px shadow-md">
<h3 class="text-3xl text-center font-semibold mb-2">{{ $t('activity.createProject') }}</h3> <h3 class="text-3xl text-center font-semibold mb-2">{{ $t('activity.createProject') }}</h3>
<a-form ref="form" :model="formState" name="basic" layout="vertical" autocomplete="off" @finish="createProject"> <a-form ref="form" :model="formState" name="basic" layout="vertical" autocomplete="off" @finish="createProject">

4
packages/nc-gui-v2/pages/signin.vue

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import type { RuleObject } from 'ant-design-vue/es/form' import type { RuleObject } from 'ant-design-vue/es/form'
import { definePageMeta } from '#imports' import { definePageMeta, useSidebar } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { navigateTo, useNuxtApp } from '#app' import { navigateTo, useNuxtApp } from '#app'
import { isEmail } from '~/utils/validation' import { isEmail } from '~/utils/validation'
@ -12,6 +12,8 @@ const { $api, $state } = $(useNuxtApp())
const { t } = useI18n() const { t } = useI18n()
useSidebar({ hasSidebar: false })
definePageMeta({ definePageMeta({
requiresAuth: false, requiresAuth: false,
title: 'title.headLogin', title: 'title.headLogin',

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

@ -1,6 +1,5 @@
import { breakpointsTailwind } from '@vueuse/core'
import { defineNuxtPlugin } from '#app' import { defineNuxtPlugin } from '#app'
import { useBreakpoints, useDark, useGlobal, watch } from '#imports' import { useDark, useGlobal, watch } from '#imports'
/** /**
* Initialize global state and watches for changes * Initialize global state and watches for changes
@ -19,9 +18,6 @@ export default defineNuxtPlugin((nuxtApp) => {
const darkMode = useDark() const darkMode = useDark()
/** get current breakpoints (for enabling sidebar) */
const breakpoints = useBreakpoints(breakpointsTailwind)
/** set i18n locale to stored language */ /** set i18n locale to stored language */
nuxtApp.vueApp.i18n.locale.value = state.lang.value nuxtApp.vueApp.i18n.locale.value = state.lang.value
@ -33,7 +29,4 @@ export default defineNuxtPlugin((nuxtApp) => {
}, },
{ immediate: true }, { immediate: true },
) )
/** is initial sidebar open */
state.sidebarOpen.value = state.signedIn.value && breakpoints.greater('md').value
}) })

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

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

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

@ -9,7 +9,6 @@ import MdiEyeIcon from '~icons/mdi/eye-circle-outline'
export const viewIcons = { export const viewIcons = {
[ViewTypes.GRID]: { icon: MdiGridIcon, color: 'blue' }, [ViewTypes.GRID]: { icon: MdiGridIcon, color: 'blue' },
// [ViewTypes.GRID]: { icon: "mdi-grid-large", color: "blue" },
[ViewTypes.FORM]: { icon: MdiFormIcon, color: 'pink' }, [ViewTypes.FORM]: { icon: MdiFormIcon, color: 'pink' },
calendar: { icon: MdiCalendarIcon, color: 'purple' }, calendar: { icon: MdiCalendarIcon, color: 'purple' },
[ViewTypes.GALLERY]: { icon: MdiGalleryIcon, color: 'orange' }, [ViewTypes.GALLERY]: { icon: MdiGalleryIcon, color: 'orange' },

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
}

2
packages/nc-gui-v2/windi.config.ts

@ -44,6 +44,8 @@ export default defineConfig({
'color-transition': 'transition-color duration-100 ease-in', 'color-transition': 'transition-color duration-100 ease-in',
'scrollbar-thin-primary': 'scrollbar-thin-primary':
'scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-primary scrollbar-track-white dark:(!scrollbar-track-black)', 'scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-primary scrollbar-track-white dark:(!scrollbar-track-black)',
'scrollbar-thin-dull':
'scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-gray-300 scrollbar-track-white dark:(!scrollbar-track-black)',
}, },
theme: { theme: {

Loading…
Cancel
Save