Browse Source

Merge branch 'develop' into fix/runtime-directive-warnings

pull/6954/head
աɨռɢӄաօռɢ 12 months ago
parent
commit
cbd11e9219
  1. 2
      packages/nc-gui/assets/nc-icons/check.svg
  2. 4
      packages/nc-gui/assets/nc-icons/lock.svg
  3. 6
      packages/nc-gui/assets/nc-icons/sort.svg
  4. 5
      packages/nc-gui/assets/style.scss
  5. 1
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  6. 79
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  7. 4
      packages/nc-gui/components/dlg/ViewCreate.vue
  8. 7
      packages/nc-gui/components/smartsheet/Toolbar.vue
  9. 2
      packages/nc-gui/components/smartsheet/grid/Table.vue
  10. 46
      packages/nc-gui/components/smartsheet/toolbar/ExportSubActions.vue
  11. 34
      packages/nc-gui/components/smartsheet/toolbar/LockType.vue
  12. 207
      packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue
  13. 2
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  14. 317
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  15. 31
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  16. 22
      packages/nc-gui/composables/useSmartsheetStore.ts
  17. 23
      packages/nc-gui/lang/en.json
  18. 10
      packages/nc-gui/store/views.ts
  19. 4
      tests/playwright/pages/Dashboard/common/Toolbar/Actions/index.ts
  20. 10
      tests/playwright/pages/Dashboard/common/Toolbar/ViewMenu.ts
  21. 6
      tests/playwright/pages/Dashboard/common/Toolbar/index.ts
  22. 2
      tests/playwright/tests/db/columns/columnAttachments.spec.ts
  23. 12
      tests/playwright/tests/db/general/viewMenu.spec.ts
  24. 2
      tests/playwright/tests/db/views/viewGridShare.spec.ts

2
packages/nc-gui/assets/nc-icons/check.svg

@ -1,5 +1,5 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="check">
<path id="Vector" d="M13.3333 4.5L5.99996 11.8333L2.66663 8.5" stroke="#40444D" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector" d="M13.3333 4.5L5.99996 11.8333L2.66663 8.5" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 275 B

After

Width:  |  Height:  |  Size: 280 B

4
packages/nc-gui/assets/nc-icons/lock.svg

@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 7.33337H3.33333C2.59695 7.33337 2 7.93033 2 8.66671V13.3334C2 14.0698 2.59695 14.6667 3.33333 14.6667H12.6667C13.403 14.6667 14 14.0698 14 13.3334V8.66671C14 7.93033 13.403 7.33337 12.6667 7.33337Z" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.66667 7.33337V4.66671C4.66667 3.78265 5.01786 2.93481 5.64298 2.30968C6.2681 1.68456 7.11595 1.33337 8 1.33337C8.88406 1.33337 9.73191 1.68456 10.357 2.30968C10.9821 2.93481 11.3333 3.78265 11.3333 4.66671V7.33337" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.6667 7.33337H3.33333C2.59695 7.33337 2 7.93033 2 8.66671V13.3334C2 14.0698 2.59695 14.6667 3.33333 14.6667H12.6667C13.403 14.6667 14 14.0698 14 13.3334V8.66671C14 7.93033 13.403 7.33337 12.6667 7.33337Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.66667 7.33337V4.66671C4.66667 3.78265 5.01786 2.93481 5.64298 2.30968C6.2681 1.68456 7.11595 1.33337 8 1.33337C8.88406 1.33337 9.73191 1.68456 10.357 2.30968C10.9821 2.93481 11.3333 3.78265 11.3333 4.66671V7.33337" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 735 B

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

@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 13.3334V10.6667" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13.3334V6.66669" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 13.3334V2.66669" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 13.3334V10.6667" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13.3334V6.66669" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 13.3334V2.66669" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 473 B

5
packages/nc-gui/assets/style.scss

@ -236,7 +236,7 @@ a {
}
.nc-base-menu-item {
@apply cursor-pointer flex items-center gap-2 py-2 after:(content-[''] absolute top-0 left-0 bottom-0 right-0 w-full h-full bg-current opacity-0 transition transition-opacity duration-100) hover:(after:(opacity-5));
@apply cursor-pointer flex items-center gap-2 py-2;
// &:hover {
// .nc-icon {
@ -479,6 +479,9 @@ a {
.nc-toolbar-btn {
@apply !shadow-none rounded hover:(ring-1 ring-gray-200 ring-opacity-100 bg-gray-100 !text-gray-800) focus:(ring-1 ring-gray-300 ring-opacity-100 !text-gray-800 bg-gray-100) text-gray-600 text-xs font-medium px-2 border-0;
}
.nc-toolbar-btn[disabled] {
@apply !text-gray-400 !cursor-not-allowed !hover:ring-0;
}
.nc-warning-info {
@apply !shadow-none rounded ring-1 ring-red-600;

1
packages/nc-gui/components/dashboard/TreeView/ViewsList.vue

@ -420,6 +420,7 @@ function onOpenModal({
:key="view.id"
:view="view"
:on-validate="validate"
:table="table"
class="nc-view-item !rounded-md !px-0.75 !py-0.5 w-full transition-all ease-in duration-100"
:class="{
'bg-gray-200': isMarked === view.id,

79
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import type { KanbanType, ViewType, ViewTypes } from 'nocodb-sdk'
import type { TableType, ViewType, ViewTypes } from 'nocodb-sdk'
import type { WritableComputedRef } from '@vue/reactivity'
import {
IsLockedInj,
@ -16,6 +16,7 @@ import {
interface Props {
view: ViewType
table: TableType
onValidate: (view: ViewType) => boolean | string
}
@ -47,7 +48,15 @@ const { isUIAllowed } = useRoles()
const base = inject(ProjectInj, ref())
const activeView = inject(ActiveViewInj, ref())
const { activeView } = storeToRefs(useViewsStore())
const { getMeta } = useMetas()
const table = computed(() => props.table)
const injectedTable = ref(table.value)
provide(ActiveViewInj, vModel)
provide(MetaInj, injectedTable)
const isLocked = inject(IsLockedInj, ref(false))
@ -121,28 +130,6 @@ onKeyStroke('Enter', (event) => {
const focusInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
/** Duplicate a view */
// todo: This is not really a duplication, maybe we need to implement a true duplication?
function onDuplicate() {
isDropdownOpen.value = false
emits('openModal', {
type: vModel.value.type!,
title: vModel.value.title,
copyViewId: vModel.value.id,
groupingFieldColumnId: (vModel.value.view as KanbanType).fk_grp_col_id!,
})
$e('c:view:copy', { view: vModel.value.type })
}
/** Delete a view */
async function onDelete() {
isDropdownOpen.value = false
emits('delete', vModel.value)
}
/** Rename a view */
async function onRename() {
isDropdownOpen.value = false
@ -189,6 +176,18 @@ function onStopEdit() {
isStopped.value = false
}, 250)
}
const onDelete = () => {
isDropdownOpen.value = false
emits('delete', vModel.value)
}
watch(isDropdownOpen, async () => {
if (!isDropdownOpen.value) return
injectedTable.value = (await getMeta(table.value.id!)) as any
})
</script>
<template>
@ -262,29 +261,15 @@ function onStopEdit() {
</NcButton>
<template #overlay>
<NcMenu class="min-w-27" :data-testid="`view-sidebar-view-actions-${vModel.alias || vModel.title}`">
<NcMenuItem @click.stop="onDblClick">
<div v-e="['c:view:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="edit" />
<div class="-ml-0.25">{{ $t('general.rename') }}</div>
</div>
</NcMenuItem>
<NcMenuItem @click.stop="onDuplicate">
<div v-e="['c:view:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="nc-view-copy-icon" />
{{ $t('general.duplicate') }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem v-if="!vModel.is_default" class="!text-red-500 !hover:bg-red-50" @click.stop="onDelete">
<div v-e="['c:view:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" class="text-sm nc-view-delete-icon" />
<div class="-ml-0.25">{{ $t('general.delete') }}</div>
</div>
</NcMenuItem>
</NcMenu>
<SmartsheetToolbarViewActionMenu
:data-testid="`view-sidebar-view-actions-${vModel.alias || vModel.title}`"
:view="vModel"
:table="table"
in-sidebar
@close-modal="isDropdownOpen = false"
@rename="onRename"
@delete="onDelete"
/>
</template>
</NcDropdown>
</template>

4
packages/nc-gui/components/dlg/ViewCreate.vue

@ -308,7 +308,7 @@ onMounted(async () => {
<NcSelect
v-model:value="form.fk_grp_col_id"
class="w-full nc-kanban-grouping-field-select"
:disabled="groupingFieldColumnId || isMetaLoading"
:disabled="isMetaLoading"
:loading="isMetaLoading"
:options="viewSelectFieldOptions"
:placeholder="$t('placeholder.selectGroupField')"
@ -325,7 +325,7 @@ onMounted(async () => {
v-model:value="form.fk_geo_data_col_id"
class="w-full"
:options="viewSelectFieldOptions"
:disabled="groupingFieldColumnId || isMetaLoading"
:disabled="isMetaLoading"
:loading="isMetaLoading"
:placeholder="$t('placeholder.selectGeoField')"
:not-found-content="$t('placeholder.selectGeoFieldNotFound')"

7
packages/nc-gui/components/smartsheet/Toolbar.vue

@ -51,13 +51,6 @@ const { allowCSVDownload } = useSharedView()
'w-full': isMobileMode,
}"
/>
<template v-if="!isMobileMode">
<LazySmartsheetToolbarViewActions
v-if="(isGrid || isGallery || isKanban || isMap) && !isPublic && isUIAllowed('dataInsert')"
:show-system-fields="false"
/>
</template>
</template>
</div>
</template>

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

@ -385,7 +385,7 @@ const gridWrapperClass = computed<string>(() => {
const classes = []
if (headerOnly !== true) {
if (!scrollParent.value) {
classes.push('nc-scrollbar-x-lg overflow-auto')
classes.push('nc-scrollbar-x-lg !overflow-auto')
}
} else {
classes.push('overflow-visible')

46
packages/nc-gui/components/smartsheet/toolbar/ExportSubActions.vue

@ -13,13 +13,9 @@ import {
ref,
storeToRefs,
useBase,
useI18n,
useNuxtApp,
useSmartsheetStoreOrThrow,
} from '#imports'
const { t } = useI18n()
const isPublicView = inject(IsPublicInj, ref(false))
const fields = inject(FieldsInj, ref([]))
@ -33,13 +29,17 @@ const meta = inject(MetaInj, ref())
const selectedView = inject(ActiveViewInj)
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
const { activeNestedFilters: nestedFilters, activeSorts: sorts } = storeToRefs(useViewsStore())
const isExportingType = ref<ExportTypes | undefined>(undefined)
const exportFile = async (exportType: ExportTypes) => {
let offset = 0
let c = 1
const responseType = exportType === ExportTypes.EXCEL ? 'base64' : 'blob'
isExportingType.value = exportType
const XLSX = await import('xlsx')
const FileSaver = await import('file-saver')
@ -84,13 +84,10 @@ const exportFile = async (exportType: ExportTypes) => {
}
offset = +headers['nc-export-offset']
if (offset > -1) {
// Downloading more files
message.info(t('msg.info.downloadingMoreFiles'))
} else {
// Successfully exported all table data
message.success(t('msg.success.tableDataExported'))
}
setTimeout(() => {
isExportingType.value = undefined
}, 200)
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -99,17 +96,30 @@ const exportFile = async (exportType: ExportTypes) => {
</script>
<template>
<a-menu-item>
<div v-e="['a:download:csv']" class="nc-base-menu-item" @click="exportFile(ExportTypes.CSV)">
<component :is="iconMap.csv" />
<div class="flex py-3 px-4 font-bold uppercase text-xs text-gray-500">{{ $t('labels.downloadData') }}</div>
<a-menu-item class="!mx-1 !py-2 !rounded-md">
<div
v-e="['a:download:csv']"
class="flex flex-row items-center nc-base-menu-item !py-0"
@click.stop="exportFile(ExportTypes.CSV)"
>
<GeneralLoader v-if="isExportingType === ExportTypes.CSV" class="!max-h-4.5 !-mt-1 !mr-0.7" />
<component :is="iconMap.csv" v-else />
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</div>
</a-menu-item>
<a-menu-item>
<div v-e="['a:download:excel']" class="nc-base-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<component :is="iconMap.excel" />
<a-menu-item class="!mx-1 !py-2 !rounded-md">
<div
v-e="['a:download:excel']"
class="flex flex-row items-center nc-base-menu-item !py-0"
@click="exportFile(ExportTypes.EXCEL)"
>
<GeneralLoader v-if="isExportingType === ExportTypes.EXCEL" class="!max-h-4.5 !-mt-1 !mr-0.7" />
<component :is="iconMap.excel" v-else />
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}
</div>

34
packages/nc-gui/components/smartsheet/toolbar/LockType.vue

@ -9,17 +9,17 @@ const emit = defineEmits(['select'])
const types = {
[LockType.Personal]: {
title: 'title.personalView',
title: 'title.personal',
icon: iconMap.account,
subtitle: 'msg.info.personalView',
},
[LockType.Collaborative]: {
title: 'title.collabView',
title: 'title.collaborative',
icon: UsersIcon,
subtitle: 'msg.info.collabView',
},
[LockType.Locked]: {
title: 'title.lockedView',
title: 'title.locked',
icon: LockIcon,
subtitle: 'msg.info.lockedView',
},
@ -29,29 +29,31 @@ const selectedView = inject(ActiveViewInj)
</script>
<template>
<div class="nc-locked-menu-item min-w-50" @click="emit('select', type)">
<div class="nc-locked-menu-item !px-1 text-gray-800" @click="emit('select', type)">
<div :class="{ 'show-tick': !hideTick }">
<div class="flex items-center gap-2 flex-grow">
<component :is="types[type].icon" class="text-gray-800 !w-4 !h-4" />
<div class="flex flex-col">
{{ $t(types[type].title) }}
<div v-if="!hideTick" class="nc-subtitle max-w-120 text-sm text-gray-500 whitespace-normal">
{{ $t(types[type].subtitle) }}
<div class="flex flex-col gap-y-1">
<div class="flex items-center gap-2 flex-grow">
<component :is="types[type].icon" class="!w-4 !min-w-4 text-inherit !h-4" />
<div class="flex">
{{ $t(types[type].title) }}
</div>
<div v-if="!hideTick" class="flex flex-grow"></div>
<template v-if="!hideTick">
<GeneralIcon v-if="selectedView?.lock_type === type" icon="check" class="!text-brand-500" />
<span v-else />
</template>
</div>
<div v-if="!hideTick" class="nc-subtitle max-w-120 text-sm text-gray-500 whitespace-normal ml-6">
{{ $t(types[type].subtitle) }}
</div>
</div>
<template v-if="!hideTick">
<GeneralIcon v-if="selectedView?.lock_type === type" icon="check" />
<span v-else />
</template>
</div>
</div>
</template>
<style scoped lang="scss">
.nc-locked-menu-item > div {
@apply py-2 items-center;
@apply !py-0 items-center;
&.show-tick {
@apply flex gap-2;

207
packages/nc-gui/components/smartsheet/toolbar/OpenedViewAction.vue

@ -0,0 +1,207 @@
<script lang="ts" setup>
const { activeTable } = storeToRefs(useTablesStore())
const { isMobileMode } = useGlobal()
const { isSharedBase, base } = storeToRefs(useBase())
const { t } = useI18n()
const { $api } = useNuxtApp()
const { refreshCommandPalette } = useCommandPalette()
const { activeView, views, openedViewsTab, viewsByTable } = storeToRefs(useViewsStore())
const { loadViews, removeFromRecentViews } = useViewsStore()
const { navigateToTable } = useTablesStore()
const isDropdownOpen = ref(false)
const isViewIdCopied = ref(false)
const isRenaming = ref(false)
const renameInputDom = ref()
const viewRenameTitle = ref('')
const error = ref<string | undefined>()
const onRenameMenuClick = () => {
isRenaming.value = true
isDropdownOpen.value = false
viewRenameTitle.value = activeView.value!.title
setTimeout(() => {
renameInputDom.value.focus()
})
}
watch(renameInputDom, () => {
renameInputDom.value?.focus()
})
const onRenameBlur = async () => {
if (validate()) {
activeView.value!.title = viewRenameTitle.value
isRenaming.value = false
error.value = undefined
await $api.dbView.update(activeView.value!.id!, {
title: viewRenameTitle.value,
})
} else {
renameInputDom.value?.focus()
}
}
/** validate view title */
function validate() {
if (!viewRenameTitle.value || viewRenameTitle.value.trim().length < 0) {
error.value = t('msg.error.viewNameRequired')
return false
}
if (viewRenameTitle.value.trim().length > 255) {
error.value = t('msg.error.nameMaxLength256')
return false
}
if (views.value.some((v) => v.title === viewRenameTitle.value && v.id !== activeView.value!.id)) {
error.value = t('msg.error.viewNameDuplicate')
return false
}
return true
}
watch(viewRenameTitle, () => {
if (error.value) {
error.value = undefined
}
})
watch(isDropdownOpen, () => {
setTimeout(() => {
isViewIdCopied.value = false
}, 250)
})
const resetViewRename = () => {
viewRenameTitle.value = activeView.value!.title
isRenaming.value = false
}
function openDeleteDialog() {
const isOpen = ref(true)
isDropdownOpen.value = false
const { close } = useDialog(resolveComponent('DlgViewDelete'), {
'modelValue': isOpen,
'view': activeView.value,
'onUpdate:modelValue': closeDialog,
'onDeleted': async () => {
closeDialog()
removeFromRecentViews({ viewId: activeView.value!.id, tableId: activeView.value!.fk_model_id, baseId: base.value.id })
refreshCommandPalette()
if (activeView.value?.id === activeView.value!.id) {
navigateToTable({
tableId: activeTable.value!.id!,
baseId: base.value.id!,
})
}
await loadViews({
tableId: activeTable.value!.id!,
force: true,
})
const activeNonDefaultViews = viewsByTable.value.get(activeTable!.value!.id!)?.filter((v) => !v.is_default) ?? []
activeTable!.value!.meta = {
...(activeTable!.value!.meta as object),
hasNonDefaultViews: activeNonDefaultViews.length > 1,
}
},
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
</script>
<template>
<div
v-if="isRenaming"
class="h-6 relative"
:class="{
'max-w-2/5': !isSharedBase && !isMobileMode && activeView?.is_default,
'max-w-3/5': !isSharedBase && !isMobileMode && !activeView?.is_default,
}"
>
<input
ref="renameInputDom"
v-model="viewRenameTitle"
class="ml-0.25 w-full px-1 py-0.5 rounded-md font-medium text-gray-800"
:class="{
'outline-brand-500': !error,
'outline-red-500 pr-6': error,
}"
@blur="onRenameBlur"
@keydown.enter="onRenameBlur"
@keydown.esc="resetViewRename"
/>
<NcTooltip v-if="error" class="absolute top-0.25 right-0.5 bg-white rounded-lg">
<template #title>
{{ error }}
</template>
<GeneralIcon icon="info" class="cursor-pointer" />
</NcTooltip>
</div>
<NcDropdown
v-else
v-model:visible="isDropdownOpen"
v-e="['c:breadcrumb:view-actions']"
class="!xs:pointer-events-none nc-actions-menu-btn nc-view-context-btn"
overlay-class-name="nc-dropdown-actions-menu"
>
<div
class="truncate nc-active-view-title !hover:(bg-gray-100 text-gray-800) ml-0.25 pl-1 pr-0.25 rounded-md py-1 cursor-pointer"
:class="{
'max-w-2/5': !isSharedBase && !isMobileMode && activeView?.is_default,
'max-w-3/5': !isSharedBase && !isMobileMode && !activeView?.is_default,
'max-w-1/2': isMobileMode,
'text-gray-500': activeView?.is_default,
'text-gray-800 font-medium': !activeView?.is_default,
}"
>
<span
class="truncate xs:pl-1.25 text-inherit"
:class="{
'max-w-28/100': !isMobileMode,
}"
>
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span>
<GeneralIcon icon="arrowDown" class="ml-1" />
</div>
<template #overlay>
<SmartsheetToolbarViewActionMenu
:table="activeTable"
:view="activeView"
@close-modal="isDropdownOpen = false"
@rename="onRenameMenuClick"
@delete="openDeleteDialog"
/>
</template>
</NcDropdown>
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view' && !isMobileMode && !isRenaming" />
</template>

2
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -113,7 +113,7 @@ onMounted(() => {
<div :class="{ 'nc-active-btn': sorts?.length }">
<a-button v-e="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2">
<component :is="iconMap.sort" class="h-4 w-4" />
<component :is="iconMap.sort" class="h-4 w-4 text-inherit" />
<!-- Sort -->
<span v-if="!isMobileMode" class="text-capitalize !text-sm font-medium">{{ $t('activity.sort') }}</span>

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

@ -0,0 +1,317 @@
<script lang="ts" setup>
import type { TableType, ViewType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
import { LockType } from '~/lib'
import UploadIcon from '~icons/nc-icons/upload'
import DownloadIcon from '~icons/nc-icons/download'
const props = defineProps<{
view: ViewType
table: TableType
inSidebar: boolean
}>()
const emits = defineEmits(['rename', 'closeModal', 'delete'])
const { isUIAllowed } = useRoles()
const isPublicView = inject(IsPublicInj, ref(false))
const isLocked = inject(IsLockedInj, ref(false))
const { $api, $e } = useNuxtApp()
const { t } = useI18n()
const view = computed(() => props.view)
const table = computed(() => props.table)
const { viewsByTable } = storeToRefs(useViewsStore())
const { loadViews, navigateToView } = useViewsStore()
const { base } = storeToRefs(useBase())
const { refreshCommandPalette } = useCommandPalette()
const lockType = computed(() => (view.value?.lock_type as LockType) || LockType.Collaborative)
const views = computed(() => viewsByTable.value.get(table.value.id!))
const isViewIdCopied = ref(false)
const currentBaseId = computed(() => table.value?.source_id)
const onRenameMenuClick = () => {
emits('rename')
}
const quickImportDialogTypes: QuickImportDialogType[] = ['csv', 'excel']
const quickImportDialogs: Record<(typeof quickImportDialogTypes)[number], Ref<boolean>> = quickImportDialogTypes.reduce(
(acc: any, curr) => {
acc[curr] = ref(false)
return acc
},
{},
) as Record<QuickImportDialogType, Ref<boolean>>
const onImportClick = (dialog: any) => {
if (isLocked.value) return
emits('closeModal')
dialog.value = true
}
async function changeLockType(type: LockType) {
$e('a:grid:lockmenu', { lockType: type, sidebar: props.inSidebar })
if (!view.value) return
if (type === 'personal') {
// Coming soon
return message.info(t('msg.toast.futureRelease'))
}
try {
view.value.lock_type = type
await $api.dbView.update(view.value.id as string, {
lock_type: type,
})
message.success(`Successfully Switched to ${type} view`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
emits('closeModal')
}
/** Duplicate a view */
// todo: This is not really a duplication, maybe we need to implement a true duplication?
function onDuplicate() {
emits('closeModal')
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgViewCreate'), {
'modelValue': isOpen,
'title': view.value!.title,
'type': view.value!.type as ViewTypes,
'tableId': table.value!.id,
'selectedViewId': view.value!.id,
'groupingFieldColumnId': view.value!.view!.fk_grp_col_id,
'views': views,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
refreshCommandPalette()
await loadViews({
force: true,
tableId: table.value!.id!,
})
navigateToView({
view,
tableId: table.value!.id!,
baseId: base.value.id!,
hardReload: view.type === ViewTypes.FORM,
})
$e('a:view:create', { view: view.type, sidebar: props.inSidebar })
},
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
const { copy } = useCopy()
const onViewIdCopy = async () => {
await copy(view.value!.id!)
isViewIdCopied.value = true
}
const onDelete = async () => {
emits('delete')
}
</script>
<template>
<NcMenu class="!min-w-70" data-id="toolbar-actions" :data-testid="`view-sidebar-view-actions-${view!.alias || view!.title}`">
<NcTooltip>
<template #title> {{ $t('labels.clickToCopyViewID') }} </template>
<div class="flex items-center justify-between py-2 px-3 cursor-pointer hover:bg-gray-100 group" @click="onViewIdCopy">
<div class="flex text-xs font-bold text-gray-500 ml-1">
{{
$t('labels.viewIdColon', {
viewId: view?.id,
})
}}
</div>
<NcButton size="xsmall" type="secondary" class="!group-hover:bg-gray-100">
<GeneralIcon v-if="isViewIdCopied" icon="check" class="max-h-4 min-w-4" />
<GeneralIcon v-else else icon="copy" class="max-h-4 min-w-4" />
</NcButton>
</div>
</NcTooltip>
<NcDivider />
<template v-if="!view?.is_default">
<NcMenuItem @click="onRenameMenuClick">
<GeneralIcon icon="edit" />
{{ $t('activity.renameView') }}
</NcMenuItem>
<NcMenuItem @click="onDuplicate">
<GeneralIcon icon="duplicate" class="nc-view-copy-icon" />
{{ $t('labels.duplicateView') }}
</NcMenuItem>
<NcDivider />
</template>
<template v-if="view.type !== ViewTypes.FORM">
<template v-if="isUIAllowed('csvTableImport') && !isPublicView">
<NcSubMenu key="upload">
<template #title>
<div
v-e="[
'c:navdraw:preview-as',
{
sidebar: props.inSidebar,
},
]"
class="nc-base-menu-item group"
>
<UploadIcon class="w-4 h-4" />
{{ $t('general.upload') }}
</div>
</template>
<template #expandIcon></template>
<div class="flex py-3 px-4 font-bold uppercase text-xs text-gray-500">{{ $t('activity.uploadData') }}</div>
<template v-for="(dialog, type) in quickImportDialogs">
<NcMenuItem v-if="isUIAllowed(`${type}TableImport`) && !isPublicView" :key="type" @click="onImportClick(dialog)">
<div
v-e="[
`a:upload:${type}`,
{
sidebar: props.inSidebar,
},
]"
class="nc-base-menu-item"
:class="{ disabled: isLocked }"
>
<component :is="iconMap.upload" />
{{ `${$t('general.upload')} ${type.toUpperCase()}` }}
</div>
</NcMenuItem>
</template>
</NcSubMenu>
</template>
<NcSubMenu key="download">
<template #title>
<div
v-e="[
'c:download',
{
sidebar: props.inSidebar,
},
]"
class="nc-base-menu-item group nc-view-context-download-option"
>
<DownloadIcon class="w-4 h-4" />
{{ $t('general.download') }}
</div>
</template>
<template #expandIcon></template>
<LazySmartsheetToolbarExportSubActions />
</NcSubMenu>
<NcDivider />
</template>
<NcSubMenu v-if="isUIAllowed('viewCreateOrEdit')" key="lock-type" class="scrollbar-thin-dull max-h-90vh overflow-auto !py-0">
<template #title>
<div
v-e="[
'c:navdraw:preview-as',
{
sidebar: props.inSidebar,
},
]"
class="flex flex-row items-center gap-x-3"
>
<div>
{{ $t('labels.viewMode') }}
</div>
<div class="nc-base-menu-item flex !flex-shrink group !py-1 !px-1 rounded-md bg-brand-50">
<LazySmartsheetToolbarLockType
hide-tick
:type="lockType"
class="flex nc-view-actions-lock-type !text-brand-500 !flex-shrink"
/>
</div>
<div class="flex flex-grow"></div>
</div>
</template>
<template #expandIcon></template>
<div class="flex py-3 px-4 font-bold uppercase text-xs text-gray-500">{{ $t('labels.viewMode') }}</div>
<a-menu-item class="!mx-1 !py-2 !rounded-md nc-view-action-lock-subaction">
<LazySmartsheetToolbarLockType :type="LockType.Collaborative" @click="changeLockType(LockType.Collaborative)" />
</a-menu-item>
<a-menu-item class="!mx-1 !py-2 !rounded-md nc-view-action-lock-subaction">
<LazySmartsheetToolbarLockType :type="LockType.Locked" @click="changeLockType(LockType.Locked)" />
</a-menu-item>
</NcSubMenu>
<template v-if="!view.is_default">
<NcDivider />
<NcMenuItem class="!hover:bg-red-50 !text-red-500" @click="onDelete">
<GeneralIcon icon="delete" class="nc-view-delete-icon" />
{{
$t('general.deleteEntity', {
entity: $t('objects.view'),
})
}}
</NcMenuItem>
</template>
<template v-if="currentBaseId">
<LazyDlgQuickImport
v-for="tp in quickImportDialogTypes"
:key="tp"
v-model="quickImportDialogs[tp].value"
:import-type="tp"
:source-id="currentBaseId"
:import-data-only="true"
/>
</template>
</NcMenu>
</template>
<style lang="scss" scoped>
.nc-base-menu-item {
@apply !py-0;
}
.nc-view-actions-lock-type {
@apply !min-w-0;
}
</style>
<style lang="scss">
.nc-view-actions-lock-type > div {
@apply !py-0;
}
.nc-view-action-lock-subaction {
@apply !min-w-82;
}
</style>

31
packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
const { isMobileMode } = useGlobal()
const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
const { activeView } = storeToRefs(useViewsStore())
const { base, isSharedBase } = storeToRefs(useBase())
const { baseUrl } = useBase()
@ -68,7 +68,7 @@ const openedBaseUrl = computed(() => {
</div>
</NcTooltip>
</NuxtLink>
<div class="px-1.5 text-gray-500">/</div>
<div class="px-1.75 text-gray-500">/</div>
</template>
<template v-if="!(isMobileMode && !activeView?.is_default)">
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeTable?.meta?.icon" readonly size="xsmall">
@ -121,7 +121,7 @@ const openedBaseUrl = computed(() => {
</div>
</template>
<div v-if="!isMobileMode" class="px-1 text-gray-500">/</div>
<div v-if="!isMobileMode" class="pl-1.25 text-gray-500">/</div>
<template v-if="!(isMobileMode && activeView?.is_default)">
<LazyGeneralEmojiPicker v-if="isMobileMode" :emoji="activeView?.meta?.icon" readonly size="xsmall">
@ -130,30 +130,7 @@ const openedBaseUrl = computed(() => {
</template>
</LazyGeneralEmojiPicker>
<NcTooltip
class="truncate nc-active-view-title"
:class="{
'max-w-2/5': !isSharedBase && !isMobileMode && activeView?.is_default,
'max-w-3/5': !isSharedBase && !isMobileMode && !activeView?.is_default,
'max-w-1/2': isMobileMode,
}"
>
<template #title>
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</template>
<span
class="truncate xs:pl-1.25"
:class="{
'max-w-28/100': !isMobileMode,
'text-gray-500': activeView?.is_default,
'text-gray-800 font-medium': !activeView?.is_default,
}"
>
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span>
</NcTooltip>
<SmartsheetToolbarOpenedViewAction />
</template>
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view' && !isMobileMode" />
</div>
</template>

22
packages/nc-gui/composables/useSmartsheetStore.ts

@ -15,7 +15,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
) => {
const { $api } = useNuxtApp()
const { activeView: view } = storeToRefs(useViewsStore())
const { activeView: view, activeNestedFilters, activeSorts } = storeToRefs(useViewsStore())
const baseStore = useBase()
@ -57,6 +57,26 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
const sorts = ref<SortType[]>(unref(initialSorts) ?? [])
const nestedFilters = ref<FilterType[]>(unref(initialFilters) ?? [])
watch(
sorts,
() => {
activeSorts.value = sorts.value
},
{
immediate: true,
},
)
watch(
nestedFilters,
() => {
activeNestedFilters.value = nestedFilters.value
},
{
immediate: true,
},
)
return {
view,
meta,

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

@ -88,6 +88,7 @@
"action": "Action",
"insert": "Insert",
"delete": "Delete",
"deleteEntity": "Delete {entity}",
"bulkInsert": "Bulk Insert",
"bulkDelete": "Bulk Delete",
"bulkUpdate": "Bulk Update",
@ -343,9 +344,9 @@
"createBase": "Create Base",
"myProject": "My Bases",
"formTitle": "Form Title",
"collabView": "Collaborative View",
"lockedView": "Locked View",
"personalView": "Personal View",
"collaborative": "Collaborative",
"locked": "Locked",
"personal": "Personal",
"appStore": "App Store",
"teamAndAuth": "Team & Auth",
"rolesUserMgmt": "Roles & Users Management",
@ -410,9 +411,11 @@
}
},
"labels": {
"downloadData": "Download Data",
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached",
"viewIdColon": "VIEW ID: {viewId}",
"toAddress": "To Address",
"subject": "Subject",
"body": "Body",
@ -447,6 +450,8 @@
"clickToHide": "Click to hide",
"clickToDownload": "Click to download",
"forRole": "for role",
"clickToCopyViewID": "Click to copy View ID",
"viewMode": "View Mode",
"searchUsers": "Search Users",
"superAdmin": "Super Admin",
"allTables": "All Tables",
@ -743,8 +748,8 @@
"deleteSelectedRow": "Delete Selected Records",
"importExcel": "Import Excel",
"importCSV": "Import CSV",
"downloadCSV": "Download as CSV",
"downloadExcel": "Download as XLSX",
"downloadCSV": "Download CSV",
"downloadExcel": "Download Excel",
"uploadCSV": "Upload CSV",
"import": "Import",
"importMetadata": "Import Metadata",
@ -758,9 +763,10 @@
"fillByCodeScan": "Fill by scan",
"listSharedView": "Shared View List",
"ListView": "Views List",
"copyView": "Copy view",
"renameView": "Rename view",
"deleteView": "Delete view",
"copyView": "Copy View",
"renameView": "Rename View",
"uploadData": "Upload Data",
"deleteView": "Delete View",
"createGrid": "Create Grid View",
"createGallery": "Create Gallery View",
"createCalendar": "Create Calendar View",
@ -1186,6 +1192,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "Your search for {search} found no results",
"invalidChar": "Invalid character in folder path.",

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

@ -1,4 +1,4 @@
import type { ViewType, ViewTypes } from 'nocodb-sdk'
import type { FilterType, SortType, ViewType, ViewTypes } from 'nocodb-sdk'
import { acceptHMRUpdate, defineStore } from 'pinia'
import type { ViewPageType } from '~/lib'
@ -41,6 +41,12 @@ export const useViewsStore = defineStore('viewsStore', () => {
},
})
// Both are synced with `useSmartsheetStore` state
// Sort of active view
const activeSorts = ref<SortType[]>([])
// Filters of active view (used for local filters)
const activeNestedFilters = ref<FilterType[]>([])
const isViewsLoading = ref(true)
const isViewDataLoading = ref(true)
const isPublic = computed(() => route.value.meta?.public)
@ -304,6 +310,8 @@ export const useViewsStore = defineStore('viewsStore', () => {
navigateToView,
changeView,
removeFromRecentViews,
activeSorts,
activeNestedFilters,
}
})

4
tests/playwright/pages/Dashboard/common/Toolbar/Actions/index.ts

@ -13,7 +13,7 @@ export class ToolbarActionsPage extends BasePage {
}
get() {
return this.rootPage.locator(`[data-testid="toolbar-actions"]`);
return this.rootPage.locator(`[data-id="toolbar-actions"]`);
}
// todo: use enum
@ -22,6 +22,6 @@ export class ToolbarActionsPage extends BasePage {
}
async clickDownloadSubmenu(label: string) {
await this.rootPage.locator(`div[class="nc-base-menu-item"]:has-text("${label}")`).first().click();
await this.rootPage.locator(`div.nc-base-menu-item:has-text("${label}")`).first().click();
}
}

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

@ -12,7 +12,7 @@ export class ToolbarViewMenuPage extends BasePage {
constructor(toolbar: ToolbarPage) {
super(toolbar.rootPage);
this.toolbar = toolbar;
this.viewsMenuBtn = this.toolbar.get().locator(`.nc-actions-menu-btn`);
this.viewsMenuBtn = this.rootPage.locator('.nc-view-context-btn');
}
get() {
@ -89,12 +89,12 @@ export class ToolbarViewMenuPage extends BasePage {
await this.get().locator(`.ant-dropdown-menu-title-content:has-text("${menu}")`).first().click();
if (subMenu) {
// for CSV download, pass locator instead of clicking it here
if (subMenu === 'Download as CSV') {
if (subMenu === 'Download CSV') {
await this.verifyDownloadAsCSV({
downloadLocator: this.rootPage.locator(`.ant-dropdown-menu-title-content:has-text("${subMenu}")`).last(),
expectedDataFile: verificationInfo?.verificationFile ?? './fixtures/expectedBaseDownloadData.txt',
});
} else if (subMenu === 'Download as XLSX') {
} else if (subMenu === 'Download Excel') {
await this.verifyDownloadAsXLSX({
downloadLocator: this.rootPage.locator(`.ant-dropdown-menu-title-content:has-text("${subMenu}")`).last(),
expectedDataFile: verificationInfo?.verificationFile ?? './fixtures/expectedBaseDownloadData.txt',
@ -104,12 +104,12 @@ export class ToolbarViewMenuPage extends BasePage {
}
switch (subMenu) {
case 'Download as CSV':
case 'Download CSV':
await this.verifyToast({
message: 'Successfully exported all table data',
});
break;
case 'Download as XLSX':
case 'Download Excel':
await this.verifyToast({
message: 'Successfully exported all table data',
});

6
tests/playwright/pages/Dashboard/common/Toolbar/index.ts

@ -65,7 +65,7 @@ export class ToolbarPage extends BasePage {
async clickActions() {
const menuOpen = await this.actions.get().isVisible();
await this.get().locator(`button.nc-actions-menu-btn`).click();
await this.rootPage.locator(`div.nc-view-context-btn`).click();
// Wait for the menu to close
if (menuOpen) await this.fields.get().waitFor({ state: 'hidden' });
@ -206,8 +206,8 @@ export class ToolbarPage extends BasePage {
const menuItems = {
creator: ['Download', 'Upload'],
editor: ['Download', 'Upload'],
commenter: ['Download as CSV', 'Download as XLSX'],
viewer: ['Download as CSV', 'Download as XLSX'],
commenter: ['Download CSV', 'Download Excel'],
viewer: ['Download CSV', 'Download Excel'],
};
const vMenu = this.rootPage.locator('.nc-dropdown-actions-menu:visible');
for (const item of menuItems[param.role.toLowerCase()]) {

2
tests/playwright/tests/db/columns/columnAttachments.spec.ts

@ -97,7 +97,7 @@ test.describe('Attachment column', () => {
await dashboard.grid.toolbar.actions.click('Download');
const csvFileData: string = await dashboard.downloadAndGetFile({
downloadUIAction: dashboard.grid.toolbar.actions.clickDownloadSubmenu('Download as CSV'),
downloadUIAction: dashboard.grid.toolbar.actions.clickDownloadSubmenu('Download CSV'),
});
const csvArray = csvFileData.split('\r\n');
const columns = csvArray[0];

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

@ -23,8 +23,8 @@ test.describe('Grid view locked', () => {
// enable view lock
await dashboard.grid.toolbar.viewsMenu.click({
menu: 'Collaborative View',
subMenu: 'Locked View',
menu: 'Collaborative',
subMenu: 'Locked',
});
// verify view lock
@ -32,8 +32,8 @@ test.describe('Grid view locked', () => {
// enable collaborative view
await dashboard.grid.toolbar.viewsMenu.click({
menu: 'Locked View',
subMenu: 'Collaborative View',
menu: 'Locked',
subMenu: 'Collaborative',
});
await dashboard.grid.verifyCollaborativeMode();
@ -52,7 +52,7 @@ test.describe('Grid view locked', () => {
await dashboard.grid.toolbar.viewsMenu.click({
menu: 'Download',
subMenu: 'Download as CSV',
subMenu: 'Download CSV',
verificationInfo: {
verificationFile: isPg(context) ? './fixtures/expectedBaseDownloadDataPg.txt' : null,
},
@ -72,7 +72,7 @@ test.describe('Grid view locked', () => {
await dashboard.grid.toolbar.viewsMenu.click({
menu: 'Download',
subMenu: 'Download as XLSX',
subMenu: 'Download Excel',
verificationInfo: {
verificationFile: isPg(context) ? './fixtures/expectedBaseDownloadDataPg.txt' : null,
},

2
tests/playwright/tests/db/views/viewGridShare.spec.ts

@ -247,7 +247,7 @@ test.describe('Shared view', () => {
// verify download
await sharedPage.grid.toolbar.clickDownload(
'Download as CSV',
'Download CSV',
isSqlite(context) || isPg(context) ? 'expectedDataSqlite.txt' : 'expectedData.txt'
);
});

Loading…
Cancel
Save