* 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> |