mirror of https://github.com/nocodb/nocodb
աɨռɢӄաօռɢ
1 year ago
24 changed files with 684 additions and 154 deletions
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 280 B |
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 735 B |
Before Width: | Height: | Size: 458 B After Width: | Height: | Size: 473 B |
@ -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> |
@ -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> |
Loading…
Reference in new issue