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. 27
      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. 17
      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. 2
      packages/nc-gui/pages/index/[typeOrId].vue
  33. 14
      packages/nc-gui/store/project.ts
  34. 37
      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. 6
      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:
playwright:
runs-on: [self-hosted, v2]
timeout-minutes: 100
runs-on: ubuntu-20.04
timeout-minutes: 40
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
uses: actions/setup-node@v3
with:
@ -30,31 +42,11 @@ jobs:
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
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: |
${{ runner.os }}-v2-build-${{ env.cache-name }}-
${{ runner.os }}-v2-build-
${{ runner.os }}-v2
- 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
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: install dependencies nocodb-sdk
working-directory: ./packages/nocodb-sdk
run: npm install
@ -63,23 +55,16 @@ jobs:
run: npm run build
- name: Setup mysql
if: ${{ inputs.db == 'mysql' }}
working-directory: ./packages/nocodb/tests/mysql-sakila-db
run: |
# Get a list of non-system databases and construct the DROP DATABASE statement for each
service mysql start
mysql -u'root' -p'password' -e "SHOW DATABASES" --skip-column-names | grep -Ev "(information_schema|mysql|performance_schema|sys)" | while read db; do
mysql -u'root' -p'password' -e "DROP DATABASE IF EXISTS \`$db\`";
done
# 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
working-directory: ./
run: docker-compose -f ./tests/playwright/scripts/docker-compose-mysql-playwright.yml up -d &
- name: setup pg
if: ${{ inputs.db == 'pg' }}
working-directory: ./
run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d &
- name: setup pg for quick tests
if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }}
working-directory: ./packages/nocodb/tests/pg-cy-quick/
run: |
sudo -u postgres psql -U postgres -f 01-cy-quick.sql
working-directory: ./
run: docker-compose -f ./tests/playwright/scripts/docker-compose-pg-pw-quick.yml up -d &
- name: run frontend
working-directory: ./packages/nc-gui
run: npm run ci:run
@ -89,28 +74,26 @@ jobs:
working-directory: ./packages/nocodb
run: |
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
if: ${{ inputs.db == 'mysql' }}
working-directory: ./packages/nocodb
run: |
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
if: ${{ inputs.db == 'pg' }}
working-directory: ./packages/nocodb
run: |
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
uses: actions/cache@v3
id: playwright-cache
with:
path: |
**/tests/playwright/node_modules
key: cache-v2-nc-playwright-${{ hashFiles('**/tests/playwright/package-lock.json') }}
restore-keys: |
cache-v2-nc-playwright-
key: cache-nc-playwright-${{ hashFiles('**/tests/playwright/package-lock.json') }}
- name: Install dependencies
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: ./tests/playwright
@ -137,20 +120,20 @@ jobs:
working-directory: ./tests/playwright
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)
# if: ${{ inputs.db == 'sqlite' && inputs.shard == '1' }}
# working-directory: ./packages/nocodb
# run: |
# 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)
# if: ${{ inputs.db == 'sqlite' && inputs.shard == '2' }}
# working-directory: ./packages/nocodb
# run: |
# kill -9 $(lsof -t -i:8080)
# 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' }}
# working-directory: ./tests/playwright
# run: |
@ -158,11 +141,7 @@ jobs:
# printf '.'
# sleep 2
# done
# timeout-minutes: 1
# - name: Run quick tests
# if: ${{ inputs.db == 'sqlite' }}
# working-directory: ./tests/playwright
# run: PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick
# PLAYWRIGHT_HTML_REPORT=playwright-report-quick npm run test:quick
- uses: actions/upload-artifact@v3
if: ${{ inputs.db == 'sqlite' }}
with:
@ -194,9 +173,3 @@ jobs:
name: backend-logs-${{ inputs.db }}-${{ inputs.shard }}
path: ./packages/nocodb/${{ inputs.db }}_${{ inputs.shard }}_test_backend.log
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']
MdiEarth: typeof import('~icons/mdi/earth')['default']
MdiEditOutline: typeof import('~icons/mdi/edit-outline')['default']
MdiEye: typeof import('~icons/mdi/eye')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']

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

@ -103,7 +103,7 @@ onClickOutside(inputWrapperRef, (e) => {
</NcButton>
</div>
<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
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"

27
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"
:style="{
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 class="flex flex-row flex-grow hover:bg-gray-100 pl-2 pr-1 py-0.5 rounded-md max-w-full">
<a
@ -66,12 +67,7 @@ const navigateToSettings = () => {
href="https://github.com/nocodb/nocodb"
target="_blank"
>
<a-tooltip placement="bottom">
<template #title>
{{ currentVersion }}
</template>
<img width="25" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</a-tooltip>
<img width="25" alt="NocoDB" src="~/assets/img/icons/512x512.png" />
</a>
<WorkspaceMenu :workspace="activeWorkspace" :is-open="true">
@ -90,8 +86,7 @@ const navigateToSettings = () => {
</div>
<template v-if="!isSharedBase">
<div class="w-full mt-2"></div>
<div class="h-17.5">
<div class="h-auto">
<div
v-if="isWorkspaceOwnerOrCreator"
role="button"
@ -123,9 +118,10 @@ const navigateToSettings = () => {
</div>
</WorkspaceCreateProjectBtn>
</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>
</template>
<div
class="w-full border-b-1"
:class="{
@ -133,8 +129,13 @@ const navigateToSettings = () => {
'border-transparent': isTreeViewOnScrollTop,
}"
></div>
</template>
</div>
<LazyDashboardTreeViewNew
class="flex-1"
:class="{
'nc-shared-base': isSharedBase,
}"
@create-base-dlg="toggleDialog(true, 'dataSources', undefined, projectId)"
@on-scroll-top="onTreeViewScrollTop"
/>
@ -145,4 +146,8 @@ const navigateToSettings = () => {
.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;
}
:deep(.nc-shared-base.nc-treeview-container) {
@apply !h-full;
}
</style>

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

@ -113,6 +113,15 @@ const { isSharedBase } = useProject()
</template>
<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="{
'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 = () => {
form.value?.$el.querySelector('.ant-form-item-explain-error')?.parentNode?.parentNode?.querySelector('input')?.focus()
}
const isConnSuccess = ref(false)
const createBase = async () => {
try {
await validate()
isConnSuccess.value = false
} catch (e) {
focusInvalidInput()
isConnSuccess.value = false
return
}
@ -287,17 +290,7 @@ const testConnection = async () => {
if (result.code === 0) {
testSuccess.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',
})
isConnSuccess.value = true
} else {
testSuccess.value = false
@ -374,6 +367,15 @@ watch(
</script>
<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">
<h1 class="prose-2xl font-bold self-start mb-4 flex items-center gap-2">
New Base
@ -656,6 +658,17 @@ watch(
>
<a-input v-model:value="importURL" />
</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>
</template>

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

@ -1,4 +1,6 @@
<script lang="ts" setup>
import { navigateTo } from '#app'
const props = defineProps<{
visible: boolean
projectId: string
@ -11,7 +13,7 @@ const visible = useVModel(props, 'visible', emits)
const { closeTab } = useTabs()
const projectsStore = useProjects()
const { deleteProject } = projectsStore
const { deleteProject, navigateToFirstProjectOrHome } = projectsStore
const { projects } = storeToRefs(projectsStore)
const { refreshCommandPalette } = useCommandPalette()
@ -20,6 +22,8 @@ const project = computed(() => projects.value.get(props.projectId))
const isLoading = ref(false)
const { projectsList } = storeToRefs(projectsStore)
const onDelete = async () => {
if (!project.value) return
@ -33,6 +37,10 @@ const onDelete = async () => {
refreshCommandPalette()
visible.value = false
if (toBeDeletedProject.id === projectsStore.activeProjectId) {
await navigateToFirstProjectOrHome()
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} 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-row w-full justify-between">
<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 v-if="isSharedBaseEnabled" class="flex flex-col w-full mt-3 border-t-1 pt-3 border-gray-100">
<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() ?? ''
})
const passwordProtectedLocal = ref(false)
const passwordProtected = computed(() => {
return !!(activeView.value?.password !== undefined && activeView.value?.password !== null)
return !!activeView.value?.password || passwordProtectedLocal.value
})
const password = computed({
@ -74,6 +76,7 @@ const viewTheme = computed({
})
const togglePasswordProtected = async () => {
passwordProtectedLocal.value = !passwordProtected.value
if (!activeView.value) 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>
import { LoadingOutlined } from '@ant-design/icons-vue'
import ManageUsers from './ManageUsers.vue'
import {useViewsStore} from "~/store/views";
import { useViewsStore } from '~/store/views'
const { isViewToolbar } = defineProps<{
isViewToolbar?: boolean
@ -13,7 +13,7 @@ const { copy } = useCopy()
const { dashboardUrl } = useDashboard()
const projectStore = useProject()
const { project } = storeToRefs(projectStore)
const { navigateToProject } = projectStore
const { navigateToProjectPage } = projectStore
const { activeView } = storeToRefs(useViewsStore())
let view
@ -64,7 +64,7 @@ const copyInvitationLink = async () => {
const openManageAccess = async () => {
isOpeningManageAccess.value = true
try {
await navigateToProject({ projectId: project.value.id!, page: 'collaborators' })
await navigateToProjectPage({ page: 'collaborator' })
showShareModal.value = false
} catch (e) {
console.error(e)

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

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

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

@ -7,6 +7,7 @@ const props = defineProps<{
// filterOptions is a function
filterOption?: (input: string, option: any) => boolean
dropdownMatchSelectWidth?: boolean
allowClear?: boolean
}>()
const emits = defineEmits(['update:value', 'change'])
@ -38,6 +39,7 @@ const onChange = (value: string) => {
:filter-option="filterOption"
:dropdown-match-select-width="dropdownMatchSelectWidth"
@change="onChange"
:allow-clear="allowClear"
>
<template #suffixIcon>
<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>
const props = defineProps<{
modelValue: string
modelValue?: string
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 projectsStore = useProjects()
const { getProjectUsers, createProjectUser, updateProjectUser } = projectsStore
const { getProjectUsers, createProjectUser, updateProjectUser, removeProjectUser } = projectsStore
const { activeProjectId } = storeToRefs(projectsStore)
const collaborators = ref<WorkspaceUserType[]>([])
@ -74,7 +74,10 @@ onMounted(async () => {
const updateCollaborator = async (collab, roles) => {
try {
if (collab.projectRoles) {
if (!roles) {
await removeProjectUser(activeProjectId.value!, collab)
collab.projectRoles = null
} else if (collab.projectRoles) {
await updateProjectUser(activeProjectId.value!, collab)
} else {
await createProjectUser(activeProjectId.value!, collab)
@ -184,7 +187,8 @@ const accessibleRoles = computed<(typeof ProjectRoles)[keyof typeof ProjectRoles
class="w-35 !rounded px-1"
:virtual="true"
: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)"
>
<template #suffixIcon>
@ -195,7 +199,7 @@ const accessibleRoles = computed<(typeof ProjectRoles)[keyof typeof ProjectRoles
<p class="badge-text">{{ RoleLabels[userProjectRole] }}</p>
</NcBadge>
</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]">
<p class="badge-text">{{ RoleLabels[collab.roles] }}</p>
</NcBadge>

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

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

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

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

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

@ -285,9 +285,12 @@ const kanbanListScrollHandler = useDebounceFn(async (e: any) => {
if (e.target.scrollTop + e.target.clientHeight + INFINITY_SCROLL_THRESHOLD >= e.target.scrollHeight) {
const stackTitle = e.target.getAttribute('data-stack-title')
const pageSize = appInfo.value.defaultLimit || 25
const page = Math.ceil(formattedData.value.get(stackTitle)!.length / pageSize)
const stack = formattedData.value.get(stackTitle)
if (stack) {
const page = Math.ceil(stack.length / pageSize)
await loadMoreKanbanData(stackTitle, { offset: page * pageSize })
}
}
})
const kanbanListRef = (kanbanListElement: HTMLElement) => {
@ -568,7 +571,7 @@ watch(
:key="`record-${record.row.id}-${col.id}`"
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 -->
<div class="flex flex-row w-full justify-start pt-2">
<div class="w-full text-gray-400">
@ -587,14 +590,14 @@ watch(
:class="{ '!ml-[-12px] pl-3': col.uidt === UITypes.SingleSelect }"
>
<LazySmartsheetVirtualCell
v-if="isVirtualCol(col)"
v-if="col.title && isVirtualCol(col)"
v-model="record.row[col.title]"
class="text-sm pt-1 pl-5"
:column="col"
:row="record"
/>
<LazySmartsheetCell
v-else
v-else-if="col.title"
v-model="record.row[col.title]"
class="text-sm pt-1 pl-7.25"
:column="col"
@ -613,12 +616,12 @@ watch(
</a-layout-content>
<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 -->
<!-- Record Count -->
<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') }}
</div>
@ -664,7 +667,7 @@ watch(
<component :is="iconMap.arrowDown" class="text-grey text-lg" />
</div>
<!-- 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') }}
</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-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"
>
<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,
useProject,
useSmartsheetStoreOrThrow,
useVModel,
useViewData,
watch,
} from '#imports'
const props = defineProps<{
modelValue: boolean
}>()
const emits = defineEmits(['update:modelValue'])
const { t } = useI18n()
const projectStore = useProject()
@ -40,8 +33,6 @@ const { queryParams } = useViewData(meta, view, xWhere)
const { copy } = useCopy()
const vModel = useVModel(props, 'modelValue', emits)
const langs = [
{
name: 'shell',

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

@ -21,7 +21,7 @@ import {
useUndoRedo,
} 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'])

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 === 'fields'
) {
router.push({ params: { viewTitle: view.title || '' }, query: router.currentRoute.value.query })
router.push({ params: { viewTitle: view.id || '' }, query: router.currentRoute.value.query })
} else {
router.push({ params: { viewTitle: view.title || '' } })
router.push({ params: { viewTitle: 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({
params: {
viewTitle: view.title,
viewTitle: view.id,
},
})
@ -266,7 +266,7 @@ function openDeleteDialog(view: ViewType) {
// return to the default view
router.replace({
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) {
router.replace({
params: {
viewTitle: view.title,
viewTitle: view.id,
},
})
}
@ -83,7 +83,7 @@ watch(
/** if active view is not found, set it to last opened view */
router.replace({
params: {
viewTitle: lastOpenedView.title,
viewTitle: lastOpenedView.id,
},
})
} else {
@ -130,7 +130,7 @@ function onOpenModal({
await loadViews()
router.push({ params: { viewTitle: view.title || '' } })
router.push({ params: { viewTitle: view.id || '' } })
$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 { project } = storeToRefs(useProject())
const meta = inject(MetaInj, ref())
const titleDomRef = ref<HTMLInputElement | undefined>()
@ -372,7 +374,13 @@ function onEventChange() {
async function loadPluginList() {
if (isEeUI) return
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) => {
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'
const props = defineProps<{
activeWorkspaceId: string
activeWorkspaceId?: string | undefined
modal?: boolean
type?: string
isOpen: boolean

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

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

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

@ -33,14 +33,6 @@ const workspaceModalVisible = ref(false)
const isWorkspaceDropdownOpen = 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) => {
switch (mode) {
case 'swatch': {
@ -346,8 +338,6 @@ onKeyStroke('Escape', () => {
</a-tabs>
</div>
</GeneralModal>
<WorkspaceCreateDlg v-model="createDlg" @success="onWorkspaceCreate" />
</div>
</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 NcProjectType, message, useNuxtApp } from '#imports'
import { navigateTo } from '#app'
@ -16,6 +17,13 @@ export function useGlobalActions(state: State): Actions {
} finally {
state.token.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,
extractSdkResponseErrorMsg,
isDrawerOrModalExist,
isExpandedCellInputExist,
isMac,
isTypableInputColumn,
message,
@ -704,7 +705,7 @@ export function useMultiSelect(
const clearSelectedRange = selectedRange.clear.bind(selectedRange)
const handlePaste = async (e: ClipboardEvent) => {
if (isDrawerOrModalExist()) {
if (isDrawerOrModalExist() || isExpandedCellInputExist()) {
return
}

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

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

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

@ -5,5 +5,7 @@ const route = router.currentRoute
</script>
<template>
<div>
<NuxtPage :transition="false" :page-key="route.params.typeOrId" />
</div>
</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 {
project,
bases,
@ -284,6 +297,7 @@ export const useProject = defineStore('projectStore', () => {
setProject,
projectUrl,
getBaseType,
navigateToProjectPage,
}
})

37
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)
}
const removeProjectUser = async (projectId: string, user: User) => {
await api.auth.projectUserRemove(projectId, user.id)
}
const loadProjects = async (page: 'recent' | 'shared' | 'starred' | 'workspace' = 'recent') => {
// if shared base then get the shared project and create a list
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.get(project.id!) || {}),
...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,
isLoading: false,
})
@ -121,18 +125,9 @@ export const useProjects = defineStore('projectsStore', () => {
isProjectsLoading.value = true
try {
const { list } = await $api.project.list(
page
? {
query: {
[page]: true,
},
const { list } = await $api.project.list({
baseURL: getBaseUrl(activeWorkspace?.id ?? workspace?.id),
}
: {
baseURL: getBaseUrl(activeWorkspace?.id ?? workspace?.id),
},
)
})
_projects = list
projects.value = _projects.reduce((acc, project) => {
@ -176,7 +171,13 @@ export const useProjects = defineStore('projectsStore', () => {
if (!force && isProjectPopulated(projectId)) return projects.value.get(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)
@ -224,7 +225,7 @@ export const useProjects = defineStore('projectsStore', () => {
linked_db_project_ids: projectPayload.linkedDbProjectIds,
},
{
baseURL: getBaseUrl('default'),
baseURL: getBaseUrl('nc'),
},
)
@ -286,6 +287,12 @@ export const useProjects = defineStore('projectsStore', () => {
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 {
projects,
projectsList,
@ -310,6 +317,8 @@ export const useProjects = defineStore('projectsStore', () => {
createProjectUser,
updateProjectUser,
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)
const typeOrId = (route.value.params.typeOrId as string) || 'nc'
// const typeOrId = (route.value.params.typeOrId as string) || 'nc'
let workspaceIdOrType = workspaceId
@ -144,7 +144,7 @@ export const useTablesStore = defineStore('tablesStore', () => {
}
await navigateTo({
path: `/${workspaceIdOrType}/${projectIdOrBaseId}/table/${table?.id}${table.title ? `/${table.title}` : ''}`,
path: `/${workspaceIdOrType}/${projectIdOrBaseId}/table/${table?.id}`,
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 activeViewTitle = computed(() => {
if (!route.value.params.viewTitle?.length) return views.value.length ? views.value[0].title : undefined
const activeViewTitleOrId = computed(() => {
if (!route.value.params.viewTitle?.length) return views.value.length ? views.value[0].id : undefined
return route.value.params.viewTitle
})
@ -42,9 +42,12 @@ export const useViewsStore = defineStore('viewsStore', () => {
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) {
if (sharedView.value) {
@ -55,7 +58,9 @@ export const useViewsStore = defineStore('viewsStore', () => {
if (!activeTable.value) 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
views.value[viewIndex] = _view
@ -86,7 +91,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
projectId: route.value.params.projectId,
type: route.value.params.type,
viewId: route.value.params.viewId,
viewTitle: activeViewTitle.value,
viewTitle: activeViewTitleOrId.value,
slugs: [page],
},
})
@ -124,6 +129,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
activeView,
openedViewsTab,
onViewsTabChange,
sharedView,
}
})

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

@ -178,7 +178,7 @@ export const useWebhooksStore = defineStore('webhooksStore', () => {
projectId: route.value.params.projectId,
type: route.value.params.type,
viewId: route.value.params.viewId,
viewTitle: activeView.title,
viewTitle: activeView.id,
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 isDrawerExist = () => document.querySelector('.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')

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

@ -1,7 +1,10 @@
import type { ColumnType, LinkToAnotherRecordType } 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) =>
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;
}
// 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 (
['/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.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),
excludeViews,
excludeHooks,
excludeData,
});
elapsedTime(
@ -150,6 +151,7 @@ export class DuplicateProcessor {
modelIds: [modelId],
excludeViews,
excludeHooks,
excludeData,
})
)[0];

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

@ -23,9 +23,11 @@ export class ExportService {
modelIds: string[];
excludeViews?: boolean;
excludeHooks?: boolean;
excludeData?: boolean;
}) {
const { modelIds } = param;
const excludeData = param?.excludeData || false;
const excludeViews = param?.excludeViews || false;
const excludeHooks = param?.excludeHooks || false;
@ -41,6 +43,8 @@ export class ExportService {
for (const modelId of modelIds) {
const model = await Model.get(modelId);
let pgSerialLastVal;
if (!model)
return NcError.badRequest(`Model not found for id '${modelId}'`);
@ -67,6 +71,35 @@ export class ExportService {
for (const column of model.columns) {
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) {
for (const [k, v] of Object.entries(column.colOptions)) {
switch (k) {
@ -235,6 +268,7 @@ export class ExportService {
prefix: project.prefix,
title: model.title,
table_name: clearPrefix(model.table_name, project.prefix),
pgSerialLastVal,
meta: model.meta,
columns: model.columns.map((column) => ({
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 NcPluginMgrv2 from '~/helpers/NcPluginMgrv2';
import { BulkDataAliasService } from '~/services/bulk-data-alias.service';
import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
@Injectable()
export class ImportService {
@ -152,6 +153,19 @@ export class ImportService {
(a) => a.column_name === col.column_name,
);
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);

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

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

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

@ -300,19 +300,30 @@ export class CellPageObject extends BasePage {
if (type === 'bt') {
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) {
await chips.nth(i).locator('.name').waitFor({ state: 'visible' });
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;
}
// verify chip count & contents
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) {

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

@ -98,6 +98,8 @@ test.describe('Shared view', () => {
await dashboard.goto();
await page.reload();
// kludge: wait for 3 seconds to avoid flaky test
await page.waitForTimeout(5000);
await dashboard.treeView.openTable({ title: 'Film' });
await dashboard.grid.toolbar.clickGroupBy();
@ -106,6 +108,8 @@ test.describe('Shared view', () => {
await page.goto(sharedLink);
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' });
});

Loading…
Cancel
Save