Browse Source

fix(nc-gui): Fixed Sidebar projectnode ctx menu

pull/6370/head
Muhammed Mustafa 10 months ago
parent
commit
a81abcf7c8
  1. 4
      packages/nc-gui/assets/nc-icons/star-remove.svg
  2. 5
      packages/nc-gui/assets/nc-icons/star.svg
  3. 2
      packages/nc-gui/components/cell/GeoData.vue
  4. 18
      packages/nc-gui/components/dashboard/Sidebar.vue
  5. 2
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  6. 2
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  7. 61
      packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue
  8. 244
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  9. 42
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  10. 111
      packages/nc-gui/components/dashboard/TreeView/index.vue
  11. 190
      packages/nc-gui/components/dashboard/View.vue
  12. 2
      packages/nc-gui/components/general/EmojiPicker.vue
  13. 2
      packages/nc-gui/components/general/Loader.vue
  14. 19
      packages/nc-gui/components/nc/Button.vue
  15. 2
      packages/nc-gui/components/nc/Divider.vue
  16. 2
      packages/nc-gui/components/nc/Dropdown.vue
  17. 6
      packages/nc-gui/components/nc/Menu.vue
  18. 14
      packages/nc-gui/components/nc/MenuItem.vue
  19. 43
      packages/nc-gui/components/nc/SubMenu.vue
  20. 2
      packages/nc-gui/components/smartsheet/sidebar/toolbar/Webhook.vue
  21. 12
      packages/nc-gui/components/workspace/CreateProjectDlg.vue
  22. 2
      packages/nc-gui/components/workspace/EmptyPlaceholder.vue
  23. 1
      packages/nc-gui/context/index.ts
  24. 3
      packages/nc-gui/lang/en.json
  25. 192
      packages/nc-gui/layouts/dashboard.vue
  26. 60
      packages/nc-gui/pages/index.vue
  27. 3
      packages/nc-gui/store/projects.ts
  28. 7
      packages/nc-gui/utils/iconUtils.ts
  29. 19
      tests/playwright/pages/Dashboard/TreeView.ts
  30. 2
      tests/playwright/pages/Dashboard/common/LeftSidebar/index.ts
  31. 4
      tests/playwright/pages/Dashboard/index.ts
  32. 2
      tests/playwright/pages/SharedForm/index.ts
  33. 4
      tests/playwright/pages/WorkspacePage/ContainerPage.ts
  34. 2
      tests/playwright/setup/index.ts
  35. 7
      tests/playwright/tests/db/columns/columnAttachments.spec.ts

4
packages/nc-gui/assets/nc-icons/star-remove.svg

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99992 1.33334L10.0599 5.50668L14.6666 6.18001L11.3333 9.42668L12.1199 14.0133L7.99992 11.8467L3.87992 14.0133L4.66659 9.42668L1.33325 6.18001L5.93992 5.50668L7.99992 1.33334Z" stroke="#4A5268" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 1L1 15" stroke="#4A5268" stroke-width="1.33" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 463 B

5
packages/nc-gui/assets/nc-icons/star.svg

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="star">
<path id="Vector" d="M7.99992 1.33333L10.0599 5.50666L14.6666 6.18L11.3333 9.42666L12.1199 14.0133L7.99992 11.8467L3.87992 14.0133L4.66659 9.42666L1.33325 6.18L5.93992 5.50666L7.99992 1.33333Z" stroke="#374151" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 405 B

2
packages/nc-gui/components/cell/GeoData.vue

@ -83,7 +83,7 @@ const openInOSM = () => {
</script>
<template>
<a-dropdown :is="isExpanded ? AModal : 'div'" v-model:visible="isExpanded" trigger="click">
<a-dropdown :is="isExpanded ? AModal : 'div'" v-model:visible="isExpanded" :trigger="['click']">
<div
v-if="!isLocationSet"
class="group cursor-pointer flex gap-1 items-center mx-auto max-w-64 justify-center active:(ring ring-accent ring-opacity-100) rounded border-1 p-1 shadow-sm hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"

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

@ -52,24 +52,6 @@ onUnmounted(() => {
'pt-0.25': isSharedBase,
}"
>
<div v-if="!isSharedBase" class="flex flex-row w-full justify-between items-center my-1.5 pl-4 pr-1.75">
<template v-if="!isWorkspaceLoading">
<div class="text-gray-500 font-medium">{{ $t('objects.projects') }}</div>
<WorkspaceCreateProjectBtn
v-model:is-open="isCreateProjectOpen"
modal
type="text"
size="xxsmall"
class="!hover:bg-gray-200 !hover-text-gray-800 !text-gray-600"
:centered="true"
data-testid="nc-sidebar-create-project-btn-small"
>
<GeneralIcon icon="plus" class="text-lg leading-6" style="-webkit-text-stroke: 0.2px" />
</WorkspaceCreateProjectBtn>
</template>
<a-skeleton-input v-else :active="true" class="mt-0.5 !w-40 !h-4 !rounded overflow-hidden" />
</div>
<LazyDashboardTreeView v-if="!isWorkspaceLoading" />
</div>
<div v-if="!isSharedBase" style="height: var(--sidebar-bottom-height)">

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

@ -41,7 +41,7 @@ const navigateToSettings = () => {
</div>
</template>
<template v-else-if="!isSharedBase">
<div class="flex flex-col p-1 gap-y-0.5 mt-0.25">
<div class="flex flex-col p-1 gap-y-0.5 mt-0.25 mb-0.5">
<DashboardSidebarTopSectionHeader />
<NcButton

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

@ -105,7 +105,7 @@ onMounted(() => {
</a>
<template v-if="!appInfo.ee">
<NcDivider />
<a-popover key="language" class="lang-menu !py-0" placement="rightBottom">
<a-popover key="language" class="lang-menu !py-1.5" placement="rightBottom">
<NcMenuItem>
<GeneralIcon icon="translate" class="group-hover:text-black nc-language ml-0.25 menu-icon" />
{{ $t('labels.language') }}

61
packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue

@ -57,61 +57,46 @@ function openQuickImportDialog(type: string) {
</script>
<template>
<a-menu-divider class="my-0" />
<!-- Quick Import From -->
<a-sub-menu class="py-0">
<NcSubMenu class="py-0">
<template #title>
<div class="nc-project-menu-item group">
<GeneralIcon icon="download" class="-ml-0.25" />
<div class="-ml-0.5">
{{ $t('title.quickImportFrom') }}
</div>
<MaterialSymbolsChevronRightRounded class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" />
</div>
<GeneralIcon icon="download" />
{{ $t('title.quickImportFrom') }}
</template>
<template #expandIcon></template>
<a-menu-item
<NcMenuItem
v-if="isUIAllowed('airtableImport', false, projectRole)"
key="quick-import-airtable"
@click="openAirtableImportDialog(base.id)"
>
<div class="color-transition nc-project-menu-item group">
<GeneralIcon icon="airtable" class="group-hover:text-black" />
Airtable
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('csvImport', false, projectRole)" key="quick-import-csv" @click="openQuickImportDialog('csv')">
<div class="color-transition nc-project-menu-item group">
<GeneralIcon icon="csv" class="group-hover:text-black" />
CSV file
</div>
</a-menu-item>
<a-menu-item
<GeneralIcon icon="airtable" class="max-w-3.75 group-hover:text-black" />
<div class="ml-0.5">Airtable</div>
</NcMenuItem>
<NcMenuItem v-if="isUIAllowed('csvImport', false, projectRole)" key="quick-import-csv" @click="openQuickImportDialog('csv')">
<GeneralIcon icon="csv" class="w-4 group-hover:text-black" />
CSV file
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('jsonImport', false, projectRole)"
key="quick-import-json"
@click="openQuickImportDialog('json')"
>
<div class="color-transition nc-project-menu-item group">
<GeneralIcon icon="code" class="group-hover:text-black" />
JSON file
</div>
</a-menu-item>
<GeneralIcon icon="code" class="w-4 group-hover:text-black" />
JSON file
</NcMenuItem>
<a-menu-item
<NcMenuItem
v-if="isUIAllowed('excelImport', false, projectRole)"
key="quick-import-excel"
@click="openQuickImportDialog('excel')"
>
<div class="color-transition nc-project-menu-item group">
<GeneralIcon icon="excel" class="group-hover:text-black" />
Microsoft Excel
</div>
</a-menu-item>
</a-sub-menu>
<GeneralIcon icon="excel" class="max-w-4 group-hover:text-black" />
Microsoft Excel
</NcMenuItem>
</NcSubMenu>
</template>

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

@ -363,7 +363,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</script>
<template>
<a-dropdown :trigger="['contextmenu']" overlay-class-name="nc-dropdown-tree-view-context-menu">
<NcDropdown :trigger="['contextmenu']" overlay-class-name="nc-dropdown-tree-view-context-menu">
<div
class="mx-1 nc-project-sub-menu rounded-md"
:class="{ active: project.isExpanded }"
@ -378,27 +378,31 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
'hover:bg-gray-200': !(activeProjectId === project.id && projectViewOpen),
}"
:data-testid="`nc-sidebar-project-title-${project.title}`"
class="project-title-node h-7.25 flex-grow rounded-md group flex items-center w-full"
class="project-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1"
>
<div
class="nc-sidebar-expand ml-0.75 min-h-5.75 min-w-5.75 px-1.5 text-gray-500 hover:(hover:bg-gray-500 hover:bg-opacity-15 !text-black) rounded-md relative"
<NcButton
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand ml-0.75"
@click="onProjectClick(project, true, true)"
>
<PhTriangleFill
<GeneralIcon
icon="triangleFill"
class="absolute top-2.25 left-2 group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90"
:class="{ '!rotate-180': project.isExpanded, '!visible': isOptionsOpen }"
/>
</div>
</NcButton>
<div class="flex items-center mr-1" @click="onProjectClick(project)">
<div class="flex items-center select-none w-6 h-full">
<a-spin
v-if="project.isLoading"
class="nc-sidebar-icon !flex !flex-row !items-center !my-0.5 !mx-1.5 w-8"
class="!ml-1.25 !flex !flex-row !items-center !my-0.5 w-8"
:indicator="indicator"
/>
<LazyGeneralEmojiPicker
v-else
:key="project.meta?.icon"
:emoji="project.meta?.icon"
:readonly="true"
@ -434,15 +438,19 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</span>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(project)"></div>
<a-dropdown v-if="isUIAllowed('tableCreate', false, projectRole)" v-model:visible="isOptionsOpen" trigger="click">
<MdiDotsHorizontal
class="min-w-5.75 min-h-5.75 px-0.5 py-0.5 mr-0.25 !ring-0 focus:!ring-0 !focus:border-0 !focus:outline-0 opacity-0 group-hover:(opacity-100) hover:text-black text-gray-600 rounded-md hover:(bg-gray-500 bg-opacity-15)"
<NcDropdown v-if="isUIAllowed('tableCreate', false, projectRole)" v-model:visible="isOptionsOpen" :trigger="['click']">
<NcButton
class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isOptionsOpen }"
data-testid="nc-sidebar-context-menu"
type="text"
size="xxsmall"
@click.stop
/>
>
<GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
</NcButton>
<template #overlay>
<a-menu
<NcMenu
class="nc-scrollbar-md"
:style="{
maxHeight: '70vh',
@ -451,88 +459,80 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
@click="isOptionsOpen = false"
>
<template v-if="!isSharedBase">
<a-menu-item @click="enableEditMode">
<div class="nc-project-menu-item group">
<GeneralIcon icon="edit" class="group-hover:text-black" />
{{ $t('general.rename') }}
</div>
</a-menu-item>
<NcMenuItem @click="enableEditMode">
<GeneralIcon icon="edit" class="group-hover:text-black" />
{{ $t('general.rename') }}
</NcMenuItem>
<!-- Copy Project Info -->
<a-menu-item v-if="!isEeUI" key="copy">
<div v-e="['c:navbar:user:copy-proj-info']" class="nc-project-menu-item group" @click.stop="copyProjectInfo">
<GeneralIcon icon="copy" class="group-hover:text-black" />
{{ $t('activity.account.projInfo') }}
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('duplicateProject', true, projectRole)" @click="duplicateProject(project)">
<div class="nc-menu-item-wrapper">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</div>
</a-menu-item>
<NcMenuItem v-if="!isEeUI" key="copy" v-e="['c:navbar:user:copy-proj-info']" @click.stop="copyProjectInfo">
<GeneralIcon icon="copy" class="group-hover:text-black" />
{{ $t('activity.account.projInfo') }}
</NcMenuItem>
<NcMenuItem v-if="isUIAllowed('duplicateProject', true, projectRole)" @click="duplicateProject(project)">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</NcMenuItem>
<!-- ERD View -->
<a-menu-item key="erd" @click="openProjectErdView(project)">
<div class="nc-project-menu-item group">
<GeneralIcon icon="erd" />
Relations
</div>
</a-menu-item>
<NcMenuItem key="erd" @click="openProjectErdView(project)">
<GeneralIcon icon="erd" />
Relations
</NcMenuItem>
<!-- Swagger: Rest APIs -->
<a-menu-item key="api">
<div
v-if="isUIAllowed('apiDocs')"
v-e="['e:api-docs']"
class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${project.id}/swagger`, appInfo.ncSiteUrl)"
>
<GeneralIcon icon="snippet" class="group-hover:text-black" />
{{ $t('activity.account.swagger') }}
</div>
</a-menu-item>
<NcMenuItem
v-if="isUIAllowed('apiDocs')"
key="api"
v-e="['e:api-docs']"
@click.stop="openLink(`/api/v1/db/meta/projects/${project.id}/swagger`, appInfo.ncSiteUrl)"
>
<GeneralIcon icon="snippet" class="group-hover:text-black" />
{{ $t('activity.account.swagger') }}
</NcMenuItem>
</template>
<!-- Team & Settings -->
<a-menu-item key="teamAndSettings">
<div
v-if="isUIAllowed('settings')"
v-e="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth', undefined, project.id)"
>
<GeneralIcon icon="settings" class="group-hover:text-black" />
{{ $t('activity.settings') }}
</div>
</a-menu-item>
<NcMenuItem
v-if="isUIAllowed('settings')"
key="teamAndSettings"
v-e="['c:navdraw:project-settings']"
class="nc-sidebar-project-project-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, project.id)"
>
<GeneralIcon icon="settings" class="group-hover:text-black" />
{{ $t('activity.settings') }}
</NcMenuItem>
<template v-if="project.bases && project.bases[0]">
<NcDivider />
<DashboardTreeViewBaseOptions v-model:project="project" :base="project.bases[0]" />
<a-menu-divider />
<NcDivider />
</template>
<a-menu-item v-if="isUIAllowed('projectDelete', false, projectRole)" @click="isProjectDeleteDialogVisible = true">
<div class="nc-project-menu-item group text-red-500">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</div>
</a-menu-item>
</a-menu>
<NcMenuItem
v-if="isUIAllowed('projectDelete', false, projectRole)"
class="!text-red-500 !hover:bg-red-50"
@click="isProjectDeleteDialogVisible = true"
>
<GeneralIcon icon="delete" class="w-4" />
{{ $t('general.delete') }}
</NcMenuItem>
</NcMenu>
</template>
</a-dropdown>
</NcDropdown>
<div
<NcButton
v-if="isUIAllowed('tableCreate', false, projectRole)"
class="min-h-5.75 min-w-5.75 mr-1 flex flex-row items-center justify-center gap-x-2 cursor-pointer hover:(text-black) text-gray-600 text-sm invisible !group-hover:visible rounded-md hover:(bg-gray-500 bg-opacity-15)"
class="nc-sidebar-node-btn"
size="xxsmall"
type="text"
data-testid="nc-sidebar-add-project-entity"
:class="{ '!text-black !visible': isAddNewProjectChildEntityLoading, '!visible': isOptionsOpen }"
:loading="isAddNewProjectChildEntityLoading"
@click.stop="addNewProjectChildEntity"
>
<div v-if="isAddNewProjectChildEntityLoading" class="flex flex-row items-center">
<a-spin class="!flex !flex-row !items-center !my-0.5" :indicator="indicator" />
</div>
<MdiPlus v-else class="min-w-5 min-h-5 py-0.25" />
</div>
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
</NcButton>
</div>
</div>
@ -565,7 +565,8 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
>
<template #expandIcon="{ isActive }">
<div class="flex flex-row items-center -mt-2">
<PhTriangleFill
<GeneralIcon
icon="triangleFill"
class="nc-sidebar-base-node-btns -mt-0.75 invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 text-gray-500 rotate-90"
:class="{ '!rotate-180': isActive }"
/>
@ -573,7 +574,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</template>
<a-collapse-panel :key="`collapse-${base.id}`">
<template #header>
<div class="min-w-20 w-full flex flex-row">
<div class="min-w-20 w-full flex flex-row group">
<div
v-if="baseIndex === 0"
class="base-context flex items-center gap-2 text-gray-800"
@ -606,18 +607,22 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
v-if="isUIAllowed('tableCreate', false, projectRole)"
class="flex flex-row items-center gap-x-0.25 w-12.25"
>
<a-dropdown
<NcDropdown
:visible="isBasesOptionsOpen[base!.id!]"
trigger="click"
:trigger="['click']"
@update:visible="isBasesOptionsOpen[base!.id!] = $event"
>
<MdiDotsHorizontal
class="min-w-6 min-h-6 mt-0.15 invisible nc-sidebar-base-node-btns !ring-0 focus:!ring-0 !focus:border-0 !focus:outline-0 hover:text-black py-0.25 px-0.5 rounded-md text-gray-600 hover:(bg-gray-400 bg-opacity-20)"
<NcButton
class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isBasesOptionsOpen[base!.id!] }"
type="text"
size="xxsmall"
@click.stop="isBasesOptionsOpen[base!.id!] = !isBasesOptionsOpen[base!.id!]"
/>
>
<GeneralIcon icon="threeDotHorizontal" class="text-xl w-4.75" />
</NcButton>
<template #overlay>
<a-menu
<NcMenu
class="nc-scrollbar-md"
:style="{
maxHeight: '70vh',
@ -626,33 +631,28 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
@click="isBasesOptionsOpen[base!.id!] = false"
>
<!-- ERD View -->
<a-menu-item key="erd" @click="openErdView(base)">
<div class="nc-project-menu-item group">
<GeneralIcon icon="erd" />
Relations
</div>
</a-menu-item>
<NcMenuItem key="erd" @click="openErdView(base)">
<GeneralIcon icon="erd" />
Relations
</NcMenuItem>
<DashboardTreeViewBaseOptions v-model:project="project" :base="base" />
</a-menu>
</NcMenu>
</template>
</a-dropdown>
</NcDropdown>
<div
<NcButton
v-if="isUIAllowed('tableCreate', false, projectRole)"
class="flex invisible nc-sidebar-base-node-btns !focus:outline-0 text-gray-600 hover:text-black px-0.35 rounded-md hover:(bg-gray-500 bg-opacity-15) min-h-6 mt-0.15 min-w-6"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn"
@click.stop="openTableCreateDialog(baseIndex)"
>
<component :is="iconMap.plus" class="text-inherit mt-0.25 h-5.5 w-5.5 py-0.5 !focus:outline-0" />
</div>
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
</NcButton>
</div>
</div>
</template>
<!-- <AddNewTableNode
:project="project"
:base-index="baseIndex"
@open-table-create-dialog="openTableCreateDialog()"
/> -->
<div
ref="menuRefs"
:key="`sortable-${base.id}-${base.id && base.id in keys ? keys[base.id] : '0'}`"
@ -670,47 +670,47 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div>
</div>
<template v-if="!isSharedBase" #overlay>
<a-menu class="!py-0 rounded text-sm">
<NcMenu class="!py-0 rounded text-sm">
<template v-if="contextMenuTarget.type === 'project' && project.type === 'database'"></template>
<template v-else-if="contextMenuTarget.type === 'base'"></template>
<template v-else-if="contextMenuTarget.type === 'table'">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(contextMenuTarget.value, true)">
<div class="nc-project-menu-item">
<NcMenuItem v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(contextMenuTarget.value, true)">
<div class="nc-project-option-item">
<GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }}
</div>
</a-menu-item>
</NcMenuItem>
<a-menu-item
<NcMenuItem
v-if="isUIAllowed('table-duplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)"
@click="duplicateTable(contextMenuTarget.value)"
>
<div class="nc-project-menu-item">
<div class="nc-project-option-item">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</div>
</a-menu-item>
</NcMenuItem>
<a-menu-item v-if="isUIAllowed('table-delete')" @click="isTableDeleteDialogVisible = true">
<div class="nc-project-menu-item text-red-600">
<NcMenuItem v-if="isUIAllowed('table-delete')" @click="isTableDeleteDialogVisible = true">
<div class="nc-project-option-item text-red-600">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</div>
</a-menu-item>
</NcMenuItem>
</template>
<template v-else>
<a-menu-item @click="reloadTables">
<div class="nc-project-menu-item">
<NcMenuItem @click="reloadTables">
<div class="nc-project-option-item">
{{ $t('general.reload') }}
</div>
</a-menu-item>
</NcMenuItem>
</template>
</a-menu>
</NcMenu>
</template>
</a-dropdown>
</NcDropdown>
<DlgTableDelete
v-if="contextMenuTarget.value?.id && project?.id"
v-model:visible="isTableDeleteDialogVisible"
@ -727,19 +727,19 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</template>
<style lang="scss" scoped>
.nc-sidebar-icon {
@apply ml-0.5 mr-1;
:deep(.ant-collapse-header) {
@apply !mx-0 !pl-8.75 !pr-0.5 !py-0.75 hover:bg-gray-200 !rounded-md;
}
:deep(.ant-collapse-header) {
@apply !mx-0 !pl-8.75 !pr-1 !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-header:hover .nc-sidebar-base-node-btns) {
@apply visible;
:deep(.ant-collapse-content-box) {
@apply !px-0 !pb-0 !pt-0.25;
}
:deep(.ant-dropdown-menu-submenu-title) {
@apply !py-0;
:deep(.ant-collapse-header:hover .nc-sidebar-base-node-btns) {
@apply visible;
}
</style>

42
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -148,13 +148,14 @@ const canUserEditEmote = computed(() => {
:class="{
'text-black !font-semibold': openedTableId === table.id,
}"
:data-testid="`nc-tbl-title-${table.title}`"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ table.title }}
</span>
<div class="flex flex-grow h-full"></div>
<a-dropdown
<NcDropdown
v-if="
!isSharedBase && (isUIAllowed('table-rename', false, projectRole) || isUIAllowed('table-delete', false, projectRole))
"
@ -170,44 +171,41 @@ const canUserEditEmote = computed(() => {
/>
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item
<NcMenu>
<NcMenuItem
v-if="isUIAllowed('table-rename', false, projectRole)"
:data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, project.bases[baseIndex].id)"
>
<div class="nc-project-menu-item" :data-testid="`sidebar-table-rename-${table.title}`">
<GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }}
</div>
</a-menu-item>
<GeneralIcon icon="edit" class="text-gray-700" />
{{ $t('general.rename') }}
</NcMenuItem>
<a-menu-item
<NcMenuItem
v-if="
isUIAllowed('table-duplicate') &&
project.bases?.[baseIndex] &&
(project.bases[baseIndex].is_meta || project.bases[baseIndex].is_local)
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
>
<div class="nc-project-menu-item" :data-testid="`sidebar-table-duplicate-${table.title}`">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</div>
</a-menu-item>
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</NcMenuItem>
<a-menu-item
<NcMenuItem
v-if="isUIAllowed('table-delete', false, projectRole)"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="isTableDeleteDialogVisible = true"
>
<div class="nc-project-menu-item text-red-600">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</div>
</a-menu-item>
</a-menu>
<GeneralIcon icon="delete" />
{{ $t('general.delete') }}
</NcMenuItem>
</NcMenu>
</template>
</a-dropdown>
</NcDropdown>
</div>
<DlgTableDelete
v-if="table.id && project?.id"

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

@ -252,6 +252,7 @@ watch(
<template>
<div class="nc-treeview-container flex flex-col justify-between select-none">
<div class="text-gray-500 font-medium pl-3.5 mb-1">{{ $t('objects.projects') }}</div>
<div mode="inline" class="nc-treeview pb-0.5 flex-grow min-h-50 overflow-x-hidden">
<template v-if="projectsList?.length">
<ProjectWrapper v-for="project of projectsList" :key="project.id" :project-role="project.project_role" :project="project">
@ -265,112 +266,4 @@ watch(
</div>
</template>
<style scoped lang="scss">
.nc-treeview-footer-item {
@apply cursor-pointer px-4 py-2 flex items-center hover:bg-gray-200/20 text-xs text-current;
}
:deep(.nc-filter-input input::placeholder) {
@apply !text-xs;
}
:deep(.ant-dropdown-menu-title-content) {
@apply !p-2;
}
:deep(.ant-input-group-addon:last-child) {
@apply top-[-0.5px];
}
.nc-treeview-container {
.ghost,
.ghost > * {
@apply !pointer-events-none;
}
& .dragging {
.nc-icon {
@apply !hidden;
}
.nc-view-icon {
@apply !block;
}
}
.ant-menu-item:not(.sortable-chosen) {
@apply color-transition hover:!bg-transparent;
}
.sortable-chosen {
@apply !bg-primary bg-opacity-25 text-primary;
}
}
.nc-tree-item:hover {
@apply text-primary after:(!opacity-5);
}
:deep(.nc-filter-input) {
.ant-input {
@apply pr-6 !border-0;
}
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply !mx-0;
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-accent ring-opacity-100);
}
:deep(.ant-dropdown-menu-title-content) {
@apply !p-0;
}
:deep(.ant-collapse-content-box) {
@apply !p-0;
}
:deep(.ant-collapse-header) {
@apply !border-0;
}
:deep(.ant-menu-sub.ant-menu-inline .ant-menu-item-group-title) {
@apply !py-0;
}
:deep(.nc-project-sub-menu .ant-menu-submenu-title) {
@apply !pr-1 !pl-3;
}
:deep(.ant-menu-inline .ant-menu-submenu-title) {
@apply !h-28px;
}
:deep(.nc-project-sub-menu.active) {
}
.nc-create-project-btn {
@apply px-2;
:deep(.ant-btn) {
@apply w-full !text-center justify-center h-auto rounded-lg py-2 px-4 border-gray-100 bg-white;
& > div {
@apply !justify-center;
}
}
}
</style>
<style scoped lang="scss"></style>

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

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

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

@ -78,7 +78,7 @@ const showClearButton = computed(() => {
</script>
<template>
<a-dropdown v-model:visible="isOpen" trigger="click" :disabled="readonly">
<a-dropdown v-model:visible="isOpen" :trigger="['click']" :disabled="readonly">
<div
class="flex flex-row justify-center items-center select-none rounded-md"
:class="{

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

@ -22,7 +22,7 @@ function getFontSize() {
}
const indicator = h(LoadingOutlined, {
class: `!${getFontSize()} flex flex-row items-center ${props.loaderClass || '!text-gray-400'}}`,
class: `!${getFontSize()} flex flex-row items-center !bg-inherit !hover:bg-inherit !text-inherit ${props.loaderClass}}`,
spin: true,
})
</script>

19
packages/nc-gui/components/nc/Button.vue

@ -95,10 +95,19 @@ useEventListener(NcButton, 'mousedown', () => {
'justify-start': !props.centered,
}"
>
<GeneralLoader v-if="loading" size="medium" class="flex !text-white" loader-class="!text-white" />
<GeneralLoader
v-if="loading"
size="medium"
class="flex !bg-inherit"
:class="{
'!text-white': type === 'primary' || type === 'danger',
'!text-gray-800': type !== 'primary' && type !== 'danger',
}"
/>
<slot v-else name="icon" />
<div
v-if="!(size === 'xxsmall' && loading)"
class="flex flex-row items-center"
:class="{
'font-medium': type === 'primary' || type === 'danger',
@ -113,6 +122,10 @@ useEventListener(NcButton, 'mousedown', () => {
</template>
<style lang="scss">
.ant-btn:before {
display: none !important;
}
.nc-button {
> .ant-btn-loading-icon {
display: none !important;
@ -145,7 +158,7 @@ useEventListener(NcButton, 'mousedown', () => {
}
.nc-button.ant-btn.xxsmall {
@apply p-0 h-6 min-w-6 rounded-md;
@apply p-0 h-5.75 min-w-5.75 rounded-md;
}
.nc-button.ant-btn[disabled] {
@ -188,7 +201,7 @@ useEventListener(NcButton, 'mousedown', () => {
.nc-button.ant-btn-text {
box-shadow: none;
@apply bg-transparent border-0 text-gray-700 hover:bg-gray-100;
@apply bg-transparent border-0 text-gray-700 hover:text-gray-900 hover:bg-gray-100;
&:focus {
box-shadow: none;

2
packages/nc-gui/components/nc/Divider.vue

@ -4,6 +4,6 @@
<style lang="scss">
.nc-divider.ant-divider {
@apply !my-1;
@apply my-1.25;
}
</style>

2
packages/nc-gui/components/nc/Dropdown.vue

@ -25,7 +25,7 @@ const overlayClassName = toRef(props, 'overlayClassName')
const autoClose = computed(() => props.autoClose)
const overlayClassNameComputed = computed(() => {
let className = 'nc-dropdown bg-white rounded-lg border-1 border-gray-100 shadow-md overflow-hidden'
let className = 'nc-dropdown bg-white rounded-lg border-1 border-gray-100 shadow-lg shadow-gray-200'
if (overlayClassName.value) {
className += ` ${overlayClassName.value}`
}

6
packages/nc-gui/components/nc/Menu.vue

@ -14,6 +14,10 @@ const selectable = computed(() => props.selectable ?? false)
<style lang="scss">
.nc-menu {
@apply bg-white !rounded-md !py-1;
@apply bg-white !rounded-md !py-1.5;
}
.nc-menu.ant-dropdown-menu {
@apply !rounded-lg !shadow-none;
}
</style>

14
packages/nc-gui/components/nc/MenuItem.vue

@ -7,11 +7,19 @@
</template>
<style lang="scss">
.nc-menu-item {
@apply !py-2 font-normal text-sm;
.ant-dropdown-menu-item.nc-menu-item {
@apply py-2 px-2 mx-1.5 font-normal text-sm rounded-md overflow-hidden hover:bg-gray-100;
}
.nc-menu-item-inner {
@apply flex flex-row items-center gap-x-2.25;
@apply flex flex-row items-center gap-x-2;
}
.nc-menu-item > .ant-dropdown-menu-title-content {
@apply flex flex-row items-center;
}
.nc-menu-item::after {
background: none;
}
</style>

43
packages/nc-gui/components/nc/SubMenu.vue

@ -0,0 +1,43 @@
<template>
<a-sub-menu class="nc-sub-menu" popup-class-name="nc-submenu-popup">
<template #title>
<div class="flex flex-row items-center gap-x-1.5 py-1.75 justify-between group hover:text-gray-800">
<div class="flex flex-row items-center gap-x-2">
<slot name="title" />
</div>
<GeneralIcon icon="arrowRight" class="text-base text-gray-600 group-hover:text-gray-800" />
</div>
</template>
<template #expandIcon> </template>
<div class="py-1.5">
<slot />
</div>
</a-sub-menu>
</template>
<style lang="scss">
.ant-dropdown-menu-submenu.nc-sub-menu {
@apply flex mx-1.5 rounded-md overflow-hidden !hover:bg-gray-100;
}
.nc-sub-menu > .ant-dropdown-menu-submenu-title {
@apply pl-2 py-0 w-full;
}
.ant-dropdown-menu-submenu .ant-dropdown-menu-submenu-title:hover {
@apply !bg-gray-100;
}
.nc-submenu-popup {
@apply !rounded-lg border-1 border-gray-50;
}
.nc-submenu-popup .ant-dropdown-menu.ant-dropdown-menu-sub {
@apply !rounded-lg !shadow-lg shadow-gray-200;
}
.nc-menu-item::after {
background: none;
}
</style>

2
packages/nc-gui/components/smartsheet/sidebar/toolbar/Webhook.vue

@ -138,7 +138,7 @@ watch(
</div>
</div>
<div class="flex">
<a-dropdown placement="bottom" trigger="click">
<a-dropdown placement="bottom" :trigger="['click']">
<div
class="nc-docs-sidebar-page-options px-0.5 hover:(!bg-gray-300 !bg-opacity-30 rounded-md) cursor-pointer select-none"
data-testid="nc-view-sidebar-webhook-context-menu"

12
packages/nc-gui/components/workspace/CreateProjectDlg.vue

@ -7,13 +7,15 @@ import { NcProjectType, extractSdkResponseErrorMsg, projectTitleValidator, ref,
const props = defineProps<{
modelValue: boolean
type: NcProjectType
type?: NcProjectType
}>()
const emit = defineEmits(['update:modelValue'])
const dialogShow = useVModel(props, 'modelValue', emit)
const projectType = computed(() => props.type ?? NcProjectType.DB)
const projectsStore = useProjects()
const { createProject: _createProject } = projectsStore
@ -39,13 +41,13 @@ const createProject = async () => {
creating.value = true
try {
const project = await _createProject({
type: props.type,
type: projectType.value,
title: formState.value.title,
})
navigateToProject({
projectId: project.id!,
type: props.type,
type: projectType.value,
workspaceId: 'nc',
})
dialogShow.value = false
@ -79,7 +81,7 @@ watch(dialogShow, async (n, o) => {
})
const typeLabel = computed(() => {
switch (props.type) {
switch (projectType.value) {
case NcProjectType.DB:
default:
return 'Database'
@ -92,7 +94,7 @@ const typeLabel = computed(() => {
<template #header>
<!-- Create A New Table -->
<div class="flex flex-row items-center">
<GeneralProjectIcon :type="props.type" class="mr-2.5 !text-lg !h-4" />
<GeneralProjectIcon :type="projectType" class="mr-2.5 !text-lg !h-4" />
Create {{ typeLabel }}
</div>
</template>

2
packages/nc-gui/components/workspace/EmptyPlaceholder.vue

@ -15,7 +15,7 @@ const openCreateProjectDlg = (type: NcProjectType) => {
</script>
<template>
<div class="flex items-center justify-center h-full">
<div class="flex items-center justify-center mt-8">
<div class="flex flex-col gap-4 items-center text-gray-500">
<NcIconsInbox />
<div class="font-weight-medium">No Projects</div>

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

@ -39,6 +39,7 @@ export const CurrentCellInj: InjectionKey<Ref<Element | undefined>> = Symbol('cu
export const IsUnderLookupInj: InjectionKey<Ref<boolean>> = Symbol('is-under-lookup-injection')
export const DocsLocalPageInj: InjectionKey<Ref<PageSidebarNode | undefined>> = Symbol('docs-local-page-injection')
export const ProjectRoleInj: InjectionKey<Ref<string | string[]>> = Symbol('project-roles-injection')
export const ProjectStarredModeInj: InjectionKey<Ref<boolean>> = Symbol('project-starred-injection')
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')

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

@ -404,7 +404,8 @@
"prevRow": "Previous Row",
"addRowGrid": "Manually add data in grid view",
"addRowForm": "Enter record data through a form",
"noAccess": "No access"
"noAccess": "No access",
"restApis": "Rest APIs"
},
"activity": {
"moveProject": "Move Project",

192
packages/nc-gui/layouts/dashboard.vue

@ -1,95 +1,6 @@
<script lang="ts" setup>
import { Pane, Splitpanes } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
const router = useRouter()
const route = router.currentRoute
const {
isLeftSidebarOpen,
leftSidebarWidthPercent,
leftSideBarSize: sideBarSize,
leftSidebarState: sidebarState,
} = storeToRefs(useSidebarStore())
const wrapperRef = ref<HTMLDivElement>()
const contentSize = computed(() => 100 - sideBarSize.value.current)
const animationDuration = 250
const viewportWidth = ref(window.innerWidth)
const sidebarWidth = computed(() => (sideBarSize.value.old * viewportWidth.value) / 100)
const currentSidebarSize = computed({
get: () => sideBarSize.value.current,
set: (val) => {
sideBarSize.value.current = val
sideBarSize.value.old = val
},
})
watch(currentSidebarSize, () => {
leftSidebarWidthPercent.value = currentSidebarSize.value
})
watch(isLeftSidebarOpen, () => {
sideBarSize.value.current = sideBarSize.value.old
if (isLeftSidebarOpen.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
if (e.clientX < 4 && ['hiddenEnd', 'peekCloseEnd'].includes(sidebarState.value)) {
sidebarState.value = 'peekOpenStart'
setTimeout(() => {
sidebarState.value = 'peekOpenEnd'
}, animationDuration)
} else if (e.clientX > sidebarWidth.value + 10 && sidebarState.value === 'peekOpenEnd') {
sidebarState.value = 'peekCloseOpen'
setTimeout(() => {
sidebarState.value = 'peekCloseEnd'
}, animationDuration)
}
}
function onWindowResize() {
viewportWidth.value = window.innerWidth
}
onMounted(() => {
document.addEventListener('mousemove', handleMouseMove)
window.addEventListener('resize', onWindowResize)
})
onBeforeUnmount(() => {
document.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('resize', onWindowResize)
})
watch(route, () => {
if (route.value.name === 'index-index') {
isLeftSidebarOpen.value = true
}
})
</script>
<script lang="ts">
@ -101,101 +12,14 @@ export default {
<template>
<NuxtLayout class="h-screen">
<slot v-if="!route.meta.hasSidebar" name="content" />
<Splitpanes
v-else
class="nc-sidebar-content-resizable-wrapper w-full h-full"
:class="{
'hide-resize-bar': !isLeftSidebarOpen || sidebarState === 'openStart',
}"
@resize="currentSidebarSize = $event[0].size"
>
<Pane min-size="15%" :size="currentSidebarSize" max-size="40%" class="nc-sidebar-splitpane relative !overflow-visible">
<div
ref="wrapperRef"
class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-32 absolute overflow-visible"
:class="{
'minimized-height': !isLeftSidebarOpen,
'hide-sidebar': ['hiddenStart', 'hiddenEnd', 'peekCloseEnd'].includes(sidebarState),
}"
:style="{
width: sidebarState === 'hiddenEnd' ? '0px' : `${sidebarWidth}px`,
}"
>
<slot name="sidebar" />
</div>
</Pane>
<Pane :size="contentSize">
<LazyDashboardView v-else>
<template #sidebar>
<slot name="sidebar" />
</template>
<template #content>
<slot name="content" />
</Pane>
</Splitpanes>
</template>
</LazyDashboardView>
</NuxtLayout>
</template>
<style lang="scss">
.nc-sidebar-wrapper.minimized-height > * {
@apply h-4/5 pb-2 !(rounded-r-lg border-1 border-gray-200 shadow-lg);
width: calc(100% + 4px);
}
.nc-sidebar-wrapper > * {
transition: all 0.2s ease-in-out;
@apply z-10 absolute;
}
.nc-sidebar-wrapper.hide-sidebar {
@apply !min-w-0;
> * {
@apply opacity-0;
transform: translateX(-100%);
}
}
/** Split pane CSS */
.nc-sidebar-content-resizable-wrapper > {
.splitpanes__splitter {
@apply !w-0 relative overflow-visible;
}
.splitpanes__splitter:before {
@apply bg-gray-200 w-0.25 absolute left-0 top-0 h-full z-40;
content: '';
}
.splitpanes__splitter:hover:before {
@apply bg-scrollbar;
width: 3px !important;
left: -3px;
}
.splitpanes--dragging .splitpanes__splitter:before {
@apply bg-scrollbar;
width: 3px !important;
left: -3px;
}
.splitpanes--dragging .splitpanes__splitter {
@apply w-1 mr-0;
}
}
.nc-sidebar-content-resizable-wrapper.hide-resize-bar > {
.splitpanes__splitter {
cursor: default !important;
opacity: 0 !important;
background-color: transparent !important;
}
}
.splitpanes__pane {
transition: width 0.15s ease-in-out !important;
}
.splitpanes--dragging {
cursor: col-resize;
> .splitpanes__pane {
transition: none !important;
}
}
</style>

60
packages/nc-gui/pages/index.vue

@ -40,37 +40,45 @@ const isSharedView = computed(() => {
return !routeName.startsWith('index-typeOrId-projectId-') && !['index', 'index-typeOrId'].includes(routeName)
})
watch(
() => route.value.params.typeOrId,
async () => {
// avoid loading projects for shared views
if (isSharedView.value) {
return
}
// avoid loading projects for shared base
if (route.value.params.typeOrId === 'base') {
await populateWorkspace()
return
}
if (!signedIn.value) {
navigateTo('/signIn')
return
}
// Load projects
async function handleRouteTypeIdChange() {
// avoid loading projects for shared views
if (isSharedView.value) {
return
}
// avoid loading projects for shared base
if (route.value.params.typeOrId === 'base') {
await populateWorkspace()
return
}
if (!route.value.params.projectId && projectsList.value.length > 0) {
await autoNavigateToProject()
}
},
{
immediate: true,
if (!signedIn.value) {
navigateTo('/signIn')
return
}
// Load projects
await populateWorkspace()
if (!route.value.params.projectId && projectsList.value.length > 0) {
await autoNavigateToProject()
}
}
watch(
() => route.value.params.typeOrId,
() => {
handleRouteTypeIdChange()
},
)
// onMounted is needed instead having this function called through
// immediate watch, because if route is changed during page transition
// It will error out nuxt
onMounted(() => {
handleRouteTypeIdChange()
})
function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) {
dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key || ''

3
packages/nc-gui/store/projects.ts

@ -291,6 +291,8 @@ export const useProjects = defineStore('projectsStore', () => {
else navigateTo('/')
}
const toggleStarred = async (..._args: any) => {}
return {
projects,
projectsList,
@ -316,6 +318,7 @@ export const useProjects = defineStore('projectsStore', () => {
navigateToProject,
removeProjectUser,
navigateToFirstProjectOrHome,
toggleStarred,
}
})

7
packages/nc-gui/utils/iconUtils.ts

@ -25,6 +25,8 @@ import MsAddBoxOutline from '~icons/nc-icons/add-box'
import MsDownloadRounded from '~icons/nc-icons/download'
import LogosAirtable from '~icons/logos/airtable'
import MsStar from '~icons/material-symbols/star-outline-rounded'
import NcStar from '~icons/nc-icons/star'
import NcUnStar from '~icons/nc-icons/star-remove'
import MsSort from '~icons/material-symbols/sort'
import MaterialSymbolsEdit from '~icons/material-symbols/edit-outline-rounded'
import MaterialDuplicate from '~icons/material-symbols/file-copy-outline-rounded'
@ -73,6 +75,7 @@ import Right from '~icons/material-symbols/chevron-right-rounded'
import Left from '~icons/material-symbols/chevron-left-rounded'
import Up from '~icons/material-symbols/keyboard-arrow-up-rounded'
import Down from '~icons/material-symbols/keyboard-arrow-down-rounded'
import PhTriangleFill from '~icons/ph/triangle-fill'
// keep it for reference
// todo: remove it after all icons are migrated
@ -241,9 +244,11 @@ export const iconMap = {
export: h('span', { class: 'material-symbols' }, 'get_app'),
colInsertAfter: TablerColumnInsertRight,
colInsertBefore: TablerColumnInsertLeft,
star: MsStar,
star: NcStar,
unStar: NcUnStar,
sortDesc: MsSort,
article: NcArticle,
triangleFill: PhTriangleFill,
sortAsc: h('span', { class: 'material-symbols', style: { transform: 'scaleY(-1)' } }, 'sort'),
contentSaveExit: h('span', { class: 'material-symbols' }, 'save'),
contentSaveStay: h('span', { class: 'material-symbols' }, 'save_as'),

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

@ -160,8 +160,8 @@ export class TreeViewPage extends BasePage {
await this.get().locator(`.nc-project-tree-tbl-${title}`).waitFor({ state: 'visible' });
await this.get().locator(`.nc-project-tree-tbl-${title}`).scrollIntoViewIfNeeded();
await this.get().locator(`.nc-project-tree-tbl-${title}`).locator('.nc-icon.ant-dropdown-trigger').click();
await this.dashboard.get().locator('div.nc-project-menu-item:has-text("Delete"):visible').click();
await this.get().locator(`.nc-project-tree-tbl-${title}`).locator('.nc-tbl-context-menu').click();
await this.rootPage.locator('.ant-dropdown').locator('.nc-menu-item:has-text("Delete")').click();
await this.waitForResponse({
uiAction: async () => {
@ -190,8 +190,11 @@ export class TreeViewPage extends BasePage {
}
async renameTable({ title, newTitle }: { title: string; newTitle: string }) {
await this.get().locator(`.nc-project-tree-tbl-${title}`).locator('.nc-icon.ant-dropdown-trigger').click();
await this.dashboard.get().locator('div.nc-project-menu-item:has-text("Rename")').click();
await this.get().locator(`.nc-project-tree-tbl-${title}`).waitFor({ state: 'visible' });
await this.get().locator(`.nc-project-tree-tbl-${title}`).scrollIntoViewIfNeeded();
await this.get().locator(`.nc-project-tree-tbl-${title}`).locator('.nc-tbl-context-menu').click();
await this.rootPage.locator('.ant-dropdown').locator('.nc-menu-item:has-text("Rename")').click();
await this.dashboard.get().locator('[placeholder="Enter table name"]').fill(newTitle);
await this.dashboard.get().locator('button:has-text("Rename Table")').click();
await this.verifyToast({ message: 'Table renamed successfully' });
@ -208,14 +211,14 @@ export class TreeViewPage extends BasePage {
await this.getProjectContextMenu({ projectTitle: title }).hover();
await this.getProjectContextMenu({ projectTitle: title }).click();
const settingsMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md');
await settingsMenu.locator(`[data-menu-id="teamAndSettings"]`).click();
await settingsMenu.locator(`.nc-sidebar-project-project-settings`).click();
}
async quickImport({ title, projectTitle }: { title: string; projectTitle: string }) {
await this.getProjectContextMenu({ projectTitle }).hover();
await this.getProjectContextMenu({ projectTitle }).click();
const importMenu = this.dashboard.get().locator('.ant-dropdown-menu.nc-scrollbar-md');
await importMenu.locator(`.ant-dropdown-menu-submenu:has-text("Quick Import From")`).click();
const importMenu = this.dashboard.get().locator('.ant-dropdown-menu');
await importMenu.locator(`.nc-sub-menu:has-text("Import Data")`).click();
await this.rootPage.locator(`.ant-dropdown-menu-item:has-text("${title}")`).waitFor();
await this.rootPage.locator(`.ant-dropdown-menu-item:has-text("${title}")`).click();
}
@ -340,7 +343,7 @@ export class TreeViewPage extends BasePage {
await contextMenu.waitFor();
await contextMenu.locator(`.ant-dropdown-menu-item:has-text("Delete")`).click();
await this.rootPage.locator('div.ant-modal-content').locator(`button.ant-btn:has-text("Delete Project")`).click();
await this.rootPage.locator('div.ant-modal-content').locator(`button.ant-btn:has-text("Delete")`).click();
}
async duplicateProject(param: { title: string }) {

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

@ -88,7 +88,7 @@ export class LeftSidebarPage extends BasePage {
for (let i = 0; i < (await nodes.count()); i++) {
const text = await getTextExcludeIconText(nodes.nth(i));
if (text.toLowerCase() === param.title.toLowerCase()) {
await nodes.nth(i).click();
await nodes.nth(i).click({ force: true });
break;
}
}

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

@ -63,7 +63,7 @@ export class DashboardPage extends BasePage {
this.workspaceMenuLink = rootPage.getByTestId('nc-project-menu');
this.projectMenuLink = rootPage
.locator(`.project-title-node:has-text("${project.title}")`)
.locator('.nc-icon.ant-dropdown-trigger')
.locator('[data-testid="nc-sidebar-context-menu"]')
.first();
this.tabBar = rootPage.locator('.nc-tab-bar');
this.treeView = new TreeViewPage(this, project);
@ -118,7 +118,7 @@ export class DashboardPage extends BasePage {
async gotoSettings() {
await this.projectMenuLink.click();
await this.rootPage.locator('div.nc-project-menu-item:has-text("Settings")').click();
await this.rootPage.locator('.ant-dropdown').locator(`.nc-menu-item:has-text("Settings")`).click();
}
async gotoProjectSubMenu({ title }: { title: string }) {

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

@ -16,7 +16,7 @@ export class SharedFormPage extends BasePage {
async submit() {
await this.waitForResponse({
uiAction: async () => await this.get().getByTestId('shared-form-submit-button').click(),
uiAction: async () => await this.get().getByTestId('shared-form-submit-button').first().click(),
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/rows',
});

4
tests/playwright/pages/WorkspacePage/ContainerPage.ts

@ -181,9 +181,9 @@ export class ContainerPage extends BasePage {
await this.rootPage.waitForTimeout(1000);
const row = await this.getProjectRow({ title });
await row.locator('td.ant-table-cell').nth(3).locator('.nc-icon').click();
await this.rootPage.locator('.ant-dropdown-menu-item:has-text("Delete Project")').click();
await this.rootPage.locator('.ant-dropdown-menu-item:has-text("Delete")').click();
await this.waitForResponse({
uiAction: () => this.rootPage.locator('.ant-modal-content').locator('button:has-text("Delete Project")').click(),
uiAction: () => this.rootPage.locator('.ant-modal-content').locator('button:has-text("Delete")').click(),
httpMethodsToMatch: ['DELETE'],
requestUrlPathToMatch: `api/v1/db/meta/projects/`,
});

2
tests/playwright/setup/index.ts

@ -342,7 +342,7 @@ const setup = async ({
url?: string;
}): Promise<NcContext> => {
let dbType = process.env.CI ? process.env.E2E_DB_TYPE : process.env.E2E_DEV_DB_TYPE;
dbType = dbType || 'sqlite';
dbType = dbType || isEE() ? 'pg' : 'sqlite';
let response;

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

@ -49,6 +49,7 @@ test.describe('Attachment column', () => {
await dashboard.viewSidebar.createFormView({
title: 'Form 1',
});
await dashboard.rootPage.waitForTimeout(500);
const sharedFormUrl = await dashboard.form.topbar.getSharedViewUrl();
await dashboard.viewSidebar.openView({ title: 'Country' });
@ -56,15 +57,21 @@ test.describe('Attachment column', () => {
const newPage = await context.newPage();
await newPage.goto(sharedFormUrl);
const sharedForm = new SharedFormPage(newPage);
await sharedForm.rootPage.waitForTimeout(500);
await sharedForm.cell.fillText({
index: 0,
columnHeader: 'Country',
text: 'test',
});
await sharedForm.rootPage.waitForTimeout(500);
await sharedForm.cell.attachment.addFile({
columnHeader: 'testAttach',
filePath: [`${process.cwd()}/fixtures/sampleFiles/1.json`],
});
await sharedForm.rootPage.waitForTimeout(500);
await sharedForm.submit();
await sharedForm.verifySuccessMessage();
await newPage.close();

Loading…
Cancel
Save