|
|
@ -2,7 +2,19 @@ |
|
|
|
import type { FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk' |
|
|
|
import type { FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk' |
|
|
|
import { ViewTypes } from 'nocodb-sdk' |
|
|
|
import { ViewTypes } from 'nocodb-sdk' |
|
|
|
import { notification } from 'ant-design-vue' |
|
|
|
import { notification } from 'ant-design-vue' |
|
|
|
import { inject, onClickOutside, onKeyStroke, provide, ref, useApi, useDebounceFn, useTabs, useViews, watch } from '#imports' |
|
|
|
import { |
|
|
|
|
|
|
|
inject, |
|
|
|
|
|
|
|
onClickOutside, |
|
|
|
|
|
|
|
onKeyStroke, |
|
|
|
|
|
|
|
provide, |
|
|
|
|
|
|
|
ref, |
|
|
|
|
|
|
|
useApi, |
|
|
|
|
|
|
|
useDebounceFn, |
|
|
|
|
|
|
|
useNuxtApp, |
|
|
|
|
|
|
|
useTabs, |
|
|
|
|
|
|
|
useViews, |
|
|
|
|
|
|
|
watch, |
|
|
|
|
|
|
|
} from '#imports' |
|
|
|
import { ActiveViewInj, MetaInj, ViewListInj } from '~/context' |
|
|
|
import { ActiveViewInj, MetaInj, ViewListInj } from '~/context' |
|
|
|
import { extractSdkResponseErrorMsg, viewIcons } from '~/utils' |
|
|
|
import { extractSdkResponseErrorMsg, viewIcons } from '~/utils' |
|
|
|
import MdiPlusIcon from '~icons/mdi/plus' |
|
|
|
import MdiPlusIcon from '~icons/mdi/plus' |
|
|
@ -13,14 +25,17 @@ const meta = inject(MetaInj, ref()) |
|
|
|
|
|
|
|
|
|
|
|
const activeView = inject(ActiveViewInj, ref()) |
|
|
|
const activeView = inject(ActiveViewInj, ref()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { $e } = useNuxtApp() |
|
|
|
|
|
|
|
|
|
|
|
const { addTab } = useTabs() |
|
|
|
const { addTab } = useTabs() |
|
|
|
|
|
|
|
|
|
|
|
const { views } = useViews(meta) |
|
|
|
const { views, loadViews } = useViews(meta) |
|
|
|
|
|
|
|
|
|
|
|
const { api } = useApi() |
|
|
|
const { api } = useApi() |
|
|
|
|
|
|
|
|
|
|
|
provide(ViewListInj, views) |
|
|
|
provide(ViewListInj, views) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Watch current views and on change set the next active view */ |
|
|
|
watch( |
|
|
|
watch( |
|
|
|
views, |
|
|
|
views, |
|
|
|
(nextViews) => { |
|
|
|
(nextViews) => { |
|
|
@ -31,22 +46,33 @@ watch( |
|
|
|
{ immediate: true }, |
|
|
|
{ immediate: true }, |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Sidebar visible */ |
|
|
|
const toggleDrawer = ref(false) |
|
|
|
const toggleDrawer = ref(false) |
|
|
|
|
|
|
|
|
|
|
|
const isView = ref(false) |
|
|
|
const isView = ref(false) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Is editing the view name enabled */ |
|
|
|
let isEditing = $ref<number | null>(null) |
|
|
|
let isEditing = $ref<number | null>(null) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Helper to check if editing was disabled before the view navigation timeout triggers */ |
|
|
|
let isStopped = $ref(false) |
|
|
|
let isStopped = $ref(false) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Original view title when editing the view name */ |
|
|
|
let originalTitle = $ref<string | undefined>() |
|
|
|
let originalTitle = $ref<string | undefined>() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** View type to create from modal */ |
|
|
|
let viewCreateType = $ref<ViewTypes>() |
|
|
|
let viewCreateType = $ref<ViewTypes>() |
|
|
|
|
|
|
|
|
|
|
|
let viewCreateDlg = $ref(false) |
|
|
|
/** View title to create from modal (when duplicating) */ |
|
|
|
|
|
|
|
const viewCreateTitle = $ref('') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** is view creation modal open */ |
|
|
|
|
|
|
|
let modalOpen = $ref(false) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Selected view(s) for menu */ |
|
|
|
const selected = ref<string[]>([]) |
|
|
|
const selected = ref<string[]>([]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Watch currently active view so we can mark it in the menu */ |
|
|
|
watch(activeView, (nextActiveView) => { |
|
|
|
watch(activeView, (nextActiveView) => { |
|
|
|
const _nextActiveView = nextActiveView as GridType | FormType | KanbanType |
|
|
|
const _nextActiveView = nextActiveView as GridType | FormType | KanbanType |
|
|
|
|
|
|
|
|
|
|
@ -55,30 +81,21 @@ watch(activeView, (nextActiveView) => { |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
onKeyStroke('Escape', (event) => { |
|
|
|
/** Open view creation modal */ |
|
|
|
if (isEditing !== null) { |
|
|
|
function openModal(type: ViewTypes) { |
|
|
|
onKeyEsc(event, isEditing) |
|
|
|
modalOpen = true |
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onKeyStroke('Enter', (event) => { |
|
|
|
|
|
|
|
if (isEditing !== null) { |
|
|
|
|
|
|
|
onKeyEnter(event, isEditing) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function openCreateViewDlg(type: ViewTypes) { |
|
|
|
|
|
|
|
viewCreateDlg = true |
|
|
|
|
|
|
|
viewCreateType = type |
|
|
|
viewCreateType = type |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Handle view creation */ |
|
|
|
function onCreate(view: GridType | FormType | KanbanType | GalleryType) { |
|
|
|
function onCreate(view: GridType | FormType | KanbanType | GalleryType) { |
|
|
|
views.value?.push(view) |
|
|
|
views.value?.push(view) |
|
|
|
activeView.value = view |
|
|
|
activeView.value = view |
|
|
|
viewCreateDlg = false |
|
|
|
modalOpen = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// todo: fix view type, alias is missing for some reason? |
|
|
|
// todo: fix view type, alias is missing for some reason? |
|
|
|
|
|
|
|
/** Navigate to view and add new tab if necessary */ |
|
|
|
function changeView(view: { id: string; alias?: string; title?: string; type: ViewTypes }) { |
|
|
|
function changeView(view: { id: string; alias?: string; title?: string; type: ViewTypes }) { |
|
|
|
activeView.value = view |
|
|
|
activeView.value = view |
|
|
|
|
|
|
|
|
|
|
@ -91,12 +108,14 @@ function changeView(view: { id: string; alias?: string; title?: string; type: Vi |
|
|
|
addTab(tabProps) |
|
|
|
addTab(tabProps) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Debounce click handler so we can potentially enable editing view name {@see onDblClick} */ |
|
|
|
const onClick = useDebounceFn((view) => { |
|
|
|
const onClick = useDebounceFn((view) => { |
|
|
|
if (isEditing !== null || isStopped) return |
|
|
|
if (isEditing !== null || isStopped) return |
|
|
|
|
|
|
|
|
|
|
|
changeView(view) |
|
|
|
changeView(view) |
|
|
|
}, 250) |
|
|
|
}, 250) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Enable editing view name on dbl click */ |
|
|
|
function onDblClick(index: number) { |
|
|
|
function onDblClick(index: number) { |
|
|
|
if (isEditing === null) { |
|
|
|
if (isEditing === null) { |
|
|
|
isEditing = index |
|
|
|
isEditing = index |
|
|
@ -104,6 +123,7 @@ function onDblClick(index: number) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Handle keydown on input field */ |
|
|
|
function onKeyDown(event: KeyboardEvent, index: number) { |
|
|
|
function onKeyDown(event: KeyboardEvent, index: number) { |
|
|
|
if (event.key === 'Escape') { |
|
|
|
if (event.key === 'Escape') { |
|
|
|
onKeyEsc(event, index) |
|
|
|
onKeyEsc(event, index) |
|
|
@ -112,6 +132,7 @@ function onKeyDown(event: KeyboardEvent, index: number) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Rename view when enter is pressed */ |
|
|
|
function onKeyEnter(event: KeyboardEvent, index: number) { |
|
|
|
function onKeyEnter(event: KeyboardEvent, index: number) { |
|
|
|
event.stopImmediatePropagation() |
|
|
|
event.stopImmediatePropagation() |
|
|
|
event.preventDefault() |
|
|
|
event.preventDefault() |
|
|
@ -119,6 +140,7 @@ function onKeyEnter(event: KeyboardEvent, index: number) { |
|
|
|
onRename(index) |
|
|
|
onRename(index) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Disable renaming view when escape is pressed */ |
|
|
|
function onKeyEsc(event: KeyboardEvent, index: number) { |
|
|
|
function onKeyEsc(event: KeyboardEvent, index: number) { |
|
|
|
event.stopImmediatePropagation() |
|
|
|
event.stopImmediatePropagation() |
|
|
|
event.preventDefault() |
|
|
|
event.preventDefault() |
|
|
@ -126,33 +148,82 @@ function onKeyEsc(event: KeyboardEvent, index: number) { |
|
|
|
onCancel(index) |
|
|
|
onCancel(index) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const inputRef = $ref<HTMLInputElement>() |
|
|
|
onKeyStroke('Escape', (event) => { |
|
|
|
|
|
|
|
if (isEditing !== null) { |
|
|
|
|
|
|
|
onKeyEsc(event, isEditing) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onKeyStroke('Enter', (event) => { |
|
|
|
|
|
|
|
if (isEditing !== null) { |
|
|
|
|
|
|
|
onKeyEnter(event, isEditing) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Current input element, changes when edit is enabled on a view menu item */ |
|
|
|
|
|
|
|
let inputRef = $ref<HTMLInputElement>() |
|
|
|
|
|
|
|
|
|
|
|
function setInputRef(el: HTMLInputElement) { |
|
|
|
function setInputRef(el: HTMLInputElement) { |
|
|
|
if (el) el.focus() |
|
|
|
if (el) { |
|
|
|
|
|
|
|
el.focus() |
|
|
|
|
|
|
|
inputRef = el |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Cancel editing when clicked outside of input */ |
|
|
|
onClickOutside(inputRef, () => { |
|
|
|
onClickOutside(inputRef, () => { |
|
|
|
if (isEditing !== null) { |
|
|
|
if (isEditing !== null) { |
|
|
|
onCancel(isEditing) |
|
|
|
onCancel(isEditing) |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
function onCopy(index: number) { |
|
|
|
/** Duplicate a view */ |
|
|
|
// copy view |
|
|
|
// todo: This is not really a duplication, maybe we need to implement a true duplication? |
|
|
|
|
|
|
|
function onDuplicate(index: number) { |
|
|
|
|
|
|
|
const view: any = views.value[index] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
openModal(view.type) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$e('c:view:copy', { view: view.type }) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onDelete(index: number) { |
|
|
|
/** Delete a view */ |
|
|
|
// delete view |
|
|
|
async function onDelete(index: number) { |
|
|
|
|
|
|
|
const view: any = views.value[index] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
await api.dbView.delete(view.id) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
notification.success({ |
|
|
|
|
|
|
|
message: 'View deleted successfully', |
|
|
|
|
|
|
|
duration: 3, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await loadViews() |
|
|
|
|
|
|
|
} catch (e: any) { |
|
|
|
|
|
|
|
notification.error({ |
|
|
|
|
|
|
|
message: await extractSdkResponseErrorMsg(e), |
|
|
|
|
|
|
|
duration: 3, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// telemetry event |
|
|
|
|
|
|
|
$e('a:view:delete', { view: view.type }) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
|
|
|
|
notification.open({ |
|
|
|
|
|
|
|
message: 'Welcome to the View Manager', |
|
|
|
|
|
|
|
duration: 3, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Rename a view */ |
|
|
|
async function onRename(index: number) { |
|
|
|
async function onRename(index: number) { |
|
|
|
// todo: validate if title is unique and not empty |
|
|
|
// todo: validate if title is unique and not empty |
|
|
|
if (isEditing === null) return |
|
|
|
if (isEditing === null) return |
|
|
|
const view = views.value[index] |
|
|
|
const view = views.value[index] |
|
|
|
|
|
|
|
|
|
|
|
console.log(view.title, originalTitle) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (view.title === '' || view.title === originalTitle) { |
|
|
|
if (view.title === '' || view.title === originalTitle) { |
|
|
|
onCancel(index) |
|
|
|
onCancel(index) |
|
|
|
return |
|
|
|
return |
|
|
@ -169,25 +240,27 @@ async function onRename(index: number) { |
|
|
|
|
|
|
|
|
|
|
|
notification.success({ |
|
|
|
notification.success({ |
|
|
|
message: 'View renamed successfully', |
|
|
|
message: 'View renamed successfully', |
|
|
|
duration: 3000, |
|
|
|
duration: 3, |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
console.log('success') |
|
|
|
console.log('success') |
|
|
|
} catch (e: any) { |
|
|
|
} catch (e: any) { |
|
|
|
notification.error({ |
|
|
|
notification.error({ |
|
|
|
message: await extractSdkResponseErrorMsg(e), |
|
|
|
message: await extractSdkResponseErrorMsg(e), |
|
|
|
duration: 3000, |
|
|
|
duration: 3, |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
onStopEdit() |
|
|
|
onStopEdit() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Cancel renaming view */ |
|
|
|
function onCancel(index: number) { |
|
|
|
function onCancel(index: number) { |
|
|
|
views.value[index].title = originalTitle |
|
|
|
views.value[index].title = originalTitle |
|
|
|
onStopEdit() |
|
|
|
onStopEdit() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Stop editing view name, timeout makes sure that view navigation (click trigger) does not pick up before stop is done */ |
|
|
|
function onStopEdit() { |
|
|
|
function onStopEdit() { |
|
|
|
isStopped = true |
|
|
|
isStopped = true |
|
|
|
isEditing = null |
|
|
|
isEditing = null |
|
|
@ -233,7 +306,7 @@ function onStopEdit() { |
|
|
|
{{ $t('activity.copyView') }} |
|
|
|
{{ $t('activity.copyView') }} |
|
|
|
</template> |
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
|
|
<MdiContentCopy class="hidden group-hover:block text-gray-500" /> |
|
|
|
<MdiContentCopy class="hidden group-hover:block text-gray-500" @click.stop="onDuplicate(i)" /> |
|
|
|
</a-tooltip> |
|
|
|
</a-tooltip> |
|
|
|
|
|
|
|
|
|
|
|
<a-tooltip placement="left"> |
|
|
|
<a-tooltip placement="left"> |
|
|
@ -241,7 +314,7 @@ function onStopEdit() { |
|
|
|
{{ $t('activity.deleteView') }} |
|
|
|
{{ $t('activity.deleteView') }} |
|
|
|
</template> |
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
|
|
<MdiTrashCan class="hidden group-hover:block text-red-500" /> |
|
|
|
<MdiTrashCan class="hidden group-hover:block text-red-500" @click.stop="onDelete(i)" /> |
|
|
|
</a-tooltip> |
|
|
|
</a-tooltip> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
@ -251,7 +324,7 @@ function onStopEdit() { |
|
|
|
|
|
|
|
|
|
|
|
<h3 class="px-3 text-xs font-semibold">{{ $t('activity.createView') }}</h3> |
|
|
|
<h3 class="px-3 text-xs font-semibold">{{ $t('activity.createView') }}</h3> |
|
|
|
|
|
|
|
|
|
|
|
<a-menu-item key="grid" class="group !flex !items-center !h-[30px]" @click="openCreateViewDlg(ViewTypes.GRID)"> |
|
|
|
<a-menu-item key="grid" class="group !flex !items-center !h-[30px]" @click="openModal(ViewTypes.GRID)"> |
|
|
|
<a-tooltip placement="left"> |
|
|
|
<a-tooltip placement="left"> |
|
|
|
<template #title> |
|
|
|
<template #title> |
|
|
|
{{ $t('msg.info.addView.grid') }} |
|
|
|
{{ $t('msg.info.addView.grid') }} |
|
|
@ -269,7 +342,7 @@ function onStopEdit() { |
|
|
|
</a-tooltip> |
|
|
|
</a-tooltip> |
|
|
|
</a-menu-item> |
|
|
|
</a-menu-item> |
|
|
|
|
|
|
|
|
|
|
|
<a-menu-item key="gallery" class="group !flex !items-center !h-[30px]" @click="openCreateViewDlg(ViewTypes.GALLERY)"> |
|
|
|
<a-menu-item key="gallery" class="group !flex !items-center !h-[30px]" @click="openModal(ViewTypes.GALLERY)"> |
|
|
|
<a-tooltip placement="left"> |
|
|
|
<a-tooltip placement="left"> |
|
|
|
<template #title> |
|
|
|
<template #title> |
|
|
|
{{ $t('msg.info.addView.gallery') }} |
|
|
|
{{ $t('msg.info.addView.gallery') }} |
|
|
@ -287,12 +360,7 @@ function onStopEdit() { |
|
|
|
</a-tooltip> |
|
|
|
</a-tooltip> |
|
|
|
</a-menu-item> |
|
|
|
</a-menu-item> |
|
|
|
|
|
|
|
|
|
|
|
<a-menu-item |
|
|
|
<a-menu-item v-if="!isView" key="form" class="group !flex !items-center !h-[30px]" @click="openModal(ViewTypes.FORM)"> |
|
|
|
v-if="!isView" |
|
|
|
|
|
|
|
key="form" |
|
|
|
|
|
|
|
class="group !flex !items-center !h-[30px]" |
|
|
|
|
|
|
|
@click="openCreateViewDlg(ViewTypes.FORM)" |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
<a-tooltip placement="left"> |
|
|
|
<a-tooltip placement="left"> |
|
|
|
<template #title> |
|
|
|
<template #title> |
|
|
|
{{ $t('msg.info.addView.form') }} |
|
|
|
{{ $t('msg.info.addView.form') }} |
|
|
@ -313,7 +381,7 @@ function onStopEdit() { |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<DlgViewCreate v-if="views" v-model="viewCreateDlg" :type="viewCreateType" @created="onCreate" /> |
|
|
|
<DlgViewCreate v-if="views" v-model="modalOpen" :title="viewCreateTitle" :type="viewCreateType" @created="onCreate" /> |
|
|
|
</a-layout-sider> |
|
|
|
</a-layout-sider> |
|
|
|
</template> |
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
|
|