diff --git a/packages/nc-gui-v2/components/dlg/ViewCreate.vue b/packages/nc-gui-v2/components/dlg/ViewCreate.vue index af46d38545..f5dbf5e4f7 100644 --- a/packages/nc-gui-v2/components/dlg/ViewCreate.vue +++ b/packages/nc-gui-v2/components/dlg/ViewCreate.vue @@ -13,6 +13,7 @@ import { computed, nextTick, reactive, unref, useApi, useVModel, watch } from '# interface Props { modelValue: boolean type: ViewTypes + title?: string } interface Emits { @@ -45,7 +46,7 @@ const meta = inject(MetaInj) const viewList = inject(ViewListInj) const form = reactive
({ - title: '', + title: props.title || '', type: props.type, copy_from_id: null, }) diff --git a/packages/nc-gui-v2/components/smartsheet/Sidebar.vue b/packages/nc-gui-v2/components/smartsheet/Sidebar.vue index 522ca52ea6..dca1c58a37 100644 --- a/packages/nc-gui-v2/components/smartsheet/Sidebar.vue +++ b/packages/nc-gui-v2/components/smartsheet/Sidebar.vue @@ -2,7 +2,19 @@ import type { FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk' 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 { extractSdkResponseErrorMsg, viewIcons } from '~/utils' import MdiPlusIcon from '~icons/mdi/plus' @@ -13,14 +25,17 @@ const meta = inject(MetaInj, ref()) const activeView = inject(ActiveViewInj, ref()) +const { $e } = useNuxtApp() + const { addTab } = useTabs() -const { views } = useViews(meta) +const { views, loadViews } = useViews(meta) const { api } = useApi() provide(ViewListInj, views) +/** Watch current views and on change set the next active view */ watch( views, (nextViews) => { @@ -31,22 +46,33 @@ watch( { immediate: true }, ) +/** Sidebar visible */ const toggleDrawer = ref(false) const isView = ref(false) +/** Is editing the view name enabled */ let isEditing = $ref(null) +/** Helper to check if editing was disabled before the view navigation timeout triggers */ let isStopped = $ref(false) +/** Original view title when editing the view name */ let originalTitle = $ref() +/** View type to create from modal */ let viewCreateType = $ref() -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([]) +/** Watch currently active view so we can mark it in the menu */ watch(activeView, (nextActiveView) => { const _nextActiveView = nextActiveView as GridType | FormType | KanbanType @@ -55,30 +81,21 @@ watch(activeView, (nextActiveView) => { } }) -onKeyStroke('Escape', (event) => { - if (isEditing !== null) { - onKeyEsc(event, isEditing) - } -}) - -onKeyStroke('Enter', (event) => { - if (isEditing !== null) { - onKeyEnter(event, isEditing) - } -}) - -function openCreateViewDlg(type: ViewTypes) { - viewCreateDlg = true +/** Open view creation modal */ +function openModal(type: ViewTypes) { + modalOpen = true viewCreateType = type } +/** Handle view creation */ function onCreate(view: GridType | FormType | KanbanType | GalleryType) { views.value?.push(view) activeView.value = view - viewCreateDlg = false + modalOpen = false } // 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 }) { activeView.value = view @@ -91,12 +108,14 @@ function changeView(view: { id: string; alias?: string; title?: string; type: Vi addTab(tabProps) } +/** Debounce click handler so we can potentially enable editing view name {@see onDblClick} */ const onClick = useDebounceFn((view) => { if (isEditing !== null || isStopped) return changeView(view) }, 250) +/** Enable editing view name on dbl click */ function onDblClick(index: number) { if (isEditing === null) { isEditing = index @@ -104,6 +123,7 @@ function onDblClick(index: number) { } } +/** Handle keydown on input field */ function onKeyDown(event: KeyboardEvent, index: number) { if (event.key === 'Escape') { 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) { event.stopImmediatePropagation() event.preventDefault() @@ -119,6 +140,7 @@ function onKeyEnter(event: KeyboardEvent, index: number) { onRename(index) } +/** Disable renaming view when escape is pressed */ function onKeyEsc(event: KeyboardEvent, index: number) { event.stopImmediatePropagation() event.preventDefault() @@ -126,33 +148,82 @@ function onKeyEsc(event: KeyboardEvent, index: number) { onCancel(index) } -const inputRef = $ref() +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() function setInputRef(el: HTMLInputElement) { - if (el) el.focus() + if (el) { + el.focus() + inputRef = el + } } +/** Cancel editing when clicked outside of input */ onClickOutside(inputRef, () => { if (isEditing !== null) { onCancel(isEditing) } }) -function onCopy(index: number) { - // copy view +/** Duplicate a 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 view +/** Delete a 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) { // todo: validate if title is unique and not empty if (isEditing === null) return const view = views.value[index] - console.log(view.title, originalTitle) - if (view.title === '' || view.title === originalTitle) { onCancel(index) return @@ -169,25 +240,27 @@ async function onRename(index: number) { notification.success({ message: 'View renamed successfully', - duration: 3000, + duration: 3, }) console.log('success') } catch (e: any) { notification.error({ message: await extractSdkResponseErrorMsg(e), - duration: 3000, + duration: 3, }) } onStopEdit() } +/** Cancel renaming view */ function onCancel(index: number) { views.value[index].title = originalTitle onStopEdit() } +/** Stop editing view name, timeout makes sure that view navigation (click trigger) does not pick up before stop is done */ function onStopEdit() { isStopped = true isEditing = null @@ -233,7 +306,7 @@ function onStopEdit() { {{ $t('activity.copyView') }} -