Browse Source

Merge pull request #6229 from nocodb/test/github-runner

bug fixes & stability changes
pull/6230/head
Raju Udava 1 year ago committed by GitHub
parent
commit
9012fc8bd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 97
      .github/workflows/playwright-test-workflow.yml
  2. 1
      packages/nc-gui/components.d.ts
  3. 2
      packages/nc-gui/components/cell/TextArea.vue
  4. 39
      packages/nc-gui/components/dashboard/Sidebar.vue
  5. 9
      packages/nc-gui/components/dashboard/TreeViewNew/TableNode.vue
  6. 35
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  7. 10
      packages/nc-gui/components/dlg/ProjectDelete.vue
  8. 2
      packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue
  9. 5
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  10. 6
      packages/nc-gui/components/dlg/share-and-collaborate/View.vue
  11. 4
      packages/nc-gui/components/nc/Dropdown.vue
  12. 2
      packages/nc-gui/components/nc/Select.vue
  13. 2
      packages/nc-gui/components/nc/Tabs.vue
  14. 12
      packages/nc-gui/components/project/AccessSettings.vue
  15. 17
      packages/nc-gui/components/project/InviteProjectCollabSection.vue
  16. 39
      packages/nc-gui/components/project/View.vue
  17. 2
      packages/nc-gui/components/smartsheet/Gallery.vue
  18. 19
      packages/nc-gui/components/smartsheet/Kanban.vue
  19. 2
      packages/nc-gui/components/smartsheet/SharedMapMarkerPopup.vue
  20. 9
      packages/nc-gui/components/smartsheet/details/Api.vue
  21. 2
      packages/nc-gui/components/smartsheet/header/Menu.vue
  22. 8
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  23. 6
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  24. 10
      packages/nc-gui/components/webhook/Editor.vue
  25. 2
      packages/nc-gui/components/workspace/CreateProjectBtn.vue
  26. 2
      packages/nc-gui/components/workspace/CreateProjectDlg.vue
  27. 10
      packages/nc-gui/components/workspace/Menu.vue
  28. 8
      packages/nc-gui/composables/useGlobal/actions.ts
  29. 3
      packages/nc-gui/composables/useMultiSelect/index.ts
  30. 5
      packages/nc-gui/composables/useSharedFormViewStore.ts
  31. 16
      packages/nc-gui/pages/index-old/index/index.vue
  32. 4
      packages/nc-gui/pages/index/[typeOrId].vue
  33. 14
      packages/nc-gui/store/project.ts
  34. 39
      packages/nc-gui/store/projects.ts
  35. 4
      packages/nc-gui/store/tables.ts
  36. 18
      packages/nc-gui/store/views.ts
  37. 2
      packages/nc-gui/store/webhooks.ts
  38. 1
      packages/nc-gui/utils/browserUtils.ts
  39. 5
      packages/nc-gui/utils/virtualCell.ts
  40. 11
      packages/nocodb/src/middlewares/extract-ids/extract-ids.middleware.ts
  41. 2
      packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts
  42. 34
      packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts
  43. 14
      packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts
  44. 10
      packages/nocodb/src/services/projects.service.ts
  45. 17
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  46. 4
      tests/playwright/tests/db/views/viewGridShare.spec.ts

97
.github/workflows/playwright-test-workflow.yml

@ -13,9 +13,21 @@ on:
jobs: jobs:
playwright: playwright:
runs-on: [self-hosted, v2] runs-on: ubuntu-20.04
timeout-minutes: 100 timeout-minutes: 40
steps: steps:
# Reference: https://github.com/pierotofy/set-swap-space/blob/master/action.yml
- name: Set 5gb swap
shell: bash
# Delete the swap file, allocate a new one, and activate it
run: |
export SWAP_FILE=$(swapon --show=NAME | tail -n 1)
sudo swapoff $SWAP_FILE
sudo rm $SWAP_FILE
sudo fallocate -l 5G $SWAP_FILE
sudo chmod 600 $SWAP_FILE
sudo mkswap $SWAP_FILE
sudo swapon $SWAP_FILE
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
@ -30,31 +42,11 @@ jobs:
with: with:
# npm cache files are stored in `~/.npm` on Linux/macOS # npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm path: ~/.npm
key: ${{ runner.os }}-v2-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: | restore-keys: |
${{ runner.os }}-v2-build-${{ env.cache-name }}- ${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-v2-build- ${{ runner.os }}-build-
${{ runner.os }}-v2 ${{ runner.os }}-
- name: setup pg
if: ${{ inputs.db == 'pg' || ( inputs.db == 'sqlite' && inputs.shard == '1' ) }}
working-directory: ./
run: |
service postgresql start
cd /var/lib/postgresql/ && sudo -u postgres psql -c "SELECT 'dropdb '||datname||'' FROM pg_database WHERE datistemplate = false AND datallowconn = true And datname NOT IN ('postgres')" |grep ' dropdb ' | sudo -u postgres /bin/bash ; cd
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'password';"
sudo -u postgres psql -c "ALTER USER postgres WITH SUPERUSER;"
service postgresql restart
- name: Set CI env
run: export CI=true
- name: Kill stale servers
run: |
# export NODE_OPTIONS=\"--max_old_space_size=16384\";
kill -9 $(lsof -t -i:8080) || echo "no process running on 8080"
kill -9 $(lsof -t -i:3000) || echo "no process running on 3000"
- name: Set CI env
run: export CI=true
- name: Set NC Edition
run: export EE=true
- name: install dependencies nocodb-sdk - name: install dependencies nocodb-sdk
working-directory: ./packages/nocodb-sdk working-directory: ./packages/nocodb-sdk
run: npm install run: npm install
@ -63,23 +55,16 @@ jobs:
run: npm run build run: npm run build
- name: Setup mysql - name: Setup mysql
if: ${{ inputs.db == 'mysql' }} if: ${{ inputs.db == 'mysql' }}
working-directory: ./packages/nocodb/tests/mysql-sakila-db working-directory: ./
run: | run: docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d &
# Get a list of non-system databases and construct the DROP DATABASE statement for each - name: setup pg
service mysql start if: ${{ inputs.db == 'pg' }}
mysql -u'root' -p'password' -e "SHOW DATABASES" --skip-column-names | grep -Ev "(information_schema|mysql|performance_schema|sys)" | while read db; do working-directory: ./
mysql -u'root' -p'password' -e "DROP DATABASE IF EXISTS \`$db\`"; run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d &
done - name: setup pg for quick tests
# keep sql_mode default except remove "STRICT_TRANS_TABLES"
mysql -u'root' -p'password' -e "SET GLOBAL sql_mode = 'ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';"
# this is only needed for connecting to sakila db as its refeferred in multiple places in test code
mysql -u'root' -p'password' < 01-mysql-sakila-schema.sql
mysql -u'root' -p'password' < 02-mysql-sakila-insert-data.sql
- name: Setup pg for quick tests
if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }} if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }}
working-directory: ./packages/nocodb/tests/pg-cy-quick/ working-directory: ./
run: | run: docker-compose -f ./tests/playwright/scripts/docker-compose-pg-pw-quick.yml up -d &
sudo -u postgres psql -U postgres -f 01-cy-quick.sql
- name: run frontend - name: run frontend
working-directory: ./packages/nc-gui working-directory: ./packages/nc-gui
run: npm run ci:run run: npm run ci:run
@ -89,28 +74,26 @@ jobs:
working-directory: ./packages/nocodb working-directory: ./packages/nocodb
run: | run: |
npm install npm install
npm run watch:run:playwright &> ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & npm run watch:run:playwright > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log &
- name: Run backend:mysql - name: Run backend:mysql
if: ${{ inputs.db == 'mysql' }} if: ${{ inputs.db == 'mysql' }}
working-directory: ./packages/nocodb working-directory: ./packages/nocodb
run: | run: |
npm install npm install
npm run watch:run:playwright:mysql &> ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & npm run watch:run:playwright:mysql > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log &
- name: Run backend:pg - name: Run backend:pg
if: ${{ inputs.db == 'pg' }} if: ${{ inputs.db == 'pg' }}
working-directory: ./packages/nocodb working-directory: ./packages/nocodb
run: | run: |
npm install npm install
npm run watch:run:playwright:pg &> ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log & npm run watch:run:playwright:pg > ${{ inputs.db }}_${{ inputs.shard }}_test_backend.log &
- name: Cache playwright npm modules - name: Cache playwright npm modules
uses: actions/cache@v3 uses: actions/cache@v3
id: playwright-cache id: playwright-cache
with: with:
path: | path: |
**/tests/playwright/node_modules **/tests/playwright/node_modules
key: cache-v2-nc-playwright-${{ hashFiles('**/tests/playwright/package-lock.json') }} key: cache-nc-playwright-${{ hashFiles('**/tests/playwright/package-lock.json') }}
restore-keys: |
cache-v2-nc-playwright-
- name: Install dependencies - name: Install dependencies
if: steps.playwright-cache.outputs.cache-hit != 'true' if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: ./tests/playwright working-directory: ./tests/playwright
@ -137,20 +120,20 @@ jobs:
working-directory: ./tests/playwright working-directory: ./tests/playwright
run: E2E_DB_TYPE=${{ inputs.db }} node ./scripts/stressTestNewlyAddedTest.js run: E2E_DB_TYPE=${{ inputs.db }} node ./scripts/stressTestNewlyAddedTest.js
# # Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1) # Quick tests (pg on sqlite shard 0 and sqlite on sqlite shard 1)
# - name: Run quick server and tests (pg) # - name: Run quick server and tests (pg)
# if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }} # if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }}
# working-directory: ./packages/nocodb # working-directory: ./packages/nocodb
# run: | # run: |
# kill -9 $(lsof -t -i:8080) # kill -9 $(lsof -t -i:8080)
# npm run watch:run:playwright:pg:cyquick > quick_${{ inputs.shard }}_test_backend.log & # npm run watch:run:playwright:pg:cyquick &
# - name: Run quick server and tests (sqlite) # - name: Run quick server and tests (sqlite)
# if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }} # if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }}
# working-directory: ./packages/nocodb # working-directory: ./packages/nocodb
# run: | # run: |
# kill -9 $(lsof -t -i:8080) # kill -9 $(lsof -t -i:8080)
# npm run watch:run:playwright:quick > quick_${{ inputs.shard }}_test_backend.log & # npm run watch:run:playwright:quick > quick_${{ inputs.shard }}_test_backend.log &
# - name: Wait for backend for sqlite-tests # - name: Wait for backend & run quick tests
# if: ${{ inputs.db == 'sqlite' }} # if: ${{ inputs.db == 'sqlite' }}
# working-directory: ./tests/playwright # working-directory: ./tests/playwright
# run: | # run: |
@ -158,11 +141,7 @@ jobs:
# printf '.' # printf '.'
# sleep 2 # sleep 2
# done # done
# timeout-minutes: 1 # PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick
# - name: Run quick tests
# if: ${{ inputs.db == 'sqlite' }}
# working-directory: ./tests/playwright
# run: PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ inputs.db == 'sqlite' }} if: ${{ inputs.db == 'sqlite' }}
with: with:
@ -194,9 +173,3 @@ jobs:
name: backend-logs-${{ inputs.db }}-${{ inputs.shard }} name: backend-logs-${{ inputs.db }}-${{ inputs.shard }}
path: ./packages/nocodb/${{ inputs.db }}_${{ inputs.shard }}_test_backend.log path: ./packages/nocodb/${{ inputs.db }}_${{ inputs.shard }}_test_backend.log
retention-days: 2 retention-days: 2
- name: stop database servers
if: always()
working-directory: ./packages/nocodb
run: |
service postgresql stop
service mysql stop

1
packages/nc-gui/components.d.ts vendored

@ -151,6 +151,7 @@ declare module '@vue/runtime-core' {
MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default'] MdiDotsVertical: typeof import('~icons/mdi/dots-vertical')['default']
MdiEarth: typeof import('~icons/mdi/earth')['default'] MdiEarth: typeof import('~icons/mdi/earth')['default']
MdiEditOutline: typeof import('~icons/mdi/edit-outline')['default'] MdiEditOutline: typeof import('~icons/mdi/edit-outline')['default']
MdiEye: typeof import('~icons/mdi/eye')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default'] MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default'] MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default'] MdiHeart: typeof import('~icons/mdi/heart')['default']

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

@ -103,7 +103,7 @@ onClickOutside(inputWrapperRef, (e) => {
</NcButton> </NcButton>
</div> </div>
<template #overlay> <template #overlay>
<div ref="inputWrapperRef" class="flex flex-col min-w-120 min-h-70 py-3 pl-3 pr-1"> <div ref="inputWrapperRef" class="flex flex-col min-w-120 min-h-70 py-3 pl-3 pr-1 expanded-cell-input">
<div <div
v-if="column" v-if="column"
class="flex flex-row gap-x-1 items-center font-medium pb-2.5 mb-1 py-1 mr-3 ml-1 border-b-1 border-gray-100" class="flex flex-row gap-x-1 items-center font-medium pb-2.5 mb-1 py-1 mr-3 ml-1 border-b-1 border-gray-100"

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

@ -55,9 +55,10 @@ const navigateToSettings = () => {
class="nc-sidebar flex flex-col bg-gray-50 outline-r-1 outline-gray-100 select-none" class="nc-sidebar flex flex-col bg-gray-50 outline-r-1 outline-gray-100 select-none"
:style="{ :style="{
outlineWidth: '1px', outlineWidth: '1px',
height: isSharedBase ? '100%' : null,
}" }"
> >
<div class="flex flex-col" :style="{ height: isSharedBase ? 'auto' : 'var(--sidebar-top-height)' }"> <div class="flex flex-col">
<div style="border-bottom-width: 1px" class="flex items-center px-1 nc-sidebar-header !border-0 py-1.25 pl-2"> <div style="border-bottom-width: 1px" class="flex items-center px-1 nc-sidebar-header !border-0 py-1.25 pl-2">
<div class="flex flex-row flex-grow hover:bg-gray-100 pl-2 pr-1 py-0.5 rounded-md max-w-full"> <div class="flex flex-row flex-grow hover:bg-gray-100 pl-2 pr-1 py-0.5 rounded-md max-w-full">
<a <a
@ -66,12 +67,7 @@ const navigateToSettings = () => {
href="https://github.com/nocodb/nocodb" href="https://github.com/nocodb/nocodb"
target="_blank" target="_blank"
> >
<a-tooltip placement="bottom"> <img width="25" alt="NocoDB" src="~/assets/img/icons/512x512.png" />
<template #title>
{{ currentVersion }}
</template>
<img width="25" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</a-tooltip>
</a> </a>
<WorkspaceMenu :workspace="activeWorkspace" :is-open="true"> <WorkspaceMenu :workspace="activeWorkspace" :is-open="true">
@ -90,8 +86,7 @@ const navigateToSettings = () => {
</div> </div>
<template v-if="!isSharedBase"> <template v-if="!isSharedBase">
<div class="w-full mt-2"></div> <div class="h-auto">
<div class="h-17.5">
<div <div
v-if="isWorkspaceOwnerOrCreator" v-if="isWorkspaceOwnerOrCreator"
role="button" role="button"
@ -123,18 +118,24 @@ const navigateToSettings = () => {
</div> </div>
</WorkspaceCreateProjectBtn> </WorkspaceCreateProjectBtn>
</div> </div>
<div class="flex flex-grow"></div>
<div class="w-full mt-2"></div>
<div class="text-gray-500 mx-5 font-medium mb-1.5">{{ $t('objects.projects') }}</div> <div class="text-gray-500 mx-5 font-medium mb-1.5">{{ $t('objects.projects') }}</div>
<div
class="w-full border-b-1"
:class="{
'border-gray-200': !isTreeViewOnScrollTop,
'border-transparent': isTreeViewOnScrollTop,
}"
></div>
</template> </template>
<div
class="w-full border-b-1"
:class="{
'border-gray-200': !isTreeViewOnScrollTop,
'border-transparent': isTreeViewOnScrollTop,
}"
></div>
</div> </div>
<LazyDashboardTreeViewNew <LazyDashboardTreeViewNew
class="flex-1"
:class="{
'nc-shared-base': isSharedBase,
}"
@create-base-dlg="toggleDialog(true, 'dataSources', undefined, projectId)" @create-base-dlg="toggleDialog(true, 'dataSources', undefined, projectId)"
@on-scroll-top="onTreeViewScrollTop" @on-scroll-top="onTreeViewScrollTop"
/> />
@ -145,4 +146,8 @@ const navigateToSettings = () => {
.nc-sidebar-top-button { .nc-sidebar-top-button {
@apply flex flex-row mx-1 px-3.5 rounded-md items-center py-0.75 my-0.5 gap-x-2 hover:bg-gray-200 cursor-pointer; @apply flex flex-row mx-1 px-3.5 rounded-md items-center py-0.75 my-0.5 gap-x-2 hover:bg-gray-200 cursor-pointer;
} }
:deep(.nc-shared-base.nc-treeview-container) {
@apply !h-full;
}
</style> </style>

9
packages/nc-gui/components/dashboard/TreeViewNew/TableNode.vue

@ -113,6 +113,15 @@ const { isSharedBase } = useProject()
</template> </template>
<MdiTable <MdiTable
v-if="table.type === 'table'"
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop', false, projectRole),
'!text-black': openedTableId === table.id,
}"
/>
<MdiEye
v-else
class="flex w-5 !text-gray-500 text-sm" class="flex w-5 !text-gray-500 text-sm"
:class="{ :class="{
'group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop', false, projectRole), 'group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop', false, projectRole),

35
packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue

@ -225,12 +225,15 @@ function getConnectionConfig() {
const focusInvalidInput = () => { const focusInvalidInput = () => {
form.value?.$el.querySelector('.ant-form-item-explain-error')?.parentNode?.parentNode?.querySelector('input')?.focus() form.value?.$el.querySelector('.ant-form-item-explain-error')?.parentNode?.parentNode?.querySelector('input')?.focus()
} }
const isConnSuccess = ref(false)
const createBase = async () => { const createBase = async () => {
try { try {
await validate() await validate()
isConnSuccess.value = false
} catch (e) { } catch (e) {
focusInvalidInput() focusInvalidInput()
isConnSuccess.value = false
return return
} }
@ -287,17 +290,7 @@ const testConnection = async () => {
if (result.code === 0) { if (result.code === 0) {
testSuccess.value = true testSuccess.value = true
isConnSuccess.value = true
Modal.confirm({
title: t('msg.info.dbConnected'),
icon: null,
type: 'success',
okText: 'Ok & Add Base',
okType: 'primary',
cancelText: t('general.cancel'),
onOk: createBase,
style: 'top: 30%!important',
})
} else { } else {
testSuccess.value = false testSuccess.value = false
@ -374,6 +367,15 @@ watch(
</script> </script>
<template> <template>
<GeneralModal v-model:visible="isConnSuccess" class="!w-[25rem]">
<div class="flex flex-col h-full p-8">
<div class="text-lg font-semibold self-start mb-4">{{ t('msg.info.dbConnected') }}</div>
<div class="flex gap-x-2 mt-5 ml-7 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="isConnSuccess = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" type="primary" @click="createBase">Ok & Add Base</NcButton>
</div>
</div>
</GeneralModal>
<div class="create-base bg-white relative flex flex-col justify-center gap-2 w-full"> <div class="create-base bg-white relative flex flex-col justify-center gap-2 w-full">
<h1 class="prose-2xl font-bold self-start mb-4 flex items-center gap-2"> <h1 class="prose-2xl font-bold self-start mb-4 flex items-center gap-2">
New Base New Base
@ -656,6 +658,17 @@ watch(
> >
<a-input v-model:value="importURL" /> <a-input v-model:value="importURL" />
</a-modal> </a-modal>
<!-- connection succesfull modal -->
<GeneralModal v-model:visible="isConnSuccess" class="!w-[25rem]">
<div class="flex flex-col h-full p-8">
<div class="text-lg font-semibold self-start mb-4">{{ t('msg.info.dbConnected') }}</div>
<div class="flex gap-x-2 mt-5 ml-7 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="isConnSuccess = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" type="primary" @click="createBase">Ok & Add Base</NcButton>
</div>
</div>
</GeneralModal>
</div> </div>
</template> </template>

10
packages/nc-gui/components/dlg/ProjectDelete.vue

@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { navigateTo } from '#app'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
projectId: string projectId: string
@ -11,7 +13,7 @@ const visible = useVModel(props, 'visible', emits)
const { closeTab } = useTabs() const { closeTab } = useTabs()
const projectsStore = useProjects() const projectsStore = useProjects()
const { deleteProject } = projectsStore const { deleteProject, navigateToFirstProjectOrHome } = projectsStore
const { projects } = storeToRefs(projectsStore) const { projects } = storeToRefs(projectsStore)
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
@ -20,6 +22,8 @@ const project = computed(() => projects.value.get(props.projectId))
const isLoading = ref(false) const isLoading = ref(false)
const { projectsList } = storeToRefs(projectsStore)
const onDelete = async () => { const onDelete = async () => {
if (!project.value) return if (!project.value) return
@ -33,6 +37,10 @@ const onDelete = async () => {
refreshCommandPalette() refreshCommandPalette()
visible.value = false visible.value = false
if (toBeDeletedProject.id === projectsStore.activeProjectId) {
await navigateToFirstProjectOrHome()
}
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} finally { } finally {

2
packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue

@ -119,7 +119,7 @@ const onRoleToggle = async () => {
<div class="flex flex-col w-full p-3 border-1 border-gray-100 rounded-md"> <div class="flex flex-col w-full p-3 border-1 border-gray-100 rounded-md">
<div class="flex flex-row w-full justify-between"> <div class="flex flex-row w-full justify-between">
<div class="text-black font-medium">Enable Public Access</div> <div class="text-black font-medium">Enable Public Access</div>
<a-switch v-model:checked="isSharedBaseEnabled" :loading="isToggleBaseLoading" class="ml-2" @click="toggleSharedBase" /> <a-switch :checked="isSharedBaseEnabled" :loading="isToggleBaseLoading" class="ml-2" @click="toggleSharedBase" />
</div> </div>
<div v-if="isSharedBaseEnabled" class="flex flex-col w-full mt-3 border-t-1 pt-3 border-gray-100"> <div v-if="isSharedBaseEnabled" class="flex flex-col w-full mt-3 border-t-1 pt-3 border-gray-100">
<GeneralCopyUrl v-model:url="url" /> <GeneralCopyUrl v-model:url="url" />

5
packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue

@ -45,8 +45,10 @@ const url = computed(() => {
return sharedViewUrl() ?? '' return sharedViewUrl() ?? ''
}) })
const passwordProtectedLocal = ref(false)
const passwordProtected = computed(() => { const passwordProtected = computed(() => {
return !!(activeView.value?.password !== undefined && activeView.value?.password !== null) return !!activeView.value?.password || passwordProtectedLocal.value
}) })
const password = computed({ const password = computed({
@ -74,6 +76,7 @@ const viewTheme = computed({
}) })
const togglePasswordProtected = async () => { const togglePasswordProtected = async () => {
passwordProtectedLocal.value = !passwordProtected.value
if (!activeView.value) return if (!activeView.value) return
if (isUpdating.value.password) return if (isUpdating.value.password) return

6
packages/nc-gui/components/dlg/share-and-collaborate/View.vue

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { LoadingOutlined } from '@ant-design/icons-vue' import { LoadingOutlined } from '@ant-design/icons-vue'
import ManageUsers from './ManageUsers.vue' import ManageUsers from './ManageUsers.vue'
import {useViewsStore} from "~/store/views"; import { useViewsStore } from '~/store/views'
const { isViewToolbar } = defineProps<{ const { isViewToolbar } = defineProps<{
isViewToolbar?: boolean isViewToolbar?: boolean
@ -13,7 +13,7 @@ const { copy } = useCopy()
const { dashboardUrl } = useDashboard() const { dashboardUrl } = useDashboard()
const projectStore = useProject() const projectStore = useProject()
const { project } = storeToRefs(projectStore) const { project } = storeToRefs(projectStore)
const { navigateToProject } = projectStore const { navigateToProjectPage } = projectStore
const { activeView } = storeToRefs(useViewsStore()) const { activeView } = storeToRefs(useViewsStore())
let view let view
@ -64,7 +64,7 @@ const copyInvitationLink = async () => {
const openManageAccess = async () => { const openManageAccess = async () => {
isOpeningManageAccess.value = true isOpeningManageAccess.value = true
try { try {
await navigateToProject({ projectId: project.value.id!, page: 'collaborators' }) await navigateToProjectPage({ page: 'collaborator' })
showShareModal.value = false showShareModal.value = false
} catch (e) { } catch (e) {
console.error(e) console.error(e)

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

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
trigger: Array<'click' | 'hover' | 'contextmenu'> trigger?: Array<'click' | 'hover' | 'contextmenu'>
visible: boolean | undefined visible?: boolean | undefined
overlayClassName?: string | undefined overlayClassName?: string | undefined
}>(), }>(),
{ {

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

@ -7,6 +7,7 @@ const props = defineProps<{
// filterOptions is a function // filterOptions is a function
filterOption?: (input: string, option: any) => boolean filterOption?: (input: string, option: any) => boolean
dropdownMatchSelectWidth?: boolean dropdownMatchSelectWidth?: boolean
allowClear?: boolean
}>() }>()
const emits = defineEmits(['update:value', 'change']) const emits = defineEmits(['update:value', 'change'])
@ -38,6 +39,7 @@ const onChange = (value: string) => {
:filter-option="filterOption" :filter-option="filterOption"
:dropdown-match-select-width="dropdownMatchSelectWidth" :dropdown-match-select-width="dropdownMatchSelectWidth"
@change="onChange" @change="onChange"
:allow-clear="allowClear"
> >
<template #suffixIcon> <template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-800 nc-select-expand-btn" /> <GeneralIcon icon="arrowDown" class="text-gray-800 nc-select-expand-btn" />

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

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
const props = defineProps<{ const props = defineProps<{
modelValue: string modelValue?: string
centered?: boolean centered?: boolean
}>() }>()

12
packages/nc-gui/components/project/AccessSettings.vue

@ -6,7 +6,7 @@ import { storeToRefs, stringToColour, timeAgo, useGlobal } from '#imports'
const { user } = useGlobal() const { user } = useGlobal()
const projectsStore = useProjects() const projectsStore = useProjects()
const { getProjectUsers, createProjectUser, updateProjectUser } = projectsStore const { getProjectUsers, createProjectUser, updateProjectUser, removeProjectUser } = projectsStore
const { activeProjectId } = storeToRefs(projectsStore) const { activeProjectId } = storeToRefs(projectsStore)
const collaborators = ref<WorkspaceUserType[]>([]) const collaborators = ref<WorkspaceUserType[]>([])
@ -74,7 +74,10 @@ onMounted(async () => {
const updateCollaborator = async (collab, roles) => { const updateCollaborator = async (collab, roles) => {
try { try {
if (collab.projectRoles) { if (!roles) {
await removeProjectUser(activeProjectId.value!, collab)
collab.projectRoles = null
} else if (collab.projectRoles) {
await updateProjectUser(activeProjectId.value!, collab) await updateProjectUser(activeProjectId.value!, collab)
} else { } else {
await createProjectUser(activeProjectId.value!, collab) await createProjectUser(activeProjectId.value!, collab)
@ -184,7 +187,8 @@ const accessibleRoles = computed<(typeof ProjectRoles)[keyof typeof ProjectRoles
class="w-35 !rounded px-1" class="w-35 !rounded px-1"
:virtual="true" :virtual="true"
:placeholder="$t('labels.noAccess')" :placeholder="$t('labels.noAccess')"
:disabled="collab.id === user?.id || !accessibleRoles.includes(collab.roles)" :disabled="collab.id === user?.id"
allow-clear
@change="(value) => updateCollaborator(collab, value)" @change="(value) => updateCollaborator(collab, value)"
> >
<template #suffixIcon> <template #suffixIcon>
@ -195,7 +199,7 @@ const accessibleRoles = computed<(typeof ProjectRoles)[keyof typeof ProjectRoles
<p class="badge-text">{{ RoleLabels[userProjectRole] }}</p> <p class="badge-text">{{ RoleLabels[userProjectRole] }}</p>
</NcBadge> </NcBadge>
</a-select-option> </a-select-option>
<a-select-option v-if="!accessibleRoles.includes(collab.roles)" :value="collab.roles"> <a-select-option v-if="collab.roles && !accessibleRoles.includes(collab.roles)" :value="collab.roles">
<NcBadge :color="RoleColors[collab.roles]"> <NcBadge :color="RoleColors[collab.roles]">
<p class="badge-text">{{ RoleLabels[collab.roles] }}</p> <p class="badge-text">{{ RoleLabels[collab.roles] }}</p>
</NcBadge> </NcBadge>

17
packages/nc-gui/components/project/InviteProjectCollabSection.vue

@ -46,9 +46,9 @@ const { copy } = useCopy(true)
const { t } = useI18n() const { t } = useI18n()
const copyUrl = async () => { const copyUrl = async () => {
if (!inviteUrl) return if (!inviteUrl.value) return
try { try {
await copy(inviteUrl) await copy(inviteUrl.value)
// Copied shareable base url to clipboard! // Copied shareable base url to clipboard!
message.success(t('msg.success.shareableURLCopied')) message.success(t('msg.success.shareableURLCopied'))
@ -124,19 +124,19 @@ const copyUrl = async () => {
{{ role }} {{ role }}
</div> </div>
</div> --> </div> -->
<NcBadge v-if='role===ProjectRole.Owner' color="purple"> <NcBadge v-if="role === ProjectRole.Owner" color="purple">
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
<NcBadge v-if="role===ProjectRole.Creator" color="blue"> <NcBadge v-if="role === ProjectRole.Creator" color="blue">
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
<NcBadge v-if='role===ProjectRole.Editor' color="green"> <NcBadge v-if="role === ProjectRole.Editor" color="green">
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
<NcBadge v-if="role===ProjectRole.Commenter" color="orange"> <NcBadge v-if="role === ProjectRole.Commenter" color="orange">
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
<NcBadge v-if="role===ProjectRole.Viewer" color="yellow"> <NcBadge v-if="role === ProjectRole.Viewer" color="yellow">
<p class="badge-text">{{ role }}</p> <p class="badge-text">{{ role }}</p>
</NcBadge> </NcBadge>
</a-select-option> </a-select-option>
@ -162,8 +162,9 @@ const copyUrl = async () => {
<style scoped> <style scoped>
.badge-text { .badge-text {
@apply text-[14px] pt-1 text-center @apply text-[14px] pt-1 text-center;
} }
:deep(.ant-select .ant-select-selector) { :deep(.ant-select .ant-select-selector) {
@apply rounded; @apply rounded;
} }

39
packages/nc-gui/components/project/View.vue

@ -5,7 +5,10 @@ const { openedProject } = storeToRefs(useProjects())
const { activeTables } = storeToRefs(useTablesStore()) const { activeTables } = storeToRefs(useTablesStore())
const { activeWorkspace } = storeToRefs(useWorkspace()) const { activeWorkspace } = storeToRefs(useWorkspace())
const route = useRoute() const { navigateToProjectPage } = useProject()
const router = useRouter()
const route = router.currentRoute
/* const defaultBase = computed(() => { /* const defaultBase = computed(() => {
return openedProject.value?.bases?.[0] return openedProject.value?.bases?.[0]
@ -15,26 +18,34 @@ const { isUIAllowed } = useUIPermission()
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const activeKey = ref<'allTables' | 'collaborators' | 'data-sources'>('allTables') const activeKey = ref<'allTable' | 'collaborator' | 'data-source'>('allTable')
const baseSettingsState = ref('') const baseSettingsState = ref('')
watch( watch(
() => route.query.page, () => route.value.query?.page,
(newVal, oldVal) => { (newVal, oldVal) => {
if (newVal && newVal !== oldVal) { if (newVal && newVal !== oldVal) {
if (newVal === 'collaborators') { if (newVal === 'collaborator') {
activeKey.value = 'collaborators' activeKey.value = 'collaborator'
} else if (newVal === 'data-sources') { } else if (newVal === 'data-source') {
activeKey.value = 'data-sources' activeKey.value = 'data-source'
} else { } else {
activeKey.value = 'allTables' activeKey.value = 'allTable'
} }
} }
}, },
{ immediate: true }, { immediate: true },
) )
watch(activeKey, () => {
if (activeKey.value) {
navigateToProjectPage({
page: activeKey.value as any,
})
}
})
watch( watch(
() => openedProject.value?.title, () => openedProject.value?.title,
() => { () => {
@ -64,7 +75,7 @@ watch(
}" }"
> >
<a-tabs v-model:activeKey="activeKey" class="w-full"> <a-tabs v-model:activeKey="activeKey" class="w-full">
<a-tab-pane key="allTables"> <a-tab-pane key="allTable">
<template #tab> <template #tab>
<div class="tab-title" data-testid="proj-view-tab__all-tables"> <div class="tab-title" data-testid="proj-view-tab__all-tables">
<NcLayout /> <NcLayout />
@ -72,8 +83,8 @@ watch(
<div <div
class="flex pl-1.25 px-1.5 py-0.75 rounded-md text-xs" class="flex pl-1.25 px-1.5 py-0.75 rounded-md text-xs"
:class="{ :class="{
'bg-primary-selected': activeKey === 'allTables', 'bg-primary-selected': activeKey === 'allTable',
'bg-gray-50': activeKey !== 'allTables', 'bg-gray-50': activeKey !== 'allTable',
}" }"
> >
{{ activeTables.length }} {{ activeTables.length }}
@ -85,16 +96,16 @@ watch(
<!-- <a-tab-pane v-if="defaultBase" key="erd" tab="Project ERD" force-render class="pt-4 pb-12"> <!-- <a-tab-pane v-if="defaultBase" key="erd" tab="Project ERD" force-render class="pt-4 pb-12">
<ErdView :base-id="defaultBase!.id" class="!h-full" /> <ErdView :base-id="defaultBase!.id" class="!h-full" />
</a-tab-pane> --> </a-tab-pane> -->
<a-tab-pane v-if="isUIAllowed('shareProject')" key="collaborators"> <a-tab-pane v-if="isUIAllowed('shareProject')" key="collaborator">
<template #tab> <template #tab>
<div class="tab-title" data-testid="proj-view-tab__access-settings"> <div class="tab-title" data-testid="proj-view-tab__access-settings">
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" /> <GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
<div>Collaborators</div> <div>Collaborator</div>
</div> </div>
</template> </template>
<ProjectAccessSettings /> <ProjectAccessSettings />
</a-tab-pane> </a-tab-pane>
<a-tab-pane v-if="isUIAllowed('createBase')" key="data-sources"> <a-tab-pane v-if="isUIAllowed('createBase')" key="data-source">
<template #tab> <template #tab>
<div class="tab-title" data-testid="proj-view-tab__data-sources"> <div class="tab-title" data-testid="proj-view-tab__data-sources">
<GeneralIcon icon="database" /> <GeneralIcon icon="database" />

2
packages/nc-gui/components/smartsheet/Gallery.vue

@ -289,7 +289,7 @@ watch(
<div v-for="col in fieldsWithoutCover" :key="`record-${record.row.id}-${col.id}`"> <div v-for="col in fieldsWithoutCover" :key="`record-${record.row.id}-${col.id}`">
<div <div
v-if="!isRowEmpty(record, col) || isLTAR(col.uidt)" v-if="!isRowEmpty(record, col) || isLTAR(col.uidt, col.colOptions)"
class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full" class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full"
> >
<div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5"> <div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">

19
packages/nc-gui/components/smartsheet/Kanban.vue

@ -285,8 +285,11 @@ const kanbanListScrollHandler = useDebounceFn(async (e: any) => {
if (e.target.scrollTop + e.target.clientHeight + INFINITY_SCROLL_THRESHOLD >= e.target.scrollHeight) { if (e.target.scrollTop + e.target.clientHeight + INFINITY_SCROLL_THRESHOLD >= e.target.scrollHeight) {
const stackTitle = e.target.getAttribute('data-stack-title') const stackTitle = e.target.getAttribute('data-stack-title')
const pageSize = appInfo.value.defaultLimit || 25 const pageSize = appInfo.value.defaultLimit || 25
const page = Math.ceil(formattedData.value.get(stackTitle)!.length / pageSize) const stack = formattedData.value.get(stackTitle)
await loadMoreKanbanData(stackTitle, { offset: page * pageSize }) if (stack) {
const page = Math.ceil(stack.length / pageSize)
await loadMoreKanbanData(stackTitle, { offset: page * pageSize })
}
} }
}) })
@ -568,7 +571,7 @@ watch(
:key="`record-${record.row.id}-${col.id}`" :key="`record-${record.row.id}-${col.id}`"
class="flex flex-col rounded-lg w-full" class="flex flex-col rounded-lg w-full"
> >
<div v-if="!isRowEmpty(record, col) || isLTAR(col.uidt)"> <div v-if="!isRowEmpty(record, col) || isLTAR(col.uidt, col.colOptions)">
<!-- Smartsheet Header (Virtual) Cell --> <!-- Smartsheet Header (Virtual) Cell -->
<div class="flex flex-row w-full justify-start pt-2"> <div class="flex flex-row w-full justify-start pt-2">
<div class="w-full text-gray-400"> <div class="w-full text-gray-400">
@ -587,14 +590,14 @@ watch(
:class="{ '!ml-[-12px] pl-3': col.uidt === UITypes.SingleSelect }" :class="{ '!ml-[-12px] pl-3': col.uidt === UITypes.SingleSelect }"
> >
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="isVirtualCol(col)" v-if="col.title && isVirtualCol(col)"
v-model="record.row[col.title]" v-model="record.row[col.title]"
class="text-sm pt-1 pl-5" class="text-sm pt-1 pl-5"
:column="col" :column="col"
:row="record" :row="record"
/> />
<LazySmartsheetCell <LazySmartsheetCell
v-else v-else-if="col.title"
v-model="record.row[col.title]" v-model="record.row[col.title]"
class="text-sm pt-1 pl-7.25" class="text-sm pt-1 pl-7.25"
:column="col" :column="col"
@ -613,12 +616,12 @@ watch(
</a-layout-content> </a-layout-content>
<div class="!rounded-lg !px-3 pt-3"> <div class="!rounded-lg !px-3 pt-3">
<div v-if="formattedData.get(stack.title) && countByStack.get(stack.title) >= 0" class="text-center"> <div v-if="formattedData.get(stack.title)" class="text-center">
<!-- Stack Title --> <!-- Stack Title -->
<!-- Record Count --> <!-- Record Count -->
<div class="nc-kanban-data-count text-gray-500"> <div class="nc-kanban-data-count text-gray-500">
{{ formattedData.get(stack.title).length }} / {{ countByStack.get(stack.title) }} {{ formattedData.get(stack.title)!.length }} / {{ countByStack.get(stack.title) ?? 0 }}
{{ countByStack.get(stack.title) !== 1 ? $t('objects.records') : $t('objects.record') }} {{ countByStack.get(stack.title) !== 1 ? $t('objects.records') : $t('objects.record') }}
</div> </div>
@ -664,7 +667,7 @@ watch(
<component :is="iconMap.arrowDown" class="text-grey text-lg" /> <component :is="iconMap.arrowDown" class="text-grey text-lg" />
</div> </div>
<!-- Record Count --> <!-- Record Count -->
{{ formattedData.get(stack.title).length }} / {{ countByStack.get(stack.title) }} {{ formattedData.get(stack.title)!.length }} / {{ countByStack.get(stack.title) }}
{{ countByStack.get(stack.title) !== 1 ? $t('objects.records') : $t('objects.record') }} {{ countByStack.get(stack.title) !== 1 ? $t('objects.records') : $t('objects.record') }}
</div> </div>
</div> </div>

2
packages/nc-gui/components/smartsheet/SharedMapMarkerPopup.vue

@ -64,7 +64,7 @@ useProvideSmartsheetRowStore(meta as Ref<TableType>, currentRow)
> >
<div v-for="col in fields" :key="`record-${currentRow.row.id}-${col.id}`"> <div v-for="col in fields" :key="`record-${currentRow.row.id}-${col.id}`">
<div <div
v-if="!isRowEmpty(currentRow, col) || isLTAR(col.uidt)" v-if="!isRowEmpty(currentRow, col) || isLTAR(col.uidt, colOptions)"
class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full" class="flex flex-col space-y-1 px-4 mb-6 bg-gray-50 rounded-lg w-full"
> >
<div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5"> <div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">

9
packages/nc-gui/components/smartsheet/details/Api.vue

@ -12,17 +12,10 @@ import {
useI18n, useI18n,
useProject, useProject,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useVModel,
useViewData, useViewData,
watch, watch,
} from '#imports' } from '#imports'
const props = defineProps<{
modelValue: boolean
}>()
const emits = defineEmits(['update:modelValue'])
const { t } = useI18n() const { t } = useI18n()
const projectStore = useProject() const projectStore = useProject()
@ -40,8 +33,6 @@ const { queryParams } = useViewData(meta, view, xWhere)
const { copy } = useCopy() const { copy } = useCopy()
const vModel = useVModel(props, 'modelValue', emits)
const langs = [ const langs = [
{ {
name: 'shell', name: 'shell',

2
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -21,7 +21,7 @@ import {
useUndoRedo, useUndoRedo,
} from '#imports' } from '#imports'
const props = defineProps<{ virtual?: boolean; isOpen: boolean; close: () => void }>() const props = defineProps<{ virtual?: boolean; isOpen: boolean }>()
const emit = defineEmits(['edit', 'addColumn', 'update:isOpen']) const emit = defineEmits(['edit', 'addColumn', 'update:isOpen'])

8
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -188,9 +188,9 @@ function changeView(view: ViewType) {
router.currentRoute.value.query.page && router.currentRoute.value.query.page &&
router.currentRoute.value.query.page === 'fields' router.currentRoute.value.query.page === 'fields'
) { ) {
router.push({ params: { viewTitle: view.title || '' }, query: router.currentRoute.value.query }) router.push({ params: { viewTitle: view.id || '' }, query: router.currentRoute.value.query })
} else { } else {
router.push({ params: { viewTitle: view.title || '' } }) router.push({ params: { viewTitle: view.id || '' } })
} }
if (view.type === ViewTypes.FORM && selected.value[0] === view.id) { if (view.type === ViewTypes.FORM && selected.value[0] === view.id) {
@ -213,7 +213,7 @@ async function onRename(view: ViewType, originalTitle?: string, undo = false) {
await router.replace({ await router.replace({
params: { params: {
viewTitle: view.title, viewTitle: view.id,
}, },
}) })
@ -266,7 +266,7 @@ function openDeleteDialog(view: ViewType) {
// return to the default view // return to the default view
router.replace({ router.replace({
params: { params: {
viewTitle: views[0].title, viewTitle: views[0].id,
}, },
}) })
} }

6
packages/nc-gui/components/smartsheet/sidebar/index.vue

@ -74,7 +74,7 @@ watch(
if (view) { if (view) {
router.replace({ router.replace({
params: { params: {
viewTitle: view.title, viewTitle: view.id,
}, },
}) })
} }
@ -83,7 +83,7 @@ watch(
/** if active view is not found, set it to last opened view */ /** if active view is not found, set it to last opened view */
router.replace({ router.replace({
params: { params: {
viewTitle: lastOpenedView.title, viewTitle: lastOpenedView.id,
}, },
}) })
} else { } else {
@ -130,7 +130,7 @@ function onOpenModal({
await loadViews() await loadViews()
router.push({ params: { viewTitle: view.title || '' } }) router.push({ params: { viewTitle: view.id || '' } })
$e('a:view:create', { view: view.type }) $e('a:view:create', { view: view.type })
}, },

10
packages/nc-gui/components/webhook/Editor.vue

@ -39,6 +39,8 @@ const { appInfo } = useGlobal()
const { hooks } = storeToRefs(useWebhooksStore()) const { hooks } = storeToRefs(useWebhooksStore())
const { project } = storeToRefs(useProject())
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const titleDomRef = ref<HTMLInputElement | undefined>() const titleDomRef = ref<HTMLInputElement | undefined>()
@ -372,7 +374,13 @@ function onEventChange() {
async function loadPluginList() { async function loadPluginList() {
if (isEeUI) return if (isEeUI) return
try { try {
const plugins = (await api.plugin.webhookList()).list! const plugins = (
await api.plugin.webhookList({
query: {
project_id: project.value.id,
},
})
).list!
apps.value = plugins.reduce((o, p) => { apps.value = plugins.reduce((o, p) => {
const plugin: { title: string; tags: string[]; parsedInput: Record<string, any> } = { const plugin: { title: string; tags: string[]; parsedInput: Record<string, any> } = {

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

@ -2,7 +2,7 @@
import { NcProjectType, useRouter } from '#imports' import { NcProjectType, useRouter } from '#imports'
const props = defineProps<{ const props = defineProps<{
activeWorkspaceId: string activeWorkspaceId?: string | undefined
modal?: boolean modal?: boolean
type?: string type?: string
isOpen: boolean isOpen: boolean

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

@ -46,7 +46,7 @@ const createProject = async () => {
navigateToProject({ navigateToProject({
projectId: project.id!, projectId: project.id!,
type: props.type, type: props.type,
workspaceId: 'default', workspaceId: 'nc',
}) })
dialogShow.value = false dialogShow.value = false
} catch (e: any) { } catch (e: any) {

10
packages/nc-gui/components/workspace/Menu.vue

@ -33,14 +33,6 @@ const workspaceModalVisible = ref(false)
const isWorkspaceDropdownOpen = ref(false) const isWorkspaceDropdownOpen = ref(false)
const isAuthTokenCopied = ref(false) const isAuthTokenCopied = ref(false)
const createDlg = ref(false)
const onWorkspaceCreate = async (workspace: WorkspaceType) => {
createDlg.value = false
await loadWorkspaces()
navigateTo(`/${workspace.id}`)
}
const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color?: string) => { const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color?: string) => {
switch (mode) { switch (mode) {
case 'swatch': { case 'swatch': {
@ -346,8 +338,6 @@ onKeyStroke('Escape', () => {
</a-tabs> </a-tabs>
</div> </div>
</GeneralModal> </GeneralModal>
<WorkspaceCreateDlg v-model="createDlg" @success="onWorkspaceCreate" />
</div> </div>
</template> </template>

8
packages/nc-gui/composables/useGlobal/actions.ts

@ -1,3 +1,4 @@
import { getActivePinia } from 'pinia'
import type { Actions, AppInfo, State } from './types' import type { Actions, AppInfo, State } from './types'
import { type NcProjectType, message, useNuxtApp } from '#imports' import { type NcProjectType, message, useNuxtApp } from '#imports'
import { navigateTo } from '#app' import { navigateTo } from '#app'
@ -16,6 +17,13 @@ export function useGlobalActions(state: State): Actions {
} finally { } finally {
state.token.value = null state.token.value = null
state.user.value = null state.user.value = null
const pn = getActivePinia()
if (pn) {
pn._s.forEach((store) => {
store.$dispose()
delete pn.state.value[store.$id]
})
}
} }
} }

3
packages/nc-gui/composables/useMultiSelect/index.ts

@ -14,6 +14,7 @@ import {
extractPkFromRow, extractPkFromRow,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
isDrawerOrModalExist, isDrawerOrModalExist,
isExpandedCellInputExist,
isMac, isMac,
isTypableInputColumn, isTypableInputColumn,
message, message,
@ -704,7 +705,7 @@ export function useMultiSelect(
const clearSelectedRange = selectedRange.clear.bind(selectedRange) const clearSelectedRange = selectedRange.clear.bind(selectedRange)
const handlePaste = async (e: ClipboardEvent) => { const handlePaste = async (e: ClipboardEvent) => {
if (isDrawerOrModalExist()) { if (isDrawerOrModalExist() || isExpandedCellInputExist()) {
return return
} }

5
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -9,7 +9,6 @@ import type {
LinkToAnotherRecordType, LinkToAnotherRecordType,
StringOrNullType, StringOrNullType,
TableType, TableType,
ViewType,
} from 'nocodb-sdk' } from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk' import { ErrorMessages, RelationTypes, UITypes, isLinksOrLTAR, isVirtualCol } from 'nocodb-sdk'
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
@ -28,6 +27,7 @@ import {
useMetas, useMetas,
useProject, useProject,
useProvideSmartsheetRowStore, useProvideSmartsheetRowStore,
useViewsStore,
watch, watch,
} from '#imports' } from '#imports'
import type { SharedViewMeta } from '#imports' import type { SharedViewMeta } from '#imports'
@ -41,9 +41,10 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const passwordError = ref<string | null>(null) const passwordError = ref<string | null>(null)
const secondsRemain = ref(0) const secondsRemain = ref(0)
const { sharedView } = storeToRefs(useViewsStore())
provide(SharedViewPasswordInj, password) provide(SharedViewPasswordInj, password)
const sharedView = ref<ViewType>()
const sharedFormView = ref<FormType>() const sharedFormView = ref<FormType>()
const meta = ref<TableType>() const meta = ref<TableType>()
const columns = const columns =

16
packages/nc-gui/pages/index-old/index/index.vue

@ -64,22 +64,6 @@ watch(
}, },
) )
useDialog(resolveComponent('WorkspaceCreateDlg'), {
'modelValue': isCreateDlgOpen,
'onUpdate:modelValue': (isOpen: boolean) => (isCreateDlgOpen.value = isOpen),
'onSuccess': async () => {
isCreateDlgOpen.value = false
await loadWorkspaces()
await nextTick(() => {
;[...menuEl?.value?.$el?.querySelectorAll('li.ant-menu-item')]?.pop()?.scrollIntoView({
block: 'nearest',
inline: 'nearest',
})
selectedWorkspaceIndex.value = [workspacesList.value?.length - 1]
})
},
})
const tab = computed({ const tab = computed({
get() { get() {
return route.value.query?.tab ?? 'projects' return route.value.query?.tab ?? 'projects'

4
packages/nc-gui/pages/index/[typeOrId].vue

@ -5,5 +5,7 @@ const route = router.currentRoute
</script> </script>
<template> <template>
<NuxtPage :transition="false" :page-key="route.params.typeOrId" /> <div>
<NuxtPage :transition="false" :page-key="route.params.typeOrId" />
</div>
</template> </template>

14
packages/nc-gui/store/project.ts

@ -256,6 +256,19 @@ export const useProject = defineStore('projectStore', () => {
}, },
) )
const navigateToProjectPage = async ({ page }: { page: 'all-table' | 'collaborator' | 'data-source' }) => {
await router.push({
name: 'index-typeOrId-projectId-index-index',
params: {
typeOrId: route.value.params.typeOrId,
projectId: route.value.params.projectId,
},
query: {
page,
},
})
}
return { return {
project, project,
bases, bases,
@ -284,6 +297,7 @@ export const useProject = defineStore('projectStore', () => {
setProject, setProject,
projectUrl, projectUrl,
getBaseType, getBaseType,
navigateToProjectPage,
} }
}) })

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

@ -86,6 +86,10 @@ export const useProjects = defineStore('projectsStore', () => {
await api.auth.projectUserUpdate(projectId, user.id, user as ProjectUserReqType) await api.auth.projectUserUpdate(projectId, user.id, user as ProjectUserReqType)
} }
const removeProjectUser = async (projectId: string, user: User) => {
await api.auth.projectUserRemove(projectId, user.id)
}
const loadProjects = async (page: 'recent' | 'shared' | 'starred' | 'workspace' = 'recent') => { const loadProjects = async (page: 'recent' | 'shared' | 'starred' | 'workspace' = 'recent') => {
// if shared base then get the shared project and create a list // if shared base then get the shared project and create a list
if (route.value.params.typeOrId === 'base' && route.value.params.projectId) { if (route.value.params.typeOrId === 'base' && route.value.params.projectId) {
@ -102,7 +106,7 @@ export const useProjects = defineStore('projectsStore', () => {
projects.value.set(project.id!, { projects.value.set(project.id!, {
...(projects.value.get(project.id!) || {}), ...(projects.value.get(project.id!) || {}),
...project, ...project,
bases: [...(projects.value.get(project.id!)?.bases ?? []), ...(project.bases ?? [])], bases: [...(project.bases ?? projects.value.get(project.id!)?.bases ?? [])],
isExpanded: route.value.params.projectId === project.id || projects.value.get(project.id!)?.isExpanded, isExpanded: route.value.params.projectId === project.id || projects.value.get(project.id!)?.isExpanded,
isLoading: false, isLoading: false,
}) })
@ -121,18 +125,9 @@ export const useProjects = defineStore('projectsStore', () => {
isProjectsLoading.value = true isProjectsLoading.value = true
try { try {
const { list } = await $api.project.list( const { list } = await $api.project.list({
page baseURL: getBaseUrl(activeWorkspace?.id ?? workspace?.id),
? { })
query: {
[page]: true,
},
baseURL: getBaseUrl(activeWorkspace?.id ?? workspace?.id),
}
: {
baseURL: getBaseUrl(activeWorkspace?.id ?? workspace?.id),
},
)
_projects = list _projects = list
projects.value = _projects.reduce((acc, project) => { projects.value = _projects.reduce((acc, project) => {
@ -176,7 +171,13 @@ export const useProjects = defineStore('projectsStore', () => {
if (!force && isProjectPopulated(projectId)) return projects.value.get(projectId) if (!force && isProjectPopulated(projectId)) return projects.value.get(projectId)
const _project = await api.project.read(projectId) const _project = await api.project.read(projectId)
_project.meta = typeof _project.meta === 'string' ? JSON.parse(_project.meta) : {}
if (!_project) {
await navigateTo(`/`)
return
}
_project.meta = _project?.meta && typeof _project.meta === 'string' ? JSON.parse(_project.meta) : {}
const existingProject = projects.value.get(projectId) ?? ({} as any) const existingProject = projects.value.get(projectId) ?? ({} as any)
@ -224,7 +225,7 @@ export const useProjects = defineStore('projectsStore', () => {
linked_db_project_ids: projectPayload.linkedDbProjectIds, linked_db_project_ids: projectPayload.linkedDbProjectIds,
}, },
{ {
baseURL: getBaseUrl('default'), baseURL: getBaseUrl('nc'),
}, },
) )
@ -286,6 +287,12 @@ export const useProjects = defineStore('projectsStore', () => {
loadProject(activeProjectId.value) loadProject(activeProjectId.value)
}) })
const navigateToFirstProjectOrHome = async () => {
// if active project id is deleted, navigate to first project or home page
if (projectsList.value?.length) await navigateToProject({ projectId: projectsList.value[0].id! })
else navigateTo('/')
}
return { return {
projects, projects,
projectsList, projectsList,
@ -310,6 +317,8 @@ export const useProjects = defineStore('projectsStore', () => {
createProjectUser, createProjectUser,
updateProjectUser, updateProjectUser,
navigateToProject, navigateToProject,
removeProjectUser,
navigateToFirstProjectOrHome,
} }
}) })

4
packages/nc-gui/store/tables.ts

@ -129,7 +129,7 @@ export const useTablesStore = defineStore('tablesStore', () => {
await getMeta(table.id as string) await getMeta(table.id as string)
const typeOrId = (route.value.params.typeOrId as string) || 'nc' // const typeOrId = (route.value.params.typeOrId as string) || 'nc'
let workspaceIdOrType = workspaceId let workspaceIdOrType = workspaceId
@ -144,7 +144,7 @@ export const useTablesStore = defineStore('tablesStore', () => {
} }
await navigateTo({ await navigateTo({
path: `/${workspaceIdOrType}/${projectIdOrBaseId}/table/${table?.id}${table.title ? `/${table.title}` : ''}`, path: `/${workspaceIdOrType}/${projectIdOrBaseId}/table/${table?.id}`,
query: route.value.query, query: route.value.query,
}) })
} }

18
packages/nc-gui/store/views.ts

@ -15,8 +15,8 @@ export const useViewsStore = defineStore('viewsStore', () => {
const { activeTable } = storeToRefs(useTablesStore()) const { activeTable } = storeToRefs(useTablesStore())
const activeViewTitle = computed(() => { const activeViewTitleOrId = computed(() => {
if (!route.value.params.viewTitle?.length) return views.value.length ? views.value[0].title : undefined if (!route.value.params.viewTitle?.length) return views.value.length ? views.value[0].id : undefined
return route.value.params.viewTitle return route.value.params.viewTitle
}) })
@ -42,9 +42,12 @@ export const useViewsStore = defineStore('viewsStore', () => {
if (!activeTable.value) return undefined if (!activeTable.value) return undefined
if (!activeViewTitle.value) return undefined if (!activeViewTitleOrId.value) return undefined
return views.value.find((v) => v.title === activeViewTitle.value) return (
views.value.find((v) => v.id === activeViewTitleOrId.value) ??
views.value.find((v) => v.title === activeViewTitleOrId.value)
)
}, },
set(_view: ViewType | undefined) { set(_view: ViewType | undefined) {
if (sharedView.value) { if (sharedView.value) {
@ -55,7 +58,9 @@ export const useViewsStore = defineStore('viewsStore', () => {
if (!activeTable.value) return if (!activeTable.value) return
if (!_view) return if (!_view) return
const viewIndex = views.value.findIndex((v) => v.title === activeViewTitle.value) const viewIndex =
views.value.findIndex((v) => v.id === activeViewTitleOrId.value) ??
views.value.findIndex((v) => v.title === activeViewTitleOrId.value)
if (viewIndex === -1) return if (viewIndex === -1) return
views.value[viewIndex] = _view views.value[viewIndex] = _view
@ -86,7 +91,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
projectId: route.value.params.projectId, projectId: route.value.params.projectId,
type: route.value.params.type, type: route.value.params.type,
viewId: route.value.params.viewId, viewId: route.value.params.viewId,
viewTitle: activeViewTitle.value, viewTitle: activeViewTitleOrId.value,
slugs: [page], slugs: [page],
}, },
}) })
@ -124,6 +129,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
activeView, activeView,
openedViewsTab, openedViewsTab,
onViewsTabChange, onViewsTabChange,
sharedView,
} }
}) })

2
packages/nc-gui/store/webhooks.ts

@ -178,7 +178,7 @@ export const useWebhooksStore = defineStore('webhooksStore', () => {
projectId: route.value.params.projectId, projectId: route.value.params.projectId,
type: route.value.params.type, type: route.value.params.type,
viewId: route.value.params.viewId, viewId: route.value.params.viewId,
viewTitle: activeView.title, viewTitle: activeView.id,
slugs: openMainPage ? ['webhook'] : ['webhook', openCreatePage ? 'create' : hookId!], slugs: openMainPage ? ['webhook'] : ['webhook', openCreatePage ? 'create' : hookId!],
}, },
} }

1
packages/nc-gui/utils/browserUtils.ts

@ -2,4 +2,5 @@
export const isMac = () => /Mac/i.test(navigator.platform) export const isMac = () => /Mac/i.test(navigator.platform)
export const isDrawerExist = () => document.querySelector('.ant-drawer-open') export const isDrawerExist = () => document.querySelector('.ant-drawer-open')
export const isDrawerOrModalExist = () => document.querySelector('.ant-modal.active, .ant-drawer-open') export const isDrawerOrModalExist = () => document.querySelector('.ant-modal.active, .ant-drawer-open')
export const isExpandedCellInputExist = () => document.querySelector('.expanded-cell-input')
export const cmdKActive = () => document.querySelector('.cmdk-modal-active') export const cmdKActive = () => document.querySelector('.cmdk-modal-active')

5
packages/nc-gui/utils/virtualCell.ts

@ -1,7 +1,10 @@
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk' import { RelationTypes, UITypes, isLinksOrLTAR } from 'nocodb-sdk'
export const isLTAR = (uidt: string, colOptions: unknown): colOptions is LinkToAnotherRecordType => isLinksOrLTAR(uidt) export const isLTAR = (uidt: string | undefined, colOptions: unknown): colOptions is LinkToAnotherRecordType => {
if (!uidt) return false
return isLinksOrLTAR(uidt)
}
export const isHm = (column: ColumnType) => export const isHm = (column: ColumnType) =>
isLTAR(column.uidt!, column.colOptions) && column.colOptions.type === RelationTypes.HAS_MANY isLTAR(column.uidt!, column.colOptions) && column.colOptions.type === RelationTypes.HAS_MANY

11
packages/nocodb/src/middlewares/extract-ids/extract-ids.middleware.ts

@ -154,11 +154,14 @@ export class ExtractIdsMiddleware implements NestMiddleware, CanActivate {
}); });
req.ncProjectId = model?.project_id; req.ncProjectId = model?.project_id;
} }
// extract project id from query params only if it's userMe endpoint // extract project id from query params only if it's userMe endpoint or webhook plugin list
else if ( else if (
['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me'].some( [
(userMePath) => req.route.path === userMePath, '/auth/user/me',
) && '/api/v1/db/auth/user/me',
'/api/v1/auth/user/me',
'/api/v1/db/meta/plugins/webhook',
].some((userMePath) => req.route.path === userMePath) &&
req.query.project_id req.query.project_id
) { ) {
req.ncProjectId = req.query.project_id; req.ncProjectId = req.query.project_id;

2
packages/nocodb/src/modules/jobs/jobs/export-import/duplicate.processor.ts

@ -56,6 +56,7 @@ export class DuplicateProcessor {
modelIds: models.map((m) => m.id), modelIds: models.map((m) => m.id),
excludeViews, excludeViews,
excludeHooks, excludeHooks,
excludeData,
}); });
elapsedTime( elapsedTime(
@ -150,6 +151,7 @@ export class DuplicateProcessor {
modelIds: [modelId], modelIds: [modelId],
excludeViews, excludeViews,
excludeHooks, excludeHooks,
excludeData,
}) })
)[0]; )[0];

34
packages/nocodb/src/modules/jobs/jobs/export-import/export.service.ts

@ -23,9 +23,11 @@ export class ExportService {
modelIds: string[]; modelIds: string[];
excludeViews?: boolean; excludeViews?: boolean;
excludeHooks?: boolean; excludeHooks?: boolean;
excludeData?: boolean;
}) { }) {
const { modelIds } = param; const { modelIds } = param;
const excludeData = param?.excludeData || false;
const excludeViews = param?.excludeViews || false; const excludeViews = param?.excludeViews || false;
const excludeHooks = param?.excludeHooks || false; const excludeHooks = param?.excludeHooks || false;
@ -41,6 +43,8 @@ export class ExportService {
for (const modelId of modelIds) { for (const modelId of modelIds) {
const model = await Model.get(modelId); const model = await Model.get(modelId);
let pgSerialLastVal;
if (!model) if (!model)
return NcError.badRequest(`Model not found for id '${modelId}'`); return NcError.badRequest(`Model not found for id '${modelId}'`);
@ -67,6 +71,35 @@ export class ExportService {
for (const column of model.columns) { for (const column of model.columns) {
await column.getColOptions(); await column.getColOptions();
// if data is not excluded, get currval for ai column (pg)
if (!excludeData) {
if (base.type === 'pg') {
if (column.ai) {
try {
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
const seq = await sqlClient.knex.raw(
`SELECT pg_get_serial_sequence('??', ?) as seq;`,
[model.table_name, column.column_name],
);
if (seq.rows.length > 0) {
const seqName = seq.rows[0].seq;
const res = await sqlClient.knex.raw(
`SELECT last_value as last FROM ${seqName};`,
);
if (res.rows.length > 0) {
pgSerialLastVal = res.rows[0].last;
}
}
} catch (e) {
this.logger.error(e);
}
}
}
}
if (column.colOptions) { if (column.colOptions) {
for (const [k, v] of Object.entries(column.colOptions)) { for (const [k, v] of Object.entries(column.colOptions)) {
switch (k) { switch (k) {
@ -235,6 +268,7 @@ export class ExportService {
prefix: project.prefix, prefix: project.prefix,
title: model.title, title: model.title,
table_name: clearPrefix(model.table_name, project.prefix), table_name: clearPrefix(model.table_name, project.prefix),
pgSerialLastVal,
meta: model.meta, meta: model.meta,
columns: model.columns.map((column) => ({ columns: model.columns.map((column) => ({
id: idMap.get(column.id), id: idMap.get(column.id),

14
packages/nocodb/src/modules/jobs/jobs/export-import/import.service.ts

@ -32,6 +32,7 @@ import { HooksService } from '~/services/hooks.service';
import { ViewsService } from '~/services/views.service'; import { ViewsService } from '~/services/views.service';
import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2'; import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2';
import { BulkDataAliasService } from '~/services/bulk-data-alias.service'; import { BulkDataAliasService } from '~/services/bulk-data-alias.service';
import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
@Injectable() @Injectable()
export class ImportService { export class ImportService {
@ -152,6 +153,19 @@ export class ImportService {
(a) => a.column_name === col.column_name, (a) => a.column_name === col.column_name,
); );
idMap.set(colRef.id, col.id); idMap.set(colRef.id, col.id);
// setval for auto increment column in pg
if (base.type === 'pg') {
if (modelData.pgSerialLastVal) {
if (col.ai) {
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
await sqlClient.knex.raw(
`SELECT setval(pg_get_serial_sequence('??', ?), ?);`,
[table.table_name, col.column_name, modelData.pgSerialLastVal],
);
}
}
}
} }
tableReferences.set(modelData.id, table); tableReferences.set(modelData.id, table);

10
packages/nocodb/src/services/projects.service.ts

@ -35,11 +35,11 @@ export class ProjectsService {
user: { id: string; roles: Record<string, boolean> }; user: { id: string; roles: Record<string, boolean> };
query?: any; query?: any;
}) { }) {
const projects = const projects = extractRolesObj(param.user?.roles)[
extractRolesObj(param.user?.roles)[OrgUserRoles.SUPER_ADMIN] && OrgUserRoles.SUPER_ADMIN
!['shared', 'starred', 'recent'].some((k) => k in param.query) ]
? await Project.list(param.query) ? await Project.list(param.query)
: await ProjectUser.getProjectsList(param.user.id, param.query); : await ProjectUser.getProjectsList(param.user.id, param.query);
return projects; return projects;
} }

17
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -300,19 +300,30 @@ export class CellPageObject extends BasePage {
if (type === 'bt') { if (type === 'bt') {
const chips = cell.locator('.chips > .chip'); const chips = cell.locator('.chips > .chip');
await expect(await chips.count()).toBe(count); expect(await chips.count()).toBe(count);
for (let i = 0; i < value.length; ++i) { for (let i = 0; i < value.length; ++i) {
await chips.nth(i).locator('.name').waitFor({ state: 'visible' }); await chips.nth(i).locator('.name').waitFor({ state: 'visible' });
await chips.nth(i).locator('.name').scrollIntoViewIfNeeded(); await chips.nth(i).locator('.name').scrollIntoViewIfNeeded();
await expect(await chips.nth(i).locator('.name')).toHaveText(value[i]); await expect(chips.nth(i).locator('.name')).toHaveText(value[i]);
} }
return; return;
} }
// verify chip count & contents // verify chip count & contents
if (count) { if (count) {
await expect(await cell.innerText()).toContain(`${count} ${count === 1 ? options.singular : options.plural}`); const expectedText = `${count} ${count === 1 ? options.singular : options.plural}`;
let retryCount = 0;
while (retryCount < 5) {
const receivedText = await linkText.innerText();
if (receivedText.includes(expectedText)) {
break;
}
retryCount++;
// add delay of 100ms
await this.rootPage.waitForTimeout(100 * retryCount);
}
expect(await cell.innerText()).toContain(expectedText);
} }
if (verifyChildList) { if (verifyChildList) {

4
tests/playwright/tests/db/views/viewGridShare.spec.ts

@ -98,6 +98,8 @@ test.describe('Shared view', () => {
await dashboard.goto(); await dashboard.goto();
await page.reload(); await page.reload();
// kludge: wait for 3 seconds to avoid flaky test
await page.waitForTimeout(5000);
await dashboard.treeView.openTable({ title: 'Film' }); await dashboard.treeView.openTable({ title: 'Film' });
await dashboard.grid.toolbar.clickGroupBy(); await dashboard.grid.toolbar.clickGroupBy();
@ -106,6 +108,8 @@ test.describe('Shared view', () => {
await page.goto(sharedLink); await page.goto(sharedLink);
await page.reload(); await page.reload();
// kludge: wait for 3 seconds to avoid flaky test
await page.waitForTimeout(5000);
await sharedPage.grid.cell.verify({ index: 0, columnHeader: 'Title', value: 'ZORRO ARK' }); await sharedPage.grid.cell.verify({ index: 0, columnHeader: 'Title', value: 'ZORRO ARK' });
}); });

Loading…
Cancel
Save