Browse Source

Merge branch 'develop' into fix/csv-infinity-wait

pull/4384/head
Wing-Kam Wong 2 years ago
parent
commit
f54a1b95f0
  1. 2
      packages/nc-gui/assets/style.scss
  2. 3
      packages/nc-gui/components.d.ts
  3. 7
      packages/nc-gui/components/cell/Percent.vue
  4. 1
      packages/nc-gui/components/dlg/TableRename.vue
  5. 1
      packages/nc-gui/components/smartsheet/Cell.vue
  6. 10
      packages/nc-gui/components/smartsheet/Grid.vue
  7. 42
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  8. 3
      packages/nc-gui/composables/useExpandedFormStore.ts
  9. 3
      packages/nc-gui/composables/useViewData.ts
  10. 2
      packages/nc-gui/lang/en.json
  11. 8
      packages/nc-gui/pages/index/index/create-external.vue
  12. 1
      packages/nocodb-sdk/src/lib/Api.ts
  13. 3
      scripts/sdk/swagger.json
  14. 28
      tests/playwright/pages/Dashboard/ExpandedForm/index.ts
  15. 10
      tests/playwright/tests/expandedFormUrl.spec.ts
  16. 14
      tests/playwright/tests/tableColumnOperation.spec.ts

2
packages/nc-gui/assets/style.scss

@ -257,7 +257,7 @@ a {
}
.ant-dropdown-menu-item, .ant-menu-item {
@apply !py-0;
@apply py-0;
}
.ant-dropdown-menu-title-content,

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

@ -24,6 +24,7 @@ declare module '@vue/runtime-core' {
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
ADropdownButton: typeof import('ant-design-vue/es')['DropdownButton']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
@ -126,6 +127,7 @@ declare module '@vue/runtime-core' {
MdiBugOutline: typeof import('~icons/mdi/bug-outline')['default']
MdiCalculator: typeof import('~icons/mdi/calculator')['default']
MdiCalendarMonth: typeof import('~icons/mdi/calendar-month')['default']
MdiCancel: typeof import('~icons/mdi/cancel')['default']
MdiCardsHeart: typeof import('~icons/mdi/cards-heart')['default']
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default']
@ -143,6 +145,7 @@ declare module '@vue/runtime-core' {
MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiContentSaveEdit: typeof import('~icons/mdi/content-save-edit')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
MdiDatabaseSync: typeof import('~icons/mdi/database-sync')['default']

7
packages/nc-gui/components/cell/Percent.vue

@ -1,4 +1,5 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { EditModeInj, inject, useVModel } from '#imports'
interface Props {
@ -12,15 +13,21 @@ const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
const vModel = useVModel(props, 'modelValue', emits)
const focus: VNodeRef = (el) => {
;(el as HTMLInputElement)?.focus()
}
</script>
<template>
<input
v-if="editEnabled"
:ref="focus"
v-model="vModel"
class="w-full !border-none text-base"
:class="{ '!px-2': editEnabled }"
type="number"
@blur="editEnabled = false"
@keydown.down.stop
@keydown.left.stop
@keydown.right.stop

1
packages/nc-gui/components/dlg/TableRename.vue

@ -119,6 +119,7 @@ const renameTable = async () => {
await $api.dbTable.update(tableMeta.id as string, {
project_id: tableMeta.project_id,
table_name: formState.title,
title: formState.title,
})
dialogShow.value = false

1
packages/nc-gui/components/smartsheet/Cell.vue

@ -111,7 +111,6 @@ const vModel = computed({
})
const syncAndNavigate = (dir: NavigateDir, e: KeyboardEvent) => {
console.log('syncAndNavigate', e.target)
if (isJSON(column.value)) return
if (currentRow.value.rowMeta.changed || currentRow.value.rowMeta.new) {

10
packages/nc-gui/components/smartsheet/Grid.vue

@ -423,6 +423,7 @@ onClickOutside(smartTable, (e) => {
const onNavigate = (dir: NavigateDir) => {
if (selected.row === null || selected.col === null) return
editEnabled = false
switch (dir) {
case NavigateDir.NEXT:
if (selected.row < data.value.length - 1) {
@ -435,8 +436,6 @@ const onNavigate = (dir: NavigateDir) => {
case NavigateDir.PREV:
if (selected.row > 0) {
selected.row--
} else {
editEnabled = false
}
break
}
@ -521,11 +520,14 @@ provide(ReloadRowDataHookInj, reloadViewDataHook)
// trigger initial data load in grid
// reloadViewDataHook.trigger()
const switchingTab = ref(false)
watch(
view,
async (next, old) => {
try {
if (next && next.id !== old?.id) {
switchingTab.value = true
// whenever tab changes or view changes save any unsaved data
if (old?.id) {
const oldMeta = await getMeta(old.fk_model_id!)
@ -541,6 +543,8 @@ watch(
}
} catch (e) {
console.log(e)
} finally {
switchingTab.value = false
}
},
{ immediate: true },
@ -707,7 +711,7 @@ watch(
@mouseover="selectBlock(rowIndex, colIndex)"
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
>
<div class="w-full h-full">
<div v-if="!switchingTab" class="w-full h-full">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(columnObj)"
v-model="row.row[columnObj.title]"

42
packages/nc-gui/components/smartsheet/expanded-form/Header.vue

@ -25,6 +25,8 @@ const { isUIAllowed } = useUIPermission()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const saveRowAndStay = ref(0)
const save = async () => {
if (isNew.value) {
const data = await _save(state.value)
@ -34,6 +36,9 @@ const save = async () => {
await _save()
reloadTrigger?.trigger()
}
if (!saveRowAndStay.value) {
emit('cancel')
}
}
// todo: accept as a prop / inject
@ -101,14 +106,39 @@ const copyRecordUrl = () => {
</a-tooltip>
<a-button class="!text mx-1 nc-expand-form-close-btn" @click="emit('cancel')">
<!-- Cancel -->
{{ $t('general.cancel') }}
<div class="flex items-center">
<MdiCloseCircleOutline class="mr-1" />
<!-- Close -->
{{ $t('general.close') }}
</div>
</a-button>
<a-button :disabled="!isUIAllowed('tableRowUpdate')" type="primary" class="mx-1" @click="save">
<!-- Save Row -->
{{ $t('activity.saveRow') }}
</a-button>
<a-dropdown-button class="nc-expand-form-save-btn" type="primary" :disabled="!isUIAllowed('tableRowUpdate')" @click="save">
<template #overlay>
<a-menu class="nc-expand-form-save-dropdown-menu">
<a-menu-item key="0" class="!py-2 flex gap-2" @click="saveRowAndStay = 0">
<div class="flex items-center">
<MdiContentSave class="mr-1" />
{{ $t('activity.saveAndExit') }}
</div>
</a-menu-item>
<a-menu-item key="1" class="!py-2 flex gap-2 items-center" @click="saveRowAndStay = 1">
<div class="flex items-center">
<MdiContentSaveEdit class="mr-1" />
{{ $t('activity.saveAndStay') }}
</div>
</a-menu-item>
</a-menu>
</template>
<div v-if="saveRowAndStay === 0" class="flex items-center">
<MdiContentSave class="mr-1" />
{{ $t('activity.saveAndExit') }}
</div>
<div v-if="saveRowAndStay === 1" class="flex items-center">
<MdiContentSaveEdit class="mr-1" />
{{ $t('activity.saveAndStay') }}
</div>
</a-dropdown-button>
</div>
</template>

3
packages/nc-gui/composables/useExpandedFormStore.ts

@ -46,8 +46,6 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
const activeView = inject(ActiveViewInj, ref())
const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
const { sharedView } = useSharedView()
// getters
@ -197,6 +195,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
if (activeView.value?.type === ViewTypes.KANBAN) {
const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
addOrEditStackRow(row.value, isNewRow)
}

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

@ -203,8 +203,7 @@ export function useViewData(
}
async function loadGalleryData() {
if (!viewMeta?.value?.id) return
if (!viewMeta?.value?.id || isPublic.value) return
galleryData.value = await $api.dbView.galleryRead(viewMeta.value.id)
}

2
packages/nc-gui/lang/en.json

@ -368,6 +368,8 @@
"setPrimary": "Set as Primary value",
"addRow": "Add new row",
"saveRow": "Save row",
"saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay",
"insertRow": "Insert New Row",
"deleteRow": "Delete Row",
"deleteSelectedRow": "Delete Selected Rows",

8
packages/nc-gui/pages/index/index/create-external.vue

@ -46,8 +46,8 @@ let formState = $ref<ProjectCreateForm>({
title: '',
dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) },
inflection: {
inflectionColumn: 'camelize',
inflectionTable: 'camelize',
inflectionColumn: 'none',
inflectionTable: 'none',
},
sslUse: SSLUsage.No,
extraParameters: [],
@ -57,8 +57,8 @@ const customFormState = ref<ProjectCreateForm>({
title: '',
dataSource: { ...getDefaultConnectionConfig(ClientType.MYSQL) },
inflection: {
inflectionColumn: 'camelize',
inflectionTable: 'camelize',
inflectionColumn: 'none',
inflectionTable: 'none',
},
sslUse: SSLUsage.No,
extraParameters: [],

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

@ -1950,6 +1950,7 @@ export class Api<
tableId: string,
data: {
table_name?: string;
title?: string;
project_id?: string;
},
params: RequestParams = {}

3
scripts/sdk/swagger.json

@ -1703,6 +1703,9 @@
"table_name": {
"type": "string"
},
"title": {
"type": "string"
},
"project_id": {
"type": "string"
}

28
tests/playwright/pages/Dashboard/ExpandedForm/index.ts

@ -54,10 +54,21 @@ export class ExpandedFormPage extends BasePage {
async save({
waitForRowsData = true,
saveAndExitMode = true,
}: {
waitForRowsData?: boolean;
saveAndExitMode?: boolean;
} = {}) {
const saveRowAction = this.get().locator('button:has-text("Save Row")').click();
if (!saveAndExitMode) {
await this.get().locator('.nc-expand-form-save-btn .ant-dropdown-trigger').click();
const dropdownList = this.rootPage.locator('.nc-expand-form-save-dropdown-menu');
await dropdownList.locator('.ant-dropdown-menu-item:has-text("Save & Stay")').click();
}
const saveRowAction = saveAndExitMode
? this.get().locator('button:has-text("Save & Exit")').click()
: this.get().locator('button:has-text("Save & Stay")').click();
if (waitForRowsData) {
await this.waitForResponse({
uiAction: saveRowAction,
@ -73,7 +84,10 @@ export class ExpandedFormPage extends BasePage {
});
}
await this.get().press('Escape');
if (!saveAndExitMode) {
await this.get().press('Escape');
}
await this.get().waitFor({ state: 'hidden' });
await this.verifyToast({ message: `updated successfully.` });
await this.rootPage.locator('[data-testid="grid-load-spinner"]').waitFor({ state: 'hidden' });
@ -84,13 +98,13 @@ export class ExpandedFormPage extends BasePage {
await expect.poll(() => this.rootPage.url()).toContain(url);
}
async close() {
async escape() {
await this.rootPage.keyboard.press('Escape');
await this.get().waitFor({ state: 'hidden' });
}
async cancel() {
await this.get().locator('button:has-text("Cancel")').last().click();
async close() {
await this.get().locator('button:has-text("Close")').last().click();
}
async openChildCard(param: { column: string; title: string }) {
@ -104,9 +118,9 @@ export class ExpandedFormPage extends BasePage {
async validateRoleAccess(param: { role: string }) {
if (param.role === 'commenter' || param.role === 'viewer') {
await expect(await this.get().locator('button:has-text("Save Row")')).toBeDisabled();
await expect(await this.get().locator('button:has-text("Save & Exit")')).toBeDisabled();
} else {
await expect(await this.get().locator('button:has-text("Save Row")')).toBeEnabled();
await expect(await this.get().locator('button:has-text("Save & Exit")')).toBeEnabled();
}
if (param.role === 'viewer') {
await expect(await this.toggleCommentsButton).toHaveCount(0);

10
tests/playwright/tests/expandedFormUrl.spec.ts

@ -37,7 +37,7 @@ test.describe('Expanded form URL', () => {
// expand row & verify URL
await viewObj.openExpandedRow({ index: 0 });
const url = await dashboard.expandedForm.getShareRowUrl();
await dashboard.expandedForm.close();
await dashboard.expandedForm.escape();
await dashboard.rootPage.goto(url);
await dashboard.expandedForm.verify({
@ -81,13 +81,13 @@ test.describe('Expanded form URL', () => {
// expect(expandedFormUrl).toContain("rowId=1");
// access a new rowID using URL
await dashboard.expandedForm.close();
await dashboard.expandedForm.escape();
await dashboard.expandedForm.gotoUsingUrlAndRowId({ rowId: '2' });
await dashboard.expandedForm.verify({
header: 'Algeria',
url: 'rowId=2',
});
await dashboard.expandedForm.close();
await dashboard.expandedForm.escape();
// visit invalid rowID
await dashboard.expandedForm.gotoUsingUrlAndRowId({ rowId: '999' });
@ -114,12 +114,12 @@ test.describe('Expanded form URL', () => {
await dashboard.expandedForm.verifyCount({ count: 2 });
// close child card
await dashboard.expandedForm.cancel();
await dashboard.expandedForm.close();
await dashboard.expandedForm.verify({
header: 'Afghanistan',
url: 'rowId=1',
});
await dashboard.expandedForm.cancel();
await dashboard.expandedForm.close();
}
test('Grid', async () => {

14
tests/playwright/tests/tableColumnOperation.spec.ts

@ -36,13 +36,25 @@ test.describe('Table Column Operations', () => {
columnTitle: 'Title',
value: 'value_a',
});
await dashboard.expandedForm.save();
await dashboard.expandedForm.save({ saveAndExitMode: true });
await grid.cell.verify({
index: 0,
columnHeader: 'Title',
value: 'value_a',
});
await grid.openExpandedRow({ index: 0 });
await dashboard.expandedForm.fillField({
columnTitle: 'Title',
value: 'value_a_a',
});
await dashboard.expandedForm.save({ saveAndExitMode: false });
await grid.cell.verify({
index: 0,
columnHeader: 'Title',
value: 'value_a_a',
});
await grid.deleteRow(0);
await grid.verifyRowDoesNotExist({ index: 0 });

Loading…
Cancel
Save