Browse Source

refactor(gui-v2): split sidebar into smaller components

pull/2837/head
braks 2 years ago
parent
commit
8710519f54
  1. 457
      packages/nc-gui-v2/components/smartsheet/Sidebar.vue
  2. 148
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  3. 171
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  4. 179
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  5. 60
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue

457
packages/nc-gui-v2/components/smartsheet/Sidebar.vue

@ -1,457 +0,0 @@
<script setup lang="ts">
import type { FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
import { notification } from 'ant-design-vue'
import { inject, 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'
import MdiTrashCan from '~icons/mdi/trash-can'
import MdiContentCopy from '~icons/mdi/content-copy'
import MdiXml from '~icons/mdi/xml'
import MdiHook from '~icons/mdi/hook'
import MdiHeartsCard from '~icons/mdi/cards-heart'
import MdiShieldLockOutline from '~icons/mdi/shield-lock-outline'
import type { TabItem } from '~/composables/useTabs'
import { TabType } from '~/composables/useTabs'
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const { $e } = useNuxtApp()
const { addTab } = useTabs()
const { views, loadViews } = useViews(meta)
const { api } = useApi()
provide(ViewListInj, views)
/** Sidebar visible */
const drawerOpen = inject('navDrawerOpen', ref(false))
/** Watch current views and on change set the next active view */
watch(
views,
(nextViews) => {
if (nextViews.length) {
activeView.value = nextViews[0]
}
},
{ immediate: true },
)
const isView = ref(false)
/** Is editing the view name enabled */
let isEditing = $ref<number | null>(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<string | undefined>()
/** View type to create from modal */
let viewCreateType = $ref<ViewTypes>()
/** 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 validate = (value?: string) => {
if (!value || value.trim().length < 0) {
return 'View name is required'
}
if ((unref(views) || []).every((v1) => ((v1 as GridType | KanbanType | GalleryType).alias || v1.title) !== value)) {
return 'View name should be unique'
}
return true
}
/** Watch currently active view so we can mark it in the menu */
watch(activeView, (nextActiveView) => {
const _nextActiveView = nextActiveView as GridType | FormType | KanbanType
if (_nextActiveView && _nextActiveView.id) {
selected.value = [_nextActiveView.id]
}
})
/** 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
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
const tabProps: TabItem = {
id: view.id,
title: (view.alias ?? view.title) || '',
type: TabType.VIEW,
}
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
originalTitle = views.value[index].title
}
}
/** Handle keydown on input field */
function onKeyDown(event: KeyboardEvent, index: number) {
if (event.key === 'Escape') {
onKeyEsc(event, index)
} else if (event.key === 'Enter') {
onKeyEnter(event, index)
}
}
/** Rename view when enter is pressed */
function onKeyEnter(event: KeyboardEvent, index: number) {
event.stopImmediatePropagation()
event.preventDefault()
onRename(index)
}
/** Disable renaming view when escape is pressed */
function onKeyEsc(event: KeyboardEvent, index: number) {
event.stopImmediatePropagation()
event.preventDefault()
onCancel(index)
}
onKeyStroke('Enter', (event) => {
if (isEditing !== null) {
onKeyEnter(event, isEditing)
}
})
function setInputRef(el: HTMLInputElement) {
if (el) el.focus()
}
/** 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 })
}
/** 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 })
}
/** Rename a view */
async function onRename(index: number) {
if (isEditing === null) return
const view = views.value[index]
const valid = validate(view?.title)
if (valid !== true) {
notification.error({
message: valid,
duration: 2,
})
}
if (view.title === '' || view.title === originalTitle) {
onCancel(index)
return
}
try {
// todo typing issues, order and id do not exist on all members of ViewTypes (Kanban, Gallery, Form, Grid)
await api.dbView.update((view as any).id, {
title: view.title,
order: (view as any).order,
})
notification.success({
message: 'View renamed successfully',
duration: 3,
})
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
duration: 3,
})
}
onStopEdit()
}
/** Cancel renaming view */
function onCancel(index: number) {
if (isEditing === null) return
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
originalTitle = ''
setTimeout(() => {
isStopped = false
}, 250)
}
function onApiSnippet() {
// get API snippet
$e('a:view:api-snippet')
}
</script>
<template>
<a-layout-sider class="shadow" :width="drawerOpen ? 0 : 250">
<div class="flex flex-col h-full">
<a-menu class="flex-1 max-h-50vh overflow-y-scroll scrollbar-thin-primary" :selected-keys="selected">
<h3 class="pt-3 px-3 text-xs font-semibold">{{ $t('objects.views') }}</h3>
<a-menu-item
v-for="(view, i) of views"
:key="view.id"
class="group !flex !items-center !my-0"
@dblclick="onDblClick(i)"
@click="onClick(view)"
>
<div v-t="['a:view:open', { view: view.type }]" class="text-xs flex items-center w-full gap-2">
<component :is="viewIcons[view.type].icon" :class="`text-${viewIcons[view.type].color}`" />
<a-input
v-if="isEditing === i"
:ref="setInputRef"
v-model:value="view.title"
@blur="onCancel(i)"
@keydown="onKeyDown($event, i)"
/>
<div v-else>{{ view.alias || view.title }}</div>
<div class="flex-1" />
<template v-if="isEditing !== i">
<div class="flex items-center gap-1">
<a-tooltip placement="left">
<template #title>
{{ $t('activity.copyView') }}
</template>
<MdiContentCopy class="hidden group-hover:block text-gray-500" @click.stop="onDuplicate(i)" />
</a-tooltip>
<a-popconfirm
v-if="!view.is_default"
placement="left"
:title="$t('msg.info.deleteProject')"
:ok-text="$t('general.yes')"
:cancel-text="$t('general.no')"
@confirm="onDelete(i)"
>
<a-tooltip placement="left">
<template #title>
{{ $t('activity.deleteView') }}
</template>
<MdiTrashCan class="hidden group-hover:block text-red-500" @click.stop />
</a-tooltip>
</a-popconfirm>
</div>
</template>
</div>
</a-menu-item>
</a-menu>
<a-menu class="flex-1 flex flex-col">
<a-divider class="my-2" />
<h3 class="px-3 text-xs font-semibold flex items-center gap-4">
{{ $t('activity.createView') }}
<a-tooltip>
<template #title>
{{ $t('msg.info.onlyCreator') }}
</template>
<MdiShieldLockOutline class="text-pink-500" />
</a-tooltip>
</h3>
<a-menu-item key="grid" class="group !flex !items-center !my-0 !h-[30px]" @click="openModal(ViewTypes.GRID)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.grid') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GRID].icon" :class="`text-${viewIcons[ViewTypes.GRID].color}`" />
<div>{{ $t('objects.viewType.grid') }}</div>
<div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item key="gallery" class="group !flex !items-center !-my0 !h-[30px]" @click="openModal(ViewTypes.GALLERY)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.gallery') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GALLERY].icon" :class="`text-${viewIcons[ViewTypes.GALLERY].color}`" />
<div>{{ $t('objects.viewType.gallery') }}</div>
<div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item
v-if="!isView"
key="form"
class="group !flex !items-center !my-0 !h-[30px]"
@click="openModal(ViewTypes.FORM)"
>
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.form') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.FORM].icon" :class="`text-${viewIcons[ViewTypes.FORM].color}`" />
<div>{{ $t('objects.viewType.form') }}</div>
<div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<div class="flex-auto justify-end flex flex-col gap-4 mt-4">
<button
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded !bg-primary text-white transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease"
@click="onApiSnippet"
>
<MdiXml />Get API Snippet
</button>
<button
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease"
@click="onApiSnippet"
>
<MdiHook />{{ $t('objects.webhooks') }}
</button>
</div>
<general-flipping-card class="my-4 min-h-[100px] w-[250px]" :triggers="['click', { duration: 15000 }]">
<template #front>
<div class="flex h-full w-full gap-6 flex-col">
<general-social />
<div>
<a
v-t="['e:hiring']"
class="px-4 py-3 !bg-primary rounded shadow text-white"
href="https://angel.co/company/nocodb"
target="_blank"
@click.stop
>
🚀 We are Hiring! 🚀
</a>
</div>
</div>
</template>
<template #back>
<!-- todo: add project cost -->
<a
href="https://github.com/sponsors/nocodb"
target="_blank"
class="group flex items-center gap-2 w-full mx-3 px-4 py-2 rounded-l !bg-primary text-white transform translate-x-4 hover:(translate-x-0 shadow-lg !opacity-100) transition duration-150 ease"
@click.stop
>
<MdiHeartsCard class="text-red-500" />
{{ $t('activity.sponsorUs') }}
</a>
</template>
</general-flipping-card>
</a-menu>
</div>
<dlg-view-create v-if="views" v-model="modalOpen" :title="viewCreateTitle" :type="viewCreateType" @created="onCreate" />
</a-layout-sider>
</template>
<style scoped>
:deep(.ant-menu-title-content) {
@apply w-full;
}
</style>

148
packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue

@ -0,0 +1,148 @@
<script lang="ts" setup>
import { ViewTypes } from 'nocodb-sdk'
import { ref, useNuxtApp } from '#imports'
import { viewIcons } from '~/utils'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiXml from '~icons/mdi/xml'
import MdiHook from '~icons/mdi/hook'
import MdiHeartsCard from '~icons/mdi/cards-heart'
import MdiShieldLockOutline from '~icons/mdi/shield-lock-outline'
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void
}
const emits = defineEmits<Emits>()
const { $e } = useNuxtApp()
const isView = ref(false)
function onApiSnippet() {
// get API snippet
$e('a:view:api-snippet')
}
function onOpenModal(type: ViewTypes, title = '') {
emits('openModal', { type, title })
}
</script>
<template>
<a-menu class="flex-1 flex flex-col">
<a-divider class="my-2" />
<h3 class="px-3 text-xs font-semibold flex items-center gap-4">
{{ $t('activity.createView') }}
<a-tooltip>
<template #title>
{{ $t('msg.info.onlyCreator') }}
</template>
<MdiShieldLockOutline class="text-pink-500" />
</a-tooltip>
</h3>
<a-menu-item key="grid" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.GRID)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.grid') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GRID].icon" :class="`text-${viewIcons[ViewTypes.GRID].color}`" />
<div>{{ $t('objects.viewType.grid') }}</div>
<div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item key="gallery" class="group !flex !items-center !-my0 !h-[30px]" @click="onOpenModal(ViewTypes.GALLERY)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.gallery') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GALLERY].icon" :class="`text-${viewIcons[ViewTypes.GALLERY].color}`" />
<div>{{ $t('objects.viewType.gallery') }}</div>
<div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item v-if="!isView" key="form" class="group !flex !items-center !my-0 !h-[30px]" @click="onOpenModal(ViewTypes.FORM)">
<a-tooltip placement="left">
<template #title>
{{ $t('msg.info.addView.form') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.FORM].icon" :class="`text-${viewIcons[ViewTypes.FORM].color}`" />
<div>{{ $t('objects.viewType.form') }}</div>
<div class="flex-1" />
<MdiPlusIcon class="group-hover:text-primary" />
</div>
</a-tooltip>
</a-menu-item>
<div class="flex-auto justify-end flex flex-col gap-4 mt-4">
<button
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded !bg-primary text-white transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease"
@click="onApiSnippet"
>
<MdiXml />Get API Snippet
</button>
<button
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease"
@click="onApiSnippet"
>
<MdiHook />{{ $t('objects.webhooks') }}
</button>
</div>
<general-flipping-card class="my-4 min-h-[100px] w-[250px]" :triggers="['click', { duration: 15000 }]">
<template #front>
<div class="flex h-full w-full gap-6 flex-col">
<general-social />
<div>
<a
v-t="['e:hiring']"
class="px-4 py-3 !bg-primary rounded shadow text-white"
href="https://angel.co/company/nocodb"
target="_blank"
@click.stop
>
🚀 We are Hiring! 🚀
</a>
</div>
</div>
</template>
<template #back>
<!-- todo: add project cost -->
<a
href="https://github.com/sponsors/nocodb"
target="_blank"
class="group flex items-center gap-2 w-full mx-3 px-4 py-2 rounded-l !bg-primary text-white transform translate-x-4 hover:(translate-x-0 shadow-lg !opacity-100) transition duration-150 ease"
@click.stop
>
<MdiHeartsCard class="text-red-500" />
{{ $t('activity.sponsorUs') }}
</a>
</template>
</general-flipping-card>
</a-menu>
</template>

171
packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue

@ -0,0 +1,171 @@
<script lang="ts" setup>
import type { FormType, GalleryType, GridType, KanbanType, ViewTypes } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { notification } from 'ant-design-vue'
import RenameableMenuItem from './RenameableMenuItem.vue'
import { inject, onBeforeUnmount, ref, unref, useApi, useNuxtApp, useTabs, useViews, watch } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
import type { TabItem } from '~/composables/useTabs'
import { TabType } from '~/composables/useTabs'
import { ActiveViewInj, MetaInj } from '~/context'
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void
}
const emits = defineEmits<Emits>()
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const { $e } = useNuxtApp()
const { addTab } = useTabs()
const { views, loadViews } = useViews(meta)
const { api } = useApi()
/** sortable instance */
let sortable: Sortable
/** Selected view(s) for menu */
const selected = ref<string[]>([])
/** Watch current views and on change set the next active view */
watch(
views,
(nextViews) => {
if (nextViews.length) {
activeView.value = nextViews[0]
}
},
{ immediate: true },
)
/** Watch currently active view, so we can mark it in the menu */
watch(activeView, (nextActiveView) => {
const _nextActiveView = nextActiveView as GridType | FormType | KanbanType
if (_nextActiveView && _nextActiveView.id) {
selected.value = [_nextActiveView.id]
}
})
onBeforeUnmount(() => {
if (sortable) sortable.destroy()
})
function validate(value?: string) {
if (!value || value.trim().length < 0) {
return 'View name is required'
}
if ((unref(views) || []).every((v1) => ((v1 as GridType | KanbanType | GalleryType).alias || v1.title) !== value)) {
return 'View name should be unique'
}
return true
}
function initializeSortable(el: HTMLElement) {
/** if instance exists, destroy it first */
if (sortable) sortable.destroy()
sortable = Sortable.create(el, {
handle: '.nc-drag-icon',
onEnd: async (evt) => {
const { newIndex = 0, oldIndex = 0 } = evt
const itemEl = evt.item as HTMLLIElement
console.log(itemEl)
},
animation: 150,
})
}
// 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
const tabProps: TabItem = {
id: view.id,
title: (view.alias ?? view.title) || '',
type: TabType.VIEW,
}
addTab(tabProps)
}
/** Rename a view */
async function onRename(view: Record<string, any>) {
const valid = validate(view.title)
if (valid !== true) {
notification.error({
message: valid,
duration: 2,
})
}
try {
// todo typing issues, order and id do not exist on all members of ViewTypes (Kanban, Gallery, Form, Grid)
await api.dbView.update(view.id, {
title: view.title,
order: view.order,
})
notification.success({
message: 'View renamed successfully',
duration: 3,
})
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
duration: 3,
})
}
}
/** Delete a view */
async function onDelete(view: Record<string, any>) {
try {
await api.dbView.delete(view.id)
notification.success({
message: 'View deleted successfully',
duration: 3,
})
await loadViews()
console.log(views.value)
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
duration: 3,
})
}
// telemetry event
$e('a:view:delete', { view: view.type })
}
</script>
<template>
<a-menu class="flex-1 max-h-50vh overflow-y-scroll scrollbar-thin-primary" :selected-keys="selected">
<h3 class="pt-3 px-3 text-xs font-semibold">{{ $t('objects.views') }}</h3>
<RenameableMenuItem
v-for="view of views"
:key="view.id"
:view="view"
@change-view="changeView"
@open-modal="$emit('openModal', $event)"
@delete="onDelete"
@rename="onRename"
/>
</a-menu>
</template>

179
packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -0,0 +1,179 @@
<script lang="ts" setup>
import type { ViewTypes } from 'nocodb-sdk'
import { viewIcons } from '~/utils'
import { useDebounceFn, useNuxtApp, useVModel } from '#imports'
import MdiTrashCan from '~icons/mdi/trash-can'
import MdiContentCopy from '~icons/mdi/content-copy'
import MdiDrag from '~icons/mdi/drag-vertical'
interface Props {
view: Record<string, any>
}
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void
(event: 'update:view', data: Record<string, any>): void
(event: 'changeView', view: Record<string, any>): void
(event: 'rename', view: Record<string, any>): void
(event: 'delete', view: Record<string, any>): void
}
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const vModel = useVModel(props, 'view', emits)
const { $e } = useNuxtApp()
/** Is editing the view name enabled */
let isEditing = $ref<boolean>(false)
/** 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<string | undefined>()
/** Debounce click handler, so we can potentially enable editing view name {@see onDblClick} */
const onClick = useDebounceFn(() => {
if (isEditing || isStopped) return
emits('changeView', vModel.value)
}, 250)
/** Enable editing view name on dbl click */
function onDblClick() {
if (!isEditing) {
isEditing = true
originalTitle = vModel.value.title
}
}
/** Handle keydown on input field */
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
onKeyEsc(event)
} else if (event.key === 'Enter') {
onKeyEnter(event)
}
}
/** Rename view when enter is pressed */
function onKeyEnter(event: KeyboardEvent) {
event.stopImmediatePropagation()
event.preventDefault()
onRename()
}
/** Disable renaming view when escape is pressed */
function onKeyEsc(event: KeyboardEvent) {
event.stopImmediatePropagation()
event.preventDefault()
onCancel()
}
onKeyStroke('Enter', (event) => {
if (isEditing) {
onKeyEnter(event)
}
})
function focusInput(el: HTMLInputElement) {
if (el) el.focus()
}
/** Duplicate a view */
// todo: This is not really a duplication, maybe we need to implement a true duplication?
function onDuplicate() {
emits('openModal', { type: vModel.value.type, title: vModel.value.title })
$e('c:view:copy', { view: vModel.value.type })
}
/** Delete a view */
async function onDelete() {
emits('delete', vModel.value)
}
/** Rename a view */
async function onRename() {
if (!isEditing) return
if (vModel.value.title === '' || vModel.value.title === originalTitle) {
onCancel()
return
}
emits('rename', vModel.value)
onStopEdit()
}
/** Cancel renaming view */
function onCancel() {
if (!isEditing) return
vModel.value.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 = false
originalTitle = ''
setTimeout(() => {
isStopped = false
}, 250)
}
</script>
<template>
<a-menu-item class="group !flex !items-center !my-0" @dblclick="onDblClick" @click="onClick">
<div v-t="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2">
<MdiDrag
:class="`transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 nc-drag-icon cursor-move nc-child-draggable-icon-${vModel.title}`"
/>
<component :is="viewIcons[vModel.type].icon" :class="`text-${viewIcons[vModel.type].color}`" />
<a-input v-if="isEditing" :ref="focusInput" v-model:value="vModel.title" @blur="onCancel" @keydown="onKeyDown($event)" />
<div v-else>{{ vModel.alias || vModel.title }}</div>
<div class="flex-1" />
<template v-if="!isEditing">
<div class="flex items-center gap-1">
<a-tooltip placement="left">
<template #title>
{{ $t('activity.copyView') }}
</template>
<MdiContentCopy class="hidden group-hover:block text-gray-500" @click.stop="onDuplicate" />
</a-tooltip>
<a-popconfirm
v-if="!vModel.is_default"
placement="left"
:title="$t('msg.info.deleteProject')"
:ok-text="$t('general.yes')"
:cancel-text="$t('general.no')"
@confirm="onDelete"
>
<a-tooltip placement="left">
<template #title>
{{ $t('activity.deleteView') }}
</template>
<MdiTrashCan class="hidden group-hover:block text-red-500" @click.stop />
</a-tooltip>
</a-popconfirm>
</div>
</template>
</div>
</a-menu-item>
</template>

60
packages/nc-gui-v2/components/smartsheet/sidebar/index.vue

@ -0,0 +1,60 @@
<script setup lang="ts">
import type { FormType, GalleryType, GridType, KanbanType, ViewTypes } from 'nocodb-sdk'
import MenuTop from './MenuTop.vue'
import MenuBottom from './MenuBottom.vue'
import { inject, provide, ref, useApi, useViews } from '#imports'
import { ActiveViewInj, MetaInj, ViewListInj } from '~/context'
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const { views } = useViews(meta)
const { api } = useApi()
provide(ViewListInj, views)
/** Sidebar visible */
const drawerOpen = inject('navDrawerOpen', ref(false))
/** View type to create from modal */
let viewCreateType = $ref<ViewTypes>()
/** View title to create from modal (when duplicating) */
let viewCreateTitle = $ref('')
/** is view creation modal open */
let modalOpen = $ref(false)
/** Open view creation modal */
function openModal({ type, title = '' }: { type: ViewTypes; title: string }) {
modalOpen = true
viewCreateType = type
viewCreateTitle = title
}
/** Handle view creation */
function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
views.value?.push(view)
activeView.value = view
modalOpen = false
}
</script>
<template>
<a-layout-sider class="shadow" :width="drawerOpen ? 0 : 250">
<div class="flex flex-col h-full">
<MenuTop @open-modal="openModal" />
<MenuBottom @open-modal="openModal" />
</div>
<dlg-view-create v-if="views" v-model="modalOpen" :title="viewCreateTitle" :type="viewCreateType" @created="onCreate" />
</a-layout-sider>
</template>
<style scoped>
:deep(.ant-menu-title-content) {
@apply w-full;
}
</style>
Loading…
Cancel
Save