mirror of https://github.com/nocodb/nocodb
239 changed files with 6238 additions and 2505 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,140 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import type { ColumnType } from 'nocodb-sdk' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { useVModel } from '#imports' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
modelValue: boolean |
||||||
|
column: ColumnType |
||||||
|
extra: any |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const { api } = useApi() |
||||||
|
|
||||||
|
const dialogShow = useVModel(props, 'modelValue', emit) |
||||||
|
|
||||||
|
const { $e, $poller } = useNuxtApp() |
||||||
|
|
||||||
|
const basesStore = useBases() |
||||||
|
|
||||||
|
const { createProject: _createProject } = basesStore |
||||||
|
|
||||||
|
const { activeTable: _activeTable } = storeToRefs(useTablesStore()) |
||||||
|
|
||||||
|
const reloadDataHook = inject(ReloadViewDataHookInj) |
||||||
|
|
||||||
|
const { eventBus } = useSmartsheetStoreOrThrow() |
||||||
|
|
||||||
|
const { getMeta } = useMetas() |
||||||
|
|
||||||
|
const meta = inject(MetaInj, ref()) |
||||||
|
|
||||||
|
const options = ref({ |
||||||
|
includeData: true, |
||||||
|
}) |
||||||
|
|
||||||
|
const optionsToExclude = computed(() => { |
||||||
|
const { includeData } = options.value |
||||||
|
return { |
||||||
|
excludeData: !includeData, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const isLoading = ref(false) |
||||||
|
|
||||||
|
const reloadTable = async () => { |
||||||
|
await getMeta(meta!.value!.id!, true) |
||||||
|
eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD) |
||||||
|
reloadDataHook?.trigger() |
||||||
|
} |
||||||
|
|
||||||
|
const _duplicate = async () => { |
||||||
|
try { |
||||||
|
isLoading.value = true |
||||||
|
const jobData = await api.dbTable.duplicateColumn(props.column.base_id!, props.column.id!, { |
||||||
|
options: optionsToExclude.value, |
||||||
|
extra: props.extra, |
||||||
|
}) |
||||||
|
|
||||||
|
$poller.subscribe( |
||||||
|
{ id: jobData.id }, |
||||||
|
async (data: { |
||||||
|
id: string |
||||||
|
status?: string |
||||||
|
data?: { |
||||||
|
error?: { |
||||||
|
message: string |
||||||
|
} |
||||||
|
message?: string |
||||||
|
result?: any |
||||||
|
} |
||||||
|
}) => { |
||||||
|
if (data.status !== 'close') { |
||||||
|
if (data.status === JobStatus.COMPLETED) { |
||||||
|
reloadTable() |
||||||
|
isLoading.value = false |
||||||
|
dialogShow.value = false |
||||||
|
} else if (data.status === JobStatus.FAILED) { |
||||||
|
message.error(`There was an error duplicating the column.`) |
||||||
|
reloadTable() |
||||||
|
isLoading.value = false |
||||||
|
dialogShow.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
) |
||||||
|
|
||||||
|
$e('a:column:duplicate') |
||||||
|
} catch (e: any) { |
||||||
|
message.error(await extractSdkResponseErrorMsg(e)) |
||||||
|
isLoading.value = false |
||||||
|
dialogShow.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onKeyStroke('Enter', () => { |
||||||
|
// should only trigger this when our modal is open |
||||||
|
if (dialogShow.value) { |
||||||
|
_duplicate() |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const isEaster = ref(false) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<GeneralModal |
||||||
|
v-model:visible="dialogShow" |
||||||
|
:class="{ active: dialogShow }" |
||||||
|
:closable="!isLoading" |
||||||
|
:mask-closable="!isLoading" |
||||||
|
:keyboard="!isLoading" |
||||||
|
centered |
||||||
|
wrap-class-name="nc-modal-column-duplicate" |
||||||
|
:footer="null" |
||||||
|
class="!w-[30rem]" |
||||||
|
@keydown.esc="dialogShow = false" |
||||||
|
> |
||||||
|
<div> |
||||||
|
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster"> |
||||||
|
{{ $t('general.duplicate') }} {{ $t('objects.column') }} |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="mt-4">{{ $t('msg.warning.duplicateProject') }}</div> |
||||||
|
|
||||||
|
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div> |
||||||
|
|
||||||
|
<a-divider class="!m-0 !p-0 !my-2" /> |
||||||
|
|
||||||
|
<div class="text-xs p-2"> |
||||||
|
<a-checkbox v-model:checked="options.includeData" :disabled="isLoading">{{ $t('labels.includeData') }}</a-checkbox> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end"> |
||||||
|
<NcButton v-if="!isLoading" key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton> |
||||||
|
<NcButton key="submit" type="primary" :loading="isLoading" @click="_duplicate">{{ $t('general.confirm') }} </NcButton> |
||||||
|
</div> |
||||||
|
</GeneralModal> |
||||||
|
</template> |
@ -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> |
@ -0,0 +1,69 @@ |
|||||||
|
function copyStylesInline(destinationNode: any, sourceNode: any) { |
||||||
|
const containerElements = ['svg', 'g'] |
||||||
|
for (let cd = 0; cd < destinationNode.childNodes.length; cd++) { |
||||||
|
const child = destinationNode.childNodes[cd] |
||||||
|
if (containerElements.includes(child.tagName)) { |
||||||
|
copyStylesInline(child, sourceNode.childNodes[cd]) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
const style = sourceNode.childNodes[cd].currentStyle || window.getComputedStyle(sourceNode.childNodes[cd]) |
||||||
|
if (style === 'undefined' || style == null) continue |
||||||
|
for (let st = 0; st < style.length; st++) { |
||||||
|
child.style.setProperty(style[st], style.getPropertyValue(style[st])) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function triggerDownload(imgURI: string, fileName: string) { |
||||||
|
const evt = new MouseEvent('click', { |
||||||
|
view: window, |
||||||
|
bubbles: false, |
||||||
|
cancelable: true, |
||||||
|
}) |
||||||
|
const a = document.createElement('a') |
||||||
|
a.setAttribute('download', fileName) |
||||||
|
a.setAttribute('href', imgURI) |
||||||
|
a.setAttribute('target', '_blank') |
||||||
|
a.setAttribute('rel', 'noopener,noreferrer') |
||||||
|
a.dispatchEvent(evt) |
||||||
|
} |
||||||
|
|
||||||
|
function downloadSvg(svg: SVGGraphicsElement, fileName: string) { |
||||||
|
const copy = svg.cloneNode(true) |
||||||
|
|
||||||
|
copyStylesInline(copy, svg) |
||||||
|
|
||||||
|
const canvas = document.createElement('canvas') |
||||||
|
const bbox = svg.getBBox() |
||||||
|
canvas.width = bbox.width |
||||||
|
canvas.height = bbox.height |
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')! |
||||||
|
ctx.clearRect(0, 0, bbox.width, bbox.height) |
||||||
|
|
||||||
|
const data = new XMLSerializer().serializeToString(copy) |
||||||
|
const DOMURL = window.URL || window.webkitURL || window |
||||||
|
const img = new Image() |
||||||
|
const svgBlob = new Blob([data], { type: 'image/svg+xml;charset=utf-8' }) |
||||||
|
const url = DOMURL.createObjectURL(svgBlob) |
||||||
|
|
||||||
|
img.onload = function () { |
||||||
|
ctx.drawImage(img, 0, 0) |
||||||
|
DOMURL.revokeObjectURL(url) |
||||||
|
if (typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob) { |
||||||
|
const blob = canvas.msToBlob() |
||||||
|
navigator.msSaveOrOpenBlob(blob, fileName) |
||||||
|
} else { |
||||||
|
const imgURI = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream') |
||||||
|
triggerDownload(imgURI, fileName) |
||||||
|
} |
||||||
|
console.log(canvas) |
||||||
|
|
||||||
|
// TODO: Somehow canvas dom element is getting deleted
|
||||||
|
// document.removeChild(canvas)
|
||||||
|
} |
||||||
|
img.src = url |
||||||
|
} |
||||||
|
|
||||||
|
export { downloadSvg } |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue