Browse Source

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

pull/3129/head
Wing-Kam Wong 2 years ago
parent
commit
bf7164e5bd
  1. 12
      packages/nc-gui-v2/components.d.ts
  2. 18
      packages/nc-gui-v2/components/cell/Checkbox.vue
  3. 5
      packages/nc-gui-v2/components/cell/Currency.vue
  4. 10
      packages/nc-gui-v2/components/cell/DatePicker.vue
  5. 12
      packages/nc-gui-v2/components/cell/DateTimePicker.vue
  6. 5
      packages/nc-gui-v2/components/cell/Decimal.vue
  7. 21
      packages/nc-gui-v2/components/cell/Duration.vue
  8. 6
      packages/nc-gui-v2/components/cell/Email.vue
  9. 5
      packages/nc-gui-v2/components/cell/Float.vue
  10. 5
      packages/nc-gui-v2/components/cell/Integer.vue
  11. 5
      packages/nc-gui-v2/components/cell/Json.vue
  12. 11
      packages/nc-gui-v2/components/cell/MultiSelect.vue
  13. 9
      packages/nc-gui-v2/components/cell/Percent.vue
  14. 10
      packages/nc-gui-v2/components/cell/Rating.vue
  15. 10
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  16. 5
      packages/nc-gui-v2/components/cell/Text.vue
  17. 5
      packages/nc-gui-v2/components/cell/TextArea.vue
  18. 9
      packages/nc-gui-v2/components/cell/TimePicker.vue
  19. 6
      packages/nc-gui-v2/components/cell/Url.vue
  20. 8
      packages/nc-gui-v2/components/cell/YearPicker.vue
  21. 72
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  22. 17
      packages/nc-gui-v2/components/dashboard/settings/Modal.vue
  23. 2
      packages/nc-gui-v2/components/general/MiniSidebar.vue
  24. 5
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  25. 12
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  26. 32
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  27. 4
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  28. 25
      packages/nc-gui-v2/components/smartsheet/Form.vue
  29. 9
      packages/nc-gui-v2/components/smartsheet/Gallery.vue
  30. 28
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  31. 17
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  32. 10
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  33. 13
      packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue
  34. 2
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  35. 14
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  36. 10
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  37. 6
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  38. 6
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  39. 17
      packages/nc-gui-v2/components/tabs/auth/ApiTokenManagement.vue
  40. 2
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  41. 6
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  42. 10
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  43. 10
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  44. 8
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  45. 10
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  46. 3
      packages/nc-gui-v2/composables/useApi/interceptors.ts
  47. 46
      packages/nc-gui-v2/composables/useMetas.ts
  48. 30
      packages/nc-gui-v2/composables/useProject.ts
  49. 18
      packages/nc-gui-v2/composables/useTabs.ts
  50. 4
      packages/nc-gui-v2/composables/useViewColumns.ts
  51. 13
      packages/nc-gui-v2/composables/useViewSorts.ts
  52. 1
      packages/nc-gui-v2/context/index.ts
  53. 4
      packages/nc-gui-v2/layouts/base.vue
  54. 3
      packages/nc-gui-v2/middleware/auth.global.ts
  55. 46
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  56. 7
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue
  57. 0
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue
  58. 0
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/auth.vue
  59. 0
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/index.vue
  60. 7
      packages/nc-gui-v2/pages/index/index.vue

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

@ -65,7 +65,6 @@ declare module '@vue/runtime-core' {
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
CilFullscreen: typeof import('~icons/cil/fullscreen')['default'] CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default'] CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
EvaEmailOutline: typeof import('~icons/eva/email-outline')['default'] EvaEmailOutline: typeof import('~icons/eva/email-outline')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default'] IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default'] IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
@ -77,7 +76,6 @@ declare module '@vue/runtime-core' {
MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default'] MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default']
MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['default'] MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default'] MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsRocketLaunchOutline: typeof import('~icons/material-symbols/rocket-launch-outline')['default']
MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default'] MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default'] MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default'] MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
@ -86,12 +84,14 @@ declare module '@vue/runtime-core' {
MdiAccountIcon: typeof import('~icons/mdi/account-icon')['default'] MdiAccountIcon: typeof import('~icons/mdi/account-icon')['default']
MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default'] MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default']
MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default'] MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default'] MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
MdiApi: typeof import('~icons/mdi/api')['default'] MdiApi: typeof import('~icons/mdi/api')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default'] MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['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']
MdiCalendarMonth: typeof import('~icons/mdi/calendar-month')['default']
MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default'] MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default']
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default'] MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default'] MdiChat: typeof import('~icons/mdi/chat')['default']
@ -118,13 +118,12 @@ declare module '@vue/runtime-core' {
MdiFolder: typeof import('~icons/mdi/folder')['default'] MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiFunction: typeof import('~icons/mdi/function')['default'] MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default'] MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiGithub: typeof import('~icons/mdi/github')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default'] MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default'] MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default'] MdiInformation: typeof import('~icons/mdi/information')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default'] MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLens: typeof import('~icons/mdi/lens')['default']
MdiLense: typeof import('~icons/mdi/lense')['default']
MdiLink: typeof import('~icons/mdi/link')['default'] MdiLink: typeof import('~icons/mdi/link')['default']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default'] MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
MdiLogin: typeof import('~icons/mdi/login')['default'] MdiLogin: typeof import('~icons/mdi/login')['default']
@ -143,6 +142,7 @@ declare module '@vue/runtime-core' {
MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default'] MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default'] MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiStar: typeof import('~icons/mdi/star')['default'] MdiStar: typeof import('~icons/mdi/star')['default']
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
MdiStore: typeof import('~icons/mdi/store')['default'] MdiStore: typeof import('~icons/mdi/store')['default']
MdiTable: typeof import('~icons/mdi/table')['default'] MdiTable: typeof import('~icons/mdi/table')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default'] MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
@ -150,11 +150,9 @@ declare module '@vue/runtime-core' {
MdiText: typeof import('~icons/mdi/text')['default'] MdiText: typeof import('~icons/mdi/text')['default']
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default'] MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']
MdiTrashCan: typeof import('~icons/mdi/trash-can')['default'] MdiTrashCan: typeof import('~icons/mdi/trash-can')['default']
MdiTwitter: typeof import('~icons/mdi/twitter')['default']
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default'] MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MdiXml: typeof import('~icons/mdi/xml')['default'] MdiXml: typeof import('~icons/mdi/xml')['default']
MdiZomm: typeof import('~icons/mdi/zomm')['default']
MdiZoom: typeof import('~icons/mdi/zoom')['default']
MdiZoomIn: typeof import('~icons/mdi/zoom-in')['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']
} }

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

@ -1,7 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject } from '#imports' import { ColumnInj, IsFormInj, getMdiIcon, inject } from '#imports'
import { ColumnInj, IsFormInj } from '~/context'
import { getMdiIcon } from '@/utils'
interface Props { interface Props {
modelValue?: boolean | undefined | number modelValue?: boolean | undefined | number
@ -14,11 +12,15 @@ interface Emits {
const props = defineProps<Props>() const props = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const vModel = $(useVModel(props, 'modelValue', emits))
let vModel = $(useVModel(props, 'modelValue', emits))
const column = inject(ColumnInj) const column = inject(ColumnInj)
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj)
const editEnabled = inject(ReadonlyInj)
const checkboxMeta = $computed(() => { const checkboxMeta = $computed(() => {
return { return {
icon: { icon: {
@ -29,11 +31,17 @@ const checkboxMeta = $computed(() => {
...(column?.value?.meta || {}), ...(column?.value?.meta || {}),
} }
}) })
function onClick() {
if (editEnabled) {
vModel = !vModel
}
}
</script> </script>
<template> <template>
<div class="flex" :class="{ 'justify-center': !isForm, 'nc-cell-hover-show': !vModel }"> <div class="flex" :class="{ 'justify-center': !isForm, 'nc-cell-hover-show': !vModel }">
<div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel }" @click="vModel = !vModel"> <div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel }" @click="onClick">
<component <component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)" :is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
:style="{ :style="{

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

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref, useVModel } from '#imports' import { ColumnInj, ReadonlyInj, computed, inject, useVModel } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: number | null modelValue: number | null
@ -13,7 +12,7 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(ReadonlyInj)
const vModel = useVModel(props, 'modelValue', emit) const vModel = useVModel(props, 'modelValue', emit)

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ColumnInj, ReadonlyInj } from '~/context' import { ColumnInj, ReadonlyInj } from '#imports'
interface Props { interface Props {
modelValue: string | null modelValue: string | null
@ -11,9 +11,11 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const columnMeta = inject(ColumnInj, null) const columnMeta = inject(ColumnInj, null)
const readOnlyMode = inject(ReadonlyInj, false)
const editEnabled = inject(ReadonlyInj)
let isDateInvalid = $ref(false) let isDateInvalid = $ref(false)
const dateFormat = columnMeta?.value?.meta?.date_format ?? 'YYYY-MM-DD' const dateFormat = columnMeta?.value?.meta?.date_format ?? 'YYYY-MM-DD'
const localState = $computed({ const localState = $computed({
@ -61,10 +63,10 @@ watch(
class="!w-full px-1" class="!w-full px-1"
:format="dateFormat" :format="dateFormat"
:placeholder="isDateInvalid ? 'Invalid date' : !readOnlyMode ? 'Select date' : ''" :placeholder="isDateInvalid ? 'Invalid date' : !readOnlyMode ? 'Select date' : ''"
:allow-clear="!readOnlyMode" :allow-clear="!editEnabled"
:input-read-only="true" :input-read-only="true"
:dropdown-class-name="randomClass" :dropdown-class-name="randomClass"
:open="readOnlyMode ? false : open" :open="editEnabled ? false : open"
@click="open = !open" @click="open = !open"
> >
<template #suffixIcon></template> <template #suffixIcon></template>

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context' import { ReadonlyInj } from '#imports'
interface Props { interface Props {
modelValue: string | null modelValue: string | null
@ -12,9 +12,10 @@ const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject() const { isMysql } = useProject()
const readOnlyMode = inject(ReadonlyInj, false) const editEnabled = inject(ReadonlyInj)
let isDateInvalid = $ref(false) let isDateInvalid = $ref(false)
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const localState = $computed({ const localState = $computed({
@ -63,11 +64,12 @@ watch(
:bordered="false" :bordered="false"
class="!w-full px-1" class="!w-full px-1"
format="YYYY-MM-DD HH:mm" format="YYYY-MM-DD HH:mm"
:placeholder="isDateInvalid ? 'Invalid date' : !readOnlyMode ? 'Select date and time' : ''" :placeholder="isDateInvalid ? 'Invalid date' : !editEnabled ? 'Select date and time' : ''"
:allow-clear="!readOnlyMode" :allow-clear="!editEnabled"
:input-read-only="true" :input-read-only="true"
:dropdown-class-name="randomClass" :dropdown-class-name="randomClass"
:open="readOnlyMode ? false : open" :open="editEnabled ? false : open"
:disabled="!editEnabled"
@click="open = !open" @click="open = !open"
@ok="open = !open" @ok="open = !open"
> >

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

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports' import { ReadonlyInj, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: number | null | string modelValue: number | null | string
@ -15,7 +14,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(ReadonlyInj)
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)

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

@ -1,7 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, ref } from '#imports' import {
import { ColumnInj } from '~/context' ColumnInj,
import { convertDurationToSeconds, convertMS2Duration, durationOptions } from '~/utils' ReadonlyInj,
computed,
convertDurationToSeconds,
convertMS2Duration,
durationOptions,
inject,
ref,
} from '#imports'
interface Props { interface Props {
modelValue: number | string | null modelValue: number | string | null
@ -13,12 +20,18 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj) const column = inject(ColumnInj)
const editEnabled = inject(ReadonlyInj)
const showWarningMessage = ref(false) const showWarningMessage = ref(false)
const durationInMS = ref(0) const durationInMS = ref(0)
const isEdited = ref(false) const isEdited = ref(false)
const durationType = ref(column?.value?.meta?.duration || 0) const durationType = ref(column?.value?.meta?.duration || 0)
const durationPlaceholder = computed(() => durationOptions[durationType.value].title) const durationPlaceholder = computed(() => durationOptions[durationType.value].title)
const localState = computed({ const localState = computed({
get: () => convertMS2Duration(modelValue, durationType.value), get: () => convertMS2Duration(modelValue, durationType.value),
set: (val) => { set: (val) => {
@ -59,6 +72,7 @@ const submitDuration = () => {
<template> <template>
<div class="duration-cell-wrapper"> <div class="duration-cell-wrapper">
<input <input
v-if="editEnabled"
ref="durationInput" ref="durationInput"
v-model="localState" v-model="localState"
:placeholder="durationPlaceholder" :placeholder="durationPlaceholder"
@ -66,6 +80,7 @@ const submitDuration = () => {
@keypress="checkDurationFormat($event)" @keypress="checkDurationFormat($event)"
@keydown.enter="submitDuration" @keydown.enter="submitDuration"
/> />
<span v-else> {{ localState }}</span>
<div v-if="showWarningMessage" class="duration-warning"> <div v-if="showWarningMessage" class="duration-warning">
<!-- TODO: i18n --> <!-- TODO: i18n -->
Please enter a number Please enter a number

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

@ -1,8 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref, useVModel } from '#imports' import { ReadonlyInj, computed, inject, isEmail, useVModel } from '#imports'
import { isEmail } from '~/utils'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: string | null modelValue: string | null
@ -16,7 +14,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(ReadonlyInj)
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)

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

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports' import { ReadonlyInj, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: number | null modelValue: number | null
@ -15,7 +14,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(ReadonlyInj)
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)

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

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports' import { ReadonlyInj, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: number | null modelValue: number | null
@ -15,7 +14,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(ReadonlyInj)
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)

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

@ -1,8 +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 { computed, inject, ref, useVModel, watch } from '#imports' import { ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: string | Record<string, any> | undefined modelValue: string | Record<string, any> | undefined
@ -16,7 +15,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(ReadonlyInj)
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)

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

@ -1,8 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Select as AntSelect } from 'ant-design-vue' import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk' import type { SelectOptionType } from 'nocodb-sdk'
import { computed, inject } from '#imports' import { ActiveCellInj, ColumnInj, ReadonlyInj, computed, inject } from '#imports'
import { ActiveCellInj, ColumnInj } from '~/context'
import MdiCloseCircle from '~icons/mdi/close-circle' import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props { interface Props {
@ -16,12 +15,17 @@ const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject() const { isMysql } = useProject()
const column = inject(ColumnInj) const column = inject(ColumnInj)
// const isForm = inject<boolean>('isForm', false) // const isForm = inject<boolean>('isForm', false)
// const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(ReadonlyInj)
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
const selectedIds = ref<string[]>([]) const selectedIds = ref<string[]>([])
const aselect = ref<typeof AntSelect>() const aselect = ref<typeof AntSelect>()
const isOpen = ref(false) const isOpen = ref(false)
const options = computed(() => { const options = computed(() => {
@ -112,6 +116,7 @@ watch(isOpen, (n, _o) => {
show-arrow show-arrow
:show-search="false" :show-search="false"
:open="isOpen" :open="isOpen"
:disabled="!editEnabled"
@keydown="handleKeys" @keydown="handleKeys"
@click="isOpen = !isOpen" @click="isOpen = !isOpen"
> >

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

@ -1,7 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject } from '#imports' import { ColumnInj, ReadonlyInj, computed, getPercentStep, inject, isValidPercent, renderPercent } from '#imports'
import { ColumnInj } from '~/context'
import { getPercentStep, isValidPercent, renderPercent } from '@/utils/percentUtils'
interface Props { interface Props {
modelValue: number | string | null modelValue: number | string | null
@ -11,6 +9,8 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const editEnabled = inject(ReadonlyInj)
const column = inject(ColumnInj) const column = inject(ColumnInj)
const percent = ref() const percent = ref()
@ -63,5 +63,6 @@ function onKeyDownEnter() {
@blur="onBlur" @blur="onBlur"
@keydown.enter="onKeyDownEnter" @keydown.enter="onKeyDownEnter"
/> />
<input v-else v-model="localState" type="text" @focus="isEdited = true" /> <input v-if="editEnabled" v-model="localState" type="text" @focus="isEdited = true" />
<span v-else>{{ localState }}</span>
</template> </template>

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

@ -1,18 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject } from '#imports' import { ColumnInj, ReadonlyInj, computed, inject } from '#imports'
import { ColumnInj } from '~/context'
interface Props { interface Props {
modelValue?: number | null modelValue?: number | null
readOnly?: boolean
} }
const { modelValue, readOnly } = defineProps<Props>() const { modelValue } = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const editEnabled = inject(ReadonlyInj)
const ratingMeta = computed(() => { const ratingMeta = computed(() => {
return { return {
icon: { icon: {
@ -32,7 +32,7 @@ const vModel = computed({
</script> </script>
<template> <template>
<a-rate v-model:value="vModel" :count="ratingMeta.max" :style="`color: ${ratingMeta.color}`" :disabled="readOnly"> <a-rate v-model:value="vModel" :count="ratingMeta.max" :style="`color: ${ratingMeta.color}`" :disabled="!editEnabled">
<template #character> <template #character>
<MdiStar v-if="ratingMeta.icon.full === 'mdi-star'" class="text-sm" /> <MdiStar v-if="ratingMeta.icon.full === 'mdi-star'" class="text-sm" />
<MdiHeart v-if="ratingMeta.icon.full === 'mdi-heart'" class="text-sm" /> <MdiHeart v-if="ratingMeta.icon.full === 'mdi-heart'" class="text-sm" />

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

@ -1,8 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Select as AntSelect } from 'ant-design-vue' import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk' import type { SelectOptionType } from 'nocodb-sdk'
import { computed, inject } from '#imports' import { ActiveCellInj, ColumnInj, ReadonlyInj, computed, inject } from '#imports'
import { ActiveCellInj, ColumnInj } from '~/context'
interface Props { interface Props {
modelValue?: string modelValue?: string
@ -13,11 +12,15 @@ 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 isForm = inject<boolean>('isForm', false)
// const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(ReadonlyInj)
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
const aselect = ref<typeof AntSelect>() const aselect = ref<typeof AntSelect>()
const isOpen = ref(false) const isOpen = ref(false)
const vModel = computed({ const vModel = computed({
@ -72,6 +75,7 @@ watch(isOpen, (n, _o) => {
placeholder="Select an option" placeholder="Select an option"
:bordered="false" :bordered="false"
:open="isOpen" :open="isOpen"
:disabled="!editEnabled"
:show-arrow="active || vModel === null" :show-arrow="active || vModel === null"
@select="isOpen = false" @select="isOpen = false"
@keydown="handleKeys" @keydown="handleKeys"

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

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { inject, ref, useVModel } from '#imports' import { ReadonlyInj, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: string | null modelValue: string | null
@ -11,7 +10,7 @@ const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(ReadonlyInj)
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)

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

@ -1,7 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref } from '#imports' import { ReadonlyInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue: string | null modelValue: string | null
@ -11,7 +10,7 @@ const { modelValue } = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(ReadonlyInj)
const vModel = computed({ const vModel = computed({
get: () => modelValue ?? '', get: () => modelValue ?? '',

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

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context' import { ReadonlyInj } from '#imports'
interface Props { interface Props {
modelValue?: string | null modelValue?: string | null
@ -13,9 +13,10 @@ const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject() const { isMysql } = useProject()
const readOnlyMode = inject(ReadonlyInj, false) const editEnabled = inject(ReadonlyInj)
let isTimeInvalid = $ref(false) let isTimeInvalid = $ref(false)
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ' const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const localState = $computed({ const localState = $computed({
@ -76,9 +77,9 @@ watch(
format="HH:mm" format="HH:mm"
class="!w-full px-1" class="!w-full px-1"
:placeholder="isTimeInvalid ? 'Invalid time' : !readOnlyMode ? 'Select time' : ''" :placeholder="isTimeInvalid ? 'Invalid time' : !readOnlyMode ? 'Select time' : ''"
:allow-clear="!readOnlyMode" :allow-clear="!editEnabled"
:input-read-only="true" :input-read-only="true"
:open="readOnlyMode ? false : open" :open="editEnabled ? false : open"
:popup-class-name="randomClass" :popup-class-name="randomClass"
@click="open = !open" @click="open = !open"
@ok="open = !open" @ok="open = !open"

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

@ -1,8 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, ref } from '#imports' import { ColumnInj, ReadonlyInj, computed, inject, isValidURL } from '#imports'
import { ColumnInj, EditModeInj } from '~/context'
import { isValidURL } from '~/utils'
interface Props { interface Props {
modelValue: string | null modelValue: string | null
@ -14,7 +12,7 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(ReadonlyInj)
const vModel = computed({ const vModel = computed({
get: () => value, get: () => value,

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

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ReadonlyInj } from '~/context' import { ReadonlyInj } from '#imports'
interface Props { interface Props {
modelValue: number | string | null modelValue: number | string | null
@ -11,7 +11,7 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const readOnlyMode = inject(ReadonlyInj, false) const editEnabled = inject(ReadonlyInj)
let isYearInvalid = $ref(false) let isYearInvalid = $ref(false)
@ -62,9 +62,9 @@ watch(
:bordered="false" :bordered="false"
class="!w-full px-1" class="!w-full px-1"
:placeholder="isYearInvalid ? 'Invalid year' : !readOnlyMode ? 'Select year' : ''" :placeholder="isYearInvalid ? 'Invalid year' : !readOnlyMode ? 'Select year' : ''"
:allow-clear="!readOnlyMode" :allow-clear="!editEnabled"
:input-read-only="true" :input-read-only="true"
:open="readOnlyMode ? false : open" :open="editEnabled ? false : open"
:dropdown-class-name="randomClass" :dropdown-class-name="randomClass"
@click="open = !open" @click="open = !open"
@change="open = !open" @change="open = !open"

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

@ -2,7 +2,7 @@
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import { Empty } from 'ant-design-vue' import { Empty } from 'ant-design-vue'
import { useNuxtApp, useRoute } from '#app' import { useNuxtApp } from '#app'
import { computed, useProject, useTable, useTabs, watchEffect } from '#imports' import { computed, useProject, useTable, useTabs, watchEffect } from '#imports'
import { TabType } from '~/composables' import { TabType } from '~/composables'
import MdiView from '~icons/mdi/eye-circle-outline' import MdiView from '~icons/mdi/eye-circle-outline'
@ -14,12 +14,14 @@ const { addTab } = useTabs()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const route = useRoute() const { tables, loadTables, isSharedBase } = useProject()
const { tables, loadTables } = useProject(route.params.projectId as string)
const { activeTab } = useTabs() const { activeTab } = useTabs()
const { deleteTable } = useTable() const { deleteTable } = useTable()
const { isUIAllowed } = useUIPermission()
const tablesById = $computed<Record<string, TableType>>(() => const tablesById = $computed<Record<string, TableType>>(() =>
tables?.value?.reduce((acc: Record<string, TableType>, table: TableType) => { tables?.value?.reduce((acc: Record<string, TableType>, table: TableType) => {
acc[table.id as string] = table acc[table.id as string] = table
@ -27,7 +29,10 @@ const tablesById = $computed<Record<string, TableType>>(() =>
}, {}), }, {}),
) )
const showTableList = ref(true)
const tableCreateDlg = ref(false) const tableCreateDlg = ref(false)
let key = $ref(0) let key = $ref(0)
const menuRef = $ref<HTMLLIElement>() const menuRef = $ref<HTMLLIElement>()
@ -145,7 +150,11 @@ const activeTable = computed(() => {
</div> </div>
<a-dropdown :trigger="['contextmenu']"> <a-dropdown :trigger="['contextmenu']">
<div class="p-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull" style="direction: rtl"> <div
class="p-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull"
:class="{ 'mb-[20px]': isSharedBase }"
style="direction: rtl"
>
<div <div
style="direction: ltr" 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"
@ -156,6 +165,17 @@ const activeTable = computed(() => {
<template v-if="tables?.length"> ({{ tables.length }}) </template> <template v-if="tables?.length"> ({{ tables.length }}) </template>
</span> </span>
<MdiPlus
v-if="isUIAllowed('treeview-add-button')"
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
class="transition-transform !duration-100 text-gray-500 hover:text-pink-500"
:class="{ 'transform rotate-180': showTableList }"
/>
</div> </div>
<div style="direction: ltr" class="flex-1"> <div style="direction: ltr" class="flex-1">
<div v-if="tables.length" class="transition-height duration-200 overflow-hidden"> <div v-if="tables.length" class="transition-height duration-200 overflow-hidden">
@ -176,26 +196,42 @@ const activeTable = computed(() => {
<div class="flex align-center gap-2 h-full" @contextmenu="setMenuContext('table', table)"> <div class="flex align-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto"> <div class="flex w-auto">
<MdiDrag <MdiDrag
v-if="isUIAllowed('treeview-drag-n-drop')"
:class="`nc-child-draggable-icon-${table.title}`" :class="`nc-child-draggable-icon-${table.title}`"
class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move" 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 @click.stop.prevent
/> />
<component :is="icon(table)" class="nc-view-icon group-hover:hidden text-xs group-hover:text-gray-500" /> <component
:is="icon(table)"
class="nc-view-icon text-xs"
:class="{ 'group-hover:hidden group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop') }"
/>
</div> </div>
<div class="nc-tbl-title flex-1">{{ table.title }}</div> <div class="nc-tbl-title flex-1">{{ table.title }}</div>
<a-dropdown :trigger="['click']" @click.stop> <a-dropdown v-if="isUIAllowed('table-rename') || isUIAllowed('table-delete')" :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)"> <a-menu-item
<div>Rename</div> v-if="isUIAllowed('table-rename')"
</a-menu-item> v-t="['c:table:rename']"
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
v-if="isUIAllowed('table-delete')"
v-t="['c:table:delete']"
class="!text-xs"
@click="deleteTable(table)"
>
Delete</a-menu-item
>
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
@ -217,15 +253,25 @@ const activeTable = computed(() => {
<template #overlay> <template #overlay>
<a-menu class="cursor-pointer"> <a-menu class="cursor-pointer">
<template v-if="contextMenuTarget.type === 'table'"> <template v-if="contextMenuTarget.type === 'table'">
<a-menu-item class="!text-xs" @click="showRenameTableDlg(contextMenuTarget.value)"> <a-menu-item
v-if="isUIAllowed('table-rename')"
v-t="['c:table:rename']"
class="!text-xs"
@click="showRenameTableDlg(contextMenuTarget.value)"
>
{{ $t('general.rename') }} {{ $t('general.rename') }}
</a-menu-item> </a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(contextMenuTarget.value)"> <a-menu-item
v-if="isUIAllowed('table-delete')"
v-t="['c:table:delete']"
class="!text-xs"
@click="deleteTable(contextMenuTarget.value)"
>
{{ $t('general.delete') }} {{ $t('general.delete') }}
</a-menu-item> </a-menu-item>
</template> </template>
<template v-else> <template v-else>
<a-menu-item class="!text-xs" @click="reloadTables"> <a-menu-item v-t="['c:table:reload']" class="!text-xs" @click="reloadTables">
{{ $t('general.reload') }} {{ $t('general.reload') }}
</a-menu-item> </a-menu-item>
</template> </template>

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

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue' import type { FunctionalComponent, SVGAttributes } from 'vue'
import { useI18n } from 'vue-i18n'
import AuditTab from './AuditTab.vue' import AuditTab from './AuditTab.vue'
import AppStore from './AppStore.vue' import AppStore from './AppStore.vue'
import Metadata from './Metadata.vue' import Metadata from './Metadata.vue'
@ -10,7 +11,7 @@ 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' import { useUIPermission, useVModel, watch } from '#imports'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
@ -38,19 +39,29 @@ const emits = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
const { isUIAllowed } = useUIPermission()
const { t } = useI18n()
const tabsInfo: TabGroup = { const tabsInfo: TabGroup = {
teamAndAuth: { teamAndAuth: {
title: 'Team and Auth', title: 'Team and Auth',
icon: TeamFillIcon, icon: TeamFillIcon,
subTabs: { subTabs: {
...(isUIAllowed('userMgmtTab') && {
usersManagement: { usersManagement: {
title: 'Users Management', // Users Management
title: t('title.userMgmt'),
body: UserManagement, body: UserManagement,
}, },
}),
...(isUIAllowed('apiTokenTab') && {
apiTokenManagement: { apiTokenManagement: {
title: 'API Token Management', // API Tokens Management
title: t('title.apiTokenMgmt'),
body: ApiTokenManagement, body: ApiTokenManagement,
}, },
}),
}, },
}, },
appStore: { appStore: {

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

@ -106,7 +106,7 @@ const logout = () => {
<div <div
:class="[route.name.includes('nc-projectId') ? 'active' : 'pointer-events-none !text-gray-400']" :class="[route.name.includes('nc-projectId') ? 'active' : 'pointer-events-none !text-gray-400']"
class="nc-mini-sidebar-item" class="nc-mini-sidebar-item"
@click="navigateTo(`/nc/${route.params.projectId}`)" @click="navigateTo(`/${route.params.projectType}/${route.params.projectId}`)"
> >
<MdiDatabase class="cursor-pointer transform hover:scale-105 text-2xl" /> <MdiDatabase class="cursor-pointer transform hover:scale-105 text-2xl" />
</div> </div>

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

@ -10,10 +10,13 @@ const props = defineProps<{ column: ColumnType & { meta: any }; required?: boole
const hideMenu = toRef(props, 'hideMenu') const hideMenu = toRef(props, 'hideMenu')
const meta = inject(MetaInj) const meta = inject(MetaInj)
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const column = toRef(props, 'column') const column = toRef(props, 'column')
const { isUIAllowed } = useUIPermission()
provide(ColumnInj, column) provide(ColumnInj, column)
const editColumnDropdown = ref(false) const editColumnDropdown = ref(false)
@ -36,7 +39,7 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<template v-if="!hideMenu"> <template v-if="!hideMenu">
<div class="flex-1" /> <div class="flex-1" />
<SmartsheetHeaderMenu v-if="!isForm" @edit="editColumnDropdown = true" /> <SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" @edit="editColumnDropdown = true" />
</template> </template>
<a-dropdown <a-dropdown

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

@ -5,8 +5,10 @@ import type { Ref } from 'vue'
import { ColumnInj, IsFormInj, MetaInj } from '~/context' import { ColumnInj, IsFormInj, MetaInj } from '~/context'
import { provide, toRef, useMetas, useProvideColumnCreateStore } from '#imports' import { provide, toRef, useMetas, useProvideColumnCreateStore } from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean; required: boolean }>() const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean; required?: boolean }>()
const column = toRef(props, 'column') const column = toRef(props, 'column')
const hideMenu = toRef(props, 'hideMenu') const hideMenu = toRef(props, 'hideMenu')
const editColumnDropdown = ref(false) const editColumnDropdown = ref(false)
@ -15,13 +17,18 @@ provide(ColumnInj, column)
const { metas } = useMetas() const { metas } = useMetas()
const { isUIAllowed } = useUIPermission()
const meta = inject(MetaInj) const meta = inject(MetaInj)
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const { isLookup, isBt, isRollup, isMm, isHm, isFormula } = useVirtualCell(column) const { isLookup, isBt, isRollup, isMm, isHm, isFormula } = useVirtualCell(column)
const colOptions = $computed(() => column.value?.colOptions) const colOptions = $computed(() => column.value?.colOptions)
const tableTile = $computed(() => meta?.value?.title) const tableTile = $computed(() => meta?.value?.title)
const relationColumnOptions = $computed<LinkToAnotherRecordType | null>(() => { const relationColumnOptions = $computed<LinkToAnotherRecordType | null>(() => {
if (isMm.value || isHm.value || isBt.value) { if (isMm.value || isHm.value || isBt.value) {
return column.value?.colOptions as LinkToAnotherRecordType return column.value?.colOptions as LinkToAnotherRecordType
@ -108,8 +115,7 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<!-- </v-tooltip> --> <!-- </v-tooltip> -->
<template v-if="!hideMenu"> <template v-if="!hideMenu">
<v-spacer /> <v-spacer />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" />
<SmartsheetHeaderMenu v-if="!isForm" :virtual="true" @edit="editColumnDropdown = true" />
</template> </template>
<a-dropdown <a-dropdown

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

@ -18,6 +18,8 @@ const sharedViewListDlg = ref(false)
const publicViewId = null const publicViewId = null
const isView = false
// TODO: pending for shared view // TODO: pending for shared view
// interface Props { // interface Props {
@ -40,6 +42,8 @@ const showWebhookDrawer = ref(false)
const quickImportDialog = ref(false) const quickImportDialog = ref(false)
const { isUIAllowed } = useUIPermission()
const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => { const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
let offset = 0 let offset = 0
let c = 1 let c = 1
@ -119,30 +123,44 @@ const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
<template #overlay> <template #overlay>
<div class="bg-white shadow-lg !border"> <div class="bg-white shadow-lg !border">
<div> <div>
<div class="nc-menu-item" @click="exportFile(ExportTypes.CSV)"> <div v-t="['a:actions:download-csv']" class="nc-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadIcon /> <MdiDownloadIcon />
<!-- Download as CSV --> <!-- Download as CSV -->
{{ $t('activity.downloadCSV') }} {{ $t('activity.downloadCSV') }}
</div> </div>
<div class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)"> <div v-t="['a:actions:download-excel']" class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadIcon /> <MdiDownloadIcon />
<!-- Download as XLSX --> <!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }} {{ $t('activity.downloadExcel') }}
</div> </div>
<div class="nc-menu-item" @click="quickImportDialog = true"> <div
v-if="isUIAllowed('csvImport') && !isView"
v-t="['a:actions:upload-csv']"
class="nc-menu-item"
@click="quickImportDialog = true"
>
<MdiUploadIcon /> <MdiUploadIcon />
<!-- Upload CSV --> <!-- Upload CSV -->
{{ $t('activity.uploadCSV') }} {{ $t('activity.uploadCSV') }}
</div> </div>
<div class="nc-menu-item" @click="sharedViewListDlg = true"> <div
v-if="isUIAllowed('SharedViewList') && !isView"
v-t="['a:actions:shared-view-list']"
class="nc-menu-item"
@click="sharedViewListDlg = true"
>
<MdiViewListIcon /> <MdiViewListIcon />
<!-- Shared View List --> <!-- Shared View List -->
{{ $t('activity.listSharedView') }} {{ $t('activity.listSharedView') }}
</div> </div>
<div class="nc-menu-item" @click="showWebhookDrawer = true"> <div
v-if="isUIAllowed('webhook') && !isView"
v-t="['c:actions:webhook']"
class="nc-menu-item"
@click="showWebhookDrawer = true"
>
<MdiHookIcon /> <MdiHookIcon />
<!-- todo: i18n --> {{ $t('objects.webhooks') }}
Webhook
</div> </div>
</div> </div>
</div> </div>

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

@ -17,6 +17,8 @@ const { $e } = useNuxtApp()
const { dashboardUrl } = useDashboard() const { dashboardUrl } = useDashboard()
const { isUIAllowed } = useUIPermission()
let showShareModel = $ref(false) let showShareModel = $ref(false)
const passwordProtected = $ref(false) const passwordProtected = $ref(false)
@ -102,7 +104,7 @@ const copyLink = () => {
<template> <template>
<div> <div>
<a-button v-t="['c:view:share']" outlined class="nc-btn-share-view nc-toolbar-btn"> <a-button v-if="isUIAllowed('share-view')" v-t="['c:view:share']" outlined class="nc-btn-share-view nc-toolbar-btn">
<div class="flex align-center gap-1" @click="genShareLink"> <div class="flex align-center gap-1" @click="genShareLink">
<MdiOpenInNewIcon /> <MdiOpenInNewIcon />
<!-- Share View --> <!-- Share View -->

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

@ -405,12 +405,14 @@ onMounted(async () => {
</Draggable> </Draggable>
</a-col> </a-col>
<a-col v-if="formViewData" :span="isEditable ? 16 : 24" class="h-full overflow-auto scrollbar-thin-primary"> <a-col v-if="formViewData" :span="isEditable ? 16 : 24" class="h-full overflow-auto scrollbar-thin-primary">
<div class="h-[200px]"> <div class="h-[200px] !bg-[#dbdad7]">
<a-card class="h-full !bg-[#dbdad7] ma-0 rounded-b-0 pa-8"> <!-- for future implementation of cover image -->
</div>
<a-card class="h-full ma-0 rounded-b-0 pa-4" body-style="max-width: 700px; margin: 0 auto; margin-top: -200px;">
<a-form ref="formRef" :model="formState"> <a-form ref="formRef" :model="formState">
<a-card class="rounded ma-6 pb-10 px-15"> <a-card class="rounded ma-2 py-10 px-5">
<!-- Header --> <!-- Header -->
<a-form-item class="ma-0 gap-0 pa-0"> <a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0">
<a-input <a-input
v-model:value="formViewData.heading" v-model:value="formViewData.heading"
class="w-full text-bold text-h3" class="w-full text-bold text-h3"
@ -422,8 +424,9 @@ onMounted(async () => {
@keydown.enter="updateView" @keydown.enter="updateView"
/> />
</a-form-item> </a-form-item>
<div v-else class="ml-3 w-full text-bold text-h3">{{ formViewData.heading }}</div>
<!-- Sub Header --> <!-- Sub Header -->
<a-form-item> <a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0">
<a-input <a-input
v-model:value="formViewData.subheading" v-model:value="formViewData.subheading"
class="w-full" class="w-full"
@ -431,10 +434,12 @@ onMounted(async () => {
hide-details hide-details
:placeholder="$t('msg.info.formDesc')" :placeholder="$t('msg.info.formDesc')"
:bordered="false" :bordered="false"
:disabled="!isEditable"
@blur="updateView" @blur="updateView"
@click="updateView" @click="updateView"
/> />
</a-form-item> </a-form-item>
<div v-else class="ml-3 mb-5 w-full text-bold text-h3">{{ formViewData.subheading }}</div>
<Draggable <Draggable
ref="draggableRef" ref="draggableRef"
:list="localColumns" :list="localColumns"
@ -484,12 +489,7 @@ onMounted(async () => {
:name="element.title" :name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]" :rules="[{ required: element.required, message: `${element.title} is required` }]"
> >
<SmartsheetCell <SmartsheetCell v-model="formState[element.title]" class="nc-input" :column="element" :edit-enabled="true" />
v-model="formState[element.title]"
class="nc-input"
:column="element"
:edit-enabled="true"
/>
</a-form-item> </a-form-item>
<div v-if="activeRow === element.title"> <div v-if="activeRow === element.title">
<a-form-item class="my-0 w-1/2"> <a-form-item class="my-0 w-1/2">
@ -538,7 +538,7 @@ onMounted(async () => {
</a-card> </a-card>
</a-form> </a-form>
<div class="mx-10 px-10"> <div v-if="isEditable" class="mx-10 px-10">
<!-- After form is submitted --> <!-- After form is submitted -->
<div class="text-gray-500 mt-4 mb-2"> <div class="text-gray-500 mt-4 mb-2">
{{ $t('msg.info.afterFormSubmitted') }} {{ $t('msg.info.afterFormSubmitted') }}
@ -580,7 +580,6 @@ onMounted(async () => {
</div> </div>
</div> </div>
</a-card> </a-card>
</div>
</a-col> </a-col>
</a-row> </a-row>
</template> </template>

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

@ -1,6 +1,7 @@
<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 Row from '~/components/smartsheet/Row.vue'
import { ActiveViewInj, ChangePageInj, FieldsInj, 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' import ImageIcon from '~icons/mdi/file-image-box'
@ -50,11 +51,11 @@ const attachments = (record: any): Array<Attachment> => {
} }
</script> </script>
<!-- TODO: Fix scrolling -->
<template> <template>
<div class="flex flex-col h-full min-h-0 w-full"> <div class="flex flex-col h-full w-full">
<div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3"> <div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3 overflow-auto">
<div v-for="(record, recordIndex) in data" :key="recordIndex" class="flex flex-col"> <div v-for="(record, recordIndex) in data" :key="recordIndex" class="flex flex-col">
<Row :row="record">
<a-card hoverable class="!rounded-lg h-full"> <a-card hoverable class="!rounded-lg h-full">
<template #cover> <template #cover>
<a-carousel v-if="attachments(record).length !== 0" autoplay> <a-carousel v-if="attachments(record).length !== 0" autoplay>
@ -89,6 +90,7 @@ const attachments = (record: any): Array<Attachment> => {
</div> </div>
</div> </div>
</a-card> </a-card>
</Row>
</div> </div>
</div> </div>
<SmartsheetPagination /> <SmartsheetPagination />
@ -97,7 +99,6 @@ const attachments = (record: any): Array<Attachment> => {
<style scoped> <style scoped>
.nc-gallery-container { .nc-gallery-container {
height: calc(100vh - 250px);
overflow: auto; overflow: auto;
} }
</style> </style>

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

@ -43,6 +43,8 @@ const isLocked = inject(IsLockedInj, false)
const reloadViewDataHook = inject(ReloadViewDataHookInj) const reloadViewDataHook = inject(ReloadViewDataHookInj)
const { isUIAllowed } = useUIPermission()
// todo: get from parent ( inject or use prop ) // todo: get from parent ( inject or use prop )
const isPublicView = false const isPublicView = false
@ -83,10 +85,15 @@ const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useG
onMounted(loadGridViewColumns) onMounted(loadGridViewColumns)
provide(IsFormInj, ref(false)) provide(IsFormInj, ref(false))
provide(IsGridInj, true) provide(IsGridInj, true)
provide(PaginationDataInj, paginationData) provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage) provide(ChangePageInj, changePage)
provide(ReadonlyInj, isUIAllowed('xcDatatableEditable'))
reloadViewDataHook?.on(async () => { reloadViewDataHook?.on(async () => {
await loadData() await loadData()
loadAggCommentsCount() loadAggCommentsCount()
@ -322,7 +329,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
</div> </div>
</th> </th>
<!-- v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" --> <!-- v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" -->
<th v-t="['c:column:add']" @click="addColumnDropdown = true"> <th v-if="isUIAllowed('add-column')" 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">
<MdiPlus class="text-sm" /> <MdiPlus class="text-sm" />
@ -413,7 +420,11 @@ const expandForm = (row: Row, state: Record<string, any>) => {
</template> </template>
</SmartsheetRow> </SmartsheetRow>
<tr v-if="!isLocked"> <!--
TODO: add relationType !== 'bt' ?
v1: <tr v-if="!isView && !isLocked && !isPublicView && isEditable && relationType !== 'bt'">
-->
<tr v-if="!isView && !isLocked && !isPublicView && isUIAllowed('xcDatatableEditable')">
<td <td
v-t="['c:row:add:grid-bottom']" v-t="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1" :colspan="visibleColLength + 1"
@ -462,10 +473,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<style scoped lang="scss"> <style scoped lang="scss">
.nc-grid-wrapper { .nc-grid-wrapper {
width: 100%; @apply h-full w-full overflow-auto;
// todo : proper height calculation
height: calc(100vh - 215px);
overflow: auto;
td, td,
th { th {
@ -482,10 +490,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
table, table,
td, td,
th { th {
border-right: 1px solid #f0f0f0 !important; @apply !border-1;
border-left: 1px solid #f0f0f0 !important;
border-bottom: 1px solid #f0f0f0 !important;
border-top: 1px solid #f0f0f0 !important;
border-collapse: collapse; border-collapse: collapse;
} }
@ -511,8 +516,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
} }
td.active::before { td.active::before {
background: #0040bc; @apply bg-primary/5;
opacity: 0.1;
} }
} }

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

@ -34,21 +34,12 @@ const page = computed({
:show-size-changer="false" :show-size-changer="false"
/> />
<div v-else class="mx-auto d-flex align-center mt-n1" style="max-width: 250px"> <div v-else class="mx-auto d-flex align-center mt-n1" style="max-width: 250px">
<span class="caption" style="white-space: nowrap"> Change page:</span> <span class="text-xs" style="white-space: nowrap"> Change page:</span>
<v-text-field <a-input :value="page" size="small" class="ml-1 !text-xs" type="number" @keydown.enter="changePage(page)">
:value="page" <template #suffix>
class="ml-1 caption"
:full-width="false"
outlined
dense
hide-details
type="number"
@keydown.enter="changePage(page)"
>
<template #append>
<MdiKeyboardIcon class="mt-1" @click="changePage(page)" /> <MdiKeyboardIcon class="mt-1" @click="changePage(page)" />
</template> </template>
</v-text-field> </a-input>
</div> </div>
<div class="flex-1" /> <div class="flex-1" />

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

@ -1,22 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { useSmartsheetStoreOrThrow } from '~/composables' import { useSmartsheetStoreOrThrow } from '~/composables'
const { isGrid, isForm } = useSmartsheetStoreOrThrow() const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow()
</script> </script>
<template> <template>
<div class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[48px] px-2 border-b" style="z-index: 7"> <div class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[48px] px-2 border-b" style="z-index: 7">
<SmartsheetToolbarFieldsMenu v-if="isGrid" :show-system-fields="false" class="ml-1" /> <SmartsheetToolbarFieldsMenu v-if="isGrid || isGallery" :show-system-fields="false" class="ml-1" />
<SmartsheetToolbarColumnFilterMenu v-if="isGrid" /> <SmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery" />
<SmartsheetToolbarSortListMenu v-if="isGrid" /> <SmartsheetToolbarSortListMenu v-if="isGrid || isGallery" />
<SmartsheetToolbarShareView v-if="isForm || isGrid" /> <SmartsheetToolbarShareView v-if="isForm || isGrid" />
<SmartsheetToolbarMoreActions v-if="isGrid" /> <SmartsheetToolbarMoreActions v-if="isGrid" />
<div class="flex-1" /> <div class="flex-1" />
<SmartsheetToolbarSearchData v-if="isGrid" class="shrink mr-2" /> <SmartsheetToolbarSearchData v-if="isGrid || isGallery" class="shrink mr-2" />
</div> </div>
</template> </template>

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

@ -10,9 +10,13 @@ import MdiDoorOpen from '~icons/mdi/door-open'
import MdiDoorClosed from '~icons/mdi/door-closed' import MdiDoorClosed from '~icons/mdi/door-closed'
const emit = defineEmits(['cancel']) const emit = defineEmits(['cancel'])
const { meta } = useSmartsheetStoreOrThrow() const { meta } = useSmartsheetStoreOrThrow()
const { commentsDrawer, primaryValue, save: _save } = useExpandedFormStoreOrThrow() const { commentsDrawer, primaryValue, save: _save } = useExpandedFormStoreOrThrow()
const { isNew, syncLTARRefs } = useSmartsheetRowStoreOrThrow() const { isNew, syncLTARRefs } = useSmartsheetRowStoreOrThrow()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const save = async () => { const save = async () => {
@ -32,7 +36,7 @@ const iconColor = '#1890ff'
<template> <template>
<div class="flex p-2 align-center gap-2 p-4"> <div class="flex p-2 align-center gap-2 p-4">
<h5 class="text-lg font-weight-medium flex align-center gap-1 mb-0"> <h5 class="text-lg font-weight-medium flex align-center gap-1 mb-0 min-w-0 overflow-x-hidden truncate">
<mdi-table-arrow-right :style="{ color: iconColor }" /> <mdi-table-arrow-right :style="{ color: iconColor }" />
<template v-if="meta"> <template v-if="meta">
@ -45,7 +49,12 @@ const iconColor = '#1890ff'
</h5> </h5>
<div class="flex-grow" /> <div class="flex-grow" />
<mdi-reload class="cursor-pointer select-none" /> <mdi-reload class="cursor-pointer select-none" />
<component :is="drawerToggleIcon" class="cursor-pointer select-none" @click="commentsDrawer = !commentsDrawer" /> <component
:is="drawerToggleIcon"
v-if="isUIAllowed('rowComments')"
class="cursor-pointer select-none"
@click="commentsDrawer = !commentsDrawer"
/>>
<a-button class="!text" @click="emit('cancel')"> <a-button class="!text" @click="emit('cancel')">
<!-- Cancel --> <!-- Cancel -->
{{ $t('general.cancel') }} {{ $t('general.cancel') }}

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

@ -82,7 +82,7 @@ const isExpanded = useVModel(props, 'modelValue', emits)
<template> <template>
<a-modal v-model:visible="isExpanded" :footer="null" width="min(90vw,1000px)" :body-style="{ padding: 0 }" :closable="false"> <a-modal v-model:visible="isExpanded" :footer="null" width="min(90vw,1000px)" :body-style="{ padding: 0 }" :closable="false">
<Header @cancel="isExpanded = false" /> <Header @cancel="isExpanded = false" />
<a-card class="!bg-gray-100"> <a-card class="!bg-gray-100 min-h-[70vh]">
<div class="flex h-full nc-form-wrapper items-stretch"> <div class="flex h-full nc-form-wrapper items-stretch">
<div class="flex-grow overflow-auto scrollbar-thin-primary"> <div class="flex-grow overflow-auto scrollbar-thin-primary">
<div class="w-[500px] mx-auto"> <div class="w-[500px] mx-auto">

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

@ -11,7 +11,10 @@ const emits = defineEmits<Emits>()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const isView = ref(false) const isView = ref(false)
let showApiSnippet = $ref(false) let showApiSnippet = $ref(false)
const showWebhookDrawer = ref(false) const showWebhookDrawer = ref(false)
@ -33,6 +36,7 @@ function onOpenModal(type: ViewTypes, title = '') {
<template> <template>
<a-menu :selected-keys="[]" class="flex-1 flex flex-col"> <a-menu :selected-keys="[]" class="flex-1 flex flex-col">
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')">
<h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500"> <h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500">
{{ $t('activity.createView') }} {{ $t('activity.createView') }}
</h3> </h3>
@ -73,7 +77,12 @@ function onOpenModal(type: ViewTypes, title = '') {
</a-tooltip> </a-tooltip>
</a-menu-item> </a-menu-item>
<a-menu-item v-if="!isView" key="form" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.FORM)"> <a-menu-item
v-if="!isView"
key="form"
class="group !flex !items-center !my-0 !h-[30px]"
@click="onOpenModal(ViewTypes.FORM)"
>
<a-tooltip placement="left"> <a-tooltip placement="left">
<template #title> <template #title>
{{ $t('msg.info.addView.form') }} {{ $t('msg.info.addView.form') }}
@ -90,10 +99,13 @@ function onOpenModal(type: ViewTypes, title = '') {
</div> </div>
</a-tooltip> </a-tooltip>
</a-menu-item> </a-menu-item>
</div>
<SmartsheetSidebarMenuApiSnippet v-model="showApiSnippet" /> <SmartsheetSidebarMenuApiSnippet v-model="showApiSnippet" />
<div class="flex-auto justify-end flex flex-col gap-4 mt-4"> <div class="flex-auto justify-end flex flex-col gap-4 mt-4">
<button <button
v-if="isUIAllowed('virtualViewsCreateOrEdit')"
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease !text-xs" class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease !text-xs"
@click="onWebhooks" @click="onWebhooks"
> >

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

@ -25,6 +25,8 @@ const vModel = useVModel(props, 'view', emits)
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
/** Is editing the view name enabled */ /** Is editing the view name enabled */
let isEditing = $ref<boolean>(false) let isEditing = $ref<boolean>(false)
@ -141,7 +143,11 @@ function onStopEdit() {
</script> </script>
<template> <template>
<a-menu-item class="select-none group !flex !items-center !my-0" @dblclick.stop="onDblClick" @click.stop="onClick"> <a-menu-item
class="select-none group !flex !items-center !my-0"
@dblclick.stop="isUIAllowed('virtualViewsCreateOrEdit') && onDblClick"
@click.stop="onClick"
>
<div v-t="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2"> <div v-t="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2">
<div class="flex w-auto"> <div class="flex w-auto">
<MdiDrag <MdiDrag
@ -161,7 +167,7 @@ function onStopEdit() {
<div class="flex-1" /> <div class="flex-1" />
<template v-if="!isEditing"> <template v-if="!isEditing && isUIAllowed('virtualViewsCreateOrEdit')">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<a-tooltip placement="left"> <a-tooltip placement="left">
<template #title> <template #title>

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

@ -12,6 +12,8 @@ const activeView = inject(ActiveViewInj, ref())
const { views, loadViews } = useViews(meta) const { views, loadViews } = useViews(meta)
const { isUIAllowed } = useUIPermission()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -103,7 +105,7 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
<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]">
<template #start> <template #start>
<a-tooltip placement="left"> <a-tooltip v-if="isUIAllowed('virtualViewsCreateOrEdit')" placement="left">
<template #title> {{ $t('objects.webhooks') }}</template> <template #title> {{ $t('objects.webhooks') }}</template>
<div class="nc-sidebar-right-item hover:after:bg-gray-300"> <div class="nc-sidebar-right-item hover:after:bg-gray-300">
@ -128,7 +130,7 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
<div v-if="sidebarOpen" class="flex-1 flex flex-col"> <div v-if="sidebarOpen" class="flex-1 flex flex-col">
<MenuTop @open-modal="openModal" @deleted="loadViews" @sorted="loadViews" /> <MenuTop @open-modal="openModal" @deleted="loadViews" @sorted="loadViews" />
<a-divider class="my-2" /> <a-divider v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="my-2" />
<MenuBottom @open-modal="openModal" /> <MenuBottom @open-modal="openModal" />
</div> </div>

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

@ -2,13 +2,15 @@
import AddRow from './AddRow.vue' import AddRow from './AddRow.vue'
import LockMenu from './LockMenu.vue' import LockMenu from './LockMenu.vue'
import Reload from './Reload.vue' import Reload from './Reload.vue'
const { isUIAllowed } = useUIPermission()
</script> </script>
<template> <template>
<div class="flex gap-2"> <div class="flex gap-2">
<slot name="start" /> <slot name="start" />
<LockMenu /> <LockMenu v-if="isUIAllowed('view-type')" />
<div class="dot" /> <div class="dot" />
@ -16,7 +18,7 @@ import Reload from './Reload.vue'
<div class="dot" /> <div class="dot" />
<AddRow /> <AddRow v-if="isUIAllowed('xcDatatableEditable')" />
<slot name="end" /> <slot name="end" />
</div> </div>

17
packages/nc-gui-v2/components/tabs/auth/ApiTokenManagement.vue

@ -17,12 +17,17 @@ interface ApiToken extends ApiTokenType {
} }
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { project } = $(useProject()) const { project } = $(useProject())
const { copy } = useClipboard() const { copy } = useClipboard()
let tokensInfo = $ref<ApiToken[] | undefined>([]) let tokensInfo = $ref<ApiToken[] | undefined>([])
let showNewTokenModal = $ref(false) let showNewTokenModal = $ref(false)
let showDeleteTokenModal = $ref(false) let showDeleteTokenModal = $ref(false)
let selectedTokenData = $ref<ApiToken>({}) let selectedTokenData = $ref<ApiToken>({})
const loadApiTokens = async () => { const loadApiTokens = async () => {
@ -110,8 +115,8 @@ onMounted(() => {
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">This action will remove this API Token</div> <div class="flex flex-row justify-center mt-2 text-center w-full text-base">This action will remove this API Token</div>
<div class="flex mt-6 justify-center space-x-2"> <div class="flex mt-6 justify-center space-x-2">
<a-button @click="showDeleteTokenModal = false"> Cancel </a-button> <a-button @click="showDeleteTokenModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="deleteToken()"> Confirm </a-button> <a-button type="primary" danger @click="deleteToken()"> {{ $t('general.confirm') }} </a-button>
</div> </div>
</div> </div>
</a-modal> </a-modal>
@ -121,7 +126,7 @@ onMounted(() => {
<a-button size="middle" type="text" @click="loadApiTokens()"> <a-button size="middle" type="text" @click="loadApiTokens()">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1"> <div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<ReloadIcon class="text-gray-500" /> <ReloadIcon class="text-gray-500" />
<div class="text-gray-500">Reload</div> <div class="text-gray-500">{{ $t('general.reload') }}</div>
</div> </div>
</a-button> </a-button>
<a-button size="middle" type="primary" ghost @click="openNewTokenModal"> <a-button size="middle" type="primary" ghost @click="openNewTokenModal">
@ -134,9 +139,9 @@ onMounted(() => {
</div> </div>
<div v-if="tokensInfo" class="w-full flex flex-col mt-2 px-1"> <div v-if="tokensInfo" class="w-full flex flex-col mt-2 px-1">
<div class="flex flex-row border-b-1 text-gray-600 text-xs pb-2 pt-2"> <div class="flex flex-row border-b-1 text-gray-600 text-xs pb-2 pt-2">
<div class="flex w-4/10 pl-2">Description</div> <div class="flex w-4/10 pl-2">{{ $t('labels.description') }}</div>
<div class="flex w-4/10 justify-center">Token</div> <div class="flex w-4/10 justify-center">{{ $t('labels.token') }}</div>
<div class="flex w-2/10 justify-end pr-2">Actions</div> <div class="flex w-2/10 justify-end pr-2">{{ $t('labels.action') }}</div>
</div> </div>
<div v-for="(item, index) in tokensInfo" :key="index" class="flex flex-col"> <div v-for="(item, index) in tokensInfo" :key="index" class="flex flex-col">
<div class="flex flex-row border-b-1 items-center px-2 py-2"> <div class="flex flex-row border-b-1 items-center px-2 py-2">

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

@ -26,7 +26,7 @@ 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}/base/${base.uuid}` : null))
const loadBase = async () => { const loadBase = async () => {
try { try {

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

@ -4,7 +4,7 @@ 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 { inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports' import { inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { ActiveCellInj, CellValueInj, ColumnInj, ReloadViewDataHookInj, RowInj } from '~/context' import { ActiveCellInj, CellValueInj, ColumnInj, ReadonlyInj, ReloadViewDataHookInj, RowInj } from '~/context'
import MdiArrowExpand from '~icons/mdi/arrow-expand' import MdiArrowExpand from '~icons/mdi/arrow-expand'
import MdiPlus from '~icons/mdi/plus' import MdiPlus from '~icons/mdi/plus'
@ -18,6 +18,8 @@ const row = inject(RowInj)
const active = inject(ActiveCellInj) const active = inject(ActiveCellInj)
const editEnabled = inject(ReadonlyInj)
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
@ -57,7 +59,7 @@ const unlinkRef = async (rec: Record<string, any>) => {
<ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" /> <ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" />
</template> </template>
</div> </div>
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center"> <div v-if="editEnabled" class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<component <component
:is="addIcon" :is="addIcon"
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)"

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

@ -5,7 +5,7 @@ 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 { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports' import { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context' import { CellValueInj, ColumnInj, IsFormInj, ReadonlyInj, ReloadViewDataHookInj, RowInj } from '~/context'
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
@ -17,6 +17,8 @@ const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj)
const editEnabled = inject(ReadonlyInj)
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
@ -76,7 +78,11 @@ const unlinkRef = async (rec: Record<string, any>) => {
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"
/> />
<MdiPlus class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" /> <MdiPlus
v-if="editEnabled"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
@click="listItemsDlg = true"
/>
</div> </div>
</template> </template>
<ListItems v-model="listItemsDlg" /> <ListItems v-model="listItemsDlg" />

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

@ -5,7 +5,7 @@ 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 { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports' import { computed, inject, ref, useProvideLTARStore, useSmartsheetRowStoreOrThrow } from '#imports'
import { CellValueInj, ColumnInj, IsFormInj, ReloadViewDataHookInj, RowInj } from '~/context' import { CellValueInj, ColumnInj, IsFormInj, ReadonlyInj, ReloadViewDataHookInj, RowInj } from '~/context'
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
@ -17,6 +17,8 @@ const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj)
const editEnabled = inject(ReadonlyInj)
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
@ -75,7 +77,11 @@ const unlinkRef = async (rec: Record<string, any>) => {
<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">
<MdiArrowExpand 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" />
<MdiPlus class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="listItemsDlg = true" /> <MdiPlus
v-if="editEnabled"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
@click="listItemsDlg = true"
/>
</div> </div>
</template> </template>

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

@ -13,8 +13,10 @@ const emit = defineEmits(['unlink'])
const { relatedTableMeta } = useLTARStoreOrThrow() const { relatedTableMeta } = useLTARStoreOrThrow()
const readonly = inject(ReadonlyInj, false) const editEnabled = inject(ReadonlyInj)
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj)
const expandedFormDlg = ref(false) const expandedFormDlg = ref(false)
@ -27,12 +29,12 @@ const expandedFormDlg = ref(false)
@click="expandedFormDlg = true" @click="expandedFormDlg = true"
> >
<span class="name">{{ value }}</span> <span class="name">{{ value }}</span>
<div v-show="active || isForm" v-if="!readonly" class="flex align-center"> <div v-show="active || isForm" v-if="editEnabled" class="flex align-center">
<MdiCloseThick class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click.stop="emit('unlink')" /> <MdiCloseThick class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click.stop="emit('unlink')" />
</div> </div>
<SmartsheetExpandedForm <SmartsheetExpandedForm
v-if="expandedFormDlg" v-if="expandedFormDlg && editEnabled"
v-model="expandedFormDlg" v-model="expandedFormDlg"
:row="{ row: item }" :row="{ row: item }"
:meta="relatedTableMeta" :meta="relatedTableMeta"

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

@ -2,15 +2,19 @@
import { Empty, Modal } from 'ant-design-vue' import { Empty, Modal } from 'ant-design-vue'
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel, watch } from '#imports' import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel, watch } from '#imports'
import { ColumnInj, IsFormInj } from '~/context' import { ColumnInj, IsFormInj, ReadonlyInj } from '~/context'
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)
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const column = inject(ColumnInj) const column = inject(ColumnInj)
const editEnabled = inject(ReadonlyInj)
const { const {
childrenList, childrenList,
meta, meta,
@ -64,7 +68,7 @@ const expandedFormRow = ref()
<MdiReload v-if="!isForm" class="cursor-pointer text-gray-500" @click="loadChildrenList" /> <MdiReload v-if="!isForm" class="cursor-pointer text-gray-500" @click="loadChildrenList" />
<a-button type="primary" ghost class="!text-xs" size="small" @click="emit('attachRecord')"> <a-button v-if="editEnabled" type="primary" ghost class="!text-xs" size="small" @click="emit('attachRecord')">
<div class="flex align-center gap-1"> <div class="flex align-center gap-1">
<MdiLinkVariantRemove class="text-xs" type="primary" @click="unlinkRow(row)" /> <MdiLinkVariantRemove class="text-xs" type="primary" @click="unlinkRow(row)" />
Link to '{{ meta.title }}' Link to '{{ meta.title }}'
@ -90,7 +94,7 @@ const expandedFormRow = ref()
}}<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span> }}<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div> </div>
<div class="flex-1"></div> <div class="flex-1"></div>
<div class="flex gap-2"> <div v-if="editEnabled" class="flex gap-2">
<MdiLinkVariantRemove <MdiLinkVariantRemove
class="text-xs text-grey hover:(!text-red-500) cursor-pointer" class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
@click.stop="unlinkRow(row)" @click.stop="unlinkRow(row)"

3
packages/nc-gui-v2/composables/useApi/interceptors.ts

@ -18,7 +18,8 @@ export function addAxiosInterceptors(api: Api<any>) {
} }
if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) { if (!config.url?.endsWith('/user/me') && !config.url?.endsWith('/admin/roles')) {
if (route && route.params && route.params.shared_base_id) config.headers['xc-shared-base-id'] = route.params.shared_base_id if (route && route.params && route.params.projectType === 'base')
config.headers['xc-shared-base-id'] = route.params.projectId
} }
return config return config

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

@ -1,6 +1,8 @@
import { message } from 'ant-design-vue'
import type { WatchStopHandle } from 'vue' import type { WatchStopHandle } from 'vue'
import type { TableInfoType, TableType } from 'nocodb-sdk' import type { TableInfoType, TableType } from 'nocodb-sdk'
import { useProject } from './useProject' import { useProject } from './useProject'
import { extractSdkResponseErrorMsg } from '~/utils'
import { useNuxtApp, useState } from '#app' import { useNuxtApp, useState } from '#app'
export function useMetas() { export function useMetas() {
@ -11,42 +13,48 @@ export function useMetas() {
const loadingState = useState<Record<string, boolean>>('metas-loading-state', () => ({})) const loadingState = useState<Record<string, boolean>>('metas-loading-state', () => ({}))
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => { const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => {
if (!force && metas.value[tableIdOrTitle]) return metas.value[tableIdOrTitle] if (!tableIdOrTitle) return null
const modelId = (tables.value.find((t) => t.title === tableIdOrTitle || t.id === tableIdOrTitle) || {}).id
if (!modelId) {
console.warn(`Table '${tableIdOrTitle}' is not found in the table list`)
return null
}
/** wait until loading is finished if requesting same meta */ /** wait until loading is finished if requesting same meta */
if (!force) { if (!force && loadingState.value[tableIdOrTitle]) {
await new Promise((resolve) => { await new Promise((resolve) => {
let unwatch: WatchStopHandle let unwatch: WatchStopHandle
// set maximum 20sec timeout to wait loading meta
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
unwatch?.() unwatch?.()
clearTimeout(timeout) clearTimeout(timeout)
resolve(null) resolve(null)
}, 20000) }, 10000)
// watch for loading state change
unwatch = watch( unwatch = watch(
() => loadingState.value[modelId], () => !!loadingState.value[tableIdOrTitle],
(isLoading) => { (isLoading) => {
if (!isLoading) { if (!isLoading) {
clearTimeout(timeout) clearTimeout(timeout)
resolve(null)
unwatch?.() unwatch?.()
resolve(null)
} }
}, },
{ immediate: true }, { immediate: true },
) )
}) })
if (metas.value[modelId]) return metas.value[modelId] if (metas.value[tableIdOrTitle]) {
return metas.value[tableIdOrTitle]
}
}
loadingState.value[tableIdOrTitle] = true
try {
if (!force && metas.value[tableIdOrTitle]) {
return metas.value[tableIdOrTitle]
} }
loadingState.value[modelId] = true const modelId = tableIdOrTitle.startsWith('md_') ? tableIdOrTitle : tables.value.find((t) => t.title === tableIdOrTitle)?.id
if (!modelId) {
console.warn(`Table '${tableIdOrTitle}' is not found in the table list`)
return null
}
const model = await $api.dbTable.read(modelId) const model = await $api.dbTable.read(modelId)
metas.value = { metas.value = {
@ -55,9 +63,13 @@ export function useMetas() {
[model.title]: model, [model.title]: model,
} }
loadingState.value[modelId] = false
return model return model
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
delete loadingState.value[tableIdOrTitle]
}
return null
} }
const clearAllMeta = () => { const clearAllMeta = () => {

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

@ -1,17 +1,20 @@
import { SqlUiFactory } from 'nocodb-sdk' import { SqlUiFactory } from 'nocodb-sdk'
import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk' import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core' import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp, useState } from '#app' import { useNuxtApp, useRoute, useState } from '#app'
import { USER_PROJECT_ROLES } from '~/lib' import { USER_PROJECT_ROLES } from '~/lib'
export function useProject(projectId?: MaybeRef<string>) { export function useProject(projectId?: MaybeRef<string>) {
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({})) const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
let _projectId = $ref('')
const _projectId = $computed(() => unref(projectId))
const project = useState<ProjectType>('project') const project = useState<ProjectType>('project')
const tables = useState<TableType[]>('tables', () => [] as TableType[]) const tables = useState<TableType[]>('tables', () => [] as TableType[])
const route = useRoute()
// todo: refactor path param name and variable name
const projectType = $computed(() => route.params.projectType as string)
async function loadProjectRoles() { async function loadProjectRoles() {
projectRoles.value = {} projectRoles.value = {}
@ -29,17 +32,19 @@ export function useProject(projectId?: MaybeRef<string>) {
} }
} }
async function loadProject(id: string) { async function loadProject() {
project.value = await $api.project.read(id) if (unref(projectId)) {
await loadProjectRoles() _projectId = unref(projectId)!
} else if (projectType === 'base') {
const baseData = await $api.public.sharedBaseGet(route.params.projectId as string)
_projectId = baseData.project_id!
} else {
_projectId = route.params.projectId as string
} }
project.value = await $api.project.read(_projectId!)
watchEffect(async () => { await loadProjectRoles()
if (_projectId) {
await loadProject(_projectId)
await loadTables() await loadTables()
} }
})
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '') const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
@ -48,6 +53,7 @@ export function useProject(projectId?: MaybeRef<string>) {
const sqlUi = computed( const sqlUi = computed(
() => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>, () => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>,
) )
const isSharedBase = computed(() => projectType === 'base')
return { project, tables, loadProjectRoles, loadProject, loadTables, isMysql, isPg, sqlUi } return { project, tables, loadProjectRoles, loadProject, loadTables, isMysql, isPg, sqlUi, isSharedBase }
} }

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

@ -29,9 +29,11 @@ export function useTabs() {
const router = useRouter() const router = useRouter()
const { tables } = useProject() const { tables } = useProject()
const projectType = $computed(() => route.params.projectType as string)
const activeTabIndex: WritableComputedRef<number> = computed({ const activeTabIndex: WritableComputedRef<number> = computed({
get() { get() {
if ((route.name as string)?.startsWith('nc-projectId-index-index-type-title-viewTitle') && tables.value?.length) { if ((route.name as string)?.startsWith('projectType-projectId-index-index-type-title-viewTitle') && tables.value?.length) {
const tab: Partial<TabItem> = { type: route.params.type as TabType, title: route.params.title as string } const tab: Partial<TabItem> = { type: route.params.type as TabType, title: route.params.title as string }
const id = tables.value?.find((t) => t.title === tab.title)?.id const id = tables.value?.find((t) => t.title === tab.title)?.id
@ -56,7 +58,7 @@ export function useTabs() {
}, },
set(index: number) { set(index: number) {
if (index === -1) { if (index === -1) {
navigateTo(`/nc/${route.params.projectId}`) navigateTo(`/${projectType}/${route.params.projectId}`)
} else { } else {
const tab = tabs.value[index] const tab = tabs.value[index]
@ -91,7 +93,7 @@ export function useTabs() {
let newTabIndex = index - 1 let newTabIndex = index - 1
if (newTabIndex < 0 && tabs.value?.length > 1) newTabIndex = index + 1 if (newTabIndex < 0 && tabs.value?.length > 1) newTabIndex = index + 1
if (newTabIndex === -1) { if (newTabIndex === -1) {
await navigateTo(`/nc/${route.params.projectId}`) await navigateTo(`/${projectType}/${route.params.projectId}`)
} else { } else {
await navigateToTab(tabs.value?.[newTabIndex]) await navigateToTab(tabs.value?.[newTabIndex])
} }
@ -102,11 +104,15 @@ export function useTabs() {
function navigateToTab(tab: TabItem) { function navigateToTab(tab: TabItem) {
switch (tab.type) { switch (tab.type) {
case TabType.TABLE: case TabType.TABLE:
return navigateTo(`/nc/${route.params.projectId}/table/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`) return navigateTo(
`/${projectType}/${route.params.projectId}/table/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`,
)
case TabType.VIEW: case TabType.VIEW:
return navigateTo(`/nc/${route.params.projectId}/view/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`) return navigateTo(
`/${projectType}/${route.params.projectId}/view/${tab?.title}${tab.viewTitle ? `/${tab.viewTitle}` : ''}`,
)
case TabType.AUTH: case TabType.AUTH:
return navigateTo(`/nc/${route.params.projectId}/auth`) return navigateTo(`/${projectType}/${route.params.projectId}/auth`)
} }
} }

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

@ -24,6 +24,8 @@ export function useViewColumns(
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const loadViewColumns = async () => { const loadViewColumns = async () => {
if (!meta || !view) return if (!meta || !view) return
@ -88,11 +90,13 @@ export function useViewColumns(
} }
const saveOrUpdate = async (field: any, index: number) => { const saveOrUpdate = async (field: any, index: number) => {
if (isUIAllowed('fieldsSync')) {
if (field.id && view?.value?.id) { if (field.id && view?.value?.id) {
await $api.dbViewColumn.update(view.value.id, field.id, field) await $api.dbViewColumn.update(view.value.id, field.id, field)
} else if (view?.value?.id) { } else if (view?.value?.id) {
if (fields.value) fields.value[index] = (await $api.dbViewColumn.create(view.value.id, field)) as any if (fields.value) fields.value[index] = (await $api.dbViewColumn.create(view.value.id, field)) as any
} }
}
reloadData?.() reloadData?.()
} }

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

@ -10,18 +10,23 @@ export function useViewSorts(
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const loadSorts = async () => { const loadSorts = async () => {
if (!view?.value) return if (!view?.value) return
sorts.value = ((await $api.dbTableSort.list(view?.value?.id as string)) as any)?.sorts?.list sorts.value = ((await $api.dbTableSort.list(view?.value?.id as string)) as any)?.sorts?.list
} }
const saveOrUpdate = async (sort: SortType, i: number) => { const saveOrUpdate = async (sort: SortType, i: number) => {
if (!sorts?.value) return // TODO:
// if (!this.shared && this._isUIAllowed('sortSync')) {
if (isUIAllowed('sortSync')) {
if (sort.id) { if (sort.id) {
await $api.dbTableSort.update(sort.id, sort) await $api.dbTableSort.update(sort.id, sort)
} else { } else {
sorts.value[i] = (await $api.dbTableSort.create(view?.value?.id as string, sort)) as any sorts.value[i] = (await $api.dbTableSort.create(view?.value?.id as string, sort)) as any
} }
}
reloadData?.() reloadData?.()
} }
const addSort = () => { const addSort = () => {
@ -31,11 +36,13 @@ export function useViewSorts(
} }
const deleteSort = async (sort: SortType, i: number) => { const deleteSort = async (sort: SortType, i: number) => {
// TOOD:
// if (!this.shared && sort.id && this._isUIAllowed('sortSync')) { // if (!this.shared && sort.id && this._isUIAllowed('sortSync')) {
if (sort.id) { if (isUIAllowed('sortSync') && sort.id) {
await $api.dbTableSort.delete(sort.id) await $api.dbTableSort.delete(sort.id)
} } else {
sorts.value.splice(i, 1) sorts.value.splice(i, 1)
} }
}
return { sorts, loadSorts, addSort, deleteSort, saveOrUpdate } return { sorts, loadSorts, addSort, deleteSort, saveOrUpdate }
} }

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

@ -5,7 +5,6 @@ import type { useViewData } from '#imports'
import type { Row } from '~/composables' import type { Row } from '~/composables'
import type { TabItem } from '~/composables/useTabs' import type { TabItem } from '~/composables/useTabs'
export const EditEnabledInj: InjectionKey<boolean> = Symbol('edit-enabled')
export const ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell') export const ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell')
export const RowInj: InjectionKey<Ref<Row>> = Symbol('row') export const RowInj: InjectionKey<Ref<Row>> = Symbol('row')
export const ColumnInj: InjectionKey<Ref<ColumnType & { meta: any }>> = Symbol('column-injection') export const ColumnInj: InjectionKey<Ref<ColumnType & { meta: any }>> = Symbol('column-injection')

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

@ -4,6 +4,8 @@ import { computed, useGlobal, useRoute } from '#imports'
const { signOut, signedIn, isLoading, user } = useGlobal() const { signOut, signedIn, isLoading, user } = useGlobal()
const { isSharedBase } = useProject()
const route = useRoute() const route = useRoute()
const email = computed(() => user.value?.email ?? '---') const email = computed(() => user.value?.email ?? '---')
@ -49,7 +51,7 @@ const logout = () => {
</div> </div>
</a-tooltip> </a-tooltip>
<template v-if="signedIn"> <template v-if="signedIn && !isSharedBase">
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent /> <MdiDotsVertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />

3
packages/nc-gui-v2/middleware/auth.global.ts

@ -23,6 +23,9 @@ import { useGlobal } from '#imports'
export default defineNuxtRouteMiddleware((to, from) => { export default defineNuxtRouteMiddleware((to, from) => {
const state = useGlobal() const state = useGlobal()
/** if shred base allow without validating */
if (to.params?.projectType === 'base') return
/** if auth is required or unspecified (same as required) and user is not signed in, redirect to signin page */ /** if auth is required or unspecified (same as required) and user is not signed in, redirect to signin page */
if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) { if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) {
return navigateTo('/signin') return navigateTo('/signin')

46
packages/nc-gui-v2/pages/nc/[projectId]/index.vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue

@ -5,7 +5,7 @@ import { openLink } from '~/utils'
const route = useRoute() const route = useRoute()
const { project, loadProject, loadTables } = useProject(route.params.projectId as string) const { project, loadProject, loadTables, isSharedBase } = useProject()
const { addTab, clearTabs } = useTabs() const { addTab, clearTabs } = useTabs()
@ -30,7 +30,7 @@ onKeyStroke(
clearTabs() clearTabs()
if (!route.params.type) { if (!route.params.type && isUIAllowed('teamAndAuth')) {
addTab({ type: TabType.AUTH, title: 'Team & Auth' }) addTab({ type: TabType.AUTH, title: 'Team & Auth' })
} }
@ -39,7 +39,7 @@ function toggleDialog(value?: boolean, key?: string) {
openDialogKey.value = key openDialogKey.value = key
} }
await loadProject(route.params.projectId as string) await loadProject()
await loadTables() await loadTables()
</script> </script>
@ -58,14 +58,31 @@ await loadTables()
> >
<div style="height: var(--header-height)" class="flex items-center !bg-primary text-white px-1 pl-5 gap-2"> <div style="height: var(--header-height)" class="flex items-center !bg-primary text-white px-1 pl-5 gap-2">
<div <div
v-if="isOpen" v-if="isOpen && !isSharedBase"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105" class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105"
@click="navigateTo('/')" @click="navigateTo('/')"
> >
<img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" /> <img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div> </div>
<a
v-if="isOpen && isSharedBase"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105"
href="https://github.com/nocodb/nocodb"
target="_blank"
>
<img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</a>
<a-dropdown :trigger="['click']" placement="bottom"> <div v-if="isSharedBase">
<template v-if="isOpen">
<div class="text-xl font-semibold truncate">{{ project.title }}</div>
</template>
<template v-else>
<MdiFolder class="text-primary cursor-pointer transform hover:scale-105 text-2xl" />
</template>
</div>
<a-dropdown v-else :trigger="['click']" placement="bottom">
<div <div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }" :style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']" :class="[isOpen ? '' : 'justify-center']"
@ -123,8 +140,8 @@ await loadTables()
<a-menu-item key="teamAndAuth"> <a-menu-item key="teamAndAuth">
<div <div
v-if="isUIAllowed('settings')" v-if="isUIAllowed('teamAndAuth')"
v-t="['c:navdraw:project-settings']" v-t="['c:navdraw:team-and-auth']"
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')" @click="toggleDialog(true, 'teamAndAuth')"
> >
@ -135,8 +152,8 @@ await loadTables()
<a-menu-item key="appStore"> <a-menu-item key="appStore">
<div <div
v-if="isUIAllowed('settings')" v-if="isUIAllowed('appStore')"
v-t="['c:navdraw:project-settings']" v-t="['c:navdraw:app-store']"
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click="toggleDialog(true, 'appStore')" @click="toggleDialog(true, 'appStore')"
> >
@ -147,8 +164,8 @@ await loadTables()
<a-menu-item key="metaData"> <a-menu-item key="metaData">
<div <div
v-if="isUIAllowed('settings')" v-if="isUIAllowed('projectMetadata')"
v-t="['c:navdraw:project-settings']" v-t="['c:navdraw:project-metadata']"
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click="toggleDialog(true, 'metaData')" @click="toggleDialog(true, 'metaData')"
> >
@ -159,8 +176,8 @@ await loadTables()
<a-menu-item key="audit"> <a-menu-item key="audit">
<div <div
v-if="isUIAllowed('settings')" v-if="isUIAllowed('audit')"
v-t="['c:navdraw:project-settings']" v-t="['c:navdraw:audit']"
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click="toggleDialog(true, 'audit')" @click="toggleDialog(true, 'audit')"
> >
@ -171,7 +188,7 @@ await loadTables()
<a-menu-divider /> <a-menu-divider />
<a-sub-menu key="preview-as"> <a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as" v-t="['c:navdraw:preview-as']">
<template #title> <template #title>
<div class="nc-project-menu-item group"> <div class="nc-project-menu-item group">
<MdiContentCopy class="group-hover:text-pink-500 nc-project-preview" /> <MdiContentCopy class="group-hover:text-pink-500 nc-project-preview" />
@ -220,7 +237,6 @@ await loadTables()
</template> </template>
<dashboard-settings-modal v-model="dialogOpen" :open-key="openDialogKey" /> <dashboard-settings-modal v-model="dialogOpen" :open-key="openDialogKey" />
<NuxtPage /> <NuxtPage />
<GeneralPreviewAs float /> <GeneralPreviewAs float />

7
packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue

@ -57,7 +57,7 @@ const icon = (tab: TabItem) => {
</a-tab-pane> </a-tab-pane>
<template #leftExtra> <template #leftExtra>
<a-menu v-model:selectedKeys="currentMenu" class="border-0" mode="horizontal"> <a-menu v-if="isUIAllowed('addOrImport')" v-model:selectedKeys="currentMenu" class="border-0" mode="horizontal">
<a-sub-menu key="addORImport"> <a-sub-menu key="addORImport">
<template #title> <template #title>
<div class="text-sm flex items-center gap-2 pt-[8px] pb-3"> <div class="text-sm flex items-center gap-2 pt-[8px] pb-3">
@ -144,9 +144,10 @@ const icon = (tab: TabItem) => {
</template> </template>
</a-tabs> </a-tabs>
</div> </div>
<div class="w-full min-h-[300px] grow">
<NuxtPage /> <NuxtPage />
</div> </div>
</div>
<DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" /> <DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" />
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" :import-type="importType" /> <DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" :import-type="importType" />
@ -156,7 +157,7 @@ const icon = (tab: TabItem) => {
<style scoped lang="scss"> <style scoped lang="scss">
.nc-container { .nc-container {
height: calc(100% - var(--header-height)); height: calc(100vh - var(--header-height));
flex: 1 1 100%; flex: 1 1 100%;
} }

0
packages/nc-gui-v2/pages/nc/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue

0
packages/nc-gui-v2/pages/nc/[projectId]/index/index/auth.vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/auth.vue

0
packages/nc-gui-v2/pages/nc/[projectId]/index/index/index.vue → packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/index.vue

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

@ -15,6 +15,8 @@ const { $e } = useNuxtApp()
const { api, isLoading } = useApi() const { api, isLoading } = useApi()
const { isUIAllowed } = useUIPermission()
useSidebar({ hasSidebar: true, isOpen: true }) useSidebar({ hasSidebar: true, isOpen: true })
const filterQuery = ref('') const filterQuery = ref('')
@ -34,6 +36,7 @@ const filteredProjects = computed(
) )
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',
@ -41,8 +44,8 @@ const deleteProject = (project: ProjectType) => {
cancelText: 'No', cancelText: 'No',
async onOk() { async onOk() {
try { try {
$e('c:project:delete')
await api.project.delete(project.id as string) await api.project.delete(project.id as string)
$e('a:project:delete')
return projects.value?.splice(projects.value.indexOf(project), 1) return projects.value?.splice(projects.value.indexOf(project), 1)
} catch (e: any) { } catch (e: any) {
return message.error(await extractSdkResponseErrorMsg(e)) return message.error(await extractSdkResponseErrorMsg(e))
@ -78,7 +81,7 @@ onMounted(() => {
></a-input-search> ></a-input-search>
<div class="flex-grow"></div> <div class="flex-grow"></div>
<a-dropdown @click.stop> <a-dropdown v-if="isUIAllowed('projectCreate', true)" @click.stop>
<a-button class="nc-new-project-menu !shadow"> <a-button class="nc-new-project-menu !shadow">
<div class="flex align-center"> <div class="flex align-center">
{{ $t('title.newProj') }} {{ $t('title.newProj') }}

Loading…
Cancel
Save