Browse Source

fix: Removed right view sidebar

pull/6444/head
Muhammed Mustafa 1 year ago
parent
commit
351f787bb2
  1. 151
      packages/nc-gui/components/smartsheet/sidebar/MenuBottom.vue
  2. 389
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  3. 292
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  4. 281
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  5. 11
      packages/nc-gui/components/tabs/Smartsheet.vue
  6. 209
      packages/nc-gui/components/tabs/SmartsheetResizable.vue

151
packages/nc-gui/components/smartsheet/sidebar/MenuBottom.vue

@ -1,151 +0,0 @@
<script lang="ts" setup>
import { ViewTypes } from 'nocodb-sdk'
import { iconMap, useNuxtApp, useSmartsheetStoreOrThrow, viewIcons } from '#imports'
const emits = defineEmits<Emits>()
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void
}
const { $e } = useNuxtApp()
const { isSqlView } = useSmartsheetStoreOrThrow()
const { betaFeatureToggleState } = useBetaFeatureToggle()
function onOpenModal(type: ViewTypes, title = '') {
$e('c:view:create', { view: type })
emits('openModal', { type, title })
}
</script>
<template>
<a-menu :selected-keys="[]" class="flex flex-col !text-gray-600 !bg-inherit">
<div class="px-6 text-xs flex items-center gap-4 my-2 !text-gray-700">
{{ $t('activity.createView') }}
</div>
<a-menu-item
key="grid"
class="nc-create-view group !flex !items-center !my-0 !h-2.5rem nc-create-grid-view"
@click="onOpenModal(ViewTypes.GRID)"
>
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title>
{{ $t('msg.info.addView.grid') }}
</template>
<div class="!py-0 text-xs flex items-center h-full w-full gap-2">
<GeneralViewIcon :meta="{ type: ViewTypes.GRID }" class="min-w-5 flex" />
<div>{{ $t('objects.viewType.grid') }}</div>
<div class="flex-1" />
<component :is="iconMap.plus" />
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item
key="gallery"
class="nc-create-view group !flex !items-center !my-0 !h-2.5rem nc-create-gallery-view"
@click="onOpenModal(ViewTypes.GALLERY)"
>
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title>
{{ $t('msg.info.addView.gallery') }}
</template>
<div class="!py-0 text-xs flex items-center h-full w-full gap-2">
<GeneralViewIcon :meta="{ type: ViewTypes.GALLERY }" class="min-w-5 flex" />
<div>{{ $t('objects.viewType.gallery') }}</div>
<div class="flex-1" />
<component :is="iconMap.plus" />
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item
v-if="!isSqlView"
key="form"
class="nc-create-view group !flex !items-center !my-0 !h-2.5rem nc-create-form-view"
@click="onOpenModal(ViewTypes.FORM)"
>
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title>
{{ $t('msg.info.addView.form') }}
</template>
<div class="!py-0 text-xs flex items-center h-full w-full gap-2">
<GeneralViewIcon :meta="{ type: ViewTypes.FORM }" class="min-w-5 flex" />
<div>{{ $t('objects.viewType.form') }}</div>
<div class="flex-1" />
<component :is="iconMap.plus" />
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item
key="kanban"
class="nc-create-view group !flex !items-center !my-0 !h-2.5rem nc-create-kanban-view"
@click="onOpenModal(ViewTypes.KANBAN)"
>
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title>
{{ $t('msg.info.addView.kanban') }}
</template>
<div class="!py-0 text-xs flex items-center h-full w-full gap-2">
<GeneralViewIcon :meta="{ type: ViewTypes.KANBAN }" class="min-w-5 flex" />
<div>{{ $t('objects.viewType.kanban') }}</div>
<div class="flex-1" />
<component :is="iconMap.plus" />
</div>
</a-tooltip>
</a-menu-item>
<a-menu-item
v-if="betaFeatureToggleState.show"
key="map"
class="nc-create-view group !flex !items-center !my-0 !h-2.5rem nc-create-map-view"
@click="onOpenModal(ViewTypes.MAP)"
>
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title>
{{ $t('msg.info.addView.map') }}
</template>
<div class="!py-0 text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.MAP].icon" :style="{ color: viewIcons[ViewTypes.MAP]?.color }" />
<div>{{ $t('objects.viewType.map') }}</div>
<div class="flex-1" />
<component :is="iconMap.plus" />
</div>
</a-tooltip>
</a-menu-item>
<div class="w-full h-3" />
</a-menu>
</template>
<style lang="scss" scoped>
:deep(.nc-create-view) {
@apply !py-0 !h-8 mx-3.75 rounded-md hover:(text-gray-800 bg-gray-100);
}
:deep(.nc-create-view.ant-menu-item) {
@apply px-2.25;
}
</style>

389
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -1,389 +0,0 @@
<script lang="ts" setup>
import type { ViewType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
import type { SortableEvent } from 'sortablejs'
import Sortable from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue'
import {
ActiveViewInj,
extractSdkResponseErrorMsg,
inject,
message,
onMounted,
parseProp,
ref,
resolveComponent,
useApi,
useCommandPalette,
useDialog,
useNuxtApp,
useRouter,
useUndoRedo,
viewTypeAlias,
watch,
} from '#imports'
interface Props {
views: ViewType[]
}
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): void
(event: 'deleted'): void
}
const { views = [] } = defineProps<Props>()
const emits = defineEmits<Emits>()
const { $e } = useNuxtApp()
const activeView = inject(ActiveViewInj, ref())
const { api } = useApi()
const router = useRouter()
const { refreshCommandPalette } = useCommandPalette()
const { addUndo, defineModelScope } = useUndoRedo()
/** Selected view(s) for menu */
const selected = ref<string[]>([])
/** dragging renamable view items */
const dragging = ref(false)
const menuRef = ref<typeof AntMenu>()
const isMarked = ref<string | false>(false)
/** Watch currently active view, so we can mark it in the menu */
watch(activeView, (nextActiveView) => {
if (nextActiveView && nextActiveView.id) {
selected.value = [nextActiveView.id]
}
})
/** shortly mark an item after sorting */
function markItem(id: string) {
isMarked.value = id
setTimeout(() => {
isMarked.value = false
}, 300)
}
/** validate view title */
function validate(view: ViewType) {
if (!view.title || view.title.trim().length < 0) {
return 'View name is required'
}
if (views.some((v) => v.title === view.title && v.id !== view.id)) {
return 'View name should be unique'
}
return true
}
let sortable: Sortable
function onSortStart(evt: SortableEvent) {
evt.stopImmediatePropagation()
evt.preventDefault()
dragging.value = true
}
async function onSortEnd(evt: SortableEvent, undo = false) {
if (!undo) {
evt.stopImmediatePropagation()
evt.preventDefault()
dragging.value = false
}
if (views.length < 2) return
const { newIndex = 0, oldIndex = 0 } = evt
if (newIndex === oldIndex) return
if (!undo) {
addUndo({
redo: {
fn: async () => {
const ord = sortable.toArray()
const temp = ord.splice(oldIndex, 1)
ord.splice(newIndex, 0, temp[0])
sortable.sort(ord)
await onSortEnd(evt, true)
},
args: [],
},
undo: {
fn: async () => {
const ord = sortable.toArray()
const temp = ord.splice(newIndex, 1)
ord.splice(oldIndex, 0, temp[0])
sortable.sort(ord)
await onSortEnd({ ...evt, oldIndex: newIndex, newIndex: oldIndex }, true)
},
args: [],
},
scope: defineModelScope({ view: activeView.value }),
})
}
const children = evt.to.children as unknown as HTMLLIElement[]
const previousEl = children[newIndex - 1]
const nextEl = children[newIndex + 1]
const currentItem = views.find((v) => v.id === evt.item.id)
if (!currentItem || !currentItem.id) return
const previousItem = (previousEl ? views.find((v) => v.id === previousEl.id) : {}) as ViewType
const nextItem = (nextEl ? views.find((v) => v.id === nextEl.id) : {}) as ViewType
let nextOrder: number
// set new order value based on the new order of the items
if (views.length - 1 === newIndex) {
nextOrder = parseFloat(String(previousItem.order)) + 1
} else if (newIndex === 0) {
nextOrder = parseFloat(String(nextItem.order)) / 2
} else {
nextOrder = (parseFloat(String(previousItem.order)) + parseFloat(String(nextItem.order))) / 2
}
const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder : oldIndex
currentItem.order = _nextOrder
await api.dbView.update(currentItem.id, { order: _nextOrder })
markItem(currentItem.id)
$e('a:view:reorder')
}
const initSortable = (el: HTMLElement) => {
if (sortable) sortable.destroy()
sortable = new Sortable(el, {
// handle: '.nc-drag-icon',
ghostClass: 'ghost',
onStart: onSortStart,
onEnd: onSortEnd,
})
}
onMounted(() => menuRef.value && initSortable(menuRef.value.$el))
/** Navigate to view by changing url param */
function changeView(view: ViewType) {
if (
router.currentRoute.value.query &&
router.currentRoute.value.query.page &&
router.currentRoute.value.query.page === 'fields'
) {
router.push({ params: { viewTitle: view.id || '' }, query: router.currentRoute.value.query })
} else {
router.push({ params: { viewTitle: view.id || '' } })
}
if (view.type === ViewTypes.FORM && selected.value[0] === view.id) {
// reload the page if the same form view is clicked
// router.go(0)
// fix me: router.go(0) reloads entire page. need to reload only the form view
router.replace({ query: { reload: 'true' } }).then(() => {
router.replace({ query: {} })
})
}
}
/** Rename a view */
async function onRename(view: ViewType, originalTitle?: string, undo = false) {
try {
await api.dbView.update(view.id!, {
title: view.title,
order: view.order,
})
await router.replace({
params: {
viewTitle: view.id,
},
})
refreshCommandPalette()
if (!undo) {
addUndo({
redo: {
fn: (v: ViewType, title: string) => {
const tempTitle = v.title
v.title = title
onRename(v, tempTitle, true)
},
args: [view, view.title],
},
undo: {
fn: (v: ViewType, title: string) => {
const tempTitle = v.title
v.title = title
onRename(v, tempTitle, true)
},
args: [view, originalTitle],
},
scope: defineModelScope({ view: activeView.value }),
})
}
// View renamed successfully
// message.success(t('msg.success.viewRenamed'))
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
/** Open delete modal */
function openDeleteDialog(view: ViewType) {
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgViewDelete'), {
'modelValue': isOpen,
'view': view,
'onUpdate:modelValue': closeDialog,
'onDeleted': () => {
closeDialog()
emits('deleted')
refreshCommandPalette()
if (activeView.value === view) {
// return to the default view
router.replace({
params: {
viewTitle: views[0].id,
},
})
}
},
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
const setIcon = async (icon: string, view: ViewType) => {
try {
// modify the icon property in meta
view.meta = {
...parseProp(view.meta),
icon,
}
api.dbView.update(view.id as string, {
meta: view.meta,
})
$e('a:view:icon:sidebar', { icon })
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const scrollViewNode = () => {
const activeViewDom = document.querySelector(`.nc-views-menu [data-view-id="${activeView.value?.id}"]`) as HTMLElement
if (!activeViewDom) return
if (isElementInvisible(activeViewDom)) {
// Scroll to the view node
activeViewDom?.scrollIntoView({ behavior: 'auto', inline: 'start' })
}
}
watch(
() => activeView.value?.id,
() => {
if (!activeView.value?.id) return
// TODO: Find a better way to scroll to the view node
setTimeout(() => {
scrollViewNode()
}, 800)
},
{
immediate: true,
},
)
</script>
<template>
<a-menu
ref="menuRef"
:class="{ dragging }"
class="nc-views-menu flex flex-col !ml-3 w-full !border-r-0 !bg-inherit"
:selected-keys="selected"
>
<!-- Lazy load breaks menu item active styles, i.e. styles never change even when active item changes -->
<SmartsheetSidebarRenameableMenuItem
v-for="view of views"
:id="view.id"
:key="view.id"
:view="view"
:on-validate="validate"
class="nc-view-item !rounded-md !px-1.25 !py-0.5 w-full transition-all ease-in duration-300"
:class="{
'bg-gray-200': isMarked === view.id,
'active': activeView?.id === view.id,
[`nc-${view.type ? viewTypeAlias[view.type] : undefined || view.type}-view-item`]: true,
}"
:data-view-id="view.id"
@change-view="changeView"
@open-modal="$emit('openModal', $event)"
@delete="openDeleteDialog"
@rename="onRename"
@select-icon="setIcon($event, view)"
/>
<div class="min-h-1 max-h-1 w-full bg-transparent"></div>
</a-menu>
</template>
<style lang="scss" scoped>
.nc-views-menu {
@apply min-h-20 flex-grow;
.ghost,
.ghost > * {
@apply !pointer-events-none;
}
&.dragging {
.nc-icon {
@apply !hidden;
}
.nc-view-icon {
@apply !block;
}
}
.ant-menu-item:not(.sortable-chosen) {
@apply color-transition;
}
.sortable-chosen {
@apply !bg-gray-100 bg-opacity-60;
}
.active {
@apply bg-gray-200 bg-opacity-60 font-medium;
}
}
</style>

292
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -1,292 +0,0 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import type { KanbanType, ViewType, ViewTypes } from 'nocodb-sdk'
import type { WritableComputedRef } from '@vue/reactivity'
import { IsLockedInj, inject, message, onKeyStroke, useDebounceFn, useNuxtApp, useRoles, useVModel } from '#imports'
interface Props {
view: ViewType
onValidate: (view: ViewType) => boolean | string
}
interface Emits {
(event: 'update:view', data: Record<string, any>): void
(event: 'selectIcon', icon: string): void
(event: 'changeView', view: Record<string, any>): void
(event: 'rename', view: ViewType, title: string | undefined): void
(event: 'delete', view: ViewType): void
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): void
}
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const vModel = useVModel(props, 'view', emits) as WritableComputedRef<ViewType & { alias?: string; is_default: boolean }>
const { $e } = useNuxtApp()
const { isUIAllowed } = useRoles()
const activeView = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false))
const { rightSidebarState } = storeToRefs(useSidebarStore())
const isDropdownOpen = ref(false)
const isEditing = ref(false)
/** Is editing the view name enabled */
/** Helper to check if editing was disabled before the view navigation timeout triggers */
const isStopped = ref(false)
/** Original view title when editing the view name */
const _title = ref<string | undefined>()
/** Debounce click handler, so we can potentially enable editing view name {@see onDblClick} */
const onClick = useDebounceFn(() => {
if (isEditing.value || isStopped.value) return
emits('changeView', vModel.value)
}, 250)
/** Enable editing view name on dbl click */
function onDblClick() {
if (!isUIAllowed('viewCreateOrEdit')) return
if (!isEditing.value) {
isEditing.value = true
_title.value = vModel.value.title
$e('c:view:rename', { view: vModel.value?.type })
}
}
/** 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.value) {
onKeyEnter(event)
}
})
const focusInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
/** Duplicate a view */
// todo: This is not really a duplication, maybe we need to implement a true duplication?
function onDuplicate() {
isDropdownOpen.value = false
emits('openModal', {
type: vModel.value.type!,
title: vModel.value.title,
copyViewId: vModel.value.id,
groupingFieldColumnId: (vModel.value.view as KanbanType).fk_grp_col_id!,
})
$e('c:view:copy', { view: vModel.value.type })
}
/** Delete a view */
async function onDelete() {
isDropdownOpen.value = false
emits('delete', vModel.value)
}
/** Rename a view */
async function onRename() {
isDropdownOpen.value = false
if (!isEditing.value) return
const isValid = props.onValidate({ ...vModel.value, title: _title.value! })
if (isValid !== true) {
message.error(isValid)
onCancel()
return
}
if (vModel.value.title === '' || vModel.value.title === _title.value) {
onCancel()
return
}
const originalTitle = vModel.value.title
vModel.value.title = _title.value || ''
emits('rename', vModel.value, originalTitle)
onStopEdit()
}
/** Cancel renaming view */
function onCancel() {
if (!isEditing.value) return
// vModel.value.title = _title || ''
onStopEdit()
}
/** Stop editing view name, timeout makes sure that view navigation (click trigger) does not pick up before stop is done */
function onStopEdit() {
isStopped.value = true
isEditing.value = false
_title.value = ''
setTimeout(() => {
isStopped.value = false
}, 250)
}
watch(rightSidebarState, () => {
if (rightSidebarState.value === 'peekCloseEnd') {
isDropdownOpen.value = false
}
})
</script>
<template>
<a-menu-item
class="!min-h-8 !max-h-8 !mb-0.25 select-none group text-gray-700 !flex !items-center !mt-0 hover:(!bg-gray-100 !text-gray-900)"
:data-testid="`view-sidebar-view-${vModel.alias || vModel.title}`"
@dblclick.stop="onDblClick"
@click="onClick"
>
<div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-1" data-testid="view-item">
<div class="flex min-w-6" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`">
<LazyGeneralEmojiPicker
class="nc-table-icon"
:emoji="props.view?.meta?.icon"
size="small"
:clearable="true"
@emoji-selected="emits('selectIcon', $event)"
>
<template #default>
<GeneralViewIcon :meta="props.view" class="nc-view-icon"></GeneralViewIcon>
</template>
</LazyGeneralEmojiPicker>
</div>
<a-input
v-if="isEditing"
:ref="focusInput"
v-model:value="_title"
class="!bg-transparent !text-xs !border-0 !ring-0 !outline-transparent !border-transparent"
:class="{
'font-medium': activeView?.id === vModel.id,
}"
@blur="onRename"
@keydown.stop="onKeyDown($event)"
/>
<div
v-else
class="capitalize text-ellipsis overflow-hidden select-none w-full"
data-testid="sidebar-view-title"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ vModel.alias || vModel.title }}
</div>
<div class="flex-1" />
<template v-if="!isEditing && !isLocked && isUIAllowed('viewCreateOrEdit')">
<NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg">
<div
class="invisible !group-hover:visible"
:class="{
'!visible': isDropdownOpen,
}"
>
<NcButton
type="text"
size="xsmall"
class="nc-view-sidebar-node-context-btn !px-1 !hover:bg-gray-200"
@click.stop="isDropdownOpen = !isDropdownOpen"
>
<GeneralIcon icon="threeDotVertical" class="-mt-0.5" />
</NcButton>
</div>
<template #overlay>
<div
class="flex flex-col items-center min-w-27"
:data-testid="`view-sidebar-view-actions-${vModel.alias || vModel.title}`"
>
<NcButton
type="text"
size="small"
class="w-full !rounded-none !hover:bg-gray-200"
:centered="false"
@click.stop="onDblClick"
>
<div class="flex flex-row items-center gap-x-2 pl-2 text-xs">
<GeneralIcon icon="edit" />
Rename
</div>
</NcButton>
<NcButton
type="text"
size="small"
class="nc-view-copy-icon w-full !rounded-none !hover:bg-gray-200"
:centered="false"
@click.stop="onDuplicate"
>
<div class="flex flex-row items-center gap-x-2 pl-1.5 text-xs">
<GeneralIcon icon="copy" class="text-base" />
Duplicate
</div>
</NcButton>
<template v-if="!vModel.is_default">
<NcButton
type="text"
size="small"
class="nc-view-delete-icon w-full !hover:bg-gray-200 !rounded-none"
:centered="false"
@click.stop="onDelete"
>
<div class="flex flex-row items-center gap-x-2.25 pl-1.75 text-red-400 text-xs">
<GeneralIcon icon="delete" />
Delete
</div>
</NcButton>
</template>
</div>
</template>
</NcDropdown>
</template>
</div>
</a-menu-item>
</template>

281
packages/nc-gui/components/smartsheet/sidebar/index.vue

@ -1,281 +0,0 @@
<script setup lang="ts">
import type { ViewType, ViewTypes } from 'nocodb-sdk'
import {
ActiveViewInj,
MetaInj,
inject,
ref,
resolveComponent,
storeToRefs,
useCommandPalette,
useDialog,
useNuxtApp,
useRoles,
useRoute,
useRouter,
useViewsStore,
watch,
} from '#imports'
const { refreshCommandPalette } = useCommandPalette()
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const { activeTab } = storeToRefs(useTabs())
const viewsStore = useViewsStore()
const { loadViews } = viewsStore
const { isViewsLoading, views } = storeToRefs(viewsStore)
const { lastOpenedViewMap } = storeToRefs(useProject())
const { activeTable } = storeToRefs(useTablesStore())
const setLastOpenedViewId = (viewId?: string) => {
if (viewId && activeTab.value?.id) {
lastOpenedViewMap.value[activeTab.value?.id] = viewId
}
}
const { isUIAllowed } = useRoles()
const router = useRouter()
const route = useRoute()
const { $e } = useNuxtApp()
const { isRightSidebarOpen } = storeToRefs(useSidebarStore())
const tabBtnsContainerRef = ref<HTMLElement | null>(null)
/** Watch route param and change active view based on `viewTitle` */
watch(
[views, () => route.params.viewTitle],
([nextViews, viewTitle]) => {
const lastOpenedViewId = activeTab.value?.id && lastOpenedViewMap.value[activeTab.value?.id]
const lastOpenedView = nextViews.find((v) => v.id === lastOpenedViewId)
if (viewTitle) {
let view = nextViews.find((v) => v.title === viewTitle)
if (view) {
activeView.value = view
setLastOpenedViewId(activeView.value?.id)
} else {
/** search with view id and if found replace with title */
view = nextViews.find((v) => v.id === viewTitle)
if (view) {
router.replace({
params: {
viewTitle: view.id,
},
})
}
}
} else if (lastOpenedView) {
/** if active view is not found, set it to last opened view */
router.replace({
params: {
viewTitle: lastOpenedView.id,
},
})
} else {
if (nextViews?.length && activeView.value !== nextViews[0]) {
activeView.value = nextViews[0]
}
}
/** if active view is not found, set it to first view */
if (nextViews?.length && (!activeView.value || !nextViews.includes(activeView.value))) {
activeView.value = nextViews[0]
}
},
{ immediate: true },
)
/** Open delete modal */
function onOpenModal({
title = '',
type,
copyViewId,
groupingFieldColumnId,
}: {
title?: string
type: ViewTypes
copyViewId?: string
groupingFieldColumnId?: string
}) {
const isOpen = ref(true)
const { close } = useDialog(resolveComponent('DlgViewCreate'), {
'modelValue': isOpen,
title,
type,
meta,
'selectedViewId': copyViewId,
groupingFieldColumnId,
'views': views,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()
refreshCommandPalette()
await loadViews()
router.push({ params: { viewTitle: view.id || '' } })
$e('a:view:create', { view: view.type })
},
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
</script>
<template>
<div class="relative nc-view-sidebar flex flex-col border-l-1 border-gray-200 relative h-full w-full bg-white">
<template v-if="isViewsLoading">
<a-skeleton-input :active="true" class="!h-8 !rounded overflow-hidden ml-3 mr-3 mt-3.75 mb-3.75" />
</template>
<div
v-else
ref="tabBtnsContainerRef"
class="flex flex-row group py-1 mx-3.25 mt-1.25 mb-2.75 rounded-md gap-x-2 nc-view-sidebar-tab items-center justify-between"
>
<div class="flex text-gray-600 ml-1.75">Views</div>
<NcTooltip
placement="bottomLeft"
hide-on-click
class="flex opacity-0 group-hover:(opacity-100) transition-all duration-50"
:class="{
'!w-8 !opacity-100': !isRightSidebarOpen,
}"
>
<template #title>
{{
isRightSidebarOpen
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}`
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}`
}}
</template>
<NcButton
type="text"
size="small"
class="nc-sidebar-left-toggle-icon !text-gray-600 !hover:text-gray-800"
@click="isRightSidebarOpen = !isRightSidebarOpen"
>
<div class="flex items-center text-inherit">
<GeneralIcon
icon="doubleRightArrow"
class="duration-150 transition-all"
:class="{
'transform rotate-180': !isRightSidebarOpen,
}"
/>
</div>
</NcButton>
</NcTooltip>
</div>
<div class="flex-1 flex flex-col min-h-0">
<div class="flex flex-col h-full justify-between w-full">
<div class="flex flex-grow nc-scrollbar-md pr-1.75 mr-0.5">
<div v-if="isViewsLoading" class="flex flex-col w-full">
<div class="flex flex-row items-center w-full mt-1.5 ml-5 gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-1/2 !h-4 !rounded overflow-hidden" />
</div>
<div class="flex flex-row items-center w-full mt-4 ml-5 gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-1/2 !h-4 !rounded overflow-hidden" />
</div>
<div class="flex flex-row items-center w-full mt-4 ml-5 gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-1/2 !h-4 !rounded overflow-hidden" />
</div>
</div>
<LazySmartsheetSidebarMenuTop v-else :views="views" @open-modal="onOpenModal" @deleted="loadViews" />
</div>
<div v-if="isUIAllowed('viewCreateOrEdit')" class="flex flex-col">
<div class="!mb-3 w-full border-b-1 border-gray-200" />
<div v-if="!activeTable" class="flex flex-col pt-2 pb-5 px-6">
<a-skeleton-input :active="true" class="!w-3/5 !h-4 !rounded overflow-hidden" />
<div class="flex flex-row justify-between items-center w-full mt-4.75">
<div class="flex flex-row items-center flex-grow gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-3/5 !h-4 !rounded overflow-hidden" />
</div>
<div class="flex">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
</div>
</div>
<div class="flex flex-row justify-between items-center w-full mt-3.75">
<div class="flex flex-row items-center flex-grow gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-3/5 !h-4 !rounded overflow-hidden" />
</div>
<div class="flex">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
</div>
</div>
<div class="flex flex-row justify-between items-center w-full mt-3.75">
<div class="flex flex-row items-center flex-grow gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-3/5 !h-4 !rounded overflow-hidden" />
</div>
<div class="flex">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
</div>
</div>
<div class="flex flex-row justify-between items-center w-full mt-3.75">
<div class="flex flex-row items-center flex-grow gap-x-3">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
<a-skeleton-input :active="true" class="!w-3/5 !h-4 !rounded overflow-hidden" />
</div>
<div class="flex">
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" />
</div>
</div>
</div>
<LazySmartsheetSidebarMenuBottom v-else @open-modal="onOpenModal" />
</div>
</div>
</div>
</div>
</template>
<style scoped>
:deep(.ant-menu-title-content) {
@apply w-full;
}
:deep(.ant-layout-sider-children) {
@apply flex flex-col;
}
.tab {
@apply flex flex-row items-center h-7.5 justify-center w-1/2 py-1 bg-gray-100 rounded-md gap-x-1.5 text-gray-500 hover:text-black cursor-pointer transition-all duration-300 select-none;
}
.tab-icon {
@apply transition-all duration-300;
}
.tab .tab-title {
@apply min-w-0;
word-break: 'keep-all';
white-space: 'nowrap';
display: 'inline';
}
.active {
@apply bg-white shadow text-gray-700;
}
</style>

11
packages/nc-gui/components/tabs/Smartsheet.vue

@ -160,8 +160,7 @@ const onDrop = async (event: DragEvent) => {
<template> <template>
<div class="nc-container flex flex-col h-full" @drop="onDrop" @dragover.prevent> <div class="nc-container flex flex-col h-full" @drop="onDrop" @dragover.prevent>
<LazySmartsheetTopbar /> <LazySmartsheetTopbar />
<TabsSmartsheetResizable style="height: calc(100% - var(--topbar-height))"> <div style="height: calc(100% - var(--topbar-height))">
<template #content>
<div v-if="openedViewsTab === 'view'" class="flex flex-col h-full flex-1 min-w-0"> <div v-if="openedViewsTab === 'view'" class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar v-if="!isForm" /> <LazySmartsheetToolbar v-if="!isForm" />
<div class="flex flex-row w-full" style="height: calc(100% - var(--topbar-height))"> <div class="flex flex-row w-full" style="height: calc(100% - var(--topbar-height))">
@ -185,13 +184,7 @@ const onDrop = async (event: DragEvent) => {
</div> </div>
</div> </div>
<SmartsheetDetails v-else /> <SmartsheetDetails v-else />
</template> </div>
<template #sidebar>
<template v-if="!isPublic">
<LazySmartsheetSidebar />
</template>
</template>
</TabsSmartsheetResizable>
<LazySmartsheetExpandedFormDetached /> <LazySmartsheetExpandedFormDetached />
</div> </div>

209
packages/nc-gui/components/tabs/SmartsheetResizable.vue

@ -1,209 +0,0 @@
<script lang="ts" setup>
import { Pane, Splitpanes } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
const {
isRightSidebarOpen,
isLeftSidebarOpen,
leftSidebarWidthPercent,
rightSidebarSize: sideBarSize,
} = storeToRefs(useSidebarStore())
const wrapperRef = ref<HTMLDivElement>()
const splitpaneWrapperRef = ref()
const { rightSidebarState: sidebarState } = storeToRefs(useSidebarStore())
const contentSize = computed(() => 100 - sideBarSize.value.current)
const animationDuration = 250
const contentDomWidth = ref(window.innerWidth)
const sidebarWidth = computed(() => (sideBarSize.value.old * contentDomWidth.value) / 100)
const currentSidebarSize = computed({
get: () => sideBarSize.value.current,
set: (val) => {
sideBarSize.value.current = val
sideBarSize.value.old = val
},
})
watch(isRightSidebarOpen, () => {
sideBarSize.value.current = sideBarSize.value.old
if (isRightSidebarOpen.value) {
setTimeout(() => (sidebarState.value = 'openStart'), 0)
setTimeout(() => (sidebarState.value = 'openEnd'), animationDuration)
} else {
sideBarSize.value.old = sideBarSize.value.current
sidebarState.value = 'hiddenStart'
setTimeout(() => {
sideBarSize.value.current = 0
sidebarState.value = 'hiddenEnd'
}, animationDuration)
}
})
function handleMouseMove(e: MouseEvent) {
if (!wrapperRef.value) return
if (sidebarState.value === 'openEnd') return
const viewportWidth = window.innerWidth
if (e.clientX > viewportWidth - 14 && ['hiddenEnd', 'peekCloseEnd'].includes(sidebarState.value)) {
sidebarState.value = 'peekOpenStart'
setTimeout(() => {
sidebarState.value = 'peekOpenEnd'
}, animationDuration)
} else if (e.clientX < viewportWidth - (sidebarWidth.value + 10) && sidebarState.value === 'peekOpenEnd') {
sidebarState.value = 'peekCloseOpen'
setTimeout(() => {
sidebarState.value = 'peekCloseEnd'
}, animationDuration)
}
}
function onWindowResize() {
contentDomWidth.value = ((100 - leftSidebarWidthPercent.value) / 100) * window.innerWidth
}
onMounted(() => {
document.addEventListener('mousemove', handleMouseMove)
window.addEventListener('resize', onWindowResize)
})
onBeforeUnmount(() => {
document.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('resize', onWindowResize)
})
watch(
[isLeftSidebarOpen, leftSidebarWidthPercent],
() => {
if (isLeftSidebarOpen.value) {
contentDomWidth.value = ((100 - leftSidebarWidthPercent.value) / 100) * window.innerWidth
} else {
contentDomWidth.value = window.innerWidth
}
},
{
immediate: true,
},
)
</script>
<template>
<Splitpanes
ref="splitpaneWrapperRef"
class="nc-view-sidebar-content-resizable-wrapper w-full h-full"
:class="{
'hide-resize-bar': !isRightSidebarOpen || sidebarState === 'openStart',
}"
@resize="currentSidebarSize = $event[1].size"
>
<Pane :size="contentSize">
<slot name="content" />
</Pane>
<Pane min-size="15%" :size="currentSidebarSize" max-size="40%" class="nc-view-sidebar-splitpane relative !overflow-visible">
<div
ref="wrapperRef"
class="nc-view-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-32 absolute overflow-visible"
:class="{
'minimized-height': !isRightSidebarOpen,
'peek-sidebar': ['peekOpenEnd', 'peekCloseOpen'].includes(sidebarState),
'hide-sidebar': ['hiddenStart', 'hiddenEnd', 'peekCloseEnd'].includes(sidebarState),
}"
:style="{
width: sidebarState === 'hiddenEnd' ? '0px' : `${sidebarWidth}px`,
}"
>
<slot name="sidebar" />
</div>
</Pane>
</Splitpanes>
</template>
<style lang="scss">
.nc-view-sidebar-wrapper.minimized-height > * {
@apply pb-1 !(rounded-l-lg border-1 border-gray-200 shadow-lg);
height: 89.5%;
}
.nc-view-sidebar-wrapper > * {
transition: all 0.15s ease-in-out;
@apply z-10 absolute;
}
.nc-view-sidebar-wrapper.peek-sidebar {
> * {
@apply !opacity-100;
transform: translateX(-100%);
}
}
.nc-view-sidebar-wrapper.hide-sidebar {
@apply !min-w-0;
> * {
@apply opacity-0;
transform: translateX(100%);
}
}
/** Split pane CSS */
.nc-view-sidebar-content-resizable-wrapper > {
.splitpanes__splitter {
width: 0 !important;
position: relative;
overflow: visible;
}
.splitpanes__splitter:before {
@apply bg-transparent;
width: 1px;
content: '';
position: absolute;
left: -2px;
top: 0;
height: 100%;
z-index: 40;
}
.splitpanes__splitter:hover:before {
@apply bg-scrollbar;
z-index: 40;
width: 4px !important;
left: -2px;
}
.splitpanes--dragging .splitpanes__splitter:before {
@apply bg-scrollbar;
z-index: 40;
width: 10px !important;
left: -2px;
}
}
.splitpanes--dragging > .splitpanes__splitter::before {
@apply w-1 mr-0 bg-scrollbar;
z-index: 40;
width: 4px !important;
left: -2px;
}
.splitpanes--dragging {
cursor: col-resize;
}
.nc-view-sidebar-content-resizable-wrapper.hide-resize-bar > {
.splitpanes__splitter {
display: none !important;
background-color: transparent !important;
}
}
</style>
Loading…
Cancel
Save