Browse Source

Merge pull request #3087 from nocodb/fix/add-missing-isUIAllowed

fix(gui-v2): add i18n / isUIAllowed
pull/3133/head
աɨռɢӄաօռɢ 2 years ago committed by GitHub
parent
commit
67334fb7c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  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. 64
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  22. 29
      packages/nc-gui-v2/components/dashboard/settings/Modal.vue
  23. 5
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  24. 10
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  25. 32
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  26. 4
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  27. 13
      packages/nc-gui-v2/components/smartsheet/Form.vue
  28. 60
      packages/nc-gui-v2/components/smartsheet/Gallery.vue
  29. 15
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  30. 11
      packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue
  31. 98
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  32. 10
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  33. 6
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  34. 6
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  35. 17
      packages/nc-gui-v2/components/tabs/auth/ApiTokenManagement.vue
  36. 6
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  37. 10
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  38. 10
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  39. 8
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  40. 10
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  41. 12
      packages/nc-gui-v2/composables/useViewColumns.ts
  42. 21
      packages/nc-gui-v2/composables/useViewSorts.ts
  43. 20
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  44. 2
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue
  45. 11
      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"

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

@ -15,9 +15,13 @@ const { addTab } = useTabs()
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { tables, loadTables, isSharedBase } = useProject() const { tables, loadTables, isSharedBase } = useProject()
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
@ -25,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>()
@ -158,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">
@ -178,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"
<a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item> @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> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
@ -219,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>

29
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: {
usersManagement: { ...(isUIAllowed('userMgmtTab') && {
title: 'Users Management', usersManagement: {
body: UserManagement, // Users Management
}, title: t('title.userMgmt'),
apiTokenManagement: { body: UserManagement,
title: 'API Token Management', },
body: ApiTokenManagement, }),
}, ...(isUIAllowed('apiTokenTab') && {
apiTokenManagement: {
// API Tokens Management
title: t('title.apiTokenMgmt'),
body: ApiTokenManagement,
},
}),
}, },
}, },
appStore: { appStore: {

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

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

@ -6,7 +6,9 @@ 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 -->

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

@ -406,11 +406,11 @@ onMounted(async () => {
</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]">
<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-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 +422,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 +432,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"
@ -538,7 +541,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') }}

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

@ -56,40 +56,40 @@ const attachments = (record: any): Array<Attachment> => {
<div class="nc-gallery-container min-h-0 flex-1 grid grid-cols-4 gap-4 my-4 px-3 overflow-auto"> <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"> <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>
<img <img
v-for="(attachment, index) in attachments(record)" v-for="(attachment, index) in attachments(record)"
:key="index" :key="index"
class="h-52 rounded-t-lg" class="h-52 rounded-t-lg"
:src="attachment.url" :src="attachment.url"
/> />
</a-carousel> </a-carousel>
<ImageIcon v-else class="w-full h-48 my-4 text-cool-gray-200" /> <ImageIcon v-else class="w-full h-48 my-4 text-cool-gray-200" />
</template> </template>
<div <div
v-for="(col, colIndex) in fields" v-for="(col, colIndex) in fields"
:key="colIndex" :key="colIndex"
class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full" 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="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">
<div class="w-full text-gray-600"> <div class="w-full text-gray-600">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" /> <SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="true" /> <SmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
</div>
</div> </div>
</div>
<div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start"> <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> <div v-if="isRowEmpty(record, col)" class="h-3 bg-gray-200 px-5 rounded-lg"></div>
<template v-else> <template v-else>
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="record.row[col.title]" :column="col" /> <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" /> <SmartsheetCell v-else v-model="record.row[col.title]" :column="col" :edit-enabled="false" />
</template> </template>
</div>
</div> </div>
</div> </a-card>
</a-card>
</Row> </Row>
</div> </div>
</div> </div>

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

11
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 () => {
@ -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') }}

98
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,67 +36,76 @@ 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">
<h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500"> <div v-if="isUIAllowed('virtualViewsCreateOrEdit')">
{{ $t('activity.createView') }} <h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500">
</h3> {{ $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 class="text-xs flex items-center h-full w-full gap-2"> <a-menu-item key="grid" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.GRID)">
<component :is="viewIcons[ViewTypes.GRID].icon" :class="`text-${viewIcons[ViewTypes.GRID].color}`" /> <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 class="flex-1" />
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item key="gallery" class="group !flex !items-center !-my0 !h-[30px]" @click="onOpenModal(ViewTypes.GALLERY)"> <mdi-plus class="group-hover:text-primary" />
<a-tooltip placement="left"> </div>
<template #title> </a-tooltip>
{{ $t('msg.info.addView.gallery') }} </a-menu-item>
</template>
<div class="text-xs flex items-center h-full w-full gap-2"> <a-menu-item key="gallery" class="group !flex !items-center !-my0 !h-[30px]" @click="onOpenModal(ViewTypes.GALLERY)">
<component :is="viewIcons[ViewTypes.GALLERY].icon" :class="`text-${viewIcons[ViewTypes.GALLERY].color}`" /> <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 class="flex-1" />
</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)"> <mdi-plus class="group-hover:text-primary" />
<a-tooltip placement="left"> </div>
<template #title> </a-tooltip>
{{ $t('msg.info.addView.form') }} </a-menu-item>
</template>
<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"> <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}`" /> <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" /> <mdi-plus class="group-hover:text-primary" />
</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">

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

12
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,10 +90,12 @@ export function useViewColumns(
} }
const saveOrUpdate = async (field: any, index: number) => { const saveOrUpdate = async (field: any, index: number) => {
if (field.id && view?.value?.id) { if (isUIAllowed('fieldsSync')) {
await $api.dbViewColumn.update(view.value.id, field.id, field) if (field.id && view?.value?.id) {
} else if (view?.value?.id) { await $api.dbViewColumn.update(view.value.id, field.id, field)
if (fields.value) fields.value[index] = (await $api.dbViewColumn.create(view.value.id, field)) as any } else if (view?.value?.id) {
if (fields.value) fields.value[index] = (await $api.dbViewColumn.create(view.value.id, field)) as any
}
} }
reloadData?.() reloadData?.()

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

@ -10,17 +10,22 @@ 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 (sort.id) { // if (!this.shared && this._isUIAllowed('sortSync')) {
await $api.dbTableSort.update(sort.id, sort) if (isUIAllowed('sortSync')) {
} else { if (sort.id) {
sorts.value[i] = (await $api.dbTableSort.create(view?.value?.id as string, sort)) as any await $api.dbTableSort.update(sort.id, sort)
} else {
sorts.value[i] = (await $api.dbTableSort.create(view?.value?.id as string, sort)) as any
}
} }
reloadData?.() reloadData?.()
} }
@ -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 }
} }

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

@ -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' })
} }
@ -140,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')"
> >
@ -152,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')"
> >
@ -164,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')"
> >
@ -176,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')"
> >
@ -188,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" />

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

11
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') }}
@ -126,7 +129,7 @@ onMounted(() => {
:data-source="filteredProjects" :data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }" :pagination="{ position: ['bottomCenter'] }"
> >
<!-- Title --> <!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title"> <a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text }"> <template #default="{ text }">
<div class="capitalize !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap nc-project-row" :title="text"> <div class="capitalize !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap nc-project-row" :title="text">
@ -134,7 +137,7 @@ onMounted(() => {
</div> </div>
</template> </template>
</a-table-column> </a-table-column>
<!-- Actions --> <!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id"> <a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }"> <template #default="{ text, record }">
<div class="flex align-center"> <div class="flex align-center">

Loading…
Cancel
Save