Browse Source

Merge pull request #6444 from nocodb/nc-feat/views-in-left-sidebar

Removed right view sidebar
pull/6447/head
Muhammed Mustafa 12 months ago committed by GitHub
parent
commit
d80a0346d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      packages/nc-gui/assets/style.scss
  2. 2
      packages/nc-gui/components/dashboard/Sidebar.vue
  3. 131
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  4. 15
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  5. 8
      packages/nc-gui/components/dashboard/TreeView/TableList.vue
  6. 280
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  7. 195
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  8. 127
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  9. 6
      packages/nc-gui/components/dashboard/TreeView/index.vue
  10. 4
      packages/nc-gui/components/dashboard/View.vue
  11. 140
      packages/nc-gui/components/dlg/ViewCreate.vue
  12. 2
      packages/nc-gui/components/general/DeleteModal.vue
  13. 8
      packages/nc-gui/components/nc/Select.vue
  14. 6
      packages/nc-gui/components/project/AllTables.vue
  15. 14
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  16. 151
      packages/nc-gui/components/smartsheet/sidebar/MenuBottom.vue
  17. 281
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  18. 10
      packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue
  19. 19
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  20. 45
      packages/nc-gui/components/tabs/Smartsheet.vue
  21. 209
      packages/nc-gui/components/tabs/SmartsheetResizable.vue
  22. 17
      packages/nc-gui/composables/useViewData.ts
  23. 1
      packages/nc-gui/context/index.ts
  24. 3
      packages/nc-gui/lang/en.json
  25. 1
      packages/nc-gui/lib/acl.ts
  26. 2
      packages/nc-gui/pages/index/[typeOrId]/[projectId]/index/index/index.vue
  27. 4
      packages/nc-gui/pages/index/[typeOrId]/view/[viewId].vue
  28. 4
      packages/nc-gui/store/tables.ts
  29. 70
      packages/nc-gui/store/views.ts
  30. 5
      packages/nc-gui/utils/baseUtils.ts
  31. 2
      tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts
  32. 61
      tests/playwright/pages/Dashboard/Sidebar/index.ts
  33. 24
      tests/playwright/pages/Dashboard/TreeView.ts
  34. 51
      tests/playwright/pages/Dashboard/ViewSidebar/index.ts
  35. 2
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  36. 26
      tests/playwright/pages/Dashboard/common/Topbar/Share.ts
  37. 9
      tests/playwright/pages/Dashboard/common/Topbar/index.ts
  38. 4
      tests/playwright/pages/Dashboard/index.ts
  39. 2
      tests/playwright/pages/SharedForm/index.ts
  40. 2
      tests/playwright/tests/db/columns/columnAttachments.spec.ts
  41. 4
      tests/playwright/tests/db/features/baseShare.spec.ts
  42. 4
      tests/playwright/tests/db/features/undo-redo.spec.ts
  43. 8
      tests/playwright/tests/db/general/toolbarOperations.spec.ts
  44. 20
      tests/playwright/tests/db/general/views.spec.ts
  45. 7
      tests/playwright/tests/db/views/viewForm.spec.ts
  46. 8
      tests/playwright/tests/db/views/viewKanban.spec.ts

16
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;
}
}
@ -583,3 +591,7 @@ input[type='number'] {
.ant-pagination .ant-pagination-item-link-icon {
@apply !block !py-1.5;
}
.nc-button.ant-btn.nc-sidebar-node-btn {
@apply opacity-0 group-hover:(opacity-100) text-gray-600 hover:(bg-gray-400 bg-opacity-20 text-gray-900) duration-100;
}

2
packages/nc-gui/components/dashboard/Sidebar.vue

@ -43,7 +43,7 @@ onUnmounted(() => {
</div>
<div
ref="treeViewDom"
class="flex flex-col nc-scrollbar-sm-dark flex-grow"
class="flex flex-col nc-scrollbar-dark-md flex-grow"
:class="{
'border-t-1': !isSharedBase,
'border-transparent': !isTreeViewOnScrollTop,

131
packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue

@ -0,0 +1,131 @@
<script setup lang="ts">
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)
}
}
</script>
<template>
<NcDropdown v-model:isOpen="isOpen" destroy-popup-on-hide @click.stop="isOpen = !isOpen">
<slot />
<template #overlay>
<NcMenu class="max-w-48">
<NcMenuItem @click="onOpenModal({ type: ViewTypes.GRID })">
<div class="item" data-testid="sidebar-view-create-grid">
<div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.GRID }" />
<div>Grid</div>
</div>
<GeneralIcon class="plus" icon="plus" />
</div>
</NcMenuItem>
<NcMenuItem @click="onOpenModal({ type: ViewTypes.FORM })">
<div class="item" data-testid="sidebar-view-create-form">
<div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.FORM }" />
<div>Form</div>
</div>
<GeneralIcon class="plus" icon="plus" />
</div>
</NcMenuItem>
<NcMenuItem @click="onOpenModal({ type: ViewTypes.GALLERY })">
<div class="item" data-testid="sidebar-view-create-gallery">
<div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.GALLERY }" />
<div>Gallery</div>
</div>
<GeneralIcon class="plus" icon="plus" />
</div>
</NcMenuItem>
<NcMenuItem data-testid="sidebar-view-create-kanban" @click="onOpenModal({ type: ViewTypes.KANBAN })">
<div class="item">
<div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.KANBAN }" />
<div>Kanban</div>
</div>
<GeneralIcon class="plus" icon="plus" />
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
</template>
<style lang="scss" scoped>
.item {
@apply flex flex-row items-center w-36 justify-between;
}
.item-inner {
@apply flex flex-row items-center gap-x-1.75;
}
.plus {
@apply text-brand-400;
}
</style>

15
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -191,11 +191,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)
@ -616,10 +612,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div>
</a-tooltip>
</div>
<div
v-if="isUIAllowed('tableCreate', { roles: projectRole })"
class="flex flex-row items-center gap-x-0.25 w-12.25"
>
<div class="flex flex-row items-center gap-x-0.25 w-12.25">
<NcDropdown
:visible="isBasesOptionsOpen[base!.id!]"
:trigger="['click']"
@ -749,10 +742,6 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
@apply !mx-0 !pl-8.75 !pr-0.5 !py-0.75 hover:bg-gray-200 !rounded-md;
}
:deep(.nc-button.ant-btn.nc-sidebar-node-btn) {
@apply opacity-0 group-hover:(opacity-100) text-gray-600 hover:(bg-gray-400 bg-opacity-20 text-gray-900) duration-100;
}
:deep(.ant-collapse-content-box) {
@apply !px-0 !pb-0 !pt-0.25;
}

8
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<Record<string, TableType>>((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)"
>
</TableNode>
</div>

280
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
})
</script>
<template>
<div
class="nc-tree-item text-sm cursor-pointer group select-none"
class="nc-tree-item nc-table-node-wrapper text-sm select-none w-full"
:data-order="table.order"
:data-id="table.id"
:data-testid="`tree-view-table-${table.title}`"
:data-table-id="table.id"
:class="[
// todo: table filter
// { hidden: !filteredTables?.includes(table), active: openedTableId === table.id },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,
{ active: openedTableId === table.id },
]"
:class="[`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`]"
:data-active="openedTableId === table.id"
>
<GeneralTooltip
class="pl-11 pr-0.75 mb-0.25 rounded-md h-7.1"
class="nc-tree-item-inner pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200"
:class="{
'hover:bg-gray-200': openedTableId !== table.id,
'pl-17.75': baseIndex !== 0,
'pl-12.25': baseIndex === 0,
'pl-12': baseIndex !== 0,
'pl-6.5': baseIndex === 0,
'!bg-primary-selected': isTableOpened,
}"
modifier-key="Alt"
>
<template #title>{{ table.table_name }}</template>
<div class="table-context flex items-center gap-1 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<div
class="flex items-center nc-table-icon"
:class="{
'pointer-events-none': !canUserEditEmote,
}"
@click.stop
>
<LazyGeneralEmojiPicker
:key="table.meta?.icon"
:emoji="table.meta?.icon"
size="small"
:readonly="!canUserEditEmote"
@emoji-selected="setIcon($event, table)"
<div
class="table-context flex items-center gap-1 h-full"
:data-testid="`nc-tbl-side-node-${table.title}`"
@contextmenu="setMenuContext('table', table)"
@click="openTable(table)"
>
<div class="flex flex-row h-full items-center">
<NcButton type="text" size="xxsmall" class="nc-sidebar-node-btn" @click.stop="onExpand">
<GeneralIcon
icon="triangleFill"
class="nc-sidebar-base-node-btns group-hover:visible invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 !text-gray-600 rotate-90 hover:bg-"
:class="{ '!rotate-180': isExpanded }"
/>
</NcButton>
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<div
class="flex items-center nc-table-icon"
:class="{
'pointer-events-none': !canUserEditEmote,
}"
@click.stop
>
<template #default>
<NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote">
<template #title>
{{ 'Change icon' }}
</template>
<MdiTable
v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: projectRole }),
'!text-black': openedTableId === table.id,
}"
/>
<MdiEye
v-else
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: projectRole }),
'!text-black': openedTableId === table.id,
}"
/>
</NcTooltip>
</template>
</LazyGeneralEmojiPicker>
<LazyGeneralEmojiPicker
:key="table.meta?.icon"
:emoji="table.meta?.icon"
size="small"
:readonly="!canUserEditEmote"
@emoji-selected="setIcon($event, table)"
>
<template #default>
<NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote">
<template #title>
{{ 'Change icon' }}
</template>
<MdiTable
v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: projectRole }),
'!text-black': openedTableId === table.id,
}"
/>
<MdiEye
v-else
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: projectRole }),
'!text-black': openedTableId === table.id,
}"
/>
</NcTooltip>
</template>
</LazyGeneralEmojiPicker>
</div>
</div>
</div>
<span
class="nc-tbl-title capitalize text-ellipsis overflow-hidden select-none"
:class="{
'text-black !font-semibold': openedTableId === table.id,
'text-black !font-medium': isTableOpened,
}"
:data-testid="`nc-tbl-title-${table.title}`"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
@ -155,58 +211,62 @@ const canUserEditEmote = computed(() => {
</span>
<div class="flex flex-grow h-full"></div>
<NcDropdown
v-if="
!isSharedBase &&
(isUIAllowed('tableRename', { roles: projectRole }) || isUIAllowed('tableDelete', { roles: projectRole }))
"
:trigger="['click']"
@click.stop
>
<MdiDotsHorizontal
class="min-w-5.75 min-h-5.75 mt-0.2 mr-0.25 px-0.5 transition-opacity opacity-0 group-hover:opacity-100 nc-tbl-context-menu outline-0 rounded-md hover:(bg-gray-500 bg-opacity-15 !text-black)"
:class="{
'!text-gray-600': openedTableId !== table.id,
'!text-black': openedTableId === table.id,
}"
/>
<template #overlay>
<NcMenu>
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: projectRole })"
:data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, project.bases[baseIndex].id)"
>
<GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }}
</NcMenuItem>
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate') &&
project.bases?.[baseIndex] &&
(project.bases[baseIndex].is_meta || project.bases[baseIndex].is_local)
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
>
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: projectRole })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="isTableDeleteDialogVisible = true"
>
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
<div class="flex flex-row items-center">
<NcDropdown
v-if="
!isSharedBase &&
(isUIAllowed('tableRename', { roles: projectRole }) || isUIAllowed('tableDelete', { roles: projectRole }))
"
:trigger="['click']"
class="nc-sidebar-node-btn"
@click.stop
>
<MdiDotsHorizontal
class="min-w-5.75 min-h-5.75 mt-0.2 mr-0.25 px-0.5 !text-gray-600 transition-opacity opacity-0 group-hover:opacity-100 nc-tbl-context-menu outline-0 rounded-md hover:(bg-gray-500 bg-opacity-15 !text-black)"
/>
<template #overlay>
<NcMenu>
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: projectRole })"
:data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, project.bases[baseIndex].id)"
>
<GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }}
</NcMenuItem>
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate') &&
project.bases?.[baseIndex] &&
(project.bases[baseIndex].is_meta || project.bases[baseIndex].is_local)
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
>
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: projectRole })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="isTableDeleteDialogVisible = true"
>
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
<DashboardTreeViewCreateViewBtn v-if="isUIAllowed('viewCreateOrEdit')">
<NcButton type="text" size="xxsmall" class="nc-create-view-btn nc-sidebar-node-btn">
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
</NcButton>
</DashboardTreeViewCreateViewBtn>
</div>
</div>
<DlgTableDelete
v-if="table.id && project?.id"
@ -215,24 +275,16 @@ const canUserEditEmote = computed(() => {
:project-id="project.id"
/>
</GeneralTooltip>
<DashboardTreeViewViewsList v-if="isExpanded" :table-id="table.id" :project-id="project.id" />
</div>
</template>
<style scoped lang="scss">
.nc-tree-item {
@apply relative cursor-pointer after:(pointer-events-none content-[''] rounded absolute top-0 left-0 w-full h-full right-0 !bg-current transition transition-opactity duration-100 opacity-0);
@apply relative after:(pointer-events-none content-[''] rounded absolute top-0 left-0 w-full h-full right-0 !bg-current transition duration-100 opacity-0);
}
.nc-tree-item svg {
@apply text-primary text-opacity-60;
}
.nc-tree-item.active {
@apply !bg-primary-selected rounded-md;
//@apply border-r-3 border-primary;
svg {
@apply !text-opacity-100;
}
}
</style>

195
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue → 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<Props>()
const emits = defineEmits<Emits>()
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<string[]>([])
@ -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,
},
)
</script>
<template>
<DashboardTreeViewCreateViewBtn
v-if="isUIAllowed('viewCreateOrEdit')"
:overlay-class-name="isDefaultBase ? '!left-18 !min-w-42' : '!left-25 !min-w-42'"
>
<NcButton
type="text"
size="xsmall"
class="!w-full !py-0 !h-7 !text-gray-500 !hover:(bg-transparent font-normal text-brand-500) !font-normal !text-sm"
:centered="false"
>
<GeneralIcon
icon="plus"
class="mr-2"
:class="{
'ml-18.75': isDefaultBase,
'ml-24.25': !isDefaultBase,
}"
/>
<span class="text-sm">New View</span>
</NcButton>
</DashboardTreeViewCreateViewBtn>
<a-menu
ref="menuRef"
:class="{ dragging }"
class="nc-views-menu flex flex-col !ml-3 w-full !border-r-0 !bg-inherit"
class="nc-views-menu flex flex-col 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
<DashboardTreeViewViewsNode
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="nc-view-item !rounded-md !px-0.75 !py-0.5 w-full transition-all ease-in duration-100"
:class="{
'bg-gray-200': isMarked === view.id,
'active': activeView?.id === view.id,
@ -346,19 +390,16 @@ watch(
}"
:data-view-id="view.id"
@change-view="changeView"
@open-modal="$emit('openModal', $event)"
@open-modal="onOpenModal"
@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">
.nc-views-menu {
@apply min-h-20 flex-grow;
.ghost,
.ghost > * {
@apply !pointer-events-none;
@ -378,12 +419,16 @@ watch(
@apply color-transition;
}
.ant-menu-title-content {
@apply !w-full;
}
.sortable-chosen {
@apply !bg-gray-100 bg-opacity-60;
@apply !bg-gray-200;
}
.active {
@apply bg-gray-200 bg-opacity-60 font-medium;
@apply !bg-primary-selected font-medium;
}
}
</style>

127
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue → 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)
})
}
}
</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)"
class="!min-h-7 !max-h-7 !mb-0.25 select-none group text-gray-700 !flex !items-center !mt-0 hover:(!bg-gray-200 !text-gray-900) cursor-pointer"
:class="{
'!pl-18': isDefaultBase,
'!pl-23.5': !isDefaultBase,
}"
: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
:ref="onRef"
v-e="['a:view:open', { view: vModel.type }]"
class="text-sm 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"
@ -203,7 +243,7 @@ watch(rightSidebarState, () => {
v-if="isEditing"
:ref="focusInput"
v-model:value="_title"
class="!bg-transparent !text-xs !border-0 !ring-0 !outline-transparent !border-transparent"
class="!bg-transparent !border-0 !ring-0 !outline-transparent !border-transparent"
:class="{
'font-medium': activeView?.id === vModel.id,
}"
@ -215,6 +255,9 @@ watch(rightSidebarState, () => {
v-else
class="capitalize text-ellipsis overflow-hidden select-none w-full"
data-testid="sidebar-view-title"
:class="{
'font-medium': activeView?.id === vModel.id,
}"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ vModel.alias || vModel.title }}
@ -224,66 +267,38 @@ watch(rightSidebarState, () => {
<template v-if="!isEditing && !isLocked && isUIAllowed('viewCreateOrEdit')">
<NcDropdown v-model:visible="isDropdownOpen" overlay-class-name="!rounded-lg">
<div
class="invisible !group-hover:visible"
<NcButton
type="text"
size="xxsmall"
class="nc-sidebar-node-btn invisible !group-hover:visible nc-sidebar-view-node-context-btn"
:class="{
'!visible': isDropdownOpen,
}"
@click.stop="isDropdownOpen = !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>
<GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
</NcButton>
<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>
<NcMenu class="min-w-27" :data-testid="`view-sidebar-view-actions-${vModel.alias || vModel.title}`">
<NcMenuItem @click.stop="onDblClick">
<GeneralIcon icon="edit" />
<div class="-ml-0.25">Rename</div>
</NcMenuItem>
<NcMenuItem @click.stop="onDuplicate">
<GeneralIcon icon="duplicate" class="nc-view-copy-icon" />
Duplicate
</NcMenuItem>
<NcDivider />
<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>
<NcMenuItem class="!text-red-500" l @click.stop="onDelete">
<GeneralIcon icon="delete" class="text-sm nc-view-delete-icon" />
<div class="-ml-0.25">Delete</div>
</NcMenuItem>
</template>
</div>
</NcMenu>
</template>
</NcDropdown>
</template>

6
packages/nc-gui/components/dashboard/TreeView/index.vue

@ -212,10 +212,8 @@ const scrollTableNode = () => {
const activeTableDom = document.querySelector(`.nc-treeview [data-table-id="${_activeTable.value?.id}"]`)
if (!activeTableDom) return
if (isElementInvisible(activeTableDom)) {
// Scroll to the table node
activeTableDom?.scrollIntoView({ behavior: 'smooth' })
}
// Scroll to the table node
activeTableDom?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
watch(

4
packages/nc-gui/components/dashboard/View.vue

@ -154,13 +154,13 @@ watch(route, () => {
.splitpanes__splitter:hover:before {
@apply bg-scrollbar;
width: 3px !important;
left: -3px;
left: 0px;
}
.splitpanes--dragging .splitpanes__splitter:before {
@apply bg-scrollbar;
width: 3px !important;
left: -3px;
left: 0px;
}
.splitpanes--dragging .splitpanes__splitter {

140
packages/nc-gui/components/dlg/ViewCreate.vue

@ -14,7 +14,7 @@ interface Props {
groupingFieldColumnId?: string
geoDataFieldColumnId?: string
views: ViewType[]
meta: TableType
tableId: string
}
interface Emits {
@ -31,10 +31,20 @@ interface Form {
fk_geo_data_col_id: string | null
}
const { views = [], meta, selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, ...props } = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
selectedViewId: undefined,
groupingFieldColumnId: undefined,
geoDataFieldColumnId: undefined,
})
const emits = defineEmits<Emits>()
const { getMeta } = useMetas()
const { views, selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, tableId } = toRefs(props)
const meta = ref<TableType | undefined>()
const inputEl = ref<ComponentPublicInstance>()
const formValidator = ref<typeof AntForm>()
@ -64,7 +74,7 @@ const viewNameRules = [
{
validator: (_: unknown, v: string) =>
new Promise((resolve, reject) => {
views.every((v1) => v1.title !== v) ? resolve(true) : reject(new Error(`View name should be unique`))
views.value.every((v1) => v1.title !== v) ? resolve(true) : reject(new Error(`View name should be unique`))
}),
message: 'View name should be unique',
},
@ -97,52 +107,13 @@ watch(
function init() {
form.title = `Untitled ${capitalize(typeAlias.value)}`
const repeatCount = views.filter((v) => v.title.startsWith(form.title)).length
const repeatCount = views.value.filter((v) => v.title.startsWith(form.title)).length
if (repeatCount) {
form.title = `${form.title} ${repeatCount}`
}
if (selectedViewId) {
form.copy_from_id = selectedViewId
}
// preset the grouping field column
if (props.type === ViewTypes.KANBAN) {
viewSelectFieldOptions.value = meta
.columns!.filter((el) => el.uidt === UITypes.SingleSelect)
.map((field) => {
return {
value: field.id,
label: field.title,
}
})
if (groupingFieldColumnId) {
// take from the one from copy view
form.fk_grp_col_id = groupingFieldColumnId
} else {
// take the first option
form.fk_grp_col_id = viewSelectFieldOptions.value?.[0]?.value as string
}
}
if (props.type === ViewTypes.MAP) {
viewSelectFieldOptions.value = meta
.columns!.filter((el) => el.uidt === UITypes.GeoData)
.map((field) => {
return {
value: field.id,
label: field.title,
}
})
if (geoDataFieldColumnId) {
// take from the one from copy view
form.fk_geo_data_col_id = geoDataFieldColumnId
} else {
// take the first option
form.fk_geo_data_col_id = viewSelectFieldOptions.value?.[0]?.value as string
}
if (selectedViewId.value) {
form.copy_from_id = selectedViewId?.value
}
nextTick(() => {
@ -165,9 +136,7 @@ async function onSubmit() {
}
if (isValid && form.type) {
const _meta = unref(meta)
if (!_meta || !_meta.id) return
if (!tableId.value) return
try {
let data: GridType | KanbanType | GalleryType | FormType | MapType | null = null
@ -176,19 +145,19 @@ async function onSubmit() {
switch (form.type) {
case ViewTypes.GRID:
data = await api.dbView.gridCreate(_meta.id, form)
data = await api.dbView.gridCreate(tableId.value, form)
break
case ViewTypes.GALLERY:
data = await api.dbView.galleryCreate(_meta.id, form)
data = await api.dbView.galleryCreate(tableId.value, form)
break
case ViewTypes.FORM:
data = await api.dbView.formCreate(_meta.id, form)
data = await api.dbView.formCreate(tableId.value, form)
break
case ViewTypes.KANBAN:
data = await api.dbView.kanbanCreate(_meta.id, form)
data = await api.dbView.kanbanCreate(tableId.value, form)
break
case ViewTypes.MAP:
data = await api.dbView.mapCreate(_meta.id, form)
data = await api.dbView.mapCreate(tableId.value, form)
}
if (data) {
@ -208,6 +177,60 @@ async function onSubmit() {
}, 500)
}
}
const isMetaLoading = ref(false)
onMounted(async () => {
if (props.type === ViewTypes.KANBAN || props.type === ViewTypes.MAP) {
isMetaLoading.value = true
try {
meta.value = (await getMeta(tableId.value))!
if (props.type === ViewTypes.MAP) {
viewSelectFieldOptions.value = meta
.value!.columns!.filter((el) => el.uidt === UITypes.GeoData)
.map((field) => {
return {
value: field.id,
label: field.title,
}
})
if (geoDataFieldColumnId.value) {
// take from the one from copy view
form.fk_geo_data_col_id = geoDataFieldColumnId.value
} else {
// take the first option
form.fk_geo_data_col_id = viewSelectFieldOptions.value?.[0]?.value as string
}
}
// preset the grouping field column
if (props.type === ViewTypes.KANBAN) {
viewSelectFieldOptions.value = meta.value
.columns!.filter((el) => el.uidt === UITypes.SingleSelect)
.map((field) => {
return {
value: field.id,
label: field.title,
}
})
if (groupingFieldColumnId.value) {
// take from the one from copy view
form.fk_grp_col_id = groupingFieldColumnId.value
} else {
// take the first option
form.fk_grp_col_id = viewSelectFieldOptions.value?.[0]?.value as string
}
}
} catch (e) {
console.error(e)
} finally {
isMetaLoading.value = false
}
}
})
</script>
<template>
@ -237,11 +260,11 @@ async function onSubmit() {
name="fk_grp_col_id"
:rules="groupingFieldColumnRules"
>
<a-select
<NcSelect
v-model:value="form.fk_grp_col_id"
class="w-full nc-kanban-grouping-field-select"
:options="viewSelectFieldOptions"
:disabled="groupingFieldColumnId"
:disabled="groupingFieldColumnId || isMetaLoading"
:loading="true"
placeholder="Select a Grouping Field"
not-found-content="No Single Select Field can be found. Please create one first."
/>
@ -252,11 +275,12 @@ async function onSubmit() {
name="fk_geo_data_col_id"
:rules="geoDataFieldColumnRules"
>
<a-select
<NcSelect
v-model:value="form.fk_geo_data_col_id"
class="w-full"
:options="viewSelectFieldOptions"
:disabled="geoDataFieldColumnId"
:disabled="groupingFieldColumnId || isMetaLoading"
:loading="isMetaLoading"
placeholder="Select a GeoData Field"
not-found-content="No GeoData Field can be found. Please create one first."
/>

2
packages/nc-gui/components/general/DeleteModal.vue

@ -40,7 +40,7 @@ onKeyStroke('Enter', () => {
</script>
<template>
<GeneralModal v-model:visible="visible" size="small">
<GeneralModal v-model:visible="visible" size="small" centered>
<div class="flex flex-col p-6">
<div class="flex flex-row pb-2 mb-4 font-medium text-lg border-b-1 border-gray-50 text-gray-800">
{{ $t('general.delete') }} {{ props.entityName }}

8
packages/nc-gui/components/nc/Select.vue

@ -8,6 +8,7 @@ const props = defineProps<{
filterOption?: (input: string, option: any) => boolean
dropdownMatchSelectWidth?: boolean
allowClear?: boolean
loading?: boolean
}>()
const emits = defineEmits(['update:value', 'change'])
@ -22,6 +23,8 @@ const filterOption = computed(() => props.filterOption)
const dropdownMatchSelectWidth = computed(() => props.dropdownMatchSelectWidth)
const loading = computed(() => props.loading)
const vModel = useVModel(props, 'value', emits)
const onChange = (value: string) => {
@ -39,10 +42,13 @@ const onChange = (value: string) => {
:filter-option="filterOption"
:dropdown-match-select-width="dropdownMatchSelectWidth"
:allow-clear="allowClear"
:loading="loading"
:disabled="loading"
@change="onChange"
>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-800 nc-select-expand-btn" />
<GeneralLoader v-if="loading" />
<GeneralIcon v-else icon="arrowDown" class="text-gray-800 nc-select-expand-btn" />
</template>
<slot />
</a-select>

6
packages/nc-gui/components/project/AllTables.vue

@ -57,11 +57,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)

14
packages/nc-gui/components/smartsheet/grid/GroupBy.vue

@ -30,6 +30,8 @@ const emits = defineEmits(['update:paginationData'])
const vGroup = useVModel(props, 'group', emits)
const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const _depth = props.depth ?? 0
@ -85,13 +87,19 @@ reloadViewDataHook?.on(reloadViewDataHandler)
watch(
[() => vGroup.value.key],
(n, o) => {
async (n, o) => {
if (n !== o) {
isViewDataLoading.value = true
isPaginationLoading.value = true
if (vGroup.value.nested) {
props.loadGroups({}, vGroup.value)
await props.loadGroups({}, vGroup.value)
} else {
props.loadGroupData(vGroup.value, true)
await props.loadGroupData(vGroup.value, true)
}
isViewDataLoading.value = false
isPaginationLoading.value = false
}
},
{ immediate: true },

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>

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>

10
packages/nc-gui/components/smartsheet/toolbar/FieldListAutoCompleteDropdown.vue

@ -100,14 +100,4 @@ if (!localValue.value && allowEmpty !== true) {
.ant-select-selection-search-input {
box-shadow: none !important;
}
::-webkit-scrollbar {
-webkit-appearance: none;
width: 7px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
}
</style>

19
packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue

@ -1,9 +1,7 @@
<script setup lang="ts">
import { ViewTypes } from 'nocodb-sdk'
import { ActiveViewInj, inject } from '#imports'
const selectedView = inject(ActiveViewInj)
const { openedViewsTab } = storeToRefs(useViewsStore())
const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
const { activeTable } = storeToRefs(useTablesStore())
</script>
@ -12,32 +10,33 @@ const { activeTable } = storeToRefs(useTablesStore())
<div
class="flex flex-row font-medium items-center border-gray-50 mt-0.5"
:class="{
'min-w-2/5 max-w-2/5': selectedView?.type !== ViewTypes.KANBAN,
'min-w-1/4 max-w-1/4': selectedView?.type === ViewTypes.KANBAN,
'min-w-2/5 max-w-2/5': activeView?.type !== ViewTypes.KANBAN,
'min-w-1/4 max-w-1/4': activeView?.type === ViewTypes.KANBAN,
}"
>
<LazyGeneralEmojiPicker :emoji="activeTable?.meta?.icon" readonly size="xsmall">
<template #default>
<MdiTable class="min-w-5 !text-gray-500" :class="{}" />
<MdiTable class="min-w-5 !text-gray-500" />
</template>
</LazyGeneralEmojiPicker>
<span
class="text-ellipsis overflow-hidden pl-1 text-gray-500 max-w-1/2"
class="text-ellipsis overflow-hidden pl-1 max-w-1/2 text-gray-500"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ activeTable?.title }}
</span>
<div class="px-2 text-gray-300">/</div>
<LazyGeneralEmojiPicker :emoji="selectedView?.meta?.icon" readonly size="xsmall">
<LazyGeneralEmojiPicker :emoji="activeView?.meta?.icon" readonly size="xsmall">
<template #default>
<GeneralViewIcon :meta="{ type: selectedView?.type }" class="min-w-4.5 text-lg flex" />
<GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" />
</template>
</LazyGeneralEmojiPicker>
<span class="truncate pl-1.25 text-gray-700 max-w-28/100">
{{ selectedView?.title }}
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span>
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view'" />
</div>
</template>

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

@ -160,38 +160,31 @@ const onDrop = async (event: DragEvent) => {
<template>
<div class="nc-container flex flex-col h-full" @drop="onDrop" @dragover.prevent>
<LazySmartsheetTopbar />
<TabsSmartsheetResizable 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">
<LazySmartsheetToolbar v-if="!isForm" />
<div class="flex flex-row w-full" style="height: calc(100% - var(--topbar-height))">
<Transition name="layout" mode="out-in">
<template v-if="meta">
<div class="flex flex-1 min-h-0 w-3/4">
<div v-if="activeView" class="h-full flex-1 min-w-0 min-h-0 bg-white">
<LazySmartsheetGrid v-if="isGrid" ref="grid" />
<div style="height: calc(100% - var(--topbar-height))">
<div v-if="openedViewsTab === 'view'" class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar v-if="!isForm" />
<div class="flex flex-row w-full" style="height: calc(100% - var(--topbar-height))">
<Transition name="layout" mode="out-in">
<template v-if="meta">
<div class="flex flex-1 min-h-0 w-3/4">
<div v-if="activeView" class="h-full flex-1 min-w-0 min-h-0 bg-white">
<LazySmartsheetGrid v-if="isGrid" ref="grid" />
<LazySmartsheetGallery v-else-if="isGallery" />
<LazySmartsheetGallery v-else-if="isGallery" />
<LazySmartsheetForm v-else-if="isForm && !$route.query.reload" />
<LazySmartsheetForm v-else-if="isForm && !$route.query.reload" />
<LazySmartsheetKanban v-else-if="isKanban" />
<LazySmartsheetKanban v-else-if="isKanban" />
<LazySmartsheetMap v-else-if="isMap" />
</div>
<LazySmartsheetMap v-else-if="isMap" />
</div>
</template>
</Transition>
</div>
</div>
</template>
</Transition>
</div>
<SmartsheetDetails v-else />
</template>
<template #sidebar>
<template v-if="!isPublic">
<LazySmartsheetSidebar />
</template>
</template>
</TabsSmartsheetResizable>
</div>
<SmartsheetDetails v-else />
</div>
<LazySmartsheetExpandedFormDetached />
</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>

17
packages/nc-gui/composables/useViewData.ts

@ -32,11 +32,16 @@ const formatData = (list: Record<string, any>[]) =>
}))
export function useViewData(
meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
_meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>,
viewMeta: Ref<ViewType | undefined> | ComputedRef<(ViewType & { id: string }) | undefined>,
where?: ComputedRef<string | undefined>,
) {
if (!meta) {
const { activeTableId, activeTable } = storeToRefs(useTablesStore())
const meta = computed(() => _meta.value || activeTable.value)
const metaId = computed(() => _meta.value?.id || activeTableId.value)
if (!meta.value) {
throw new Error('Table meta is not available')
}
@ -101,7 +106,7 @@ export function useViewData(
const { count } = await $api.dbViewRow.count(
NOCO,
project?.value?.id as string,
meta?.value?.id as string,
metaId.value as string,
viewMeta?.value?.id as string,
)
paginationData.value.totalRows = count
@ -150,7 +155,7 @@ export function useViewData(
aggCommentCount.value = await $api.utils.commentCount({
ids,
fk_model_id: meta.value?.id as string,
fk_model_id: metaId.value as string,
})
for (const row of formattedData.value) {
@ -160,9 +165,9 @@ export function useViewData(
}
async function loadData(params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) {
if ((!project?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic.value) return
if ((!project?.value?.id || !metaId.value || !viewMeta.value?.id) && !isPublic.value) return
const response = !isPublic.value
? await api.dbViewRow.list('noco', project.value.id!, meta.value!.id!, viewMeta.value!.id!, {
? await api.dbViewRow.list('noco', project.value.id!, metaId.value!, viewMeta.value!.id!, {
...queryParams.value,
...params,
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),

1
packages/nc-gui/context/index.ts

@ -43,6 +43,7 @@ export const ProjectStarredModeInj: InjectionKey<Ref<boolean>> = Symbol('project
export const ProjectInj: InjectionKey<Ref<NcProject>> = Symbol('project-injection')
export const ProjectIdInj: InjectionKey<Ref<string>> = Symbol('project-id-injection')
export const EditColumnInj: InjectionKey<Ref<boolean>> = Symbol('edit-column-injection')
export const SidebarTableInj: InjectionKey<Ref<TableType>> = Symbol('sidebar-table-injection')
export const TreeViewInj: InjectionKey<{
setMenuContext: (type: 'project' | 'base' | 'table' | 'main' | 'layout', value?: any) => void
duplicateTable: (table: TableType) => void

3
packages/nc-gui/lang/en.json

@ -281,7 +281,8 @@
"resetPasswordMenu": "Reset Password",
"tokens": "Tokens",
"userManagement": "User Management",
"licence": "Licence"
"licence": "Licence",
"defaultView": "Default View"
},
"labels": {
"searchProjects": "Search Projects",

1
packages/nc-gui/lib/acl.ts

@ -25,6 +25,7 @@ const rolePermissions = {
projectDelete: true,
projectDuplicate: true,
newUser: true,
viewCreateOrEdit: true,
},
},
[OrgUserRoles.VIEWER]: {

2
packages/nc-gui/pages/index/[typeOrId]/[projectId]/index/index/index.vue

@ -112,7 +112,7 @@ function openQuickImportDialog(type: QuickImportTypes, file: File) {
}
watch(
() => project.value.id,
() => project.value?.id,
() => {
if (project.value?.id && project.value.type === 'database') {
const { addTab } = useTabs()

4
packages/nc-gui/pages/index/[typeOrId]/view/[viewId].vue

@ -10,7 +10,7 @@ definePageMeta({
const route = useRoute()
const { loadSharedView } = useSharedView()
const { loadSharedView, meta } = useSharedView()
const { isViewDataLoading } = storeToRefs(useViewsStore())
const showPassword = ref(false)
@ -37,5 +37,5 @@ onMounted(async () => {
<LazySharedViewAskPassword v-model="showPassword" />
</div>
<LazySharedViewGrid v-else />
<LazySharedViewGrid v-else-if="meta" />
</template>

4
packages/nc-gui/store/tables.ts

@ -63,14 +63,10 @@ export const useTablesStore = defineStore('tablesStore', () => {
)
const loadProjectTables = async (projectId: string, force = false) => {
const projects = projectsStore.projects
if (!force && projectTables.value.get(projectId)) {
return
}
const workspaceProject = projects.get(projectId)
if (!workspaceProject) throw new Error('Project not found')
const existingTables = projectTables.value.get(projectId)
if (existingTables && !force) {
return

70
packages/nc-gui/store/views.ts

@ -1,4 +1,4 @@
import type { ViewType } from 'nocodb-sdk'
import { type ViewType } from 'nocodb-sdk'
import { acceptHMRUpdate, defineStore } from 'pinia'
import type { ViewPageType } from '~/lib'
@ -8,7 +8,18 @@ export const useViewsStore = defineStore('viewsStore', () => {
const router = useRouter()
const route = router.currentRoute
const views = ref<ViewType[]>([])
const tablesStore = useTablesStore()
const viewsByTable = ref<Map<string, ViewType[]>>(new Map())
const views = computed({
get: () => (tablesStore.activeTableId ? viewsByTable.value.get(tablesStore.activeTableId) : []) ?? [],
set: (value) => {
if (!tablesStore.activeTableId) return
if (!value) return viewsByTable.value.delete(tablesStore.activeTableId)
viewsByTable.value.set(tablesStore.activeTableId, value)
},
})
const isViewsLoading = ref(true)
const isViewDataLoading = ref(true)
const isPublic = computed(() => route.value.meta?.public)
@ -70,16 +81,20 @@ export const useViewsStore = defineStore('viewsStore', () => {
// Used for Grid View Pagination
const isPaginationLoading = ref(false)
const tablesStore = useTablesStore()
const loadViews = async ({ tableId, ignoreLoading }: { tableId?: string; ignoreLoading?: boolean } = {}) => {
tableId = tableId ?? tablesStore.activeTableId
if (tableId) {
if (!ignoreLoading) isViewsLoading.value = true
const loadViews = async () => {
if (tablesStore.activeTableId) {
isViewsLoading.value = true
const response = (await $api.dbView.list(tablesStore.activeTableId)).list as ViewType[]
const response = (await $api.dbView.list(tableId)).list as ViewType[]
if (response) {
views.value = response.sort((a, b) => a.order! - b.order!)
viewsByTable.value.set(
tableId,
response.sort((a, b) => a.order! - b.order!),
)
}
isViewsLoading.value = false
if (!ignoreLoading) isViewsLoading.value = false
}
}
@ -121,6 +136,40 @@ export const useViewsStore = defineStore('viewsStore', () => {
const isLockedView = computed(() => activeView.value?.lock_type === 'locked')
const navigateToView = async ({
view,
projectId,
tableId,
hardReload,
}: {
view: ViewType
projectId: string
tableId: string
hardReload?: boolean
}) => {
const routeName = 'index-typeOrId-projectId-index-index-viewId-viewTitle'
if (
router.currentRoute.value.query &&
router.currentRoute.value.query.page &&
router.currentRoute.value.query.page === 'fields'
) {
await router.push({
name: routeName,
params: { viewTitle: view.id || '', viewId: tableId, projectId },
query: router.currentRoute.value.query,
})
} else {
await router.push({ name: routeName, params: { viewTitle: view.id || '', viewId: tableId, projectId } })
}
if (hardReload) {
await router.replace({ name: routeName, query: { reload: 'true' }, params: { viewId: tableId, projectId } }).then(() => {
router.replace({ name: routeName, query: {}, params: { viewId: tableId, projectId } })
})
}
}
return {
isLockedView,
isViewsLoading,
@ -132,6 +181,9 @@ export const useViewsStore = defineStore('viewsStore', () => {
openedViewsTab,
onViewsTabChange,
sharedView,
viewsByTable,
activeViewTitleOrId,
navigateToView,
}
})

5
packages/nc-gui/utils/baseUtils.ts

@ -0,0 +1,5 @@
import type { BaseType } from 'nocodb-sdk'
const isDefaultBase = (base: BaseType) => base.is_meta
export { isDefaultBase }

2
tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts

@ -24,7 +24,7 @@ export class LinkRecord extends BasePage {
{
const childList = linkRecord.getByTestId(`nc-excluded-list-item`);
expect.poll(() => linkRecord.getByTestId(`nc-excluded-list-item`).count()).toBe(cardTitle.length);
await expect.poll(() => linkRecord.getByTestId(`nc-excluded-list-item`).count()).toBe(cardTitle.length);
for (let i = 0; i < cardTitle.length; i++) {
await childList.nth(i).locator('.nc-display-value').scrollIntoViewIfNeeded();
await childList.nth(i).locator('.nc-display-value').waitFor({ state: 'visible' });

61
tests/playwright/pages/Dashboard/Sidebar/index.ts

@ -1,5 +1,5 @@
import { expect, Locator } from '@playwright/test';
import { ProjectTypes } from 'nocodb-sdk';
import { ProjectTypes, ViewTypes } from 'nocodb-sdk';
import { DashboardPage } from '..';
import BasePage from '../../Base';
import { DocsSidebarPage } from './DocsSidebar';
@ -59,4 +59,63 @@ export class SidebarPage extends BasePage {
});
await this.dashboard.docs.pagesList.waitForOpen({ title });
}
async createView({ title, type }: { title: string; type: ViewTypes }) {
const createViewButtonOfActiveProject = this.dashboard
.get()
.locator('.nc-table-node-wrapper[data-active="true"] .nc-create-view-btn');
await createViewButtonOfActiveProject.scrollIntoViewIfNeeded();
await createViewButtonOfActiveProject.click();
// TODO: Find a better way to do it
let createViewTypeButton: Locator;
if (type === ViewTypes.GRID) {
createViewTypeButton = this.rootPage.getByTestId('sidebar-view-create-grid');
} else if (type === ViewTypes.FORM) {
createViewTypeButton = this.rootPage.getByTestId('sidebar-view-create-form');
} else if (type === ViewTypes.KANBAN) {
createViewTypeButton = this.rootPage.getByTestId('sidebar-view-create-kanban');
} else if (type === ViewTypes.GALLERY) {
createViewTypeButton = this.rootPage.getByTestId('sidebar-view-create-gallery');
}
await this.rootPage.waitForTimeout(750);
const allButtons = await createViewTypeButton.all();
for (const btn of allButtons) {
if (await btn.isVisible()) {
createViewTypeButton = btn;
break;
}
}
await createViewTypeButton.click({
force: true,
});
await this.rootPage.locator('input[id="form_item_title"]:visible').waitFor({ state: 'visible' });
await this.rootPage.locator('input[id="form_item_title"]:visible').fill(title);
const submitAction = () =>
this.rootPage.locator('.ant-modal-content').locator('button.ant-btn.ant-btn-primary').click();
await this.waitForResponse({
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/api/v1/db/meta/tables/',
uiAction: submitAction,
responseJsonMatcher: json => json.title === title,
});
// Todo: Wait for view to be rendered
await this.rootPage.waitForTimeout(1000);
}
async verifyCreateViewButtonVisibility({ isVisible }: { isVisible: boolean }) {
const createViewButtonOfActiveProject = this.dashboard
.get()
.locator('.nc-table-node-wrapper[data-active="true"] .nc-create-view-btn');
if (isVisible) {
await expect(createViewButtonOfActiveProject).toBeVisible();
} else {
await expect(createViewButtonOfActiveProject).toHaveCount(0);
}
}
}

24
tests/playwright/pages/Dashboard/TreeView.ts

@ -72,7 +72,7 @@ export class TreeViewPage extends BasePage {
async getTable({ index, tableTitle }: { index: number; tableTitle?: string }) {
if (tableTitle) {
return this.get().getByTestId(`tree-view-table-${tableTitle}`);
return this.get().getByTestId(`nc-tbl-side-node-${tableTitle}`);
}
return this.get().locator('.nc-tree-item').nth(index);
@ -95,18 +95,26 @@ export class TreeViewPage extends BasePage {
await this.rootPage.locator('.h-full > div > .nc-sidebar-left-toggle-icon').click();
}
await this.get().getByTestId(`tree-view-table-${title}`).waitFor({ state: 'visible' });
await this.get().getByTestId(`nc-tbl-title-${title}`).waitFor({ state: 'visible' });
if (networkResponse === true) {
await this.waitForResponse({
uiAction: () => this.get().getByTestId(`tree-view-table-${title}`).click(),
uiAction: () =>
this.get()
.getByTestId(`nc-tbl-title-${title}`)
.click({
position: {
x: 10,
y: 10,
},
}),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: `/api/v1/db/data/noco/`,
responseJsonMatcher: json => json.pageInfo,
});
await this.dashboard.waitForTabRender({ title, mode });
} else {
await this.get().getByTestId(`tree-view-table-${title}`).click();
await this.get().getByTestId(`nc-tbl-title-${title}`).click();
await this.rootPage.waitForTimeout(1000);
}
}
@ -148,13 +156,13 @@ export class TreeViewPage extends BasePage {
async verifyTable({ title, index, exists = true }: { title: string; index?: number; exists?: boolean }) {
if (exists) {
await expect(this.get().getByTestId(`tree-view-table-${title}`)).toHaveCount(1);
await expect(this.get().getByTestId(`nc-tbl-title-${title}`)).toHaveCount(1);
if (index) {
await expect(this.get().locator('.nc-tbl-title').nth(index)).toHaveText(title);
}
} else {
await expect(this.get().getByTestId(`tree-view-table-${title}`)).toHaveCount(0);
await expect(this.get().getByTestId(`nc-tbl-title-${title}`)).toHaveCount(0);
}
}
@ -206,7 +214,7 @@ export class TreeViewPage extends BasePage {
await this.dashboard
.get()
.locator(`[data-testid="tree-view-table-draggable-handle-${sourceTable}"]`)
.dragTo(this.get().locator(`[data-testid="tree-view-table-${destinationTable}"]`));
.dragTo(this.get().locator(`[data-testid="nc-tbl-title-${destinationTable}"]`));
}
async projectSettings({ title }: { title?: string }) {
@ -261,7 +269,7 @@ export class TreeViewPage extends BasePage {
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: `/api/v1/db/meta/duplicate/`,
});
await this.get().locator(`[data-testid="tree-view-table-${title} copy"]`).waitFor();
await this.get().locator(`[data-testid="nc-tbl-title-${title} copy"]`).waitFor();
}
async verifyTabIcon({ title, icon, iconDisplay }: { title: string; icon: string; iconDisplay?: string }) {

51
tests/playwright/pages/Dashboard/ViewSidebar/index.ts

@ -1,6 +1,7 @@
import { expect, Locator } from '@playwright/test';
import { DashboardPage } from '..';
import BasePage from '../../Base';
import { ViewTypes } from 'nocodb-sdk';
export class ViewSidebarPage extends BasePage {
readonly project: any;
@ -31,7 +32,7 @@ export class ViewSidebarPage extends BasePage {
}
get() {
return this.dashboard.get().locator('.nc-view-sidebar');
return this.dashboard.get().locator('.nc-table-node-wrapper[data-active="true"]');
}
async isVisible() {
@ -53,34 +54,22 @@ export class ViewSidebarPage extends BasePage {
await this.rootPage.goto(this.rootPage.url());
}
private async createView({ title, locator }: { title: string; locator: Locator }) {
await this.rootPage.waitForTimeout(1000);
await locator.click();
await this.rootPage.locator('input[id="form_item_title"]:visible').waitFor({ state: 'visible' });
await this.rootPage.locator('input[id="form_item_title"]:visible').fill(title);
const submitAction = () =>
this.rootPage.locator('.ant-modal-content').locator('button.ant-btn.ant-btn-primary').click({ force: true });
await this.waitForResponse({
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/api/v1/db/meta/tables/',
uiAction: submitAction,
responseJsonMatcher: json => json.title === title,
});
await this.verifyToast({ message: 'View created successfully' });
// Todo: Wait for view to be rendered
private async createView({ title, type }: { title: string; type: ViewTypes }) {
await this.rootPage.waitForTimeout(1000);
await this.dashboard.sidebar.createView({ title, type });
}
async createGalleryView({ title }: { title: string }) {
await this.createView({ title, locator: this.createGalleryButton });
await this.createView({ title, type: ViewTypes.GALLERY });
}
async createGridView({ title }: { title: string }) {
await this.createView({ title, locator: this.createGridButton });
await this.createView({ title, type: ViewTypes.GRID });
}
async createFormView({ title }: { title: string }) {
await this.createView({ title, locator: this.createFormButton });
await this.createView({ title, type: ViewTypes.FORM });
}
async openView({ title }: { title: string }) {
@ -90,13 +79,13 @@ export class ViewSidebarPage extends BasePage {
}
async createKanbanView({ title }: { title: string }) {
await this.createView({ title, locator: this.createKanbanButton });
await this.createView({ title, type: ViewTypes.KANBAN });
await this.rootPage.waitForTimeout(1500);
}
async createMapView({ title }: { title: string }) {
await this.createView({ title, locator: this.createMapButton });
await this.createView({ title, type: ViewTypes.MAP });
}
// Todo: Make selection better
@ -135,13 +124,15 @@ export class ViewSidebarPage extends BasePage {
await this.get().locator(`[data-testid="view-sidebar-view-${title}"]`).hover();
await this.get()
.locator(`[data-testid="view-sidebar-view-${title}"]`)
.locator('.nc-view-sidebar-node-context-btn')
.locator('.nc-sidebar-view-node-context-btn')
.click();
await this.rootPage
.locator(`[data-testid="view-sidebar-view-actions-${title}"]`)
.locator('.nc-view-delete-icon')
.click();
.click({
force: true,
});
await this.rootPage.locator('button:has-text("Delete View"):visible').click();
}
@ -157,13 +148,15 @@ export class ViewSidebarPage extends BasePage {
await this.get().locator(`[data-testid="view-sidebar-view-${title}"]`).hover();
await this.get()
.locator(`[data-testid="view-sidebar-view-${title}"]`)
.locator('.nc-view-sidebar-node-context-btn')
.locator('.nc-sidebar-view-node-context-btn')
.click();
await this.rootPage
.locator(`[data-testid="view-sidebar-view-actions-${title}"]`)
.locator('.nc-view-copy-icon')
.click();
.click({
force: true,
});
const submitAction = () =>
this.rootPage.locator('.ant-modal-content').locator('button:has-text("Create View"):visible').click();
await this.waitForResponse({
@ -198,11 +191,9 @@ export class ViewSidebarPage extends BasePage {
}
async validateRoleAccess(param: { role: string }) {
const count = param.role.toLowerCase() === 'creator' ? 1 : 0;
await expect(this.createGridButton).toHaveCount(count);
await expect(this.createGalleryButton).toHaveCount(count);
await expect(this.createFormButton).toHaveCount(count);
await expect(this.createKanbanButton).toHaveCount(count);
await this.dashboard.sidebar.verifyCreateViewButtonVisibility({
isVisible: param.role.toLowerCase() === 'creator',
});
// await this.openDeveloperTab({});
// await expect(this.erdButton).toHaveCount(1);

2
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -341,7 +341,7 @@ export class CellPageObject extends BasePage {
await this.rootPage.waitForSelector('.nc-modal-child-list:visible');
// verify child list count & contents
expect.poll(() => this.rootPage.locator('.ant-card:visible').count()).toBe(count);
await expect.poll(() => this.rootPage.locator('.ant-card:visible').count()).toBe(count);
// close child list
await this.rootPage.locator('.nc-modal-child-list').locator('.nc-close-btn').last().click();

26
tests/playwright/pages/Dashboard/common/Topbar/Share.ts

@ -51,7 +51,16 @@ export class TopbarSharePage extends BasePage {
}
async clickShareBasePublicAccess() {
await this.get().locator(`[data-testid="nc-share-base-sub-modal"]`).locator('.ant-switch').nth(0).click();
await this.get()
.locator(`[data-testid="nc-share-base-sub-modal"]`)
.locator('.ant-switch')
.nth(0)
.click({
position: {
x: 4,
y: 4,
},
});
}
async isSharedBasePublicAccessEnabled() {
@ -63,14 +72,25 @@ export class TopbarSharePage extends BasePage {
}
async clickShareBaseEditorAccess() {
await this.get().locator(`[data-testid="nc-share-base-sub-modal"]`).locator('.ant-switch').nth(1).click();
await this.rootPage.waitForTimeout(1000);
const shareBaseSwitch = this.get().locator(`[data-testid="nc-share-base-sub-modal"]`).locator('.ant-switch');
const count = await shareBaseSwitch.count();
await this.get()
.locator(`[data-testid="nc-share-base-sub-modal"]`)
.locator('.ant-switch')
.nth(count - 1)
.click({
position: { x: 4, y: 4 },
});
}
async isSharedBaseEditorAccessEnabled() {
return await this.get()
.locator(`[data-testid="nc-share-base-sub-modal"]`)
.locator('.ant-switch')
.nth(1)
.nth(0)
.isChecked();
}

9
tests/playwright/pages/Dashboard/common/Topbar/index.ts

@ -54,12 +54,13 @@ export class TopbarPage extends BasePage {
return await this.getClipboardText();
}
async getSharedBaseUrl({ role }: { role: string }) {
async getSharedBaseUrl({ role, enableSharedBase }: { role: string; enableSharedBase: boolean }) {
await this.clickShare();
if (!(await this.share.isSharedBasePublicAccessEnabled())) await this.share.clickShareBasePublicAccess();
if (role === 'editor' && !(await this.share.isSharedBaseEditorAccessEnabled())) {
if (enableSharedBase) await this.share.clickShareBasePublicAccess();
if (role === 'editor' && enableSharedBase) {
await this.share.clickShareBaseEditorAccess();
} else if (role === 'viewer' && (await this.share.isSharedBaseEditorAccessEnabled())) {
} else if (role === 'viewer' && !enableSharedBase) {
await this.share.clickShareBaseEditorAccess();
}
await this.share.clickCopyLink();

4
tests/playwright/pages/Dashboard/index.ts

@ -191,6 +191,10 @@ export class DashboardPage extends BasePage {
await this.sidebar.userMenu.click();
await this.rootPage.getByTestId('nc-sidebar-user-logout').waitFor({ state: 'visible' });
await this.sidebar.userMenu.clickLogout();
// TODO: Remove this
await this.rootPage.reload();
await this.rootPage.locator('[data-testid="nc-form-signin"]:visible').waitFor();
await new Promise(resolve => setTimeout(resolve, 150));
}

2
tests/playwright/pages/SharedForm/index.ts

@ -58,7 +58,7 @@ export class SharedFormPage extends BasePage {
{
const childList = linkRecord.locator(`.ant-card`);
expect.poll(() => linkRecord.locator(`.ant-card`).count()).toBe(cardTitle.length);
await expect.poll(() => linkRecord.locator(`.ant-card`).count()).toBe(cardTitle.length);
for (let i = 0; i < cardTitle.length; i++) {
expect(await childList.nth(i).textContent()).toContain(cardTitle[i]);
}

2
tests/playwright/tests/db/columns/columnAttachments.spec.ts

@ -51,7 +51,7 @@ test.describe('Attachment column', () => {
});
await dashboard.rootPage.waitForTimeout(500);
const sharedFormUrl = await dashboard.form.topbar.getSharedViewUrl();
await dashboard.viewSidebar.openView({ title: 'Country' });
await dashboard.treeView.openTable({ title: 'Country' });
// Verify attachment in shared form
const newPage = await context.newPage();

4
tests/playwright/tests/db/features/baseShare.spec.ts

@ -66,7 +66,7 @@ test.describe('Shared base', () => {
let url = '';
// share button visible only if a table is opened
await dashboard.treeView.openTable({ title: 'Country' });
url = await dashboard.grid.topbar.getSharedBaseUrl({ role: 'editor' });
url = await dashboard.grid.topbar.getSharedBaseUrl({ role: 'editor', enableSharedBase: true });
await dashboard.rootPage.waitForTimeout(2000);
// access shared base link
@ -85,7 +85,7 @@ test.describe('Shared base', () => {
// await dashboard.treeView.openProject({ title: context.project.title });
await dashboard.treeView.openTable({ title: 'Country' });
url = await dashboard.grid.topbar.getSharedBaseUrl({ role: 'viewer' });
url = await dashboard.grid.topbar.getSharedBaseUrl({ role: 'viewer', enableSharedBase: false });
await dashboard.rootPage.waitForTimeout(2000);
// access shared base link

4
tests/playwright/tests/db/features/undo-redo.spec.ts

@ -457,10 +457,10 @@ test.describe('Undo Redo - Table & view rename operations', () => {
break;
}
await dashboard.viewSidebar.renameView({ title: viewTypes[i], newTitle: 'newNameForTest' });
await dashboard.viewSidebar.verifyView({ title: 'newNameForTest', index: 1 });
await dashboard.viewSidebar.verifyView({ title: 'newNameForTest', index: 0 });
await new Promise(resolve => setTimeout(resolve, 100));
await undo({ page, dashboard });
await dashboard.viewSidebar.verifyView({ title: viewTypes[i], index: 1 });
await dashboard.viewSidebar.verifyView({ title: viewTypes[i], index: 0 });
await dashboard.viewSidebar.deleteView({ title: viewTypes[i] });
}
});

8
tests/playwright/tests/db/general/toolbarOperations.spec.ts

@ -512,8 +512,9 @@ test.describe('Toolbar operations (GRID)', () => {
await toolbar.clickGroupBy();
await dashboard.viewSidebar.createGridView({ title: 'Test' });
await dashboard.viewSidebar.openView({ title: 'Test' });
await dashboard.viewSidebar.openView({ title: 'Film' });
await dashboard.rootPage.waitForTimeout(500);
await dashboard.treeView.openTable({ title: 'Film' });
await dashboard.grid.groupPage.verifyGroupHeader({
indexMap: [0],
@ -529,13 +530,14 @@ test.describe('Toolbar operations (GRID)', () => {
test('Duplicate View and Verify GroupBy', async () => {
await dashboard.treeView.openTable({ title: 'Film' });
await dashboard.viewSidebar.createGridView({ title: 'Film Grid' });
// Open GroupBy Menu
await toolbar.clickGroupBy();
await toolbar.groupBy.add({ title: 'Length', ascending: false, locallySaved: false });
await toolbar.clickGroupBy();
await dashboard.viewSidebar.copyView({ title: 'Film' });
await dashboard.viewSidebar.copyView({ title: 'Film Grid' });
await dashboard.grid.groupPage.verifyGroupHeader({
indexMap: [0],

20
tests/playwright/tests/db/general/views.spec.ts

@ -21,29 +21,29 @@ test.describe('Views CRUD Operations', () => {
test('Create views, reorder and delete', async () => {
await dashboard.treeView.openTable({ title: 'City' });
await dashboard.viewSidebar.createGridView({ title: 'CityGrid' });
await dashboard.viewSidebar.verifyView({ title: 'CityGrid', index: 1 });
await dashboard.viewSidebar.verifyView({ title: 'CityGrid', index: 0 });
await dashboard.viewSidebar.renameView({
title: 'CityGrid',
newTitle: 'CityGrid2',
});
await dashboard.viewSidebar.verifyView({
title: 'CityGrid2',
index: 1,
index: 0,
});
await dashboard.viewSidebar.createFormView({ title: 'CityForm' });
await dashboard.viewSidebar.verifyView({ title: 'CityForm', index: 2 });
await dashboard.viewSidebar.verifyView({ title: 'CityForm', index: 1 });
await dashboard.viewSidebar.renameView({
title: 'CityForm',
newTitle: 'CityForm2',
});
await dashboard.viewSidebar.verifyView({
title: 'CityForm2',
index: 2,
index: 1,
});
await dashboard.viewSidebar.createGalleryView({ title: 'CityGallery' });
await dashboard.viewSidebar.verifyView({ title: 'CityGallery', index: 3 });
await dashboard.viewSidebar.verifyView({ title: 'CityGallery', index: 2 });
await dashboard.viewSidebar.renameView({
title: 'CityGallery',
newTitle: 'CityGallery2',
@ -51,7 +51,7 @@ test.describe('Views CRUD Operations', () => {
await dashboard.viewSidebar.verifyView({
title: 'CityGallery2',
index: 3,
index: 2,
});
await dashboard.viewSidebar.changeViewIcon({
@ -77,14 +77,14 @@ test.describe('Views CRUD Operations', () => {
await dashboard.viewSidebar.deleteView({ title: 'CityForm2' });
await dashboard.viewSidebar.verifyViewNotPresent({
title: 'CityForm2',
index: 2,
index: 1,
});
// fix index after enabling reorder test
await dashboard.viewSidebar.deleteView({ title: 'CityGallery2' });
await dashboard.viewSidebar.verifyViewNotPresent({
title: 'CityGallery2',
index: 1,
index: 0,
});
});
@ -109,7 +109,7 @@ test.describe('Views CRUD Operations', () => {
await dashboard.rootPage.waitForTimeout(1000);
await toolbar.searchData.verify('City-CityGrid');
await dashboard.viewSidebar.openView({ title: 'City' });
await dashboard.treeView.openTable({ title: 'City' });
await dashboard.rootPage.waitForTimeout(1000);
await toolbar.searchData.verify('City-City');
@ -121,7 +121,7 @@ test.describe('Views CRUD Operations', () => {
await toolbar.searchData.get().fill('Actor-ActorGrid');
await toolbar.searchData.verify('Actor-ActorGrid');
await dashboard.viewSidebar.openView({ title: 'Actor' });
await dashboard.treeView.openTable({ title: 'Actor' });
await dashboard.rootPage.waitForTimeout(1000);
await toolbar.searchData.verify('');

7
tests/playwright/tests/db/views/viewForm.spec.ts

@ -31,7 +31,7 @@ test.describe('Form view', () => {
await dashboard.treeView.openTable({ title: 'Country' });
await dashboard.viewSidebar.createFormView({ title: 'CountryForm' });
await dashboard.viewSidebar.verifyView({ title: 'CountryForm', index: 1 });
await dashboard.viewSidebar.verifyView({ title: 'CountryForm', index: 0 });
// verify form-view fields order
await form.verifyFormViewFieldsOrder({
@ -92,7 +92,7 @@ test.describe('Form view', () => {
await dashboard.treeView.openTable({ title: 'Country' });
await dashboard.viewSidebar.createFormView({ title: 'CountryForm' });
await dashboard.viewSidebar.verifyView({ title: 'CountryForm', index: 1 });
await dashboard.viewSidebar.verifyView({ title: 'CountryForm', index: 0 });
await form.configureHeader({
title: 'Country',
@ -385,7 +385,6 @@ test.describe('Form view with LTAR', () => {
await dashboard.rootPage.waitForTimeout(500);
await dashboard.treeView.openTable({ title: 'Country' });
await dashboard.viewSidebar.openView({ title: 'Country' });
await dashboard.grid.cell.verify({
index: 3,
@ -492,7 +491,7 @@ test.describe('Form view', () => {
// kludge- reload
await dashboard.rootPage.reload();
await dashboard.viewSidebar.openView({ title: 'selectBased' });
await dashboard.treeView.openTable({ title: 'selectBased' });
await dashboard.rootPage.waitForTimeout(2000);

8
tests/playwright/tests/db/views/viewKanban.spec.ts

@ -61,7 +61,7 @@ test.describe('View', () => {
});
await dashboard.viewSidebar.verifyView({
title: 'Film Kanban',
index: 1,
index: 0,
});
// configure stack-by field
@ -202,7 +202,7 @@ test.describe('View', () => {
});
await dashboard.viewSidebar.verifyView({
title: 'Film Kanban',
index: 1,
index: 0,
});
await toolbar.sort.add({
@ -227,7 +227,7 @@ test.describe('View', () => {
await dashboard.viewSidebar.copyView({ title: 'Film Kanban' });
await dashboard.viewSidebar.verifyView({
title: 'Untitled Kanban',
index: 2,
index: 1,
});
const kanban = dashboard.kanban;
await kanban.verifyStackCount({ count: 6 });
@ -328,7 +328,7 @@ test.describe('View', () => {
});
await dashboard.viewSidebar.verifyView({
title: 'Film Kanban',
index: 1,
index: 0,
});
// Share view

Loading…
Cancel
Save