* feat: Improved ui (#6156) * refactor: revert Signed-off-by: Pranav C <pranavxc@gmail.com> feat: shared base Signed-off-by: Pranav C <pranavxc@gmail.com> fix: remove duplicate import statement Signed-off-by: Pranav C <pranavxc@gmail.com> fix: disable starred & license menu Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: fix airtable wait issue Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: enable mysql in ci Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: fix checkbox order for sqlite Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: disable quick tests Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: fix dbType env variable for CI Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: workspace API access error fix Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: enable SQLite CI CD Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: use DB_TYPE env variable Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: enable SQLite UT Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: isHub cleanup Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: add check for EE Timezone spec Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> chore: cleanup Signed-off-by: Pranav C <pranavxc@gmail.com> chore: cleanup Signed-off-by: Pranav C <pranavxc@gmail.com> test: EE check fix Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> chore: test correction Signed-off-by: Pranav C <pranavxc@gmail.com> chore: sync latest changes Signed-off-by: Pranav C <pranavxc@gmail.com> test: set EE=false Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: set NC Edition to community in workflow file Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> chore: update sdk build command Signed-off-by: Pranav C <pranavxc@gmail.com> refactor: i18n and other changes Signed-off-by: Pranav C <pranavxc@gmail.com> feat: new ui Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: sync tests Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: lint Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: shared view/base related bugs Signed-off-by: Pranav C <pranavxc@gmail.com> * test: checkbox verification sort order fix Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: fix sqlite reset Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: enable selfhosted runners Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * docs: table ops (draft) Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * Docs: screenshots for table-operations.md * refactor: introduce missing buttons Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: get all fields Signed-off-by: Pranav C <pranavxc@gmail.com> * test: UT fix- new data API response Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: EE is false Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: webhook lookup as string in CE Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * fix: include created_at and updated_at Signed-off-by: Pranav C <pranavxc@gmail.com> * test: fix UT newDataAPI response for PG Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * fix: separate api for webhook related plugins Signed-off-by: Pranav C <pranavxc@gmail.com> * test: msyql filter corrections Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: mysql group by test corrections Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: fix datatype for rating field in groupby spec for pg Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: kanban datatype correction Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: column edit for mysql- rating field Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: misc fixes Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: enable 4 workers Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: enable 2 workers per shard only Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * docs: table CRUD * Rename table-operations.md to table-crud.md * Create column-crud.md * docs: row CRUD * Rename row.md to row-crud.md * docs: project crud * docs: toolbar (skeleton) * refactor: single page UI and bug fixes Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: sync tests playwright Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: add missing dependency Signed-off-by: Pranav C <pranavxc@gmail.com> * feat: single page ui, test corrections Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: tests Signed-off-by: Pranav C <pranavxc@gmail.com> * test: project rename test correction Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: remove only Signed-off-by: Pranav C <pranavxc@gmail.com> * test: remove wrong import statement Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: delete option not visible in project context menu Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: move ws access within isEE() Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: fix groupby * test: groupby fix Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * docs: signup & landing page * docs: project crud * docs: project-crud misc * docs: toolbar fields * docs: toolbar / filters * docs: toolbar / group by * docs: toolbar / sort * docs: toolbar / row height * docs: filters additional options * docs: file re-order Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * docs: add links to column types * docs: code snippets * docs: links * docs: lookup * docs: rollup * docs: formula * docs: primary key * docs: display value * docs: development setup * docs: swagger * fix(nc-gui): encodeURIComponent for row id - closes: #6202 * docs: language * docs: expanded record * docs: import airtable * docs: airtable * docs: webhook * docs: revert file rename Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * docs: account settings * docs: audit * docs: meta management * docs: project settings * docs: shared base * docs: shared view * docs: meta sync * docs: team-auth * docs: views * docs: fix URL * docs: URL corrections * fix: shared base, view related bugs Signed-off-by: Pranav C <pranavxc@gmail.com> * test: EE check for WSaccess Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: exclude EE tests Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * fix: missing project delete closes #6215 Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: merge existing project meta if found closes #6216 Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: merge existing project meta if found closes #6216 Signed-off-by: Pranav C <pranavxc@gmail.com> --------- Signed-off-by: Pranav C <pranavxc@gmail.com> Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com> Co-authored-by: DarkPhoenix2704 <anbarasun123@gmail.com> Co-authored-by: Wing-Kam Wong <wingkwong.code@gmail.com> * refactor: docs and other bug fixes Signed-off-by: Pranav C <pranavxc@gmail.com> * feat: populate default project on super admin signup Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: include created project details in signup response if avail, missing Dockerfile Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: use custom function for resolving ts path aliases Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: add missing generate script Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: webpack build correction - ts path resolve Signed-off-by: Pranav C <pranavxc@gmail.com> --------- Signed-off-by: Pranav C <pranavxc@gmail.com> Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> Co-authored-by: mertmit <mertmit99@gmail.com> Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com> Co-authored-by: DarkPhoenix2704 <anbarasun123@gmail.com> Co-authored-by: Wing-Kam Wong <wingkwong.code@gmail.com>pull/6225/head
@ -0,0 +1,31 @@
|
||||
.nc-dashboard-layouts-propspanel-value-input { |
||||
@apply flex-grow py-1 px-3 border-grey-light border border-solid rounded-lg text-sm w-full my-2; |
||||
} |
||||
|
||||
.nc-dashboard-layouts-propspanel-description-input { |
||||
@apply flex-grow py-1 px-3 !border-gray-200 border border-solid rounded-lg text-sm w-full my-2; |
||||
} |
||||
|
||||
.nc-dashboard-layouts-propspanel-selectable-config-section { |
||||
@apply bg-gray-100 rouwded-lg p-2; |
||||
h3 { |
||||
@apply text-black; |
||||
} |
||||
h4 { |
||||
@apply text-gray-500 |
||||
} |
||||
} |
||||
|
||||
.nc-dashboard-layouts-propspanel-collapse { |
||||
background-color: transparent; |
||||
.nc-dashboard-layouts-propspanel-collapse-panel { |
||||
@apply border-1 border-grey-light rounded-lg my-2 min-w-full; |
||||
.ant-collapse-header { |
||||
@apply font-semibold; |
||||
@apply !text-sm !2xl:text-base; |
||||
} |
||||
h3 { |
||||
@apply text-gray-500; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,529 @@
|
||||
.nc-docs-page { |
||||
overflow-y: overlay; |
||||
height: calc(100vh - var(--topbar-height)); |
||||
|
||||
&::-webkit-scrollbar { |
||||
width: 6px; |
||||
} |
||||
&::-webkit-scrollbar-track { |
||||
background: #f6f6f600 !important; |
||||
} |
||||
&::-webkit-scrollbar-thumb { |
||||
background: #f6f6f600; |
||||
} |
||||
&::-webkit-scrollbar-thumb:hover { |
||||
background: #f6f6f600; |
||||
} |
||||
} |
||||
.nc-docs-page:hover { |
||||
&::-webkit-scrollbar { |
||||
width: 6px; |
||||
} |
||||
&::-webkit-scrollbar-track { |
||||
background: #f6f6f600 !important; |
||||
} |
||||
&::-webkit-scrollbar-thumb { |
||||
background: rgb(215, 215, 215); |
||||
} |
||||
&::-webkit-scrollbar-thumb:hover { |
||||
background: rgb(203, 203, 203); |
||||
} |
||||
} |
||||
|
||||
.nc-docs-page-title { |
||||
font-weight: 600; |
||||
font-size: 3rem; |
||||
line-height: 1.25; |
||||
word-break: break-all; |
||||
outline: none; |
||||
padding: 0; |
||||
color: black; |
||||
} |
||||
|
||||
.nc-docs-page-content { |
||||
@apply min-h-full; |
||||
.ProseMirror { |
||||
@apply min-h-full; |
||||
} |
||||
.ProseMirror-focused { |
||||
// remove all border |
||||
outline: none; |
||||
} |
||||
|
||||
[data-diff-node='ins'] { |
||||
@apply !bg-green-200 rounded-sm p-0.5 m-0.5; |
||||
} |
||||
|
||||
[data-diff-node='del'] { |
||||
@apply !bg-red-200 rounded-sm p-0.5 m-0.5; |
||||
} |
||||
|
||||
del { |
||||
@apply !bg-red-200 rounded-sm my-0.5; |
||||
text-decoration: none; |
||||
} |
||||
ins { |
||||
@apply !bg-green-200 rounded-sm my-0.5 mx-0.5; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
ins[isempty='true'] { |
||||
display: block; |
||||
color: transparent; |
||||
user-select: none; |
||||
@apply !w-full; |
||||
} |
||||
del[isempty='true'] { |
||||
display: block; |
||||
color: transparent; |
||||
user-select: none; |
||||
@apply !w-full; |
||||
} |
||||
|
||||
td { |
||||
ins { |
||||
@apply !p-0 !m-0; |
||||
} |
||||
} |
||||
|
||||
.draggable-block-wrapper{ |
||||
overflow: visible; |
||||
} |
||||
.draggable-block-wrapper.focused { |
||||
.attachment-wrapper .attachment { |
||||
@apply !bg-primary-selected; |
||||
} |
||||
} |
||||
|
||||
.draggable-block-wrapper.selected { |
||||
table { |
||||
@apply !bg-primary-selected; |
||||
tr:first-child td { |
||||
@apply !bg-primary-selected; |
||||
} |
||||
} |
||||
.attachment-wrapper .attachment { |
||||
@apply !bg-primary-selected; |
||||
} |
||||
p, |
||||
h1, |
||||
h2, |
||||
h3, |
||||
h4, |
||||
h5, |
||||
h6, |
||||
li, |
||||
blockquote, |
||||
pre, |
||||
code, |
||||
img, |
||||
.link-to-page-wrapper { |
||||
@apply !bg-primary-selected; |
||||
} |
||||
|
||||
.node-view-drag-content > ul { |
||||
@apply !bg-primary-selected; |
||||
} |
||||
} |
||||
|
||||
div[contenteditable='false'].ProseMirror { |
||||
user-select: text !important; |
||||
} |
||||
|
||||
p.is-empty::after, |
||||
h1.is-empty::after, |
||||
h2.is-empty::after, |
||||
h3.is-empty::after { |
||||
content: attr(data-placeholder); |
||||
float: left; |
||||
color: #afafaf; |
||||
pointer-events: none; |
||||
margin-top: -1.55rem; |
||||
margin-left: 0.01rem; |
||||
} |
||||
|
||||
[data-one-content='true'] [data-type='collapsable_content'] { |
||||
p.is-empty::after { |
||||
content: 'Empty collapsable. Press / to open the command menu or start writing'; |
||||
} |
||||
} |
||||
|
||||
p.is-empty::after { |
||||
margin-top: -1.55rem; |
||||
} |
||||
h1.is-empty::after { |
||||
margin-top: -2.85rem; |
||||
} |
||||
h2.is-empty::after { |
||||
margin-top: -2.25rem; |
||||
} |
||||
h3.is-empty::after { |
||||
margin-top: -1.8rem; |
||||
} |
||||
.collapsable-wrapper { |
||||
h1, |
||||
h2, |
||||
h3 { |
||||
margin-top: 0; |
||||
margin-bottom: 0; |
||||
} |
||||
} |
||||
|
||||
.editable { |
||||
.focused { |
||||
div[data-is-empty='true'] { |
||||
p::after { |
||||
content: 'Press / to open the command menu or start writing' !important; |
||||
float: left; |
||||
color: #afafaf; |
||||
pointer-events: none; |
||||
margin-top: -1.55rem; |
||||
margin-left: 0.01rem; |
||||
} |
||||
} |
||||
} |
||||
div.is-empty.focused { |
||||
p::after { |
||||
content: 'Press / to open the command menu or start writing' !important; |
||||
float: left; |
||||
color: #afafaf; |
||||
pointer-events: none; |
||||
margin-top: -1.55rem; |
||||
margin-left: 0.01rem; |
||||
} |
||||
} |
||||
} |
||||
|
||||
h1.is-empty::before, |
||||
h2.is-empty::before, |
||||
h3.is-empty::before { |
||||
color: #d6d6d6; |
||||
} |
||||
|
||||
.nc-docs-list-item > p { |
||||
margin-top: 0.25rem !important; |
||||
margin-bottom: 0.25rem !important; |
||||
} |
||||
|
||||
p { |
||||
font-weight: 400; |
||||
color: #000000; |
||||
font-size: 1rem; |
||||
margin-top: 0.25rem; |
||||
margin-bottom: 0.25rem; |
||||
} |
||||
|
||||
h1 { |
||||
font-weight: 600; |
||||
font-size: 1.85rem; |
||||
margin-bottom: 0.6em; |
||||
} |
||||
|
||||
h2 { |
||||
font-weight: 600; |
||||
font-size: 1.45rem; |
||||
margin-bottom: 0.5em; |
||||
} |
||||
|
||||
h3 { |
||||
font-weight: 600; |
||||
font-size: 1.15rem; |
||||
margin-bottom: 0.3em; |
||||
} |
||||
|
||||
h4 { |
||||
font-size: 1.2rem; |
||||
} |
||||
|
||||
h5 { |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
h6 { |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
// Pre tag is the parent wrapper for Code block |
||||
pre { |
||||
background: #f2f4f7; |
||||
border-color: #d0d5dd; |
||||
border: 1px; |
||||
color: black; |
||||
font-family: 'JetBrainsMono', monospace; |
||||
padding: 1rem; |
||||
border-radius: 0.5rem; |
||||
@apply overflow-auto mt-3; |
||||
|
||||
code { |
||||
@apply !px-0; |
||||
} |
||||
} |
||||
|
||||
code { |
||||
background: #f2f4f7; |
||||
@apply rounded-md px-2 py-1; |
||||
color: inherit; |
||||
font-size: 0.8rem; |
||||
} |
||||
|
||||
|
||||
[data-group-type='list-item'] { |
||||
@apply flex items-start; |
||||
.tiptap-list-item-start { |
||||
@apply flex mt-1.125 pr-2 !select-none; |
||||
|
||||
input { |
||||
@apply mt-0.75 flex rounded-sm; |
||||
} |
||||
// Unchecked |
||||
input:not(:checked) { |
||||
// Add border to checkbox |
||||
border-width: 1.5px; |
||||
@apply border-gray-700; |
||||
} |
||||
} |
||||
|
||||
} |
||||
table { |
||||
[data-group-type='list-item'] { |
||||
.tiptap-list-item-start { |
||||
@apply mt-0.125; |
||||
} |
||||
} |
||||
} |
||||
|
||||
[data-type='ordered'] { |
||||
@apply flex flex-row items-start gap-x-1; |
||||
.tiptap-list-item-start > span::before { |
||||
margin-top: 6px; |
||||
content: attr(data-number) '. '; |
||||
display: inline-block; |
||||
white-space: nowrap; |
||||
} |
||||
.tiptap-list-item-content { |
||||
@apply flex flex-grow; |
||||
} |
||||
} |
||||
|
||||
[data-type='image'] { |
||||
@apply mb-3 |
||||
} |
||||
|
||||
hr { |
||||
border: 0; |
||||
border-top: 1px solid #ccc; |
||||
margin: 1.5em 0; |
||||
} |
||||
|
||||
hr.ProseMirror-selectednode { |
||||
// outline with rounded corners |
||||
outline: 4px solid #e8eafd; |
||||
border-radius: 4px; |
||||
} |
||||
.focused { |
||||
hr { |
||||
// outline with rounded corners |
||||
outline: 4px solid #e8eafd; |
||||
border-radius: 4px; |
||||
} |
||||
} |
||||
.selected { |
||||
hr { |
||||
// outline with rounded corners |
||||
outline: 4px solid #e8eafd; |
||||
border-radius: 4px; |
||||
} |
||||
} |
||||
|
||||
.selected { |
||||
.external-content-wrapper { |
||||
// outline with rounded corners |
||||
outline: 2px solid #e8eafd; |
||||
border-radius: 1px; |
||||
} |
||||
} |
||||
|
||||
.external-content-wrapper.ProseMirror-selectednode { |
||||
// outline with rounded corners |
||||
outline: 2px solid #e8eafd; |
||||
border-radius: 1px; |
||||
} |
||||
|
||||
blockquote { |
||||
border-left: 3px solid #d0d5dd; |
||||
padding: 0 1em; |
||||
color: #666; |
||||
margin: 1em 0; |
||||
font-style: italic; |
||||
} |
||||
|
||||
.column-resize-handle { |
||||
background-color: #e3e5ff !important; |
||||
width: 6px; |
||||
cursor: col-resize; |
||||
z-index: 1; |
||||
} |
||||
|
||||
.resize-cursor { |
||||
cursor: ew-resize; |
||||
cursor: col-resize; |
||||
} |
||||
|
||||
.external-content-wrapper { |
||||
@apply bg-gray-100 my-2; |
||||
} |
||||
|
||||
div[data-type='column'] { |
||||
@apply flex flex-row gap-x-12 justify-between; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Table styles |
||||
*/ |
||||
|
||||
.tiptap-table-wrapper { |
||||
@apply !pb-4 !pt-4; |
||||
} |
||||
|
||||
table { |
||||
border-collapse: collapse; |
||||
table-layout: fixed; |
||||
width: 100%; |
||||
padding-top: 2rem; |
||||
padding-bottom: 2rem; |
||||
overflow: visible; |
||||
tbody { |
||||
overflow: visible; |
||||
} |
||||
td { |
||||
.tiptap-list-item-start { |
||||
@apply -mt-1; |
||||
} |
||||
position: relative; |
||||
min-width: 1em; |
||||
border: 1px solid #e5e5e5; |
||||
overflow: visible !important; |
||||
height: 20px; |
||||
border-top: 0; |
||||
padding-left: 1rem; |
||||
padding-right: 1rem; |
||||
padding-top: 0.5rem; |
||||
padding-bottom: 0.5rem; |
||||
} |
||||
|
||||
// First row's td |
||||
tr:first-child { |
||||
td { |
||||
border-top: 1px solid #e5e5e5 !important; |
||||
background-color: #fafbfb; |
||||
p { |
||||
font-weight: 500; |
||||
} |
||||
} |
||||
} |
||||
|
||||
th { |
||||
@apply font-semibold; |
||||
background-color: #fafbfb; |
||||
} |
||||
|
||||
.column-resize-handle { |
||||
position: absolute; |
||||
right: -2px; |
||||
top: 0; |
||||
bottom: 0px; |
||||
margin-top: 1px; |
||||
margin-bottom: 1px; |
||||
width: 8px; |
||||
outline: 1px solid #e3e5ff; |
||||
} |
||||
|
||||
p { |
||||
margin: 0; |
||||
} |
||||
|
||||
.column-resize-handle { |
||||
background-color: #e3e5ff !important; |
||||
width: 3px; |
||||
cursor: col-resize; |
||||
z-index: 1; |
||||
} |
||||
} |
||||
|
||||
// First cell |
||||
tr:hover > td:first-child .row-drag-handle { |
||||
display: block; |
||||
} |
||||
|
||||
tr:first-child > td:hover .tiptap-column-options { |
||||
display: flex; |
||||
} |
||||
|
||||
tr:first-child > td:only-child:hover .tiptap-column-options { |
||||
display: none; |
||||
} |
||||
|
||||
|
||||
.selectedCell { |
||||
@apply !bg-primary-selected; |
||||
|
||||
// transition for white to blue background with delay as row/column creation causes cell selection for a moment which causes flicker |
||||
transition: background-color 1ms ease-out 1ms; |
||||
|
||||
.tiptap-column-options { |
||||
display: none !important; |
||||
} |
||||
.row-drag-handle { |
||||
display: none !important; |
||||
} |
||||
} |
||||
|
||||
.tiptap-table-cell { |
||||
@apply w-full; |
||||
} |
||||
|
||||
.selected { |
||||
.callout { |
||||
@apply bg-primary-selected; |
||||
} |
||||
} |
||||
.callout { |
||||
@apply my-2.5 px-2 py-2 rounded-md; |
||||
[data-type='bullet'] { |
||||
margin-left: 0.7rem; |
||||
} |
||||
.nc-callout-emoji { |
||||
@apply text-base; |
||||
} |
||||
} |
||||
|
||||
|
||||
[data-bg-color='gray'] { |
||||
@apply bg-gray-100 bg-opacity-30; |
||||
} |
||||
[data-bg-color='brown'] { |
||||
@apply bg-amber-600 bg-opacity-20; |
||||
} |
||||
[data-bg-color='orange'] { |
||||
@apply bg-orange-100 bg-opacity-50; |
||||
} |
||||
[data-bg-color='yellow'] { |
||||
@apply bg-yellow-100 bg-opacity-50; |
||||
} |
||||
[data-bg-color='green'] { |
||||
@apply bg-green-100 bg-opacity-50; |
||||
} |
||||
[data-bg-color='blue'] { |
||||
@apply bg-blue-100 bg-opacity-50; |
||||
} |
||||
[data-bg-color='purple'] { |
||||
@apply bg-purple-100 bg-opacity-50; |
||||
} |
||||
[data-bg-color='pink'] { |
||||
@apply bg-pink-100 bg-opacity-50; |
||||
} |
||||
[data-bg-color='red'] { |
||||
@apply bg-red-100 bg-opacity-50; |
||||
} |
||||
} |
After Width: | Height: | Size: 462 B |
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 647 B |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 722 B |
After Width: | Height: | Size: 557 B |
After Width: | Height: | Size: 845 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 300 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 712 B |
After Width: | Height: | Size: 602 B |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 518 B |
After Width: | Height: | Size: 486 B |
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,3 @@
|
||||
<template> |
||||
<div /> |
||||
</template> |
@ -0,0 +1,148 @@
|
||||
<script lang="ts" setup> |
||||
import { storeToRefs } from 'pinia' |
||||
import { useGlobal } from '#imports' |
||||
|
||||
const router = useRouter() |
||||
|
||||
const route = router.currentRoute |
||||
|
||||
const workspaceStore = useWorkspace() |
||||
|
||||
const { activeWorkspace, isWorkspaceOwnerOrCreator } = storeToRefs(workspaceStore) |
||||
|
||||
const projectStore = useProject() |
||||
|
||||
const { isSharedBase } = storeToRefs(projectStore) |
||||
|
||||
const { navigateToWorkspaceSettings } = useWorkspace() |
||||
|
||||
const { isUIAllowed } = useUIPermission() |
||||
|
||||
const dialogOpen = ref(false) |
||||
|
||||
const openDialogKey = ref<string>('') |
||||
|
||||
const dataSourcesState = ref<string>('') |
||||
|
||||
const projectId = ref<string>() |
||||
|
||||
const isCreateProjectOpen = ref(false) |
||||
|
||||
function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) { |
||||
dialogOpen.value = value ?? !dialogOpen.value |
||||
openDialogKey.value = key || '' |
||||
dataSourcesState.value = dsState || '' |
||||
projectId.value = pId || '' |
||||
} |
||||
|
||||
// todo: |
||||
const currentVersion = ref('') |
||||
|
||||
const isTreeViewOnScrollTop = ref(true) |
||||
const onTreeViewScrollTop = (onScrollTop: boolean) => { |
||||
isTreeViewOnScrollTop.value = !onScrollTop |
||||
} |
||||
|
||||
const { appInfo } = useGlobal() |
||||
|
||||
const navigateToSettings = () => { |
||||
navigateToWorkspaceSettings() |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
class="nc-sidebar flex flex-col bg-gray-50 outline-r-1 outline-gray-100 select-none" |
||||
:style="{ |
||||
outlineWidth: '1px', |
||||
}" |
||||
> |
||||
<div class="flex flex-col" :style="{ height: isSharedBase ? 'auto' : 'var(--sidebar-top-height)' }"> |
||||
<div style="border-bottom-width: 1px" class="flex items-center px-1 nc-sidebar-header !border-0 py-1.25 pl-2"> |
||||
<div class="flex flex-row flex-grow hover:bg-gray-100 pl-2 pr-1 py-0.5 rounded-md max-w-full"> |
||||
<a |
||||
v-if="isSharedBase" |
||||
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105" |
||||
href="https://github.com/nocodb/nocodb" |
||||
target="_blank" |
||||
> |
||||
<a-tooltip placement="bottom"> |
||||
<template #title> |
||||
{{ currentVersion }} |
||||
</template> |
||||
<img width="25" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" /> |
||||
</a-tooltip> |
||||
</a> |
||||
|
||||
<WorkspaceMenu :workspace="activeWorkspace" :is-open="true"> |
||||
<template #brandIcon> |
||||
<div |
||||
v-if="!isSharedBase" |
||||
v-e="['c:navbar:home']" |
||||
data-testid="nc-noco-brand-icon" |
||||
class="w-[29px] min-w-[29px] nc-noco-brand-icon" |
||||
> |
||||
<img width="25" class="mr-0" alt="NocoDB" src="~/assets/img/icons/512x512.png" /> |
||||
</div> |
||||
</template> |
||||
</WorkspaceMenu> |
||||
</div> |
||||
</div> |
||||
|
||||
<template v-if="!isSharedBase"> |
||||
<div class="w-full mt-2"></div> |
||||
<div class="h-17.5"> |
||||
<div |
||||
v-if="isWorkspaceOwnerOrCreator" |
||||
role="button" |
||||
class="nc-sidebar-top-button" |
||||
data-testid="nc-sidebar-team-settings-btn" |
||||
@click="navigateToSettings" |
||||
> |
||||
<GeneralIcon icon="settings" class="!h-3.9" /> |
||||
<div>Team & Settings</div> |
||||
</div> |
||||
<WorkspaceCreateProjectBtn |
||||
v-if="isUIAllowed('projectCreate', false)" |
||||
v-model:is-open="isCreateProjectOpen" |
||||
modal |
||||
type="text" |
||||
class="!p-0 mx-1" |
||||
data-testid="nc-sidebar-create-project-btn" |
||||
:active-workspace-id="route.params.typeOrId" |
||||
> |
||||
<div |
||||
class="gap-x-2 flex flex-row w-full items-center nc-sidebar-top-button !my-0 !mx-0" |
||||
:class="{ |
||||
'bg-gray-100': isCreateProjectOpen, |
||||
}" |
||||
> |
||||
<MdiPlus class="!h-4" /> |
||||
|
||||
<div class="flex">{{ $t('title.newProj') }}</div> |
||||
</div> |
||||
</WorkspaceCreateProjectBtn> |
||||
</div> |
||||
<div class="flex flex-grow"></div> |
||||
<div class="text-gray-500 mx-5 font-medium mb-1.5">{{ $t('objects.projects') }}</div> |
||||
</template> |
||||
<div |
||||
class="w-full border-b-1" |
||||
:class="{ |
||||
'border-gray-200': !isTreeViewOnScrollTop, |
||||
'border-transparent': isTreeViewOnScrollTop, |
||||
}" |
||||
></div> |
||||
</div> |
||||
<LazyDashboardTreeViewNew |
||||
@create-base-dlg="toggleDialog(true, 'dataSources', undefined, projectId)" |
||||
@on-scroll-top="onTreeViewScrollTop" |
||||
/> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
.nc-sidebar-top-button { |
||||
@apply flex flex-row mx-1 px-3.5 rounded-md items-center py-0.75 my-0.5 gap-x-2 hover:bg-gray-200 cursor-pointer; |
||||
} |
||||
</style> |
@ -0,0 +1,262 @@
|
||||
<script lang="ts" setup> |
||||
import type { ProjectType } from 'nocodb-sdk' |
||||
import { storeToRefs } from 'pinia' |
||||
import { toRef } from '@vue/reactivity' |
||||
import { resolveComponent } from '@vue/runtime-core' |
||||
import { ref } from 'vue' |
||||
import { ProjectRoleInj, useDialog, useUIPermission } from '#imports' |
||||
|
||||
const props = withDefaults( |
||||
defineProps<{ |
||||
project: ProjectType |
||||
baseIndex?: number |
||||
}>(), |
||||
{ |
||||
baseIndex: 0, |
||||
}, |
||||
) |
||||
|
||||
const emit = defineEmits<{ |
||||
openTableCreateDialog: () => void |
||||
}>() |
||||
|
||||
const { isUIAllowed } = useUIPermission() |
||||
|
||||
const project = toRef(props, 'project') |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
const projectStore = useProject() |
||||
|
||||
const { isSharedBase } = storeToRefs(projectStore) |
||||
|
||||
const projectRole = inject(ProjectRoleInj) |
||||
|
||||
function openSchemaMagicDialog(baseId?: string) { |
||||
if (!baseId) return |
||||
|
||||
$e('c:table:create:navdraw') |
||||
|
||||
const isOpen = ref(true) |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgSchemaMagic'), { |
||||
'modelValue': isOpen, |
||||
'baseId': baseId, |
||||
'onUpdate:modelValue': closeDialog, |
||||
}) |
||||
|
||||
function closeDialog() { |
||||
isOpen.value = false |
||||
|
||||
close(1000) |
||||
} |
||||
} |
||||
|
||||
function openQuickImportDialog(type: string, baseId?: string) { |
||||
if (!baseId) return |
||||
|
||||
$e(`a:actions:import-${type}`) |
||||
|
||||
const isOpen = ref(true) |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgQuickImport'), { |
||||
'modelValue': isOpen, |
||||
'importType': type, |
||||
'baseId': baseId, |
||||
'onUpdate:modelValue': closeDialog, |
||||
}) |
||||
|
||||
function closeDialog() { |
||||
isOpen.value = false |
||||
|
||||
close(1000) |
||||
} |
||||
} |
||||
|
||||
function openAirtableImportDialog(baseId?: string) { |
||||
if (!baseId) return |
||||
|
||||
$e('a:actions:import-airtable') |
||||
|
||||
const isOpen = ref(true) |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgAirtableImport'), { |
||||
'modelValue': isOpen, |
||||
'baseId': baseId, |
||||
'onUpdate:modelValue': closeDialog, |
||||
}) |
||||
|
||||
function closeDialog() { |
||||
isOpen.value = false |
||||
|
||||
close(1000) |
||||
} |
||||
} |
||||
|
||||
function openTableCreateMagicDialog(baseId?: string) { |
||||
if (!baseId) return |
||||
|
||||
$e('c:table:create:navdraw') |
||||
|
||||
const isOpen = ref(true) |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgTableMagic'), { |
||||
'modelValue': isOpen, |
||||
'baseId': baseId, |
||||
'onUpdate:modelValue': closeDialog, |
||||
}) |
||||
|
||||
function closeDialog() { |
||||
isOpen.value = false |
||||
|
||||
close(1000) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
v-if="isUIAllowed('table-create', false, projectRole)" |
||||
class="group flex items-center gap-2 pl-2 pr-4.75 py-1 text-primary/70 hover:(text-primary/100) cursor-pointer select-none" |
||||
@click="emit('openTableCreateDialog')" |
||||
> |
||||
<PhPlusThin class="w-5 ml-2" /> |
||||
|
||||
<span class="text-gray-500 group-hover:(text-primary/100) flex-1 nc-add-new-table">{{ $t('tooltip.addTable') }}</span> |
||||
|
||||
<a-dropdown v-if="!isSharedBase" :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop> |
||||
<GeneralIcon |
||||
icon="threeDotVertical" |
||||
class="transition-opacity opacity-0 group-hover:opacity-100 nc-import-menu outline-0" |
||||
/> |
||||
|
||||
<template #overlay> |
||||
<a-menu class="!py-0 rounded text-sm"> |
||||
<a-menu-item-group class="!px-0 !mx-0"> |
||||
<template #title> |
||||
<div class="flex items-center"> |
||||
Noco |
||||
<GeneralIcon icon="magic" class="ml-1 text-orange-400" /> |
||||
</div> |
||||
</template> |
||||
<a-menu-item key="table-magic" @click="openTableCreateMagicDialog(project.bases[baseIndex].id)"> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<GeneralIcon icon="magic1" class="group-hover:text-accent" /> |
||||
Create table |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item key="schema-magic" @click="openSchemaMagicDialog(project.bases[baseIndex].id)"> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<GeneralIcon icon="magic1" class="group-hover:text-accent" /> |
||||
Create schema |
||||
</div> |
||||
</a-menu-item> |
||||
</a-menu-item-group> |
||||
|
||||
<a-menu-divider class="my-0" /> |
||||
|
||||
<!-- Quick Import From --> |
||||
<a-menu-item-group :title="$t('title.quickImportFrom')" class="!px-0 !mx-0"> |
||||
<a-menu-item |
||||
v-if="isUIAllowed('airtableImport', false, projectRole)" |
||||
key="quick-import-airtable" |
||||
@click="openAirtableImportDialog(project.bases[baseIndex].id)" |
||||
> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<GeneralIcon icon="airtable" class="group-hover:text-accent" /> |
||||
Airtable |
||||
</div> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item |
||||
v-if="isUIAllowed('csvImport', false, projectRole)" |
||||
key="quick-import-csv" |
||||
@click="openQuickImportDialog('csv', project.bases[baseIndex].id)" |
||||
> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<GeneralIcon icon="csv" class="group-hover:text-accent" /> |
||||
CSV file |
||||
</div> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item |
||||
v-if="isUIAllowed('jsonImport', false, projectRole)" |
||||
key="quick-import-json" |
||||
@click="openQuickImportDialog('json', project.bases[baseIndex].id)" |
||||
> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<GeneralIcon icon="json" class="group-hover:text-accent" /> |
||||
JSON file |
||||
</div> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item |
||||
v-if="isUIAllowed('excelImport', false, projectRole)" |
||||
key="quick-import-excel" |
||||
@click="openQuickImportDialog('excel', project.bases[baseIndex].id)" |
||||
> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<GeneralIcon icon="excel" class="group-hover:text-accent" /> |
||||
Microsoft Excel |
||||
</div> |
||||
</a-menu-item> |
||||
</a-menu-item-group> |
||||
|
||||
<a-menu-divider class="my-0" /> |
||||
|
||||
<!-- <a-menu-item-group title="Connect to new datasource" class="!px-0 !mx-0"> |
||||
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MYSQL, project.id)"> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<LogosMysqlIcon class="group-hover:text-accent" /> |
||||
MySQL |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.PG, project.id)"> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<LogosPostgresql class="group-hover:text-accent" /> |
||||
Postgres |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SQLITE, project.id)"> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<VscodeIconsFileTypeSqlite class="group-hover:text-accent" /> |
||||
SQLite |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MSSQL, project.id)"> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<SimpleIconsMicrosoftsqlserver class="group-hover:text-accent" /> |
||||
MSSQL |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item |
||||
v-if="appInfo.ee" |
||||
key="connect-new-source" |
||||
@click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE, project.id)" |
||||
> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<LogosSnowflakeIcon class="group-hover:text-accent" /> |
||||
Snowflake |
||||
</div> |
||||
</a-menu-item> |
||||
</a-menu-item-group> |
||||
|
||||
<a-menu-divider class="my-0" /> --> |
||||
|
||||
<a-menu-item v-if="isUIAllowed('importRequest', false, projectRole)" key="add-new-table" class="py-1 rounded-b"> |
||||
<a |
||||
v-e="['e:datasource:import-request']" |
||||
href="https://github.com/nocodb/nocodb/issues/2052" |
||||
target="_blank" |
||||
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)" |
||||
> |
||||
<GeneralIcon icon="openInNew" class="group-hover:text-accent" /> |
||||
<!-- Request a data source you need? --> |
||||
{{ $t('labels.requestDataSource') }} |
||||
</a> |
||||
</a-menu-item> |
||||
</a-menu> |
||||
</template> |
||||
</a-dropdown> |
||||
</div> |
||||
</template> |
@ -0,0 +1,179 @@
|
||||
<script lang="ts" setup> |
||||
import type { BaseType, ProjectType } from 'nocodb-sdk' |
||||
|
||||
const props = defineProps<{ |
||||
base: BaseType |
||||
project: ProjectType |
||||
}>() |
||||
|
||||
const base = toRef(props, 'base') |
||||
|
||||
const { isUIAllowed } = useUIPermission() |
||||
|
||||
const projectRole = inject(ProjectRoleInj) |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
function openAirtableImportDialog(baseId?: string) { |
||||
if (!baseId) return |
||||
|
||||
$e('a:actions:import-airtable') |
||||
|
||||
const isOpen = ref(true) |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgAirtableImport'), { |
||||
'modelValue': isOpen, |
||||
'baseId': baseId, |
||||
'onUpdate:modelValue': closeDialog, |
||||
}) |
||||
|
||||
function closeDialog() { |
||||
isOpen.value = false |
||||
|
||||
close(1000) |
||||
} |
||||
} |
||||
|
||||
function openQuickImportDialog(type: string) { |
||||
if (!base.value?.id) return |
||||
|
||||
$e(`a:actions:import-${type}`) |
||||
|
||||
const isOpen = ref(true) |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgQuickImport'), { |
||||
'modelValue': isOpen, |
||||
'importType': type, |
||||
'baseId': base.value.id, |
||||
'onUpdate:modelValue': closeDialog, |
||||
}) |
||||
|
||||
function closeDialog() { |
||||
isOpen.value = false |
||||
|
||||
close(1000) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-menu-divider class="my-0" /> |
||||
|
||||
<!-- Quick Import From --> |
||||
<a-sub-menu 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> |
||||
</template> |
||||
|
||||
<template #expandIcon></template> |
||||
|
||||
<a-menu-item |
||||
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 |
||||
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> |
||||
|
||||
<a-menu-item |
||||
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> |
||||
|
||||
<a-menu-divider v-if="false" class="my-0" /> |
||||
|
||||
<!-- Connect to new datasource --> |
||||
<!-- <a-sub-menu> |
||||
<template #title> |
||||
<div class="nc-project-menu-item group"> |
||||
<GeneralIcon icon="datasource" class="group-hover:text-black" /> |
||||
Connect to new datasource |
||||
<div class="flex-1" /> |
||||
|
||||
<MaterialSymbolsChevronRightRounded class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<template #expandIcon></template> |
||||
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MYSQL, project.id)"> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<LogosMysqlIcon class="group-hover:text-black" /> |
||||
MySQL |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.PG, project.id)"> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<LogosPostgresql class="group-hover:text-black" /> |
||||
Postgres |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.SQLITE, project.id)"> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<VscodeIconsFileTypeSqlite class="group-hover:text-black" /> |
||||
SQLite |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item key="connect-new-source" @click="toggleDialog(true, 'dataSources', ClientType.MSSQL, project.id)"> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<SimpleIconsMicrosoftsqlserver class="group-hover:text-black" /> |
||||
MSSQL |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item |
||||
v-if="appInfo.ee" |
||||
key="connect-new-source" |
||||
@click="toggleDialog(true, 'dataSources', ClientType.SNOWFLAKE, project.id)" |
||||
> |
||||
<div class="color-transition nc-project-menu-item group"> |
||||
<LogosSnowflakeIcon class="group-hover:text-black" /> |
||||
Snowflake |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item v-if="isUIAllowed('importRequest', false, projectRole)" key="add-new-table" class="py-1 rounded-b"> |
||||
<a |
||||
v-e="['e:datasource:import-request']" |
||||
href="https://github.com/nocodb/nocodb/issues/2052" |
||||
target="_blank" |
||||
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)" |
||||
> |
||||
<GeneralIcon icon="openInNew" class="group-hover:text-black" /> |
||||
{{ $t('labels.requestDataSource') }} |
||||
</a> |
||||
</a-menu-item> |
||||
</a-sub-menu> --> |
||||
</template> |
@ -0,0 +1,742 @@
|
||||
<script lang="ts" setup> |
||||
import { nextTick } from '@vue/runtime-core' |
||||
import { message } from 'ant-design-vue' |
||||
import type { BaseType, ProjectType, TableType } from 'nocodb-sdk' |
||||
import { LoadingOutlined } from '@ant-design/icons-vue' |
||||
import { useTitle } from '@vueuse/core' |
||||
import { |
||||
NcProjectType, |
||||
ProjectInj, |
||||
ProjectRoleInj, |
||||
ToggleDialogInj, |
||||
extractSdkResponseErrorMsg, |
||||
isElementInvisible, |
||||
openLink, |
||||
storeToRefs, |
||||
useProjects, |
||||
} from '#imports' |
||||
import type { NcProject } from '#imports' |
||||
import { useNuxtApp } from '#app' |
||||
|
||||
const indicator = h(LoadingOutlined, { |
||||
class: '!text-gray-400', |
||||
style: { |
||||
fontSize: '0.85rem', |
||||
}, |
||||
spin: true, |
||||
}) |
||||
|
||||
const router = useRouter() |
||||
const route = router.currentRoute |
||||
|
||||
const { setMenuContext, openRenameTableDialog, duplicateTable, contextMenuTarget } = inject(TreeViewInj)! |
||||
|
||||
const project = inject(ProjectInj)! |
||||
|
||||
const projectsStore = useProjects() |
||||
|
||||
const { loadProject, loadProjects, createProject: _createProject, updateProject, getProjectMetaInfo } = projectsStore |
||||
const { projects } = storeToRefs(projectsStore) |
||||
|
||||
const { loadProjectTables } = useTablesStore() |
||||
const { activeTable } = storeToRefs(useTablesStore()) |
||||
|
||||
const { appInfo, navigateToProject } = useGlobal() |
||||
|
||||
useTabs() |
||||
|
||||
const editMode = ref(false) |
||||
|
||||
const tempTitle = ref('') |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const input = ref<HTMLInputElement>() |
||||
|
||||
const { isUIAllowed } = useUIPermission() |
||||
|
||||
const projectRole = inject(ProjectRoleInj) |
||||
|
||||
const { activeProjectId } = storeToRefs(useProjects()) |
||||
|
||||
const { projectUrl } = useProject() |
||||
|
||||
const toggleDialog = inject(ToggleDialogInj, () => {}) |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
const isOptionsOpen = ref(false) |
||||
const isBasesOptionsOpen = ref<Record<string, boolean>>({}) |
||||
|
||||
const activeKey = ref<string[]>([]) |
||||
const [searchActive] = useToggle() |
||||
const filterQuery = ref('') |
||||
const keys = ref<Record<string, number>>({}) |
||||
const isTableDeleteDialogVisible = ref(false) |
||||
const isProjectDeleteDialogVisible = ref(false) |
||||
|
||||
// If only project is open, i.e in case of docs, project view is open and not the page view |
||||
const projectViewOpen = computed(() => { |
||||
const routeNameSplit = String(route.value?.name).split('projectId-index-index') |
||||
if (routeNameSplit.length <= 1) return false |
||||
|
||||
const routeNameAfterProjectView = routeNameSplit[routeNameSplit.length - 1] |
||||
return routeNameAfterProjectView.split('-').length === 2 || routeNameAfterProjectView.split('-').length === 1 |
||||
}) |
||||
|
||||
const enableEditMode = () => { |
||||
editMode.value = true |
||||
tempTitle.value = project.value.title! |
||||
nextTick(() => { |
||||
input.value?.focus() |
||||
input.value?.select() |
||||
input.value?.scrollIntoView() |
||||
}) |
||||
} |
||||
|
||||
const updateProjectTitle = async () => { |
||||
if (!tempTitle.value) return |
||||
|
||||
try { |
||||
await updateProject(project.value.id!, { |
||||
title: tempTitle.value, |
||||
}) |
||||
editMode.value = false |
||||
tempTitle.value = '' |
||||
|
||||
$e('a:project:rename') |
||||
|
||||
useTitle(`${project.value?.title}`) |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
const { copy } = useCopy(true) |
||||
|
||||
const copyProjectInfo = async () => { |
||||
try { |
||||
if ( |
||||
await copy( |
||||
Object.entries(await getProjectMetaInfo(project.value.id!)!) |
||||
.map(([k, v]) => `${k}: **${v}**`) |
||||
.join('\n'), |
||||
) |
||||
) { |
||||
// Copied to clipboard |
||||
message.info(t('msg.info.copiedToClipboard')) |
||||
} |
||||
} catch (e: any) { |
||||
console.error(e) |
||||
message.error(e.message) |
||||
} |
||||
} |
||||
|
||||
defineExpose({ |
||||
enableEditMode, |
||||
}) |
||||
|
||||
const setIcon = async (icon: string, project: ProjectType) => { |
||||
try { |
||||
const meta = { |
||||
...((project.meta as object) || {}), |
||||
icon, |
||||
} |
||||
|
||||
projectsStore.updateProject(project.id!, { meta: JSON.stringify(meta) }) |
||||
|
||||
$e('a:project:icon:navdraw', { icon }) |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
function openTableCreateDialog(baseIndex?: number | undefined) { |
||||
$e('c:table:create:navdraw') |
||||
|
||||
const isOpen = ref(true) |
||||
let baseId = project.value!.bases?.[0].id |
||||
if (typeof baseIndex === 'number') { |
||||
baseId = project.value!.bases?.[baseIndex].id |
||||
} |
||||
|
||||
if (!baseId || !project.value?.id) return |
||||
|
||||
const { close } = useDialog(resolveComponent('DlgTableCreate'), { |
||||
'modelValue': isOpen, |
||||
baseId, // || bases.value[0].id, |
||||
'projectId': project.value!.id, |
||||
'onCreate': closeDialog, |
||||
'onUpdate:modelValue': () => closeDialog(), |
||||
}) |
||||
|
||||
function closeDialog(table?: TableType) { |
||||
isOpen.value = false |
||||
|
||||
if (!table) return |
||||
|
||||
if (!activeKey.value || !activeKey.value.includes(`collapse-${baseId}`)) { |
||||
activeKey.value.push(`collapse-${baseId}`) |
||||
} |
||||
|
||||
// TODO: Better way to know when the table node dom is available |
||||
setTimeout(() => { |
||||
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' }) |
||||
} |
||||
}, 1000) |
||||
|
||||
close(1000) |
||||
} |
||||
} |
||||
|
||||
const isAddNewProjectChildEntityLoading = ref(false) |
||||
const addNewProjectChildEntity = async () => { |
||||
if (isAddNewProjectChildEntityLoading.value) return |
||||
|
||||
isAddNewProjectChildEntityLoading.value = true |
||||
try { |
||||
openTableCreateDialog() |
||||
|
||||
if (!project.value.isExpanded) { |
||||
project.value.isExpanded = true |
||||
} |
||||
} finally { |
||||
isAddNewProjectChildEntityLoading.value = false |
||||
} |
||||
} |
||||
|
||||
// todo: temp |
||||
const isSharedBase = ref(false) |
||||
|
||||
const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, toggleIsExpanded?: boolean) => { |
||||
if (!project) { |
||||
return |
||||
} |
||||
|
||||
if (toggleIsExpanded) { |
||||
project.isExpanded = !project.isExpanded |
||||
} else { |
||||
project.isExpanded = true |
||||
} |
||||
|
||||
const isProjectPopulated = projectsStore.isProjectPopulated(project.id!) |
||||
|
||||
let isSharedBase = false |
||||
// if shared base ignore navigation |
||||
if (route.value.params.typeOrId === 'base') { |
||||
isSharedBase = true |
||||
} |
||||
|
||||
if (!isProjectPopulated) project.isLoading = true |
||||
|
||||
if (!ignoreNavigation) { |
||||
await navigateTo( |
||||
projectUrl({ |
||||
id: project.id!, |
||||
type: 'database', |
||||
isSharedBase, |
||||
}), |
||||
) |
||||
} |
||||
|
||||
if (!isProjectPopulated) { |
||||
await loadProject(project.id!) |
||||
await loadProjectTables(project.id!) |
||||
} |
||||
|
||||
if (!isProjectPopulated) { |
||||
const updatedProject = projects.value.get(project.id!)! |
||||
updatedProject.isLoading = false |
||||
} |
||||
} |
||||
|
||||
function openErdView(base: BaseType) { |
||||
navigateTo(`/nc/${base.project_id}/erd/${base.id}`) |
||||
} |
||||
|
||||
async function openProjectErdView(_project: ProjectType) { |
||||
if (!_project.id) return |
||||
|
||||
if (!projectsStore.isProjectPopulated(_project.id)) { |
||||
await loadProject(_project.id) |
||||
} |
||||
|
||||
const project = projects.value.get(_project.id) |
||||
|
||||
const base = project?.bases?.[0] |
||||
if (!base) return |
||||
navigateTo(`/nc/${base.project_id}/erd/${base.id}`) |
||||
} |
||||
|
||||
const reloadTables = async () => { |
||||
$e('a:table:refresh:navdraw') |
||||
|
||||
// await loadTables() |
||||
} |
||||
|
||||
const contextMenuBase = computed(() => { |
||||
if (contextMenuTarget.type === 'base') { |
||||
return contextMenuTarget.value |
||||
} else if (contextMenuTarget.type === 'table') { |
||||
const base = project.value?.bases?.find((b) => b.id === contextMenuTarget.value.base_id) |
||||
if (base) return base |
||||
} |
||||
return null |
||||
}) |
||||
|
||||
watch( |
||||
() => activeTable.value?.id, |
||||
async () => { |
||||
if (!activeTable.value) return |
||||
|
||||
const baseId = activeTable.value.base_id |
||||
if (!baseId) return |
||||
|
||||
if (!activeKey.value.includes(`collapse-${baseId}`)) { |
||||
activeKey.value.push(`collapse-${baseId}`) |
||||
} |
||||
}, |
||||
{ |
||||
immediate: true, |
||||
}, |
||||
) |
||||
|
||||
onKeyStroke('Escape', () => { |
||||
if (isOptionsOpen.value) { |
||||
isOptionsOpen.value = false |
||||
} |
||||
|
||||
for (const key of Object.keys(isBasesOptionsOpen.value)) { |
||||
isBasesOptionsOpen.value[key] = false |
||||
} |
||||
}) |
||||
|
||||
const isDuplicateDlgOpen = ref(false) |
||||
const selectedProjectToDuplicate = ref() |
||||
|
||||
const duplicateProject = (project: ProjectType) => { |
||||
selectedProjectToDuplicate.value = project |
||||
isDuplicateDlgOpen.value = true |
||||
} |
||||
const { $jobs } = useNuxtApp() |
||||
|
||||
const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string }) => { |
||||
await loadProjects('workspace') |
||||
|
||||
$jobs.subscribe({ id: jobData.id }, undefined, async (status: string) => { |
||||
if (status === JobStatus.COMPLETED) { |
||||
await loadProjects('workspace') |
||||
|
||||
const project = projects.value.get(jobData.project_id) |
||||
|
||||
// open project after duplication |
||||
if (project) { |
||||
await navigateToProject({ |
||||
projectId: project.id, |
||||
type: project.type, |
||||
}) |
||||
} |
||||
} else if (status === JobStatus.FAILED) { |
||||
message.error('Failed to duplicate project') |
||||
await loadProjects('workspace') |
||||
} |
||||
}) |
||||
|
||||
$e('a:project:duplicate') |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-dropdown :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 }" |
||||
:data-testid="`nc-sidebar-project-${project.title}`" |
||||
:data-project-id="project.id" |
||||
> |
||||
<div class="flex items-center gap-0.75 py-0.25 cursor-pointer" @contextmenu="setMenuContext('project', project)"> |
||||
<div |
||||
ref="projectNodeRefs" |
||||
:class="{ |
||||
'bg-primary-selected active': activeProjectId === project.id && projectViewOpen, |
||||
'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" |
||||
> |
||||
<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" |
||||
@click="onProjectClick(project, true, true)" |
||||
> |
||||
<PhTriangleFill |
||||
class="absolute top-2.25 left-2 invisible 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> |
||||
|
||||
<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" |
||||
:indicator="indicator" |
||||
/> |
||||
|
||||
<LazyGeneralEmojiPicker |
||||
:key="project.meta?.icon" |
||||
:emoji="project.meta?.icon" |
||||
:readonly="true" |
||||
size="small" |
||||
@emoji-selected="setIcon($event, project)" |
||||
> |
||||
<template #default> |
||||
<GeneralProjectIcon :type="project.type" /> |
||||
</template> |
||||
</LazyGeneralEmojiPicker> |
||||
</div> |
||||
</div> |
||||
|
||||
<input |
||||
v-if="editMode" |
||||
ref="input" |
||||
v-model="tempTitle" |
||||
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent w-4/5" |
||||
:class="{ 'text-black font-semibold': activeProjectId === project.id && projectViewOpen }" |
||||
@click.stop |
||||
@keyup.enter="updateProjectTitle" |
||||
@keyup.esc="updateProjectTitle" |
||||
@blur="updateProjectTitle" |
||||
/> |
||||
<span |
||||
v-else |
||||
class="capitalize text-ellipsis overflow-hidden select-none" |
||||
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" |
||||
:class="{ 'text-black font-semibold': activeProjectId === project.id && projectViewOpen }" |
||||
@click="onProjectClick(project)" |
||||
> |
||||
{{ project.title }} |
||||
</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)" |
||||
:class="{ '!text-black !opacity-100': isOptionsOpen }" |
||||
data-testid="nc-sidebar-context-menu" |
||||
@click.stop |
||||
/> |
||||
<template #overlay> |
||||
<a-menu |
||||
class="nc-scrollbar-md" |
||||
:style="{ |
||||
maxHeight: '70vh', |
||||
overflow: 'overlay', |
||||
}" |
||||
@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.edit') }} |
||||
</div> |
||||
</a-menu-item> |
||||
|
||||
<!-- 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') }} {{ $t('objects.project') }} |
||||
</div> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-divider v-if="false" /> |
||||
|
||||
<!-- 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> |
||||
|
||||
<!-- 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> |
||||
</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> |
||||
<template v-if="project.bases && project.bases[0]"> |
||||
<DashboardTreeViewNewBaseOptions v-model:project="project" :base="project.bases[0]" /> |
||||
|
||||
<a-menu-divider /> |
||||
</template> |
||||
|
||||
<a-menu-divider v-if="false" /> |
||||
|
||||
<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> |
||||
</template> |
||||
</a-dropdown> |
||||
|
||||
<div |
||||
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)" |
||||
data-testid="nc-sidebar-add-project-entity" |
||||
:class="{ '!text-black !visible': isAddNewProjectChildEntityLoading, '!visible': isOptionsOpen }" |
||||
@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> |
||||
</div> |
||||
</div> |
||||
|
||||
<div |
||||
v-if="project.id && !project.isLoading" |
||||
key="g1" |
||||
class="overflow-x-hidden transition-max-height" |
||||
:class="{ 'max-h-0': !project.isExpanded }" |
||||
> |
||||
<template v-if="project && project?.bases"> |
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden flex flex-col" :class="{ 'mb-[20px]': isSharedBase }"> |
||||
<div v-if="project?.bases?.[0]?.enabled" class="flex-1"> |
||||
<div class="transition-height duration-200"> |
||||
<DashboardTreeViewNewTableList :project="project" :base-index="0" /> |
||||
</div> |
||||
</div> |
||||
|
||||
<div v-if="project?.bases?.slice(1).filter((el) => el.enabled)?.length" class="transition-height duration-200"> |
||||
<div class="border-none sortable-list"> |
||||
<div v-for="(base, baseIndex) of project.bases" :key="`base-${base.id}`"> |
||||
<template v-if="baseIndex === 0"></template> |
||||
<a-collapse |
||||
v-else-if="base && base.enabled" |
||||
v-model:activeKey="activeKey" |
||||
class="!mx-0 !px-0 nc-sidebar-base-node" |
||||
:class="[{ hidden: searchActive && !!filterQuery }]" |
||||
expand-icon-position="left" |
||||
:bordered="false" |
||||
ghost |
||||
> |
||||
<template #expandIcon="{ isActive }"> |
||||
<div class="flex flex-row items-center -mt-2"> |
||||
<PhTriangleFill |
||||
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 }" |
||||
/> |
||||
</div> |
||||
</template> |
||||
<a-collapse-panel :key="`collapse-${base.id}`"> |
||||
<template #header> |
||||
<div class="min-w-20 w-full flex flex-row"> |
||||
<div |
||||
v-if="baseIndex === 0" |
||||
class="base-context flex items-center gap-2 text-gray-800" |
||||
@contextmenu="setMenuContext('base', base)" |
||||
> |
||||
<GeneralBaseLogo :base-type="base.type" /> |
||||
Default |
||||
</div> |
||||
<div |
||||
v-else |
||||
class="base-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full" |
||||
@contextmenu="setMenuContext('base', base)" |
||||
> |
||||
<GeneralBaseLogo :base-type="base.type" class="min-w-4" /> |
||||
<div |
||||
:data-testid="`nc-sidebar-project-${base.alias}`" |
||||
class="flex capitalize text-ellipsis overflow-hidden select-none" |
||||
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" |
||||
> |
||||
{{ base.alias || '' }} |
||||
</div> |
||||
<a-tooltip> |
||||
<template #title>External DB</template> |
||||
<div> |
||||
<GeneralIcon icon="info" class="text-gray-400 -mt-0.5 hover:text-gray-700 mr-1" /> |
||||
</div> |
||||
</a-tooltip> |
||||
</div> |
||||
<div |
||||
v-if="isUIAllowed('tableCreate', false, projectRole)" |
||||
class="flex flex-row items-center gap-x-0.25 w-12.25" |
||||
> |
||||
<a-dropdown |
||||
:visible="isBasesOptionsOpen[base!.id!]" |
||||
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)" |
||||
:class="{ '!text-black !opacity-100': isBasesOptionsOpen[base!.id!] }" |
||||
@click.stop="isBasesOptionsOpen[base!.id!] = !isBasesOptionsOpen[base!.id!]" |
||||
/> |
||||
<template #overlay> |
||||
<a-menu |
||||
class="nc-scrollbar-md" |
||||
:style="{ |
||||
maxHeight: '70vh', |
||||
overflow: 'overlay', |
||||
}" |
||||
@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> |
||||
|
||||
<DashboardTreeViewNewBaseOptions v-model:project="project" :base="base" /> |
||||
</a-menu> |
||||
</template> |
||||
</a-dropdown> |
||||
|
||||
<div |
||||
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" |
||||
@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> |
||||
</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'}`" |
||||
:nc-base="base.id" |
||||
> |
||||
<DashboardTreeViewNewTableList :project="project" :base-index="baseIndex" /> |
||||
</div> |
||||
</a-collapse-panel> |
||||
</a-collapse> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
</div> |
||||
<template v-if="!isSharedBase" #overlay> |
||||
<a-menu 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"> |
||||
<GeneralIcon icon="edit" class="text-gray-700" /> |
||||
{{ $t('general.rename') }} |
||||
</div> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item |
||||
v-if="isUIAllowed('table-duplicate') && (contextMenuBase?.is_meta || contextMenuBase?.is_local)" |
||||
@click="duplicateTable(contextMenuTarget.value)" |
||||
> |
||||
<div class="nc-project-menu-item"> |
||||
<GeneralIcon icon="duplicate" class="text-gray-700" /> |
||||
{{ $t('general.duplicate') }} |
||||
</div> |
||||
</a-menu-item> |
||||
|
||||
<a-menu-item v-if="isUIAllowed('table-delete')" @click="isTableDeleteDialogVisible = true"> |
||||
<div class="nc-project-menu-item text-red-600"> |
||||
<GeneralIcon icon="delete" /> |
||||
{{ $t('general.delete') }} |
||||
</div> |
||||
</a-menu-item> |
||||
</template> |
||||
|
||||
<template v-else> |
||||
<a-menu-item @click="reloadTables"> |
||||
<div class="nc-project-menu-item"> |
||||
{{ $t('general.reload') }} |
||||
</div> |
||||
</a-menu-item> |
||||
</template> |
||||
</a-menu> |
||||
</template> |
||||
</a-dropdown> |
||||
<DlgTableDelete |
||||
v-if="contextMenuTarget.value?.id && project?.id" |
||||
v-model:visible="isTableDeleteDialogVisible" |
||||
:table-id="contextMenuTarget.value?.id" |
||||
:project-id="project?.id" |
||||
/> |
||||
<DlgProjectDelete v-model:visible="isProjectDeleteDialogVisible" :project-id="project?.id" /> |
||||
<DlgProjectDuplicate |
||||
v-if="selectedProjectToDuplicate" |
||||
v-model="isDuplicateDlgOpen" |
||||
:project="selectedProjectToDuplicate" |
||||
:on-ok="DlgProjectDuplicateOnOk" |
||||
/> |
||||
</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-1 !py-0.75 hover:bg-gray-100 !rounded-md; |
||||
} |
||||
|
||||
:deep(.ant-collapse-header:hover .nc-sidebar-base-node-btns) { |
||||
@apply visible; |
||||
} |
||||
|
||||
:deep(.ant-dropdown-menu-submenu-title) { |
||||
@apply !py-0; |
||||
} |
||||
</style> |
@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup> |
||||
import type { ProjectType } from 'nocodb-sdk' |
||||
import { ProjectInj, ProjectRoleInj } from '#imports' |
||||
|
||||
const props = defineProps<{ |
||||
projectRole: string | string[] |
||||
project: ProjectType |
||||
}>() |
||||
|
||||
const projectRole = toRef(props, 'projectRole') |
||||
const project = toRef(props, 'project') |
||||
|
||||
provide(ProjectRoleInj, projectRole) |
||||
provide(ProjectInj, project) |
||||
</script> |
||||
|
||||
<template> |
||||
<slot /> |
||||
</template> |
@ -0,0 +1,172 @@
|
||||
<script setup lang="ts"> |
||||
import type { ProjectType, TableType } from 'nocodb-sdk' |
||||
import { storeToRefs } from 'pinia' |
||||
import Sortable from 'sortablejs' |
||||
import TableNode from './TableNode.vue' |
||||
import { useNuxtApp } from '#app' |
||||
import { toRef } from '#imports' |
||||
|
||||
const props = withDefaults( |
||||
defineProps<{ |
||||
project: ProjectType |
||||
baseIndex?: number |
||||
}>(), |
||||
{ |
||||
baseIndex: 0, |
||||
}, |
||||
) |
||||
|
||||
const project = toRef(props, 'project') |
||||
const baseIndex = toRef(props, 'baseIndex') |
||||
|
||||
const base = computed(() => project.value?.bases?.[baseIndex.value]) |
||||
|
||||
const { projectTables } = storeToRefs(useTablesStore()) |
||||
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 |
||||
|
||||
return acc |
||||
}, {}), |
||||
) |
||||
|
||||
const keys = ref<Record<string, number>>({}) |
||||
|
||||
const menuRefs = ref<HTMLElement[] | HTMLElement>() |
||||
|
||||
const sortables: Record<string, Sortable> = {} |
||||
|
||||
// todo: replace with vuedraggable |
||||
const initSortable = (el: Element) => { |
||||
const base_id = el.getAttribute('nc-base') |
||||
if (!base_id) return |
||||
|
||||
if (sortables[base_id]) sortables[base_id].destroy() |
||||
Sortable.create(el as HTMLLIElement, { |
||||
onEnd: async (evt) => { |
||||
const offset = tables.value.findIndex((table) => table.base_id === base_id) |
||||
|
||||
const { newIndex = 0, oldIndex = 0 } = evt |
||||
|
||||
if (newIndex === oldIndex) return |
||||
|
||||
const itemEl = evt.item as HTMLLIElement |
||||
const item = tablesById.value[itemEl.dataset.id as string] |
||||
|
||||
// get the html collection of all list items |
||||
const children: HTMLCollection = evt.to.children |
||||
|
||||
// skip if children count is 1 |
||||
if (children.length < 2) return |
||||
|
||||
// get items before and after the moved item |
||||
const itemBeforeEl = children[newIndex - 1] as HTMLLIElement |
||||
const itemAfterEl = children[newIndex + 1] as HTMLLIElement |
||||
|
||||
// get items meta of before and after the moved item |
||||
const itemBefore = itemBeforeEl && tablesById.value[itemBeforeEl.dataset.id as string] |
||||
const itemAfter = itemAfterEl && tablesById.value[itemAfterEl.dataset.id as string] |
||||
|
||||
// set new order value based on the new order of the items |
||||
if (children.length - 1 === evt.newIndex) { |
||||
item.order = (itemBefore.order as number) + 1 |
||||
} else if (newIndex === 0) { |
||||
item.order = (itemAfter.order as number) / 2 |
||||
} else { |
||||
item.order = ((itemBefore.order as number) + (itemAfter.order as number)) / 2 |
||||
} |
||||
|
||||
// update the order of the moved item |
||||
tables.value?.splice(newIndex + offset, 0, ...tables.value?.splice(oldIndex + offset, 1)) |
||||
|
||||
// force re-render the list |
||||
if (keys.value[base_id]) { |
||||
keys.value[base_id] = keys.value[base_id] + 1 |
||||
} else { |
||||
keys.value[base_id] = 1 |
||||
} |
||||
|
||||
// update the item order |
||||
await $api.dbTable.reorder(item.id as string, { |
||||
order: item.order, |
||||
}) |
||||
}, |
||||
animation: 150, |
||||
setData(dataTransfer, dragEl) { |
||||
dataTransfer.setData( |
||||
'text/json', |
||||
JSON.stringify({ |
||||
id: dragEl.dataset.id, |
||||
title: dragEl.dataset.title, |
||||
type: dragEl.dataset.type, |
||||
baseId: dragEl.dataset.baseId, |
||||
}), |
||||
) |
||||
}, |
||||
revertOnSpill: true, |
||||
}) |
||||
} |
||||
|
||||
watchEffect(() => { |
||||
if (menuRefs.value) { |
||||
if (menuRefs.value instanceof HTMLElement) { |
||||
initSortable(menuRefs.value) |
||||
} else { |
||||
menuRefs.value.forEach((el) => initSortable(el)) |
||||
} |
||||
} |
||||
}) |
||||
|
||||
const availableTables = computed(() => { |
||||
return tables.value.filter((table) => table.base_id === project.value?.bases?.[baseIndex.value].id) |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="border-none sortable-list"> |
||||
<template v-if="project"> |
||||
<div |
||||
v-if="availableTables.length === 0" |
||||
class="py-0.5 text-gray-500" |
||||
:class="{ |
||||
'ml-13.55': baseIndex === 0, |
||||
'ml-19.25': baseIndex !== 0, |
||||
}" |
||||
> |
||||
Empty |
||||
</div> |
||||
<div |
||||
v-if="project.bases?.[baseIndex] && project!.bases[baseIndex].enabled" |
||||
ref="menuRefs" |
||||
:key="`sortable-${base?.id}-${base?.id && base?.id in keys ? keys[base?.id] : '0'}`" |
||||
:nc-base="base?.id" |
||||
> |
||||
<TableNode |
||||
v-for="table of availableTables" |
||||
:key="table.id" |
||||
v-e="['a:table:open']" |
||||
class="nc-tree-item text-sm cursor-pointer group" |
||||
: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> |
||||
</template> |
||||
</div> |
||||
</template> |