Browse Source

Merge pull request #7067 from nocodb/nc-fix/locked-mode-fix-2

Locked mode
pull/7057/head
Raju Udava 1 year ago committed by GitHub
parent
commit
475af0bdef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      packages/nc-gui/assets/nc-icons/credit-card.svg
  2. 6
      packages/nc-gui/assets/nc-icons/layers.svg
  3. 6
      packages/nc-gui/components/cell/DatePicker.vue
  4. 6
      packages/nc-gui/components/cell/DateTimePicker.vue
  5. 12
      packages/nc-gui/components/cell/MultiSelect.vue
  6. 12
      packages/nc-gui/components/cell/SingleSelect.vue
  7. 8
      packages/nc-gui/components/cell/attachment/Modal.vue
  8. 10
      packages/nc-gui/components/cell/attachment/index.vue
  9. 8
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  10. 8
      packages/nc-gui/components/smartsheet/Cell.vue
  11. 33
      packages/nc-gui/components/smartsheet/Form.vue
  12. 16
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  13. 53
      packages/nc-gui/components/smartsheet/details/Fields.vue
  14. 28
      packages/nc-gui/components/smartsheet/grid/Table.vue
  15. 6
      packages/nc-gui/components/smartsheet/header/Cell.vue
  16. 6
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  17. 28
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  18. 5
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  19. 5
      packages/nc-gui/components/virtual-cell/HasMany.vue
  20. 10
      packages/nc-gui/components/virtual-cell/Links.vue
  21. 5
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  22. 11
      packages/nc-gui/components/virtual-cell/components/ItemChip.vue
  23. 1
      packages/nc-gui/lang/en.json
  24. 3
      packages/nc-gui/store/views.ts
  25. 2
      tests/playwright/pages/Dashboard/Grid/index.ts
  26. 2
      tests/playwright/pages/Dashboard/common/Footbar/index.ts
  27. 3
      tests/playwright/pages/Dashboard/common/Toolbar/ViewMenu.ts
  28. 4
      tests/playwright/tests/db/general/viewMenu.spec.ts

4
packages/nc-gui/assets/nc-icons/credit-card.svg

@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2.66602H1.99999C1.26361 2.66602 0.666656 3.26297 0.666656 3.99935V11.9993C0.666656 12.7357 1.26361 13.3327 1.99999 13.3327H14C14.7364 13.3327 15.3333 12.7357 15.3333 11.9993V3.99935C15.3333 3.26297 14.7364 2.66602 14 2.66602Z" stroke="#565B66" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M14 2.66602H1.99999C1.26361 2.66602 0.666656 3.26297 0.666656 3.99935V11.9993C0.666656 12.7357 1.26361 13.3327 1.99999 13.3327H14C14.7364 13.3327 15.3333 12.7357 15.3333 11.9993V3.99935C15.3333 3.26297 14.7364 2.66602 14 2.66602Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0.666656 6.66602H15.3333" stroke="#565B66" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M0.666656 6.66602H15.3333" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 557 B

After

Width:  |  Height:  |  Size: 567 B

6
packages/nc-gui/assets/nc-icons/layers.svg

@ -1,8 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_656_42199)"> <g clip-path="url(#clip0_656_42199)">
<path d="M1.33325 11.334L7.99992 14.6673L14.6666 11.334" stroke="#565B66" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M1.33325 11.334L7.99992 14.6673L14.6666 11.334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.33325 8L7.99992 11.3333L14.6666 8" stroke="#565B66" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M1.33325 8L7.99992 11.3333L14.6666 8" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99992 1.33398L1.33325 4.66732L7.99992 8.00065L14.6666 4.66732L7.99992 1.33398Z" stroke="#565B66" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M7.99992 1.33398L1.33325 4.66732L7.99992 8.00065L14.6666 4.66732L7.99992 1.33398Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g> </g>
<defs> <defs>
<clipPath id="clip0_656_42199"> <clipPath id="clip0_656_42199">

Before

Width:  |  Height:  |  Size: 712 B

After

Width:  |  Height:  |  Size: 727 B

6
packages/nc-gui/components/cell/DatePicker.vue

@ -32,8 +32,6 @@ const columnMeta = inject(ColumnInj, null)!
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
const isLockedMode = inject(IsLockedInj, ref(false))
const isEditColumn = inject(EditColumnInj, ref(false)) const isEditColumn = inject(EditColumnInj, ref(false))
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
@ -188,9 +186,7 @@ useSelectedCellKeyupListener(active, (e: KeyboardEvent) => {
const isOpen = computed(() => { const isOpen = computed(() => {
if (readOnly.value) return false if (readOnly.value) return false
return ((readOnly.value || (localState.value && isPk)) && !active.value && !editable.value) || isLockedMode.value return (readOnly.value || (localState.value && isPk)) && !active.value && !editable.value ? false : open.value
? false
: open.value
}) })
// use the default date picker open sync only to close the picker // use the default date picker open sync only to close the picker

6
packages/nc-gui/components/cell/DateTimePicker.vue

@ -37,8 +37,6 @@ const active = inject(ActiveCellInj, ref(false))
const editable = inject(EditModeInj, ref(false)) const editable = inject(EditModeInj, ref(false))
const isLockedMode = inject(IsLockedInj, ref(false))
const { t } = useI18n() const { t } = useI18n()
const isEditColumn = inject(EditColumnInj, ref(false)) const isEditColumn = inject(EditColumnInj, ref(false))
@ -126,9 +124,7 @@ const open = ref(false)
const isOpen = computed(() => { const isOpen = computed(() => {
if (readOnly.value) return false if (readOnly.value) return false
return readOnly.value || (localState.value && isPk) || isLockedMode.value return readOnly.value || (localState.value && isPk) ? false : open.value && (active.value || editable.value)
? false
: open.value && (active.value || editable.value)
}) })
const randomClass = `picker_${Math.floor(Math.random() * 99999)}` const randomClass = `picker_${Math.floor(Math.random() * 99999)}`

12
packages/nc-gui/components/cell/MultiSelect.vue

@ -49,8 +49,6 @@ const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)! const readOnly = inject(ReadonlyInj)!
const isLockedMode = inject(IsLockedInj, ref(false))
const isEditable = inject(EditModeInj, ref(false)) const isEditable = inject(EditModeInj, ref(false))
const activeCell = inject(ActiveCellInj, ref(false)) const activeCell = inject(ActiveCellInj, ref(false))
@ -343,11 +341,7 @@ const selectedOpts = computed(() => {
</script> </script>
<template> <template>
<div <div class="nc-multi-select h-full w-full flex items-center" :class="{ 'read-only': readOnly }" @click="toggleMenu">
class="nc-multi-select h-full w-full flex items-center"
:class="{ 'read-only': readOnly || isLockedMode }"
@click="toggleMenu"
>
<div <div
v-if="!active" v-if="!active"
class="flex flex-wrap" class="flex flex-wrap"
@ -386,9 +380,9 @@ const selectedOpts = computed(() => {
:bordered="false" :bordered="false"
clear-icon clear-icon
:show-search="!isMobileMode" :show-search="!isMobileMode"
:show-arrow="editAllowed && !(readOnly || isLockedMode)" :show-arrow="editAllowed && !readOnly"
:open="isOpen && editAllowed" :open="isOpen && editAllowed"
:disabled="readOnly || !editAllowed || isLockedMode" :disabled="readOnly || !editAllowed"
:class="{ 'caret-transparent': !hasEditRoles }" :class="{ 'caret-transparent': !hasEditRoles }"
:dropdown-class-name="`nc-dropdown-multi-select-cell ${isOpen ? 'active' : ''}`" :dropdown-class-name="`nc-dropdown-multi-select-cell ${isOpen ? 'active' : ''}`"
@search="search" @search="search"

12
packages/nc-gui/components/cell/SingleSelect.vue

@ -43,8 +43,6 @@ const column = inject(ColumnInj)!
const readOnly = inject(ReadonlyInj)! const readOnly = inject(ReadonlyInj)!
const isLockedMode = inject(IsLockedInj, ref(false))
const isEditable = inject(EditModeInj, ref(false)) const isEditable = inject(EditModeInj, ref(false))
const activeCell = inject(ActiveCellInj, ref(false)) const activeCell = inject(ActiveCellInj, ref(false))
@ -264,11 +262,7 @@ const selectedOpt = computed(() => {
</script> </script>
<template> <template>
<div <div class="h-full w-full flex items-center nc-single-select" :class="{ 'read-only': readOnly }" @click="toggleMenu">
class="h-full w-full flex items-center nc-single-select"
:class="{ 'read-only': readOnly || isLockedMode }"
@click="toggleMenu"
>
<div v-if="!(active || isEditable)"> <div v-if="!(active || isEditable)">
<a-tag v-if="selectedOpt" class="rounded-tag" :color="selectedOpt.color"> <a-tag v-if="selectedOpt" class="rounded-tag" :color="selectedOpt.color">
<span <span
@ -295,8 +289,8 @@ const selectedOpt = computed(() => {
:allow-clear="!column.rqd && editAllowed" :allow-clear="!column.rqd && editAllowed"
:bordered="false" :bordered="false"
:open="isOpen && editAllowed" :open="isOpen && editAllowed"
:disabled="readOnly || !editAllowed || isLockedMode" :disabled="readOnly || !editAllowed"
:show-arrow="hasEditRoles && !(readOnly || isLockedMode) && active && vModel === null" :show-arrow="hasEditRoles && !readOnly && active && vModel === null"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && active ? 'active' : ''}`" :dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen && active ? 'active' : ''}`"
:show-search="!isMobileMode && isOpen && active" :show-search="!isMobileMode && isOpen && active"
@select="onSelect" @select="onSelect"

8
packages/nc-gui/components/cell/attachment/Modal.vue

@ -25,8 +25,6 @@ const {
renameFile, renameFile,
} = useAttachmentCell()! } = useAttachmentCell()!
const isLocked = inject(IsLockedInj, ref(false))
const dropZoneRef = ref<HTMLDivElement>() const dropZoneRef = ref<HTMLDivElement>()
const sortableRef = ref<HTMLDivElement>() const sortableRef = ref<HTMLDivElement>()
@ -96,7 +94,7 @@ const handleFileDelete = (i: number) => {
<template #title> <template #title>
<div class="flex gap-4"> <div class="flex gap-4">
<div <div
v-if="isSharedForm || (!readOnly && isUIAllowed('dataEdit') && !isPublic && !isLocked)" v-if="isSharedForm || (!readOnly && isUIAllowed('dataEdit') && !isPublic)"
class="nc-attach-file group" class="nc-attach-file group"
data-testid="attachment-expand-file-picker-button" data-testid="attachment-expand-file-picker-button"
@click="open" @click="open"
@ -143,7 +141,7 @@ const handleFileDelete = (i: number) => {
<template #title> {{ $t('title.removeFile') }} </template> <template #title> {{ $t('title.removeFile') }} </template>
<component <component
:is="iconMap.closeCircle" :is="iconMap.closeCircle"
v-if="isSharedForm || (isUIAllowed('dataEdit') && !isPublic && !isLocked)" v-if="isSharedForm || (isUIAllowed('dataEdit') && !isPublic)"
class="nc-attachment-remove" class="nc-attachment-remove"
@click.stop="onRemoveFileClick(item.title, i)" @click.stop="onRemoveFileClick(item.title, i)"
/> />
@ -157,7 +155,7 @@ const handleFileDelete = (i: number) => {
</div> </div>
</a-tooltip> </a-tooltip>
<a-tooltip v-if="isSharedForm || (!readOnly && isUIAllowed('dataEdit') && !isPublic && !isLocked)" placement="bottom"> <a-tooltip v-if="isSharedForm || (!readOnly && isUIAllowed('dataEdit') && !isPublic)" placement="bottom">
<template #title> {{ $t('title.renameFile') }} </template> <template #title> {{ $t('title.renameFile') }} </template>
<div class="nc-attachment-download group-hover:(opacity-100) mr-[35px]"> <div class="nc-attachment-download group-hover:(opacity-100) mr-[35px]">

10
packages/nc-gui/components/cell/attachment/index.vue

@ -43,8 +43,6 @@ const sortableRef = ref<HTMLDivElement>()
const currentCellRef = inject(CurrentCellInj, dropZoneInjection.value) const currentCellRef = inject(CurrentCellInj, dropZoneInjection.value)
const isLockedMode = inject(IsLockedInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false)) const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false)) const isKanban = inject(IsKanbanInj, ref(false))
@ -69,18 +67,14 @@ const {
open: _open, open: _open,
FileIcon, FileIcon,
selectedImage, selectedImage,
isReadonly: _isReadonly, isReadonly,
storedFiles, storedFiles,
} = useProvideAttachmentCell(updateModelValue) } = useProvideAttachmentCell(updateModelValue)
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, _isReadonly) const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, isReadonly)
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
const isReadonly = computed(() => {
return isLockedMode.value || _isReadonly.value
})
const { state: rowState } = useSmartsheetRowStoreOrThrow() const { state: rowState } = useSmartsheetRowStoreOrThrow()
const { isOverDropZone } = useDropZone(currentCellRef as any, onDrop) const { isOverDropZone } = useDropZone(currentCellRef as any, onDrop)

8
packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue

@ -16,6 +16,8 @@ const { metas } = useMetas()
const workspaceStore = useWorkspace() const workspaceStore = useWorkspace()
const isLocked = inject(IsLockedInj, ref(false))
const isUpdating = ref({ const isUpdating = ref({
public: false, public: false,
password: false, password: false,
@ -269,10 +271,6 @@ function onChangeTheme(color: string) {
const isPublicShared = computed(() => { const isPublicShared = computed(() => {
return !!activeView.value?.uuid return !!activeView.value?.uuid
}) })
const isPublicShareDisabled = computed(() => {
return false
})
</script> </script>
<template> <template>
@ -286,7 +284,7 @@ const isPublicShareDisabled = computed(() => {
:checked="isPublicShared" :checked="isPublicShared"
:loading="isUpdating.public" :loading="isUpdating.public"
class="share-view-toggle !mt-0.25" class="share-view-toggle !mt-0.25"
:disabled="isPublicShareDisabled" :disabled="isLocked"
@click="toggleShare" @click="toggleShare"
/> />
</div> </div>

8
packages/nc-gui/components/smartsheet/Cell.vue

@ -87,8 +87,6 @@ const isGrid = inject(IsGridInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false)) const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isEditColumnMenu = inject(EditColumnInj, ref(false)) const isEditColumnMenu = inject(EditColumnInj, ref(false))
@ -255,11 +253,7 @@ onUnmounted(() => {
<LazyCellJson v-else-if="isJSON(column)" v-model="vModel" /> <LazyCellJson v-else-if="isJSON(column)" v-model="vModel" />
<LazyCellText v-else v-model="vModel" /> <LazyCellText v-else v-model="vModel" />
<div <div
v-if=" v-if="(isPublic && readOnly && !isForm) || (isSystemColumn(column) && !isAttachment(column) && !isTextArea(column))"
(isLocked || (isPublic && readOnly && !isForm) || isSystemColumn(column)) &&
!isAttachment(column) &&
!isTextArea(column)
"
class="nc-locked-overlay" class="nc-locked-overlay"
/> />
</template> </template>

33
packages/nc-gui/components/smartsheet/Form.vue

@ -48,6 +48,8 @@ const formState = reactive({})
const secondsRemain = ref(0) const secondsRemain = ref(0)
const isLocked = inject(IsLockedInj, ref(false))
const isEditable = isUIAllowed('viewFieldEdit' as Permission) const isEditable = isUIAllowed('viewFieldEdit' as Permission)
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
@ -347,6 +349,8 @@ watch(submitted, (v) => {
}) })
function handleMouseUp(col: Record<string, any>, hiddenColIndex: number) { function handleMouseUp(col: Record<string, any>, hiddenColIndex: number) {
if (isLocked.value) return
if (!moved.value) { if (!moved.value) {
const index = localColumns.value.length const index = localColumns.value.length
col.order = (index ? localColumns.value[index - 1].order : 0) + 1 col.order = (index ? localColumns.value[index - 1].order : 0) + 1
@ -378,6 +382,12 @@ watch(view, (nextView) => {
reloadEventHook.trigger() reloadEventHook.trigger()
} }
}) })
const onFormItemClick = (element: any) => {
if (isLocked.value) return
activeRow.value = element.title
}
</script> </script>
<template> <template>
@ -427,6 +437,7 @@ watch(view, (nextView) => {
type="secondary" type="secondary"
class="nc-form-add-all" class="nc-form-add-all"
data-testid="nc-form-add-all" data-testid="nc-form-add-all"
:disabled="isLocked"
@click="addAllColumns" @click="addAllColumns"
> >
<!-- Add all --> <!-- Add all -->
@ -438,6 +449,7 @@ watch(view, (nextView) => {
type="secondary" type="secondary"
class="nc-form-remove-all" class="nc-form-remove-all"
data-testid="nc-form-remove-all" data-testid="nc-form-remove-all"
:disabled="isLocked"
@click="removeAllColumns" @click="removeAllColumns"
> >
<!-- Remove all --> <!-- Remove all -->
@ -451,6 +463,7 @@ watch(view, (nextView) => {
item-key="id" item-key="id"
draggable=".item" draggable=".item"
group="form-inputs" group="form-inputs"
:disabled="isLocked"
class="flex flex-col gap-2" class="flex flex-col gap-2"
@start="drag = true" @start="drag = true"
@end="drag = false" @end="drag = false"
@ -459,6 +472,9 @@ watch(view, (nextView) => {
<a-card <a-card
size="small" size="small"
class="!border-0 color-transition cursor-pointer item hover:(bg-primary ring-1 ring-accent ring-opacity-100) bg-opacity-10 !rounded !shadow-lg" class="!border-0 color-transition cursor-pointer item hover:(bg-primary ring-1 ring-accent ring-opacity-100) bg-opacity-10 !rounded !shadow-lg"
:class="{
'!bg-gray-50 !hover:bg-gray-50 !hover:ring-transparent !cursor-not-allowed': isLocked,
}"
:data-testid="`nc-form-hidden-column-${element.label || element.title}`" :data-testid="`nc-form-hidden-column-${element.label || element.title}`"
@mousedown="moved = false" @mousedown="moved = false"
@mousemove="moved = false" @mousemove="moved = false"
@ -484,7 +500,7 @@ watch(view, (nextView) => {
</a-card> </a-card>
</template> </template>
<template #footer> <template v-if="!isLocked" #footer>
<div <div
class="my-4 select-none border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center nc-drag-n-drop-to-hide" class="my-4 select-none border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center nc-drag-n-drop-to-hide"
data-testid="nc-drag-n-drop-to-hide" data-testid="nc-drag-n-drop-to-hide"
@ -553,6 +569,7 @@ watch(view, (nextView) => {
autosize autosize
size="large" size="large"
hide-details hide-details
:disabled="isLocked"
placeholder="Form Title" placeholder="Form Title"
:bordered="false" :bordered="false"
data-testid="nc-form-heading" data-testid="nc-form-heading"
@ -580,7 +597,7 @@ watch(view, (nextView) => {
hide-details hide-details
:placeholder="$t('msg.info.formDesc')" :placeholder="$t('msg.info.formDesc')"
:bordered="false" :bordered="false"
:disabled="!isEditable" :disabled="!isEditable || isLocked"
data-testid="nc-form-sub-heading" data-testid="nc-form-sub-heading"
@blur="updateView" @blur="updateView"
@click="updateView" @click="updateView"
@ -598,6 +615,7 @@ watch(view, (nextView) => {
group="form-inputs" group="form-inputs"
class="h-full" class="h-full"
:move="onMoveCallback" :move="onMoveCallback"
:disabled="isLocked"
@change="onMove($event)" @change="onMove($event)"
@start="drag = true" @start="drag = true"
@end="drag = false" @end="drag = false"
@ -610,12 +628,15 @@ watch(view, (nextView) => {
{ {
'bg-primary bg-opacity-5 ring-0.5 ring-accent ring-opacity-100': activeRow === element.title, 'bg-primary bg-opacity-5 ring-0.5 ring-accent ring-opacity-100': activeRow === element.title,
}, },
{
'!hover:bg-white !ring-0 !cursor-auto': isLocked,
},
]" ]"
data-testid="nc-form-fields" data-testid="nc-form-fields"
@click="activeRow = element.title" @click="onFormItemClick(element)"
> >
<div <div
v-if="isUIAllowed('viewFieldEdit') && !isRequired(element, element.required)" v-if="isUIAllowed('viewFieldEdit') && !isRequired(element, element.required) && !isLocked"
class="absolute flex top-2 right-2" class="absolute flex top-2 right-2"
> >
<component <component
@ -795,9 +816,9 @@ watch(view, (nextView) => {
</a-card> </a-card>
</a-form> </a-form>
<a-divider /> <a-divider v-if="!isLocked" />
<div v-if="isEditable" class="px-4 flex flex-col gap-2"> <div v-if="isEditable && !isLocked" class="px-4 flex flex-col gap-2">
<!-- After form is submitted --> <!-- After form is submitted -->
<div class="text-lg text-gray-700"> <div class="text-lg text-gray-700">
{{ $t('msg.info.afterFormSubmitted') }} {{ $t('msg.info.afterFormSubmitted') }}

16
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -37,6 +37,7 @@ const props = defineProps<{
hideType?: boolean hideType?: boolean
hideAdditionalOptions?: boolean hideAdditionalOptions?: boolean
fromTableExplorer?: boolean fromTableExplorer?: boolean
readonly?: boolean
}>() }>()
const emit = defineEmits(['submit', 'cancel', 'mounted', 'add', 'update']) const emit = defineEmits(['submit', 'cancel', 'mounted', 'add', 'update'])
@ -66,6 +67,8 @@ const isForm = inject(IsFormInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false)) const isKanban = inject(IsKanbanInj, ref(false))
const readOnly = computed(() => props.readonly)
const { isMysql, isMssql, isXcdbBase } = useBase() const { isMysql, isMssql, isXcdbBase } = useBase()
const reloadDataTrigger = inject(ReloadViewDataHookInj) const reloadDataTrigger = inject(ReloadViewDataHookInj)
@ -114,6 +117,8 @@ const reloadMetaAndData = async () => {
const saving = ref(false) const saving = ref(false)
async function onSubmit() { async function onSubmit() {
if (readOnly.value) return
saving.value = true saving.value = true
const saved = await addOrUpdate(reloadMetaAndData, props.columnPosition) const saved = await addOrUpdate(reloadMetaAndData, props.columnPosition)
saving.value = false saving.value = false
@ -134,7 +139,7 @@ async function onSubmit() {
// focus and select the column name field // focus and select the column name field
const antInput = ref() const antInput = ref()
watchEffect(() => { watchEffect(() => {
if (antInput.value && formState.value) { if (antInput.value && formState.value && !readOnly.value) {
// todo: replace setTimeout // todo: replace setTimeout
setTimeout(() => { setTimeout(() => {
// focus and select input only if active element is not an input or textarea // focus and select input only if active element is not an input or textarea
@ -232,6 +237,7 @@ if (props.fromTableExplorer) {
<input <input
ref="antInput" ref="antInput"
v-model="formState.title" v-model="formState.title"
:disabled="readOnly"
class="flex flex-grow nc-fields-input text-lg font-bold outline-none bg-inherit" class="flex flex-grow nc-fields-input text-lg font-bold outline-none bg-inherit"
:contenteditable="true" :contenteditable="true"
/> />
@ -247,7 +253,7 @@ if (props.fromTableExplorer) {
ref="antInput" ref="antInput"
v-model:value="formState.title" v-model:value="formState.title"
class="nc-column-name-input !rounded !mt-1" class="nc-column-name-input !rounded !mt-1"
:disabled="isKanban" :disabled="isKanban || readOnly"
@input="onAlter(8)" @input="onAlter(8)"
/> />
</a-form-item> </a-form-item>
@ -263,7 +269,7 @@ if (props.fromTableExplorer) {
v-model:value="formState.uidt" v-model:value="formState.uidt"
show-search show-search
class="nc-column-type-input !rounded" class="nc-column-type-input !rounded"
:disabled="isKanban" :disabled="isKanban || readOnly"
dropdown-class-name="nc-dropdown-column-type " dropdown-class-name="nc-dropdown-column-type "
@change="onUidtOrIdTypeChange" @change="onUidtOrIdTypeChange"
@dblclick="showDeprecated = !showDeprecated" @dblclick="showDeprecated = !showDeprecated"
@ -285,6 +291,7 @@ if (props.fromTableExplorer) {
</div> --> </div> -->
</div> </div>
<template v-if="!readOnly">
<LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" /> <LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" /> <LazySmartsheetColumnQrCodeOptions v-if="formState.uidt === UITypes.QrCode" v-model="formState" />
<LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" /> <LazySmartsheetColumnBarcodeOptions v-if="formState.uidt === UITypes.Barcode" v-model="formState" />
@ -307,6 +314,7 @@ if (props.fromTableExplorer) {
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect" v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"
v-model:value="formState" v-model:value="formState"
/> />
</template>
</div> </div>
<a-checkbox <a-checkbox
v-if="formState.meta && columnToValidate.includes(formState.uidt)" v-if="formState.meta && columnToValidate.includes(formState.uidt)"
@ -317,6 +325,7 @@ if (props.fromTableExplorer) {
{{ `${$t('msg.acceptOnlyValid')} ${formState.uidt}` }} {{ `${$t('msg.acceptOnlyValid')} ${formState.uidt}` }}
</span> </span>
</a-checkbox> </a-checkbox>
<template v-if="!readOnly">
<div class="!my-3"> <div class="!my-3">
<!-- <!--
Default Value for JSON & LongText is not supported in MySQL Default Value for JSON & LongText is not supported in MySQL
@ -352,6 +361,7 @@ if (props.fromTableExplorer) {
/> />
</div> </div>
</Transition> </Transition>
</template>
<template v-if="props.fromTableExplorer"> <template v-if="props.fromTableExplorer">
<a-form-item></a-form-item> <a-form-item></a-form-item>

53
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -40,6 +40,8 @@ const { getMeta } = useMetas()
const { meta, view } = useSmartsheetStoreOrThrow() const { meta, view } = useSmartsheetStoreOrThrow()
const isLocked = inject(IsLockedInj, ref(false))
const { openedViewsTab } = storeToRefs(useViewsStore()) const { openedViewsTab } = storeToRefs(useViewsStore())
const moveOps = ref<moveOp[]>([]) const moveOps = ref<moveOp[]>([])
@ -587,6 +589,8 @@ const toggleVisibility = async (checked: boolean, field: Field) => {
useEventListener(document, 'keydown', async (e: KeyboardEvent) => { useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
if (isLocked.value) return
if (cmdOrCtrl && e.key.toLowerCase() === 's') { if (cmdOrCtrl && e.key.toLowerCase() === 's') {
if (openedViewsTab.value !== 'field') return if (openedViewsTab.value !== 'field') return
e.preventDefault() e.preventDefault()
@ -625,7 +629,11 @@ onKeyDown('ArrowUp', () => {
}) })
onKeyDown('Delete', () => { onKeyDown('Delete', () => {
if (isLocked.value) return
if (document.activeElement?.tagName === 'INPUT') return
if (document.activeElement?.tagName === 'TEXTAREA') return if (document.activeElement?.tagName === 'TEXTAREA') return
const isDeletedField = fieldStatus(activeField.value) === 'delete' const isDeletedField = fieldStatus(activeField.value) === 'delete'
if (!isDeletedField && activeField.value) { if (!isDeletedField && activeField.value) {
onFieldDelete(activeField.value) onFieldDelete(activeField.value)
@ -633,7 +641,11 @@ onKeyDown('Delete', () => {
}) })
onKeyDown('Backspace', () => { onKeyDown('Backspace', () => {
if (isLocked.value) return
if (document.activeElement?.tagName === 'INPUT') return
if (document.activeElement?.tagName === 'TEXTAREA') return if (document.activeElement?.tagName === 'TEXTAREA') return
const isDeletedField = fieldStatus(activeField.value) === 'delete' const isDeletedField = fieldStatus(activeField.value) === 'delete'
if (!isDeletedField && activeField.value) { if (!isDeletedField && activeField.value) {
onFieldDelete(activeField.value) onFieldDelete(activeField.value)
@ -659,11 +671,15 @@ const onClickCopyFieldUrl = async (field: ColumnType) => {
const keys = useMagicKeys() const keys = useMagicKeys()
whenever(keys.meta_s, () => { whenever(keys.meta_s, () => {
if (isLocked.value) return
if (!meta.value?.id) return if (!meta.value?.id) return
if (openedViewsTab.value === 'field') saveChanges() if (openedViewsTab.value === 'field') saveChanges()
}) })
whenever(keys.ctrl_s, () => { whenever(keys.ctrl_s, () => {
if (isLocked.value) return
if (!meta.value?.id) return if (!meta.value?.id) return
if (openedViewsTab.value === 'field') saveChanges() if (openedViewsTab.value === 'field') saveChanges()
}) })
@ -718,9 +734,9 @@ const onFieldOptionUpdate = () => {
</template> </template>
</a-input> </a-input>
<div class="flex gap-2"> <div class="flex gap-2">
<NcTooltip> <NcTooltip :disabled="isLocked">
<template #title> {{ `${renderAltOrOptlKey()} + C` }} </template> <template #title> {{ `${renderAltOrOptlKey()} + C` }} </template>
<NcButton type="secondary" size="small" class="mr-1" :disabled="loading" @click="addField()"> <NcButton type="secondary" size="small" class="mr-1" :disabled="loading || isLocked" @click="addField()">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<GeneralIcon icon="plus" class="w-3" /> <GeneralIcon icon="plus" class="w-3" />
New Field New Field
@ -730,19 +746,22 @@ const onFieldOptionUpdate = () => {
<NcButton <NcButton
type="secondary" type="secondary"
size="small" size="small"
:disabled="!loading && ops.length < 1 && moveOps.length < 1 && visibilityOps.length < 1" :disabled="(!loading && ops.length < 1 && moveOps.length < 1 && visibilityOps.length < 1) || isLocked"
@click="clearChanges()" @click="clearChanges()"
> >
Reset Reset
</NcButton> </NcButton>
<NcTooltip> <NcTooltip :disabled="isLocked">
<template #title> {{ `${renderCmdOrCtrlKey()} + S` }} </template> <template #title> {{ `${renderCmdOrCtrlKey()} + S` }} </template>
<NcButton <NcButton
type="primary" type="primary"
size="small" size="small"
:loading="loading" :loading="loading"
:disabled="isColumnsValid ? !loading && ops.length < 1 && moveOps.length < 1 && visibilityOps.length < 1 : true" :disabled="
(isColumnsValid ? !loading && ops.length < 1 && moveOps.length < 1 && visibilityOps.length < 1 : true) ||
isLocked
"
@click="saveChanges()" @click="saveChanges()"
> >
Save changes Save changes
@ -752,7 +771,7 @@ const onFieldOptionUpdate = () => {
</div> </div>
<div class="flex flex-row rounded-lg border-1 border-gray-200"> <div class="flex flex-row rounded-lg border-1 border-gray-200">
<div ref="fieldsListWrapperDomRef" class="nc-scrollbar-md !overflow-auto w-full flex-grow-1 nc-fields-height"> <div ref="fieldsListWrapperDomRef" class="nc-scrollbar-md !overflow-auto w-full flex-grow-1 nc-fields-height">
<Draggable v-model="fields" item-key="id" @change="onMove($event)"> <Draggable v-model="fields" :disabled="isLocked" item-key="id" @change="onMove($event)">
<template #item="{ element: field }"> <template #item="{ element: field }">
<div <div
v-if="field.title.toLowerCase().includes(searchQuery.toLowerCase()) && !field.pv" v-if="field.title.toLowerCase().includes(searchQuery.toLowerCase()) && !field.pv"
@ -761,9 +780,16 @@ const onFieldOptionUpdate = () => {
@click="changeField(field, $event)" @click="changeField(field, $event)"
> >
<div class="flex items-center flex-1 py-2.5 gap-1 w-2/6"> <div class="flex items-center flex-1 py-2.5 gap-1 w-2/6">
<component :is="iconMap.drag" class="cursor-move !h-3.75 text-gray-600 mr-1" /> <component
:is="iconMap.drag"
class="cursor-move !h-3.75 text-gray-600 mr-1"
:class="{
'opacity-0 !cursor-default': isLocked,
}"
/>
<NcCheckbox <NcCheckbox
v-if="field.id && viewFieldsMap[field.id]" v-if="field.id && viewFieldsMap[field.id]"
:disabled="isLocked"
:checked=" :checked="
visibilityOps.find((op) => op.column.fk_column_id === field.id)?.visible ?? viewFieldsMap[field.id].show visibilityOps.find((op) => op.column.fk_column_id === field.id)?.visible ?? viewFieldsMap[field.id].show
" "
@ -882,9 +908,10 @@ const onFieldOptionUpdate = () => {
</NcButton> </NcButton>
</div> </div>
</NcTooltip> </NcTooltip>
<a-menu-divider class="my-1.5" /> <a-menu-divider v-if="!isLocked" class="my-1.5" />
</template> </template>
<template v-if="!isLocked">
<NcMenuItem key="table-explorer-duplicate" @click="duplicateField(field)"> <NcMenuItem key="table-explorer-duplicate" @click="duplicateField(field)">
<Icon class="iconify text-gray-800" icon="lucide:copy" /><span>Duplicate</span> <Icon class="iconify text-gray-800" icon="lucide:copy" /><span>Duplicate</span>
</NcMenuItem> </NcMenuItem>
@ -903,6 +930,7 @@ const onFieldOptionUpdate = () => {
Delete Delete
</div> </div>
</NcMenuItem> </NcMenuItem>
</template>
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>
@ -927,7 +955,13 @@ const onFieldOptionUpdate = () => {
@click="changeField(displayColumn, $event)" @click="changeField(displayColumn, $event)"
> >
<div class="flex items-center flex-1 py-2.5 gap-1 w-2/6"> <div class="flex items-center flex-1 py-2.5 gap-1 w-2/6">
<component :is="iconMap.drag" class="cursor-move !h-3.75 text-gray-200 mr-1" /> <component
:is="iconMap.drag"
class="cursor-move !h-3.75 text-gray-200 mr-1"
:class="{
'opacity-0 !cursor-default': isLocked,
}"
/>
<NcCheckbox :disabled="true" :checked="true" /> <NcCheckbox :disabled="true" :checked="true" />
<SmartsheetHeaderCellIcon <SmartsheetHeaderCellIcon
v-if="displayColumn" v-if="displayColumn"
@ -1046,6 +1080,7 @@ const onFieldOptionUpdate = () => {
:preload="fieldState(activeField)" :preload="fieldState(activeField)"
:table-explorer-columns="fields" :table-explorer-columns="fields"
embed-mode embed-mode
:readonly="isLocked"
from-table-explorer from-table-explorer
@update="onFieldUpdate" @update="onFieldUpdate"
@add="onFieldAdd" @add="onFieldAdd"

28
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -188,7 +188,7 @@ const isViewColumnsLoading = computed(() => _isViewColumnsLoading.value || !meta
// #Permissions // #Permissions
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const hasEditPermission = computed(() => isUIAllowed('dataEdit') && !isLocked.value) const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
const isAddingColumnAllowed = computed(() => !readOnly.value && !isLocked.value && isUIAllowed('fieldAdd') && !isSqlView.value) const isAddingColumnAllowed = computed(() => !readOnly.value && !isLocked.value && isUIAllowed('fieldAdd') && !isSqlView.value)
const { onDrag, onDragStart, draggedCol, dragColPlaceholderDomRef, toBeDroppedColId } = useColumnDrag({ const { onDrag, onDragStart, draggedCol, dragColPlaceholderDomRef, toBeDroppedColId } = useColumnDrag({
@ -351,7 +351,7 @@ async function clearCell(ctx: { row: number; col: number } | null, skipUpdate =
} }
function makeEditable(row: Row, col: ColumnType) { function makeEditable(row: Row, col: ColumnType) {
if (!hasEditPermission.value || editEnabled.value || isView || isLocked.value || readOnly.value || isSystemColumn(col)) { if (!hasEditPermission.value || editEnabled.value || isView || readOnly.value || isSystemColumn(col)) {
return return
} }
@ -382,9 +382,7 @@ function makeEditable(row: Row, col: ColumnType) {
// #Computed // #Computed
const isAddingEmptyRowAllowed = computed( const isAddingEmptyRowAllowed = computed(() => !isView && hasEditPermission.value && !isSqlView.value && !isPublicView.value)
() => !isView && !isLocked.value && hasEditPermission.value && !isSqlView.value && !isPublicView.value,
)
const visibleColLength = computed(() => fields.value?.length) const visibleColLength = computed(() => fields.value?.length)
@ -452,9 +450,7 @@ async function openNewRecordHandler() {
} }
const onDraftRecordClick = () => { const onDraftRecordClick = () => {
if (!isLocked?.value) {
openNewRecordFormHook.trigger() openNewRecordFormHook.trigger()
}
} }
const onNewRecordToGridClick = () => { const onNewRecordToGridClick = () => {
@ -987,7 +983,6 @@ const refreshFillHandle = () => {
const showFillHandle = computed( const showFillHandle = computed(
() => () =>
!readOnly.value && !readOnly.value &&
!isLocked.value &&
!editEnabled.value && !editEnabled.value &&
(!selectedRange.isEmpty() || (activeCell.row !== null && activeCell.col !== null)) && (!selectedRange.isEmpty() || (activeCell.row !== null && activeCell.col !== null)) &&
!dataRef.value[(isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row) ?? -1]?.rowMeta?.new, !dataRef.value[(isNaN(selectedRange.end.row) ? activeCell.row : selectedRange.end.row) ?? -1]?.rowMeta?.new,
@ -1477,7 +1472,7 @@ onKeyStroke('ArrowDown', onDown)
> >
<div class="items-center flex gap-1 min-w-[60px]"> <div class="items-center flex gap-1 min-w-[60px]">
<div <div
v-if="!readOnly || !isLocked || isMobileMode" v-if="!readOnly || isMobileMode"
class="nc-row-no sm:min-w-4 text-xs text-gray-500" class="nc-row-no sm:min-w-4 text-xs text-gray-500"
:class="{ toggle: !readOnly, hidden: row.rowMeta.selected }" :class="{ toggle: !readOnly, hidden: row.rowMeta.selected }"
> >
@ -1503,7 +1498,7 @@ onKeyStroke('ArrowDown', onDown)
class="!flex items-center" class="!flex items-center"
:data-testid="`row-save-spinner-${rowIndex}`" :data-testid="`row-save-spinner-${rowIndex}`"
/> />
<template v-else-if="!isLocked">
<span <span
v-if="row.rowMeta?.commentCount && expandForm" v-if="row.rowMeta?.commentCount && expandForm"
v-e="['c:expanded-form:open']" v-e="['c:expanded-form:open']"
@ -1514,7 +1509,7 @@ onKeyStroke('ArrowDown', onDown)
{{ row.rowMeta.commentCount }} {{ row.rowMeta.commentCount }}
</span> </span>
<div <div
v-else v-else-if="!row.rowMeta.saving"
class="cursor-pointer flex items-center border-1 border-gray-100 active:ring rounded p-1 hover:(bg-gray-50)" class="cursor-pointer flex items-center border-1 border-gray-100 active:ring rounded p-1 hover:(bg-gray-50)"
> >
<component <component
@ -1525,7 +1520,6 @@ onKeyStroke('ArrowDown', onDown)
@click="expandAndLooseFocus(row, state)" @click="expandAndLooseFocus(row, state)"
/> />
</div> </div>
</template>
</div> </div>
</div> </div>
</td> </td>
@ -1717,16 +1711,14 @@ onKeyStroke('ArrowDown', onDown)
{{ $t('general.clear') }} {{ $t('general.clear') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
<NcDivider /> <NcDivider />
<NcMenuItem <NcMenuItem
v-if="contextMenuTarget && !isLocked && selectedRange.isSingleCell() && isUIAllowed('commentEdit') && !isMobileMode" v-if="contextMenuTarget && selectedRange.isSingleCell() && isUIAllowed('commentEdit') && !isMobileMode"
class="nc-base-menu-item" class="nc-base-menu-item"
@click="commentRow(contextMenuTarget.row)" @click="commentRow(contextMenuTarget.row)"
> >
<div v-e="['a:row:comment']" class="flex gap-2 items-center"> <div v-e="['a:row:comment']" class="flex gap-2 items-center">
<MdiMessageOutline class="h-4 w-4" /> <MdiMessageOutline class="h-4 w-4" />
{{ $t('general.comment') }} {{ $t('general.comment') }}
</div> </div>
</NcMenuItem> </NcMenuItem>
@ -1810,8 +1802,7 @@ onKeyStroke('ArrowDown', onDown)
> >
<div <div
v-e="['c:row:add:grid']" v-e="['c:row:add:grid']"
:class="{ 'group': !isLocked, 'disabled-ring': isLocked }" class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer hover:bg-gray-100 text-gray-600 nc-new-record-with-grid group"
class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer hover:bg-gray-100 text-gray-600 nc-new-record-with-grid"
@click="onNewRecordToGridClick" @click="onNewRecordToGridClick"
> >
<div class="flex flex-row items-center justify-between w-full"> <div class="flex flex-row items-center justify-between w-full">
@ -1827,8 +1818,7 @@ onKeyStroke('ArrowDown', onDown)
</div> </div>
<div <div
v-e="['c:row:add:form']" v-e="['c:row:add:form']"
:class="{ 'group': !isLocked, 'disabled-ring': isLocked }" class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer hover:bg-gray-100 text-gray-600 nc-new-record-with-form group"
class="px-4 py-3 flex flex-col select-none gap-y-2 cursor-pointer hover:bg-gray-100 text-gray-600 nc-new-record-with-form"
@click="onNewRecordToFormClick" @click="onNewRecordToFormClick"
> >
<div class="flex flex-row items-center justify-between w-full"> <div class="flex flex-row items-center justify-between w-full">

6
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -17,6 +17,8 @@ const hideMenu = toRef(props, 'hideMenu')
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const isSurveyForm = inject(IsSurveyFormInj, ref(false)) const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false)) const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
@ -46,12 +48,16 @@ const closeAddColumnDropdown = () => {
} }
const openHeaderMenu = () => { const openHeaderMenu = () => {
if (isLocked.value) return
if (!isForm.value && !isExpandedForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value) { if (!isForm.value && !isExpandedForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value) {
editColumnDropdown.value = true editColumnDropdown.value = true
} }
} }
const openDropDown = (e: Event) => { const openDropDown = (e: Event) => {
if (isLocked.value) return
if (isForm.value || isExpandedForm.value || (!isUIAllowed('fieldEdit') && !isMobileMode.value)) return if (isForm.value || isExpandedForm.value || (!isUIAllowed('fieldEdit') && !isMobileMode.value)) return
e.preventDefault() e.preventDefault()

6
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -36,6 +36,8 @@ const editColumnDropdown = ref(false)
const isDropDownOpen = ref(false) const isDropDownOpen = ref(false)
const isLocked = inject(IsLockedInj, ref(false))
provide(ColumnInj, column) provide(ColumnInj, column)
const { metas } = useMetas() const { metas } = useMetas()
@ -124,12 +126,16 @@ const closeAddColumnDropdown = () => {
} }
const openHeaderMenu = () => { const openHeaderMenu = () => {
if (isLocked.value) return
if (!isForm.value && !isExpandedForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value) { if (!isForm.value && !isExpandedForm.value && isUIAllowed('fieldEdit') && !isMobileMode.value) {
editColumnDropdown.value = true editColumnDropdown.value = true
} }
} }
const openDropDown = (e: Event) => { const openDropDown = (e: Event) => {
if (isLocked.value) return
if (isForm.value || isExpandedForm.value || (!isUIAllowed('fieldEdit') && !isMobileMode.value)) return if (isForm.value || isExpandedForm.value || (!isUIAllowed('fieldEdit') && !isMobileMode.value)) return
e.preventDefault() e.preventDefault()

28
packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue

@ -18,8 +18,6 @@ const { isUIAllowed } = useRoles()
const isPublicView = inject(IsPublicInj, ref(false)) const isPublicView = inject(IsPublicInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const { $api, $e } = useNuxtApp() const { $api, $e } = useNuxtApp()
const { t } = useI18n() const { t } = useI18n()
@ -58,7 +56,7 @@ const quickImportDialogs: Record<(typeof quickImportDialogTypes)[number], Ref<bo
) as Record<QuickImportDialogType, Ref<boolean>> ) as Record<QuickImportDialogType, Ref<boolean>>
const onImportClick = (dialog: any) => { const onImportClick = (dialog: any) => {
if (isLocked.value) return if (lockType.value === LockType.Locked) return
emits('closeModal') emits('closeModal')
dialog.value = true dialog.value = true
@ -163,10 +161,17 @@ const onDelete = async () => {
</NcTooltip> </NcTooltip>
<NcDivider /> <NcDivider />
<template v-if="!view?.is_default"> <template v-if="!view?.is_default">
<NcMenuItem @click="onRenameMenuClick"> <NcMenuItem v-if="lockType !== LockType.Locked" @click="onRenameMenuClick">
<GeneralIcon icon="edit" />
{{ $t('activity.renameView') }}
</NcMenuItem>
<NcTooltip v-else>
<template #title> {{ $t('msg.info.disabledAsViewLocked') }} </template>
<NcMenuItem class="!cursor-not-allowed !text-gray-400">
<GeneralIcon icon="edit" /> <GeneralIcon icon="edit" />
{{ $t('activity.renameView') }} {{ $t('activity.renameView') }}
</NcMenuItem> </NcMenuItem>
</NcTooltip>
<NcMenuItem @click="onDuplicate"> <NcMenuItem @click="onDuplicate">
<GeneralIcon icon="duplicate" class="nc-view-copy-icon" /> <GeneralIcon icon="duplicate" class="nc-view-copy-icon" />
{{ $t('labels.duplicateView') }} {{ $t('labels.duplicateView') }}
@ -205,7 +210,7 @@ const onDelete = async () => {
}, },
]" ]"
class="nc-base-menu-item" class="nc-base-menu-item"
:class="{ disabled: isLocked }" :class="{ disabled: lockType === LockType.Locked }"
> >
<component :is="iconMap.upload" /> <component :is="iconMap.upload" />
{{ `${$t('general.upload')} ${type.toUpperCase()}` }} {{ `${$t('general.upload')} ${type.toUpperCase()}` }}
@ -274,7 +279,18 @@ const onDelete = async () => {
</NcSubMenu> </NcSubMenu>
<template v-if="!view.is_default"> <template v-if="!view.is_default">
<NcDivider /> <NcDivider />
<NcMenuItem class="!hover:bg-red-50 !text-red-500" @click="onDelete"> <NcTooltip v-if="lockType === LockType.Locked">
<template #title> {{ $t('msg.info.disabledAsViewLocked') }} </template>
<NcMenuItem class="!cursor-not-allowed !text-gray-400">
<GeneralIcon icon="delete" class="nc-view-delete-icon" />
{{
$t('general.deleteEntity', {
entity: $t('objects.view'),
})
}}
</NcMenuItem>
</NcTooltip>
<NcMenuItem v-else class="!hover:bg-red-50 !text-red-500" @click="onDelete">
<GeneralIcon icon="delete" class="nc-view-delete-icon" /> <GeneralIcon icon="delete" class="nc-view-delete-icon" />
{{ {{
$t('general.deleteEntity', { $t('general.deleteEntity', {

5
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -6,7 +6,6 @@ import {
CellValueInj, CellValueInj,
ColumnInj, ColumnInj,
IsFormInj, IsFormInj,
IsLockedInj,
IsUnderLookupInj, IsUnderLookupInj,
ReadonlyInj, ReadonlyInj,
ReloadRowDataHookInj, ReloadRowDataHookInj,
@ -35,8 +34,6 @@ const readOnly = inject(ReadonlyInj, ref(false))
const isForm = inject(IsFormInj, ref(false)) const isForm = inject(IsFormInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const isUnderLookup = inject(IsUnderLookupInj, ref(false)) const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
@ -103,7 +100,7 @@ const belongsToColumn = computed(
</div> </div>
<div <div
v-if="!readOnly && !isLocked && (isUIAllowed('dataEdit') || isForm) && !isUnderLookup" v-if="!readOnly && (isUIAllowed('dataEdit') || isForm) && !isUnderLookup"
class="flex justify-end gap-1 min-h-[30px] items-center" class="flex justify-end gap-1 min-h-[30px] items-center"
> >
<GeneralIcon <GeneralIcon

5
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -5,7 +5,6 @@ import {
CellValueInj, CellValueInj,
ColumnInj, ColumnInj,
IsFormInj, IsFormInj,
IsLockedInj,
IsUnderLookupInj, IsUnderLookupInj,
ReadonlyInj, ReadonlyInj,
ReloadRowDataHookInj, ReloadRowDataHookInj,
@ -31,8 +30,6 @@ const isForm = inject(IsFormInj)
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj)
const isUnderLookup = inject(IsUnderLookupInj, ref(false)) const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
@ -121,7 +118,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
</template> </template>
</div> </div>
<div v-if="!isLocked && !isUnderLookup" class="flex justify-end gap-1 min-h-[30px] items-center"> <div v-if="!isUnderLookup" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon <GeneralIcon
icon="expand" icon="expand"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand" class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"

10
packages/nc-gui/components/virtual-cell/Links.vue

@ -17,8 +17,6 @@ const isForm = inject(IsFormInj)
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const isUnderLookup = inject(IsUnderLookupInj, ref(false)) const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const colTitle = computed(() => column.value?.title || '') const colTitle = computed(() => column.value?.title || '')
@ -79,15 +77,13 @@ const onAttachRecord = () => {
const openChildList = () => { const openChildList = () => {
if (isUnderLookup.value) return if (isUnderLookup.value) return
if (!isLocked.value) {
childListDlg.value = true childListDlg.value = true
}
} }
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => { useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
switch (e.key) { switch (e.key) {
case 'Enter': case 'Enter':
if (isLocked.value || listItemsDlg.value) return if (listItemsDlg.value) return
childListDlg.value = true childListDlg.value = true
e.stopPropagation() e.stopPropagation()
break break
@ -112,7 +108,7 @@ const openListDlg = () => {
<div class="flex w-full group items-center nc-links-wrapper" @dblclick.stop="openChildList"> <div class="flex w-full group items-center nc-links-wrapper" @dblclick.stop="openChildList">
<div class="block flex-shrink truncate"> <div class="block flex-shrink truncate">
<component <component
:is="isLocked || isUnderLookup ? 'span' : 'a'" :is="isUnderLookup ? 'span' : 'a'"
v-e="['c:cell:links:modal:open']" v-e="['c:cell:links:modal:open']"
:title="textVal" :title="textVal"
class="text-center nc-datatype-link underline-transparent" class="text-center nc-datatype-link underline-transparent"
@ -124,7 +120,7 @@ const openListDlg = () => {
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<div v-if="!isLocked && !isUnderLookup" class="!xs:hidden flex justify-end hidden group-hover:flex items-center"> <div v-if="!isUnderLookup" class="!xs:hidden flex justify-end hidden group-hover:flex items-center">
<MdiPlus <MdiPlus
v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm" v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus" class="select-none !text-md text-gray-700 nc-action-icon nc-plus"

5
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -6,7 +6,6 @@ import {
CellValueInj, CellValueInj,
ColumnInj, ColumnInj,
IsFormInj, IsFormInj,
IsLockedInj,
IsUnderLookupInj, IsUnderLookupInj,
ReadonlyInj, ReadonlyInj,
ReloadRowDataHookInj, ReloadRowDataHookInj,
@ -33,8 +32,6 @@ const isForm = inject(IsFormInj)
const readOnly = inject(ReadonlyInj, ref(false)) const readOnly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj)
const isUnderLookup = inject(IsUnderLookupInj, ref(false)) const isUnderLookup = inject(IsUnderLookupInj, ref(false))
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
@ -123,7 +120,7 @@ const m2mColumn = computed(
</template> </template>
</div> </div>
<div v-if="(!isLocked && !isUnderLookup) || isForm" class="flex justify-end gap-1 min-h-[30px] items-center"> <div v-if="!isUnderLookup || isForm" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon <GeneralIcon
icon="expand" icon="expand"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand" class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"

11
packages/nc-gui/components/virtual-cell/components/ItemChip.vue

@ -4,7 +4,6 @@ import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { import {
ActiveCellInj, ActiveCellInj,
IsFormInj, IsFormInj,
IsLockedInj,
ReadonlyInj, ReadonlyInj,
iconMap, iconMap,
inject, inject,
@ -37,13 +36,11 @@ const active = inject(ActiveCellInj, ref(false))
const isForm = inject(IsFormInj)! const isForm = inject(IsFormInj)!
const isLocked = inject(IsLockedInj, ref(false))
const { open } = useExpandedFormDetached() const { open } = useExpandedFormDetached()
function openExpandedForm() { function openExpandedForm() {
const rowId = extractPkFromRow(item, relatedTableMeta.value.columns as ColumnType[]) const rowId = extractPkFromRow(item, relatedTableMeta.value.columns as ColumnType[])
if (!readOnly.value && !isLocked.value && !readonlyProp && rowId) { if (!readOnly.value && !readonlyProp && rowId) {
open({ open({
isOpen: true, isOpen: true,
row: { row: item, rowMeta: {}, oldRow: { ...item } }, row: { row: item, rowMeta: {}, oldRow: { ...item } },
@ -98,11 +95,7 @@ export default {
</template> </template>
</span> </span>
<div <div v-show="active || isForm" v-if="showUnlinkButton && !readOnly && isUIAllowed('dataEdit')" class="flex items-center">
v-show="active || isForm"
v-if="showUnlinkButton && !readOnly && !isLocked && isUIAllowed('dataEdit')"
class="flex items-center"
>
<component <component
:is="iconMap.closeThick" :is="iconMap.closeThick"
class="nc-icon unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" class="nc-icon unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500"

1
packages/nc-gui/lang/en.json

@ -1033,6 +1033,7 @@
"duplicateTable": "Are you sure you want to duplicate the table?" "duplicateTable": "Are you sure you want to duplicate the table?"
}, },
"info": { "info": {
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.", "basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell", "pasteNotSupported": "Paste operation is not supported on the active cell",
"roles": { "roles": {

3
packages/nc-gui/store/views.ts

@ -110,6 +110,8 @@ export const useViewsStore = defineStore('viewsStore', () => {
}, },
}) })
const isActiveViewLocked = computed(() => activeView.value?.lock_type === 'locked')
// Used for Grid View Pagination // Used for Grid View Pagination
const isPaginationLoading = ref(true) const isPaginationLoading = ref(true)
@ -317,6 +319,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
removeFromRecentViews, removeFromRecentViews,
activeSorts, activeSorts,
activeNestedFilters, activeNestedFilters,
isActiveViewLocked,
} }
}) })

2
tests/playwright/pages/Dashboard/Grid/index.ts

@ -55,7 +55,7 @@ export class GridPage extends BasePage {
async verifyLockMode() { async verifyLockMode() {
// add new row button // add new row button
expect(await this.btn_addNewRow.count()).toBe(0); expect(await this.btn_addNewRow.count()).toBe(1);
await this.toolbar.verifyLockMode(); await this.toolbar.verifyLockMode();
await this.footbar.verifyLockMode(); await this.footbar.verifyLockMode();

2
tests/playwright/pages/Dashboard/common/Footbar/index.ts

@ -45,7 +45,7 @@ export class FootbarPage extends BasePage {
async verifyLockMode() { async verifyLockMode() {
// add record button // add record button
await expect(this.btn_addNewRow).toBeVisible({ visible: false }); await expect(this.btn_addNewRow).toBeVisible({ visible: true });
} }
async verifyCollaborativeMode() { async verifyCollaborativeMode() {

3
tests/playwright/pages/Dashboard/common/Toolbar/ViewMenu.ts

@ -86,6 +86,9 @@ export class ToolbarViewMenuPage extends BasePage {
// todo: Move verification out of the click method // todo: Move verification out of the click method
async click({ menu, subMenu, verificationInfo }: { menu: string; subMenu?: string; verificationInfo?: any }) { async click({ menu, subMenu, verificationInfo }: { menu: string; subMenu?: string; verificationInfo?: any }) {
await this.viewsMenuBtn.click(); await this.viewsMenuBtn.click();
await this.rootPage.waitForTimeout(1000);
await this.get().locator(`.ant-dropdown-menu-title-content:has-text("${menu}")`).first().click(); await this.get().locator(`.ant-dropdown-menu-title-content:has-text("${menu}")`).first().click();
if (subMenu) { if (subMenu) {
// for CSV download, pass locator instead of clicking it here // for CSV download, pass locator instead of clicking it here

4
tests/playwright/tests/db/general/viewMenu.spec.ts

@ -23,7 +23,7 @@ test.describe('Grid view locked', () => {
// enable view lock // enable view lock
await dashboard.grid.toolbar.viewsMenu.click({ await dashboard.grid.toolbar.viewsMenu.click({
menu: 'Collaborative', menu: 'View Mode',
subMenu: 'Locked', subMenu: 'Locked',
}); });
@ -32,7 +32,7 @@ test.describe('Grid view locked', () => {
// enable collaborative view // enable collaborative view
await dashboard.grid.toolbar.viewsMenu.click({ await dashboard.grid.toolbar.viewsMenu.click({
menu: 'Locked', menu: 'View Mode',
subMenu: 'Collaborative', subMenu: 'Collaborative',
}); });

Loading…
Cancel
Save