diff --git a/.github/workflows/cleanup-caches-by-branch.yml b/.github/workflows/cleanup-caches-by-branch.yml new file mode 100644 index 0000000000..11dcee031d --- /dev/null +++ b/.github/workflows/cleanup-caches-by-branch.yml @@ -0,0 +1,34 @@ +name: cleanup caches by branch +on: + pull_request: + types: + - closed +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + + REPO=${{ github.repository }} + + # get the branch + BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" + + # fetch list of cache key + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 ) + + # set this to not fail the workflow while deleting cache keys + set +e + + # delete cache key + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + done + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/package.json b/package.json index abd83cc0e8..725df55cd0 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,8 @@ "install:local:dep": "cd packages/nc-lib-gui;npm uninstall -S xc-lib;rm package-lock.json; npm i ../../../xc-lib-private; cd ../xc-instant;npm uninstall -S xc-lib xc-lib-gui;npm i ../../../xc-lib-private;npm i ../xc-lib-gui", "install:npm:dep": "cd packages/nc-lib-gui;npm uninstall -S xc-lib; npm i -S xc-lib@latest; cd ../xc-instant;npm uninstall -S xc-lib xc-lib-gui;npm i -S xc-lib@latest xc-lib-gui@latest;npm i ../xc-lib-gui", "prepare": "husky install", + "start:mysql": "docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d", + "stop:mysql": "docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml down", "start:pg": "docker-compose -f ./tests/playwright/scripts/docker-compose-pg.yml up -d", "stop:pg": "docker-compose -f ./tests/playwright/scripts/docker-compose-pg.yml down" }, diff --git a/packages/nc-gui/assets/style.scss b/packages/nc-gui/assets/style.scss index 0894200782..39a800d94b 100644 --- a/packages/nc-gui/assets/style.scss +++ b/packages/nc-gui/assets/style.scss @@ -312,7 +312,7 @@ a { .ant-btn-loading-icon{ & > span { - @apply block bg-red-500 + @apply block; } } diff --git a/packages/nc-gui/components.d.ts b/packages/nc-gui/components.d.ts index cbd44ab30b..2a111be52c 100644 --- a/packages/nc-gui/components.d.ts +++ b/packages/nc-gui/components.d.ts @@ -81,7 +81,6 @@ declare module '@vue/runtime-core' { ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default'] EvaEmailOutline: typeof import('~icons/eva/email-outline')['default'] IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default'] - Icon: typeof import('~icons/ic/on')['default'] IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default'] IcRoundEdit: typeof import('~icons/ic/round-edit')['default'] IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default'] @@ -235,6 +234,7 @@ declare module '@vue/runtime-core' { MdiTable: typeof import('~icons/mdi/table')['default'] MdiTableColumnPlusAfter: typeof import('~icons/mdi/table-column-plus-after')['default'] MdiTableColumnPlusBefore: typeof import('~icons/mdi/table-column-plus-before')['default'] + MdiTableKey: typeof import('~icons/mdi/table-key')['default'] MdiTableLarge: typeof import('~icons/mdi/table-large')['default'] MdiText: typeof import('~icons/mdi/text')['default'] MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default'] diff --git a/packages/nc-gui/components/cell/attachment/Carousel.vue b/packages/nc-gui/components/cell/attachment/Carousel.vue index bf0bf3983b..78ac360651 100644 --- a/packages/nc-gui/components/cell/attachment/Carousel.vue +++ b/packages/nc-gui/components/cell/attachment/Carousel.vue @@ -1,7 +1,7 @@ + + diff --git a/packages/nc-gui/components/cell/attachment/Modal.vue b/packages/nc-gui/components/cell/attachment/Modal.vue index ff9bbc0cdb..d8df476f92 100644 --- a/packages/nc-gui/components/cell/attachment/Modal.vue +++ b/packages/nc-gui/components/cell/attachment/Modal.vue @@ -2,7 +2,7 @@ import { onKeyDown } from '@vueuse/core' import { useAttachmentCell } from './utils' import { useSortable } from './sort' -import { isImage, openLink, ref, useDropZone, useUIPermission, watch } from '#imports' +import { isImage, ref, useAttachment, useDropZone, useUIPermission, watch } from '#imports' const { isUIAllowed } = useUIPermission() @@ -38,6 +38,8 @@ const { isOverDropZone } = useDropZone(dropZoneRef, onDrop) const { isSharedForm } = useSmartsheetStoreOrThrow() +const { getPossibleAttachmentSrc, openAttachment } = useAttachment() + onKeyDown('Escape', () => { modalVisible.value = false isOverDropZone.value = false @@ -159,12 +161,12 @@ function onRemoveFileClick(title: any, i: number) {
-
@@ -173,10 +175,10 @@ function onRemoveFileClick(title: any, i: number) { v-else-if="item.icon" height="150" width="150" - @click.stop="openLink(item.url || item.data)" + @click.stop="openAttachment(item)" /> - +
diff --git a/packages/nc-gui/components/cell/attachment/index.vue b/packages/nc-gui/components/cell/attachment/index.vue index 7cf0364f86..a43fd39bf4 100644 --- a/packages/nc-gui/components/cell/attachment/index.vue +++ b/packages/nc-gui/components/cell/attachment/index.vue @@ -10,8 +10,8 @@ import { inject, isImage, nextTick, - openLink, ref, + useAttachment, useDropZone, useSelectedCellKeyupListener, useSmartsheetRowStoreOrThrow, @@ -46,6 +46,8 @@ const currentCellRef = ref(dropZoneInjection.value) const { cellRefs, isSharedForm } = useSmartsheetStoreOrThrow()! +const { getPossibleAttachmentSrc, openAttachment } = useAttachment() + const { isPublic, isForm, @@ -60,7 +62,6 @@ const { selectedImage, isReadonly, storedFiles, - getAttachmentUrl, } = useProvideAttachmentCell(updateModelValue) watch( @@ -101,16 +102,7 @@ watch( async (nextModel) => { if (nextModel) { try { - let nextAttachments = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean) - - // reconstruct the url - // See /packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts for the details - nextAttachments = await Promise.all( - nextAttachments.map(async (attachment: any) => ({ - ...attachment, - url: await getAttachmentUrl(attachment), - })), - ) + const nextAttachments = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean) if (isPublic.value && isForm.value) { storedFiles.value = nextAttachments @@ -229,21 +221,12 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e) => { - - - -
+
+
diff --git a/packages/nc-gui/components/cell/attachment/utils.ts b/packages/nc-gui/components/cell/attachment/utils.ts index 6b202c519c..438ae5c599 100644 --- a/packages/nc-gui/components/cell/attachment/utils.ts +++ b/packages/nc-gui/components/cell/attachment/utils.ts @@ -14,6 +14,7 @@ import { message, ref, useApi, + useAttachment, useFileDialog, useI18n, useInjectionState, @@ -60,6 +61,8 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState( const { t } = useI18n() + const { getAttachmentSrc } = useAttachment() + const defaultAttachmentMeta = { ...(appInfo.value.ee && { // Maximum Number of Attachments per cell @@ -226,31 +229,12 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState( /** download a file */ async function downloadFile(item: AttachmentType) { - ;(await import('file-saver')).saveAs(item.url || item.data, item.title) - } - - /** construct the attachment url - * See /packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts for the details - * */ - async function getAttachmentUrl(item: AttachmentType) { - const path = item?.path - // if path doesn't exist, use `item.url` - if (path) { - // try ${appInfo.value.ncSiteUrl}/${item.path} first - const url = `${appInfo.value.ncSiteUrl}/${item.path}` - try { - const res = await fetch(url) - if (res.ok) { - // use `url` if it is accessible - return Promise.resolve(url) - } - } catch { - // for some cases, `url` is not accessible as expected - // do nothing here - } + const src = await getAttachmentSrc(item) + if (src) { + ;(await import('file-saver')).saveAs(src, item.title) + } else { + message.error('Failed to download file') } - // if it fails, use the original url - return Promise.resolve(item.url) } const FileIcon = (icon: string) => { @@ -294,7 +278,6 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState( storedFiles, bulkDownloadFiles, defaultAttachmentMeta, - getAttachmentUrl, } }, 'useAttachmentCell', diff --git a/packages/nc-gui/components/dashboard/TreeView.vue b/packages/nc-gui/components/dashboard/TreeView.vue index 3f0e23100b..2c028ad472 100644 --- a/packages/nc-gui/components/dashboard/TreeView.vue +++ b/packages/nc-gui/components/dashboard/TreeView.vue @@ -35,7 +35,7 @@ const { addTab, updateTab } = useTabs() const { $api, $e } = useNuxtApp() -const { project, loadProject, bases, tables, loadTables, isSharedBase } = useProject() +const { bases, tables, loadTables, isSharedBase } = useProject() const { activeTab } = useTabs() @@ -324,12 +324,6 @@ const setIcon = async (icon: string, table: TableType) => { message.error(await extractSdkResponseErrorMsg(e)) } } - -onMounted(async () => { - if (!project.value?.id) { - await loadProject() - } -}) - + diff --git a/packages/nc-gui/components/smartsheet/Grid.vue b/packages/nc-gui/components/smartsheet/Grid.vue index 9ae398dc51..d33ffa5507 100644 --- a/packages/nc-gui/components/smartsheet/Grid.vue +++ b/packages/nc-gui/components/smartsheet/Grid.vue @@ -714,9 +714,9 @@ const closeAddColumnDropdown = () => { @contextmenu="showContextMenu" > - - -
+ + +
- + diff --git a/packages/nc-gui/components/smartsheet/header/Menu.vue b/packages/nc-gui/components/smartsheet/header/Menu.vue index b1fbd20a6a..62e019b0d2 100644 --- a/packages/nc-gui/components/smartsheet/header/Menu.vue +++ b/packages/nc-gui/components/smartsheet/header/Menu.vue @@ -74,6 +74,8 @@ const setAsPrimaryValue = async () => { await getMeta(meta?.value?.id as string, true) + eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD) + // Successfully updated as primary column message.success(t('msg.success.primaryColumnUpdated')) @@ -154,6 +156,7 @@ const duplicateColumn = async () => { await $api.dbTableColumn.create(meta!.value!.id!, { ...columnCreatePayload, + pv: false, column_order: { order: newColumnOrder, view_id: view.value?.id as string, @@ -241,7 +244,7 @@ const hideField = async () => { - +
@@ -268,7 +271,7 @@ const hideField = async () => { {{ t('general.insertAfter') }}
- +
@@ -277,7 +280,7 @@ const hideField = async () => { - +
@@ -287,7 +290,7 @@ const hideField = async () => {
- +
diff --git a/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue b/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue index bb06fb9659..8567956e6f 100644 --- a/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue +++ b/packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue @@ -68,6 +68,12 @@ watch( const numberOfHiddenFields = computed(() => filteredFieldList.value?.filter((field) => !field.show)?.length) +const gridPrimaryValueField = computed(() => { + if (activeView.value?.type !== ViewTypes.GRID) return null + const pvCol = Object.values(metaColumnById.value)?.find((col) => col?.pv) + return filteredFieldList.value?.find((field) => field.fk_column_id === pvCol?.id) +}) + const onMove = (_event: { moved: { newIndex: number } }) => { // todo : sync with server if (!fields.value) return @@ -188,7 +194,7 @@ useMenuCloseOnEsc(open) +
diff --git a/packages/nc-gui/composables/useApi/interceptors.ts b/packages/nc-gui/composables/useApi/interceptors.ts index 22e2c139ff..a2f4cc5daa 100644 --- a/packages/nc-gui/composables/useApi/interceptors.ts +++ b/packages/nc-gui/composables/useApi/interceptors.ts @@ -1,12 +1,12 @@ import type { Api } from 'nocodb-sdk' -import { navigateTo, useGlobal, useRoute, useRouter } from '#imports' +import { navigateTo, useGlobal, useRouter } from '#imports' const DbNotFoundMsg = 'Database config not found' export function addAxiosInterceptors(api: Api) { const state = useGlobal() const router = useRouter() - const route = useRoute() + const route = $(router.currentRoute) api.instance.interceptors.request.use((config) => { config.headers['xc-gui'] = 'true' @@ -40,7 +40,6 @@ export function addAxiosInterceptors(api: Api) { // Logout user if token refresh didn't work or user is disabled if (error.config.url === '/auth/token/refresh') { state.signOut() - return Promise.reject(error) } diff --git a/packages/nc-gui/composables/useAttachment.ts b/packages/nc-gui/composables/useAttachment.ts new file mode 100644 index 0000000000..637c2666a4 --- /dev/null +++ b/packages/nc-gui/composables/useAttachment.ts @@ -0,0 +1,40 @@ +import { mimeTypes, openLink, useGlobal } from '#imports' + +const useAttachment = () => { + const { appInfo } = useGlobal() + + const getPossibleAttachmentSrc = (item: Record) => { + const res: string[] = [] + if (item?.path) res.push(`${appInfo.value.ncSiteUrl}/${item.path}`) + if (item?.url) res.push(item.url) + return res + } + + const getAttachmentSrc = async (item: Record) => { + if (item?.data) { + return item.data + } + const sources = getPossibleAttachmentSrc(item) + const mimeType = mimeTypes[item?.mimetype?.split('/')?.pop() || 'txt'] + for (const source of sources) { + // test if the source is accessible or not + const res = await fetch(source) + if (res.ok && res.headers.get('Content-Type') === mimeType) { + return source + } + } + return null + } + + const openAttachment = async (item: Record) => { + openLink(await getAttachmentSrc(item)) + } + + return { + getAttachmentSrc, + getPossibleAttachmentSrc, + openAttachment, + } +} + +export default useAttachment diff --git a/packages/nc-gui/composables/useCellUrlConfig.ts b/packages/nc-gui/composables/useCellUrlConfig.ts index d5604445c5..256ea11dcc 100644 --- a/packages/nc-gui/composables/useCellUrlConfig.ts +++ b/packages/nc-gui/composables/useCellUrlConfig.ts @@ -1,5 +1,5 @@ import type { MaybeRef } from '@vueuse/core' -import { computed, unref, useRoute } from '#imports' +import { computed, unref, useRouter } from '#imports' export interface CellUrlOptions { behavior?: string @@ -21,7 +21,9 @@ const parseUrlRules = (serialized?: string): ParsedRules[] | undefined => { } export function useCellUrlConfig(url?: MaybeRef) { - const route = useRoute() + const router = useRouter() + + const route = $(router.currentRoute) const config = $computed(() => ({ behavior: route.query.url_behavior as string | undefined, diff --git a/packages/nc-gui/composables/useDashboard.ts b/packages/nc-gui/composables/useDashboard.ts index 34eef3a735..e49d5b53f6 100644 --- a/packages/nc-gui/composables/useDashboard.ts +++ b/packages/nc-gui/composables/useDashboard.ts @@ -1,7 +1,9 @@ -import { computed, useRoute } from '#imports' +import { computed, useRouter } from '#imports' export function useDashboard() { - const route = useRoute() + const router = useRouter() + + const route = $(router.currentRoute) const dashboardUrl = computed(() => { // todo: test in different scenarios diff --git a/packages/nc-gui/composables/useGlobal/actions.ts b/packages/nc-gui/composables/useGlobal/actions.ts index 3dcddc296a..8dc1ffc849 100644 --- a/packages/nc-gui/composables/useGlobal/actions.ts +++ b/packages/nc-gui/composables/useGlobal/actions.ts @@ -28,19 +28,22 @@ export function useGlobalActions(state: State): Actions { const nuxtApp = useNuxtApp() const t = nuxtApp.vueApp.i18n.global.t - nuxtApp.$api.instance - .post('/auth/token/refresh', null, { - withCredentials: true, - }) - .then((response) => { - if (response.data?.token) { - signIn(response.data.token) - } - }) - .catch((err) => { - message.error(err.message || t('msg.error.youHaveBeenSignedOut')) - signOut() - }) + return new Promise((resolve) => { + nuxtApp.$api.instance + .post('/auth/token/refresh', null, { + withCredentials: true, + }) + .then((response) => { + if (response.data?.token) { + signIn(response.data.token) + } + }) + .catch((err) => { + message.error(err.message || t('msg.error.youHaveBeenSignedOut')) + signOut() + }) + .finally(resolve) + }) } const loadAppInfo = async () => { diff --git a/packages/nc-gui/composables/useGlobal/index.ts b/packages/nc-gui/composables/useGlobal/index.ts index 31599e1cf1..81556a715c 100644 --- a/packages/nc-gui/composables/useGlobal/index.ts +++ b/packages/nc-gui/composables/useGlobal/index.ts @@ -53,7 +53,7 @@ export const useGlobal = createGlobalState((): UseGlobalReturn => { state.jwtPayload.value.exp && state.jwtPayload.value.exp - 5 * 60 < state.timestamp.value / 1000 ), - async (expiring) => { + async (expiring: boolean) => { if (getters.signedIn.value && state.jwtPayload.value && expiring) { await actions.refreshToken() } diff --git a/packages/nc-gui/composables/useKanbanViewStore.ts b/packages/nc-gui/composables/useKanbanViewStore.ts index a9b46f8f4e..d960bc35d5 100644 --- a/packages/nc-gui/composables/useKanbanViewStore.ts +++ b/packages/nc-gui/composables/useKanbanViewStore.ts @@ -1,15 +1,5 @@ import type { ComputedRef, Ref } from 'vue' -import type { - Api, - AttachmentType, - ColumnType, - KanbanType, - SelectOptionType, - SelectOptionsType, - TableType, - ViewType, -} from 'nocodb-sdk' -import { UITypes } from 'nocodb-sdk' +import type { Api, ColumnType, KanbanType, SelectOptionType, SelectOptionsType, TableType, ViewType } from 'nocodb-sdk' import type { Row } from '~/lib' import { IsPublicInj, @@ -25,7 +15,6 @@ import { ref, useApi, useFieldQuery, - useGlobal, useI18n, useInjectionState, useNuxtApp, @@ -55,8 +44,6 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState( const { $e, $api } = useNuxtApp() - const { appInfo } = useGlobal() - const { sorts, nestedFilters } = useSmartsheetStoreOrThrow() const { sharedView, fetchSharedViewData, fetchSharedViewGroupedData } = useSharedView() @@ -91,10 +78,6 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState( return where }) - const attachmentColumns = computed(() => - (meta.value?.columns as ColumnType[])?.filter((c) => c.uidt === UITypes.Attachment).map((c) => c.title), - ) - provide(SharedViewPasswordInj, password) // kanban view meta data @@ -144,27 +127,6 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState( rowMeta: {}, })) - async function getAttachmentUrl(item: AttachmentType) { - const path = item?.path - // if path doesn't exist, use `item.url` - if (path) { - // try ${appInfo.value.ncSiteUrl}/${item.path} first - const url = `${appInfo.value.ncSiteUrl}/${item.path}` - try { - const res = await fetch(url) - if (res.ok) { - // use `url` if it is accessible - return Promise.resolve(url) - } - } catch { - // for some cases, `url` is not accessible as expected - // do nothing here - } - } - // if it fails, use the original url - return Promise.resolve(item.url) - } - async function loadKanbanData() { if ((!project?.value?.id || !meta.value?.id || !viewMeta?.value?.id || !groupingFieldColumn?.value?.id) && !isPublic.value) return @@ -193,28 +155,8 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState( } for (const data of groupData) { - const records = [] const key = data.key - // TODO: optimize - // reconstruct the url - // See /packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts for the details - for (const record of data.value.list) { - for (const attachmentColumn of attachmentColumns.value) { - // attachment column can be hidden - if (!record[attachmentColumn!]) continue - const oldAttachment = JSON.parse(record[attachmentColumn!]) - const newAttachment = [] - for (const attachmentObj of oldAttachment) { - newAttachment.push({ - ...attachmentObj, - url: await getAttachmentUrl(attachmentObj), - }) - } - record[attachmentColumn!] = newAttachment - } - records.push(record) - } - formattedData.value.set(key, formatData(records)) + formattedData.value.set(key, formatData(data.value.list)) countByStack.value.set(key, data.value.pageInfo.totalRows || 0) } } diff --git a/packages/nc-gui/composables/useLTARStore.ts b/packages/nc-gui/composables/useLTARStore.ts index 902718719e..1bc22cc1ca 100644 --- a/packages/nc-gui/composables/useLTARStore.ts +++ b/packages/nc-gui/composables/useLTARStore.ts @@ -16,6 +16,7 @@ import { useMetas, useNuxtApp, useProject, + useRouter, useSharedView, watch, } from '#imports' @@ -107,7 +108,10 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( const loadChildrenExcludedList = async () => { try { if (isPublic) { - const route = useRoute() + const router = useRouter() + + const route = $(router.currentRoute) + childrenExcludedList.value = await $api.public.dataRelationList( route.params.viewId as string, column.value.id, diff --git a/packages/nc-gui/composables/useProject.ts b/packages/nc-gui/composables/useProject.ts index f0791dbb43..5d06ef1f15 100644 --- a/packages/nc-gui/composables/useProject.ts +++ b/packages/nc-gui/composables/useProject.ts @@ -5,31 +5,30 @@ import { ClientType, computed, createEventHook, + createSharedComposable, ref, useApi, useGlobal, - useInjectionState, useNuxtApp, useRoles, - useRoute, useRouter, useTheme, } from '#imports' import type { ProjectMetaInfo, ThemeConfig } from '~/lib' -const [setup, use] = useInjectionState(() => { +export const useProject = createSharedComposable(() => { const { $e } = useNuxtApp() const { api, isLoading } = useApi() - const route = useRoute() + const router = useRouter() + + const route = $(router.currentRoute) const { includeM2M } = useGlobal() const { setTheme, theme } = useTheme() - const router = useRouter() - const { projectRoles, loadProjectRoles } = useRoles() const projectLoadedHook = createEventHook() @@ -178,6 +177,14 @@ const [setup, use] = useInjectionState(() => { setTheme() } + watch( + () => route.params.projectType, + (n) => { + if (!n) reset() + }, + { immediate: true }, + ) + return { project, bases, @@ -201,16 +208,4 @@ const [setup, use] = useInjectionState(() => { lastOpenedViewMap, isXcdbBase, } -}, 'useProject') - -export const provideProject = setup - -export function useProject() { - const state = use() - - if (!state) { - return setup() - } - - return state -} +}) diff --git a/packages/nc-gui/composables/useTabs.ts b/packages/nc-gui/composables/useTabs.ts index 24ca6ceca2..c29f118961 100644 --- a/packages/nc-gui/composables/useTabs.ts +++ b/packages/nc-gui/composables/useTabs.ts @@ -1,5 +1,5 @@ import type { WritableComputedRef } from '@vue/reactivity' -import { computed, navigateTo, ref, useInjectionState, useProject, useRoute, useRouter, watch } from '#imports' +import { computed, createSharedComposable, navigateTo, ref, useProject, useRouter, watch } from '#imports' import type { TabItem } from '~/lib' import { TabType } from '~/lib' @@ -10,13 +10,13 @@ function getPredicate(key: Partial) { (!('type' in key) || tab.type === key.type) } -const [setup, use] = useInjectionState(() => { +export const useTabs = createSharedComposable(() => { const tabs = ref([]) - const route = useRoute() - const router = useRouter() + const route = $(router.currentRoute) + const { bases, tables } = useProject() const projectType = $computed(() => route.params.projectType as string) @@ -157,13 +157,3 @@ const [setup, use] = useInjectionState(() => { return { tabs, addTab, activeTabIndex, activeTab, clearTabs, closeTab, updateTab } }) - -export function useTabs() { - const state = use() - - if (!state) { - return setup() - } - - return state -} diff --git a/packages/nc-gui/composables/useViewColumns.ts b/packages/nc-gui/composables/useViewColumns.ts index 0922527003..2d186b2f31 100644 --- a/packages/nc-gui/composables/useViewColumns.ts +++ b/packages/nc-gui/composables/useViewColumns.ts @@ -162,7 +162,10 @@ export function useViewColumns( .update(view.value.id, { show_system_fields: v, }) - .finally(() => reloadData?.()) + .finally(() => { + loadViewColumns() + reloadData?.() + }) } view.value.show_system_fields = v } @@ -173,6 +176,8 @@ export function useViewColumns( const filteredFieldList = computed(() => { return ( fields.value?.filter((field: Field) => { + if (metaColumnById?.value?.[field.fk_column_id!]?.pv) return true + // hide system columns if not enabled if (!showSystemFields.value && isSystemColumn(metaColumnById?.value?.[field.fk_column_id!])) { return false @@ -195,7 +200,8 @@ export function useViewColumns( !showSystemFields.value && metaColumnById.value && metaColumnById?.value?.[field.fk_column_id!] && - isSystemColumn(metaColumnById.value?.[field.fk_column_id!]) + isSystemColumn(metaColumnById.value?.[field.fk_column_id!]) && + !metaColumnById.value?.[field.fk_column_id!]?.pv ) { return false } diff --git a/packages/nc-gui/composables/useViewData.ts b/packages/nc-gui/composables/useViewData.ts index 670eead85b..cbc9b4a894 100644 --- a/packages/nc-gui/composables/useViewData.ts +++ b/packages/nc-gui/composables/useViewData.ts @@ -1,15 +1,5 @@ import { UITypes, ViewTypes } from 'nocodb-sdk' -import type { - Api, - AttachmentType, - ColumnType, - FormColumnType, - FormType, - GalleryType, - PaginatedType, - TableType, - ViewType, -} from 'nocodb-sdk' +import type { Api, ColumnType, FormColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { ComputedRef, Ref } from 'vue' import { IsPublicInj, @@ -29,7 +19,6 @@ import { useMetas, useNuxtApp, useProject, - useRoute, useRouter, useSharedView, useSmartsheetStoreOrThrow, @@ -59,7 +48,7 @@ export function useViewData( const router = useRouter() - const route = useRoute() + const route = $(router.currentRoute) const { appInfo } = $(useGlobal()) @@ -91,10 +80,6 @@ export function useViewData( const { isUIAllowed } = useUIPermission() - const attachmentColumns = computed(() => - (meta.value?.columns as ColumnType[])?.filter((c) => c.uidt === UITypes.Attachment).map((c) => c.title), - ) - const routeQuery = $computed(() => route.query as Record) const paginationData = computed({ @@ -201,28 +186,6 @@ export function useViewData( } } - // TODO: refactor - async function getAttachmentUrl(item: AttachmentType) { - const path = item?.path - // if path doesn't exist, use `item.url` - if (path) { - // try ${appInfo.value.ncSiteUrl}/${item.path} first - const url = `${appInfo.ncSiteUrl}/${item.path}` - try { - const res = await fetch(url) - if (res.ok) { - // use `url` if it is accessible - return Promise.resolve(url) - } - } catch { - // for some cases, `url` is not accessible as expected - // do nothing here - } - } - // if it fails, use the original url - return Promise.resolve(item.url) - } - async function loadData(params: Parameters['dbViewRow']['list']>[4] = {}) { if ((!project?.value?.id || !meta.value?.id || !viewMeta.value?.id) && !isPublic.value) return const response = !isPublic.value @@ -234,27 +197,7 @@ export function useViewData( where: where?.value, }) : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value }) - // reconstruct the url - // See /packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts for the details - const records = [] - for (const record of response.list) { - for (const attachmentColumn of attachmentColumns.value) { - // attachment column can be hidden - if (!record[attachmentColumn!]) continue - const oldAttachment = - typeof record[attachmentColumn!] === 'string' ? JSON.parse(record[attachmentColumn!]) : record[attachmentColumn!] - const newAttachment = [] - for (const attachmentObj of oldAttachment) { - newAttachment.push({ - ...attachmentObj, - url: await getAttachmentUrl(attachmentObj), - }) - } - record[attachmentColumn!] = newAttachment - } - records.push(record) - } - formattedData.value = formatData(records) + formattedData.value = formatData(response.list) paginationData.value = response.pageInfo // to cater the case like when querying with a non-zero offset diff --git a/packages/nc-gui/middleware/auth.global.ts b/packages/nc-gui/middleware/auth.global.ts index 2fda82fe37..6bd60b4440 100644 --- a/packages/nc-gui/middleware/auth.global.ts +++ b/packages/nc-gui/middleware/auth.global.ts @@ -52,7 +52,11 @@ export default defineNuxtRouteMiddleware(async (to, from) => { return navigateTo('/signup') } - return navigateTo('/signin') + /** try generating access token using refresh token */ + await state.refreshToken() + + /** if user is still not signed in, redirect to signin page */ + if (!state.signedIn.value) return navigateTo('/signin') } else if (to.meta.requiresAuth === false && state.signedIn.value) { /** * if user was turned away from non-auth page but also came from a non-auth page (e.g. user went to /signin and reloaded the page) diff --git a/packages/nc-gui/plugins/tele.ts b/packages/nc-gui/plugins/tele.ts index 4d608508c0..eed8abe5b7 100644 --- a/packages/nc-gui/plugins/tele.ts +++ b/packages/nc-gui/plugins/tele.ts @@ -1,12 +1,12 @@ import type { Socket } from 'socket.io-client' import io from 'socket.io-client' -import { defineNuxtPlugin, useGlobal, useRoute, useRouter, watch } from '#imports' +import { defineNuxtPlugin, useGlobal, useRouter, watch } from '#imports' // todo: ignore init if tele disabled export default defineNuxtPlugin(async (nuxtApp) => { const router = useRouter() - const route = useRoute() + const route = $(router.currentRoute) const { appInfo } = $(useGlobal()) diff --git a/packages/nc-gui/utils/index.ts b/packages/nc-gui/utils/index.ts index 73eb58b9b1..bbb469e8d5 100644 --- a/packages/nc-gui/utils/index.ts +++ b/packages/nc-gui/utils/index.ts @@ -21,3 +21,4 @@ export * from './stringUtils' export * from './memStorage' export * from './browserUtils' export * from './geoDataUtils' +export * from './mimeTypeUtils' diff --git a/packages/nc-gui/utils/mimeTypeUtils.ts b/packages/nc-gui/utils/mimeTypeUtils.ts new file mode 100644 index 0000000000..e95d73cc60 --- /dev/null +++ b/packages/nc-gui/utils/mimeTypeUtils.ts @@ -0,0 +1,714 @@ +const mimeTypes: Record = { + '123': 'application/vnd.lotus-1-2-3', + 'x3d': 'application/vnd.hzn-3d-crossword', + '3gp': 'video/3gpp', + '3g2': 'video/3gpp2', + 'mseq': 'application/vnd.mseq', + 'pwn': 'application/vnd.3m.post-it-notes', + 'plb': 'application/vnd.3gpp.pic-bw-large', + 'psb': 'application/vnd.3gpp.pic-bw-small', + 'pvb': 'application/vnd.3gpp.pic-bw-var', + 'tcap': 'application/vnd.3gpp2.tcap', + '7z': 'application/x-7z-compressed', + 'abw': 'application/x-abiword', + 'ace': 'application/x-ace-compressed', + 'acc': 'application/vnd.americandynamics.acc', + 'acu': 'application/vnd.acucobol', + 'atc': 'application/vnd.acucorp', + 'adp': 'audio/adpcm', + 'aab': 'application/x-authorware-bin', + 'aam': 'application/x-authorware-map', + 'aas': 'application/x-authorware-seg', + 'air': 'application/vnd.adobe.air-application-installer-package+zip', + 'swf': 'application/x-shockwave-flash', + 'fxp': 'application/vnd.adobe.fxp', + 'pdf': 'application/pdf', + 'ppd': 'application/vnd.cups-ppd', + 'dir': 'application/x-director', + 'xdp': 'application/vnd.adobe.xdp+xml', + 'xfdf': 'application/vnd.adobe.xfdf', + 'aac': 'audio/x-aac', + 'ahead': 'application/vnd.ahead.space', + 'azf': 'application/vnd.airzip.filesecure.azf', + 'azs': 'application/vnd.airzip.filesecure.azs', + 'azw': 'application/vnd.amazon.ebook', + 'ami': 'application/vnd.amiga.ami', + '/A': 'application/andrew-inset', + 'apk': 'application/vnd.android.package-archive', + 'cii': 'application/vnd.anser-web-certificate-issue-initiation', + 'fti': 'application/vnd.anser-web-funds-transfer-initiation', + 'atx': 'application/vnd.antix.game-component', + 'dmg': 'application/x-apple-diskimage', + 'mpkg': 'application/vnd.apple.installer+xml', + 'aw': 'application/applixware', + 'les': 'application/vnd.hhe.lesson-player', + 'swi': 'application/vnd.aristanetworks.swi', + 's': 'text/x-asm', + 'atomcat': 'application/atomcat+xml', + 'atomsvc': 'application/atomsvc+xml', + 'atom, .xml': 'application/atom+xml', + 'ac': 'application/pkix-attr-cert', + 'aif': 'audio/x-aiff', + 'avi': 'video/x-msvideo', + 'aep': 'application/vnd.audiograph', + 'dxf': 'image/vnd.dxf', + 'dwf': 'model/vnd.dwf', + 'par': 'text/plain-bas', + 'bcpio': 'application/x-bcpio', + 'bin': 'application/octet-stream', + 'bmp': 'image/bmp', + 'torrent': 'application/x-bittorrent', + 'cod': 'application/vnd.rim.cod', + 'mpm': 'application/vnd.blueice.multipass', + 'bmi': 'application/vnd.bmi', + 'sh': 'application/x-sh', + 'btif': 'image/prs.btif', + 'rep': 'application/vnd.businessobjects', + 'bz': 'application/x-bzip', + 'bz2': 'application/x-bzip2', + 'csh': 'application/x-csh', + 'c': 'text/x-c', + 'cdxml': 'application/vnd.chemdraw+xml', + 'css': 'text/css', + 'cdx': 'chemical/x-cdx', + 'cml': 'chemical/x-cml', + 'csml': 'chemical/x-csml', + 'cdbcmsg': 'application/vnd.contact.cmsg', + 'cla': 'application/vnd.claymore', + 'c4g': 'application/vnd.clonk.c4group', + 'sub': 'image/vnd.dvb.subtitle', + 'cdmia': 'application/cdmi-capability', + 'cdmic': 'application/cdmi-container', + 'cdmid': 'application/cdmi-domain', + 'cdmio': 'application/cdmi-object', + 'cdmiq': 'application/cdmi-queue', + 'c11amc': 'application/vnd.cluetrust.cartomobile-config', + 'c11amz': 'application/vnd.cluetrust.cartomobile-config-pkg', + 'ras': 'image/x-cmu-raster', + 'dae': 'model/vnd.collada+xml', + 'csv': 'text/csv', + 'cpt': 'application/mac-compactpro', + 'wmlc': 'application/vnd.wap.wmlc', + 'cgm': 'image/cgm', + 'ice': 'x-conference/x-cooltalk', + 'cmx': 'image/x-cmx', + 'xar': 'application/vnd.xara', + 'cmc': 'application/vnd.cosmocaller', + 'cpio': 'application/x-cpio', + 'clkx': 'application/vnd.crick.clicker', + 'clkk': 'application/vnd.crick.clicker.keyboard', + 'clkp': 'application/vnd.crick.clicker.palette', + 'clkt': 'application/vnd.crick.clicker.template', + 'clkw': 'application/vnd.crick.clicker.wordbank', + 'wbs': 'application/vnd.criticaltools.wbs+xml', + 'cryptonote': 'application/vnd.rig.cryptonote', + 'cif': 'chemical/x-cif', + 'cmdf': 'chemical/x-cmdf', + 'cu': 'application/cu-seeme', + 'cww': 'application/prs.cww', + 'curl': 'text/vnd.curl', + 'dcurl': 'text/vnd.curl.dcurl', + 'mcurl': 'text/vnd.curl.mcurl', + 'scurl': 'text/vnd.curl.scurl', + 'car': 'application/vnd.curl.car', + 'pcurl': 'application/vnd.curl.pcurl', + 'cmp': 'application/vnd.yellowriver-custom-menu', + 'dssc': 'application/dssc+der', + 'xdssc': 'application/dssc+xml', + 'deb': 'application/x-debian-package', + 'uva': 'audio/vnd.dece.audio', + 'uvi': 'image/vnd.dece.graphic', + 'uvh': 'video/vnd.dece.hd', + 'uvm': 'video/vnd.dece.mobile', + 'uvu': 'video/vnd.uvvu.mp4', + 'uvp': 'video/vnd.dece.pd', + 'uvs': 'video/vnd.dece.sd', + 'uvv': 'video/vnd.dece.video', + 'dvi': 'application/x-dvi', + 'seed': 'application/vnd.fdsn.seed', + 'dtb': 'application/x-dtbook+xml', + 'res': 'application/x-dtbresource+xml', + 'ait': 'application/vnd.dvb.ait', + 'svc': 'application/vnd.dvb.service', + 'eol': 'audio/vnd.digital-winds', + 'djvu': 'image/vnd.djvu', + 'dtd': 'application/xml-dtd', + 'mlp': 'application/vnd.dolby.mlp', + 'wad': 'application/x-doom', + 'dpg': 'application/vnd.dpgraph', + 'dra': 'audio/vnd.dra', + 'dfac': 'application/vnd.dreamfactory', + 'dts': 'audio/vnd.dts', + 'dtshd': 'audio/vnd.dts.hd', + 'dwg': 'image/vnd.dwg', + 'geo': 'application/vnd.dynageo', + 'es': 'application/ecmascript', + 'mag': 'application/vnd.ecowin.chart', + 'mmr': 'image/vnd.fujixerox.edmics-mmr', + 'rlc': 'image/vnd.fujixerox.edmics-rlc', + 'exi': 'application/exi', + 'mgz': 'application/vnd.proteus.magazine', + 'epub': 'application/epub+zip', + 'eml': 'message/rfc822', + 'nml': 'application/vnd.enliven', + 'xpr': 'application/vnd.is-xpr', + 'xif': 'image/vnd.xiff', + 'xfdl': 'application/vnd.xfdl', + 'emma': 'application/emma+xml', + 'ez2': 'application/vnd.ezpix-album', + 'ez3': 'application/vnd.ezpix-package', + 'fst': 'image/vnd.fst', + 'fvt': 'video/vnd.fvt', + 'fbs': 'image/vnd.fastbidsheet', + 'fe_launch': 'application/vnd.denovo.fcselayout-link', + 'f4v': 'video/x-f4v', + 'flv': 'video/x-flv', + 'fpx': 'image/vnd.fpx', + 'npx': 'image/vnd.net-fpx', + 'flx': 'text/vnd.fmi.flexstor', + 'fli': 'video/x-fli', + 'ftc': 'application/vnd.fluxtime.clip', + 'fdf': 'application/vnd.fdf', + 'f': 'text/x-fortran', + 'mif': 'application/vnd.mif', + 'fm': 'application/vnd.framemaker', + 'fh': 'image/x-freehand', + 'fsc': 'application/vnd.fsc.weblaunch', + 'fnc': 'application/vnd.frogans.fnc', + 'ltf': 'application/vnd.frogans.ltf', + 'ddd': 'application/vnd.fujixerox.ddd', + 'xdw': 'application/vnd.fujixerox.docuworks', + 'xbd': 'application/vnd.fujixerox.docuworks.binder', + 'oas': 'application/vnd.fujitsu.oasys', + 'oa2': 'application/vnd.fujitsu.oasys2', + 'oa3': 'application/vnd.fujitsu.oasys3', + 'fg5': 'application/vnd.fujitsu.oasysgp', + 'bh2': 'application/vnd.fujitsu.oasysprs', + 'spl': 'application/x-futuresplash', + 'fzs': 'application/vnd.fuzzysheet', + 'g3': 'image/g3fax', + 'gmx': 'application/vnd.gmx', + 'gtw': 'model/vnd.gtw', + 'txd': 'application/vnd.genomatix.tuxedo', + 'ggb': 'application/vnd.geogebra.file', + 'ggt': 'application/vnd.geogebra.tool', + 'gdl': 'model/vnd.gdl', + 'gex': 'application/vnd.geometry-explorer', + 'gxt': 'application/vnd.geonext', + 'g2w': 'application/vnd.geoplan', + 'g3w': 'application/vnd.geospace', + 'gsf': 'application/x-font-ghostscript', + 'bdf': 'application/x-font-bdf', + 'gtar': 'application/x-gtar', + 'texinfo': 'application/x-texinfo', + 'gnumeric': 'application/x-gnumeric', + 'kml': 'application/vnd.google-earth.kml+xml', + 'kmz': 'application/vnd.google-earth.kmz', + 'gpx': 'application/gpx+xml', + 'gqf': 'application/vnd.grafeq', + 'gif': 'image/gif', + 'gv': 'text/vnd.graphviz', + 'gac': 'application/vnd.groove-account', + 'ghf': 'application/vnd.groove-help', + 'gim': 'application/vnd.groove-identity-message', + 'grv': 'application/vnd.groove-injector', + 'gtm': 'application/vnd.groove-tool-message', + 'tpl': 'application/vnd.groove-tool-template', + 'vcg': 'application/vnd.groove-vcard', + 'h261': 'video/h261', + 'h263': 'video/h263', + 'h264': 'video/h264', + 'hpid': 'application/vnd.hp-hpid', + 'hps': 'application/vnd.hp-hps', + 'hdf': 'application/x-hdf', + 'rip': 'audio/vnd.rip', + 'hbci': 'application/vnd.hbci', + 'jlt': 'application/vnd.hp-jlyt', + 'pcl': 'application/vnd.hp-pcl', + 'hpgl': 'application/vnd.hp-hpgl', + 'hvs': 'application/vnd.yamaha.hv-script', + 'hvd': 'application/vnd.yamaha.hv-dic', + 'hvp': 'application/vnd.yamaha.hv-voice', + 'sfd-hdstx': 'application/vnd.hydrostatix.sof-data', + 'stk': 'application/hyperstudio', + 'hal': 'application/vnd.hal+xml', + 'html': 'text/html', + 'irm': 'application/vnd.ibm.rights-management', + 'sc': 'application/vnd.ibm.secure-container', + 'ics': 'text/calendar', + 'icc': 'application/vnd.iccprofile', + 'ico': 'image/x-icon', + 'igl': 'application/vnd.igloader', + 'ief': 'image/ief', + 'ivp': 'application/vnd.immervision-ivp', + 'ivu': 'application/vnd.immervision-ivu', + 'rif': 'application/reginfo+xml', + '3dml': 'text/vnd.in3d.3dml', + 'spot': 'text/vnd.in3d.spot', + 'igs': 'model/iges', + 'i2g': 'application/vnd.intergeo', + 'cdy': 'application/vnd.cinderella', + 'xpw': 'application/vnd.intercon.formnet', + 'fcs': 'application/vnd.isac.fcs', + 'ipfix': 'application/ipfix', + 'cer': 'application/pkix-cert', + 'pki': 'application/pkixcmp', + 'crl': 'application/pkix-crl', + 'pkipath': 'application/pkix-pkipath', + 'igm': 'application/vnd.insors.igm', + 'rcprofile': 'application/vnd.ipunplugged.rcprofile', + 'irp': 'application/vnd.irepository.package+xml', + 'jad': 'text/vnd.sun.j2me.app-descriptor', + 'jar': 'application/java-archive', + 'class': 'application/java-vm', + 'jnlp': 'application/x-java-jnlp-file', + 'ser': 'application/java-serialized-object', + 'java': 'text/x-java-source,java', + 'js': 'application/javascript', + 'json': 'application/json', + 'joda': 'application/vnd.joost.joda-archive', + 'jpm': 'video/jpm', + 'jpeg': 'image/x-citrix-jpeg', + 'jpg': 'image/x-citrix-jpeg', + 'pjpeg': 'image/pjpeg', + 'jpgv': 'video/jpeg', + 'ktz': 'application/vnd.kahootz', + 'mmd': 'application/vnd.chipnuts.karaoke-mmd', + 'karbon': 'application/vnd.kde.karbon', + 'chrt': 'application/vnd.kde.kchart', + 'kfo': 'application/vnd.kde.kformula', + 'flw': 'application/vnd.kde.kivio', + 'kon': 'application/vnd.kde.kontour', + 'kpr': 'application/vnd.kde.kpresenter', + 'ksp': 'application/vnd.kde.kspread', + 'kwd': 'application/vnd.kde.kword', + 'htke': 'application/vnd.kenameaapp', + 'kia': 'application/vnd.kidspiration', + 'kne': 'application/vnd.kinar', + 'sse': 'application/vnd.kodak-descriptor', + 'lasxml': 'application/vnd.las.las+xml', + 'latex': 'application/x-latex', + 'lbd': 'application/vnd.llamagraphics.life-balance.desktop', + 'lbe': 'application/vnd.llamagraphics.life-balance.exchange+xml', + 'jam': 'application/vnd.jam', + 'apr': 'application/vnd.lotus-approach', + 'pre': 'application/vnd.lotus-freelance', + 'nsf': 'application/vnd.lotus-notes', + 'org': 'application/vnd.lotus-organizer', + 'scm': 'application/vnd.lotus-screencam', + 'lwp': 'application/vnd.lotus-wordpro', + 'lvp': 'audio/vnd.lucent.voice', + 'm3u': 'audio/x-mpegurl', + 'm4v': 'video/x-m4v', + 'hqx': 'application/mac-binhex40', + 'portpkg': 'application/vnd.macports.portpkg', + 'mgp': 'application/vnd.osgeo.mapguide.package', + 'mrc': 'application/marc', + 'mrcx': 'application/marcxml+xml', + 'mxf': 'application/mxf', + 'nbp': 'application/vnd.wolfram.player', + 'ma': 'application/mathematica', + 'mathml': 'application/mathml+xml', + 'mbox': 'application/mbox', + 'mc1': 'application/vnd.medcalcdata', + 'mscml': 'application/mediaservercontrol+xml', + 'cdkey': 'application/vnd.mediastation.cdkey', + 'mwf': 'application/vnd.mfer', + 'mfm': 'application/vnd.mfmp', + 'msh': 'model/mesh', + 'mads': 'application/mads+xml', + 'mets': 'application/mets+xml', + 'mods': 'application/mods+xml', + 'meta4': 'application/metalink4+xml', + 'mcd': 'application/vnd.mcd', + 'flo': 'application/vnd.micrografx.flo', + 'igx': 'application/vnd.micrografx.igx', + 'es3': 'application/vnd.eszigno3+xml', + 'mdb': 'application/x-msaccess', + 'asf': 'video/x-ms-asf', + 'exe': 'application/x-msdownload', + 'cil': 'application/vnd.ms-artgalry', + 'cab': 'application/vnd.ms-cab-compressed', + 'ims': 'application/vnd.ms-ims', + 'application': 'application/x-ms-application', + 'clp': 'application/x-msclip', + 'mdi': 'image/vnd.ms-modi', + 'eot': 'application/vnd.ms-fontobject', + 'xls': 'application/vnd.ms-excel', + 'xlam': 'application/vnd.ms-excel.addin.macroenabled.12', + 'xlsb': 'application/vnd.ms-excel.sheet.binary.macroenabled.12', + 'xltm': 'application/vnd.ms-excel.template.macroenabled.12', + 'xlsm': 'application/vnd.ms-excel.sheet.macroenabled.12', + 'chm': 'application/vnd.ms-htmlhelp', + 'crd': 'application/x-mscardfile', + 'lrm': 'application/vnd.ms-lrm', + 'mvb': 'application/x-msmediaview', + 'mny': 'application/x-msmoney', + 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'sldx': 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'ppsx': 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'potx': 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'obd': 'application/x-msbinder', + 'thmx': 'application/vnd.ms-officetheme', + 'onetoc': 'application/onenote', + 'pya': 'audio/vnd.ms-playready.media.pya', + 'pyv': 'video/vnd.ms-playready.media.pyv', + 'ppt': 'application/vnd.ms-powerpoint', + 'ppam': 'application/vnd.ms-powerpoint.addin.macroenabled.12', + 'sldm': 'application/vnd.ms-powerpoint.slide.macroenabled.12', + 'pptm': 'application/vnd.ms-powerpoint.presentation.macroenabled.12', + 'ppsm': 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', + 'potm': 'application/vnd.ms-powerpoint.template.macroenabled.12', + 'mpp': 'application/vnd.ms-project', + 'pub': 'application/x-mspublisher', + 'scd': 'application/x-msschedule', + 'xap': 'application/x-silverlight-app', + 'stl': 'application/vnd.ms-pki.stl', + 'cat': 'application/vnd.ms-pki.seccat', + 'vsd': 'application/vnd.visio', + 'vsdx': 'application/vnd.visio2013', + 'wm': 'video/x-ms-wm', + 'wma': 'audio/x-ms-wma', + 'wax': 'audio/x-ms-wax', + 'wmx': 'video/x-ms-wmx', + 'wmd': 'application/x-ms-wmd', + 'wpl': 'application/vnd.ms-wpl', + 'wmz': 'application/x-ms-wmz', + 'wmv': 'video/x-ms-wmv', + 'wvx': 'video/x-ms-wvx', + 'wmf': 'application/x-msmetafile', + 'trm': 'application/x-msterminal', + 'doc': 'application/msword', + 'docm': 'application/vnd.ms-word.document.macroenabled.12', + 'dotm': 'application/vnd.ms-word.template.macroenabled.12', + 'wri': 'application/x-mswrite', + 'wps': 'application/vnd.ms-works', + 'xbap': 'application/x-ms-xbap', + 'xps': 'application/vnd.ms-xpsdocument', + 'mid': 'audio/midi', + 'mpy': 'application/vnd.ibm.minipay', + 'afp': 'application/vnd.ibm.modcap', + 'rms': 'application/vnd.jcp.javame.midlet-rms', + 'tmo': 'application/vnd.tmobile-livetv', + 'prc': 'application/x-mobipocket-ebook', + 'mbk': 'application/vnd.mobius.mbk', + 'dis': 'application/vnd.mobius.dis', + 'plc': 'application/vnd.mobius.plc', + 'mqy': 'application/vnd.mobius.mqy', + 'msl': 'application/vnd.mobius.msl', + 'txf': 'application/vnd.mobius.txf', + 'daf': 'application/vnd.mobius.daf', + 'fly': 'text/vnd.fly', + 'mpc': 'application/vnd.mophun.certificate', + 'mpn': 'application/vnd.mophun.application', + 'mj2': 'video/mj2', + 'mpga': 'audio/mpeg', + 'mxu': 'video/vnd.mpegurl', + 'mpeg': 'video/mpeg', + 'm21': 'application/mp21', + 'mp4a': 'audio/mp4', + 'mp4': 'application/mp4', + 'm3u8': 'application/vnd.apple.mpegurl', + 'mus': 'application/vnd.musician', + 'msty': 'application/vnd.muvee.style', + 'mxml': 'application/xv+xml', + 'ngdat': 'application/vnd.nokia.n-gage.data', + 'n-gage': 'application/vnd.nokia.n-gage.symbian.install', + 'ncx': 'application/x-dtbncx+xml', + 'nc': 'application/x-netcdf', + 'nlu': 'application/vnd.neurolanguage.nlu', + 'dna': 'application/vnd.dna', + 'nnd': 'application/vnd.noblenet-directory', + 'nns': 'application/vnd.noblenet-sealer', + 'nnw': 'application/vnd.noblenet-web', + 'rpst': 'application/vnd.nokia.radio-preset', + 'rpss': 'application/vnd.nokia.radio-presets', + 'n3': 'text/n3', + 'edm': 'application/vnd.novadigm.edm', + 'edx': 'application/vnd.novadigm.edx', + 'ext': 'application/vnd.novadigm.ext', + 'gph': 'application/vnd.flographit', + 'ecelp4800': 'audio/vnd.nuera.ecelp4800', + 'ecelp7470': 'audio/vnd.nuera.ecelp7470', + 'ecelp9600': 'audio/vnd.nuera.ecelp9600', + 'oda': 'application/oda', + 'ogx': 'application/ogg', + 'oga': 'audio/ogg', + 'ogv': 'video/ogg', + 'dd2': 'application/vnd.oma.dd2+xml', + 'oth': 'application/vnd.oasis.opendocument.text-web', + 'opf': 'application/oebps-package+xml', + 'qbo': 'application/vnd.intu.qbo', + 'oxt': 'application/vnd.openofficeorg.extension', + 'osf': 'application/vnd.yamaha.openscoreformat', + 'weba': 'audio/webm', + 'webm': 'video/webm', + 'odc': 'application/vnd.oasis.opendocument.chart', + 'otc': 'application/vnd.oasis.opendocument.chart-template', + 'odb': 'application/vnd.oasis.opendocument.database', + 'odf': 'application/vnd.oasis.opendocument.formula', + 'odft': 'application/vnd.oasis.opendocument.formula-template', + 'odg': 'application/vnd.oasis.opendocument.graphics', + 'otg': 'application/vnd.oasis.opendocument.graphics-template', + 'odi': 'application/vnd.oasis.opendocument.image', + 'oti': 'application/vnd.oasis.opendocument.image-template', + 'odp': 'application/vnd.oasis.opendocument.presentation', + 'otp': 'application/vnd.oasis.opendocument.presentation-template', + 'ods': 'application/vnd.oasis.opendocument.spreadsheet', + 'ots': 'application/vnd.oasis.opendocument.spreadsheet-template', + 'odt': 'application/vnd.oasis.opendocument.text', + 'odm': 'application/vnd.oasis.opendocument.text-master', + 'ott': 'application/vnd.oasis.opendocument.text-template', + 'ktx': 'image/ktx', + 'sxc': 'application/vnd.sun.xml.calc', + 'stc': 'application/vnd.sun.xml.calc.template', + 'sxd': 'application/vnd.sun.xml.draw', + 'std': 'application/vnd.sun.xml.draw.template', + 'sxi': 'application/vnd.sun.xml.impress', + 'sti': 'application/vnd.sun.xml.impress.template', + 'sxm': 'application/vnd.sun.xml.math', + 'sxw': 'application/vnd.sun.xml.writer', + 'sxg': 'application/vnd.sun.xml.writer.global', + 'stw': 'application/vnd.sun.xml.writer.template', + 'otf': 'application/x-font-otf', + 'osfpvg': 'application/vnd.yamaha.openscoreformat.osfpvg+xml', + 'dp': 'application/vnd.osgi.dp', + 'pdb': 'application/vnd.palm', + 'p': 'text/x-pascal', + 'paw': 'application/vnd.pawaafile', + 'pclxl': 'application/vnd.hp-pclxl', + 'efif': 'application/vnd.picsel', + 'pcx': 'image/x-pcx', + 'psd': 'image/vnd.adobe.photoshop', + 'prf': 'application/pics-rules', + 'pic': 'image/x-pict', + 'chat': 'application/x-chat', + 'p10': 'application/pkcs10', + 'p12': 'application/x-pkcs12', + 'p7m': 'application/pkcs7-mime', + 'p7s': 'application/pkcs7-signature', + 'p7r': 'application/x-pkcs7-certreqresp', + 'p7b': 'application/x-pkcs7-certificates', + 'p8': 'application/pkcs8', + 'plf': 'application/vnd.pocketlearn', + 'pnm': 'image/x-portable-anymap', + 'pbm': 'image/x-portable-bitmap', + 'pcf': 'application/x-font-pcf', + 'pfr': 'application/font-tdpfr', + 'pgn': 'application/x-chess-pgn', + 'pgm': 'image/x-portable-graymap', + 'png': 'image/x-png', + 'ppm': 'image/x-portable-pixmap', + 'pskcxml': 'application/pskc+xml', + 'pml': 'application/vnd.ctc-posml', + 'ai': 'application/postscript', + 'pfa': 'application/x-font-type1', + 'pbd': 'application/vnd.powerbuilder6', + 'pgp': 'application/pgp-signature', + 'box': 'application/vnd.previewsystems.box', + 'ptid': 'application/vnd.pvi.ptid1', + 'pls': 'application/pls+xml', + 'str': 'application/vnd.pg.format', + 'ei6': 'application/vnd.pg.osasli', + 'dsc': 'text/prs.lines.tag', + 'psf': 'application/x-font-linux-psf', + 'qps': 'application/vnd.publishare-delta-tree', + 'wg': 'application/vnd.pmi.widget', + 'qxd': 'application/vnd.quark.quarkxpress', + 'esf': 'application/vnd.epson.esf', + 'msf': 'application/vnd.epson.msf', + 'ssf': 'application/vnd.epson.ssf', + 'qam': 'application/vnd.epson.quickanime', + 'qfx': 'application/vnd.intu.qfx', + 'qt': 'video/quicktime', + 'rar': 'application/x-rar-compressed', + 'ram': 'audio/x-pn-realaudio', + 'rmp': 'audio/x-pn-realaudio-plugin', + 'rsd': 'application/rsd+xml', + 'rm': 'application/vnd.rn-realmedia', + 'bed': 'application/vnd.realvnc.bed', + 'mxl': 'application/vnd.recordare.musicxml', + 'musicxml': 'application/vnd.recordare.musicxml+xml', + 'rnc': 'application/relax-ng-compact-syntax', + 'rdz': 'application/vnd.data-vision.rdz', + 'rdf': 'application/rdf+xml', + 'rp9': 'application/vnd.cloanto.rp9', + 'jisp': 'application/vnd.jisp', + 'rtf': 'application/rtf', + 'rtx': 'text/richtext', + 'link66': 'application/vnd.route66.link66+xml', + 'rss, .xml': 'application/rss+xml', + 'shf': 'application/shf+xml', + 'st': 'application/vnd.sailingtracker.track', + 'svg': 'image/svg+xml', + 'sus': 'application/vnd.sus-calendar', + 'sru': 'application/sru+xml', + 'setpay': 'application/set-payment-initiation', + 'setreg': 'application/set-registration-initiation', + 'sema': 'application/vnd.sema', + 'semd': 'application/vnd.semd', + 'semf': 'application/vnd.semf', + 'see': 'application/vnd.seemail', + 'snf': 'application/x-font-snf', + 'spq': 'application/scvp-vp-request', + 'spp': 'application/scvp-vp-response', + 'scq': 'application/scvp-cv-request', + 'scs': 'application/scvp-cv-response', + 'sdp': 'application/sdp', + 'etx': 'text/x-setext', + 'movie': 'video/x-sgi-movie', + 'ifm': 'application/vnd.shana.informed.formdata', + 'itp': 'application/vnd.shana.informed.formtemplate', + 'iif': 'application/vnd.shana.informed.interchange', + 'ipk': 'application/vnd.shana.informed.package', + 'tfi': 'application/thraud+xml', + 'shar': 'application/x-shar', + 'rgb': 'image/x-rgb', + 'slt': 'application/vnd.epson.salt', + 'aso': 'application/vnd.accpac.simply.aso', + 'imp': 'application/vnd.accpac.simply.imp', + 'twd': 'application/vnd.simtech-mindmapper', + 'csp': 'application/vnd.commonspace', + 'saf': 'application/vnd.yamaha.smaf-audio', + 'mmf': 'application/vnd.smaf', + 'spf': 'application/vnd.yamaha.smaf-phrase', + 'teacher': 'application/vnd.smart.teacher', + 'svd': 'application/vnd.svd', + 'rq': 'application/sparql-query', + 'srx': 'application/sparql-results+xml', + 'gram': 'application/srgs', + 'grxml': 'application/srgs+xml', + 'ssml': 'application/ssml+xml', + 'skp': 'application/vnd.koan', + 'sgml': 'text/sgml', + 'sdc': 'application/vnd.stardivision.calc', + 'sda': 'application/vnd.stardivision.draw', + 'sdd': 'application/vnd.stardivision.impress', + 'smf': 'application/vnd.stardivision.math', + 'sdw': 'application/vnd.stardivision.writer', + 'sgl': 'application/vnd.stardivision.writer-global', + 'sm': 'application/vnd.stepmania.stepchart', + 'sit': 'application/x-stuffit', + 'sitx': 'application/x-stuffitx', + 'sdkm': 'application/vnd.solent.sdkm+xml', + 'xo': 'application/vnd.olpc-sugar', + 'au': 'audio/basic', + 'wqd': 'application/vnd.wqd', + 'sis': 'application/vnd.symbian.install', + 'smi': 'application/smil+xml', + 'xsm': 'application/vnd.syncml+xml', + 'bdm': 'application/vnd.syncml.dm+wbxml', + 'xdm': 'application/vnd.syncml.dm+xml', + 'sv4cpio': 'application/x-sv4cpio', + 'sv4crc': 'application/x-sv4crc', + 'sbml': 'application/sbml+xml', + 'tsv': 'text/tab-separated-values', + 'tiff': 'image/tiff', + 'tao': 'application/vnd.tao.intent-module-archive', + 'tar': 'application/x-tar', + 'tcl': 'application/x-tcl', + 'tex': 'application/x-tex', + 'tfm': 'application/x-tex-tfm', + 'tei': 'application/tei+xml', + 'txt': 'text/plain', + 'dxp': 'application/vnd.spotfire.dxp', + 'sfs': 'application/vnd.spotfire.sfs', + 'tsd': 'application/timestamped-data', + 'tpt': 'application/vnd.trid.tpt', + 'mxs': 'application/vnd.triscape.mxs', + 't': 'text/troff', + 'tra': 'application/vnd.trueapp', + 'ttf': 'application/x-font-ttf', + 'ttl': 'text/turtle', + 'umj': 'application/vnd.umajin', + 'uoml': 'application/vnd.uoml+xml', + 'unityweb': 'application/vnd.unity', + 'ufd': 'application/vnd.ufdl', + 'uri': 'text/uri-list', + 'utz': 'application/vnd.uiq.theme', + 'ustar': 'application/x-ustar', + 'uu': 'text/x-uuencode', + 'vcs': 'text/x-vcalendar', + 'vcf': 'text/x-vcard', + 'vcd': 'application/x-cdlink', + 'vsf': 'application/vnd.vsf', + 'wrl': 'model/vrml', + 'vcx': 'application/vnd.vcx', + 'mts': 'model/vnd.mts', + 'vtu': 'model/vnd.vtu', + 'vis': 'application/vnd.visionary', + 'viv': 'video/vnd.vivo', + 'ccxml': 'application/ccxml+xml,', + 'vxml': 'application/voicexml+xml', + 'src': 'application/x-wais-source', + 'wbxml': 'application/vnd.wap.wbxml', + 'wbmp': 'image/vnd.wap.wbmp', + 'wav': 'audio/x-wav', + 'davmount': 'application/davmount+xml', + 'woff': 'application/x-font-woff', + 'wspolicy': 'application/wspolicy+xml', + 'webp': 'image/webp', + 'wtb': 'application/vnd.webturbo', + 'wgt': 'application/widget', + 'hlp': 'application/winhlp', + 'wml': 'text/vnd.wap.wml', + 'wmls': 'text/vnd.wap.wmlscript', + 'wmlsc': 'application/vnd.wap.wmlscriptc', + 'wpd': 'application/vnd.wordperfect', + 'stf': 'application/vnd.wt.stf', + 'wsdl': 'application/wsdl+xml', + 'xbm': 'image/x-xbitmap', + 'xpm': 'image/x-xpixmap', + 'xwd': 'image/x-xwindowdump', + 'der': 'application/x-x509-ca-cert', + 'fig': 'application/x-xfig', + 'xhtml': 'application/xhtml+xml', + 'xml': 'application/xml', + 'xdf': 'application/xcap-diff+xml', + 'xenc': 'application/xenc+xml', + 'xer': 'application/patch-ops-error+xml', + 'rl': 'application/resource-lists+xml', + 'rs': 'application/rls-services+xml', + 'rld': 'application/resource-lists-diff+xml', + 'xslt': 'application/xslt+xml', + 'xop': 'application/xop+xml', + 'xpi': 'application/x-xpinstall', + 'xspf': 'application/xspf+xml', + 'xul': 'application/vnd.mozilla.xul+xml', + 'xyz': 'chemical/x-xyz', + 'yaml': 'text/yaml', + 'yang': 'application/yang', + 'yin': 'application/yin+xml', + 'zir': 'application/vnd.zul', + 'zip': 'application/zip', + 'zmm': 'application/vnd.handheld-entertainment+xml', + 'zaz': 'application/vnd.zzazz.deck+xml', +} + +const mimeIcons = { + pdf: 'mdi-pdf-box', + + docx: 'mdi-file-word-outline', + dotx: 'mdi-file-word-outline', + odt: 'mdi-file-word-outline', + odm: 'mdi-file-word-outline', + ott: 'mdi-file-word-outline', + + ppt: 'mdi-file-powerpoint-box', + pptx: 'mdi-file-powerpoint-box', + sldx: 'mdi-file-powerpoint-box', + ppsx: 'mdi-file-powerpoint-box', + potx: 'mdi-file-powerpoint-box', + + xls: 'mdi-file-excel-outline', + xlam: 'mdi-file-excel-outline', + xlsb: 'mdi-file-excel-outline', + xltm: 'mdi-file-excel-outline', + xlsm: 'mdi-file-excel-outline', +} + +export { mimeTypes, mimeIcons } diff --git a/packages/noco-docs/content/en/engineering/unit-testing.md b/packages/noco-docs/content/en/engineering/unit-testing.md index 4d0b48c13f..ad9716903c 100644 --- a/packages/noco-docs/content/en/engineering/unit-testing.md +++ b/packages/noco-docs/content/en/engineering/unit-testing.md @@ -43,7 +43,7 @@ npm run test:unit ### Folder Structure -The root folder for unit tests is `packages/tests/unit` +The root folder for unit tests is `packages/nocodb/tests/unit` - `rest` folder contains all the test suites for rest apis. - `model` folder contains all the test suites for models. @@ -69,7 +69,7 @@ We will create an `Table` test suite as an example. #### Configure test -We will configure `beforeEach` which is called before each test is executed. We will use `init` function from `nocodb/packages/tests/unit/init/index.ts`, which is a helper function which configures the test environment(i.e resetting state, etc.). +We will configure `beforeEach` which is called before each test is executed. We will use `init` function from `nocodb/packages/nocodb/tests/unit/init/index.ts`, which is a helper function which configures the test environment(i.e resetting state, etc.). `init` does the following things - diff --git a/packages/nocodb/src/lib/Noco.ts b/packages/nocodb/src/lib/Noco.ts index bd2fed4b61..d144579e41 100644 --- a/packages/nocodb/src/lib/Noco.ts +++ b/packages/nocodb/src/lib/Noco.ts @@ -105,7 +105,7 @@ export default class Noco { constructor() { process.env.PORT = process.env.PORT || '8080'; // todo: move - process.env.NC_VERSION = '0104002'; + process.env.NC_VERSION = '0104003'; // if env variable NC_MINIMAL_DBS is set, then disable project creation with external sources if (process.env.NC_MINIMAL_DBS) { diff --git a/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts b/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts index e387b61688..9c47ab34ac 100644 --- a/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts +++ b/packages/nocodb/src/lib/db/sql-client/lib/KnexClient.ts @@ -2367,39 +2367,26 @@ class KnexClient extends SqlClient { const foreignKeyName = args.foreignKeyName || null; try { - // s = await this.sqlClient.schema.index(Object.keys(args.columns)); - - await this.sqlClient.schema.table(args.childTable, function (table) { - table = table - .foreign(args.childColumn, foreignKeyName) - .references(args.parentColumn) - .on(args.parentTable); - - if (args.onUpdate) { - table = table.onUpdate(args.onUpdate); - } - if (args.onDelete) { - table = table.onDelete(args.onDelete); + const upQb = this.sqlClient.schema.table( + args.childTable, + function (table) { + table = table + .foreign(args.childColumn, foreignKeyName) + .references(args.parentColumn) + .on(args.parentTable); + + if (args.onUpdate) { + table = table.onUpdate(args.onUpdate); + } + if (args.onDelete) { + table.onDelete(args.onDelete); + } } - }); + ); - const upStatement = - this.querySeparator() + - (await this.sqlClient.schema - .table(args.childTable, function (table) { - table = table - .foreign(args.childColumn, foreignKeyName) - .references(args.parentColumn) - .on(args.parentTable); + await upQb; - if (args.onUpdate) { - table = table.onUpdate(args.onUpdate); - } - if (args.onDelete) { - table = table.onDelete(args.onDelete); - } - }) - .toQuery()); + const upStatement = this.querySeparator() + upQb.toQuery(); this.emit(`Success : ${upStatement}`); @@ -2407,7 +2394,7 @@ class KnexClient extends SqlClient { this.querySeparator() + this.sqlClient.schema .table(args.childTable, function (table) { - table = table.dropForeign(args.childColumn, foreignKeyName); + table.dropForeign(args.childColumn, foreignKeyName); }) .toQuery(); diff --git a/packages/nocodb/src/lib/meta/api/attachmentApis.ts b/packages/nocodb/src/lib/meta/api/attachmentApis.ts index 9d619ae592..0053b99304 100644 --- a/packages/nocodb/src/lib/meta/api/attachmentApis.ts +++ b/packages/nocodb/src/lib/meta/api/attachmentApis.ts @@ -17,7 +17,9 @@ import { NC_ATTACHMENT_FIELD_SIZE } from '../../constants'; const isUploadAllowed = async (req: Request, _res: Response, next: any) => { if (!req['user']?.id) { - NcError.unauthorized('Unauthorized'); + if (!req['user']?.isPublicBase) { + NcError.unauthorized('Unauthorized'); + } } try { @@ -25,6 +27,7 @@ const isUploadAllowed = async (req: Request, _res: Response, next: any) => { if ( req['user'].roles?.includes(OrgUserRoles.SUPER_ADMIN) || req['user'].roles?.includes(OrgUserRoles.CREATOR) || + req['user'].roles?.includes(ProjectRoles.EDITOR) || // if viewer then check at-least one project have editor or higher role // todo: cache !!(await Noco.ncMeta @@ -54,7 +57,7 @@ export async function upload(req: Request, res: Response) { (req as any).files?.map(async (file) => { const fileName = `${nanoid(18)}${path.extname(file.originalname)}`; - let url = await storageAdapter.fileCreate( + const url = await storageAdapter.fileCreate( slash(path.join(destPath, fileName)), file ); @@ -98,7 +101,7 @@ export async function uploadViaURL(req: Request, res: Response) { const fileName = `${nanoid(18)}${_fileName || url.split('/').pop()}`; - let attachmentUrl = await (storageAdapter as any).fileCreateByUrl( + const attachmentUrl = await (storageAdapter as any).fileCreateByUrl( slash(path.join(destPath, fileName)), url ); diff --git a/packages/nocodb/src/lib/meta/api/baseApis.ts b/packages/nocodb/src/lib/meta/api/baseApis.ts index 07e3a23692..9b81d8e06b 100644 --- a/packages/nocodb/src/lib/meta/api/baseApis.ts +++ b/packages/nocodb/src/lib/meta/api/baseApis.ts @@ -1,22 +1,13 @@ import { Request, Response } from 'express'; import Project from '../../models/Project'; -import { BaseListType, ModelTypes, UITypes } from 'nocodb-sdk'; +import { BaseListType } from 'nocodb-sdk'; import { PagedResponseImpl } from '../helpers/PagedResponse'; import { syncBaseMigration } from '../helpers/syncMigration'; -import { IGNORE_TABLES } from '../../utils/common/BaseApiBuilder'; -import Column from '../../models/Column'; -import Model from '../../models/Model'; -import NcHelp from '../../utils/NcHelp'; import Base from '../../models/Base'; -import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; -import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName'; -import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; import ncMetaAclMw from '../helpers/ncMetaAclMw'; import { Tele } from 'nc-help'; -import getColumnUiType from '../helpers/getColumnUiType'; -import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue'; -import { extractAndGenerateManyToManyRelations } from './metaDiffApis'; import { metaApiMetrics } from '../helpers/apiMetrics'; +import { populateMeta } from './helpers'; export async function baseGet( req: Request, @@ -107,256 +98,6 @@ async function baseCreate(req: Request, res) { res.json(base); } -async function populateMeta(base: Base, project: Project): Promise { - const info = { - type: 'rest', - apiCount: 0, - tablesCount: 0, - relationsCount: 0, - viewsCount: 0, - client: base?.getConnectionConfig()?.client, - timeTaken: 0, - }; - - const t = process.hrtime(); - const sqlClient = await NcConnectionMgrv2.getSqlClient(base); - let order = 1; - const models2: { [tableName: string]: Model } = {}; - - const virtualColumnsInsert = []; - - /* Get all relations */ - const relations = (await sqlClient.relationListAll())?.data?.list; - - info.relationsCount = relations.length; - - let tables = (await sqlClient.tableList())?.data?.list - ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn)) - ?.map((t) => { - t.order = ++order; - t.title = getTableNameAlias(t.tn, project.prefix, base); - t.table_name = t.tn; - return t; - }); - - /* filter based on prefix */ - if (base.is_meta && project?.prefix) { - tables = tables.filter((t) => { - return t?.tn?.startsWith(project?.prefix); - }); - } - - info.tablesCount = tables.length; - - tables.forEach((t) => { - t.title = getTableNameAlias(t.tn, project.prefix, base); - }); - - relations.forEach((r) => { - r.title = getTableNameAlias(r.tn, project.prefix, base); - r.rtitle = getTableNameAlias(r.rtn, project.prefix, base); - }); - - // await this.syncRelations(); - - const tableMetasInsert = tables.map((table) => { - return async () => { - /* filter relation where this table is present */ - const tableRelations = relations.filter( - (r) => r.tn === table.tn || r.rtn === table.tn - ); - - const columns: Array< - Omit & { - cn: string; - system?: boolean; - } - > = (await sqlClient.columnList({ tn: table.tn }))?.data?.list; - - const hasMany = - table.type === 'view' - ? [] - : tableRelations.filter((r) => r.rtn === table.tn); - const belongsTo = - table.type === 'view' - ? [] - : tableRelations.filter((r) => r.tn === table.tn); - - mapDefaultPrimaryValue(columns); - - // add vitual columns - const virtualColumns = [ - ...hasMany.map((hm) => { - return { - uidt: UITypes.LinkToAnotherRecord, - type: 'hm', - hm, - title: `${hm.title} List`, - }; - }), - ...belongsTo.map((bt) => { - // find and mark foreign key column - const fkColumn = columns.find((c) => c.cn === bt.cn); - if (fkColumn) { - fkColumn.uidt = UITypes.ForeignKey; - fkColumn.system = true; - } - - return { - uidt: UITypes.LinkToAnotherRecord, - type: 'bt', - bt, - title: `${bt.rtitle}`, - }; - }), - ]; - - // await Model.insert(project.id, base.id, meta); - - /* create nc_models and its rows if it doesn't exists */ - models2[table.table_name] = await Model.insert(project.id, base.id, { - table_name: table.tn || table.table_name, - title: table.title, - type: table.type || 'table', - order: table.order, - }); - - // table crud apis - info.apiCount += 5; - - let colOrder = 1; - - for (const column of columns) { - await Column.insert({ - uidt: column.uidt || getColumnUiType(base, column), - fk_model_id: models2[table.tn].id, - ...column, - title: getColumnNameAlias(column.cn, base), - column_name: column.cn, - order: colOrder++, - }); - } - virtualColumnsInsert.push(async () => { - const columnNames = {}; - for (const column of virtualColumns) { - // generate unique name if there is any duplicate column name - let c = 0; - while (`${column.title}${c || ''}` in columnNames) { - c++; - } - column.title = `${column.title}${c || ''}`; - columnNames[column.title] = true; - - const rel = column.hm || column.bt; - - const rel_column_id = (await models2?.[rel.tn]?.getColumns())?.find( - (c) => c.column_name === rel.cn - )?.id; - - const tnId = models2?.[rel.tn]?.id; - - const ref_rel_column_id = ( - await models2?.[rel.rtn]?.getColumns() - )?.find((c) => c.column_name === rel.rcn)?.id; - - const rtnId = models2?.[rel.rtn]?.id; - - try { - await Column.insert({ - project_id: project.id, - db_alias: base.id, - fk_model_id: models2[table.tn].id, - cn: column.cn, - title: column.title, - uidt: column.uidt, - type: column.hm ? 'hm' : column.mm ? 'mm' : 'bt', - // column_id, - fk_child_column_id: rel_column_id, - fk_parent_column_id: ref_rel_column_id, - fk_index_name: rel.fkn, - ur: rel.ur, - dr: rel.dr, - order: colOrder++, - fk_related_model_id: column.hm ? tnId : rtnId, - system: column.system, - }); - - // nested relations data apis - info.apiCount += 5; - } catch (e) { - console.log(e); - } - } - }); - }; - }); - - /* handle xc_tables update in parallel */ - await NcHelp.executeOperations(tableMetasInsert, base.type); - await NcHelp.executeOperations(virtualColumnsInsert, base.type); - await extractAndGenerateManyToManyRelations(Object.values(models2)); - - let views: Array<{ order: number; table_name: string; title: string }> = ( - await sqlClient.viewList() - )?.data?.list - // ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn)) - ?.map((v) => { - v.order = ++order; - v.table_name = v.view_name; - v.title = getTableNameAlias(v.view_name, project.prefix, base); - return v; - }); - - /* filter based on prefix */ - if (base.is_meta && project?.prefix) { - views = tables.filter((t) => { - return t?.tn?.startsWith(project?.prefix); - }); - } - - info.viewsCount = views.length; - - const viewMetasInsert = views.map((table) => { - return async () => { - const columns = (await sqlClient.columnList({ tn: table.table_name })) - ?.data?.list; - - /* create nc_models and its rows if it doesn't exists */ - models2[table.table_name] = await Model.insert(project.id, base.id, { - table_name: table.table_name, - title: getTableNameAlias(table.table_name, project.prefix, base), - // todo: sanitize - type: ModelTypes.VIEW, - order: table.order, - }); - - let colOrder = 1; - - // view apis - info.apiCount += 2; - - for (const column of columns) { - await Column.insert({ - fk_model_id: models2[table.table_name].id, - ...column, - title: getColumnNameAlias(column.cn, base), - order: colOrder++, - uidt: getColumnUiType(base, column), - }); - } - }; - }); - - await NcHelp.executeOperations(viewMetasInsert, base.type); - - const t1 = process.hrtime(t); - const t2 = t1[0] + t1[1] / 1000000000; - - (info as any).timeTaken = t2.toFixed(1); - - return info; -} - export default (router) => { router.get( '/api/v1/db/meta/projects/:projectId/bases/:baseId', diff --git a/packages/nocodb/src/lib/meta/api/columnApis.ts b/packages/nocodb/src/lib/meta/api/columnApis.ts index 0977647a02..69b9e1f962 100644 --- a/packages/nocodb/src/lib/meta/api/columnApis.ts +++ b/packages/nocodb/src/lib/meta/api/columnApis.ts @@ -50,12 +50,26 @@ export enum Altered { UPDATE_COLUMN = 8, } +// generate unique foreign key constraint name for foreign key +const generateFkName = (parent: TableType, child: TableType) => { + // generate a unique constraint name by taking first 10 chars of parent and child table name (by replacing all non word chars with _) + // and appending a random string of 15 chars maximum length. + // In database constraint name can be upto 64 chars and here we are generating a name of maximum 40 chars + const constraintName = `fk_${parent.table_name + .replace(/\W+/g, '_') + .slice(0, 10)}_${child.table_name + .replace(/\W+/g, '_') + .slice(0, 10)}_${randomID(15)}`; + return constraintName; +}; + async function createHmAndBtColumn( child: Model, parent: Model, childColumn: Column, type?: RelationTypes, alias?: string, + fkColName?: string, virtual = false, isSystemCol = false ) { @@ -79,6 +93,7 @@ async function createHmAndBtColumn( fk_related_model_id: parent.id, virtual, system: isSystemCol, + fk_index_name: fkColName, }); } // save hm column @@ -97,6 +112,7 @@ async function createHmAndBtColumn( fk_related_model_id: child.id, virtual, system: isSystemCol, + fk_index_name: fkColName, }); } } @@ -262,6 +278,7 @@ export async function columnAdd( `${parent.table_name}_id` ); + let foreignKeyName; { // create foreign key const newColumn = { @@ -307,6 +324,7 @@ export async function columnAdd( // ignore relation creation if virtual if (!(req.body as LinkToAnotherColumnReqType).virtual) { + foreignKeyName = generateFkName(parent, child); // create relation await sqlMgr.sqlOpPlus(base, 'relationCreate', { childColumn: fkColName, @@ -316,6 +334,7 @@ export async function columnAdd( onUpdate: 'NO ACTION', type: 'real', parentColumn: parent.primaryKey.column_name, + foreignKeyName, }); } @@ -338,6 +357,7 @@ export async function columnAdd( childColumn, (req.body as LinkToAnotherColumnReqType).type as RelationTypes, (req.body as LinkToAnotherColumnReqType).title, + foreignKeyName, (req.body as LinkToAnotherColumnReqType).virtual ); } else if ((req.body as LinkToAnotherColumnReqType).type === 'mm') { @@ -399,7 +419,13 @@ export async function columnAdd( columns: associateTableCols, }); + let foreignKeyName1; + let foreignKeyName2; + if (!(req.body as LinkToAnotherColumnReqType).virtual) { + foreignKeyName1 = generateFkName(parent, child); + foreignKeyName2 = generateFkName(parent, child); + const rel1Args = { ...req.body, childTable: aTn, @@ -407,6 +433,7 @@ export async function columnAdd( parentTable: parent.table_name, parentColumn: parentPK.column_name, type: 'real', + foreignKeyName: foreignKeyName1, }; const rel2Args = { ...req.body, @@ -415,6 +442,7 @@ export async function columnAdd( parentTable: child.table_name, parentColumn: childPK.column_name, type: 'real', + foreignKeyName: foreignKeyName2, }; await sqlMgr.sqlOpPlus(base, 'relationCreate', rel1Args); @@ -433,6 +461,7 @@ export async function columnAdd( childCol, null, null, + foreignKeyName1, (req.body as LinkToAnotherColumnReqType).virtual, true ); @@ -442,6 +471,7 @@ export async function columnAdd( parentCol, null, null, + foreignKeyName2, (req.body as LinkToAnotherColumnReqType).virtual, true ); @@ -1724,6 +1754,31 @@ const deleteHmOrBtRelation = async ( }, ignoreFkDelete = false ) => { + let foreignKeyName; + + // if relationColOpt is not provided, extract it from child table + // and get the foreign key name for dropping the foreign key + if (!relationColOpt) { + foreignKeyName = ( + ( + await childTable.getColumns().then((cols) => { + return cols?.find((c) => { + return ( + c.uidt === UITypes.LinkToAnotherRecord && + c.colOptions.fk_related_model_id === parentTable.id && + (c.colOptions as LinkToAnotherRecordType).fk_child_column_id === + childColumn.id && + (c.colOptions as LinkToAnotherRecordType).fk_parent_column_id === + parentColumn.id + ); + }); + }) + ).colOptions as LinkToAnotherRecordType + ).fk_index_name; + } else { + foreignKeyName = relationColOpt.fk_index_name; + } + // todo: handle relation delete exception try { await sqlMgr.sqlOpPlus(base, 'relationDelete', { @@ -1731,7 +1786,7 @@ const deleteHmOrBtRelation = async ( childTable: childTable.table_name, parentTable: parentTable.table_name, parentColumn: parentColumn.column_name, - // foreignKeyName: relation.fkn + foreignKeyName, }); } catch (e) { console.log(e); diff --git a/packages/nocodb/src/lib/meta/api/helpers/index.ts b/packages/nocodb/src/lib/meta/api/helpers/index.ts new file mode 100644 index 0000000000..ab5d63a717 --- /dev/null +++ b/packages/nocodb/src/lib/meta/api/helpers/index.ts @@ -0,0 +1,3 @@ +import { populateMeta } from './populateMeta'; + +export { populateMeta }; diff --git a/packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts b/packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts new file mode 100644 index 0000000000..8e5f9d2928 --- /dev/null +++ b/packages/nocodb/src/lib/meta/api/helpers/populateMeta.ts @@ -0,0 +1,278 @@ +import Project from '../../../models/Project'; +import Column from '../../../models/Column'; +import Model from '../../../models/Model'; +import NcHelp from '../../../utils/NcHelp'; +import Base from '../../../models/Base'; +import View from '../../../models/View'; +import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; +import getTableNameAlias, { + getColumnNameAlias, +} from '../../helpers/getTableName'; +import LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; +import getColumnUiType from '../../helpers/getColumnUiType'; +import mapDefaultPrimaryValue from '../../helpers/mapDefaultPrimaryValue'; +import { extractAndGenerateManyToManyRelations } from '../metaDiffApis'; +import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk'; +import { IGNORE_TABLES } from '../../../utils/common/BaseApiBuilder'; + +export async function populateMeta(base: Base, project: Project): Promise { + const info = { + type: 'rest', + apiCount: 0, + tablesCount: 0, + relationsCount: 0, + viewsCount: 0, + client: base?.getConnectionConfig()?.client, + timeTaken: 0, + }; + + const t = process.hrtime(); + const sqlClient = await NcConnectionMgrv2.getSqlClient(base); + let order = 1; + const models2: { [tableName: string]: Model } = {}; + + const virtualColumnsInsert = []; + + /* Get all relations */ + const relations = (await sqlClient.relationListAll())?.data?.list; + + info.relationsCount = relations.length; + + let tables = (await sqlClient.tableList())?.data?.list + ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn)) + ?.map((t) => { + t.order = ++order; + t.title = getTableNameAlias(t.tn, project.prefix, base); + t.table_name = t.tn; + return t; + }); + + /* filter based on prefix */ + if (base.is_meta && project?.prefix) { + tables = tables.filter((t) => { + return t?.tn?.startsWith(project?.prefix); + }); + } + + info.tablesCount = tables.length; + + tables.forEach((t) => { + t.title = getTableNameAlias(t.tn, project.prefix, base); + }); + + relations.forEach((r) => { + r.title = getTableNameAlias(r.tn, project.prefix, base); + r.rtitle = getTableNameAlias(r.rtn, project.prefix, base); + }); + + // await this.syncRelations(); + + const tableMetasInsert = tables.map((table) => { + return async () => { + /* filter relation where this table is present */ + const tableRelations = relations.filter( + (r) => r.tn === table.tn || r.rtn === table.tn + ); + + const columns: Array< + Omit & { + cn: string; + system?: boolean; + } + > = (await sqlClient.columnList({ tn: table.tn }))?.data?.list; + + const hasMany = + table.type === 'view' + ? [] + : tableRelations.filter((r) => r.rtn === table.tn); + const belongsTo = + table.type === 'view' + ? [] + : tableRelations.filter((r) => r.tn === table.tn); + + mapDefaultPrimaryValue(columns); + + // add vitual columns + const virtualColumns = [ + ...hasMany.map((hm) => { + return { + uidt: UITypes.LinkToAnotherRecord, + type: 'hm', + hm, + title: `${hm.title} List`, + }; + }), + ...belongsTo.map((bt) => { + // find and mark foreign key column + const fkColumn = columns.find((c) => c.cn === bt.cn); + if (fkColumn) { + fkColumn.uidt = UITypes.ForeignKey; + fkColumn.system = true; + } + + return { + uidt: UITypes.LinkToAnotherRecord, + type: 'bt', + bt, + title: `${bt.rtitle}`, + }; + }), + ]; + + // await Model.insert(project.id, base.id, meta); + + /* create nc_models and its rows if it doesn't exists */ + models2[table.table_name] = await Model.insert(project.id, base.id, { + table_name: table.tn || table.table_name, + title: table.title, + type: table.type || 'table', + order: table.order, + }); + + // table crud apis + info.apiCount += 5; + + let colOrder = 1; + + for (const column of columns) { + await Column.insert({ + uidt: column.uidt || getColumnUiType(base, column), + fk_model_id: models2[table.tn].id, + ...column, + title: getColumnNameAlias(column.cn, base), + column_name: column.cn, + order: colOrder++, + }); + } + virtualColumnsInsert.push(async () => { + const columnNames = {}; + for (const column of virtualColumns) { + // generate unique name if there is any duplicate column name + let c = 0; + while (`${column.title}${c || ''}` in columnNames) { + c++; + } + column.title = `${column.title}${c || ''}`; + columnNames[column.title] = true; + + const rel = column.hm || column.bt; + + const rel_column_id = (await models2?.[rel.tn]?.getColumns())?.find( + (c) => c.column_name === rel.cn + )?.id; + + const tnId = models2?.[rel.tn]?.id; + + const ref_rel_column_id = ( + await models2?.[rel.rtn]?.getColumns() + )?.find((c) => c.column_name === rel.rcn)?.id; + + const rtnId = models2?.[rel.rtn]?.id; + + try { + await Column.insert({ + project_id: project.id, + db_alias: base.id, + fk_model_id: models2[table.tn].id, + cn: column.cn, + title: column.title, + uidt: column.uidt, + type: column.hm ? 'hm' : column.mm ? 'mm' : 'bt', + // column_id, + fk_child_column_id: rel_column_id, + fk_parent_column_id: ref_rel_column_id, + fk_index_name: rel.cstn, + ur: rel.ur, + dr: rel.dr, + order: colOrder++, + fk_related_model_id: column.hm ? tnId : rtnId, + system: column.system, + }); + + // nested relations data apis + info.apiCount += 5; + } catch (e) { + console.log(e); + } + } + }); + }; + }); + + /* handle xc_tables update in parallel */ + await NcHelp.executeOperations(tableMetasInsert, base.type); + await NcHelp.executeOperations(virtualColumnsInsert, base.type); + await extractAndGenerateManyToManyRelations(Object.values(models2)); + + let views: Array<{ order: number; table_name: string; title: string }> = ( + await sqlClient.viewList() + )?.data?.list + // ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn)) + ?.map((v) => { + v.order = ++order; + v.table_name = v.view_name; + v.title = getTableNameAlias(v.view_name, project.prefix, base); + return v; + }); + + /* filter based on prefix */ + if (base.is_meta && project?.prefix) { + views = tables.filter((t) => { + return t?.tn?.startsWith(project?.prefix); + }); + } + + info.viewsCount = views.length; + + const viewMetasInsert = views.map((table) => { + return async () => { + const columns = (await sqlClient.columnList({ tn: table.table_name })) + ?.data?.list; + + /* create nc_models and its rows if it doesn't exists */ + models2[table.table_name] = await Model.insert(project.id, base.id, { + table_name: table.table_name, + title: getTableNameAlias(table.table_name, project.prefix, base), + // todo: sanitize + type: ModelTypes.VIEW, + order: table.order, + }); + + let colOrder = 1; + + // view apis + info.apiCount += 2; + + for (const column of columns) { + await Column.insert({ + fk_model_id: models2[table.table_name].id, + ...column, + title: getColumnNameAlias(column.cn, base), + order: colOrder++, + uidt: getColumnUiType(base, column), + }); + } + }; + }); + + await NcHelp.executeOperations(viewMetasInsert, base.type); + + // fix pv column for created grid views + const models = await Model.list({ project_id: project.id, base_id: base.id }); + + for (const model of models) { + const views = await model.getViews(); + for (const view of views) { + if (view.type === ViewTypes.GRID) { + await View.fixPVColumnForView(view.id); + } + } + } + + const t1 = process.hrtime(t); + const t2 = t1[0] + t1[1] / 1000000000; + + (info as any).timeTaken = t2.toFixed(1); + + return info; +} diff --git a/packages/nocodb/src/lib/meta/api/metaDiffApis.ts b/packages/nocodb/src/lib/meta/api/metaDiffApis.ts index 1708e85717..e9a0fdff93 100644 --- a/packages/nocodb/src/lib/meta/api/metaDiffApis.ts +++ b/packages/nocodb/src/lib/meta/api/metaDiffApis.ts @@ -107,6 +107,7 @@ type MetaDiffChange = { cn?: string; rcn?: string; relationType: RelationTypes; + cstn?: string; } ); @@ -146,6 +147,7 @@ async function getMetaDiff( cn: string; rcn: string; found?: any; + cstn?: string; }> = (await sqlClient.relationListAll())?.data?.list; for (const table of tableList) { @@ -394,6 +396,7 @@ async function getMetaDiff( rcn: relation.rcn, msg: `New relation added`, relationType: RelationTypes.BELONGS_TO, + cstn: relation.cstn, }); } if (!relation?.found?.[RelationTypes.HAS_MANY]) { @@ -736,6 +739,7 @@ export async function metaDiffSync(req, res) { fk_parent_column_id: parentCol.id, fk_child_column_id: childCol.id, virtual: false, + fk_index_name: change.cstn, }); } else if (change.relationType === RelationTypes.HAS_MANY) { const title = getUniqueColumnAliasName( @@ -751,6 +755,7 @@ export async function metaDiffSync(req, res) { fk_parent_column_id: parentCol.id, fk_child_column_id: childCol.id, virtual: false, + fk_index_name: change.cstn, }); } }); diff --git a/packages/nocodb/src/lib/meta/api/projectApis.ts b/packages/nocodb/src/lib/meta/api/projectApis.ts index 33f1928ff5..8806fc41ac 100644 --- a/packages/nocodb/src/lib/meta/api/projectApis.ts +++ b/packages/nocodb/src/lib/meta/api/projectApis.ts @@ -1,33 +1,24 @@ import { Request, Response } from 'express'; import { OrgUserRoles, ProjectType } from 'nocodb-sdk'; import Project from '../../models/Project'; -import { ModelTypes, ProjectListType, UITypes } from 'nocodb-sdk'; +import { ProjectListType } from 'nocodb-sdk'; import DOMPurify from 'isomorphic-dompurify'; import { packageVersion } from '../../utils/packageVersion'; import { Tele } from 'nc-help'; import { PagedResponseImpl } from '../helpers/PagedResponse'; import syncMigration from '../helpers/syncMigration'; -import { IGNORE_TABLES } from '../../utils/common/BaseApiBuilder'; -import Column from '../../models/Column'; -import Model from '../../models/Model'; -import NcHelp from '../../utils/NcHelp'; -import Base from '../../models/Base'; import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2'; -import getTableNameAlias, { getColumnNameAlias } from '../helpers/getTableName'; -import LinkToAnotherRecordColumn from '../../models/LinkToAnotherRecordColumn'; import ncMetaAclMw from '../helpers/ncMetaAclMw'; import ProjectUser from '../../models/ProjectUser'; import { customAlphabet } from 'nanoid'; import Noco from '../../Noco'; import isDocker from 'is-docker'; import { NcError } from '../helpers/catchError'; -import getColumnUiType from '../helpers/getColumnUiType'; -import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue'; -import { extractAndGenerateManyToManyRelations } from './metaDiffApis'; import { metaApiMetrics } from '../helpers/apiMetrics'; import { extractPropsAndSanitize } from '../helpers/extractProps'; import NcConfigFactory from '../../utils/NcConfigFactory'; import { promisify } from 'util'; +import { populateMeta } from './helpers'; const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4); @@ -202,256 +193,6 @@ async function projectCreate(req: Request, res) { res.json(project); } -async function populateMeta(base: Base, project: Project): Promise { - const info = { - type: 'rest', - apiCount: 0, - tablesCount: 0, - relationsCount: 0, - viewsCount: 0, - client: base?.getConnectionConfig()?.client, - timeTaken: 0, - }; - - const t = process.hrtime(); - const sqlClient = await NcConnectionMgrv2.getSqlClient(base); - let order = 1; - const models2: { [tableName: string]: Model } = {}; - - const virtualColumnsInsert = []; - - /* Get all relations */ - const relations = (await sqlClient.relationListAll())?.data?.list; - - info.relationsCount = relations.length; - - let tables = (await sqlClient.tableList())?.data?.list - ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn)) - ?.map((t) => { - t.order = ++order; - t.title = getTableNameAlias(t.tn, project.prefix, base); - t.table_name = t.tn; - return t; - }); - - /* filter based on prefix */ - if (project?.prefix) { - tables = tables.filter((t) => { - return t?.tn?.startsWith(project?.prefix); - }); - } - - info.tablesCount = tables.length; - - tables.forEach((t) => { - t.title = getTableNameAlias(t.tn, project.prefix, base); - }); - - relations.forEach((r) => { - r.title = getTableNameAlias(r.tn, project.prefix, base); - r.rtitle = getTableNameAlias(r.rtn, project.prefix, base); - }); - - // await this.syncRelations(); - - const tableMetasInsert = tables.map((table) => { - return async () => { - /* filter relation where this table is present */ - const tableRelations = relations.filter( - (r) => r.tn === table.tn || r.rtn === table.tn - ); - - const columns: Array< - Omit & { - cn: string; - system?: boolean; - } - > = (await sqlClient.columnList({ tn: table.tn }))?.data?.list; - - const hasMany = - table.type === 'view' - ? [] - : tableRelations.filter((r) => r.rtn === table.tn); - const belongsTo = - table.type === 'view' - ? [] - : tableRelations.filter((r) => r.tn === table.tn); - - mapDefaultPrimaryValue(columns); - - // add vitual columns - const virtualColumns = [ - ...hasMany.map((hm) => { - return { - uidt: UITypes.LinkToAnotherRecord, - type: 'hm', - hm, - title: `${hm.title} List`, - }; - }), - ...belongsTo.map((bt) => { - // find and mark foreign key column - const fkColumn = columns.find((c) => c.cn === bt.cn); - if (fkColumn) { - fkColumn.uidt = UITypes.ForeignKey; - fkColumn.system = true; - } - - return { - uidt: UITypes.LinkToAnotherRecord, - type: 'bt', - bt, - title: `${bt.rtitle}`, - }; - }), - ]; - - // await Model.insert(project.id, base.id, meta); - - /* create nc_models and its rows if it doesn't exists */ - models2[table.table_name] = await Model.insert(project.id, base.id, { - table_name: table.tn || table.table_name, - title: table.title, - type: table.type || 'table', - order: table.order, - }); - - // table crud apis - info.apiCount += 5; - - let colOrder = 1; - - for (const column of columns) { - await Column.insert({ - uidt: column.uidt || getColumnUiType(base, column), - fk_model_id: models2[table.tn].id, - ...column, - title: getColumnNameAlias(column.cn, base), - column_name: column.cn, - order: colOrder++, - }); - } - virtualColumnsInsert.push(async () => { - const columnNames = {}; - for (const column of virtualColumns) { - // generate unique name if there is any duplicate column name - let c = 0; - while (`${column.title}${c || ''}` in columnNames) { - c++; - } - column.title = `${column.title}${c || ''}`; - columnNames[column.title] = true; - - const rel = column.hm || column.bt; - - const rel_column_id = (await models2?.[rel.tn]?.getColumns())?.find( - (c) => c.column_name === rel.cn - )?.id; - - const tnId = models2?.[rel.tn]?.id; - - const ref_rel_column_id = ( - await models2?.[rel.rtn]?.getColumns() - )?.find((c) => c.column_name === rel.rcn)?.id; - - const rtnId = models2?.[rel.rtn]?.id; - - try { - await Column.insert({ - project_id: project.id, - db_alias: base.id, - fk_model_id: models2[table.tn].id, - cn: column.cn, - title: column.title, - uidt: column.uidt, - type: column.hm ? 'hm' : column.mm ? 'mm' : 'bt', - // column_id, - fk_child_column_id: rel_column_id, - fk_parent_column_id: ref_rel_column_id, - fk_index_name: rel.fkn, - ur: rel.ur, - dr: rel.dr, - order: colOrder++, - fk_related_model_id: column.hm ? tnId : rtnId, - system: column.system, - }); - - // nested relations data apis - info.apiCount += 5; - } catch (e) { - console.log(e); - } - } - }); - }; - }); - - /* handle xc_tables update in parallel */ - await NcHelp.executeOperations(tableMetasInsert, base.type); - await NcHelp.executeOperations(virtualColumnsInsert, base.type); - await extractAndGenerateManyToManyRelations(Object.values(models2)); - - let views: Array<{ order: number; table_name: string; title: string }> = ( - await sqlClient.viewList() - )?.data?.list - // ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn)) - ?.map((v) => { - v.order = ++order; - v.table_name = v.view_name; - v.title = getTableNameAlias(v.view_name, project.prefix, base); - return v; - }); - - /* filter based on prefix */ - if (project?.prefix) { - views = tables.filter((t) => { - return t?.tn?.startsWith(project?.prefix); - }); - } - - info.viewsCount = views.length; - - const viewMetasInsert = views.map((table) => { - return async () => { - const columns = (await sqlClient.columnList({ tn: table.table_name })) - ?.data?.list; - - /* create nc_models and its rows if it doesn't exists */ - models2[table.table_name] = await Model.insert(project.id, base.id, { - table_name: table.table_name, - title: getTableNameAlias(table.table_name, project.prefix, base), - // todo: sanitize - type: ModelTypes.VIEW, - order: table.order, - }); - - let colOrder = 1; - - // view apis - info.apiCount += 2; - - for (const column of columns) { - await Column.insert({ - fk_model_id: models2[table.table_name].id, - ...column, - title: getColumnNameAlias(column.cn, base), - order: colOrder++, - uidt: getColumnUiType(base, column), - }); - } - }; - }); - - await NcHelp.executeOperations(viewMetasInsert, base.type); - - const t1 = process.hrtime(t); - const t2 = t1[0] + t1[1] / 1000000000; - - (info as any).timeTaken = t2.toFixed(1); - - return info; -} - export async function projectInfoGet(_req, res) { res.json({ Node: process.version, diff --git a/packages/nocodb/src/lib/meta/api/userApi/userApis.ts b/packages/nocodb/src/lib/meta/api/userApi/userApis.ts index a7ea342bc1..63aadfc393 100644 --- a/packages/nocodb/src/lib/meta/api/userApi/userApis.ts +++ b/packages/nocodb/src/lib/meta/api/userApi/userApis.ts @@ -534,7 +534,7 @@ const mapRoutes = (router) => { '/user/password/change', ncMetaAclMw(passwordChange, 'passwordChange') ); - router.post('/auth/token/refresh', ncMetaAclMw(refreshToken, 'refreshToken')); + router.post('/auth/token/refresh', catchError(refreshToken)); /* Google auth apis */ @@ -573,10 +573,7 @@ const mapRoutes = (router) => { '/api/v1/db/auth/password/change', ncMetaAclMw(passwordChange, 'passwordChange') ); - router.post( - '/api/v1/db/auth/token/refresh', - ncMetaAclMw(refreshToken, 'refreshToken') - ); + router.post('/api/v1/db/auth/token/refresh', catchError(refreshToken)); router.get( '/api/v1/db/auth/password/reset/:tokenId', catchError(renderPasswordReset) @@ -607,10 +604,7 @@ const mapRoutes = (router) => { '/api/v1/auth/password/change', ncMetaAclMw(passwordChange, 'passwordChange') ); - router.post( - '/api/v1/auth/token/refresh', - ncMetaAclMw(refreshToken, 'refreshToken') - ); + router.post('/api/v1/auth/token/refresh', catchError(refreshToken)); // respond with password reset page router.get('/auth/password/reset/:tokenId', catchError(renderPasswordReset)); }; diff --git a/packages/nocodb/src/lib/models/GridViewColumn.ts b/packages/nocodb/src/lib/models/GridViewColumn.ts index 4d0215e9d3..e1d0398276 100644 --- a/packages/nocodb/src/lib/models/GridViewColumn.ts +++ b/packages/nocodb/src/lib/models/GridViewColumn.ts @@ -109,6 +109,8 @@ export default class GridViewColumn implements GridColumnType { `${CacheScope.GRID_VIEW_COLUMN}:${id}` ); + await View.fixPVColumnForView(column.fk_view_id, ncMeta); + return this.get(id, ncMeta); } diff --git a/packages/nocodb/src/lib/models/Model.ts b/packages/nocodb/src/lib/models/Model.ts index a77e10f409..3708b2e38f 100644 --- a/packages/nocodb/src/lib/models/Model.ts +++ b/packages/nocodb/src/lib/models/Model.ts @@ -606,6 +606,23 @@ export default class Model implements TableType { newPvCol.id ); + const grid_views_with_column = await ncMeta.metaList2( + null, + null, + MetaTable.GRID_VIEW_COLUMNS, + { + condition: { + fk_column_id: newPvCol.id, + }, + } + ); + + if (grid_views_with_column.length) { + for (const gv of grid_views_with_column) { + await View.fixPVColumnForView(gv.fk_view_id, ncMeta); + } + } + return true; } diff --git a/packages/nocodb/src/lib/models/View.ts b/packages/nocodb/src/lib/models/View.ts index 8dbc13566a..c46061ded6 100644 --- a/packages/nocodb/src/lib/models/View.ts +++ b/packages/nocodb/src/lib/models/View.ts @@ -692,6 +692,35 @@ export default class View implements ViewType { break; } const updateObj = extractProps(colData, ['order', 'show']); + + // keep primary_value_column always visible and first in grid view + if (view.type === ViewTypes.GRID) { + const primary_value_column_meta = await ncMeta.metaGet2( + null, + null, + MetaTable.COLUMNS, + { + fk_model_id: view.fk_model_id, + pv: true, + } + ); + + const primary_value_column = await ncMeta.metaGet2( + null, + null, + MetaTable.GRID_VIEW_COLUMNS, + { + fk_view_id: view.id, + fk_column_id: primary_value_column_meta.id, + } + ); + + if (primary_value_column && primary_value_column.id === colId) { + updateObj.order = 1; + updateObj.show = true; + } + } + // get existing cache const key = `${cacheScope}:${colId}`; let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT); @@ -954,7 +983,16 @@ export default class View implements ViewType { // set meta await ncMeta.metaUpdate(null, null, MetaTable.VIEWS, updateObj, viewId); - return this.get(viewId); + + const view = await this.get(viewId); + + if (view.type === ViewTypes.GRID) { + if ('show_system_fields' in updateObj) { + await View.fixPVColumnForView(viewId, ncMeta); + } + } + + return view; } // @ts-ignore @@ -1165,6 +1203,24 @@ export default class View implements ViewType { const view = await this.get(viewId); const table = this.extractViewColumnsTableName(view); const scope = this.extractViewColumnsTableNameScope(view); + + if (view.type === ViewTypes.GRID) { + const primary_value_column = await ncMeta.metaGet2( + null, + null, + MetaTable.COLUMNS, + { + fk_model_id: view.fk_model_id, + pv: true, + } + ); + + // keep primary_value_column always visible + if (primary_value_column) { + ignoreColdIds.push(primary_value_column.id); + } + } + // get existing cache const dataList = await NocoCache.getList(scope, [viewId]); if (dataList?.length) { @@ -1222,4 +1278,105 @@ export default class View implements ViewType { sharedViews = sharedViews.filter((v) => v.uuid !== null); return sharedViews?.map((v) => new View(v)); } + + static async fixPVColumnForView(viewId, ncMeta = Noco.ncMeta) { + // get a list of view columns sorted by order + const view_columns = await ncMeta.metaList2( + null, + null, + MetaTable.GRID_VIEW_COLUMNS, + { + condition: { + fk_view_id: viewId, + }, + orderBy: { + order: 'asc', + }, + } + ); + const view_columns_meta = []; + + // get column meta for each view column + for (const col of view_columns) { + const col_meta = await ncMeta.metaGet2( + null, + null, + MetaTable.COLUMNS, + col.fk_column_id + ); + view_columns_meta.push(col_meta); + } + + const primary_value_column_meta = view_columns_meta.find((col) => col.pv); + + if (primary_value_column_meta) { + const primary_value_column = view_columns.find( + (col) => col.fk_column_id === primary_value_column_meta.id + ); + const primary_value_column_index = view_columns.findIndex( + (col) => col.fk_column_id === primary_value_column_meta.id + ); + const view_orders = view_columns.map((col) => col.order); + const view_min_order = Math.min(...view_orders); + + // if primary_value_column is not visible, make it visible + if (!primary_value_column.show) { + await ncMeta.metaUpdate( + null, + null, + MetaTable.GRID_VIEW_COLUMNS, + { show: true }, + primary_value_column.id + ); + await NocoCache.set( + `${CacheScope.GRID_VIEW_COLUMN}:${primary_value_column.id}`, + primary_value_column + ); + } + + if ( + primary_value_column.order === view_min_order && + view_orders.filter((o) => o === view_min_order).length === 1 + ) { + // if primary_value_column is in first order do nothing + return; + } else { + // if primary_value_column not in first order, move it to the start of array + if (primary_value_column_index !== 0) { + const temp_pv = view_columns.splice(primary_value_column_index, 1); + view_columns.unshift(...temp_pv); + } + + // update order of all columns in view to match the order in array + for (let i = 0; i < view_columns.length; i++) { + await ncMeta.metaUpdate( + null, + null, + MetaTable.GRID_VIEW_COLUMNS, + { order: i + 1 }, + view_columns[i].id + ); + await NocoCache.set( + `${CacheScope.GRID_VIEW_COLUMN}:${view_columns[i].id}`, + view_columns[i] + ); + } + } + } + + const views = await ncMeta.metaList2( + null, + null, + MetaTable.GRID_VIEW_COLUMNS, + { + condition: { + fk_view_id: viewId, + }, + orderBy: { + order: 'asc', + }, + } + ); + await NocoCache.setList(CacheScope.GRID_VIEW_COLUMN, [viewId], views); + } } diff --git a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts index 06890a86c3..1fea08ebed 100644 --- a/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts +++ b/packages/nocodb/src/lib/version-upgrader/NcUpgrader.ts @@ -11,6 +11,7 @@ import ncProjectRolesUpgrader from './ncProjectRolesUpgrader'; import ncFilterUpgrader from './ncFilterUpgrader'; import ncAttachmentUpgrader from './ncAttachmentUpgrader'; import ncAttachmentUpgrader_0104002 from './ncAttachmentUpgrader_0104002'; +import ncStickyColumnUpgrader from './ncStickyColumnUpgrader'; const log = debug('nc:version-upgrader'); import boxen from 'boxen'; @@ -41,6 +42,7 @@ export default class NcUpgrader { { name: '0100002', handler: ncFilterUpgrader }, { name: '0101002', handler: ncAttachmentUpgrader }, { name: '0104002', handler: ncAttachmentUpgrader_0104002 }, + { name: '0104003', handler: ncStickyColumnUpgrader }, ]; if (!(await ctx.ncMeta.knexConnection?.schema?.hasTable?.('nc_store'))) { return; diff --git a/packages/nocodb/src/lib/version-upgrader/ncStickyColumnUpgrader.ts b/packages/nocodb/src/lib/version-upgrader/ncStickyColumnUpgrader.ts new file mode 100644 index 0000000000..60ebe6647e --- /dev/null +++ b/packages/nocodb/src/lib/version-upgrader/ncStickyColumnUpgrader.ts @@ -0,0 +1,90 @@ +import { NcUpgraderCtx } from './NcUpgrader'; +import { MetaTable } from '../utils/globals'; + +// before 0.104.3, primary value column can be in any position in table +// with this upgrade we introduced sticky primary column feature +// this upgrader will make primary value column first column in grid views + +export default async function ({ ncMeta }: NcUpgraderCtx) { + const grid_columns = await ncMeta.metaList2( + null, + null, + MetaTable.GRID_VIEW_COLUMNS + ); + const grid_views = [...new Set(grid_columns.map((col) => col.fk_view_id))]; + + for (const view_id of grid_views) { + // get a list of view columns sorted by order + const view_columns = await ncMeta.metaList2( + null, + null, + MetaTable.GRID_VIEW_COLUMNS, + { + condition: { + fk_view_id: view_id, + }, + orderBy: { + order: 'asc', + }, + } + ); + const view_columns_meta = []; + + // get column meta for each view column + for (const col of view_columns) { + const col_meta = await ncMeta.metaGet(null, null, MetaTable.COLUMNS, { + id: col.fk_column_id, + }); + view_columns_meta.push(col_meta); + } + + const primary_value_column_meta = view_columns_meta.find((col) => col.pv); + + if (primary_value_column_meta) { + const primary_value_column = view_columns.find( + (col) => col.fk_column_id === primary_value_column_meta.id + ); + const primary_value_column_index = view_columns.findIndex( + (col) => col.fk_column_id === primary_value_column_meta.id + ); + const view_orders = view_columns.map((col) => col.order); + const view_min_order = Math.min(...view_orders); + + // if primary_value_column is not visible, make it visible + if (!primary_value_column.show) { + await ncMeta.metaUpdate( + null, + null, + MetaTable.GRID_VIEW_COLUMNS, + { show: true }, + primary_value_column.id + ); + } + + if ( + primary_value_column.order === view_min_order && + view_orders.filter((o) => o === view_min_order).length === 1 + ) { + // if primary_value_column is in first order do nothing + continue; + } else { + // if primary_value_column not in first order, move it to the start of array + if (primary_value_column_index !== 0) { + const temp_pv = view_columns.splice(primary_value_column_index, 1); + view_columns.unshift(...temp_pv); + } + + // update order of all columns in view to match the order in array + for (let i = 0; i < view_columns.length; i++) { + await ncMeta.metaUpdate( + null, + null, + MetaTable.GRID_VIEW_COLUMNS, + { order: i + 1 }, + view_columns[i].id + ); + } + } + } + } +} diff --git a/tests/playwright/pages/Dashboard/Grid/Column/index.ts b/tests/playwright/pages/Dashboard/Grid/Column/index.ts index 064b2093b5..3e2948c1bc 100644 --- a/tests/playwright/pages/Dashboard/Grid/Column/index.ts +++ b/tests/playwright/pages/Dashboard/Grid/Column/index.ts @@ -44,6 +44,7 @@ export class ColumnPageObject extends BasePage { timeFormat = '', insertAfterColumnTitle, insertBeforeColumnTitle, + isPrimaryValue = false, }: { title: string; type?: string; @@ -60,9 +61,16 @@ export class ColumnPageObject extends BasePage { timeFormat?: string; insertBeforeColumnTitle?: string; insertAfterColumnTitle?: string; + isPrimaryValue?: boolean; }) { if (insertBeforeColumnTitle) { await this.grid.get().locator(`th[data-title="${insertBeforeColumnTitle}"] .nc-ui-dt-dropdown`).click(); + + if (isPrimaryValue) { + await expect(this.rootPage.locator('li[role="menuitem"]:has-text("Insert Before")')).toHaveCount(0); + return; + } + await this.rootPage.locator('li[role="menuitem"]:has-text("Insert Before"):visible').click(); } else if (insertAfterColumnTitle) { await this.grid.get().locator(`th[data-title="${insertAfterColumnTitle}"] .nc-ui-dt-dropdown`).click(); @@ -313,9 +321,14 @@ export class ColumnPageObject extends BasePage { await this.grid.get().locator(`th[data-title="${expectedTitle}"]`).isVisible(); } - async hideColumn({ title }: { title: string }) { + async hideColumn({ title, isPrimaryValue = false }: { title: string; isPrimaryValue?: boolean }) { await this.grid.get().locator(`th[data-title="${title}"] .nc-ui-dt-dropdown`).click(); + if (isPrimaryValue) { + await expect(this.rootPage.locator('li[role="menuitem"]:has-text("Hide Field")')).toHaveCount(0); + return; + } + await this.waitForResponse({ uiAction: this.rootPage.locator('li[role="menuitem"]:has-text("Hide Field"):visible').click(), requestUrlPathToMatch: 'api/v1/db/meta/views', @@ -386,5 +399,8 @@ export class ColumnPageObject extends BasePage { ) .first() .isVisible(); + + // close sort menu + await this.grid.toolbar.clickSort(); } } diff --git a/tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts b/tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts index 37b41ff83c..52ad7fcdb8 100644 --- a/tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts +++ b/tests/playwright/pages/Dashboard/common/Cell/AttachmentCell.ts @@ -48,15 +48,21 @@ export class AttachmentCellPageObject extends BasePage { } async verifyFileCount({ index, columnHeader, count }: { index: number; columnHeader: string; count: number }) { - const attachments = await this.get({ index, columnHeader }).locator( - '.nc-cell > .nc-attachment-cell > .flex > .nc-attachment' - ); + // retry below logic for 5 times, with 1 second delay + let retryCount = 0; + while (retryCount < 5) { + const attachments = await this.get({ index, columnHeader }).locator('.nc-attachment'); + console.log(await attachments.count()); + if ((await attachments.count()) === count) { + break; + } + retryCount++; + await this.rootPage.waitForTimeout(1000); - console.log(await attachments.count()); - expect(await attachments.count()).toBe(count); - - // attachments should be of count 'count' - // await expect(await attachments.count()).toBe(count); + if (retryCount === 5) { + expect(await attachments.count()).toBe(count); + } + } } async expandModalClose() { diff --git a/tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts b/tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts index 02bb384731..08c8bfc1ed 100644 --- a/tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts +++ b/tests/playwright/pages/Dashboard/common/Toolbar/Fields.ts @@ -31,12 +31,12 @@ export class ToolbarFieldsPage extends BasePage { await this.toolbar.clickFields(); } - async verify({ title, checked }: { title: string; checked: boolean }) { + async verify({ title, checked }: { title: string; checked?: boolean }) { const checkbox = this.get().locator(`[data-testid="nc-fields-menu-${title}"]`).locator('input[type="checkbox"]'); if (checked) { await expect(checkbox).toBeChecked(); - } else { + } else if (checked === false) { await expect(checkbox).not.toBeChecked(); } } diff --git a/tests/playwright/quickTests/commonTest.ts b/tests/playwright/quickTests/commonTest.ts index 3d5d0dc6bf..1a5c30673c 100644 --- a/tests/playwright/quickTests/commonTest.ts +++ b/tests/playwright/quickTests/commonTest.ts @@ -208,7 +208,7 @@ const quickVerify = async ({ // Verify Fields await dashboard.grid.toolbar.clickFields(); - await dashboard.grid.toolbar.fields.verify({ title: 'Name', checked: true }); + await dashboard.grid.toolbar.fields.verify({ title: 'Name' }); await dashboard.grid.toolbar.fields.verify({ title: 'Notes', checked: true }); await dashboard.grid.toolbar.fields.verify({ title: 'Attachments', checked: false }); await dashboard.grid.toolbar.fields.verify({ title: 'Status', checked: true }); diff --git a/tests/playwright/tests/cellSelection.spec.ts b/tests/playwright/tests/cellSelection.spec.ts index 9ef20b7fb9..f15c6fb277 100644 --- a/tests/playwright/tests/cellSelection.spec.ts +++ b/tests/playwright/tests/cellSelection.spec.ts @@ -83,7 +83,7 @@ test.describe('Verify cell selection', () => { await dashboard.grid.toolbar.fields.toggleShowSystemFields(); await grid.selectRange({ start: { index: 2, columnHeader: 'City List' }, - end: { index: 0, columnHeader: 'CountryId' }, + end: { index: 0, columnHeader: 'Country' }, }); expect(await grid.selectedCount()).toBe(12); diff --git a/tests/playwright/tests/columnAttachments.spec.ts b/tests/playwright/tests/columnAttachments.spec.ts index fba2c41aa7..cd88220172 100644 --- a/tests/playwright/tests/columnAttachments.spec.ts +++ b/tests/playwright/tests/columnAttachments.spec.ts @@ -138,7 +138,6 @@ test.describe('Attachment column', () => { columnHeader: 'testAttach', filePath: twoFileArray, }); - await dashboard.rootPage.waitForTimeout(2000); await dashboard.grid.cell.attachment.verifyFileCount({ index: 1, columnHeader: 'testAttach', diff --git a/tests/playwright/tests/columnMenuOperations.spec.ts b/tests/playwright/tests/columnMenuOperations.spec.ts index 8217fd6964..e2c78eb9af 100644 --- a/tests/playwright/tests/columnMenuOperations.spec.ts +++ b/tests/playwright/tests/columnMenuOperations.spec.ts @@ -91,6 +91,7 @@ test.describe('Column menu operations', () => { title: 'InsertBeforeColumn', type: 'SingleLineText', insertBeforeColumnTitle: 'Title', + isPrimaryValue: true, }); await dashboard.grid.column.create({ @@ -107,6 +108,7 @@ test.describe('Column menu operations', () => { await dashboard.grid.column.hideColumn({ title: 'Title', + isPrimaryValue: true, }); await dashboard.grid.column.hideColumn({ diff --git a/tests/playwright/tests/metaSync.spec.ts b/tests/playwright/tests/metaSync.spec.ts index 2011508ca5..f845c384d3 100644 --- a/tests/playwright/tests/metaSync.spec.ts +++ b/tests/playwright/tests/metaSync.spec.ts @@ -252,18 +252,18 @@ test.describe('Meta sync', () => { await dashboard.treeView.openTable({ title: 'Table1' }); await dashboard.grid.toolbar.clickFields(); - await dashboard.grid.toolbar.fields.click({ title: 'Col1' }); + await dashboard.grid.toolbar.fields.click({ title: 'Col2' }); await dashboard.grid.toolbar.clickFields(); await dashboard.grid.toolbar.sort.add({ - columnTitle: 'Col1', + columnTitle: 'Col2', isAscending: false, isLocallySaved: false, }); await dashboard.grid.toolbar.clickFilter(); await dashboard.grid.toolbar.filter.add({ - columnTitle: 'Col1', + columnTitle: 'Col2', opType: '>=', value: '5', isLocallySaved: false,