diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index e663273e91..5b9eb669a2 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -11,6 +11,7 @@ on: - "packages/nocodb/**" - ".github/workflows/ci-cd.yml" pull_request: + types: [ready_for_review] branches: [develop] paths: - "packages/nc-gui/**" diff --git a/.github/workflows/release-draft.yml b/.github/workflows/release-draft.yml index dcdfbbbdc3..dbcb3c9744 100644 --- a/.github/workflows/release-draft.yml +++ b/.github/workflows/release-draft.yml @@ -49,9 +49,9 @@ jobs: # the SHA from the third commit (i.e. Auto PR from master to develop) will be taken # else HEAD will be taken run: | - TARGET_SHA=$(git rev-parse HEAD~2) + TARGET_SHA=$(git rev-list -n 3 HEAD | tail -1) if [[ ${{ github.event.inputs.tagHeadSHA || inputs.tagHeadSHA }} == "Y" ]]; then - TARGET_SHA=$(git rev-parse HEAD) + TARGET_SHA=$(git rev-list -n 1 HEAD | tail -1) fi echo "::set-output name=TARGET_SHA::${TARGET_SHA}" echo "Setting TARGET_SHA: ${TARGET_SHA}" diff --git a/packages/nc-gui/assets/style.scss b/packages/nc-gui/assets/style.scss index 78353c8cc8..1a899a48b1 100644 --- a/packages/nc-gui/assets/style.scss +++ b/packages/nc-gui/assets/style.scss @@ -1,4 +1,6 @@ @import 'ant-design-vue/dist/antd.variable.min.css'; +@import '@braks/vue-flow/dist/style.css'; +@import '@braks/vue-flow/dist/theme-default.css'; :root { --header-height: 42px; @@ -248,3 +250,8 @@ a { .ant-dropdown-menu-submenu-title{ @apply !pr-2; } + +.vue-flow__minimap { + transform: scale(75%); + transform-origin: bottom right; +} diff --git a/packages/nc-gui/components.d.ts b/packages/nc-gui/components.d.ts index dbc76afd2d..7e914eb710 100644 --- a/packages/nc-gui/components.d.ts +++ b/packages/nc-gui/components.d.ts @@ -143,6 +143,7 @@ declare module '@vue/runtime-core' { MdiEmailArrowRightOutline: typeof import('~icons/mdi/email-arrow-right-outline')['default'] MdiExitToApp: typeof import('~icons/mdi/exit-to-app')['default'] MdiExport: typeof import('~icons/mdi/export')['default'] + MdiEyeCircleOutline: typeof import('~icons/mdi/eye-circle-outline')['default'] MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default'] MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default'] MdiFileExcel: typeof import('~icons/mdi/file-excel')['default'] @@ -175,6 +176,7 @@ declare module '@vue/runtime-core' { MdiMoonFull: typeof import('~icons/mdi/moon-full')['default'] MdiNumeric: typeof import('~icons/mdi/numeric')['default'] MdiOpenInNew: typeof import('~icons/mdi/open-in-new')['default'] + MdiOpenInNewIcon: typeof import('~icons/mdi/open-in-new-icon')['default'] MdiPencil: typeof import('~icons/mdi/pencil')['default'] MdiPlus: typeof import('~icons/mdi/plus')['default'] MdiPlusCircleOutline: typeof import('~icons/mdi/plus-circle-outline')['default'] @@ -190,6 +192,7 @@ declare module '@vue/runtime-core' { MdiStarOutline: typeof import('~icons/mdi/star-outline')['default'] MdiTable: typeof import('~icons/mdi/table')['default'] MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default'] + MdiTableLarge: typeof import('~icons/mdi/table-large')['default'] MdiText: typeof import('~icons/mdi/text')['default'] MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default'] MdiTrashCan: typeof import('~icons/mdi/trash-can')['default'] diff --git a/packages/nc-gui/components/dashboard/settings/Erd.vue b/packages/nc-gui/components/dashboard/settings/Erd.vue new file mode 100644 index 0000000000..4ccfe17aeb --- /dev/null +++ b/packages/nc-gui/components/dashboard/settings/Erd.vue @@ -0,0 +1,5 @@ + diff --git a/packages/nc-gui/components/dashboard/settings/Misc.vue b/packages/nc-gui/components/dashboard/settings/Misc.vue index a9ce1f6d2e..fc98644a58 100644 --- a/packages/nc-gui/components/dashboard/settings/Misc.vue +++ b/packages/nc-gui/components/dashboard/settings/Misc.vue @@ -10,7 +10,7 @@ watch(includeM2M, async () => await loadTables())
- {{ + {{ $t('msg.info.showM2mTables') }}
diff --git a/packages/nc-gui/components/dashboard/settings/Modal.vue b/packages/nc-gui/components/dashboard/settings/Modal.vue index e9f7546f3f..52dba591fc 100644 --- a/packages/nc-gui/components/dashboard/settings/Modal.vue +++ b/packages/nc-gui/components/dashboard/settings/Modal.vue @@ -5,6 +5,7 @@ import AppStore from './AppStore.vue' import Metadata from './Metadata.vue' import UIAcl from './UIAcl.vue' import Misc from './Misc.vue' +import Erd from './Erd.vue' import { useNuxtApp } from '#app' import { useI18n, useUIPermission, useVModel, watch } from '#imports' import ApiTokenManagement from '~/components/tabs/auth/ApiTokenManagement.vue' @@ -90,7 +91,7 @@ const tabsInfo: TabGroup = { $e('c:settings:appstore') }, }, - metaData: { + projMetaData: { // Project Metadata title: t('title.projMeta'), icon: MultipleTableIcon, @@ -108,6 +109,13 @@ const tabsInfo: TabGroup = { $e('c:table:ui-acl') }, }, + erd: { + title: t('title.erdView'), + body: Erd, + onClick: () => { + $e('c:settings:erd') + }, + }, misc: { title: t('general.misc'), body: Misc, diff --git a/packages/nc-gui/components/dlg/AirtableImport.vue b/packages/nc-gui/components/dlg/AirtableImport.vue index 0aeca558e4..f5278df1cf 100644 --- a/packages/nc-gui/components/dlg/AirtableImport.vue +++ b/packages/nc-gui/components/dlg/AirtableImport.vue @@ -65,8 +65,8 @@ const syncSource = ref({ }) const validators = computed(() => ({ - 'details.apiKey': [fieldRequiredValidator], - 'details.syncSourceUrlOrId': [fieldRequiredValidator], + 'details.apiKey': [fieldRequiredValidator()], + 'details.syncSourceUrlOrId': [fieldRequiredValidator()], })) const dialogShow = computed({ diff --git a/packages/nc-gui/components/dlg/QuickImport.vue b/packages/nc-gui/components/dlg/QuickImport.vue index 4d98ffbff2..d7e2567063 100644 --- a/packages/nc-gui/components/dlg/QuickImport.vue +++ b/packages/nc-gui/components/dlg/QuickImport.vue @@ -70,8 +70,8 @@ const isImportTypeCsv = computed(() => importType === 'csv') const IsImportTypeExcel = computed(() => importType === 'excel') const validators = computed(() => ({ - url: [fieldRequiredValidator, importUrlValidator, isImportTypeCsv.value ? importCsvUrlValidator : importExcelUrlValidator], - maxRowsToParse: [fieldRequiredValidator], + url: [fieldRequiredValidator(), importUrlValidator, isImportTypeCsv.value ? importCsvUrlValidator : importExcelUrlValidator], + maxRowsToParse: [fieldRequiredValidator()], })) const { validate, validateInfos } = useForm(importState, validators) diff --git a/packages/nc-gui/components/erd/Flow.vue b/packages/nc-gui/components/erd/Flow.vue new file mode 100644 index 0000000000..3568a486ae --- /dev/null +++ b/packages/nc-gui/components/erd/Flow.vue @@ -0,0 +1,227 @@ + + + diff --git a/packages/nc-gui/components/erd/RelationEdge.vue b/packages/nc-gui/components/erd/RelationEdge.vue new file mode 100644 index 0000000000..fec3122b9d --- /dev/null +++ b/packages/nc-gui/components/erd/RelationEdge.vue @@ -0,0 +1,161 @@ + + + + + + + diff --git a/packages/nc-gui/components/erd/TableNode.vue b/packages/nc-gui/components/erd/TableNode.vue new file mode 100644 index 0000000000..0c753e9856 --- /dev/null +++ b/packages/nc-gui/components/erd/TableNode.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/packages/nc-gui/components/erd/View.vue b/packages/nc-gui/components/erd/View.vue new file mode 100644 index 0000000000..d480963e83 --- /dev/null +++ b/packages/nc-gui/components/erd/View.vue @@ -0,0 +1,162 @@ + + + diff --git a/packages/nc-gui/components/smartsheet-header/VirtualCell.vue b/packages/nc-gui/components/smartsheet-header/VirtualCell.vue index 8d4d458831..eeb5eea8eb 100644 --- a/packages/nc-gui/components/smartsheet-header/VirtualCell.vue +++ b/packages/nc-gui/components/smartsheet-header/VirtualCell.vue @@ -16,7 +16,7 @@ import { useVirtualCell, } from '#imports' -const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean; required?: boolean | number }>() +const props = defineProps<{ column: ColumnType; hideMenu?: boolean; required?: boolean | number }>() const { t } = useI18n() diff --git a/packages/nc-gui/components/smartsheet-toolbar/Erd.vue b/packages/nc-gui/components/smartsheet-toolbar/Erd.vue new file mode 100644 index 0000000000..c1a131fdea --- /dev/null +++ b/packages/nc-gui/components/smartsheet-toolbar/Erd.vue @@ -0,0 +1,42 @@ + + + diff --git a/packages/nc-gui/components/smartsheet-toolbar/FieldsMenu.vue b/packages/nc-gui/components/smartsheet-toolbar/FieldsMenu.vue index 537a830a78..3c8bafe524 100644 --- a/packages/nc-gui/components/smartsheet-toolbar/FieldsMenu.vue +++ b/packages/nc-gui/components/smartsheet-toolbar/FieldsMenu.vue @@ -26,6 +26,8 @@ const activeView = inject(ActiveViewInj, ref()) const reloadDataHook = inject(ReloadViewDataHookInj)! +const reloadViewMetaHook = inject(ReloadViewMetaHookInj)! + const rootFields = inject(FieldsInj) const isLocked = inject(IsLockedInj, ref(false)) @@ -93,7 +95,7 @@ const coverImageColumnId = computed({ fk_cover_image_col_id: val, }) ;(activeView.value?.view as GalleryType).fk_cover_image_col_id = val - reloadDataHook.trigger() + reloadViewMetaHook.trigger() } }, }) diff --git a/packages/nc-gui/components/smartsheet-toolbar/ViewActions.vue b/packages/nc-gui/components/smartsheet-toolbar/ViewActions.vue index f095ffa5d5..f789ffae0f 100644 --- a/packages/nc-gui/components/smartsheet-toolbar/ViewActions.vue +++ b/packages/nc-gui/components/smartsheet-toolbar/ViewActions.vue @@ -18,6 +18,7 @@ import { LockType } from '~/lib' import MdiLockOutlineIcon from '~icons/mdi/lock-outline' import MdiAccountIcon from '~icons/mdi/account' import MdiAccountGroupIcon from '~icons/mdi/account-group' +import AcountTreeRoundedIcon from '~icons/material-symbols/account-tree-rounded' const { t } = useI18n() @@ -37,6 +38,8 @@ const showWebhookDrawer = ref(false) const showApiSnippetDrawer = ref(false) +const showErd = ref(false) + const quickImportDialog = ref(false) const { isUIAllowed } = useUIPermission() @@ -160,9 +163,8 @@ const { isSqlView } = useSmartsheetStoreOrThrow() - +
- -
+ +
{{ $t('activity.listSharedView') }} @@ -200,18 +197,19 @@ const { isSqlView } = useSmartsheetStoreOrThrow() {{ $t('objects.webhooks') }}
- -
+ +
{{ $t('activity.getApiSnippet') }}
+ +
+ + {{ $t('title.erdView') }} +
+
@@ -221,6 +219,8 @@ const { isSqlView } = useSmartsheetStoreOrThrow() + + +import { onMounted } from '@vue/runtime-core' import { isVirtualCol } from 'nocodb-sdk' import { ActiveViewInj, @@ -11,7 +12,8 @@ import { OpenNewRecordFormHookInj, PaginationDataInj, ReadonlyInj, - ReloadViewDataHookInj, + ReloadViewMetaHookInj, + extractPkFromRow, inject, provide, useViewData, @@ -26,7 +28,7 @@ interface Attachment { const meta = inject(MetaInj, ref()) const view = inject(ActiveViewInj, ref()) -const reloadViewDataHook = inject(ReloadViewDataHookInj) +const reloadViewMetaHook = inject(ReloadViewMetaHookInj) const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook()) const expandedFormDlg = ref(false) @@ -54,6 +56,10 @@ provide(ReadonlyInj, !isUIAllowed('xcDatatableEditable')) const fields = inject(FieldsInj, ref([])) +const route = useRoute() + +const router = useRouter() + const fieldsWithoutCover = computed(() => fields.value.filter((f) => f.id !== galleryData.value?.fk_cover_image_col_id)) const coverImageColumn: any = $( @@ -64,17 +70,6 @@ const coverImageColumn: any = $( ), ) -watch( - [meta, view], - async () => { - if (meta?.value && view?.value) { - await loadData() - await loadGalleryData() - } - }, - { immediate: true }, -) - const isRowEmpty = (record: any, col: any) => { const val = record.row[col.title] if (!val) return true @@ -90,22 +85,23 @@ const attachments = (record: any): Array => { } } -const reloadAttachments = ref(false) +const expandForm = (row: RowType, _state?: Record) => { + if (!isUIAllowed('xcDatatableEditable')) return -reloadViewDataHook?.on(async () => { - await loadData() - await loadGalleryData() - reloadAttachments.value = true - nextTick(() => { - reloadAttachments.value = false - }) -}) + const rowId = extractPkFromRow(row.row, meta.value.columns) -const expandForm = (row: RowType, state?: Record) => { - if (!isUIAllowed('xcDatatableEditable')) return - expandedFormRow.value = row - expandedFormRowState.value = state - expandedFormDlg.value = true + if (rowId) { + router.push({ + query: { + ...route.query, + rowId, + }, + }) + } else { + expandedFormRow.value = row + expandedFormRowState.value = state + expandedFormDlg.value = true + } } const expandFormClick = async (e: MouseEvent, row: RowType) => { @@ -119,10 +115,40 @@ openNewRecordFormHook?.on(async () => { const newRow = await addEmptyRow() expandForm(newRow) }) + +const expandedFormOnRowIdDlg = computed({ + get() { + return !!route.query.rowId + }, + set(val) { + if (!val) + router.push({ + query: { + ...route.query, + rowId: undefined, + }, + }) + }, +}) + +const reloadAttachments = ref(false) + +reloadViewMetaHook?.on(async () => { + await loadGalleryData() + reloadAttachments.value = true + nextTick(() => { + reloadAttachments.value = false + }) +}) + +onMounted(async () => { + await loadData() + await loadGalleryData() +})