diff --git a/packages/nc-gui/assets/style.scss b/packages/nc-gui/assets/style.scss index 3ba970b751..3997ab5763 100644 --- a/packages/nc-gui/assets/style.scss +++ b/packages/nc-gui/assets/style.scss @@ -79,6 +79,11 @@ main { width: 4px; height: 4px; } + + &::-webkit-scrollbar-track { + -webkit-border-radius: 10px; + border-radius: 10px; + } &::-webkit-scrollbar-track-piece { width: 0px; } @@ -86,11 +91,14 @@ main { @apply bg-transparent; } &::-webkit-scrollbar-thumb { + -webkit-border-radius: 10px; + border-radius: 10px; + width: 4px; - @apply bg-gray-200; + @apply bg-gray-300; } &::-webkit-scrollbar-thumb:hover { - @apply bg-gray-300; + @apply bg-gray-400; } } diff --git a/packages/nc-gui/components/dashboard/Sidebar.vue b/packages/nc-gui/components/dashboard/Sidebar.vue index fe8c5907bc..164c11e554 100644 --- a/packages/nc-gui/components/dashboard/Sidebar.vue +++ b/packages/nc-gui/components/dashboard/Sidebar.vue @@ -43,7 +43,7 @@ onUnmounted(() => {
+import type { ViewType } from 'nocodb-sdk' +import { ViewTypes } from 'nocodb-sdk' + +const { $e } = useNuxtApp() + +const router = useRouter() + +const { refreshCommandPalette } = useCommandPalette() +const viewsStore = useViewsStore() +const { views } = storeToRefs(viewsStore) +const { loadViews, navigateToView } = viewsStore + +const table = inject(SidebarTableInj)! +const project = inject(ProjectInj)! + +const isOpen = ref(false) + +function onOpenModal({ + title = '', + type, + copyViewId, + groupingFieldColumnId, +}: { + title?: string + type: ViewTypes + copyViewId?: string + groupingFieldColumnId?: string +}) { + isOpen.value = false + + const isDlgOpen = ref(true) + + const { close } = useDialog(resolveComponent('DlgViewCreate'), { + 'modelValue': isDlgOpen, + title, + type, + 'tableId': table.value.id, + 'selectedViewId': copyViewId, + groupingFieldColumnId, + 'views': views, + 'onUpdate:modelValue': closeDialog, + 'onCreated': async (view: ViewType) => { + closeDialog() + + refreshCommandPalette() + + await loadViews() + + navigateToView({ + view, + tableId: table.value.id!, + projectId: project.value.id!, + }) + + $e('a:view:create', { view: view.type }) + }, + }) + + function closeDialog() { + isOpen.value = false + + close(1000) + } +} + + + + + diff --git a/packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue b/packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue index 825826a7f6..3461474b6d 100644 --- a/packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue +++ b/packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue @@ -195,11 +195,7 @@ function openTableCreateDialog(baseIndex?: number | undefined) { const newTableDom = document.querySelector(`[data-table-id="${table.id}"]`) if (!newTableDom) return - // Verify that table node is not in the viewport - if (isElementInvisible(newTableDom)) { - // Scroll to the table node - newTableDom?.scrollIntoView({ behavior: 'smooth' }) - } + newTableDom?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) }, 1000) close(1000) diff --git a/packages/nc-gui/components/dashboard/TreeView/TableList.vue b/packages/nc-gui/components/dashboard/TreeView/TableList.vue index 6f8beef505..4920353c03 100644 --- a/packages/nc-gui/components/dashboard/TreeView/TableList.vue +++ b/packages/nc-gui/components/dashboard/TreeView/TableList.vue @@ -26,10 +26,6 @@ const tables = computed(() => projectTables.value.get(project.value.id!) ?? []) const { $api } = useNuxtApp() -const { openTable } = useTableNew({ - projectId: project.value.id!, -}) - const tablesById = computed(() => tables.value.reduce>((acc, table) => { acc[table.id!] = table @@ -153,17 +149,15 @@ const availableTables = computed(() => { v-for="table of availableTables" :key="table.id" v-e="['a:table:open']" - class="nc-tree-item text-sm cursor-pointer group" + class="nc-tree-item text-sm" :data-order="table.order" :data-id="table.id" - :data-testid="`tree-view-table-${table.title}`" :table="table" :project="project" :base-index="baseIndex" :data-title="table.title" :data-base-id="base?.id" :data-type="table.type" - @click="openTable(table)" >
diff --git a/packages/nc-gui/components/dashboard/TreeView/TableNode.vue b/packages/nc-gui/components/dashboard/TreeView/TableNode.vue index 9a4619e3b4..a356e29b38 100644 --- a/packages/nc-gui/components/dashboard/TreeView/TableNode.vue +++ b/packages/nc-gui/components/dashboard/TreeView/TableNode.vue @@ -20,6 +20,10 @@ const project = toRef(props, 'project') const table = toRef(props, 'table') const baseIndex = toRef(props, 'baseIndex') +const { openTable } = useTableNew({ + projectId: project.value.id!, +}) + const route = useRoute() const { isUIAllowed } = useRoles() @@ -34,9 +38,13 @@ useTableNew({ }) const projectRole = inject(ProjectRoleInj) +provide(SidebarTableInj, table) const { setMenuContext, openRenameTableDialog, duplicateTable } = inject(TreeViewInj)! +const { loadViews: _loadViews } = useViewsStore() +const { activeView } = storeToRefs(useViewsStore()) + // todo: temp const { projectTables } = storeToRefs(useTablesStore()) const tables = computed(() => projectTables.value.get(project.value.id!) ?? []) @@ -73,80 +81,128 @@ const { isSharedBase } = useProject() const canUserEditEmote = computed(() => { return isUIAllowed('tableIconEdit', { roles: projectRole?.value }) }) + +const isExpanded = ref(false) +const isLoading = ref(false) + +const onExpand = async () => { + if (isExpanded.value) { + isExpanded.value = false + return + } + + isLoading.value = true + try { + await _loadViews({ tableId: table.value.id, ignoreLoading: true }) + } catch (e) { + message.error(await extractSdkResponseErrorMsg(e)) + } finally { + isLoading.value = false + isExpanded.value = true + } +} + +watch( + () => activeView.value?.id, + () => { + if (!activeView.value) return + + if (activeView.value?.fk_model_id === table.value?.id) { + isExpanded.value = true + } + }, + { + immediate: true, + }, +) + +const isTableOpened = computed(() => { + return openedTableId.value === table.value?.id && activeView.value?.is_default +}) diff --git a/packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue b/packages/nc-gui/components/dashboard/TreeView/ViewsList.vue similarity index 65% rename from packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue rename to packages/nc-gui/components/dashboard/TreeView/ViewsList.vue index 94031e9ecf..6fcdf9d7a0 100644 --- a/packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue +++ b/packages/nc-gui/components/dashboard/TreeView/ViewsList.vue @@ -5,9 +5,8 @@ import type { SortableEvent } from 'sortablejs' import Sortable from 'sortablejs' import type { Menu as AntMenu } from 'ant-design-vue' import { - ActiveViewInj, + isDefaultBase as _isDefaultBase, extractSdkResponseErrorMsg, - inject, message, onMounted, parseProp, @@ -23,23 +22,32 @@ import { 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() - const emits = defineEmits() +const project = inject(ProjectInj)! +const table = inject(SidebarTableInj)! + +const { isUIAllowed } = useRoles() const { $e } = useNuxtApp() -const activeView = inject(ActiveViewInj, ref()) +const isDefaultBase = computed(() => { + const base = project.value?.bases?.find((b) => b.id === table.value.base_id) + if (!base) return false + + return _isDefaultBase(base) +}) + +const { viewsByTable, activeView } = storeToRefs(useViewsStore()) + +const { navigateToTable } = useTablesStore() + +const views = computed(() => viewsByTable.value.get(table.value.id!)?.filter((v) => !v.is_default) ?? []) const { api } = useApi() @@ -49,6 +57,8 @@ const { refreshCommandPalette } = useCommandPalette() const { addUndo, defineModelScope } = useUndoRedo() +const { navigateToView, loadViews } = useViewsStore() + /** Selected view(s) for menu */ const selected = ref([]) @@ -80,7 +90,7 @@ function validate(view: ViewType) { return 'View name is required' } - if (views.some((v) => v.title === view.title && v.id !== view.id)) { + if (views.value.some((v) => v.title === view.title && v.id !== view.id)) { return 'View name should be unique' } @@ -102,7 +112,7 @@ async function onSortEnd(evt: SortableEvent, undo = false) { dragging.value = false } - if (views.length < 2) return + if (views.value.length < 2) return const { newIndex = 0, oldIndex = 0 } = evt @@ -139,17 +149,17 @@ async function onSortEnd(evt: SortableEvent, undo = false) { const previousEl = children[newIndex - 1] const nextEl = children[newIndex + 1] - const currentItem = views.find((v) => v.id === evt.item.id) + const currentItem = views.value.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 + const previousItem = (previousEl ? views.value.find((v) => v.id === previousEl.id) : {}) as ViewType + const nextItem = (nextEl ? views.value.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) { + if (views.value.length - 1 === newIndex) { nextOrder = parseFloat(String(previousItem.order)) + 1 } else if (newIndex === 0) { nextOrder = parseFloat(String(nextItem.order)) / 2 @@ -183,24 +193,12 @@ 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: {} }) - }) - } + navigateToView({ + view, + tableId: table.value.id!, + projectId: project.value.id!, + hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id, + }) } /** Rename a view */ @@ -211,10 +209,11 @@ async function onRename(view: ViewType, originalTitle?: string, undo = false) { order: view.order, }) - await router.replace({ - params: { - viewTitle: view.id, - }, + navigateToView({ + view, + tableId: table.value.id!, + projectId: project.value.id!, + hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id, }) refreshCommandPalette() @@ -256,20 +255,22 @@ function openDeleteDialog(view: ViewType) { 'modelValue': isOpen, 'view': view, 'onUpdate:modelValue': closeDialog, - 'onDeleted': () => { + 'onDeleted': async () => { closeDialog() emits('deleted') refreshCommandPalette() - if (activeView.value === view) { - // return to the default view - router.replace({ - params: { - viewTitle: views[0].id, - }, + if (activeView.value?.id === view.id) { + navigateToTable({ + tableId: table.value.id!, + projectId: project.value.id!, }) } + + await loadViews({ + tableId: table.value.id!, + }) }, }) @@ -298,47 +299,90 @@ const setIcon = async (icon: string, view: ViewType) => { } } -const scrollViewNode = () => { - const activeViewDom = document.querySelector(`.nc-views-menu [data-view-id="${activeView.value?.id}"]`) as HTMLElement - if (!activeViewDom) return +function onOpenModal({ + title = '', + type, + copyViewId, + groupingFieldColumnId, +}: { + title?: string + type: ViewTypes + copyViewId?: string + groupingFieldColumnId?: string +}) { + const isOpen = ref(true) - if (isElementInvisible(activeViewDom)) { - // Scroll to the view node - activeViewDom?.scrollIntoView({ behavior: 'auto', inline: 'start' }) + const { close } = useDialog(resolveComponent('DlgViewCreate'), { + 'modelValue': isOpen, + title, + type, + 'tableId': table.value.id, + 'selectedViewId': copyViewId, + groupingFieldColumnId, + 'views': views, + 'onUpdate:modelValue': closeDialog, + 'onCreated': async (view: ViewType) => { + closeDialog() + + refreshCommandPalette() + + await loadViews() + + navigateToView({ + view, + tableId: table.value.id!, + projectId: project.value.id!, + hardReload: view.type === ViewTypes.FORM && selected.value[0] === view.id, + }) + + $e('a:view:create', { view: view.type }) + }, + }) + + function closeDialog() { + isOpen.value = false + + close(1000) } } - -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, - }, -) diff --git a/packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue b/packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue similarity index 70% rename from packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue rename to packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue index fbe9942c92..95415cda72 100644 --- a/packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue +++ b/packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue @@ -2,7 +2,17 @@ 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' +import { + IsLockedInj, + isDefaultBase as _isDefaultBase, + inject, + message, + onKeyStroke, + useDebounceFn, + useNuxtApp, + useRoles, + useVModel, +} from '#imports' interface Props { view: ViewType @@ -33,12 +43,23 @@ const { $e } = useNuxtApp() const { isUIAllowed } = useRoles() +const { activeViewTitleOrId } = storeToRefs(useViewsStore()) + +const project = inject(ProjectInj, ref()) + const activeView = inject(ActiveViewInj, ref()) const isLocked = inject(IsLockedInj, ref(false)) const { rightSidebarState } = storeToRefs(useSidebarStore()) +const isDefaultBase = computed(() => { + const base = project.value?.bases?.find((b) => b.id === vModel.value.base_id) + if (!base) return false + + return _isDefaultBase(base) +}) + const isDropdownOpen = ref(false) const isEditing = ref(false) @@ -175,16 +196,35 @@ watch(rightSidebarState, () => { isDropdownOpen.value = false } }) + +function onRef(el: HTMLElement) { + if (activeViewTitleOrId.value === vModel.value.id) { + nextTick(() => { + setTimeout(() => { + el?.scrollIntoView({ block: 'nearest', inline: 'nearest' }) + }, 1000) + }) + } +}