Browse Source

Merge branch 'develop' of https://github.com/nocodb/nocodb into geodata-prototyping-restart

pull/4749/head
flisowna 2 years ago
parent
commit
d4d8d2973b
  1. 14
      .github/workflows/release-nightly-dev.yml
  2. 1
      packages/nc-gui/components.d.ts
  3. 95
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  4. 37
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  5. 11
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  6. 15
      packages/nc-gui/composables/useKanbanViewStore.ts
  7. 26
      packages/nc-gui/composables/useSharedView.ts
  8. 10
      packages/nc-gui/composables/useViewData.ts
  9. 7
      packages/nc-gui/layouts/shared-view.vue
  10. 19
      packages/noco-docs/content/en/setup-and-usages/account-settings.md
  11. 18
      packages/nocodb-sdk/src/lib/Api.ts
  12. 4
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  13. 14
      packages/nocodb/src/lib/meta/api/columnApis.ts
  14. 12
      packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts
  15. 2
      packages/nocodb/src/lib/meta/api/publicApis/publicMetaApis.ts
  16. 36
      scripts/sdk/swagger.json
  17. 17
      tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts
  18. 7
      tests/playwright/tests/columnSingleSelect.spec.ts
  19. 26
      tests/playwright/tests/viewKanban.spec.ts

14
.github/workflows/release-nightly-dev.yml

@ -49,13 +49,13 @@ jobs:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
# Build executables and publish to GitHub
release-executables:
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-timely-executables.yml
with:
tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }}
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# release-executables:
# needs: [set-tag, release-npm]
# uses: ./.github/workflows/release-timely-executables.yml
# with:
# tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }}
# secrets:
# NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# Build docker image and push to docker hub
release-docker:

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

@ -124,6 +124,7 @@ declare module '@vue/runtime-core' {
MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiArrowULeftBottom: typeof import('~icons/mdi/arrow-u-left-bottom')['default']
MdiAt: typeof import('~icons/mdi/at')['default']
MdiBackburger: typeof import('~icons/mdi/backburger')['default']
MdiBookOpenOutline: typeof import('~icons/mdi/book-open-outline')['default']

95
packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -3,6 +3,14 @@ import Draggable from 'vuedraggable'
import { UITypes } from 'nocodb-sdk'
import { IsKanbanInj, enumColor, onMounted, useColumnCreateStoreOrThrow, useVModel, watch } from '#imports'
interface Option {
color: string
title: string
id?: string
fk_colum_id?: string
order?: number
}
const props = defineProps<{
value: any
}>()
@ -13,7 +21,10 @@ const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, isPg, isMysql } = useColumnCreateStoreOrThrow()
let options = $ref<any[]>([])
let options = $ref<Option[]>([])
let renderedOptions = $ref<(Option & { status?: 'remove' })[]>([])
let savedDefaultOption = $ref<Option | null>(null)
let savedCdf = $ref<string | null>(null)
const colorMenus = $ref<any>({})
@ -58,6 +69,9 @@ onMounted(() => {
}
}
options = vModel.value.colOptions.options
renderedOptions = [...options]
// Support for older options
for (const op of options.filter((el) => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')
@ -87,13 +101,6 @@ const optionChanged = (changedId: string) => {
}
}
const optionDropped = (changedId: string) => {
if (changedId && changedId === defaultOption.value?.id) {
vModel.value.cdf = null
defaultOption.value = null
}
}
const getNextColor = () => {
let tempColor = colors[0]
if (options.length && options[options.length - 1].color) {
@ -108,13 +115,40 @@ const addNewOption = () => {
title: '',
color: getNextColor(),
}
renderedOptions.push(tempOption)
options.push(tempOption)
}
const removeOption = (index: number) => {
const optionId = options[index]?.id
options.splice(index, 1)
optionDropped(optionId)
const syncOptions = () => {
vModel.value.colOptions.options = renderedOptions.filter((op) => op.status !== 'remove')
}
const removeRenderedOption = (index: number) => {
renderedOptions[index].status = 'remove'
syncOptions()
const optionId = renderedOptions[index]?.id
if (optionId === defaultOption.value?.id) {
savedDefaultOption = { ...defaultOption.value }
savedCdf = vModel.value.cdf
defaultOption.value = null
vModel.value.cdf = null
}
}
const undoRemoveRenderedOption = (index: number) => {
renderedOptions[index].status = undefined
syncOptions()
const optionId = renderedOptions[index]?.id
if (optionId === savedDefaultOption?.id) {
defaultOption.value = { ...savedDefaultOption }
vModel.value.cdf = savedCdf
savedDefaultOption = null
savedCdf = null
}
}
// focus last created input
@ -128,9 +162,14 @@ watch(inputs, () => {
<template>
<div class="w-full">
<div class="max-h-[250px] overflow-x-auto scrollbar-thin-dull pr-3">
<Draggable :list="options" item-key="id" handle=".nc-child-draggable-icon">
<Draggable :list="renderedOptions" item-key="id" handle=".nc-child-draggable-icon" @change="syncOptions">
<template #item="{ element, index }">
<div class="flex py-1 items-center nc-select-option">
<div class="flex p-1 items-center nc-select-option">
<div
class="flex items-center w-full"
:data-testid="`select-column-option-${index}`"
:class="{ removed: element.status === 'remove' }"
>
<MdiDragVertical
v-if="!isKanban"
small
@ -161,14 +200,24 @@ watch(inputs, () => {
v-model:value="element.title"
class="caption"
:data-testid="`select-column-option-input-${index}`"
:disabled="element.status === 'remove'"
@keydown.enter.prevent="element.title?.trim() && addNewOption()"
@change="optionChanged(element.id)"
/>
</div>
<MdiClose
v-if="element.status !== 'remove'"
class="ml-2 hover:!text-black-500 text-gray-500 cursor-pointer"
:data-testid="`select-column-option-remove-${index}`"
@click="removeOption(index)"
@click="removeRenderedOption(index)"
/>
<MdiArrowULeftBottom
v-else
class="ml-2 hover:!text-black-500 text-gray-500 cursor-pointer"
:data-testid="`select-column-option-remove-undo-${index}`"
@click="undoRemoveRenderedOption(index)"
/>
</div>
</template>
@ -186,3 +235,19 @@ watch(inputs, () => {
</a-button>
</div>
</template>
<style scoped>
.removed {
position: relative;
}
.removed:after {
position: absolute;
left: 0;
top: 50%;
height: 1px;
background: #ccc;
content: '';
width: calc(100% + 5px);
display: block;
}
</style>

37
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -84,11 +84,31 @@ const onMove = (_event: { moved: { newIndex: number } }) => {
$e('a:fields:reorder')
}
const coverOptions = computed<SelectProps['options']>(() => {
const filterFields =
fields.value
?.filter((el) => el.fk_column_id && metaColumnById.value[el.fk_column_id].uidt === UITypes.Attachment)
.map((field) => {
return {
value: field.fk_column_id,
label: field.title,
}
}) ?? []
return [{ value: null, label: 'No Image' }, ...filterFields]
})
const coverImageColumnId = computed({
get: () =>
get: () => {
const fk_cover_image_col_id =
(activeView.value?.type === ViewTypes.GALLERY || activeView.value?.type === ViewTypes.KANBAN) && activeView.value?.view
? (activeView.value?.view as GalleryType).fk_cover_image_col_id
: undefined,
: undefined
// check if `fk_cover_image_col_id` is in `coverOptions`
// e.g. in share view, users may not share the cover image column
if (coverOptions.value?.find((o) => o.value === fk_cover_image_col_id)) return fk_cover_image_col_id
// set to `No Image`
return null
},
set: async (val) => {
if (
(activeView.value?.type === ViewTypes.GALLERY || activeView.value?.type === ViewTypes.KANBAN) &&
@ -113,19 +133,6 @@ const coverImageColumnId = computed({
},
})
const coverOptions = computed<SelectProps['options']>(() => {
const filterFields =
fields.value
?.filter((el) => el.fk_column_id && metaColumnById.value[el.fk_column_id].uidt === UITypes.Attachment)
.map((field) => {
return {
value: field.fk_column_id,
label: field.title,
}
}) ?? []
return [{ value: null, label: 'No Image' }, ...filterFields]
})
const getIcon = (c: ColumnType) =>
h(isVirtualCol(c) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), {
columnMeta: c,

11
packages/nc-gui/components/smartsheet/toolbar/ShareView.vue

@ -86,10 +86,17 @@ const genShareLink = async () => {
if (!view.value?.id) return
const response = (await $api.dbViewShare.create(view.value.id)) as SharedView
const meta = isString(response.meta) ? JSON.parse(response.meta) : response.meta
shared.value = { ...response, meta }
if (shared.value.type === ViewTypes.KANBAN) {
const { groupingFieldColumn } = useKanbanViewStoreOrThrow()
shared.value.meta = { ...shared.value.meta, groupingFieldColumn: groupingFieldColumn.value }
await updateSharedViewMeta(true)
}
passwordProtected.value = !!shared.value.password && shared.value.password !== ''
showShareModel = true
@ -136,7 +143,7 @@ async function saveTheme() {
// const saveTransitionDuration = useDebounceFn(updateSharedViewMeta, 1000, { maxWait: 2000 })
async function updateSharedViewMeta() {
async function updateSharedViewMeta(silentMessage = false) {
try {
const meta = shared.value.meta && isString(shared.value.meta) ? JSON.parse(shared.value.meta) : shared.value.meta
@ -144,7 +151,7 @@ async function updateSharedViewMeta() {
meta,
})
message.success(t('msg.success.updated'))
if (!silentMessage) message.success(t('msg.success.updated'))
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}

15
packages/nc-gui/composables/useKanbanViewStore.ts

@ -112,7 +112,10 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
let res
if (isPublic.value) {
res = await fetchSharedViewGroupedData(groupingFieldColumn!.value!.id!)
res = await fetchSharedViewGroupedData(groupingFieldColumn!.value!.id!, {
sortsArr: sorts.value,
filtersArr: nestedFilters.value,
})
} else {
res = await api.dbViewRow.groupedDataList(
'noco',
@ -138,6 +141,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
if (stackTitle === null) {
where = `(${groupingField.value},is,null)`
}
const response = !isPublic.value
? await api.dbViewRow.list('noco', project.value.id!, meta.value!.id!, viewMeta.value!.id!, {
...params,
@ -145,7 +149,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
where,
})
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value })
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value, offset: params.offset })
formattedData.value.set(stackTitle, [...formattedData.value.get(stackTitle)!, ...formatData(response.list)])
}
@ -155,9 +159,12 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
kanbanMetaData.value = isPublic.value
? (sharedView.value?.view as KanbanType)
: await $api.dbView.kanbanRead(viewMeta.value.id)
// set groupingField
groupingFieldColumn.value =
(meta.value.columns as ColumnType[]).filter((f) => f.id === kanbanMetaData.value.fk_grp_col_id)[0] || {}
groupingFieldColumn.value = !isPublic.value
? (meta.value.columns as ColumnType[]).filter((f) => f.id === kanbanMetaData.value.fk_grp_col_id)[0] || {}
: ((typeof sharedView.value?.meta === 'string' ? JSON.parse(sharedView.value?.meta) : sharedView.value?.meta)
.groupingFieldColumn! as ColumnType)
groupingField.value = groupingFieldColumn.value.title!

26
packages/nc-gui/composables/useSharedView.ts

@ -70,6 +70,7 @@ export function useSharedView() {
meta.value = { ...viewMeta.model }
let order = 1
meta.value!.columns = [...viewMeta.model.columns]
.filter((c) => c.show)
.map((c) => ({ ...c, order: order++ }))
@ -92,16 +93,27 @@ export function useSharedView() {
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))
}
const fetchSharedViewData = async ({ sortsArr, filtersArr }: { sortsArr: SortType[]; filtersArr: FilterType[] }) => {
const fetchSharedViewData = async ({
sortsArr,
filtersArr,
offset,
}: {
sortsArr: SortType[]
filtersArr: FilterType[]
offset?: number
}) => {
if (!sharedView.value) return
if (!offset) {
const page = paginationData.value.page || 1
const pageSize = paginationData.value.pageSize || appInfoDefaultLimit
offset = (page - 1) * pageSize
}
const { data } = await $api.public.dataList(
sharedView.value.uuid!,
{
offset: (page - 1) * pageSize,
offset,
filterArrJson: JSON.stringify(filtersArr ?? nestedFilters.value),
sortArrJson: JSON.stringify(sortsArr ?? sorts.value),
} as any,
@ -114,7 +126,10 @@ export function useSharedView() {
return data
}
const fetchSharedViewGroupedData = async (columnId: string, params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) => {
const fetchSharedViewGroupedData = async (
columnId: string,
{ sortsArr, filtersArr }: { sortsArr: SortType[]; filtersArr: FilterType[] },
) => {
if (!sharedView.value) return
const page = paginationData.value.page || 1
@ -125,9 +140,8 @@ export function useSharedView() {
columnId,
{
offset: (page - 1) * pageSize,
filterArrJson: JSON.stringify(nestedFilters.value),
sortArrJson: JSON.stringify(sorts.value),
...params,
filterArrJson: JSON.stringify(filtersArr ?? nestedFilters.value),
sortArrJson: JSON.stringify(sortsArr ?? sorts.value),
} as any,
{
headers: {

10
packages/nc-gui/composables/useViewData.ts

@ -49,7 +49,9 @@ export function useViewData(
const { getMeta } = useMetas()
const appInfoDefaultLimit = appInfo.defaultLimit || 25
const _paginationData = ref<PaginatedType>({ page: 1, pageSize: appInfoDefaultLimit })
const aggCommentCount = ref<{ row_id: string; count: number }[]>([])
const galleryData = ref<GalleryType>()
@ -64,7 +66,7 @@ export function useViewData(
const { project, isSharedBase } = useProject()
const { fetchSharedViewData, paginationData: sharedPaginationData } = useSharedView()
const { sharedView, fetchSharedViewData, paginationData: sharedPaginationData } = useSharedView()
const { $api, $e } = useNuxtApp()
@ -203,8 +205,10 @@ export function useViewData(
}
async function loadGalleryData() {
if (!viewMeta?.value?.id || isPublic.value) return
galleryData.value = await $api.dbView.galleryRead(viewMeta.value.id)
if (!viewMeta?.value?.id) return
galleryData.value = isPublic.value
? (sharedView.value?.view as GalleryType)
: await $api.dbView.galleryRead(viewMeta.value.id)
}
async function insertRow(

7
packages/nc-gui/layouts/shared-view.vue

@ -1,7 +1,10 @@
<script lang="ts" setup>
import { navigateTo, useEventListener, useRouter } from '#imports'
const { isLoading, currentVersion } = useGlobal()
const { isLoading, appInfo } = useGlobal()
const { sharedView } = useSharedView()
const router = useRouter()
onMounted(() => {
@ -43,7 +46,7 @@ export default {
<div class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105" @click="navigateTo('/')">
<a-tooltip placement="bottom">
<template #title>
{{ currentVersion }}
{{ appInfo.version }}
</template>
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</a-tooltip>

19
packages/noco-docs/content/en/setup-and-usages/account-settings.md

@ -27,14 +27,25 @@ If you are a super admin, you can also manage all user roles in organization lev
## User Management
Super-admin has new privelege to do user management at root-level.
Permissions within NocoDB are divided into two levels: Organisation level and Project level.
- `org-level-creator` - this user can create a new project and access any invited project.
- `org-level-viewer` - this user can't create a new project but they can access any invited project.
### Organisation Level Permissions:
- `Org Level Creator`: Allows users to create new projects and access invited projects.
- `Org Level Viewer`: Allows users to access invited projects but does not permit the creation of new projects.
![image](https://user-images.githubusercontent.com/35857179/203261168-5ba75f9c-476e-4fe7-ace4-f81051f42773.png)
### Project Level Permissions:
- `Owner`: The user who created the project. A project can have only one owner. The owner persists until the project exists and the role is non-transferable. The owner has access to carry out any operations within the project, including deleting it.
- `Creator`: Has access to carry out any operations within the project except deleting the project and removing the "Owner."
- `Editor`: Can modify data but cannot modify the schema (add/remove columns, tables, users, and such).
- `Commenter`: Can neither modify data nor schema, can only see data and can mark row-level comments.
- `Viewer`: Can only see data.
Additional access details for project level permissions can be found [here](https://docs.nocodb.com/setup-and-usages/team-and-auth#advanced-options--configurations).
Please note that the above-mentioned Project Level Permissions are additional to the already defined Organisation Level Permissions.
In addition to the previously defined permissions, NocoDB also includes the role of "Super Admin." The "Super Admin" is the first user to sign up on this NocoDB installation. An organisation can have only one "Super Admin" and this role is non-transferable. The "Super Admin" will have the equivalent permissions of an "Org Level Creator" and "Owner" for all projects within the organisation.
## Enable / Disable Signup
Signup without an invitation is disabled by default and can be managed from UI by a super admin.

18
packages/nocodb-sdk/src/lib/Api.ts

@ -2292,6 +2292,7 @@ export class Api<
*
* @tags DB table column
* @name Delete
* @summary Column Delete
* @request DELETE:/api/v1/db/meta/columns/{columnId}
* @response `200` `void` OK
*/
@ -2302,6 +2303,22 @@ export class Api<
...params,
}),
/**
* No description
*
* @tags DB Table Column
* @name Get
* @summary Column Get
* @request GET:/api/v1/db/meta/columns/{columnId}
* @response `200` `void` OK
*/
get: (columnId: string, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/v1/db/meta/columns/${columnId}`,
method: 'GET',
...params,
}),
/**
* No description
*
@ -3655,6 +3672,7 @@ export class Api<
where?: string;
/** Query params for nested data */
nested?: any;
offset?: number;
},
params: RequestParams = {}
) =>

4
packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts

@ -384,7 +384,7 @@ export class MssqlUi {
return '';
case 'varchar':
return '';
return 255;
default:
return '';
@ -1098,7 +1098,7 @@ export class MssqlUi {
colProp.dt = 'varchar';
break;
case 'Date':
colProp.dt = 'varchar';
colProp.dt = 'date';
break;
case 'Year':

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

@ -101,6 +101,10 @@ async function createHmAndBtColumn(
}
}
export async function columnGet(req: Request, res: Response) {
res.json(await Column.get({ colId: req.params.columnId }));
}
export async function columnAdd(
req: Request<any, any, ColumnReqType & { uidt: UITypes }>,
res: Response<TableType>
@ -1811,21 +1815,31 @@ async function createColumnIndex({
}
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/tables/:tableId/columns/',
metaApiMetrics,
ncMetaAclMw(columnAdd, 'columnAdd')
);
router.patch(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnUpdate, 'columnUpdate')
);
router.delete(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnDelete, 'columnDelete')
);
router.get(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnGet, 'columnGet')
);
router.post(
'/api/v1/db/meta/columns/:columnId/primary',
metaApiMetrics,

12
packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts

@ -28,16 +28,26 @@ async function exportExcel(req: Request, res: Response) {
}
const model = await view.getModelWithInfo();
await view.getColumns();
const { offset, dbRows, elapsed } = await getDbRows(model, view, req);
const fields = req.query.fields as string[];
const data = XLSX.utils.json_to_sheet(dbRows, { header: fields });
const data = XLSX.utils.json_to_sheet(
dbRows.map((o: Record<string, any>) =>
Object.fromEntries(fields.map((f) => [f, o[f]]))
),
{ header: fields }
);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, data, view.title);
const buf = XLSX.write(wb, { type: 'base64', bookType: 'xlsx' });
res.set({
'Access-Control-Expose-Headers': 'nc-export-offset',
'nc-export-offset': offset,

2
packages/nocodb/src/lib/meta/api/publicApis/publicMetaApis.ts

@ -39,8 +39,6 @@ export async function viewMetaGet(req: Request, res: Response) {
// todo: return only required props
delete view['password'];
// const columnsById = c;
view.model.columns = view.columns
.filter((c) => {
const column = view.model.columnsById[c.fk_column_id];

36
scripts/sdk/swagger.json

@ -1972,8 +1972,7 @@
"project_id": {
"type": "string"
},
"meta": {
}
"meta": {}
}
}
}
@ -2105,7 +2104,7 @@
]
},
"delete": {
"summary": "",
"summary": "Column Delete",
"operationId": "db-table-column-delete",
"responses": {
"200": {
@ -2115,6 +2114,18 @@
"tags": [
"DB table column"
]
},
"get": {
"summary": "Column Get",
"operationId": "db-table-column-get",
"responses": {
"200": {
"description": "OK"
}
},
"tags": [
"DB Table Column"
]
}
},
"/api/v1/db/meta/columns/{columnId}/primary": {
@ -2194,8 +2205,7 @@
"order": {
"type": "number"
},
"meta": {
},
"meta": {},
"title": {
"type": "string"
},
@ -3970,6 +3980,13 @@
"in": "query",
"name": "nested",
"description": "Query params for nested data"
},
{
"schema": {
"type": "number"
},
"in": "query",
"name": "offset"
}
],
"responses": {
@ -7585,8 +7602,7 @@
"number"
]
},
"meta": {
}
"meta": {}
},
"required": [
"table_name",
@ -7684,8 +7700,7 @@
"uuid": {
"type": "string"
},
"meta": {
},
"meta": {},
"show_system_fields": {
"type": "boolean"
},
@ -7931,8 +7946,7 @@
"$ref": "#/components/schemas/Column"
}
},
"meta": {
}
"meta": {}
},
"required": [
"table_name",

17
tests/playwright/pages/Dashboard/Grid/Column/SelectOptionColumn.ts

@ -1,5 +1,6 @@
import { ColumnPageObject } from '.';
import BasePage from '../../../Base';
import { expect } from '@playwright/test';
export class SelectOptionColumnPageObject extends BasePage {
readonly column: ColumnPageObject;
@ -49,6 +50,22 @@ export class SelectOptionColumnPageObject extends BasePage {
await this.column.get().locator(`svg[data-testid="select-column-option-remove-${index}"]`).click();
await expect(this.column.get().getByTestId(`select-column-option-${index}`)).toHaveClass(/removed/);
await this.column.save({ isUpdated: true });
}
async deleteOptionWithUndo({ columnTitle, index }: { index: number; columnTitle: string }) {
await this.column.openEdit({ title: columnTitle });
await this.column.get().locator(`svg[data-testid="select-column-option-remove-${index}"]`).click();
await expect(this.column.get().getByTestId(`select-column-option-${index}`)).toHaveClass(/removed/);
await this.column.get().locator(`svg[data-testid="select-column-option-remove-undo-${index}"]`).click();
await expect(this.column.get().getByTestId(`select-column-option-${index}`)).not.toHaveClass(/removed/);
await this.column.save({ isUpdated: true });
}

7
tests/playwright/tests/columnSingleSelect.spec.ts

@ -53,6 +53,13 @@ test.describe('Single select', () => {
await grid.cell.selectOption.select({ index: 0, columnHeader: 'SingleSelect', option: 'Option 3' });
await grid.cell.selectOption.verify({ index: 0, columnHeader: 'SingleSelect', option: 'Option 3' });
await grid.column.selectOption.deleteOptionWithUndo({ index: 0, columnTitle: 'SingleSelect' });
await grid.cell.selectOption.verifyOptions({
index: 0,
columnHeader: 'SingleSelect',
options: ['Option 1', 'Option 2', 'Option 3'],
});
await grid.column.selectOption.deleteOption({ index: 2, columnTitle: 'SingleSelect' });
await grid.cell.selectOption.verifyNoOptionsSelected({ index: 0, columnHeader: 'SingleSelect' });

26
tests/playwright/tests/viewKanban.spec.ts

@ -301,4 +301,30 @@ test.describe('View', () => {
count: [1, 25, 25, 25, 25, 25],
});
});
test('Kanban shared view operations', async ({ page }) => {
test.slow();
await dashboard.viewSidebar.createKanbanView({
title: 'Film Kanban',
});
await dashboard.viewSidebar.verifyView({
title: 'Film Kanban',
index: 1,
});
// Share view
await toolbar.fields.toggle({ title: 'Rating' });
await toolbar.clickShareView();
const sharedLink = await toolbar.shareView.getShareLink();
await toolbar.shareView.close();
// sign-out
await dashboard.signOut();
// Open shared view & verify stack count
await page.goto(sharedLink);
const kanban = dashboard.kanban;
await kanban.verifyStackCount({ count: 6 });
});
});

Loading…
Cancel
Save