Browse Source

Merge branch 'develop' into fix/gui-v2-form-view-email-me

pull/3364/head
Wing-Kam Wong 2 years ago
parent
commit
acd2423c1a
  1. 91
      .github/workflows/ci-cd-v2.yml
  2. 4
      packages/nc-gui-v2/assets/style.scss
  3. 2
      packages/nc-gui-v2/components.d.ts
  4. 12
      packages/nc-gui-v2/components/general/ColorPicker.vue
  5. 6
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  6. 12
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  7. 14
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  8. 2
      packages/nc-gui-v2/components/webhook/Editor.vue
  9. 11
      packages/nc-gui-v2/composables/useLTARStore.ts
  10. 48
      packages/nc-gui-v2/composables/useProject.ts
  11. 35
      packages/nc-gui-v2/composables/useTheme/index.ts
  12. 7
      packages/nc-gui-v2/composables/useViewData.ts
  13. 2
      packages/nc-gui-v2/nuxt.config.ts
  14. 28
      packages/nc-gui-v2/package-lock.json
  15. 6
      packages/nc-gui-v2/package.json
  16. 170
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  17. 1
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue
  18. 17
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue
  19. 70
      packages/nc-gui-v2/pages/index/index/[id].vue
  20. 8
      packages/nc-gui-v2/pages/index/index/index.vue
  21. 3
      packages/nc-gui-v2/plugins/ant.ts
  22. 20
      packages/nc-gui-v2/scripts/updateNuxtRouting.js
  23. 11740
      packages/nocodb-sdk/package-lock.json
  24. 11
      packages/nocodb-sdk/package.json
  25. 2
      packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts
  26. 14
      packages/nocodb/src/lib/meta/api/projectApis.ts
  27. 12
      packages/nocodb/src/lib/meta/helpers/extractProps.ts
  28. 2
      packages/nocodb/src/lib/models/Base.ts
  29. 2
      packages/nocodb/src/lib/models/FormViewColumn.ts
  30. 2
      packages/nocodb/src/lib/models/FormulaColumn.ts
  31. 2
      packages/nocodb/src/lib/models/GridViewColumn.ts
  32. 2
      packages/nocodb/src/lib/models/HookLog.ts
  33. 2
      packages/nocodb/src/lib/models/Project.ts
  34. 2
      packages/nocodb/src/lib/models/SyncSource.ts
  35. 2
      packages/nocodb/src/lib/models/User.ts
  36. 2
      packages/nocodb/src/lib/models/View.ts
  37. 1
      scripts/cypress-v2/integration/common/00_pre_configurations.js
  38. 14
      scripts/cypress-v2/integration/common/1c_sql_view.js
  39. 2
      scripts/cypress-v2/integration/common/3e_duration_column.js
  40. 28
      scripts/cypress-v2/integration/common/4a_table_view_grid_gallery_form.js
  41. 28
      scripts/cypress-v2/integration/common/4b_table_view_share.js
  42. 129
      scripts/cypress-v2/integration/common/4c_form_view_detailed.js
  43. 8
      scripts/cypress-v2/integration/common/4d_table_view_grid_locked.js
  44. 12
      scripts/cypress-v2/integration/common/4e_form_view_share.js
  45. 144
      scripts/cypress-v2/integration/common/4f_grid_view_share.js
  46. 252
      scripts/cypress-v2/integration/common/5a_user_role.js
  47. 12
      scripts/cypress-v2/integration/common/5b_preview_role.js
  48. 10
      scripts/cypress-v2/integration/common/7b_import_from_airtable.js
  49. 242
      scripts/cypress-v2/integration/common/9a_QuickTest.js
  50. 78
      scripts/cypress-v2/integration/spec/roleValidation.spec.js
  51. 2
      scripts/cypress-v2/integration/test/restRoles.js
  52. 5
      scripts/cypress-v2/integration/test/restViews.js
  53. 65
      scripts/cypress-v2/support/commands.js
  54. 22
      scripts/cypress-v2/support/page_objects/mainPage.js
  55. 11
      scripts/cypress-v2/support/page_objects/navigation.js

91
.github/workflows/ci-cd-v2.yml

@ -635,4 +635,93 @@ jobs:
# name: cypress-pg-restMisc-run-cache-snapshots
# path: scripts/cypress-v2/screenshots
# retention-days: 2
#
#
cy-quick-sqlite:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16.15.0
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
cp ./scripts/cypress-v2/fixtures/quickTest/noco_0_91_7.db ./packages/nocodb/noco.db
npm run start:api:cache
npm run start:web-v2
docker-compose -f ./scripts/docker-compose-cypress.yml up -d
spec: "./scripts/cypress-v2/integration/test/quickTest.js"
wait-on: "http://localhost:8080"
wait-on-timeout: 1200
config-file: scripts/cypress-v2/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: cy-quick-sqlite-snapshots
path: scripts/cypress-v2/screenshots
retention-days: 2
cy-quick-pg:
runs-on: ubuntu-20.04
steps:
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16.15.0
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Set env
run: echo "NODE_ENV=test" >> $GITHUB_ENV
- name: Cypress run
uses: cypress-io/github-action@v2
with:
start: |
docker-compose -f ./scripts/cypress-v2/docker-compose-pg-cy-quick.yml up -d
npm run start:api:cache:pg:cyquick
npm run start:web-v2
spec: "./scripts/cypress-v2/integration/test/quickTest.js"
wait-on: "http://localhost:8080"
wait-on-timeout: 1200
config-file: scripts/cypress-v2/cypress.json
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v2
with:
name: cy-quick-pg-snapshots
path: scripts/cypress-v2/screenshots
retention-days: 2

4
packages/nc-gui-v2/assets/style.scss

@ -214,10 +214,6 @@ a {
@apply !p-0 !rounded;
}
.ant-dropdown-menu-submenu-popup {
@apply scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !shadow !rounded;
}
.ant-tabs-dropdown-menu-title-content {
@apply flex items-center;
}

2
packages/nc-gui-v2/components.d.ts vendored

@ -170,6 +170,7 @@ declare module '@vue/runtime-core' {
MdiInformation: typeof import('~icons/mdi/information')['default']
MdiJson: typeof import('~icons/mdi/json')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
MdiKeyChange: typeof import('~icons/mdi/key-change')['default']
MdiKeyPlus: typeof import('~icons/mdi/key-plus')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLink: typeof import('~icons/mdi/link')['default']
@ -186,6 +187,7 @@ declare module '@vue/runtime-core' {
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiNotebookCheckOutline: typeof import('~icons/mdi/notebook-check-outline')['default']
MdiNull: typeof import('~icons/mdi/null')['default']
MdiNumeric: typeof import('~icons/mdi/numeric')['default']
MdiOpenInNew: typeof import('~icons/mdi/open-in-new')['default']
MdiPencil: typeof import('~icons/mdi/pencil')['default']

12
packages/nc-gui-v2/components/general/ColorPicker.vue

@ -23,22 +23,22 @@ const emit = defineEmits(['update:modelValue'])
const vModel = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val.hex ? val.hex : val || null)
emit('update:modelValue', val.hex8 ? val.hex8 : val || null)
},
})
const picked = ref<string | Record<string, any>>(props.modelValue || enumColor.light[0])
const selectColor = (color: string | Record<string, any>) => {
picked.value = typeof color === 'string' ? color : color.hex ? color.hex : color
vModel.value = typeof color === 'string' ? color : color.hex ? color.hex : color
picked.value = typeof color === 'string' ? color : color.hex8 ? color.hex8 : color
vModel.value = typeof color === 'string' ? color : color.hex8 ? color.hex8 : color
}
const compare = (colorA: string, colorB: string) => colorA.toLowerCase() === colorB.toLowerCase()
watch(picked, (n, _o) => {
if (!props.pickButton) {
vModel.value = typeof n === 'string' ? n : n.hex ? n.hex : n
vModel.value = typeof n === 'string' ? n : n.hex8 ? n.hex8 : n
}
})
</script>
@ -50,11 +50,11 @@ watch(picked, (n, _o) => {
v-for="(color, i) of colors.slice((colId - 1) * rowSize, colId * rowSize)"
:key="`color-${colId}-${i}`"
class="color-selector"
:class="compare(picked, color) ? 'selected' : ''"
:class="compare(typeof picked === 'string' ? picked : picked.hex8, color) ? 'selected' : ''"
:style="{ 'background-color': `${color}` }"
@click="selectColor(color)"
>
{{ compare(picked, color) ? '&#10003;' : '' }}
{{ compare(typeof picked === 'string' ? picked : picked.hex8, color) ? '&#10003;' : '' }}
</button>
</div>
<a-card v-if="props.advanced" class="w-full mt-2" :body-style="{ padding: '0px' }" :bordered="false">

6
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -79,6 +79,7 @@ const expandedFormRowState = ref<Record<string, any>>()
const visibleColLength = $computed(() => fields.value?.length)
const {
isLoading,
loadData,
paginationData,
formattedData: data,
@ -311,7 +312,10 @@ const onNavigate = (dir: NavigateDir) => {
<template>
<div class="flex flex-col h-full min-h-0 w-full">
<div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull">
<div v-if="isLoading" class="flex items-center justify-center h-full w-full">
<a-spin size="large" />
</div>
<div v-else class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull">
<a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']">
<table
ref="smartTable"

12
packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue

@ -24,7 +24,9 @@ import DlgViewDelete from '~/components/dlg/ViewDelete.vue'
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string }): void
(event: 'deleted'): void
(event: 'sorted'): void
}
@ -186,8 +188,14 @@ function openDeleteDialog(view: Record<string, any>) {
closeDialog()
emits('deleted')
// return to the default view
activeView.value = views.value[0]
if (activeView.value === view) {
// return to the default view
router.replace({
params: {
viewTitle: views.value[0].title,
},
})
}
},
})

14
packages/nc-gui-v2/components/tabs/Smartsheet.vue

@ -10,7 +10,6 @@ import {
MetaInj,
OpenNewRecordFormHookInj,
ReloadViewDataHookInj,
TabMetaInj,
computed,
inject,
provide,
@ -18,12 +17,11 @@ import {
useMetas,
useProvideSmartsheetStore,
watch,
watchEffect,
} from '#imports'
import type { TabItem } from '~/composables'
const { getMeta, metas } = useMetas()
const { metas } = useMetas()
const activeView = ref()
@ -35,13 +33,8 @@ const tabMeta = inject(
TabMetaInj,
computed(() => ({} as TabItem)),
)
const meta = computed<TableType>(() => metas.value?.[tabMeta?.value?.id as string])
watchEffect(async () => {
await getMeta(tabMeta?.value?.id as string)
})
const reloadEventHook = createEventHook<void>()
const openNewRecordFormHook = createEventHook<void>()
@ -52,7 +45,6 @@ provideSidebar({ storageKey: 'nc-right-sidebar' })
// todo: move to store
provide(MetaInj, meta)
provide(TabMetaInj, tabMeta)
provide(ActiveViewInj, activeView)
provide(IsLockedInj, isLocked)
provide(ReloadViewDataHookInj, reloadEventHook)
@ -62,10 +54,6 @@ provide(IsFormInj, isForm)
const treeViewIsLockedInj = inject('TreeViewIsLockedInj', ref(false))
watch(tabMeta, async (newTabMeta, oldTabMeta) => {
if (newTabMeta !== oldTabMeta && newTabMeta?.id) await getMeta(newTabMeta.id)
})
watch(isLocked, (nextValue) => (treeViewIsLockedInj.value = nextValue), { immediate: true })
</script>

2
packages/nc-gui-v2/components/webhook/Editor.vue

@ -475,7 +475,7 @@ onMounted(async () => {
<a-row v-if="hook.notification.type === 'URL'" class="mb-5" type="flex" :gutter="[16, 0]">
<a-col :span="6">
<a-select v-model:value="hook.notification.payload.method" size="large">
<a-select v-model:value="hook.notification.payload.method" size="large" class="nc-select-hook-url-method">
<a-select-option v-for="(method, i) in methodList" :key="i" :value="method.title">{{ method.title }}</a-select-option>
</a-select>
</a-col>

11
packages/nc-gui-v2/composables/useLTARStore.ts

@ -47,8 +47,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const colOptions = $computed(() => column?.value.colOptions as LinkToAnotherRecordType)
const { sharedView } = useSharedView() as Record<string, any>
const projectId = project?.value?.id || sharedView.value.view.project_id
const projectId = project?.value?.id || sharedView.value?.view?.project_id
// getters
const meta = computed(() => metas?.value?.[column?.value?.fk_model_id as string])
@ -131,7 +130,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
meta.value.id,
rowId.value,
colOptions.type as 'mm' | 'hm',
column?.value?.title,
encodeURIComponent(column?.value?.title),
{
limit: String(childrenExcludedListPagination.size),
offset: String(childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1)),
@ -157,7 +156,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
meta.value.id,
rowId.value,
colOptions.type as 'mm' | 'hm',
column?.value?.title,
encodeURIComponent(column?.value?.title),
{
limit: String(childrenListPagination.size),
offset: String(childrenListPagination.size * (childrenListPagination.page - 1)),
@ -213,7 +212,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
meta.value.title,
rowId.value,
colOptions.type as 'mm' | 'hm',
column?.value?.title,
encodeURIComponent(column?.value?.title),
getRelatedTableRowId(row) as string,
)
} catch (e: any) {
@ -248,7 +247,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
meta.value.title as string,
rowId.value,
colOptions.type as 'mm' | 'hm',
column?.value?.title,
encodeURIComponent(column?.value?.title),
getRelatedTableRowId(row) as string,
)
await loadChildrenList()

48
packages/nc-gui-v2/composables/useProject.ts

@ -4,6 +4,7 @@ import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp, useRoute, useState } from '#app'
import type { ProjectMetaInfo } from '~/lib'
import { USER_PROJECT_ROLES } from '~/lib'
import type { ThemeConfig } from '@/composables/useTheme'
export function useProject(projectId?: MaybeRef<string>) {
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
@ -14,9 +15,11 @@ export function useProject(projectId?: MaybeRef<string>) {
const tables = useState<TableType[]>('tables', () => [] as TableType[])
const route = useRoute()
const { includeM2M } = useGlobal()
const { setTheme } = useTheme()
const projectMetaInfo = useState<ProjectMetaInfo | undefined>('projectMetaInfo')
// todo: refactor path param name and variable name
const projectType = $computed(() => route.params.projectType as string)
const isLoaded = ref(false)
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
const isMysql = computed(() => ['mysql', 'mysql2'].includes(projectBaseType))
@ -27,6 +30,14 @@ export function useProject(projectId?: MaybeRef<string>) {
)
const isSharedBase = computed(() => projectType === 'base')
const projectMeta = computed(() => {
try {
return typeof project.value.meta === 'string' ? JSON.parse(project.value.meta) : project.value.meta
} catch (e) {
return {}
}
})
async function loadProjectMetaInfo(force?: boolean) {
if (!projectMetaInfo.value || force) {
const data = await $api.project.metaGet(project.value.id!, {}, {})
@ -71,16 +82,51 @@ export function useProject(projectId?: MaybeRef<string>) {
} else {
_projectId = route.params.projectId as string
}
isLoaded.value = true
project.value = await $api.project.read(_projectId!)
await loadProjectRoles()
await loadTables()
setTheme(projectMeta.value?.theme)
}
async function updateProject(data: Partial<ProjectType>) {
if (unref(projectId)) {
_projectId = unref(projectId)!
} else if (projectType === 'base') {
return
} else {
_projectId = route.params.projectId as string
}
await $api.project.update(_projectId, data)
}
async function saveTheme(theme: Partial<ThemeConfig>) {
await updateProject({
meta: JSON.stringify({
...projectMeta.value,
theme,
}),
})
setTheme(theme)
}
onScopeDispose(() => {
if (isLoaded.value === true) {
project.value = {}
tables.value = []
projectMetaInfo.value = undefined
projectRoles.value = {}
setTheme({})
}
})
return {
project,
tables,
loadProjectRoles,
loadProject,
updateProject,
loadTables,
isMysql,
isMssql,
@ -89,5 +135,7 @@ export function useProject(projectId?: MaybeRef<string>) {
isSharedBase,
loadProjectMetaInfo,
projectMetaInfo,
projectMeta,
saveTheme,
}
}

35
packages/nc-gui-v2/composables/useTheme/index.ts

@ -1,9 +1,9 @@
import { ConfigProvider } from 'ant-design-vue'
import type { Theme as AntTheme } from 'ant-design-vue/es/config-provider'
import { useStorage } from '@vueuse/core'
import { NOCO, hexToRGB, themeV2Colors, useCssVar, useInjectionState } from '#imports'
import tinycolor from 'tinycolor2'
import { hexToRGB, themeV2Colors, useCssVar, useInjectionState } from '#imports'
interface ThemeConfig extends AntTheme {
export interface ThemeConfig extends AntTheme {
primaryColor: string
accentColor: string
}
@ -13,29 +13,32 @@ const [setup, use] = useInjectionState((config?: Partial<ThemeConfig>) => {
const accentColor = useCssVar('--color-accent', typeof document !== 'undefined' ? document.documentElement : null)
/** current theme config */
const currentTheme = useStorage<ThemeConfig>(
`${NOCO}db-theme`,
{
primaryColor: themeV2Colors['royal-blue'].DEFAULT,
accentColor: themeV2Colors.pink['500'],
},
localStorage,
{ mergeDefaults: true },
)
const currentTheme = ref({
primaryColor: themeV2Colors['royal-blue'].DEFAULT,
accentColor: themeV2Colors.pink['500'],
})
/** set initial config */
setTheme(config ?? currentTheme.value)
/** set theme (persists in localstorage) */
function setTheme(theme: Partial<ThemeConfig>) {
const themePrimary = theme?.primaryColor ? tinycolor(theme.primaryColor) : tinycolor(themeV2Colors['royal-blue'].DEFAULT)
const themeAccent = theme?.accentColor ? tinycolor(theme.accentColor) : tinycolor(themeV2Colors.pink['500'])
// convert hex colors to rgb values
if (theme.primaryColor) primaryColor.value = hexToRGB(theme.primaryColor)
if (theme.accentColor) accentColor.value = hexToRGB(theme.accentColor)
primaryColor.value = themePrimary.isValid()
? hexToRGB(themePrimary.toHex8String())
: hexToRGB(themeV2Colors['royal-blue'].DEFAULT)
accentColor.value = themeAccent.isValid() ? hexToRGB(themeAccent.toHex8String()) : hexToRGB(themeV2Colors.pink['500'])
currentTheme.value = theme as ThemeConfig
currentTheme.value = {
primaryColor: themePrimary.toHex8String().toUpperCase(),
accentColor: themeAccent.toHex8String().toUpperCase(),
}
ConfigProvider.config({
theme,
theme: currentTheme.value,
})
}

7
packages/nc-gui-v2/composables/useViewData.ts

@ -8,6 +8,7 @@ import {
extractPkFromRow,
extractSdkResponseErrorMsg,
getHTMLEncodedText,
useApi,
useProject,
useUIPermission,
} from '#imports'
@ -38,6 +39,7 @@ export function useViewData(
throw new Error('Table meta is not available')
}
const { api, isLoading, error } = useApi()
const _paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 })
const aggCommentCount = ref<{ row_id: string; count: number }[]>([])
const galleryData = ref<GalleryType>()
@ -114,9 +116,8 @@ export function useViewData(
const loadData = async (params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) => {
if ((!project?.value?.id || !meta?.value?.id || !viewMeta?.value?.id) && !isPublic.value) return
const response = !isPublic.value
? await $api.dbViewRow.list('noco', project.value.id!, meta.value.id!, viewMeta!.value.id, {
? await api.dbViewRow.list('noco', project.value.id!, meta.value.id!, viewMeta!.value.id, {
...params,
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
@ -351,6 +352,8 @@ export function useViewData(
}
return {
error,
isLoading,
loadData,
paginationData,
queryParams,

2
packages/nc-gui-v2/nuxt.config.ts

@ -13,7 +13,7 @@ export default defineNuxtConfig({
ssr: false,
app: {
baseURL: '/dashboard/',
baseURL: process.env.NODE_ENV === 'production' ? '.' : undefined,
},
css: [
'virtual:windi.css',

28
packages/nc-gui-v2/package-lock.json generated

@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
"hasInstallScript": true,
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@vuelidate/core": "^2.0.0-alpha.44",
@ -23,6 +24,7 @@
"papaparse": "^5.3.2",
"socket.io-client": "^4.5.1",
"sortablejs": "^1.15.0",
"tinycolor2": "^1.4.2",
"unique-names-generator": "^4.7.1",
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
@ -50,6 +52,7 @@
"@types/file-saver": "^2.0.5",
"@types/papaparse": "^5.3.2",
"@types/sortablejs": "^1.13.0",
"@types/tinycolor2": "^1.4.3",
"@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37",
"@vue/test-utils": "^2.0.2",
@ -2446,6 +2449,12 @@
"integrity": "sha512-C3064MH72iEfeGCYEGCt7FCxXoAXaMPG0QPnstcxvPmbl54erpISu06d++FY37Smja64iWy5L8wOyHHBghWbJQ==",
"dev": true
},
"node_modules/@types/tinycolor2": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.3.tgz",
"integrity": "sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==",
"dev": true
},
"node_modules/@types/tough-cookie": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
@ -13677,6 +13686,14 @@
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==",
"dev": true
},
"node_modules/tinycolor2": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==",
"engines": {
"node": "*"
}
},
"node_modules/tinypool": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.2.2.tgz",
@ -17077,6 +17094,12 @@
"integrity": "sha512-C3064MH72iEfeGCYEGCt7FCxXoAXaMPG0QPnstcxvPmbl54erpISu06d++FY37Smja64iWy5L8wOyHHBghWbJQ==",
"dev": true
},
"@types/tinycolor2": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.3.tgz",
"integrity": "sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==",
"dev": true
},
"@types/tough-cookie": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
@ -25393,6 +25416,11 @@
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==",
"dev": true
},
"tinycolor2": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA=="
},
"tinypool": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.2.2.tgz",

6
packages/nc-gui-v2/package.json

@ -10,8 +10,8 @@
"test:ui": "vitest -c test/vite.config.ts --ui",
"coverage": "vitest -c test/vite.config.ts run --coverage",
"build:copy": "npm run generate; rm -rf ../nc-lib-gui-v2/lib/dist/; rsync -rvzh ./dist/ ../nc-lib-gui-v2/lib/dist/",
"build:copy:publish": "npm run generate; rm -rf ../nc-lib-gui-v2/lib/dist/; rsync -rvzh ./dist/ ../nc-lib-gui-v2/lib/dist/; npm publish ../nc-lib-gui-v2"
"build:copy:publish": "npm run generate; rm -rf ../nc-lib-gui-v2/lib/dist/; rsync -rvzh ./dist/ ../nc-lib-gui-v2/lib/dist/; npm publish ../nc-lib-gui-v2",
"postinstall": "node scripts/updateNuxtRouting.js"
},
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
@ -32,6 +32,7 @@
"papaparse": "^5.3.2",
"socket.io-client": "^4.5.1",
"sortablejs": "^1.15.0",
"tinycolor2": "^1.4.2",
"unique-names-generator": "^4.7.1",
"vue-dompurify-html": "^3.0.0",
"vue-github-button": "^3.0.3",
@ -59,6 +60,7 @@
"@types/file-saver": "^2.0.5",
"@types/papaparse": "^5.3.2",
"@types/sortablejs": "^1.13.0",
"@types/tinycolor2": "^1.4.3",
"@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37",
"@vue/test-utils": "^2.0.2",

170
packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue

@ -1,9 +1,11 @@
<script setup lang="ts">
import { Chrome } from '@ckpack/vue-color'
import { message } from 'ant-design-vue'
import { Chrome } from '@ckpack/vue-color'
import tinycolor from 'tinycolor2'
import {
computed,
definePageMeta,
enumColor,
navigateTo,
onKeyStroke,
openLink,
@ -29,7 +31,7 @@ const route = useRoute()
const { appInfo, token, signOut, signedIn, user } = useGlobal()
const { project, loadProject, loadTables, isSharedBase, loadProjectMetaInfo, projectMetaInfo } = useProject()
const { project, loadProject, loadTables, isSharedBase, loadProjectMetaInfo, projectMetaInfo, saveTheme } = useProject()
const { addTab, clearTabs } = useTabs()
@ -53,22 +55,9 @@ const dropdownOpen = ref(false)
/** Sidebar ref */
const sidebar = ref()
const pickedColor = ref<any>('#ffffff')
let pickerActive = $ref<boolean | 'primary' | 'accent'>(false)
const email = computed(() => user.value?.email ?? '---')
const { setTheme, theme } = useTheme()
watch(pickedColor, (nextColor) => {
if (pickerActive && nextColor.hex) {
setTheme({
primaryColor: pickerActive === 'primary' ? nextColor.hex : theme.value.primaryColor,
accentColor: pickerActive === 'accent' ? nextColor.hex : theme.value.accentColor,
})
}
})
const { theme } = useTheme()
const logout = () => {
signOut()
@ -94,6 +83,31 @@ await loadProject()
await loadTables()
const themePrimaryColor = ref<any>(theme.value.primaryColor)
const themeAccentColor = ref<any>(theme.value.accentColor)
// Chrome provides object so if custom picker used we only edit primary otherwise use analogous as accent
watch(themePrimaryColor, (nextColor) => {
const hexColor = nextColor.hex ? nextColor.hex : nextColor
const tcolor = tinycolor(hexColor)
if (tcolor) {
const analogous = tcolor.complement()
saveTheme({
primaryColor: hexColor,
accentColor: nextColor.hex ? theme.value.accentColor : analogous.toHexString(),
})
}
})
watch(themeAccentColor, (nextColor) => {
const hexColor = nextColor.hex ? nextColor.hex : nextColor
saveTheme({
primaryColor: theme.value.primaryColor,
accentColor: hexColor,
})
})
if (!route.params.type && isUIAllowed('teamAndAuth')) {
addTab({ type: TabType.AUTH, title: 'Team & Auth' })
}
@ -125,22 +139,6 @@ const copyAuthToken = async () => {
message.error(e.message)
}
}
const openColorPicker = (type: 'primary' | 'accent') => {
if (!pickerActive || pickerActive !== type) {
pickedColor.value = type === 'primary' ? theme.value.primaryColor : theme.value.accentColor
pickerActive = type
} else {
pickerActive = false
}
}
const onMenuClose = (visible: boolean) => {
if (!visible) {
pickedColor.value = '#ffffff'
pickerActive = false
}
}
</script>
<template>
@ -188,7 +186,7 @@ const onMenuClose = (visible: boolean) => {
</template>
</div>
<a-dropdown v-else class="h-full min-w-0 flex-1" :trigger="['click']" placement="bottom" @visible-change="onMenuClose">
<a-dropdown v-else class="h-full min-w-0 flex-1" :trigger="['click']" placement="bottom">
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"
@ -272,6 +270,63 @@ const onMenuClose = (visible: boolean) => {
</div>
</a-menu-item>
<template v-if="isUIAllowed('projectTheme')">
<a-sub-menu key="theme">
<template #title>
<div class="nc-project-menu-item group">
<ClarityImageLine class="group-hover:text-accent" />
Project Theme
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<GeneralColorPicker v-model="themePrimaryColor" :colors="enumColor.dark" :row-size="5" :advanced="false" />
<a-sub-menu key="theme-2">
<template #title>
<div class="nc-project-menu-item group">
Custom Theme
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-sub-menu key="pick-primary">
<template #title>
<div class="nc-project-menu-item group">
<ClarityColorPickerSolid class="group-hover:text-accent" />
Primary Color
</div>
</template>
<template #expandIcon></template>
<Chrome v-model="themePrimaryColor" />
</a-sub-menu>
<a-sub-menu key="pick-accent">
<template #title>
<div class="nc-project-menu-item group">
<ClarityColorPickerSolid class="group-hover:text-accent" />
Accent Color
</div>
</template>
<template #expandIcon></template>
<Chrome v-model="themeAccentColor" />
</a-sub-menu>
</a-sub-menu>
</a-sub-menu>
</template>
<a-menu-divider />
<a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as">
@ -293,7 +348,11 @@ const onMenuClose = (visible: boolean) => {
<GeneralPreviewAs />
</a-sub-menu>
<a-sub-menu key="language" class="lang-menu scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0">
<a-sub-menu
key="language"
class="lang-menu !py-0"
popup-class-name="scrollbar-thin-dull min-w-50 max-h-90vh !overflow-auto"
>
<template #title>
<div class="nc-project-menu-item group">
<MaterialSymbolsTranslate class="group-hover:text-accent nc-language" />
@ -338,54 +397,13 @@ const onMenuClose = (visible: boolean) => {
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
<MdiLogout class="group-hover:(!text-accent)" />&nbsp;
<span class="prose-sm">
<span class="prose-sm nc-user-menu-signout">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-sub-menu>
</template>
<a-menu-divider />
<a-sub-menu>
<template #title>
<div class="nc-project-menu-item group">
<ClarityImageLine class="group-hover:text-accent" />
Theme
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-menu-item>
<div class="nc-project-menu-item group" @click.stop="openColorPicker('primary')">
<ClarityColorPickerSolid class="group-hover:text-accent" />
Primary Color
</div>
</a-menu-item>
<a-menu-item>
<div class="nc-project-menu-item group" @click.stop="openColorPicker('accent')">
<ClarityColorPickerSolid class="group-hover:text-accent" />
Accent Color
</div>
</a-menu-item>
</a-sub-menu>
<Chrome
v-if="pickerActive"
v-model="pickedColor"
class="z-99 absolute right-[-225px]"
@click.stop
@blur="onMenuClose(false)"
/>
</a-menu-item-group>
</a-menu>
</template>

1
packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.vue

@ -92,6 +92,7 @@ function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
.ant-tabs-nav-add {
@apply !hidden;
}
.ant-tabs-nav-more {
@apply text-white;
}

17
packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue

@ -1,11 +1,18 @@
<script>
export default {
name: 'Index',
}
<script setup lang="ts">
const { getMeta } = useMetas()
const route = useRoute()
const loading = ref(true)
getMeta(route.params.title as string, true).finally(() => {
loading.value = false
})
</script>
<template>
<TabsSmartsheet />
<div v-if="loading" class="flex items-center justify-center h-full w-full">
<a-spin size="large" />
</div>
<TabsSmartsheet v-else />
</template>
<style scoped></style>

70
packages/nc-gui-v2/pages/index/index/[id].vue

@ -1,7 +1,8 @@
<script lang="ts" setup>
import type { Form } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import tinycolor from 'tinycolor2'
import {
extractSdkResponseErrorMsg,
navigateTo,
@ -15,12 +16,16 @@ import {
useSidebar,
} from '#imports'
const { api, isLoading } = useApi()
const { isLoading } = useApi()
useSidebar({ hasSidebar: false })
const route = useRoute()
const { project, loadProject, updateProject } = useProject(route.params.id as string)
await loadProject()
const nameValidationRules = [
{
required: true,
@ -31,22 +36,15 @@ const nameValidationRules = [
const form = ref<typeof Form>()
const formState = reactive({
const formState = reactive<Partial<ProjectType>>({
title: '',
color: '#FFFFFF00',
})
const getProject = async () => {
try {
const result: ProjectType = await api.project.read(route.params.id as string)
formState.title = result.title as string
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const renameProject = async () => {
formState.color = formState.color === '#FFFFFF00' ? '' : formState.color
try {
await api.project.update(route.params.id as string, formState)
await updateProject(formState)
navigateTo(`/nc/${route.params.id}`)
} catch (e: any) {
@ -56,6 +54,8 @@ const renameProject = async () => {
// select and focus title field on load
onMounted(async () => {
formState.title = project.value.title as string
formState.color = project.value.color && tinycolor(project.value.color).isValid() ? project.value.color : '#FFFFFF00'
await nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
@ -67,8 +67,6 @@ onMounted(async () => {
}, 500)
})
})
await getProject()
</script>
<template>
@ -100,6 +98,27 @@ await getProject()
<a-input v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
</a-form-item>
<div class="flex items-center">
<span>Project color: </span>
<a-menu class="!border-0 !m-0 !p-0">
<a-sub-menu key="project-color">
<template #title>
<button type="button" class="color-selector" :style="{ 'background-color': formState.color }">
<MdiNull v-if="formState.color === '#FFFFFF00'" />
</button>
</template>
<template #expandIcon></template>
<GeneralColorPicker v-model="formState.color" name="color" class="nc-metadb-project-color" />
</a-sub-menu>
</a-menu>
<MdiClose
v-show="formState.color !== '#FFFFFF00'"
class="cursor-pointer"
:style="{ color: 'red' }"
@click="formState.color = '#FFFFFF00'"
/>
</div>
<div class="text-center">
<button type="submit" class="submit">
<span class="flex items-center gap-2">
@ -112,7 +131,7 @@ await getProject()
</div>
</template>
<style lang="scss">
<style lang="scss" scoped>
.update-project {
.ant-input-affix-wrapper,
.ant-input {
@ -137,4 +156,23 @@ await getProject()
}
}
}
:deep(.ant-menu-submenu-title) {
@apply !p-0 !mx-2;
}
.color-selector {
position: relative;
height: 32px;
width: 32px;
border-radius: 5px;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: white;
@apply flex text-gray-500 border-4 items-center justify-center;
}
.color-selector:hover {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
</style>

8
packages/nc-gui-v2/pages/index/index/index.vue

@ -158,9 +158,13 @@ await loadProjects()
>
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text }">
<template #default="{ text, record }">
<div
class="capitalize color-transition group-hover:text-primary !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap"
class="capitalize color-transition group-hover:text-primary !w-[400px] h-full overflow-hidden overflow-ellipsis whitespace-nowrap pl-2"
:class="{ 'border-l-4': record.color }"
:style="{
'border-color': record.color,
}"
>
{{ text }}
</div>

3
packages/nc-gui-v2/plugins/ant.ts

@ -1,6 +1,7 @@
import { Menu as AntMenu } from 'ant-design-vue'
import { Menu as AntMenu, Modal as AntModal } from 'ant-design-vue'
import { defineNuxtPlugin } from '#imports'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component(AntMenu.name, AntMenu)
nuxtApp.vueApp.component(AntModal.name, AntModal)
})

20
packages/nc-gui-v2/scripts/updateNuxtRouting.js

@ -0,0 +1,20 @@
/** A temporary solution to enable hash based routing until
* nuxt-team merges - https://github.com/nuxt/framework/pull/6980
*/
const fs = require('fs')
const path = require('path')
const filePath = path.join(__dirname, '..', 'node_modules', 'nuxt', 'dist', 'pages', 'runtime', 'router.mjs')
/** Read file content to be updated */
const content = fs.readFileSync(filePath, 'utf8')
/** Replace `createWebHistory` with `createWebHashHistory` */
const updatedContent = content.replace(
/createRouter(\s*,\s*)createWebHistory(\s*,\s*)createMemoryHistory/,
`createRouter$1createWebHashHistory as createWebHistory$2createMemoryHistory`,
)
/** Update file content with updated code */
fs.writeFileSync(filePath, updatedContent, 'utf8')

11740
packages/nocodb-sdk/package-lock.json generated

File diff suppressed because it is too large Load Diff

11
packages/nocodb-sdk/package.json

@ -9,6 +9,7 @@
"license": "MIT",
"keywords": [],
"scripts": {
"preinstall": "npx npm-force-resolutions",
"build": "npm run generate:sdk && run-p build:*",
"build:main": "tsc -p tsconfig.json",
"build:module": "tsc -p tsconfig.module.json",
@ -37,8 +38,8 @@
"version": "standard-version",
"reset-hard": "git clean -dfx && git reset --hard && npm i",
"prepare-release": "run-s reset-hard test cov:check doc:html version doc:publish",
"generate:sdk": "npx swagger-typescript-api -r -p ../../scripts/sdk/swagger.json -o ./src/lib/ --axios --unwrap-response-data --module-name-first-tag --type-suffix=Type --templates ../../scripts/sdk/templates",
"generate:sdk:default": "npx swagger-typescript-api -r -p ../../scripts/sdk/swagger.json -o ./src/lib/ --name Api2.ts --unwrap-response-data --module-name-first-tag --type-suffix=Type --templates ../../scripts/sdk/templates"
"generate:sdk": "swagger-typescript-api -r -p ../../scripts/sdk/swagger.json -o ./src/lib/ --axios --unwrap-response-data --module-name-first-tag --type-suffix=Type --templates ../../scripts/sdk/templates",
"generate:sdk:default": "swagger-typescript-api -r -p ../../scripts/sdk/swagger.json -o ./src/lib/ --name Api2.ts --unwrap-response-data --module-name-first-tag --type-suffix=Type --templates ../../scripts/sdk/templates"
},
"engines": {
"node": ">=10"
@ -68,10 +69,14 @@
"open-cli": "^6.0.1",
"prettier": "^2.1.1",
"standard-version": "^9.0.0",
"swagger-typescript-api": "^10.0.1",
"ts-node": "^9.0.0",
"typedoc": "^0.22.17",
"typescript": "^4.0.2"
},
"resolutions": {
"typescript": "4.7.4"
},
"files": [
"build/main",
"build/module",
@ -107,4 +112,4 @@
"**/*.spec.js"
]
}
}
}

2
packages/nocodb/src/lib/meta/api/modelVisibilityApis.ts

@ -115,7 +115,7 @@ router.get(
await xcVisibilityMetaGet(
req.params.projectId,
null,
req.query.includeM2M
req.query.includeM2M === true || req.query.includeM2M === 'true'
)
);
}, 'modelVisibilityList')

14
packages/nocodb/src/lib/meta/api/projectApis.ts

@ -23,6 +23,7 @@ import getColumnUiType from '../helpers/getColumnUiType';
import mapDefaultPrimaryValue from '../helpers/mapDefaultPrimaryValue';
import { extractAndGenerateManyToManyRelations } from './metaDiffApis';
import { metaApiMetrics } from '../helpers/apiMetrics';
import { extractPropsAndSanitize } from '../helpers/extractProps';
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz_', 4);
@ -46,12 +47,15 @@ export async function projectUpdate(
req: Request<any, any, any>,
res: Response<ProjectListType>
) {
// only support updating title at this moment
const data: any = {
title: DOMPurify.sanitize(req?.body?.title),
};
const project = await Project.getWithInfo(req.params.projectId);
const data: Partial<Project> = extractPropsAndSanitize(req?.body, [
'title',
'meta',
'color',
]);
if (await Project.getByTitle(data.title)) {
if (data?.title && project.title !== data.title && await Project.getByTitle(data.title)) {
NcError.badRequest('Project title already in use');
}

12
packages/nocodb/src/lib/meta/helpers/extractProps.ts

@ -1,7 +1,17 @@
export default function extractProps<T>(body: T, props: string[]): Partial<T> {
import DOMPurify from 'isomorphic-dompurify';
export function extractProps<T>(body: T, props: string[]): Partial<T> {
// todo: throw error if no props found
return props.reduce((o, key) => {
if (key in body) o[key] = body[key];
return o;
}, {});
}
export function extractPropsAndSanitize<T>(body: T, props: string[]): Partial<T> {
// todo: throw error if no props found
return props.reduce((o, key) => {
if (key in body) o[key] = body[key] === '' ? null : DOMPurify.sanitize(body[key]);
return o;
}, {});
}

2
packages/nocodb/src/lib/models/Base.ts

@ -10,7 +10,7 @@ import Model from './Model';
import { BaseType } from 'nocodb-sdk';
import NocoCache from '../cache/NocoCache';
import CryptoJS from 'crypto-js';
import extractProps from '../meta/helpers/extractProps';
import { extractProps } from '../meta/helpers/extractProps';
// todo: hide credentials
export default class Base implements BaseType {

2
packages/nocodb/src/lib/models/FormViewColumn.ts

@ -3,7 +3,7 @@ import { CacheGetType, CacheScope, MetaTable } from '../utils/globals';
import { FormColumnType } from 'nocodb-sdk';
import View from './View';
import NocoCache from '../cache/NocoCache';
import extractProps from '../meta/helpers/extractProps';
import { extractProps } from '../meta/helpers/extractProps';
export default class FormViewColumn implements FormColumnType {
id?: string;

2
packages/nocodb/src/lib/models/FormulaColumn.ts

@ -1,7 +1,7 @@
import Noco from '../Noco';
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals';
import NocoCache from '../cache/NocoCache';
import extractProps from '../meta/helpers/extractProps';
import { extractProps } from '../meta/helpers/extractProps';
export default class FormulaColumn {
formula: string;

2
packages/nocodb/src/lib/models/GridViewColumn.ts

@ -1,7 +1,7 @@
import Noco from '../Noco';
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals';
import { GridColumnType } from 'nocodb-sdk';
import extractProps from '../meta/helpers/extractProps';
import { extractProps } from '../meta/helpers/extractProps';
import View from './View';
import NocoCache from '../cache/NocoCache';

2
packages/nocodb/src/lib/models/HookLog.ts

@ -1,6 +1,6 @@
import { MetaTable } from '../utils/globals';
import Noco from '../Noco';
import extractProps from '../meta/helpers/extractProps';
import { extractProps } from '../meta/helpers/extractProps';
import Hook from './Hook';
import { HookLogType } from 'nocodb-sdk';

2
packages/nocodb/src/lib/models/Project.ts

@ -7,7 +7,7 @@ import {
CacheScope,
MetaTable,
} from '../utils/globals';
import extractProps from '../meta/helpers/extractProps';
import { extractProps } from '../meta/helpers/extractProps';
import NocoCache from '../cache/NocoCache';
export default class Project implements ProjectType {

2
packages/nocodb/src/lib/models/SyncSource.ts

@ -1,6 +1,6 @@
import Noco from '../Noco';
import { MetaTable } from '../utils/globals';
import extractProps from '../meta/helpers/extractProps';
import { extractProps } from '../meta/helpers/extractProps';
import User from './User';
export default class SyncSource {

2
packages/nocodb/src/lib/models/User.ts

@ -1,7 +1,7 @@
import { UserType } from 'nocodb-sdk';
import { CacheGetType, CacheScope, MetaTable } from '../utils/globals';
import Noco from '../Noco';
import extractProps from '../meta/helpers/extractProps';
import { extractProps } from '../meta/helpers/extractProps';
import NocoCache from '../cache/NocoCache';
export default class User implements UserType {
id: string;

2
packages/nocodb/src/lib/models/View.ts

@ -18,7 +18,7 @@ import GalleryViewColumn from './GalleryViewColumn';
import FormViewColumn from './FormViewColumn';
import Column from './Column';
import NocoCache from '../cache/NocoCache';
import extractProps from '../meta/helpers/extractProps';
import { extractProps } from '../meta/helpers/extractProps';
const { v4: uuidv4 } = require('uuid');
export default class View implements ViewType {

1
scripts/cypress-v2/integration/common/00_pre_configurations.js

@ -164,6 +164,7 @@ export const genTest = (apiType, dbType) => {
it("Admin SignUp", () => {
cy.task("log", "This will be output to the terminal");
cy.saveLocalStorage();
loginPage.signUp(roles.owner.credentials);
});

14
scripts/cypress-v2/integration/common/1c_sql_view.js

@ -138,21 +138,21 @@ export const genTest = (apiType, dbType) => {
}
});
it(`SQL View List`, () => {
it.skip(`SQL View List`, () => {
// confirm if other views exist
//
cy.openViewsTab("CustomerList", 25);
cy.closeViewsTab("CustomerList");
// cy.openViewsTab("FilmList", 25);
// cy.closeViewsTab("FilmList");
cy.openViewsTab("FilmList", 25);
cy.closeViewsTab("FilmList");
// cy.openViewsTab("SalesByFilmCategory", 16);
// cy.closeViewsTab("SalesByFilmCategory");
cy.openViewsTab("SalesByFilmCategory", 16);
cy.closeViewsTab("SalesByFilmCategory");
if (!isXcdb()) {
// cy.openViewsTab("NicerButSlowerFilmList", 25);
// cy.closeViewsTab("NicerButSlowerFilmList");
cy.openViewsTab("NicerButSlowerFilmList", 25);
cy.closeViewsTab("NicerButSlowerFilmList");
// SalesByStore && StaffList contain no entries. Hence marking row count to 0
cy.openViewsTab("SalesByStore", 0);

2
scripts/cypress-v2/integration/common/3e_duration_column.js

@ -18,7 +18,6 @@ export const genTest = (apiType, dbType) => {
// Run once before test- create table
//
before(() => {
cy.fileHook();
mainPage.tabReset();
// // kludge: wait for page load to finish
@ -31,7 +30,6 @@ export const genTest = (apiType, dbType) => {
});
beforeEach(() => {
cy.fileHook();
});
after(() => {

28
scripts/cypress-v2/integration/common/4a_table_view_grid_gallery_form.js

@ -1,8 +1,6 @@
import { mainPage } from "../../support/page_objects/mainPage";
import { isTestSuiteActive } from "../../support/page_objects/projectConstants";
// let viewTypeString = ["", "Form", "Gallery", "Grid"];
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
@ -16,24 +14,17 @@ export const genTest = (apiType, dbType) => {
// Run once before test- create project (rest/graphql)
//
before(() => {
cy.fileHook();
mainPage.tabReset();
// // kludge: wait for page load to finish
// cy.wait(1000);
// // close team & auth tab
// cy.get('button.ant-tabs-tab-remove').should('exist').click();
// cy.wait(1000);
// open a table to work on views
//
cy.openTableTab("Country", 25);
// toggle right navbar (open)
cy.get('.nc-toggle-right-navbar').should('exist').click();
});
beforeEach(() => {
cy.fileHook();
});
after(() => {
@ -49,7 +40,6 @@ export const genTest = (apiType, dbType) => {
cy.get(`.nc-create-${viewType}-view`).click();
// Pop up window, click Submit (accepting default name for view)
// cy.getActiveModal().find("button:contains(Submit)").click();
cy.getActiveModal().find(".ant-btn-primary").click();
cy.toastWait("View created successfully");
@ -64,10 +54,6 @@ export const genTest = (apiType, dbType) => {
it(`Edit ${viewType} view name`, () => {
// click on edit-icon (becomes visible on hovering mouse)
// cy.get(".nc-view-edit-icon").last().click({
// force: true,
// timeout: 1000,
// });
cy.get(`.nc-${viewType}-view-item`).last().dblclick();
// feed new name
@ -89,15 +75,11 @@ export const genTest = (apiType, dbType) => {
// number of view entries should be 2 before we delete
cy.get(".nc-view-item").its("length").should("eq", 2);
cy.get(`.nc-${viewType}-view-item`).last().click();
cy.wait(3000);
// click on delete icon (becomes visible on hovering mouse)
cy.get(`.nc-${viewType}-view-item`).last().trigger("mouseover").then(() => {
cy.get(".nc-view-delete-icon").should('exist').click({force: true});
cy.getActiveModal().find(".ant-btn-dangerous").click();
cy.toastWait("View deleted successfully");
})
cy.get(".nc-view-delete-icon").click({ force: true });
cy.wait(300)
cy.getActiveModal().find('.ant-btn-dangerous').click();
cy.toastWait("View deleted successfully");
// kludge: right navbar closes abruptly. force it open again
window.localStorage.setItem('nc-right-sidebar', '{"isOpen":true,"hasSidebar":true}')

28
scripts/cypress-v2/integration/common/4b_table_view_share.js

@ -1,5 +1,6 @@
import { mainPage } from "../../support/page_objects/mainPage";
import { isTestSuiteActive } from "../../support/page_objects/projectConstants";
import {loginPage} from "../../support/page_objects/navigation";
let storedURL = "";
let linkText = "";
@ -35,32 +36,20 @@ export const genTest = (apiType, dbType) => {
// Run once before test- create project (rest/graphql)
//
before(() => {
cy.fileHook();
mainPage.tabReset();
// // kludge: wait for page load to finish
// cy.wait(1000);
// // close team & auth tab
// cy.get('button.ant-tabs-tab-remove').should('exist').click();
// cy.wait(1000);
cy.openTableTab("City", 25);
// store base URL- to re-visit and delete form view later
cy.url().then((url) => {
storedURL = url;
});
generateLinkWithPwd();
});
beforeEach(() => {
cy.fileHook();
cy.restoreLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
});
it("Share view with incorrect password", () => {
@ -89,17 +78,22 @@ export const genTest = (apiType, dbType) => {
cy.getActiveModal().find('button:contains("Unlock")').click();
// if pwd is incorrect, active modal requesting to feed in password again will persist
cy.getActiveModal().find('button:contains("Unlock")').should('not.exist');
// cy.getActiveModal().find('button:contains("Unlock")').should('not.exist');
cy.get(".ant-modal-content:visible").should("not.exist")
// Verify Download as CSV is here
mainPage.downloadCsv().should("exist");
mainPage.downloadExcel().should("exist");
});
it("Delete view", () => {
cy.visit(storedURL, {
baseUrl: null,
});
it("Delete view", () => {
// issue with restore local storage- need to refresh page to get new URL
loginPage.loginAndOpenProject(apiType, dbType);
// cy.restoreLocalStorage();
// cy.visit(storedURL, { baseUrl: null });
cy.openTableTab("City", 25);
// wait for page load to complete
cy.get(".nc-grid-row").should("have.length", 25);

129
scripts/cypress-v2/integration/common/4c_form_view_detailed.js

@ -1,5 +1,6 @@
import { isTestSuiteActive } from "../../support/page_objects/projectConstants";
import { mainPage, settingsPage } from "../../support/page_objects/mainPage";
import {loginPage} from "../../support/page_objects/navigation";
let formViewURL;
@ -11,14 +12,24 @@ function verifyFormDrawerFieldLocation(fieldName, position) {
}
function verifyFormDrawerHideObjectCount(count) {
cy.get(".nc-form")
.find(".nc-field-remove-icon")
.its("length")
.should("eq", count);
if(count) {
cy.get(".nc-form")
.find(".nc-field-remove-icon")
.its("length")
.should("eq", count);
} else {
cy.get(".nc-form")
.find(".nc-field-remove-icon")
.should("not.exist");
}
}
function verifyFormMenuDrawerCardCount(cardCount) {
cy.get('.nc-form-left-drawer').find('.ant-card').should('have.length', cardCount);
if(cardCount) {
cy.get('.nc-form-left-drawer').find('.ant-card').should('have.length', cardCount);
} else {
cy.get('.nc-form-left-drawer').find('.ant-card').should('not.exist');
}
}
function validateFormHeader() {
@ -47,14 +58,14 @@ export const genTest = (apiType, dbType) => {
// Run once before test- create project (rest/graphql)
//
before(() => {
cy.fileHook();
mainPage.tabReset();
// loginPage.loginAndOpenProject(apiType, dbType);
// // kludge: wait for page load to finish
// cy.wait(1000);
// // close team & auth tab
// cy.get('button.ant-tabs-tab-remove').should('exist').click();
// cy.wait(1000);
// kludge: wait for page load to finish
cy.wait(2000);
// close team & auth tab
cy.get('button.ant-tabs-tab-remove').should('exist').click();
cy.wait(1000);
// open a table to work on views
//
@ -63,12 +74,11 @@ export const genTest = (apiType, dbType) => {
});
beforeEach(() => {
cy.fileHook();
cy.restoreLocalStorage();
// fix me!
window.localStorage.setItem('nc-right-sidebar', '{"isOpen":true,"hasSidebar":true}')
});
afterEach(() => {
cy.saveLocalStorage();
});
after(() => {
@ -103,6 +113,7 @@ export const genTest = (apiType, dbType) => {
cy.get(".nc-form-drag-LastUpdate").drag(
".nc-form-drag-Country"
);
cy.wait(1000);
// Verify if order is: LastUpdate, Country, Country => City
verifyFormDrawerFieldLocation("LastUpdate", 0);
@ -197,8 +208,8 @@ export const genTest = (apiType, dbType) => {
// fill up mandatory fields
cy.get(".nc-form-input-Country").type("_abc");
cy.get(".nc-form-input-LastUpdate").click();
cy.getActiveModal().find("button").contains("19").click();
cy.getActiveModal().find("button").contains("OK").click();
cy.get(".ant-picker-now-btn:visible").contains("Now").click();
cy.get(".ant-btn-primary:visible").contains("Ok").click();
// default message, no update
@ -214,18 +225,15 @@ export const genTest = (apiType, dbType) => {
it(`Validate ${viewType}: Submit default, with valid Show message entry`, () => {
// clicking again on view name shows blank still. work around- toggling between two views
// cy.get(`.nc-view-item.nc-grid-view-item`)
// .contains("Country")
// .click();
cy.get(`.nc-view-item.nc-${viewType}-view-item`)
.contains("Form-1")
.click();
// fill up mandatory fields
cy.get(".nc-form-input-Country").type("_abc");
cy.get(".nc-form-input-Country").should('exist').type("_abc");
cy.get(".nc-form-input-LastUpdate").click();
cy.getActiveModal().find("button").contains("19").click();
cy.getActiveModal().find("button").contains("OK").click();
cy.get(".ant-picker-now-btn:visible").contains("Now").click();
cy.get(".ant-btn-primary:visible").contains("Ok").click();
// add message
cy.get("textarea.nc-form-after-submit-msg")
@ -240,9 +248,6 @@ export const genTest = (apiType, dbType) => {
it(`Validate ${viewType}: Submit default, Enable checkbox "Submit another form`, () => {
// clicking again on view name shows blank still. work around- toggling between two views
// cy.get(`.nc-view-item.nc-grid-view-item`)
// .contains("Country")
// .click();
cy.get(`.nc-view-item.nc-${viewType}-view-item`)
.contains("Form-1")
.click();
@ -250,8 +255,8 @@ export const genTest = (apiType, dbType) => {
// fill up mandatory fields
cy.get(".nc-form-input-Country").type("_abc");
cy.get(".nc-form-input-LastUpdate").click();
cy.getActiveModal().find("button").contains("19").click();
cy.getActiveModal().find("button").contains("OK").click();
cy.get(".ant-picker-now-btn:visible").contains("Now").click();
cy.get(".ant-btn-primary:visible").contains("Ok").click();
// enable "Submit another form" check box
cy.get("button.nc-form-checkbox-submit-another-form").click();
@ -274,8 +279,8 @@ export const genTest = (apiType, dbType) => {
cy.get(".nc-form-input-Country").type("_abc");
cy.get(".nc-form-input-LastUpdate").click();
cy.getActiveModal().find("button").contains("19").click();
cy.getActiveModal().find("button").contains("OK").click();
cy.get(".ant-picker-now-btn:visible").contains("Now").click();
cy.get(".ant-btn-primary:visible").contains("Ok").click();
// enable "New form after 5 seconds" button
cy.get("button.nc-form-checkbox-submit-another-form")
@ -303,10 +308,11 @@ export const genTest = (apiType, dbType) => {
.click();
// validate if form has appeared again
cy.wait(1000);
validateFormHeader();
cy.get(".nc-form-remove-all").click();
cy.get("button.nc-form-checkbox-send-email")
.click();
cy.get(".nc-form-checkbox-send-email").click();
// validate if toaster pops up requesting to activate SMTP
cy.toastWait(
"Please activate SMTP plugin in App store for enabling email notification"
@ -335,13 +341,17 @@ export const genTest = (apiType, dbType) => {
// validate if form has appeared again
validateFormHeader();
cy.get("button.nc-form-checkbox-send-email")
cy.get(".nc-form-checkbox-send-email")
.click();
cy.toastWait(
"Please activate SMTP plugin in App store for enabling email notification"
);
settingsPage.openMenu(settingsPage.APPSTORE)
mainPage.resetSMTP();
cy.wait(3000);
cy.wait(300);
cy.openTableTab("Country", 25);
});
@ -350,30 +360,22 @@ export const genTest = (apiType, dbType) => {
cy.get(`.nc-view-item.nc-${viewType}-view-item`)
.contains("Form-1")
.click();
cy.get(".nc-form-add-all").click();
cy.wait(3000);
cy.wait(300);
// validate if form has appeared again
validateFormHeader();
cy.get(".nc-form-input-LastUpdate").should("exist");
// remove "LastUpdate field"
cy.get(".nc-form").find(".nc-field-remove-icon").eq(2).click();
cy.get(".nc-form").find(".nc-field-remove-icon").eq(1).click();
cy.get(".nc-form-input-LastUpdate").should("not.exist");
// cy.get(".col-md-4")
// .find(".pointer.item")
// .contains("LastUpdate")
// .should("exist");
// add it back
// cy.get(".col-md-4")
// .find(".pointer.item")
// .contains("LastUpdate")
// .click();
cy.get('.nc-form-left-drawer').find('.ant-card').contains('LastUpdate').should('exist').click();
cy.get(".nc-form-input-LastUpdate").should("exist");
cy.wait(3000);
cy.wait(300);
});
it(`Validate ${viewType}: URL verification`, () => {
@ -391,27 +393,33 @@ export const genTest = (apiType, dbType) => {
formViewURL = url;
});
cy.wait(3000);
// cy.saveLocalStorage();
cy.wait(300);
});
it(`Validate ${viewType}: URL validation after re-access`, () => {
it.skip(`Validate ${viewType}: URL validation after re-access`, () => {
// visit URL
cy.log(formViewURL);
// cy.restoreLocalStorage();
cy.visit(formViewURL, {
baseUrl: null,
});
cy.wait(5000);
// New form appeared? Header & description should exist
validateFormHeader();
});
it(`Delete ${viewType} view`, () => {
// cy.restoreLocalStorage();
// number of view entries should be 2 before we delete
cy.get(".nc-view-item").its("length").should("eq", 2);
// click on delete icon (becomes visible on hovering mouse)
cy.get(".nc-view-delete-icon").click({ force: true });
cy.wait(1000)
cy.getActiveModal().find('.ant-btn-dangerous').click();
cy.toastWait("View deleted successfully");
// confirm if the number of veiw entries is reduced by 1
@ -420,8 +428,6 @@ export const genTest = (apiType, dbType) => {
// clean up newly added rows into Country table operations
// this auto verifies successfull addition of rows to table as well
mainPage.getPagination(5).click();
// kludge: flicker on load
// cy.wait(3000)
cy.get(".nc-grid-row").should("have.length", 13);
cy.get(".ant-checkbox").should('exist').eq(10).click({ force: true });
@ -433,27 +439,6 @@ export const genTest = (apiType, dbType) => {
cy.getActiveMenu()
.contains("Delete Selected Rows")
.click({ force: true });
// mainPage
// .getRow(10)
// .find(".mdi-checkbox-blank-outline")
// .click({ force: true });
// mainPage
// .getRow(11)
// .find(".mdi-checkbox-blank-outline")
// .click({ force: true });
// mainPage
// .getRow(12)
// .find(".mdi-checkbox-blank-outline")
// .click({ force: true });
// mainPage
// .getRow(13)
// .find(".mdi-checkbox-blank-outline")
// .click({ force: true });
//
// mainPage.getCell("Country", 10).rightclick();
// cy.getActiveMenu().contains("Delete Selected Row").click();
// cy.toastWait('Deleted selected rows successfully')
});
};

8
scripts/cypress-v2/integration/common/4d_table_view_grid_locked.js

@ -8,22 +8,14 @@ export const genTest = (apiType, dbType) => {
// Run once before test- create project (rest/graphql)
//
before(() => {
cy.fileHook();
mainPage.tabReset();
// // kludge: wait for page load to finish
// cy.wait(1000);
// // close team & auth tab
// cy.get('button.ant-tabs-tab-remove').should('exist').click();
// cy.wait(1000);
// open a table to work on views
//
cy.openTableTab("Country", 25);
});
beforeEach(() => {
cy.fileHook();
});
after(() => {

12
scripts/cypress-v2/integration/common/4e_form_view_share.js

@ -12,7 +12,6 @@ export const genTest = (apiType, dbType) => {
// Run once before test- create project (rest/graphql)
//
before(() => {
cy.fileHook();
mainPage.tabReset();
// open a table to work on views
//
@ -29,7 +28,6 @@ export const genTest = (apiType, dbType) => {
});
beforeEach(() => {
cy.fileHook();
cy.restoreLocalStorage();
});
@ -107,12 +105,12 @@ export const genTest = (apiType, dbType) => {
.contains("Form-1")
.click();
mainPage.shareView().click({ force: true });
cy.wait(5000);
cy.wait(2000);
mainPage.shareView().click();
// copy link text, visit URL
cy.getActiveModal()
.should('exist')
.find(".share-link-box")
.contains("/nc/form/", { timeout: 10000 })
.should("exist")
@ -199,13 +197,15 @@ export const genTest = (apiType, dbType) => {
cy.visit(storedURL, {
baseUrl: null,
});
cy.wait(5000);
cy.wait(2000);
// number of view entries should be 2 before we delete
cy.get(".nc-view-item").its("length").should("eq", 2);
// click on delete icon (becomes visible on hovering mouse)
cy.get(".nc-view-delete-icon").click({ force: true });
cy.wait(1000);
cy.getActiveModal().find('.ant-btn-dangerous').should('exist').click();
cy.toastWait("View deleted successfully");
// confirm if the number of veiw entries is reduced by 1

144
scripts/cypress-v2/integration/common/4f_grid_view_share.js

@ -15,11 +15,6 @@ export const genTest = (apiType, dbType) => {
if (!isTestSuiteActive(apiType, dbType)) return;
const generateViewLink = (viewName) => {
// click on share view
// cy.get(".v-navigation-drawer__content > .container")
// .find(".v-list > .v-list-item")
// .contains("Share View")
// .click();
mainPage.shareView().click();
cy.wait(1000);
@ -45,7 +40,6 @@ export const genTest = (apiType, dbType) => {
// Run once before test- create project (rest/graphql)
//
before(() => {
cy.fileHook();
mainPage.tabReset();
// // kludge: wait for page load to finish
@ -63,7 +57,6 @@ export const genTest = (apiType, dbType) => {
});
beforeEach(() => {
cy.fileHook();
cy.restoreLocalStorage();
});
@ -90,6 +83,7 @@ export const genTest = (apiType, dbType) => {
// store base URL- to re-visit and delete form view later
cy.url().then((url) => {
storedURL = url;
cy.saveLocalStorage();
});
});
@ -110,16 +104,8 @@ export const genTest = (apiType, dbType) => {
generateViewLink("combined");
// verify if only one link exists in table
// cy.get(".v-navigation-drawer__content > .container")
// .find(".v-list > .v-list-item")
// .contains("Share View")
// .parent()
// .find("button.mdi-dots-vertical")
// .click();
mainPage.shareViewList().click();
// cy.getActiveMenu().find(".v-list-item").contains("Views List").click();
cy.get('th:contains("View Link")').should("exist");
cy.get('th:contains("View Link")')
@ -131,9 +117,6 @@ export const genTest = (apiType, dbType) => {
.should("eq", 1)
.then(() => {
cy.get('button.ant-modal-close:visible').click();
// cy.get(".v-overlay--active > .v-overlay__scrim").click({
// force: true,
// });
});
});
@ -192,7 +175,6 @@ export const genTest = (apiType, dbType) => {
for (let j = 0; j < 4; j++) {
expect(strCol[j]).to.be.equal(retCol[j]);
}
// expect(retrievedRecords[i]).to.be.equal(storedRecords[i])
}
};
@ -291,27 +273,27 @@ export const genTest = (apiType, dbType) => {
.find(".nc-icon.nc-action-icon.nc-plus")
.should("not.exist");
// to be fixed
// mainPage
// .getCell("Customer List", 3)
// .click()
// .find(".nc-icon.nc-action-icon.nc-arrow-expand")
// .click();
// cy.getActiveModal().find(".nc-icon.nc-reload").should("exist");
// cy.getActiveModal()
// .find("button")
// .contains("Link to")
// .should("not.exist");
// cy.getActiveModal()
// .find(".ant-card")
// .contains("2")
// .should("exist");
// cy.getActiveModal()
// .find(".ant-card")
// .find("button")
// .should("not.exist");
// cy.get('button.ant-modal-close').click();
mainPage
.getCell("Customer List", 3)
.click()
.find(".nc-icon.nc-action-icon.nc-arrow-expand")
.click({ force: true });
// reload button
cy.getActiveModal().find(".nc-icon").should("exist");
cy.getActiveModal()
.find("button")
.contains("Link to")
.should("not.exist");
cy.getActiveModal()
.find(".ant-card")
.contains("2")
.should("exist");
cy.getActiveModal()
.find(".ant-card")
.find("button")
.should("not.exist");
cy.get('button.ant-modal-close').click();
});
it(`Share GRID view : Virtual column validation > belongs to`, () => {
@ -322,17 +304,16 @@ export const genTest = (apiType, dbType) => {
.find(".nc-icon.nc-unlink-icon")
.should("not.exist");
// to be fixed
// mainPage
// .getCell("City", 1)
// .click()
// .find(".nc-icon.nc-action-icon.nc-arrow-expand")
// .should("not.exist");
// mainPage
// .getCell("City", 1)
// .find(".chips")
// .contains("Kanchrapara")
// .should("exist");
mainPage
.getCell("City", 1)
.click()
.find(".nc-icon.nc-action-icon.nc-arrow-expand")
.should("not.exist");
mainPage
.getCell("City", 1)
.find(".chips")
.contains("Kanchrapara")
.should("exist");
});
it(`Share GRID view : Virtual column validation > many to many`, () => {
@ -348,23 +329,24 @@ export const genTest = (apiType, dbType) => {
.find(".nc-icon.nc-action-icon.nc-plus")
.should("not.exist");
// to be fixed
// mainPage
// .getCell("Staff List", 1)
// .click()
// .find(".nc-icon.nc-action-icon.nc-arrow-expand")
// .click();
// cy.getActiveModal().find(".nc-icon.nc-reload").should("exist");
// cy.getActiveModal()
// .find("button")
// .contains("Link to")
// .should("not.exist");
// cy.get("body").type("{esc}");
mainPage
.getCell("Staff List", 1)
.click()
.find(".nc-icon.nc-action-icon.nc-arrow-expand")
.click({ force: true });
// reload button
cy.getActiveModal().find(".nc-icon").should("exist");
cy.getActiveModal()
.find("button")
.contains("Link to")
.should("not.exist");
cy.get('button.ant-modal-close:visible').last().click();
});
it(`Delete ${viewType.toUpperCase()} view`, () => {
// go back to base page
cy.restoreLocalStorage();
cy.visit(storedURL, {
baseUrl: null,
});
@ -389,23 +371,15 @@ export const genTest = (apiType, dbType) => {
describe(`${apiType.toUpperCase()} api - Grid view/ row-column update verification`, () => {
before(() => {
cy.fileHook();
// // kludge: wait for page load to finish
// cy.wait(1000);
// // close team & auth tab
// cy.get('button.ant-tabs-tab-remove').should('exist').click();
// cy.wait(1000);
cy.restoreLocalStorage();
// Address table has belongs to, has many & many-to-many
cy.openTableTab("Country", 25);
mainPage.toggleRightSidebar();
cy.saveLocalStorage();
// store base URL- to re-visit and delete form view later
cy.url().then((url) => {
storedURL = url;
cy.saveLocalStorage();
generateViewLink("rowColUpdate");
});
});
@ -425,13 +399,6 @@ export const genTest = (apiType, dbType) => {
// wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 10);
// mainPage
// .getRow(10)
// .find(".mdi-checkbox-blank-outline")
// .click({ force: true });
// mainPage.getCell("Country", 10).rightclick();
// cy.getActiveMenu().contains("Delete Selected Row").click();
mainPage.getCell("Country", 10).rightclick();
cy.getActiveMenu()
.find('.ant-dropdown-menu-item:contains("Delete Row")')
@ -440,14 +407,6 @@ export const genTest = (apiType, dbType) => {
// delete column
mainPage.deleteColumn("dummy");
// cy.get(`th:contains('dummy') .nc-ui-dt-dropdown`)
// .trigger("mouseover")
// .click();
// cy.get(".nc-column-delete").click();
// cy.get("button:contains(Confirm)").click();
//
// cy.toastWait("Update table successful");
mainPage.deleteCreatedViews();
// close table
@ -457,12 +416,6 @@ export const genTest = (apiType, dbType) => {
it(`Generate default Shared GRID view URL`, () => {
// add row
cy.get(".nc-add-new-row-btn").click();
// cy.get("#data-table-form-Country > input")
// .first()
// .click()
// .type("a");
// cy.contains("Save row").filter("button").click({ force: true });
// cy.toastWait("updated successfully");
cy.get(".nc-expand-col-Country").find(".nc-cell > input")
.should("exist")
.first()
@ -488,7 +441,6 @@ export const genTest = (apiType, dbType) => {
});
cy.wait(5000);
//5
// wait for public view page to load!
// wait for page rendering to complete
cy.get(".nc-grid-row").should("have.length", 25);

252
scripts/cypress-v2/integration/common/5a_user_role.js

@ -23,13 +23,32 @@ export const genTest = (apiType, dbType) => {
describe("Static user creations (different roles)", () => {
before(() => {
cy.fileHook();
mainPage.tabReset();
// kludge: wait for page load to finish
cy.wait(4000);
// close team & auth tab
cy.get('button.ant-tabs-tab-remove').should('exist').click();
cy.wait(1000);
settingsPage.openMenu(settingsPage.TEAM_N_AUTH)
cy.saveLocalStorage();
});
beforeEach(() => {
cy.fileHook();
cy.restoreLocalStorage();
});
after(() => {
// sign out
cy.visit(`/`);
cy.wait(5000);
cy.get('.nc-menu-accounts').should('exist').click();
cy.getActiveMenu().find('.ant-dropdown-menu-item').eq(1).click();
cy.wait(5000);
cy.get('button:contains("SIGN")').should('exist')
});
const addUser = (user) => {
@ -38,20 +57,26 @@ export const genTest = (apiType, dbType) => {
// for subsequent projects, they will be required to just add to this project
// using ROW count to identify if its former or latter scenario
// 5 users (owner, creator, editor, viewer, commenter) = 5
cy.get(`.nc-user-row`).then((obj) => {
cy.log(obj.length);
if (obj.length == 5) {
mainPage.addExistingUserToProject(
user.credentials.username,
user.name
);
} else {
mainPage.addNewUserToProject(
user.credentials,
user.name
);
}
});
// cy.get(`.nc-user-row`).then((obj) => {
// cy.log(obj.length);
// if (obj.length == 5) {
// mainPage.addExistingUserToProject(
// user.credentials.username,
// user.name
// );
// } else {
// mainPage.addNewUserToProject(
// user.credentials,
// user.name
// );
// }
// });
cy.get(`.nc-user-row`).should('exist')
mainPage.addNewUserToProject(
user.credentials,
user.name
);
});
};
@ -60,7 +85,7 @@ export const genTest = (apiType, dbType) => {
addUser(roles.commenter);
addUser(roles.viewer);
// Access contrl list- configuration
// Access control list- configuration
//
it(`Access control list- configuration`, () => {
mainPage.closeMetaTab();
@ -96,69 +121,77 @@ export const genTest = (apiType, dbType) => {
const roleValidation = (roleType) => {
describe(`User role validation`, () => {
before(() => {
cy.fileHook();
// cy.restoreLocalStorage();
cy.visit(mainPage.roleURL[roleType])
cy.wait(5000);
cy.get('button:contains("SIGN UP")').should('exist')
cy.get('input[type="text"]', { timeout: 20000 }).type(
roles[roleType].credentials.username
);
cy.get('input[type="password"]').type(roles[roleType].credentials.password);
cy.get('button:contains("SIGN UP")').click();
cy.wait(3000);
cy.get('.nc-project-page-title').contains("My Projects").should("be.visible");
if (dbType === "xcdb") {
if ("rest" == apiType)
projectsPage.openProject(
staticProjects.sampleREST.basic.name
);
else
projectsPage.openProject(
staticProjects.sampleGQL.basic.name
);
} else if (dbType === "mysql") {
if ("rest" == apiType)
projectsPage.openProject(
staticProjects.externalREST.basic.name
);
else
projectsPage.openProject(
staticProjects.externalGQL.basic.name
);
} else if (dbType === "postgres") {
if ("rest" == apiType)
projectsPage.openProject(
staticProjects.pgExternalREST.basic.name
);
else
projectsPage.openProject(
staticProjects.pgExternalGQL.basic.name
);
}
if (roleType === "creator") {
// kludge: wait for page load to finish
// close team & auth tab
cy.wait(2000);
cy.get('button.ant-tabs-tab-remove').should('exist').click();
cy.wait(1000);
}
cy.saveLocalStorage();
})
beforeEach(() => {
cy.fileHook();
cy.restoreLocalStorage();
});
if (roleType != "owner") {
it(`[${roles[roleType].name}] SignIn, Open project`, () => {
cy.log(mainPage.roleURL[roleType]);
cy.visit(mainPage.roleURL[roleType], {
baseUrl: null,
});
cy.wait(5000);
// Redirected to new URL, feed details
//
cy.get('input[type="text"]')
.should("exist")
.type(roles[roleType].credentials.username);
cy.get('input[type="password"]').type(
roles[roleType].credentials.password
);
cy.get('button:contains("SIGN")').click();
// cy.url({ timeout: 6000 }).should("contain", "#/project");
cy.get('nc-project-page-title').contains("My Projects").should("be.visible");
if (dbType === "xcdb") {
if ("rest" == apiType)
projectsPage.openProject(
staticProjects.sampleREST.basic.name
);
else
projectsPage.openProject(
staticProjects.sampleGQL.basic.name
);
} else if (dbType === "mysql") {
if ("rest" == apiType)
projectsPage.openProject(
staticProjects.externalREST.basic.name
);
else
projectsPage.openProject(
staticProjects.externalGQL.basic.name
);
} else if (dbType === "postgres") {
if ("rest" == apiType)
projectsPage.openProject(
staticProjects.pgExternalREST.basic.name
);
else
projectsPage.openProject(
staticProjects.pgExternalGQL.basic.name
);
}
if (roleType != "creator") {
cy.closeTableTab("Actor");
}
});
}
after(() => {
// sign out
cy.visit(`/`);
cy.wait(5000);
cy.get('.nc-menu-accounts').should('exist').click();
cy.getActiveMenu().find('.ant-dropdown-menu-item').eq(1).click();
cy.wait(5000);
cy.get('button:contains("SIGN")').should('exist')
});
///////////////////////////////////////////////////////
// Test suite
@ -205,45 +238,44 @@ export const genTest = (apiType, dbType) => {
_viewMenu(roleType, "userRole");
});
it(`[${roles[roleType].name}] Top Right Menu bar`, () => {
// Share button is conditional
// Rest are static/ mandatory
//
_topRightMenu(roleType, "userRole");
});
it(`[${roles[roleType].name}] Download files`, () => {
// viewer & commenter doesn't contain hideField option in ncv2
// #ID, City, LastUpdate, City => Address, Country <= City, +
mainPage.hideField("LastUpdate");
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`City,Address List,Country`,
`A Corua (La Corua),939 Probolinggo Loop,Spain`,
`Abha,733 Mandaluyong Place,Saudi Arabia`,
`Abu Dhabi,535 Ahmadnagar Manor,United Arab Emirates`,
`Acua,1789 Saint-Denis Parkway,Mexico`,
];
// skip if xcdb
if (!isXcdb()) {
for (let i = 0; i < storedRecords.length; i++) {
// cy.log(retrievedRecords[i])
expect(retrievedRecords[i]).to.be.equal(
storedRecords[i]
);
// to be fixed
if(roleType === 'commenter' || roleType === 'viewer') {}
else {
// viewer & commenter doesn't contain hideField option in ncv2
// #ID, City, LastUpdate, City => Address, Country <= City, +
mainPage.hideField("LastUpdate");
const verifyCsv = (retrievedRecords) => {
// expected output, statically configured
let storedRecords = [
`City,Address List,Country`,
`A Corua (La Corua),939 Probolinggo Loop,Spain`,
`Abha,733 Mandaluyong Place,Saudi Arabia`,
`Abu Dhabi,535 Ahmadnagar Manor,United Arab Emirates`,
`Acua,1789 Saint-Denis Parkway,Mexico`,
];
// skip if xcdb
if (!isXcdb()) {
for (let i = 0; i < storedRecords.length; i++) {
// cy.log(retrievedRecords[i])
expect(retrievedRecords[i]).to.be.equal(
storedRecords[i]
);
}
}
}
};
};
// download & verify
mainPage.downloadAndVerifyCsv(
`City_exported_1.csv`,
verifyCsv
);
mainPage.unhideField("LastUpdate");
// download & verify
mainPage.downloadAndVerifyCsv(
`City_exported_1.csv`,
verifyCsv,
roleType
);
mainPage.unhideField("LastUpdate");
}
});
});
};

12
scripts/cypress-v2/integration/common/5b_preview_role.js

@ -29,20 +29,23 @@ export const genTest = (apiType, dbType, roleType) => {
describe("Role preview validations", () => {
// Sign in/ open project
before(() => {
cy.fileHook();
loginPage.loginAndOpenProject(apiType, dbType);
cy.openTableTab("City", 25);
cy.wait(3000);
settingsPage.openProjectMenu();
cy.getActiveMenu().find(`[data-submenu-id="preview-as"]`).should('exist').click()
cy.wait(1000)
cy.get('.ant-dropdown-menu-submenu').eq(3).find(`[data-menu-id="editor"]`).should('exist').click()
cy.get('.ant-dropdown-menu-submenu').eq(4).find(`[data-menu-id="editor"]`).should('exist').click()
cy.wait(10000)
cy.saveLocalStorage();
});
beforeEach(() => {
cy.fileHook();
cy.restoreLocalStorage();
});
after(() => {
@ -93,6 +96,9 @@ export const genTest = (apiType, dbType, roleType) => {
.find(`[type="radio"][value="${roleType}"]`)
.should('exist')
.click();
cy.wait(5000)
cy.saveLocalStorage();
});
it(`Role preview: ${roleType}: Advance settings`, () => {

10
scripts/cypress-v2/integration/common/7b_import_from_airtable.js

@ -13,7 +13,6 @@ export const genTest = (apiType, dbType) => {
describe(`Import from airtable`, () => {
before(() => {
cy.fileHook();
apiKey = Cypress.env("airtable").apiKey;
sharedBase = Cypress.env("airtable").sharedBase;
@ -21,14 +20,17 @@ export const genTest = (apiType, dbType) => {
projectsPage.createProject({ dbType: "none", apiType: "REST", name: "importSample" }, {})
});
after(() => {});
after(() => {
cy.saveLocalStorage();
});
it("Import", () => {
cy.log(apiKey, sharedBase);
// trigger import
cy.get(`[data-menu-id="addORImport"]`).click();
cy.getActivePopUp().contains("Airtable").should('exist').click();
cy.get('.nc-add-new-table').should('exist').trigger('mouseover')
cy.get('.nc-import-menu').should('exist').click()
cy.getActiveMenu().find('.ant-dropdown-menu-item').contains('Airtable').click()
cy.getActiveModal().find(".nc-input-api-key").should('exist').clear().type(apiKey)
cy.getActiveModal().find(".nc-input-shared-base").should('exist').clear().type(sharedBase)

242
scripts/cypress-v2/integration/common/9a_QuickTest.js

@ -39,30 +39,31 @@ let cn = [ "Name", "Notes", "Status", "Tags", "Done", "Date", "Phone",
"Actor", "Status (from Actor)", "RollUp", "Computation", "Producer" ]
function openWebhook(index) {
cy.get(".nc-btn-webhook").should("exist").click();
cy.get(".nc-hook").eq(index).click({ force: true });
cy.get('.nc-actions-menu-btn').should('exist').click();
cy.getActiveMenu().find('.ant-dropdown-menu-title-content').contains('Webhooks').click()
cy.get(".nc-hook").eq(index).click();
}
// to be invoked after open
function verifyWebhook(config) {
cy.get(".nc-text-field-hook-title")
.find('input').then(($element) => {
cy.get(".nc-text-field-hook-title").then(($element) => {
expect($element[0].value).to.have.string(config.title)
})
cy.get(".nc-text-field-hook-event")
.find('.v-select__selection')
.find('.ant-select-selection-item')
.contains(config.event)
.should('exist')
cy.get(".nc-text-field-hook-notification-type")
.find('.v-select__selection')
cy.get(".nc-select-hook-notification-type")
.find('.ant-select-selection-item')
.contains(config.notification)
.should('exist')
cy.get('.nc-select-hook-url-method')
.find('.v-select__selection')
.find('.ant-select-selection-item')
.contains(config.type)
.should('exist')
cy.get(".nc-text-field-hook-url-path")
.find('input').then(($element) => {
.then(($element) => {
expect($element[0].value).to.have.string(config.url)
})
cy.get(".nc-icon-hook-navigate-left").click({force:true})
@ -80,26 +81,56 @@ export const genTest = (apiType, dbType, testMode) => {
}
before(() => {
cy.fileHook();
cy.restoreLocalStorage();
if( testMode === 'CY_QUICK') {
// cy.task("copyFile")
loginPage.signIn(roles.owner.credentials);
projectsPage.openProject("sample");
// kludge: wait for page load to finish
cy.wait(2000);
// close team & auth tab
cy.get('button.ant-tabs-tab-remove').should('exist').click();
cy.wait(1000);
cy.saveLocalStorage();
}
});
after(() => {});
beforeEach(() => {
cy.restoreLocalStorage();
})
// afterEach(() => {
// cy.saveLocalStorage();
// })
after(() => {
cy.restoreLocalStorage();
// sign out
cy.visit(`/`);
cy.wait(5000);
cy.get('.nc-menu-accounts').should('exist').click();
cy.getActiveMenu().find('.ant-dropdown-menu-item').eq(1).click();
cy.wait(5000);
cy.get('button:contains("SIGN")').should('exist')
});
it("Verify Schema", () => {
cy.openTableTab("Film", 3)
// verify if all tables exist
for(let i=0; i<tn.length; i++)
cy.get(".nc-project-tree").contains(tn[i]).should('exist')
for(let i=0; i<tn.length; i++) {
cy.get(`.nc-project-tree-tbl-${tn[i]}`).should('exist')
}
// for Film table, verify columns
for(let i=0; i<columnCount; i++)
cy.get(".nc-grid-header-row").find(`[data-col="${cn[i]}"]`).should('exist')
for(let i=0; i<columnCount; i++) {
cy.get(`th[data-title="${cn[i]}"]`).should("exist");
}
});
@ -114,20 +145,18 @@ export const genTest = (apiType, dbType, testMode) => {
// checkbox
mainPage
.getCell("Done", cellIdx)
.find(".mdi-check-circle-outline")
.should(records2.Done ? "exist" : "not.exist");
.find(".nc-cell-hover-show")
.should(records2.Done ? "not.exist" : "exist");
// date
// duration
mainPage.getCell("Duration", cellIdx).find('input').then(($e) => {
expect($e[0].value).to.equal(records2.Duration)
})
mainPage.getCell("Duration", cellIdx).contains(records2.Duration).should("exist");
// rating
mainPage
.getCell("Rating", cellIdx)
.find("button.mdi-star")
.find(".ant-rate-star-full")
.should("have.length", records2.Rating);
// verifying only one instance as its different for PG & SQLite
@ -135,58 +164,34 @@ export const genTest = (apiType, dbType, testMode) => {
// for SQLite: its Actor1, Actor2
// LinkToAnotherRecord
mainPage.getCell("Actor", cellIdx).scrollIntoView();
cy.get(
`:nth-child(${cellIdx}) > [data-col="Actor"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(1) > .v-chip__content > .name`
)
cy.get(`:nth-child(${cellIdx}) > [data-title="Actor"]`)
.find('.chip')
.eq(0)
.contains(records2.Actor[0])
.should("exist");
// cy.get(
// `:nth-child(${cellIdx}) > [data-col="Actor"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(2) > .v-chip__content > .name`
// )
// .contains(records2.Actor[1])
// .should("exist");
// lookup
mainPage.getCell("Status (from Actor)", cellIdx).scrollIntoView();
cy.get(
`:nth-child(${cellIdx}) > [data-col="Status (from Actor)"] > .nc-virtual-cell > .v-lazy > .d-flex > :nth-child(1) > .v-chip__content > div > .set-item`
)
cy.get(`:nth-child(${cellIdx}) > [data-title="Status (from Actor)"]`)
.find('.nc-cell')
.eq(0)
.contains(records2["Status (from Actor)"][0])
.should("exist");
// cy.get(
// `:nth-child(${cellIdx}) > [data-col="Status (from Actor)"] > .nc-virtual-cell > .v-lazy > .d-flex > :nth-child(2) > .v-chip__content > div > .set-item`
// )
// .contains(records2["Status (from Actor)"][1])
// .should("exist");
// rollup
if( testMode === 'CY_QUICK') {
mainPage.getCell("RollUp", cellIdx).scrollIntoView();
cy.get(`:nth-child(${cellIdx}) > [data-col="RollUp"] > .nc-virtual-cell`)
.contains(records2.RollUp)
.should("exist");
mainPage.getCell("RollUp", cellIdx).contains(records2.RollUp).should("exist");
// formula
mainPage.getCell("Computation", cellIdx).scrollIntoView();
cy.get(
`:nth-child(${cellIdx}) > [data-col="Computation"] > .nc-virtual-cell`
)
.contains(records2.Computation)
.should("exist");
mainPage.getCell("Computation", cellIdx).contains(records2.Computation).should("exist");
// ltar hm relation
mainPage.getCell("Producer", cellIdx).scrollIntoView();
cy.get(
`:nth-child(${cellIdx}) > [data-col="Producer"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(1) > .v-chip__content > .name`
)
.contains(records2.Producer[0])
.should("exist");
cy.get(
`:nth-child(${cellIdx}) > [data-col="Producer"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(2) > .v-chip__content > .name`
)
.contains(records2.Producer[1])
.should("exist");
mainPage.getCell("Producer", cellIdx).find('.chip').eq(0).contains(records2.Producer[0]).should('exist')
mainPage.getCell("Producer", cellIdx).find('.chip').eq(1).contains(records2.Producer[1]).should('exist')
}
cy.closeTableTab("Film");
@ -194,55 +199,80 @@ export const genTest = (apiType, dbType, testMode) => {
it("Verify Views & Shared base", () => {
cy.openTableTab("Film", 3);
cy.get('.nc-form-view-item').eq(0)
.click({ force: true })
mainPage.toggleRightSidebar();
cy.get('.nc-form-view-item:visible')
.should('exist')
.eq(0)
.click({ force: true })
// Header & description should exist
// cy.get(".nc-form")
// .find('[placeholder="Form Title"]')
// .contains("FormTitle")
// .should("exist");
// cy.get(".nc-form")
// .find('[placeholder="Add form description"]')
// .contains("FormDescription")
// .should("exist");
cy.get(".nc-form").should("exist");
cy.get(".nc-form")
.find('[placeholder="Form Title"]')
.contains("FormTitle")
.should("exist");
.find('[placeholder="Form Title"]')
.should("exist").then(($el) => {
cy.log($el)
expect($el.val()).to.equal("FormTitle");
})
cy.get(".nc-form")
.find('[placeholder="Add form description"]')
.contains("FormDescription")
.should("exist");
.find('[placeholder="Add form description"]')
.should("exist").then(($el) => {
cy.log($el)
expect($el.val()).to.equal("FormDescription");
})
// modified column name & help text
cy.get(".nc-field-wrapper").eq(0)
.find('.nc-field-labels')
cy.get(".nc-editable").eq(0)
.find('.name')
.contains("DisplayName")
.should('exist')
cy.get(".nc-field-wrapper").eq(0)
.find('.nc-hint')
cy.get(".nc-editable").eq(0)
.find('.text-gray-500')
.contains('HelpText')
.should('exist')
cy.get(".nc-field-wrapper").eq(1)
.find('.nc-field-labels')
cy.get(".nc-editable").eq(1)
.find('.name')
.contains("Email")
.should('exist')
// add message
cy.get(".nc-form > .mx-auto")
.find("textarea").then(($element) => {
expect($element[0].value).to.have.string("Thank you for submitting the form!")
})
// submit another form button
cy.get(".nc-form > .mx-auto")
.find('[type="checkbox"]')
.eq(0)
.should('be.checked')
// "New form after 5 seconds" button
cy.get(".nc-form > .mx-auto")
.find('[type="checkbox"]')
.eq(1)
.should('be.checked')
// email me
cy.get(".nc-form > .mx-auto")
.find('[type="checkbox"]')
.eq(2)
.should('not.be.checked')
cy.get("textarea.nc-form-after-submit-msg").then(($element) => {
expect($element[0].value).to.have.string("Thank you for submitting the form!")
})
// cy.get(".nc-form > .mx-auto")
// .find("textarea").then(($element) => {
// expect($element[0].value).to.have.string("Thank you for submitting the form!")
// })
cy.get("button.nc-form-checkbox-submit-another-form.ant-switch-checked").should('exist')
cy.get("button.nc-form-checkbox-show-blank-form.ant-switch-checked").should('exist')
cy.get("button.nc-form-checkbox-send-email.ant-switch-checked").should('not.exist')
// // submit another form button
// cy.get(".nc-form > .mx-auto")
// .find('[type="checkbox"]')
// .eq(0)
// .should('be.checked')
// // "New form after 5 seconds" button
// cy.get(".nc-form > .mx-auto")
// .find('[type="checkbox"]')
// .eq(1)
// .should('be.checked')
// // email me
// cy.get(".nc-form > .mx-auto")
// .find('[type="checkbox"]')
// .eq(2)
// .should('not.be.checked')
cy.closeTableTab("Film");
});
@ -296,29 +326,34 @@ export const genTest = (apiType, dbType, testMode) => {
mainPage.getPagination(">").click();
mainPage
.getPagination(2)
.should("have.class", "v-pagination__item--active");
.should("have.class", "ant-pagination-item-active");
// verify < pagination option
mainPage.getPagination("<").click();
mainPage
.getPagination(1)
.should("have.class", "v-pagination__item--active");
.should("have.class", "ant-pagination-item-active");
cy.closeTableTab("Actor");
});
it("Verify Fields, Filter & Sort", () => {
cy.openTableTab("Actor", 25);
mainPage.toggleRightSidebar();
cy.get(".nc-grid-view-item").eq(1).click()
cy.get(".nc-grid-header-cell").contains('Name').should("be.visible");
cy.get(".nc-grid-header-cell").contains('Notes').should("be.visible");
// fix me!
if(testMode !== 'AT_IMPORT') cy.get(".nc-grid-header-cell").contains('Attachments').should("not.be.visible");
cy.get(".nc-grid-header-cell").contains('Status').should("be.visible");
cy.get(".nc-grid-header-cell").contains('Film').should("be.visible");
cy.wait(3000)
cy.get(".nc-grid-header").find(`th[data-title="Name"]`).should("be.visible");
cy.get(".nc-grid-header").find(`th[data-title="Notes"]`).should("be.visible");
cy.get(".nc-grid-header").find(`th[data-title="Attachments"]`).should("not.exist");
cy.get(".nc-grid-header").find(`th[data-title="Status"]`).should("be.visible");
cy.get(".nc-grid-header").find(`th[data-title="Film"]`).should("be.visible");
cy.wait(2000);
cy.get(".nc-fields-menu-btn").click()
cy.get(".nc-fields-menu-btn").click();
cy.getActiveMenu().find(`[type="checkbox"]`).eq(0).should('be.checked')
cy.getActiveMenu().find(`[type="checkbox"]`).eq(1).should('be.checked')
cy.getActiveMenu().find(`[type="checkbox"]`).eq(2).should('not.be.checked')
@ -358,15 +393,18 @@ export const genTest = (apiType, dbType, testMode) => {
if( testMode === 'CY_QUICK') {
cy.openTableTab("Producer", 3)
mainPage.toggleRightSidebar();
cy.get('.nc-grid-view-item').should('have.length', 4)
cy.get('.nc-form-view-item').should('have.length', 4)
cy.get('.nc-gallery-view-item').should('have.length', 3)
// LinkToAnotherRecord hm relation
mainPage.getCell("FilmRead", 1).scrollIntoView();
cy.get(
':nth-child(1) > [data-col="FilmRead"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(1) > .v-chip__content > .name'
)
cy.get('[data-title="FilmRead"] > .h-full > .nc-virtual-cell > .w-full > .chips > .chip > .name')
// cy.get(
// ':nth-child(1) > [data-col="FilmRead"] > .nc-virtual-cell > .v-lazy > .d-100 > .chips > :nth-child(1) > .v-chip__content > .name'
// )
.contains('Movie-1')
.should("exist");
@ -374,7 +412,7 @@ export const genTest = (apiType, dbType, testMode) => {
}
})
it("Delete Project", () => {
it.skip("Delete Project", () => {
if( testMode === 'AT_IMPORT') {
mainPage.toolBarTopLeft(mainPage.HOME).click({force:true})
cy.get(`.mdi-delete-outline`, {

78
scripts/cypress-v2/integration/spec/roleValidation.spec.js

@ -49,12 +49,12 @@ export function _advSettings(roleType, mode) {
export function _editSchema(roleType, mode) {
let columnName = "City";
let validationString =
true == roles[roleType].validations.editSchema ? "exist" : "not.exist";
true === roles[roleType].validations.editSchema ? "exist" : "not.exist";
cy.openTableTab(columnName, 25);
// create table
cy.get(`.nc-add-import-btn`).should(validationString);
cy.get(`.nc-add-new-table`).should(validationString);
// delete table option
cy.get(`.nc-project-tree-tbl-City`).should("exist").rightclick();
@ -64,7 +64,8 @@ export function _editSchema(roleType, mode) {
cy.getActiveMenu().find('[role="menuitem"]').contains("Delete").should("exist");
cy.getActiveMenu().find('[role="menuitem"]').contains("Rename").should("exist");
mainPage.getCell(columnName, 1).click();
// click on a cell to close table context menu
mainPage.getCell(columnName, 3).click();
}
// add new column option
@ -73,6 +74,15 @@ export function _editSchema(roleType, mode) {
// update column (edit/ delete menu)
cy.get('.nc-ui-dt-dropdown').should(validationString)
if(validationString === "exist"){
cy.get('.nc-import-menu').should('exist').click();
cy.getActiveMenu().should('exist')
cy.getActiveMenu().find('.ant-dropdown-menu-item').contains('Airtable')
cy.getActiveMenu().find('.ant-dropdown-menu-item').contains('CSV file')
cy.getActiveMenu().find('.ant-dropdown-menu-item').contains('JSON file')
cy.getActiveMenu().find('.ant-dropdown-menu-item').contains('Microsoft Excel')
}
}
export function _editData(roleType, mode) {
@ -82,20 +92,17 @@ export function _editData(roleType, mode) {
cy.openTableTab(columnName, 25);
// add row
// add row button
cy.get('.nc-add-new-row-btn:visible').should(validationString);
// add button at bottom of page
mainPage.getCell(columnName, 25).scrollIntoView();
// cy.get('.nc-grid-add-new-cell').scrollIntoView();
cy.get('.nc-grid-add-new-cell:visible').should(validationString);
// update row option (right click)
//
mainPage.getCell("City", 5).rightclick();
cy.wait(1000);
cy.wait(100);
cy.get(".ant-dropdown-content:visible").should(validationString);
if (validationString === "exist") {
@ -183,7 +190,7 @@ export function _editComment(roleType, mode) {
.click();
cy.getActiveDrawer().find(".nc-comment-box").should('exist').type("Comment-1{enter}");
cy.toastWait('Comment added successfully')
// cy.toastWait('Comment added successfully')
cy.getActiveDrawer().find(".nc-toggle-comments").click();
}
@ -200,54 +207,61 @@ export function _editComment(roleType, mode) {
export function _viewMenu(roleType, mode) {
let columnName = "City";
// Download CSV, Excel
let actionsMenuItemsCnt = 2;
// Lock, Download, Upload
let menuWithSubmenuCount = 3;
cy.openTableTab(columnName, 25);
// share view list, webhook
let menuWithoutSubmenuCount = 2;
cy.wait(1000);
cy.openTableTab(columnName, 25);
// temporary!
cy.get('.nc-toggle-right-navbar').click();
cy.wait(1000);
// hard-wire
// window.localStorage.setItem('nc-right-sidebar', '{"isOpen":true,"hasSidebar":true}')
let validationString =
true === roles[roleType].validations.shareView ? "exist" : "not.exist";
if (roleType === "owner" || roleType === "creator") {
// Download CSV / Download XLSX / Upload CSV / Shared View List / Webhook
actionsMenuItemsCnt = 5;
} else if (roleType == "editor") {
// Download CSV / Upload CSV / Download XLSX
actionsMenuItemsCnt = 2;
if (roleType === "editor") {
// Download / Upload CSV
menuWithSubmenuCount = 2;
menuWithoutSubmenuCount = 0
} else if (roleType === "commenter" || roleType === "viewer") {
// Download CSV & Download excel
menuWithSubmenuCount = 0;
menuWithoutSubmenuCount = 2
}
// view list field (default GRID view)
cy.get(`.nc-view-item`).should("exist");
// view create option, exists only for owner/ creator
cy.get(`.nc-create-1-view`).should(validationString);
cy.get(`.nc-create-2-view`).should(validationString);
cy.get(`.nc-create-3-view`).should(validationString);
// share view & automations, exists only for owner/creator
// cy.get(".nc-btn-share-view").should(validationString);
// cy.get(`.nc-webhook-btn`).should(validationString);
cy.get(`.nc-create-grid-view`).should(validationString);
cy.get(`.nc-create-gallery-view`).should(validationString);
cy.get(`.nc-create-form-view`).should(validationString);
// share view permissions are role specific
// actions menu (more), only download csv should be visible for non-previlaged users
cy.get(".nc-actions-menu-btn").click();
cy.getActiveMenu()
.find('.nc-project-menu-item')
.should("have.length", actionsMenuItemsCnt);
.find('.ant-dropdown-menu-submenu:visible')
.should("have.length", menuWithSubmenuCount);
cy.getActiveMenu()
.find('.ant-dropdown-menu-item:visible')
.should("have.length", menuWithoutSubmenuCount);
// click again to close menu
cy.get(".nc-actions-menu-btn").click();
}
export function _topRightMenu(roleType, mode) {
// kludge; download csv menu persists until clicked
let columnName = "City";
cy.closeTableTab(columnName);
cy.openTableTab(columnName, 25);
// cy.closeTableTab(columnName);
// cy.openTableTab(columnName, 25);
let validationString =
true == roles[roleType].validations.shareView ? "exist" : "not.exist";

2
scripts/cypress-v2/integration/test/restRoles.js

@ -11,7 +11,7 @@ const nocoTestSuite = (apiType, dbType) => {
t01.genTest(apiType, dbType);
t5a.genTest(apiType, dbType);
t5b.genTest(apiType, dbType);
// t5b.genTest(apiType, dbType);
};
nocoTestSuite("rest", "mysql");

5
scripts/cypress-v2/integration/test/restViews.js

@ -14,11 +14,12 @@ const nocoTestSuite = (apiType, dbType) => {
setCurrentMode(apiType, dbType);
t01.genTest(apiType, dbType);
// place plugin related activities at top
t4c.genTest(apiType, dbType);
t4a.genTest(apiType, dbType);
t4b.genTest(apiType, dbType);
t4c.genTest(apiType, dbType);
t4d.genTest(apiType, dbType);
t4e.genTest(apiType, dbType);
// to be fixed t4e.genTest(apiType, dbType);
t4f.genTest(apiType, dbType);
};

65
scripts/cypress-v2/support/commands.js

@ -154,15 +154,19 @@ Cypress.Commands.add("refreshTableTab", () => {
// rc: row count. validate row count if rc!=0
Cypress.Commands.add("openTableTab", (tn, rc) => {
cy.task("log", `[openTableTab] ${tn} ${rc}`);
cy.get(`.nc-project-tree-tbl-${tn}`, { timeout: 10000 }).should("exist")
cy.get(`.nc-project-tree-tbl-${tn}`)
.should("exist")
.first()
.click({ force: true });
.click();
// kludge to make new tab active
cy.get('.ant-tabs-tab-btn')
.contains(tn)
.should('exist')
.click({ force: true });
// cy.get('.ant-tabs-tab-btn')
// .contains(tn)
// .should('exist')
// .click();
cy.wait(3000);
cy.get('.xc-row-table.nc-grid').should('exist');
// wait for page rendering to complete
@ -227,14 +231,32 @@ Cypress.Commands.add("openOrCreateGqlProject", (_args) => {
});
let LOCAL_STORAGE_MEMORY = {};
let LOCAL_STORAGE_MEMORY_v2 = {};
Cypress.Commands.add("saveLocalStorage", (name) => {
if(name) {
cy.task('log', `[saveLocalStorage] ${name}`);
LOCAL_STORAGE_MEMORY_v2[name] = {}
Object.keys(localStorage).forEach((key) => {
LOCAL_STORAGE_MEMORY_v2[name][key] = localStorage[key];
});
return;
}
Cypress.Commands.add("saveLocalStorage", () => {
Object.keys(localStorage).forEach((key) => {
LOCAL_STORAGE_MEMORY[key] = localStorage[key];
});
});
Cypress.Commands.add("restoreLocalStorage", () => {
Cypress.Commands.add("restoreLocalStorage", (name) => {
if(name) {
cy.task('log', `[restoreLocalStorage] ${name}`);
Object.keys(LOCAL_STORAGE_MEMORY_v2[name]).forEach((key) => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY_v2[name][key]);
});
return;
}
Object.keys(LOCAL_STORAGE_MEMORY).forEach((key) => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
});
@ -246,6 +268,11 @@ Cypress.Commands.add("deleteLocalStorage", () => {
});
});
Cypress.Commands.add('printLocalStorage', () => {
cy.task('log', `[printLocalStorage]`);
cy.task('log', JSON.stringify(localStorage, null, 2));
})
Cypress.Commands.add("getActiveModal", () => {
return cy.get(".ant-modal-content:visible").last()
});
@ -264,25 +291,29 @@ Cypress.Commands.add("getActiveSelection", () => {
Cypress.Commands.add("getActiveDrawer", () => {
return cy.get(".ant-drawer-content:visible").last();
});
Cypress.Commands.add("createTable", (name) => {
// cy.get(".nc-btn-tbl-add").click();
});
// cy.get(`[data-menu-id="addORImport"]`).click();
// cy.getActivePopUp().contains("Add new table").should('exist').click();
Cypress.Commands.add("getActivePicker", () => {
return cy.get(".ant-picker-dropdown :visible").last();
});
Cypress.Commands.add("createTable", (name) => {
cy.task("log", `[createTableTab] ${name}`);
cy.wait(1000);
cy.get('.nc-add-new-table').should('exist').click();
cy.wait(1000);
cy.getActiveModal().find(`input[type="text"]:visible`)
.click()
.clear()
.type(name)
cy.getActiveModal().find("button").contains("Submit").click();
// submit button
cy.getActiveModal().find("button.ant-btn-primary:visible").click();
cy.wait(1000)
cy.get('.xc-row-table.nc-grid').should('exist');
cy.get('.ant-tabs-tab-active > .ant-tabs-tab-btn').contains(name).should("exist");
// cy.get('.ant-tabs-tab-active > .ant-tabs-tab-btn').contains(name).should("exist");
cy.url().should("contain", `table/${name}`);
cy.get(`.nc-project-tree-tbl-${name}`).should("exist");
cy.wait(1000)
});
Cypress.Commands.add("deleteTable", (name, dbType) => {

22
scripts/cypress-v2/support/page_objects/mainPage.js

@ -238,10 +238,11 @@ export class _mainPage {
cy.getActiveModal().find('#form_item_from').should('exist').clear().type(from)
cy.getActiveModal().find('#form_item_host').should('exist').clear().type(host)
cy.getActiveModal().find('#form_item_port').should('exist').clear().type(port)
cy.getActiveModal().find('#form_item_secure').should('exist').clear().type(secure)
// cy.getActiveModal().find('#form_item_secure').should('exist').clear().type(secure)
cy.getActiveModal().find("button").contains("Save").click();
cy.toastWait('Successfully installed and email notification will use SMTP configuration');
settingsPage.closeMenu()
};
resetSMTP = () => {
@ -251,6 +252,7 @@ export class _mainPage {
cy.getActiveModal().find("button").contains("Confirm").click();
cy.toastWait("Plugin uninstalled successfully");
settingsPage.closeMenu()
};
shareView = () => {
@ -365,7 +367,7 @@ export class _mainPage {
// one of the row would contain seggregation header ('other views)
if (5 == $tableRow[0].childElementCount) {
cy.wrap($tableRow).find(".nc-icon").last().click();
cy.wait(1000);
cy.wait(100);
}
})
.then(() => {
@ -380,11 +382,17 @@ export class _mainPage {
// wait for a while & check in configured download folder for the intended file
// if it exists, verify it against 'expectedRecords' passed in as parameter
//
downloadAndVerifyCsv = (filename, verifyCsv) => {
cy.get(".nc-actions-menu-btn").click();
cy.getActiveMenu().find('.nc-project-menu-item').contains('Download').click();
cy.wait(1000);
cy.get('.nc-project-menu-item').contains('Download as CSV').should('exist').click();
downloadAndVerifyCsv = (filename, verifyCsv, role) => {
if(role === 'commenter' || role === 'viewer') {
cy.get(".nc-actions-menu-btn").click();
cy.getActiveMenu().find('.nc-project-menu-item').contains('Download as CSV').click();
} else {
cy.get(".nc-actions-menu-btn").click();
cy.getActiveMenu().find('.nc-project-menu-item').contains('Download').click();
cy.wait(1000);
cy.get('.nc-project-menu-item').contains('Download as CSV').should('exist').click();
}
cy.toastWait("Successfully exported all table data").then(() => {
// download folder path, read from config file

11
scripts/cypress-v2/support/page_objects/navigation.js

@ -68,6 +68,9 @@ export class _loginPage {
// standard pre-project activity
//
loginAndOpenProject(apiType, dbType) {
cy.restoreLocalStorage();
cy.wait(1000);
loginPage.signIn(roles.owner.credentials);
if (dbType === "mysql") {
@ -77,6 +80,12 @@ export class _loginPage {
} else if (dbType === "postgres") {
projectsPage.openProject(staticProjects.pgExternalREST.basic.name);
}
// kludge: wait for page load to finish
cy.wait(2000);
// close team & auth tab
cy.get('button.ant-tabs-tab-remove').should('exist').click();
cy.wait(1000);
}
}
@ -128,6 +137,8 @@ export class _projectsPage {
cy.get(".nc-metadb-project-name").should("exist");
cy.contains("button", "Create").should("exist");
cy.wait(1000)
// feed project name
cy.get(".nc-metadb-project-name", { timeout: 20000 }).clear().type(
projectName

Loading…
Cancel
Save