mirror of https://github.com/nocodb/nocodb
Raju Udava
11 months ago
committed by
GitHub
78 changed files with 3398 additions and 642 deletions
@ -0,0 +1,435 @@
|
||||
<script lang="ts" setup> |
||||
import { onUnmounted } from '@vue/runtime-core' |
||||
import tinycolor from 'tinycolor2' |
||||
import type { Select as AntSelect } from 'ant-design-vue' |
||||
import type { UserFieldRecordType } from 'nocodb-sdk' |
||||
import { |
||||
ActiveCellInj, |
||||
CellClickHookInj, |
||||
ColumnInj, |
||||
EditColumnInj, |
||||
EditModeInj, |
||||
IsKanbanInj, |
||||
ReadonlyInj, |
||||
RowHeightInj, |
||||
computed, |
||||
h, |
||||
inject, |
||||
isDrawerOrModalExist, |
||||
onMounted, |
||||
ref, |
||||
useEventListener, |
||||
useRoles, |
||||
useSelectedCellKeyupListener, |
||||
watch, |
||||
} from '#imports' |
||||
import MdiCloseCircle from '~icons/mdi/close-circle' |
||||
|
||||
interface Props { |
||||
modelValue?: UserFieldRecordType[] | string | null |
||||
rowIndex?: number |
||||
location?: 'cell' | 'filter' |
||||
forceMulti?: boolean |
||||
} |
||||
|
||||
const { modelValue, forceMulti } = defineProps<Props>() |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
const { isMobileMode } = useGlobal() |
||||
|
||||
const meta = inject(MetaInj)! |
||||
|
||||
const column = inject(ColumnInj)! |
||||
|
||||
const readOnly = inject(ReadonlyInj)! |
||||
|
||||
const isEditable = inject(EditModeInj, ref(false)) |
||||
|
||||
const activeCell = inject(ActiveCellInj, ref(false)) |
||||
|
||||
const basesStore = useBases() |
||||
|
||||
const { basesUser } = storeToRefs(basesStore) |
||||
|
||||
const baseUsers = computed(() => (meta.value.base_id ? basesUser.value.get(meta.value.base_id) || [] : [])) |
||||
|
||||
// use both ActiveCellInj or EditModeInj to determine the active state |
||||
// since active will be false in case of form view |
||||
const active = computed(() => activeCell.value || isEditable.value) |
||||
|
||||
const isForm = inject(IsFormInj, ref(false)) |
||||
|
||||
const isEditColumn = inject(EditColumnInj, ref(false)) |
||||
|
||||
const isMultiple = computed(() => forceMulti || (column.value.meta as { is_multi: boolean; notify: boolean })?.is_multi) |
||||
|
||||
const rowHeight = inject(RowHeightInj, ref(undefined)) |
||||
|
||||
const aselect = ref<typeof AntSelect>() |
||||
|
||||
const isOpen = ref(false) |
||||
|
||||
const isKanban = inject(IsKanbanInj, ref(false)) |
||||
|
||||
const searchVal = ref<string | null>() |
||||
|
||||
const { isUIAllowed } = useRoles() |
||||
|
||||
const options = computed<UserFieldRecordType[]>(() => { |
||||
const collaborators: UserFieldRecordType[] = [] |
||||
|
||||
collaborators.push( |
||||
...(baseUsers.value?.map((user: any) => ({ |
||||
id: user.id, |
||||
email: user.email, |
||||
display_name: user.display_name, |
||||
deleted: user.deleted, |
||||
})) || []), |
||||
) |
||||
return collaborators |
||||
}) |
||||
|
||||
const hasEditRoles = computed(() => isUIAllowed('dataEdit')) |
||||
|
||||
const editAllowed = computed(() => (hasEditRoles.value || isForm.value) && active.value) |
||||
|
||||
const vModel = computed({ |
||||
get: () => { |
||||
let selected: { label: string; value: string }[] = [] |
||||
if (typeof modelValue === 'string') { |
||||
const idsOrMails = modelValue.split(',').map((idOrMail) => idOrMail.trim()) |
||||
selected = idsOrMails.reduce((acc, idOrMail) => { |
||||
const user = options.value.find((u) => u.id === idOrMail || u.email === idOrMail) |
||||
if (user) { |
||||
acc.push({ |
||||
label: user?.display_name || user?.email, |
||||
value: user.id, |
||||
}) |
||||
} |
||||
return acc |
||||
}, [] as { label: string; value: string }[]) |
||||
} else { |
||||
selected = |
||||
modelValue?.reduce((acc, item) => { |
||||
const label = item?.display_name || item?.email |
||||
if (label) { |
||||
acc.push({ |
||||
label, |
||||
value: item.id, |
||||
}) |
||||
} |
||||
return acc |
||||
}, [] as { label: string; value: string }[]) || [] |
||||
} |
||||
|
||||
return selected |
||||
}, |
||||
set: (val) => { |
||||
const value: string[] = [] |
||||
if (val && val.length) { |
||||
val.forEach((item) => { |
||||
// @ts-expect-error antd select returns string[] instead of { label: string, value: string }[] |
||||
const user = options.value.find((u) => u.id === item) |
||||
if (user) { |
||||
value.push(user.id) |
||||
} |
||||
}) |
||||
} |
||||
if (isMultiple.value) { |
||||
emit('update:modelValue', val?.length ? value.join(',') : null) |
||||
} else { |
||||
emit('update:modelValue', val?.length ? value[value.length - 1] : null) |
||||
isOpen.value = false |
||||
} |
||||
}, |
||||
}) |
||||
|
||||
watch(isOpen, (n, _o) => { |
||||
if (!n) searchVal.value = '' |
||||
|
||||
if (editAllowed.value) { |
||||
if (!n) { |
||||
aselect.value?.$el?.querySelector('input')?.blur() |
||||
} else { |
||||
aselect.value?.$el?.querySelector('input')?.focus() |
||||
} |
||||
} |
||||
}) |
||||
|
||||
// set isOpen to false when active cell is changed |
||||
watch(active, (n, _o) => { |
||||
if (!n) isOpen.value = false |
||||
}) |
||||
|
||||
useSelectedCellKeyupListener(activeCell, (e) => { |
||||
switch (e.key) { |
||||
case 'Escape': |
||||
isOpen.value = false |
||||
break |
||||
case 'Enter': |
||||
if (editAllowed.value && active.value && !isOpen.value) { |
||||
isOpen.value = true |
||||
} |
||||
break |
||||
// skip space bar key press since it's used for expand row |
||||
case ' ': |
||||
break |
||||
case 'ArrowUp': |
||||
case 'ArrowDown': |
||||
case 'ArrowRight': |
||||
case 'ArrowLeft': |
||||
case 'Delete': |
||||
// skip |
||||
break |
||||
default: |
||||
if (!editAllowed.value) { |
||||
e.preventDefault() |
||||
break |
||||
} |
||||
// toggle only if char key pressed |
||||
if (!(e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) && e.key?.length === 1 && !isDrawerOrModalExist()) { |
||||
e.stopPropagation() |
||||
isOpen.value = true |
||||
} |
||||
break |
||||
} |
||||
}) |
||||
|
||||
// close dropdown list on escape |
||||
useSelectedCellKeyupListener(isOpen, (e) => { |
||||
if (e.key === 'Escape') isOpen.value = false |
||||
}) |
||||
|
||||
const search = () => { |
||||
searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value |
||||
} |
||||
|
||||
const onTagClick = (e: Event, onClose: Function) => { |
||||
// check clicked element is remove icon |
||||
if ( |
||||
(e.target as HTMLElement)?.classList.contains('ant-tag-close-icon') || |
||||
(e.target as HTMLElement)?.closest('.ant-tag-close-icon') |
||||
) { |
||||
e.stopPropagation() |
||||
onClose() |
||||
} |
||||
} |
||||
|
||||
const cellClickHook = inject(CellClickHookInj, null) |
||||
|
||||
const toggleMenu = () => { |
||||
if (cellClickHook) return |
||||
isOpen.value = editAllowed.value && !isOpen.value |
||||
} |
||||
|
||||
const cellClickHookHandler = () => { |
||||
isOpen.value = editAllowed.value && !isOpen.value |
||||
} |
||||
onMounted(() => { |
||||
cellClickHook?.on(cellClickHookHandler) |
||||
}) |
||||
onUnmounted(() => { |
||||
cellClickHook?.on(cellClickHookHandler) |
||||
}) |
||||
|
||||
const handleClose = (e: MouseEvent) => { |
||||
// close dropdown if clicked outside of dropdown |
||||
if ( |
||||
isOpen.value && |
||||
aselect.value && |
||||
!aselect.value.$el.contains(e.target) && |
||||
!document.querySelector('.nc-dropdown-user-select-cell.active')?.contains(e.target as Node) |
||||
) { |
||||
// loose focus when clicked outside |
||||
isEditable.value = false |
||||
isOpen.value = false |
||||
} |
||||
} |
||||
|
||||
useEventListener(document, 'click', handleClose, true) |
||||
|
||||
// search with email |
||||
const filterOption = (input: string, option: any) => { |
||||
const opt = options.value.find((o) => o.id === option.value) |
||||
const searchVal = opt?.display_name || opt?.email |
||||
if (searchVal) { |
||||
return searchVal.toLowerCase().includes(input.toLowerCase()) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="nc-user-select h-full w-full flex items-center" :class="{ 'read-only': readOnly }" @click="toggleMenu"> |
||||
<div |
||||
v-if="!active" |
||||
class="flex flex-wrap" |
||||
:style="{ |
||||
'display': '-webkit-box', |
||||
'max-width': '100%', |
||||
'-webkit-line-clamp': rowHeight || 1, |
||||
'-webkit-box-orient': 'vertical', |
||||
'overflow': 'hidden', |
||||
}" |
||||
> |
||||
<template v-for="selectedOpt of vModel" :key="selectedOpt.value"> |
||||
<a-tag class="rounded-tag" color="'#ccc'"> |
||||
<span |
||||
:style="{ |
||||
'color': tinycolor.isReadable('#ccc' || '#ccc', '#fff', { level: 'AA', size: 'large' }) |
||||
? '#fff' |
||||
: tinycolor.mostReadable('#ccc' || '#ccc', ['#0b1d05', '#fff']).toHex8String(), |
||||
'font-size': '13px', |
||||
}" |
||||
:class="{ 'text-sm': isKanban }" |
||||
> |
||||
{{ selectedOpt.label }} |
||||
</span> |
||||
</a-tag> |
||||
</template> |
||||
</div> |
||||
|
||||
<a-select |
||||
v-else |
||||
ref="aselect" |
||||
v-model:value="vModel" |
||||
mode="multiple" |
||||
class="w-full overflow-hidden" |
||||
:placeholder="isEditColumn ? $t('labels.optional') : ''" |
||||
:bordered="false" |
||||
clear-icon |
||||
:show-search="!isMobileMode" |
||||
:show-arrow="editAllowed && !readOnly" |
||||
:open="isOpen && editAllowed" |
||||
:disabled="readOnly || !editAllowed" |
||||
:class="{ 'caret-transparent': !hasEditRoles }" |
||||
:dropdown-class-name="`nc-dropdown-user-select-cell ${isOpen ? 'active' : ''}`" |
||||
:filter-option="filterOption" |
||||
@search="search" |
||||
@keydown.stop |
||||
> |
||||
<template #suffixIcon> |
||||
<GeneralIcon icon="arrowDown" class="text-gray-700 nc-select-expand-btn" /> |
||||
</template> |
||||
<template v-for="op of options" :key="op.id || op.email"> |
||||
<a-select-option |
||||
v-if="!op.deleted" |
||||
:value="op.id" |
||||
:data-testid="`select-option-${column.title}-${location === 'filter' ? 'filter' : rowIndex}`" |
||||
:class="`nc-select-option-${column.title}-${op.email}`" |
||||
@click.stop |
||||
> |
||||
<a-tag class="rounded-tag" color="'#ccc'"> |
||||
<span |
||||
:style="{ |
||||
'color': tinycolor.isReadable('#ccc' || '#ccc', '#fff', { level: 'AA', size: 'large' }) |
||||
? '#fff' |
||||
: tinycolor.mostReadable('#ccc' || '#ccc', ['#0b1d05', '#fff']).toHex8String(), |
||||
'font-size': '13px', |
||||
}" |
||||
:class="{ 'text-sm': isKanban }" |
||||
> |
||||
{{ op.display_name?.length ? op.display_name : op.email }} |
||||
</span> |
||||
</a-tag> |
||||
</a-select-option> |
||||
</template> |
||||
|
||||
<template #tagRender="{ label, value: val, onClose }"> |
||||
<a-tag |
||||
v-if="options.find((el) => el.id === val)" |
||||
class="rounded-tag nc-selected-option" |
||||
:style="{ display: 'flex', alignItems: 'center' }" |
||||
color="'#ccc'" |
||||
:closable="editAllowed && ((vModel?.length ?? 0) > 1 || !column?.rqd)" |
||||
:close-icon="h(MdiCloseCircle, { class: ['ms-close-icon'] })" |
||||
@click="onTagClick($event, onClose)" |
||||
@close="onClose" |
||||
> |
||||
<span |
||||
:style="{ |
||||
'color': tinycolor.isReadable('#ccc' || '#ccc', '#fff', { |
||||
level: 'AA', |
||||
size: 'large', |
||||
}) |
||||
? '#fff' |
||||
: tinycolor.mostReadable('#ccc' || '#ccc', ['#0b1d05', '#fff']).toHex8String(), |
||||
'font-size': '13px', |
||||
}" |
||||
:class="{ 'text-sm': isKanban }" |
||||
> |
||||
{{ label }} |
||||
</span> |
||||
</a-tag> |
||||
</template> |
||||
</a-select> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
.ms-close-icon { |
||||
color: rgba(0, 0, 0, 0.25); |
||||
cursor: pointer; |
||||
display: flex; |
||||
font-size: 12px; |
||||
font-style: normal; |
||||
height: 12px; |
||||
line-height: 1; |
||||
text-align: center; |
||||
text-transform: none; |
||||
transition: color 0.3s ease, opacity 0.15s ease; |
||||
width: 12px; |
||||
z-index: 1; |
||||
margin-right: -6px; |
||||
margin-left: 3px; |
||||
} |
||||
|
||||
.ms-close-icon:before { |
||||
display: block; |
||||
} |
||||
|
||||
.ms-close-icon:hover { |
||||
color: rgba(0, 0, 0, 0.45); |
||||
} |
||||
|
||||
.read-only { |
||||
.ms-close-icon { |
||||
display: none; |
||||
} |
||||
} |
||||
|
||||
.rounded-tag { |
||||
@apply bg-gray-200 py-0 px-[12px] rounded-[12px]; |
||||
} |
||||
|
||||
:deep(.ant-tag) { |
||||
@apply "rounded-tag" my-[2px]; |
||||
} |
||||
|
||||
:deep(.ant-tag-close-icon) { |
||||
@apply "text-slate-500"; |
||||
} |
||||
|
||||
:deep(.ant-select-selection-overflow-item) { |
||||
@apply "flex overflow-hidden"; |
||||
} |
||||
|
||||
:deep(.ant-select-selection-overflow) { |
||||
@apply flex-nowrap overflow-hidden; |
||||
} |
||||
|
||||
.nc-user-select:not(.read-only) { |
||||
:deep(.ant-select-selector), |
||||
:deep(.ant-select-selector input) { |
||||
@apply "!cursor-pointer"; |
||||
} |
||||
} |
||||
|
||||
:deep(.ant-select-selector) { |
||||
@apply !px-0; |
||||
} |
||||
|
||||
:deep(.ant-select-selection-search-input) { |
||||
@apply !text-xs; |
||||
} |
||||
</style> |
@ -0,0 +1,66 @@
|
||||
<script setup lang="ts"> |
||||
import { useVModel } from '#imports' |
||||
|
||||
const props = defineProps<{ |
||||
value: any |
||||
isEdit: boolean |
||||
}>() |
||||
|
||||
const emit = defineEmits(['update:value']) |
||||
|
||||
const vModel = useVModel(props, 'value', emit) |
||||
|
||||
const future = ref(false) |
||||
|
||||
const initialIsMulti = ref() |
||||
|
||||
const validators = {} |
||||
|
||||
const { setAdditionalValidations } = useColumnCreateStoreOrThrow() |
||||
|
||||
setAdditionalValidations({ |
||||
...validators, |
||||
}) |
||||
|
||||
// set default value |
||||
vModel.value.meta = { |
||||
is_multi: false, |
||||
notify: false, |
||||
...vModel.value.meta, |
||||
} |
||||
|
||||
onMounted(() => { |
||||
initialIsMulti.value = vModel.value.meta.is_multi |
||||
}) |
||||
|
||||
const updateIsMulti = (e) => { |
||||
vModel.value.meta.is_multi = e.target.checked |
||||
if (!vModel.value.meta.is_multi) { |
||||
vModel.value.cdf = vModel.value.cdf?.split(',')[0] || null |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex flex-col"> |
||||
<div> |
||||
<a-checkbox |
||||
v-if="vModel.meta" |
||||
:checked="vModel.meta.is_multi" |
||||
class="ml-1 mb-1" |
||||
data-testid="user-column-allow-multiple" |
||||
@change="updateIsMulti" |
||||
> |
||||
<span class="text-[10px] text-gray-600">Allow adding multiple users</span> |
||||
</a-checkbox> |
||||
</div> |
||||
<div v-if="future"> |
||||
<a-checkbox v-if="vModel.meta" v-model:checked="vModel.meta.notify" class="ml-1 mb-1"> |
||||
<span class="text-[10px] text-gray-600">Notify users with base access when they're added</span> |
||||
</a-checkbox> |
||||
</div> |
||||
<div v-if="initialIsMulti && isEdit && !vModel.meta.is_multi" class="text-error text-[10px] mb-1 mt-2"> |
||||
<span>Changing from multiple mode to single will retain only first user in each cell!!!</span> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -0,0 +1,117 @@
|
||||
import { ColumnPageObject } from '.'; |
||||
import BasePage from '../../../Base'; |
||||
import { expect } from '@playwright/test'; |
||||
|
||||
export class UserOptionColumnPageObject extends BasePage { |
||||
readonly column: ColumnPageObject; |
||||
|
||||
constructor(column: ColumnPageObject) { |
||||
super(column.rootPage); |
||||
this.column = column; |
||||
} |
||||
|
||||
get() { |
||||
return this.column.get(); |
||||
} |
||||
|
||||
async allowMultipleUser({ |
||||
columnTitle, |
||||
allowMultiple = false, |
||||
}: { |
||||
columnTitle: string; |
||||
allowMultiple?: boolean; |
||||
}): Promise<void> { |
||||
await this.column.openEdit({ title: columnTitle }); |
||||
const checkbox = this.get().getByTestId('user-column-allow-multiple'); |
||||
const isChecked = await checkbox.isChecked(); |
||||
|
||||
if ((isChecked && !allowMultiple) || (!isChecked && allowMultiple)) { |
||||
await checkbox.click(); |
||||
} |
||||
await this.column.save({ isUpdated: true }); |
||||
} |
||||
|
||||
async selectDefaultValueOption({ |
||||
columnTitle, |
||||
option, |
||||
multiSelect, |
||||
}: { |
||||
columnTitle: string; |
||||
option: string | string[]; |
||||
multiSelect?: boolean; |
||||
}): Promise<void> { |
||||
// Verify allow multiple checkbox before selecting default value
|
||||
await this.allowMultipleUser({ columnTitle, allowMultiple: multiSelect }); |
||||
|
||||
await this.column.openEdit({ title: columnTitle }); |
||||
|
||||
// Clear previous default value
|
||||
await this.clearDefaultValue(); |
||||
|
||||
const selector = this.column.get().locator('.nc-user-select >> .ant-select-selector'); |
||||
await selector.click(); |
||||
|
||||
if (multiSelect) { |
||||
const optionsToSelect = Array.isArray(option) ? option : [option]; |
||||
|
||||
for (const op of optionsToSelect) { |
||||
await this.selectOption({ option: op }); |
||||
} |
||||
} else if (!Array.isArray(option)) { |
||||
await this.selectOption({ option }); |
||||
} |
||||
|
||||
// Press `Escape` to close the dropdown
|
||||
await this.rootPage.keyboard.press('Escape'); |
||||
await this.rootPage.locator('.nc-dropdown-user-select-cell').waitFor({ state: 'hidden' }); |
||||
|
||||
await this.column.save({ isUpdated: true }); |
||||
} |
||||
|
||||
async selectOption({ option }: { option: string }) { |
||||
await this.get().locator('.ant-select-selection-search-input[aria-expanded="true"]').waitFor(); |
||||
await this.get().locator('.ant-select-selection-search-input[aria-expanded="true"]').fill(option); |
||||
|
||||
// Select user option
|
||||
await this.rootPage.locator('.rc-virtual-list-holder-inner > div').locator(`text="${option}"`).click(); |
||||
} |
||||
|
||||
async clearDefaultValue(): Promise<void> { |
||||
await this.column.get().locator('.nc-cell-user + svg.nc-icon').click(); |
||||
} |
||||
|
||||
async verifyDefaultValueOptionCount({ |
||||
columnTitle, |
||||
totalCount, |
||||
}: { |
||||
columnTitle: string; |
||||
totalCount: number; |
||||
}): Promise<void> { |
||||
await this.column.openEdit({ title: columnTitle }); |
||||
|
||||
await this.column.get().locator('.nc-cell-user > .nc-user-select').click(); |
||||
|
||||
expect(await this.rootPage.getByTestId(`select-option-${columnTitle}-undefined`).count()).toEqual(totalCount); |
||||
await this.column.get().locator('.nc-cell-user').click(); |
||||
|
||||
// Press `Cancel` to close edit modal
|
||||
await this.column.get().locator('button:has-text("Cancel")').click(); |
||||
await this.get().waitFor({ state: 'hidden' }); |
||||
} |
||||
|
||||
async verifySelectedOptions({ options, columnHeader }: { columnHeader: string; options: string[] }) { |
||||
await this.column.openEdit({ title: columnHeader }); |
||||
|
||||
const defaultValueSelector = this.get().locator('.nc-user-select >> .ant-select-selector'); |
||||
|
||||
let counter = 0; |
||||
for (const option of options) { |
||||
await expect(defaultValueSelector.locator(`.nc-selected-option`).nth(counter)).toHaveText(option); |
||||
counter++; |
||||
} |
||||
|
||||
// Press `Cancel` to close edit modal
|
||||
await this.column.get().locator('button:has-text("Cancel")').click(); |
||||
await this.get().waitFor({ state: 'hidden' }); |
||||
} |
||||
} |
@ -0,0 +1,154 @@
|
||||
import { expect } from '@playwright/test'; |
||||
import { CellPageObject } from '.'; |
||||
import BasePage from '../../../Base'; |
||||
|
||||
export class UserOptionCellPageObject extends BasePage { |
||||
readonly cell: CellPageObject; |
||||
|
||||
constructor(cell: CellPageObject) { |
||||
super(cell.rootPage); |
||||
this.cell = cell; |
||||
} |
||||
|
||||
get({ index, columnHeader }: { index: number; columnHeader: string }) { |
||||
return this.cell.get({ index, columnHeader }); |
||||
} |
||||
|
||||
async select({ |
||||
index, |
||||
columnHeader, |
||||
option, |
||||
multiSelect, |
||||
}: { |
||||
index: number; |
||||
columnHeader: string; |
||||
option: string; |
||||
multiSelect?: boolean; |
||||
}) { |
||||
const selectCell = this.get({ index, columnHeader }); |
||||
|
||||
// check if cell active
|
||||
if ( |
||||
!(await selectCell.getAttribute('class')).includes('active') && |
||||
(await selectCell.locator('.nc-selected-option').count()) === 0 |
||||
) { |
||||
await selectCell.click(); |
||||
} |
||||
|
||||
await selectCell.click(); |
||||
|
||||
if (index === -1) |
||||
await this.rootPage.getByTestId(`select-option-${columnHeader}-undefined`).getByText(option).click(); |
||||
else await this.rootPage.getByTestId(`select-option-${columnHeader}-${index}`).getByText(option).click(); |
||||
|
||||
if (multiSelect) await this.get({ index, columnHeader }).click(); |
||||
|
||||
await this.rootPage |
||||
.getByTestId(`select-option-${columnHeader}-${index}`) |
||||
.getByText(option) |
||||
.waitFor({ state: 'hidden' }); |
||||
} |
||||
|
||||
async clear({ index, columnHeader, multiSelect }: { index: number; columnHeader: string; multiSelect?: boolean }) { |
||||
if (multiSelect) { |
||||
await this.cell.get({ index, columnHeader }).click(); |
||||
await this.cell.get({ index, columnHeader }).click(); |
||||
|
||||
const optionCount = await this.cell.get({ index, columnHeader }).locator('.ant-tag').count(); |
||||
|
||||
for (let i = 0; i < optionCount; i++) { |
||||
await this.cell.get({ index, columnHeader }).locator('.ant-tag > .ant-tag-close-icon').first().click(); |
||||
// wait till number of options is less than before
|
||||
await this.cell |
||||
.get({ index, columnHeader }) |
||||
.locator('.ant-tag') |
||||
.nth(optionCount - i - 1) |
||||
.waitFor({ state: 'hidden' }); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
await this.get({ index, columnHeader }).click(); |
||||
await this.rootPage.locator('.ant-tag > .ant-tag-close-icon').click(); |
||||
|
||||
// Press `Escape` to close the dropdown
|
||||
await this.rootPage.keyboard.press('Escape'); |
||||
await this.rootPage.locator('.nc-dropdown-user-select-cell').waitFor({ state: 'hidden' }); |
||||
} |
||||
|
||||
async verify({ |
||||
index = 0, |
||||
columnHeader, |
||||
option, |
||||
multiSelect, |
||||
}: { |
||||
index?: number; |
||||
columnHeader: string; |
||||
option: string; |
||||
multiSelect?: boolean; |
||||
}) { |
||||
if (multiSelect) { |
||||
return await expect(this.cell.get({ index, columnHeader })).toContainText(option, { useInnerText: true }); |
||||
} |
||||
|
||||
const locator = this.cell.get({ index, columnHeader }).locator('.ant-tag'); |
||||
await locator.waitFor({ state: 'visible' }); |
||||
const text = await locator.allInnerTexts(); |
||||
return expect(text).toContain(option); |
||||
} |
||||
|
||||
async verifyNoOptionsSelected({ index, columnHeader }: { index: number; columnHeader: string }) { |
||||
return await expect( |
||||
this.cell.get({ index, columnHeader }).locator('.ant-select-selection-overflow-item >> .ant-tag') |
||||
).toBeHidden(); |
||||
} |
||||
|
||||
async verifyOptions({ |
||||
index = 0, |
||||
columnHeader, |
||||
options, |
||||
}: { |
||||
index?: number; |
||||
columnHeader: string; |
||||
options: string[]; |
||||
}) { |
||||
const selectCell = this.get({ index, columnHeader }); |
||||
|
||||
// check if cell active
|
||||
// drag based non-primary cell will have 'active' attribute
|
||||
// primary cell with blue border will have 'active-cell' attribute
|
||||
if (!(await selectCell.getAttribute('class')).includes('active-cell')) { |
||||
await selectCell.click(); |
||||
} |
||||
|
||||
await this.get({ index, columnHeader }).click(); |
||||
await this.rootPage.waitForTimeout(500); |
||||
|
||||
let counter = 0; |
||||
for (const option of options) { |
||||
await expect(this.rootPage.locator(`div.ant-select-item-option`).nth(counter)).toHaveText(option); |
||||
counter++; |
||||
} |
||||
await this.rootPage.keyboard.press('Escape'); |
||||
await this.rootPage.locator(`.nc-dropdown-user-select-cell`).nth(index).waitFor({ state: 'hidden' }); |
||||
} |
||||
|
||||
async verifySelectedOptions({ |
||||
index, |
||||
options, |
||||
columnHeader, |
||||
}: { |
||||
columnHeader: string; |
||||
options: string[]; |
||||
index: number; |
||||
}) { |
||||
const selectCell = this.get({ index, columnHeader }); |
||||
await selectCell.click(); |
||||
|
||||
let counter = 0; |
||||
for (const option of options) { |
||||
await expect(selectCell.locator(`.nc-selected-option`).nth(counter)).toHaveText(option); |
||||
counter++; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,747 @@
|
||||
import { test } from '@playwright/test'; |
||||
import { DashboardPage } from '../../../pages/Dashboard'; |
||||
import { GridPage } from '../../../pages/Dashboard/Grid'; |
||||
import setup, { unsetup } from '../../../setup'; |
||||
import { TopbarPage } from '../../../pages/Dashboard/common/Topbar'; |
||||
import { ToolbarPage } from '../../../pages/Dashboard/common/Toolbar'; |
||||
import { WorkspacePage } from '../../../pages/WorkspacePage'; |
||||
import { CollaborationPage } from '../../../pages/WorkspacePage/CollaborationPage'; |
||||
import { Api } from 'nocodb-sdk'; |
||||
import { isEE } from '../../../setup/db'; |
||||
import { getDefaultPwd } from '../../utils/general'; |
||||
|
||||
const users: string[] = isEE() |
||||
? ['useree@nocodb.com', 'useree-0@nocodb.com', 'useree-1@nocodb.com', 'useree-2@nocodb.com', 'useree-3@nocodb.com'] |
||||
: ['user@nocodb.com', 'user-0@nocodb.com', 'user-1@nocodb.com', 'user-2@nocodb.com', 'user-3@nocodb.com']; |
||||
|
||||
const roleDb = [ |
||||
{ email: 'useree@nocodb.com', role: 'editor' }, |
||||
{ email: 'useree-0@nocodb.com', role: 'editor' }, |
||||
{ email: 'useree-1@nocodb.com', role: 'editor' }, |
||||
{ email: 'useree-2@nocodb.com', role: 'editor' }, |
||||
{ email: 'useree-3@nocodb.com', role: 'editor' }, |
||||
]; |
||||
|
||||
async function beforeEachInit({ page }: { page: any }) { |
||||
let workspacePage: WorkspacePage; |
||||
let collaborationPage: CollaborationPage; |
||||
let api: Api<any>; |
||||
|
||||
const context: any = await setup({ page, isEmptyProject: true }); |
||||
const dashboard: DashboardPage = new DashboardPage(page, context.base); |
||||
|
||||
if (isEE()) { |
||||
workspacePage = new WorkspacePage(page); |
||||
collaborationPage = workspacePage.collaboration; |
||||
|
||||
api = new Api({ |
||||
baseURL: `http://localhost:8080/`, |
||||
headers: { |
||||
'xc-auth': context.token, |
||||
}, |
||||
}); |
||||
|
||||
for (let i = 0; i < roleDb.length; i++) { |
||||
try { |
||||
await api.auth.signup({ |
||||
email: roleDb[i].email, |
||||
password: getDefaultPwd(), |
||||
}); |
||||
} catch (e) { |
||||
// ignore error even if user already exists
|
||||
} |
||||
} |
||||
|
||||
await dashboard.leftSidebar.clickTeamAndSettings(); |
||||
|
||||
for (const user of roleDb) { |
||||
await collaborationPage.addUsers(user.email, user.role); |
||||
} |
||||
} |
||||
|
||||
return { dashboard, context }; |
||||
} |
||||
|
||||
test.describe('User single select', () => { |
||||
let dashboard: DashboardPage, grid: GridPage, topbar: TopbarPage; |
||||
let context: any; |
||||
|
||||
test.beforeEach(async ({ page }) => { |
||||
const initRsp = await beforeEachInit({ page: page }); |
||||
context = initRsp.context; |
||||
dashboard = initRsp.dashboard; |
||||
grid = dashboard.grid; |
||||
topbar = dashboard.grid.topbar; |
||||
|
||||
await dashboard.treeView.createTable({ title: 'Sheet1', baseTitle: context.base.title }); |
||||
|
||||
await grid.column.create({ title: 'User', type: 'User' }); |
||||
|
||||
await grid.addNewRow({ index: 0, value: 'Row 0' }); |
||||
}); |
||||
|
||||
test.afterEach(async () => { |
||||
await unsetup(context); |
||||
}); |
||||
|
||||
test('Verify the default option count, select default value and verify', async () => { |
||||
if (!isEE()) { |
||||
await grid.column.userOption.verifyDefaultValueOptionCount({ columnTitle: 'User', totalCount: 5 }); |
||||
} |
||||
|
||||
await grid.column.userOption.selectDefaultValueOption({ |
||||
columnTitle: 'User', |
||||
option: users[0], |
||||
multiSelect: false, |
||||
}); |
||||
|
||||
// Verify default value is set
|
||||
await grid.column.userOption.verifySelectedOptions({ |
||||
columnHeader: 'User', |
||||
options: [users[0]], |
||||
}); |
||||
|
||||
// Add new row and verify default value is added in new cell
|
||||
await grid.addNewRow({ index: 1, value: 'Row 1' }); |
||||
await grid.cell.userOption.verify({ |
||||
index: 1, |
||||
columnHeader: 'User', |
||||
option: users[0], |
||||
multiSelect: false, |
||||
}); |
||||
}); |
||||
|
||||
test('Rename column title and delete the column', async () => { |
||||
// Rename column title, reload page and verify
|
||||
await grid.column.openEdit({ title: 'User' }); |
||||
await grid.column.fillTitle({ title: 'UserField' }); |
||||
await grid.column.save({ |
||||
isUpdated: true, |
||||
}); |
||||
|
||||
// reload page
|
||||
await dashboard.rootPage.reload(); |
||||
|
||||
await grid.column.verify({ title: 'UserField', isVisible: true }); |
||||
|
||||
// delete column and verify
|
||||
await grid.column.delete({ title: 'UserField' }); |
||||
await grid.column.verify({ title: 'UserField', isVisible: false }); |
||||
}); |
||||
|
||||
test('Field operations - duplicate column, convert to SingleLineText', async () => { |
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.cell.userOption.select({ index: i, columnHeader: 'User', option: users[i], multiSelect: false }); |
||||
await grid.addNewRow({ index: i + 1, value: `Row ${i + 1}` }); |
||||
} |
||||
|
||||
await grid.column.duplicateColumn({ |
||||
title: 'User', |
||||
expectedTitle: 'User copy', |
||||
}); |
||||
|
||||
// Verify duplicate column content
|
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.cell.userOption.verify({ index: i, columnHeader: 'User copy', option: users[i], multiSelect: false }); |
||||
} |
||||
|
||||
// Convert User field column to SingleLineText
|
||||
await grid.column.openEdit({ title: 'User copy' }); |
||||
await grid.column.selectType({ type: 'SingleLineText' }); |
||||
await grid.column.save({ isUpdated: true }); |
||||
|
||||
// Verify converted column content
|
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.cell.verify({ index: i, columnHeader: 'User copy', value: users[i] }); |
||||
} |
||||
}); |
||||
|
||||
test('Cell Operation - edit, copy-paste and delete', async () => { |
||||
// set default user
|
||||
await grid.column.userOption.selectDefaultValueOption({ |
||||
columnTitle: 'User', |
||||
option: users[0], |
||||
multiSelect: false, |
||||
}); |
||||
|
||||
// Edit, refresh and verify
|
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.cell.userOption.select({ index: i, columnHeader: 'User', option: users[i], multiSelect: false }); |
||||
await grid.addNewRow({ index: i + 1, value: `Row ${i + 1}` }); |
||||
} |
||||
|
||||
// refresh page
|
||||
await topbar.clickRefresh(); |
||||
|
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.cell.userOption.verify({ |
||||
index: i, |
||||
columnHeader: 'User', |
||||
option: users[i], |
||||
multiSelect: false, |
||||
}); |
||||
} |
||||
|
||||
// Delete/clear cell, refresh and verify
|
||||
// #1 Using `Delete` keyboard button
|
||||
await grid.cell.click({ index: 0, columnHeader: 'User' }); |
||||
// trigger delete button key
|
||||
await dashboard.rootPage.keyboard.press('Delete'); |
||||
|
||||
// refresh
|
||||
await topbar.clickRefresh(); |
||||
|
||||
await grid.cell.userOption.verifyNoOptionsSelected({ index: 0, columnHeader: 'user' }); |
||||
|
||||
// #2 Using mouse click
|
||||
await grid.cell.userOption.clear({ index: 1, columnHeader: 'User', multiSelect: false }); |
||||
|
||||
// refresh
|
||||
await topbar.clickRefresh(); |
||||
|
||||
await grid.cell.userOption.verifyNoOptionsSelected({ index: 1, columnHeader: 'user' }); |
||||
|
||||
// #3 Using `Cell Context Menu` right click `Clear` option
|
||||
await grid.clearWithMouse({ index: 2, columnHeader: 'User' }); |
||||
|
||||
// refresh
|
||||
await topbar.clickRefresh(); |
||||
|
||||
await grid.cell.userOption.verifyNoOptionsSelected({ index: 2, columnHeader: 'user' }); |
||||
|
||||
// Copy-paste
|
||||
// #1 Using keyboard
|
||||
await grid.cell.click({ index: 3, columnHeader: 'User' }); |
||||
await dashboard.rootPage.keyboard.press('Shift+ArrowDown'); |
||||
|
||||
await dashboard.rootPage.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c'); |
||||
await grid.cell.click({ index: 0, columnHeader: 'User' }); |
||||
await dashboard.rootPage.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v'); |
||||
|
||||
// refresh
|
||||
await topbar.clickRefresh(); |
||||
|
||||
let counter = 3; |
||||
for (let i = 0; i <= 1; i++) { |
||||
await grid.cell.userOption.verify({ |
||||
index: i, |
||||
columnHeader: 'User', |
||||
option: users[counter], |
||||
multiSelect: false, |
||||
}); |
||||
counter++; |
||||
} |
||||
|
||||
// #2 Using cell context menu copy paste option
|
||||
await grid.copyWithMouse({ index: 4, columnHeader: 'User' }); |
||||
await grid.pasteWithMouse({ index: 0, columnHeader: 'User' }); |
||||
|
||||
// refresh
|
||||
await topbar.clickRefresh(); |
||||
|
||||
await grid.cell.userOption.verify({ |
||||
index: 0, |
||||
columnHeader: 'User', |
||||
option: users[4], |
||||
multiSelect: false, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
test.describe('User single select - filter, sort & GroupBy', () => { |
||||
// Row values
|
||||
// only user@nocodb.com (row 0)
|
||||
// only user-0@nocodb.com (row 1)
|
||||
// only user-1@nocodb.com (row 2)
|
||||
// only user-2@nocodb.com (row 3)
|
||||
// only user-3@nocodb.com (row 4)
|
||||
|
||||
// Example filters:
|
||||
//
|
||||
// where tags contains all of [user@nocodb.com]
|
||||
// result: rows 0
|
||||
// where tags contains any of [user@nocodb.com, user-0@nocodb.com]
|
||||
// result: rows 0,1
|
||||
// where tags does not contain any of [user@nocodb.com, user-0@nocodb.com]
|
||||
// result: rows 2,3,4
|
||||
// where tags does not contain all of [user-0@nocodb.com]
|
||||
// result: rows 0,2,3,4
|
||||
// where tags is not blank
|
||||
// result: rows 0,1,2,3,4
|
||||
// where tags is blank
|
||||
// result: null
|
||||
|
||||
let dashboard: DashboardPage, grid: GridPage, toolbar: ToolbarPage; |
||||
let context: any; |
||||
|
||||
test.beforeEach(async ({ page }) => { |
||||
const initRsp = await beforeEachInit({ page: page }); |
||||
context = initRsp.context; |
||||
dashboard = initRsp.dashboard; |
||||
grid = dashboard.grid; |
||||
toolbar = dashboard.grid.toolbar; |
||||
|
||||
await dashboard.treeView.createTable({ title: 'sheet1', baseTitle: context.base.title }); |
||||
|
||||
await grid.column.create({ title: 'User', type: 'User' }); |
||||
|
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.addNewRow({ index: i, value: `${i}` }); |
||||
await grid.cell.userOption.select({ index: i, columnHeader: 'User', option: users[i], multiSelect: false }); |
||||
} |
||||
}); |
||||
|
||||
test.afterEach(async () => { |
||||
await unsetup(context); |
||||
}); |
||||
|
||||
// define validateRowArray function
|
||||
async function validateRowArray(value: string[]) { |
||||
const length = value.length; |
||||
for (let i = 0; i < length; i++) { |
||||
await dashboard.grid.cell.verify({ |
||||
index: i, |
||||
columnHeader: 'Title', |
||||
value: value[i], |
||||
}); |
||||
} |
||||
} |
||||
|
||||
async function verifyFilter(param: { opType: string; value?: string; result: string[] }) { |
||||
await toolbar.clickFilter(); |
||||
await toolbar.filter.add({ |
||||
title: 'User', |
||||
operation: param.opType, |
||||
value: param.value, |
||||
locallySaved: false, |
||||
dataType: 'User', |
||||
}); |
||||
await toolbar.clickFilter(); |
||||
|
||||
// verify filtered rows
|
||||
await validateRowArray(param.result); |
||||
// Reset filter
|
||||
await toolbar.filter.reset(); |
||||
} |
||||
|
||||
test('User sort & validate, filter & validate', async () => { |
||||
const ascendingOrderRowTitle = ['1', '2', '3', '4', '0']; |
||||
const descendingOrderRowTitle = ['0', '4', '3', '2', '1']; |
||||
|
||||
// Sort ascending and validate
|
||||
await toolbar.sort.add({ |
||||
title: 'User', |
||||
ascending: true, |
||||
locallySaved: false, |
||||
}); |
||||
await validateRowArray(ascendingOrderRowTitle); |
||||
await toolbar.sort.reset(); |
||||
|
||||
// sort descending and validate
|
||||
await toolbar.sort.add({ |
||||
title: 'User', |
||||
ascending: false, |
||||
locallySaved: false, |
||||
}); |
||||
await validateRowArray(descendingOrderRowTitle); |
||||
await toolbar.sort.reset(); |
||||
|
||||
// filter
|
||||
await verifyFilter({ opType: 'contains all of', value: users[0], result: ['0'] }); |
||||
await verifyFilter({ |
||||
opType: 'contains any of', |
||||
value: `${users[0]},${users[1]}`, |
||||
result: ['0', '1'], |
||||
}); |
||||
await verifyFilter({ |
||||
opType: 'does not contain any of', |
||||
value: `${users[0]},${users[1]}`, |
||||
result: ['2', '3', '4'], |
||||
}); |
||||
await verifyFilter({ opType: 'does not contain all of', value: users[1], result: ['0', '2', '3', '4'] }); |
||||
await verifyFilter({ opType: 'is not blank', result: ['0', '1', '2', '3', '4'] }); |
||||
await verifyFilter({ opType: 'is blank', result: [] }); |
||||
|
||||
//GroupBy
|
||||
// ascending order
|
||||
await toolbar.groupBy.add({ title: 'User', ascending: true, locallySaved: false }); |
||||
|
||||
for (let i = 0; i <= 4; i++) { |
||||
await dashboard.grid.groupPage.openGroup({ indexMap: [i] }); |
||||
|
||||
await dashboard.grid.groupPage.validateFirstRow({ |
||||
indexMap: [i], |
||||
rowIndex: 0, |
||||
columnHeader: 'Title', |
||||
value: ascendingOrderRowTitle[i], |
||||
}); |
||||
} |
||||
|
||||
// descending order
|
||||
await toolbar.groupBy.update({ title: 'User', ascending: false, index: 0 }); |
||||
|
||||
for (let i = 0; i <= 4; i++) { |
||||
await dashboard.grid.groupPage.openGroup({ indexMap: [i] }); |
||||
await dashboard.grid.groupPage.validateFirstRow({ |
||||
indexMap: [i], |
||||
rowIndex: 0, |
||||
columnHeader: 'Title', |
||||
value: descendingOrderRowTitle[i], |
||||
}); |
||||
} |
||||
await toolbar.groupBy.remove({ index: 0 }); |
||||
}); |
||||
}); |
||||
|
||||
test.describe('User multiple select', () => { |
||||
let dashboard: DashboardPage, grid: GridPage, topbar: TopbarPage; |
||||
let context: any; |
||||
|
||||
test.beforeEach(async ({ page }) => { |
||||
const initRsp = await beforeEachInit({ page: page }); |
||||
context = initRsp.context; |
||||
dashboard = initRsp.dashboard; |
||||
grid = dashboard.grid; |
||||
topbar = dashboard.grid.topbar; |
||||
|
||||
await dashboard.treeView.createTable({ title: 'Sheet1', baseTitle: context.base.title }); |
||||
|
||||
await grid.column.create({ title: 'User', type: 'User' }); |
||||
await grid.column.userOption.allowMultipleUser({ columnTitle: 'User', allowMultiple: true }); |
||||
}); |
||||
|
||||
test.afterEach(async () => { |
||||
await unsetup(context); |
||||
}); |
||||
|
||||
test('Verify the default option count, select default value and verify', async () => { |
||||
await grid.addNewRow({ index: 0, value: 'Row 0' }); |
||||
|
||||
if (!isEE()) { |
||||
await grid.column.userOption.verifyDefaultValueOptionCount({ columnTitle: 'User', totalCount: 5 }); |
||||
} |
||||
|
||||
await grid.column.userOption.selectDefaultValueOption({ |
||||
columnTitle: 'User', |
||||
option: [users[0], users[1]], |
||||
multiSelect: true, |
||||
}); |
||||
|
||||
// Verify default value is set
|
||||
await grid.column.userOption.verifySelectedOptions({ |
||||
columnHeader: 'User', |
||||
options: [users[0], users[1]], |
||||
}); |
||||
|
||||
// Add new row and verify default value is added in new cell
|
||||
await grid.addNewRow({ index: 1, value: 'Row 1' }); |
||||
await grid.cell.userOption.verify({ |
||||
index: 1, |
||||
columnHeader: 'User', |
||||
option: users[0], |
||||
multiSelect: true, |
||||
}); |
||||
await grid.cell.userOption.verify({ |
||||
index: 1, |
||||
columnHeader: 'User', |
||||
option: users[1], |
||||
multiSelect: true, |
||||
}); |
||||
}); |
||||
|
||||
test('Field operations - duplicate column, convert to SingleLineText', async () => { |
||||
let counter = 1; |
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.addNewRow({ index: i, value: `Row ${i}` }); |
||||
|
||||
await grid.cell.userOption.select({ index: i, columnHeader: 'User', option: users[i], multiSelect: true }); |
||||
await grid.cell.userOption.select({ index: i, columnHeader: 'User', option: users[counter], multiSelect: true }); |
||||
|
||||
if (counter === 4) counter = 0; |
||||
else counter++; |
||||
} |
||||
|
||||
await grid.column.duplicateColumn({ |
||||
title: 'User', |
||||
expectedTitle: 'User copy', |
||||
}); |
||||
|
||||
// Verify duplicate column content
|
||||
counter = 1; |
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.cell.userOption.verifySelectedOptions({ |
||||
index: i, |
||||
columnHeader: 'User copy', |
||||
options: [users[i], users[counter]], |
||||
}); |
||||
|
||||
if (counter === 4) counter = 0; |
||||
else counter++; |
||||
} |
||||
|
||||
// Convert User field column to SingleLineText
|
||||
await grid.column.openEdit({ title: 'User copy' }); |
||||
await grid.column.selectType({ type: 'SingleLineText' }); |
||||
await grid.column.save({ isUpdated: true }); |
||||
|
||||
// Verify converted column content
|
||||
counter = 1; |
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.cell.verify({ index: i, columnHeader: 'User copy', value: `${users[i]},${users[counter]}` }); |
||||
|
||||
if (counter === 4) counter = 0; |
||||
else counter++; |
||||
} |
||||
}); |
||||
|
||||
test('Cell Operation - edit, copy-paste and delete', async () => { |
||||
// Edit, refresh and verify
|
||||
let counter = 1; |
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.addNewRow({ index: i, value: `Row ${i}` }); |
||||
|
||||
await grid.cell.userOption.select({ |
||||
index: i, |
||||
columnHeader: 'User', |
||||
option: users[i], |
||||
multiSelect: true, |
||||
}); |
||||
|
||||
await grid.cell.userOption.select({ |
||||
index: i, |
||||
columnHeader: 'User', |
||||
option: users[counter], |
||||
multiSelect: true, |
||||
}); |
||||
if (counter === 4) counter = 0; |
||||
else counter++; |
||||
} |
||||
|
||||
// reload page
|
||||
await dashboard.rootPage.reload(); |
||||
|
||||
counter = 1; |
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.cell.userOption.verifySelectedOptions({ |
||||
index: i, |
||||
columnHeader: 'User', |
||||
options: [users[i], users[counter]], |
||||
}); |
||||
if (counter === 4) counter = 0; |
||||
else counter++; |
||||
} |
||||
|
||||
// Delete/clear cell, refresh and verify
|
||||
// #1 Using `Delete` keyboard button
|
||||
await grid.cell.click({ index: 0, columnHeader: 'User' }); |
||||
// trigger delete button key
|
||||
await dashboard.rootPage.keyboard.press('Delete'); |
||||
|
||||
// refresh
|
||||
await topbar.clickRefresh(); |
||||
|
||||
await grid.cell.userOption.verifyNoOptionsSelected({ index: 0, columnHeader: 'user' }); |
||||
|
||||
// #2 Using mouse click
|
||||
await grid.cell.userOption.clear({ index: 1, columnHeader: 'User', multiSelect: true }); |
||||
|
||||
// refresh
|
||||
await topbar.clickRefresh(); |
||||
|
||||
await grid.cell.userOption.verifyNoOptionsSelected({ index: 1, columnHeader: 'user' }); |
||||
|
||||
// #3 Using `Cell Context Menu` right click `Clear` option
|
||||
await grid.clearWithMouse({ index: 2, columnHeader: 'User' }); |
||||
|
||||
// refresh
|
||||
await topbar.clickRefresh(); |
||||
|
||||
await grid.cell.userOption.verifyNoOptionsSelected({ index: 2, columnHeader: 'user' }); |
||||
|
||||
// Copy-paste
|
||||
// #1 Using keyboard
|
||||
await grid.cell.click({ index: 3, columnHeader: 'User' }); |
||||
|
||||
await dashboard.rootPage.keyboard.press((await grid.isMacOs()) ? 'Meta+c' : 'Control+c'); |
||||
await grid.cell.click({ index: 0, columnHeader: 'User' }); |
||||
await dashboard.rootPage.keyboard.press((await grid.isMacOs()) ? 'Meta+v' : 'Control+v'); |
||||
|
||||
// refresh
|
||||
await topbar.clickRefresh(); |
||||
|
||||
await grid.cell.userOption.verifySelectedOptions({ |
||||
index: 0, |
||||
columnHeader: 'User', |
||||
options: [users[3], users[4]], |
||||
}); |
||||
|
||||
// #2 Using cell context menu copy paste option
|
||||
await grid.copyWithMouse({ index: 4, columnHeader: 'User' }); |
||||
await grid.pasteWithMouse({ index: 1, columnHeader: 'User' }); |
||||
|
||||
// refresh
|
||||
await topbar.clickRefresh(); |
||||
|
||||
await grid.cell.userOption.verifySelectedOptions({ |
||||
index: 1, |
||||
columnHeader: 'User', |
||||
options: [users[4], users[0]], |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
test.describe('User multiple select - filter, sort & GroupBy', () => { |
||||
// Row values
|
||||
// only user@nocodb.com (row 0)
|
||||
// user-0@nocodb.com and user-1@nocodb.com (row 1)
|
||||
// user-1@nocodb.com and user-2@nocodb.com (row 2)
|
||||
// user-2@nocodb.com and user-3@nocodb.com (row 3)
|
||||
// user-3@nocodb.com and user@nocodb.com (row 4)
|
||||
|
||||
// Example filters:
|
||||
//
|
||||
// where tags contains all of [user-0@nocodb.com, user-1@nocodb.com]
|
||||
// result: rows 1
|
||||
// where tags contains any of [user@nocodb.com, user-0@nocodb.com]
|
||||
// result: rows 0,1,4
|
||||
// where tags does not contain any of [user@nocodb.com, user-0@nocodb.com]
|
||||
// result: rows 2,3
|
||||
// where tags does not contain all of [user-0@nocodb.com]
|
||||
// result: rows 0,2,3,4
|
||||
// where tags is not blank
|
||||
// result: rows 0,1,2,3,4
|
||||
// where tags is blank
|
||||
// result: null
|
||||
|
||||
let dashboard: DashboardPage, grid: GridPage, toolbar: ToolbarPage; |
||||
let context: any; |
||||
|
||||
test.beforeEach(async ({ page }) => { |
||||
const initRsp = await beforeEachInit({ page: page }); |
||||
context = initRsp.context; |
||||
dashboard = initRsp.dashboard; |
||||
grid = dashboard.grid; |
||||
toolbar = dashboard.grid.toolbar; |
||||
|
||||
await dashboard.treeView.createTable({ title: 'sheet1', baseTitle: context.base.title }); |
||||
|
||||
await grid.column.create({ title: 'User', type: 'User' }); |
||||
await grid.column.userOption.allowMultipleUser({ columnTitle: 'User', allowMultiple: true }); |
||||
|
||||
let counter = 2; |
||||
for (let i = 0; i <= 4; i++) { |
||||
await grid.addNewRow({ index: i, value: `${i}` }); |
||||
await grid.cell.userOption.select({ index: i, columnHeader: 'User', option: users[i], multiSelect: true }); |
||||
if (i !== 0) { |
||||
await grid.cell.userOption.select({ |
||||
index: i, |
||||
columnHeader: 'User', |
||||
option: users[counter], |
||||
multiSelect: true, |
||||
}); |
||||
if (counter === 4) counter = 0; |
||||
else counter++; |
||||
} |
||||
} |
||||
}); |
||||
|
||||
test.afterEach(async () => { |
||||
await unsetup(context); |
||||
}); |
||||
|
||||
// define validateRowArray function
|
||||
async function validateRowArray(value: string[]) { |
||||
const length = value.length; |
||||
for (let i = 0; i < length; i++) { |
||||
await dashboard.grid.cell.verify({ |
||||
index: i, |
||||
columnHeader: 'Title', |
||||
value: value[i], |
||||
}); |
||||
} |
||||
} |
||||
|
||||
async function verifyFilter(param: { opType: string; value?: string; result: string[] }) { |
||||
await toolbar.clickFilter(); |
||||
await toolbar.filter.add({ |
||||
title: 'User', |
||||
operation: param.opType, |
||||
value: param.value, |
||||
locallySaved: false, |
||||
dataType: 'User', |
||||
}); |
||||
await toolbar.clickFilter(); |
||||
|
||||
// verify filtered rows
|
||||
await validateRowArray(param.result); |
||||
// Reset filter
|
||||
await toolbar.filter.reset(); |
||||
} |
||||
|
||||
test('User sort & validate, filter & validate', async () => { |
||||
const ascendingOrderRowTitle = ['1', '2', '3', '4', '0']; |
||||
const descendingOrderRowTitle = ['0', '4', '3', '2', '1']; |
||||
|
||||
// Sort ascending and validate
|
||||
await toolbar.sort.add({ |
||||
title: 'User', |
||||
ascending: true, |
||||
locallySaved: false, |
||||
}); |
||||
await validateRowArray(ascendingOrderRowTitle); |
||||
await toolbar.sort.reset(); |
||||
|
||||
// sort descending and validate
|
||||
await toolbar.sort.add({ |
||||
title: 'User', |
||||
ascending: false, |
||||
locallySaved: false, |
||||
}); |
||||
await validateRowArray(descendingOrderRowTitle); |
||||
await toolbar.sort.reset(); |
||||
|
||||
// filter
|
||||
await verifyFilter({ opType: 'contains all of', value: `${(users[1], users[2])}`, result: ['1'] }); |
||||
await verifyFilter({ |
||||
opType: 'contains any of', |
||||
value: `${users[0]},${users[1]}`, |
||||
result: ['0', '1', '4'], |
||||
}); |
||||
await verifyFilter({ |
||||
opType: 'does not contain any of', |
||||
value: `${users[0]},${users[1]}`, |
||||
result: ['2', '3'], |
||||
}); |
||||
await verifyFilter({ opType: 'does not contain all of', value: users[1], result: ['0', '2', '3', '4'] }); |
||||
await verifyFilter({ opType: 'is not blank', result: ['0', '1', '2', '3', '4'] }); |
||||
await verifyFilter({ opType: 'is blank', result: [] }); |
||||
|
||||
//GroupBy
|
||||
// ascending order
|
||||
await toolbar.groupBy.add({ title: 'User', ascending: true, locallySaved: false }); |
||||
|
||||
for (let i = 0; i <= 4; i++) { |
||||
await dashboard.grid.groupPage.openGroup({ indexMap: [i] }); |
||||
await dashboard.grid.groupPage.validateFirstRow({ |
||||
indexMap: [i], |
||||
rowIndex: 0, |
||||
columnHeader: 'Title', |
||||
value: ascendingOrderRowTitle[i], |
||||
}); |
||||
} |
||||
|
||||
// descending order
|
||||
await toolbar.groupBy.update({ title: 'User', ascending: false, index: 0 }); |
||||
|
||||
for (let i = 0; i <= 4; i++) { |
||||
await dashboard.grid.groupPage.openGroup({ indexMap: [i] }); |
||||
await dashboard.grid.groupPage.validateFirstRow({ |
||||
indexMap: [i], |
||||
rowIndex: 0, |
||||
columnHeader: 'Title', |
||||
value: descendingOrderRowTitle[i], |
||||
}); |
||||
} |
||||
await toolbar.groupBy.remove({ index: 0 }); |
||||
}); |
||||
}); |
Loading…
Reference in new issue