Browse Source

chore(gui-v2): rebase and resolve conflicts

pull/3130/head
Wing-Kam Wong 2 years ago
parent
commit
dd65b663a6
  1. 7
      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. 74
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  22. 29
      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. 13
      packages/nc-gui-v2/components/smartsheet/Form.vue
  29. 69
      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. 98
      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. 58
      packages/nc-gui-v2/composables/useMetas.ts
  48. 32
      packages/nc-gui-v2/composables/useProject.ts
  49. 18
      packages/nc-gui-v2/composables/useTabs.ts
  50. 12
      packages/nc-gui-v2/composables/useViewColumns.ts
  51. 21
      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. 9
      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

7
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']
CilFullscreen: typeof import('~icons/cil/fullscreen')['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']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['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']
MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['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']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
@ -126,8 +124,6 @@ declare module '@vue/runtime-core' {
MdiInformation: typeof import('~icons/mdi/information')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['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']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
MdiLogin: typeof import('~icons/mdi/login')['default']
@ -157,9 +153,6 @@ declare module '@vue/runtime-core' {
MdiTwitter: typeof import('~icons/mdi/twitter')['default']
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['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']
RouterView: typeof import('vue-router')['RouterView']
}

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

@ -1,7 +1,5 @@
<script setup lang="ts">
import { inject } from '#imports'
import { ColumnInj, IsFormInj } from '~/context'
import { getMdiIcon } from '@/utils'
import { ColumnInj, IsFormInj, getMdiIcon, inject } from '#imports'
interface Props {
modelValue?: boolean | undefined | number
@ -14,11 +12,15 @@ interface Emits {
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const vModel = $(useVModel(props, 'modelValue', emits))
let vModel = $(useVModel(props, 'modelValue', emits))
const column = inject(ColumnInj)
const isForm = inject(IsFormInj)
const editEnabled = inject(ReadonlyInj)
const checkboxMeta = $computed(() => {
return {
icon: {
@ -29,11 +31,17 @@ const checkboxMeta = $computed(() => {
...(column?.value?.meta || {}),
}
})
function onClick() {
if (editEnabled) {
vModel = !vModel
}
}
</script>
<template>
<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
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
:style="{

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,8 +1,7 @@
<script setup lang="ts">
import { Modal as AModal } from 'ant-design-vue'
import Editor from '~/components/monaco/Editor.vue'
import { computed, inject, ref, useVModel, watch } from '#imports'
import { EditModeInj } from '~/context'
import { ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
interface Props {
modelValue: string | Record<string, any> | undefined
@ -16,7 +15,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(ReadonlyInj)
const vModel = useVModel(props, 'modelValue', emits)

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

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

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

@ -1,7 +1,5 @@
<script setup lang="ts">
import { computed, inject } from '#imports'
import { ColumnInj } from '~/context'
import { getPercentStep, isValidPercent, renderPercent } from '@/utils/percentUtils'
import { ColumnInj, ReadonlyInj, computed, getPercentStep, inject, isValidPercent, renderPercent } from '#imports'
interface Props {
modelValue: number | string | null
@ -11,6 +9,8 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const editEnabled = inject(ReadonlyInj)
const column = inject(ColumnInj)
const percent = ref()
@ -63,5 +63,6 @@ function onKeyDownEnter() {
@blur="onBlur"
@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>

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

@ -1,18 +1,18 @@
<script setup lang="ts">
import { computed, inject } from '#imports'
import { ColumnInj } from '~/context'
import { ColumnInj, ReadonlyInj, computed, inject } from '#imports'
interface Props {
modelValue?: number | null
readOnly?: boolean
}
const { modelValue, readOnly } = defineProps<Props>()
const { modelValue } = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)!
const editEnabled = inject(ReadonlyInj)
const ratingMeta = computed(() => {
return {
icon: {
@ -32,7 +32,7 @@ const vModel = computed({
</script>
<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>
<MdiStar v-if="ratingMeta.icon.full === 'mdi-star'" 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>
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType } from 'nocodb-sdk'
import { computed, inject } from '#imports'
import { ActiveCellInj, ColumnInj } from '~/context'
import { ActiveCellInj, ColumnInj, ReadonlyInj, computed, inject } from '#imports'
interface Props {
modelValue?: string
@ -13,11 +12,15 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)
// const isForm = inject<boolean>('isForm', false)
// const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(ReadonlyInj)
const active = inject(ActiveCellInj, ref(false))
const aselect = ref<typeof AntSelect>()
const isOpen = ref(false)
const vModel = computed({
@ -72,6 +75,7 @@ watch(isOpen, (n, _o) => {
placeholder="Select an option"
:bordered="false"
:open="isOpen"
:disabled="!editEnabled"
:show-arrow="active || vModel === null"
@select="isOpen = false"
@keydown="handleKeys"

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

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

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

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

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

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

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

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

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

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

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

@ -2,7 +2,7 @@
import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { Empty } from 'ant-design-vue'
import { useNuxtApp, useRoute } from '#app'
import { useNuxtApp } from '#app'
import { computed, useProject, useTable, useTabs, watchEffect } from '#imports'
import { TabType } from '~/composables'
import MdiView from '~icons/mdi/eye-circle-outline'
@ -15,12 +15,14 @@ const { addTab } = useTabs()
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 { deleteTable } = useTable()
const { isUIAllowed } = useUIPermission()
const tablesById = $computed<Record<string, TableType>>(() =>
tables?.value?.reduce((acc: Record<string, TableType>, table: TableType) => {
acc[table.id as string] = table
@ -28,7 +30,10 @@ const tablesById = $computed<Record<string, TableType>>(() =>
}, {}),
)
const showTableList = ref(true)
const tableCreateDlg = ref(false)
let key = $ref(0)
const menuRef = $ref<HTMLLIElement>()
@ -146,7 +151,11 @@ const activeTable = computed(() => {
</div>
<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
style="direction: ltr"
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@ -157,6 +166,17 @@ const activeTable = computed(() => {
<template v-if="tables?.length"> ({{ tables.length }}) </template>
</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 style="direction: ltr" class="flex-1">
<div v-if="tables.length" class="transition-height duration-200 overflow-hidden">
@ -177,26 +197,42 @@ const activeTable = computed(() => {
<div class="flex align-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto">
<MdiDrag
v-if="isUIAllowed('treeview-drag-n-drop')"
: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"
@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 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" />
<template #overlay>
<a-menu class="cursor-pointer">
<a-menu-item v-t="" class="!text-xs" @click="showRenameTableDlg(table)">
<div>Rename</div>
</a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-rename')"
v-t="['c:table:rename']"
class="!text-xs"
@click="showRenameTableDlg(table)"
><div>Rename</div></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>
</template>
</a-dropdown>
@ -218,15 +254,25 @@ const activeTable = computed(() => {
<template #overlay>
<a-menu class="cursor-pointer">
<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') }}
</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') }}
</a-menu-item>
</template>
<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') }}
</a-menu-item>
</template>

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

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

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

@ -106,7 +106,7 @@ const logout = () => {
<div
:class="[route.name.includes('nc-projectId') ? 'active' : 'pointer-events-none !text-gray-400']"
class="nc-mini-sidebar-item"
@click="navigateTo(`/nc/${route.params.projectId}`)"
@click="navigateTo(`/${route.params.projectType}/${route.params.projectId}`)"
>
<MdiDatabase class="cursor-pointer transform hover:scale-105 text-2xl" />
</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 meta = inject(MetaInj)
const isForm = inject(IsFormInj, ref(false))
const column = toRef(props, 'column')
const { isUIAllowed } = useUIPermission()
provide(ColumnInj, column)
const editColumnDropdown = ref(false)
@ -36,7 +39,7 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<template v-if="!hideMenu">
<div class="flex-1" />
<SmartsheetHeaderMenu v-if="!isForm" @edit="editColumnDropdown = true" />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" @edit="editColumnDropdown = true" />
</template>
<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 { 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 hideMenu = toRef(props, 'hideMenu')
const editColumnDropdown = ref(false)
@ -15,13 +17,18 @@ provide(ColumnInj, column)
const { metas } = useMetas()
const { isUIAllowed } = useUIPermission()
const meta = inject(MetaInj)
const isForm = inject(IsFormInj, ref(false))
const { isLookup, isBt, isRollup, isMm, isHm, isFormula } = useVirtualCell(column)
const colOptions = $computed(() => column.value?.colOptions)
const tableTile = $computed(() => meta?.value?.title)
const relationColumnOptions = $computed<LinkToAnotherRecordType | null>(() => {
if (isMm.value || isHm.value || isBt.value) {
return column.value?.colOptions as LinkToAnotherRecordType
@ -108,8 +115,7 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<!-- </v-tooltip> -->
<template v-if="!hideMenu">
<v-spacer />
<SmartsheetHeaderMenu v-if="!isForm" :virtual="true" @edit="editColumnDropdown = true" />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" />
</template>
<a-dropdown

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

@ -18,6 +18,8 @@ const sharedViewListDlg = ref(false)
const publicViewId = null
const isView = false
// TODO: pending for shared view
// interface Props {
@ -40,6 +42,8 @@ const showWebhookDrawer = ref(false)
const quickImportDialog = ref(false)
const { isUIAllowed } = useUIPermission()
const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
let offset = 0
let c = 1
@ -119,30 +123,44 @@ const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
<template #overlay>
<div class="bg-white shadow-lg !border">
<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 />
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</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 />
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}
</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 />
<!-- Upload CSV -->
{{ $t('activity.uploadCSV') }}
</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 />
<!-- Shared View List -->
{{ $t('activity.listSharedView') }}
</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 />
<!-- todo: i18n -->
Webhook
{{ $t('objects.webhooks') }}
</div>
</div>
</div>

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

@ -17,6 +17,8 @@ const { $e } = useNuxtApp()
const { dashboardUrl } = useDashboard()
const { isUIAllowed } = useUIPermission()
let showShareModel = $ref(false)
const passwordProtected = $ref(false)
@ -102,7 +104,7 @@ const copyLink = () => {
<template>
<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">
<MdiOpenInNewIcon />
<!-- Share View -->

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

@ -406,11 +406,11 @@ onMounted(async () => {
</a-col>
<a-col v-if="formViewData" :span="isEditable ? 16 : 24" class="h-full overflow-auto scrollbar-thin-primary">
<div class="h-[200px]">
<a-card class="h-full !bg-[#dbdad7] ma-0 rounded-b-0 pa-8">
<a-card class="h-full !bg-[#dbdad7] ma-0 rounded-b-0 pa-8" body-style="max-width: 700px; margin: 0 auto;">
<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 -->
<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
v-model:value="formViewData.heading"
class="w-full text-bold text-h3"
@ -422,8 +422,9 @@ onMounted(async () => {
@keydown.enter="updateView"
/>
</a-form-item>
<div v-else class="ml-3 w-full text-bold text-h3">{{ formViewData.heading }}</div>
<!-- Sub Header -->
<a-form-item>
<a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0">
<a-input
v-model:value="formViewData.subheading"
class="w-full"
@ -431,10 +432,12 @@ onMounted(async () => {
hide-details
:placeholder="$t('msg.info.formDesc')"
:bordered="false"
:disabled="!isEditable"
@blur="updateView"
@click="updateView"
/>
</a-form-item>
<div v-else class="ml-3 mb-5 w-full text-bold text-h3">{{ formViewData.subheading }}</div>
<Draggable
ref="draggableRef"
:list="localColumns"
@ -538,7 +541,7 @@ onMounted(async () => {
</a-card>
</a-form>
<div class="mx-10 px-10">
<div v-if="isEditable" class="mx-10 px-10">
<!-- After form is submitted -->
<div class="text-gray-500 mt-4 mb-2">
{{ $t('msg.info.afterFormSubmitted') }}

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

@ -1,6 +1,7 @@
<script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk'
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 ImageIcon from '~icons/mdi/file-image-box'
@ -50,45 +51,46 @@ const attachments = (record: any): Array<Attachment> => {
}
</script>
<!-- TODO: Fix scrolling -->
<template>
<div class="flex flex-col h-full min-h-0 w-full">
<div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3">
<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 overflow-auto">
<div v-for="(record, recordIndex) in data" :key="recordIndex" class="flex flex-col">
<a-card hoverable class="!rounded-lg h-full">
<template #cover>
<a-carousel v-if="attachments(record).length !== 0" autoplay>
<img
v-for="(attachment, index) in attachments(record)"
:key="index"
class="h-52 rounded-t-lg"
:src="attachment.url"
/>
</a-carousel>
<ImageIcon v-else class="w-full h-48 my-4 text-cool-gray-200" />
</template>
<Row :row="record">
<a-card hoverable class="!rounded-lg h-full">
<template #cover>
<a-carousel v-if="attachments(record).length !== 0" autoplay>
<img
v-for="(attachment, index) in attachments(record)"
:key="index"
class="h-52 rounded-t-lg"
:src="attachment.url"
/>
</a-carousel>
<ImageIcon v-else class="w-full h-48 my-4 text-cool-gray-200" />
</template>
<div
v-for="(col, colIndex) in fields"
:key="colIndex"
class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full"
>
<div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">
<div class="w-full text-gray-600">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
<div
v-for="(col, colIndex) in fields"
:key="colIndex"
class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full"
>
<div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">
<div class="w-full text-gray-600">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
</div>
</div>
</div>
<div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start">
<div v-if="isRowEmpty(record, col)" class="h-3 bg-gray-200 px-5 rounded-lg"></div>
<template v-else>
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="record.row[col.title]" :column="col" />
<SmartsheetCell v-else v-model="record.row[col.title]" :column="col" :edit-enabled="false" />
</template>
<div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start">
<div v-if="isRowEmpty(record, col)" class="h-3 bg-gray-200 px-5 rounded-lg"></div>
<template v-else>
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="record.row[col.title]" :column="col" />
<SmartsheetCell v-else v-model="record.row[col.title]" :column="col" :edit-enabled="false" />
</template>
</div>
</div>
</div>
</a-card>
</a-card>
</Row>
</div>
</div>
<SmartsheetPagination />
@ -97,7 +99,6 @@ const attachments = (record: any): Array<Attachment> => {
<style scoped>
.nc-gallery-container {
height: calc(100vh - 250px);
overflow: auto;
}
</style>

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

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

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

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

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

@ -1,22 +1,22 @@
<script setup lang="ts">
import { useSmartsheetStoreOrThrow } from '~/composables'
const { isGrid, isForm } = useSmartsheetStoreOrThrow()
const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow()
</script>
<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">
<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" />
<SmartsheetToolbarMoreActions v-if="isGrid" />
<div class="flex-1" />
<SmartsheetToolbarSearchData v-if="isGrid" class="shrink mr-2" />
<SmartsheetToolbarSearchData v-if="isGrid || isGallery" class="shrink mr-2" />
</div>
</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'
const emit = defineEmits(['cancel'])
const { meta } = useSmartsheetStoreOrThrow()
const { commentsDrawer, primaryValue, save: _save } = useExpandedFormStoreOrThrow()
const { isNew, syncLTARRefs } = useSmartsheetRowStoreOrThrow()
const { isUIAllowed } = useUIPermission()
const save = async () => {
@ -32,7 +36,7 @@ const iconColor = '#1890ff'
<template>
<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 }" />
<template v-if="meta">
@ -45,7 +49,12 @@ const iconColor = '#1890ff'
</h5>
<div class="flex-grow" />
<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')">
<!-- 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>
<a-modal v-model:visible="isExpanded" :footer="null" width="min(90vw,1000px)" :body-style="{ padding: 0 }" :closable="false">
<Header @cancel="isExpanded = false" />
<a-card class="!bg-gray-100">
<a-card class="!bg-gray-100 min-h-[70vh]">
<div class="flex h-full nc-form-wrapper items-stretch">
<div class="flex-grow overflow-auto scrollbar-thin-primary">
<div class="w-[500px] mx-auto">

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

@ -11,7 +11,10 @@ const emits = defineEmits<Emits>()
const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const isView = ref(false)
let showApiSnippet = $ref(false)
const showWebhookDrawer = ref(false)
@ -33,67 +36,76 @@ function onOpenModal(type: ViewTypes, title = '') {
<template>
<a-menu :selected-keys="[]" class="flex-1 flex flex-col">
<h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500">
{{ $t('activity.createView') }}
</h3>
<a-menu-item key="grid" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.GRID)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.grid') }}
</template>
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')">
<h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500">
{{ $t('activity.createView') }}
</h3>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GRID].icon" :class="`text-${viewIcons[ViewTypes.GRID].color}`" />
<a-menu-item key="grid" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.GRID)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.grid') }}
</template>
<div>{{ $t('objects.viewType.grid') }}</div>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GRID].icon" :class="`text-${viewIcons[ViewTypes.GRID].color}`" />
<div class="flex-1" />
<div>{{ $t('objects.viewType.grid') }}</div>
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<div class="flex-1" />
<a-menu-item key="gallery" class="group !flex !items-center !-my0 !h-[30px]" @click="onOpenModal(ViewTypes.GALLERY)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.gallery') }}
</template>
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GALLERY].icon" :class="`text-${viewIcons[ViewTypes.GALLERY].color}`" />
<a-menu-item key="gallery" class="group !flex !items-center !-my0 !h-[30px]" @click="onOpenModal(ViewTypes.GALLERY)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.gallery') }}
</template>
<div>{{ $t('objects.viewType.gallery') }}</div>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GALLERY].icon" :class="`text-${viewIcons[ViewTypes.GALLERY].color}`" />
<div class="flex-1" />
<div>{{ $t('objects.viewType.gallery') }}</div>
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<div class="flex-1" />
<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">
<template #title>
{{ $t('msg.info.addView.form') }}
</template>
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</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-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.form') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.FORM].icon" :class="`text-${viewIcons[ViewTypes.FORM].color}`" />
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.FORM].icon" :class="`text-${viewIcons[ViewTypes.FORM].color}`" />
<div>{{ $t('objects.viewType.form') }}</div>
<div>{{ $t('objects.viewType.form') }}</div>
<div class="flex-1" />
<div class="flex-1" />
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<mdi-plus class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
</div>
<SmartsheetSidebarMenuApiSnippet v-model="showApiSnippet" />
<div class="flex-auto justify-end flex flex-col gap-4 mt-4">
<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"
@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 { isUIAllowed } = useUIPermission()
/** Is editing the view name enabled */
let isEditing = $ref<boolean>(false)
@ -141,7 +143,11 @@ function onStopEdit() {
</script>
<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 class="flex w-auto">
<MdiDrag
@ -161,7 +167,7 @@ function onStopEdit() {
<div class="flex-1" />
<template v-if="!isEditing">
<template v-if="!isEditing && isUIAllowed('virtualViewsCreateOrEdit')">
<div class="flex items-center gap-1">
<a-tooltip placement="left">
<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 { isUIAllowed } = useUIPermission()
const router = useRouter()
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]">
<template #start>
<a-tooltip placement="left">
<a-tooltip v-if="isUIAllowed('virtualViewsCreateOrEdit')" placement="left">
<template #title> {{ $t('objects.webhooks') }}</template>
<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">
<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" />
</div>

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

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

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

@ -17,12 +17,17 @@ interface ApiToken extends ApiTokenType {
}
const { $api, $e } = useNuxtApp()
const { project } = $(useProject())
const { copy } = useClipboard()
let tokensInfo = $ref<ApiToken[] | undefined>([])
let showNewTokenModal = $ref(false)
let showDeleteTokenModal = $ref(false)
let selectedTokenData = $ref<ApiToken>({})
const loadApiTokens = async () => {
@ -110,8 +115,8 @@ onMounted(() => {
<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 mt-6 justify-center space-x-2">
<a-button @click="showDeleteTokenModal = false"> Cancel </a-button>
<a-button type="primary" danger @click="deleteToken()"> Confirm </a-button>
<a-button @click="showDeleteTokenModal = false"> {{ $t('general.cancel') }} </a-button>
<a-button type="primary" danger @click="deleteToken()"> {{ $t('general.confirm') }} </a-button>
</div>
</div>
</a-modal>
@ -121,7 +126,7 @@ onMounted(() => {
<a-button size="middle" type="text" @click="loadApiTokens()">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<ReloadIcon class="text-gray-500" />
<div class="text-gray-500">Reload</div>
<div class="text-gray-500">{{ $t('general.reload') }}</div>
</div>
</a-button>
<a-button size="middle" type="primary" ghost @click="openNewTokenModal">
@ -134,9 +139,9 @@ onMounted(() => {
</div>
<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 w-4/10 pl-2">Description</div>
<div class="flex w-4/10 justify-center">Token</div>
<div class="flex w-2/10 justify-end pr-2">Actions</div>
<div class="flex w-4/10 pl-2">{{ $t('labels.description') }}</div>
<div class="flex w-4/10 justify-center">{{ $t('labels.token') }}</div>
<div class="flex w-2/10 justify-end pr-2">{{ $t('labels.action') }}</div>
</div>
<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">

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

@ -26,7 +26,7 @@ const { project } = useProject()
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 () => {
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 ListItems from './components/ListItems.vue'
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 MdiPlus from '~icons/mdi/plus'
@ -18,6 +18,8 @@ const row = inject(RowInj)
const active = inject(ActiveCellInj)
const editEnabled = inject(ReadonlyInj)
const listItemsDlg = ref(false)
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)" />
</template>
</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
:is="addIcon"
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 ListItems from './components/ListItems.vue'
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)!
@ -17,6 +17,8 @@ const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj)
const editEnabled = inject(ReadonlyInj)
const listItemsDlg = 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"
@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>
</template>
<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 ListItems from './components/ListItems.vue'
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)!
@ -17,6 +17,8 @@ const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj)
const editEnabled = inject(ReadonlyInj)
const listItemsDlg = 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">
<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>
</template>

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

@ -13,8 +13,10 @@ const emit = defineEmits(['unlink'])
const { relatedTableMeta } = useLTARStoreOrThrow()
const readonly = inject(ReadonlyInj, false)
const editEnabled = inject(ReadonlyInj)
const active = inject(ActiveCellInj, ref(false))
const isForm = inject(IsFormInj)
const expandedFormDlg = ref(false)
@ -27,12 +29,12 @@ const expandedFormDlg = ref(false)
@click="expandedFormDlg = true"
>
<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')" />
</div>
<SmartsheetExpandedForm
v-if="expandedFormDlg"
v-if="expandedFormDlg && editEnabled"
v-model="expandedFormDlg"
:row="{ row: item }"
: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 type { ColumnType } from 'nocodb-sdk'
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 emit = defineEmits(['update:modelValue', 'attachRecord'])
const vModel = useVModel(props, 'modelValue', emit)
const isForm = inject(IsFormInj, ref(false))
const column = inject(ColumnInj)
const editEnabled = inject(ReadonlyInj)
const {
childrenList,
meta,
@ -64,7 +68,7 @@ const expandedFormRow = ref()
<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">
<MdiLinkVariantRemove class="text-xs" type="primary" @click="unlinkRow(row)" />
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>
</div>
<div class="flex-1"></div>
<div class="flex gap-2">
<div v-if="editEnabled" class="flex gap-2">
<MdiLinkVariantRemove
class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
@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 (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

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

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

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

@ -1,17 +1,20 @@
import { SqlUiFactory } from 'nocodb-sdk'
import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp, useState } from '#app'
import { useNuxtApp, useRoute, useState } from '#app'
import { USER_PROJECT_ROLES } from '~/lib'
export function useProject(projectId?: MaybeRef<string>) {
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const { $api } = useNuxtApp()
const _projectId = $computed(() => unref(projectId))
let _projectId = $ref('')
const project = useState<ProjectType>('project')
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() {
projectRoles.value = {}
@ -29,18 +32,20 @@ export function useProject(projectId?: MaybeRef<string>) {
}
}
async function loadProject(id: string) {
project.value = await $api.project.read(id)
async function loadProject() {
if (unref(projectId)) {
_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!)
await loadProjectRoles()
await loadTables()
}
watchEffect(async () => {
if (_projectId) {
await loadProject(_projectId)
await loadTables()
}
})
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
const isMysql = computed(() => ['mysql', 'mysql2'].includes(projectBaseType))
@ -48,6 +53,7 @@ export function useProject(projectId?: MaybeRef<string>) {
const sqlUi = computed(
() => 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 { tables } = useProject()
const projectType = $computed(() => route.params.projectType as string)
const activeTabIndex: WritableComputedRef<number> = computed({
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 id = tables.value?.find((t) => t.title === tab.title)?.id
@ -56,7 +58,7 @@ export function useTabs() {
},
set(index: number) {
if (index === -1) {
navigateTo(`/nc/${route.params.projectId}`)
navigateTo(`/${projectType}/${route.params.projectId}`)
} else {
const tab = tabs.value[index]
@ -91,7 +93,7 @@ export function useTabs() {
let newTabIndex = index - 1
if (newTabIndex < 0 && tabs.value?.length > 1) newTabIndex = index + 1
if (newTabIndex === -1) {
await navigateTo(`/nc/${route.params.projectId}`)
await navigateTo(`/${projectType}/${route.params.projectId}`)
} else {
await navigateToTab(tabs.value?.[newTabIndex])
}
@ -102,11 +104,15 @@ export function useTabs() {
function navigateToTab(tab: TabItem) {
switch (tab.type) {
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:
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:
return navigateTo(`/nc/${route.params.projectId}/auth`)
return navigateTo(`/${projectType}/${route.params.projectId}/auth`)
}
}

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

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

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

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

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

@ -5,7 +5,6 @@ import type { useViewData } from '#imports'
import type { Row } from '~/composables'
import type { TabItem } from '~/composables/useTabs'
export const EditEnabledInj: InjectionKey<boolean> = Symbol('edit-enabled')
export const ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell')
export const RowInj: InjectionKey<Ref<Row>> = Symbol('row')
export const ColumnInj: InjectionKey<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 { isSharedBase } = useProject()
const route = useRoute()
const email = computed(() => user.value?.email ?? '---')
@ -69,7 +71,7 @@ const logout = () => {
</div>
</a-tooltip>
<template v-if="signedIn">
<template v-if="signedIn && !isSharedBase">
<a-dropdown :trigger="['click']">
<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) => {
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 ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) {
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 { project, loadProject, loadTables } = useProject(route.params.projectId as string)
const { project, loadProject, loadTables, isSharedBase } = useProject()
const { addTab, clearTabs } = useTabs()
@ -30,7 +30,7 @@ onKeyStroke(
clearTabs()
if (!route.params.type) {
if (!route.params.type && isUIAllowed('teamAndAuth')) {
addTab({ type: TabType.AUTH, title: 'Team & Auth' })
}
@ -39,7 +39,7 @@ function toggleDialog(value?: boolean, key?: string) {
openDialogKey.value = key
}
await loadProject(route.params.projectId as string)
await loadProject()
await loadTables()
</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
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"
@click="navigateTo('/')"
>
<img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</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>
<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 :trigger="['click']" placement="bottom">
<a-dropdown v-else :trigger="['click']" placement="bottom">
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"
@ -123,8 +140,8 @@ await loadTables()
<a-menu-item key="teamAndAuth">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
v-if="isUIAllowed('teamAndAuth')"
v-t="['c:navdraw:team-and-auth']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')"
>
@ -135,8 +152,8 @@ await loadTables()
<a-menu-item key="appStore">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
v-if="isUIAllowed('appStore')"
v-t="['c:navdraw:app-store']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'appStore')"
>
@ -147,8 +164,8 @@ await loadTables()
<a-menu-item key="metaData">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
v-if="isUIAllowed('projectMetadata')"
v-t="['c:navdraw:project-metadata']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'metaData')"
>
@ -159,8 +176,8 @@ await loadTables()
<a-menu-item key="audit">
<div
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
v-if="isUIAllowed('audit')"
v-t="['c:navdraw:audit']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'audit')"
>
@ -171,7 +188,7 @@ await loadTables()
<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>
<div class="nc-project-menu-item group">
<MdiContentCopy class="group-hover:text-pink-500 nc-project-preview" />
@ -220,7 +237,6 @@ await loadTables()
</template>
<dashboard-settings-modal v-model="dialogOpen" :open-key="openDialogKey" />
<NuxtPage />
<GeneralPreviewAs float />

9
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>
<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">
<template #title>
<div class="text-sm flex items-center gap-2 pt-[8px] pb-3">
@ -144,8 +144,9 @@ const icon = (tab: TabItem) => {
</template>
</a-tabs>
</div>
<NuxtPage />
<div class="w-full min-h-[300px] grow">
<NuxtPage />
</div>
</div>
<DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" />
@ -156,7 +157,7 @@ const icon = (tab: TabItem) => {
<style scoped lang="scss">
.nc-container {
height: calc(100% - var(--header-height));
height: calc(100vh - var(--header-height));
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 { isUIAllowed } = useUIPermission()
useSidebar({ hasSidebar: true, isOpen: true })
const filterQuery = ref('')
@ -34,6 +36,7 @@ const filteredProjects = computed(
)
const deleteProject = (project: ProjectType) => {
$e('c:project:delete')
Modal.confirm({
title: `Do you want to delete '${project.title}' project?`,
okText: 'Yes',
@ -41,8 +44,8 @@ const deleteProject = (project: ProjectType) => {
cancelText: 'No',
async onOk() {
try {
$e('c:project:delete')
await api.project.delete(project.id as string)
$e('a:project:delete')
return projects.value?.splice(projects.value.indexOf(project), 1)
} catch (e: any) {
return message.error(await extractSdkResponseErrorMsg(e))
@ -81,7 +84,7 @@ onMounted(() => {
></a-input-search>
<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">
<div class="flex align-center">
{{ $t('title.newProj') }}

Loading…
Cancel
Save