Browse Source

Merge pull request #7527 from nocodb/nc-fix/webhook-trigger-bulk-delete

Nc fix: bulk delete webhook trigger issue on deleting single selected row
pull/7546/head
Raju Udava 9 months ago committed by GitHub
parent
commit
54468cf760
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 70
      packages/nc-gui/components/smartsheet/grid/Table.vue
  2. 5
      packages/nc-gui/composables/useData.ts
  3. 76
      tests/playwright/tests/db/features/webhook.spec.ts

70
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -1320,7 +1320,10 @@ onKeyStroke('ArrowDown', onDown)
:title="true" :title="true"
:paragraph="false" :paragraph="false"
class="ml-2 -mt-2" class="ml-2 -mt-2"
:class="{ 'max-w-32': colIndex !== 0, 'max-w-5 !ml-3.5': colIndex === 0 }" :class="{
'max-w-32': colIndex !== 0,
'max-w-5 !ml-3.5': colIndex === 0,
}"
/> />
</td> </td>
</tr> </tr>
@ -1330,7 +1333,10 @@ onKeyStroke('ArrowDown', onDown)
<template v-if="!readOnly"> <template v-if="!readOnly">
<div class="nc-no-label text-gray-500" :class="{ hidden: vSelectedAllRecords }">#</div> <div class="nc-no-label text-gray-500" :class="{ hidden: vSelectedAllRecords }">#</div>
<div <div
:class="{ hidden: !vSelectedAllRecords, flex: vSelectedAllRecords }" :class="{
hidden: !vSelectedAllRecords,
flex: vSelectedAllRecords,
}"
class="nc-check-all w-full items-center" class="nc-check-all w-full items-center"
> >
<a-checkbox v-model:checked="vSelectedAllRecords" /> <a-checkbox v-model:checked="vSelectedAllRecords" />
@ -1405,7 +1411,9 @@ onKeyStroke('ArrowDown', onDown)
<template #title> <template #title>
<div class="flex flex-row items-center py-3"> <div class="flex flex-row items-center py-3">
<MdiTableColumnPlusAfter class="flex h-[1rem] text-gray-500" /> <MdiTableColumnPlusAfter class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.predictColumns') }}</div> <div class="text-xs pl-2">
{{ $t('activity.predictColumns') }}
</div>
<MdiChevronRight class="text-gray-500 ml-2" /> <MdiChevronRight class="text-gray-500 ml-2" />
</div> </div>
</template> </template>
@ -1434,14 +1442,18 @@ onKeyStroke('ArrowDown', onDown)
<div class="flex flex-row items-center py-3" @click="predictNextColumn"> <div class="flex flex-row items-center py-3" @click="predictNextColumn">
<MdiReload v-if="predictingNextColumn" class="animate-infinite animate-spin" /> <MdiReload v-if="predictingNextColumn" class="animate-infinite animate-spin" />
<MdiTableColumnPlusAfter v-else class="flex h-[1rem] text-gray-500" /> <MdiTableColumnPlusAfter v-else class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.predictColumns') }}</div> <div class="text-xs pl-2">
{{ $t('activity.predictColumns') }}
</div>
</div> </div>
</NcMenuItem> </NcMenuItem>
<a-sub-menu v-if="predictedNextFormulas" key="predict-formula"> <a-sub-menu v-if="predictedNextFormulas" key="predict-formula">
<template #title> <template #title>
<div class="flex flex-row items-center py-3"> <div class="flex flex-row items-center py-3">
<MdiCalculatorVariant class="flex h-[1rem] text-gray-500" /> <MdiCalculatorVariant class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.predictFormulas') }}</div> <div class="text-xs pl-2">
{{ $t('activity.predictFormulas') }}
</div>
<MdiChevronRight class="text-gray-500 ml-2" /> <MdiChevronRight class="text-gray-500 ml-2" />
</div> </div>
</template> </template>
@ -1451,7 +1463,11 @@ onKeyStroke('ArrowDown', onDown)
<NcMenuItem> <NcMenuItem>
<div <div
class="flex flex-row items-center py-3" class="flex flex-row items-center py-3"
@click="loadColumn(col.title, 'Formula', { formula_raw: col.formula })" @click="
loadColumn(col.title, 'Formula', {
formula_raw: col.formula,
})
"
> >
<div class="text-xs pl-2">{{ col.title }}</div> <div class="text-xs pl-2">{{ col.title }}</div>
</div> </div>
@ -1464,7 +1480,9 @@ onKeyStroke('ArrowDown', onDown)
<div class="flex flex-row items-center py-3" @click="predictNextFormulas"> <div class="flex flex-row items-center py-3" @click="predictNextFormulas">
<MdiReload v-if="predictingNextFormulas" class="animate-infinite animate-spin" /> <MdiReload v-if="predictingNextFormulas" class="animate-infinite animate-spin" />
<MdiCalculatorVariant v-else class="flex h-[1rem] text-gray-500" /> <MdiCalculatorVariant v-else class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.predictFormulas') }}</div> <div class="text-xs pl-2">
{{ $t('activity.predictFormulas') }}
</div>
</div> </div>
</NcMenuItem> </NcMenuItem>
</NcMenu> </NcMenu>
@ -1521,7 +1539,10 @@ onKeyStroke('ArrowDown', onDown)
</div> </div>
<div <div
v-if="!readOnly" v-if="!readOnly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }" :class="{
hidden: !row.rowMeta.selected,
flex: row.rowMeta.selected,
}"
class="nc-row-expand-and-checkbox" class="nc-row-expand-and-checkbox"
> >
<a-checkbox v-model:checked="row.rowMeta.selected" /> <a-checkbox v-model:checked="row.rowMeta.selected" />
@ -1544,7 +1565,9 @@ onKeyStroke('ArrowDown', onDown)
v-if="row.rowMeta?.commentCount && expandForm" v-if="row.rowMeta?.commentCount && expandForm"
v-e="['c:expanded-form:open']" v-e="['c:expanded-form:open']"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)" class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
:style="{ backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length] }" :style="{
backgroundColor: enumColor.light[row.rowMeta.commentCount % enumColor.light.length],
}"
@click="expandAndLooseFocus(row, state)" @click="expandAndLooseFocus(row, state)"
> >
{{ row.rowMeta.commentCount }} {{ row.rowMeta.commentCount }}
@ -1674,7 +1697,11 @@ onKeyStroke('ArrowDown', onDown)
? 'z-3' ? 'z-3'
: 'z-4' : 'z-4'
" "
:style="{ top: `${fillHandleTop}px`, left: `${fillHandleLeft}px`, cursor: 'crosshair' }" :style="{
top: `${fillHandleTop}px`,
left: `${fillHandleLeft}px`,
cursor: 'crosshair',
}"
/> />
</div> </div>
@ -1696,7 +1723,16 @@ onKeyStroke('ArrowDown', onDown)
data-testid="nc-delete-row" data-testid="nc-delete-row"
@click="deleteSelectedRows" @click="deleteSelectedRows"
> >
<div v-e="['a:row:delete-bulk']" class="flex gap-2 items-center"> <div
v-if="data.filter((r) => r.rowMeta.selected).length === 1"
v-e="['a:row:delete']"
class="flex gap-2 items-center"
>
<component :is="iconMap.delete" />
<!-- Delete Selected Rows -->
{{ $t('activity.deleteSelectedRow') }}
</div>
<div v-else v-e="['a:row:delete-bulk']" class="flex gap-2 items-center">
<component :is="iconMap.delete" /> <component :is="iconMap.delete" />
<!-- Delete Selected Rows --> <!-- Delete Selected Rows -->
{{ $t('activity.deleteSelectedRow') }} {{ $t('activity.deleteSelectedRow') }}
@ -1847,7 +1883,9 @@ onKeyStroke('ArrowDown', onDown)
> >
<div data-testid="nc-pagination-add-record" class="flex items-center px-2 text-gray-600 hover:text-black"> <div data-testid="nc-pagination-add-record" class="flex items-center px-2 text-gray-600 hover:text-black">
<span> <span>
<template v-if="isAddNewRecordGridMode"> {{ $t('activity.newRecord') }} </template> <template v-if="isAddNewRecordGridMode">
{{ $t('activity.newRecord') }}
</template>
<template v-else> {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }} </template> <template v-else> {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }} </template>
</span> </span>
</div> </div>
@ -1875,7 +1913,9 @@ onKeyStroke('ArrowDown', onDown)
<GeneralIcon v-if="isAddNewRecordGridMode" icon="check" class="w-4 h-4 text-primary" /> <GeneralIcon v-if="isAddNewRecordGridMode" icon="check" class="w-4 h-4 text-primary" />
</div> </div>
<div class="flex flex-row text-xs text-gray-400 ml-7.25">{{ $t('labels.addRowGrid') }}</div> <div class="flex flex-row text-xs text-gray-400 ml-7.25">
{{ $t('labels.addRowGrid') }}
</div>
</div> </div>
<div <div
v-e="['c:row:add:form']" v-e="['c:row:add:form']"
@ -1890,7 +1930,9 @@ onKeyStroke('ArrowDown', onDown)
<GeneralIcon v-if="!isAddNewRecordGridMode" icon="check" class="w-4 h-4 text-primary" /> <GeneralIcon v-if="!isAddNewRecordGridMode" icon="check" class="w-4 h-4 text-primary" />
</div> </div>
<div class="flex flex-row text-xs text-gray-400 ml-7.05">{{ $t('labels.addRowForm') }}</div> <div class="flex flex-row text-xs text-gray-400 ml-7.05">
{{ $t('labels.addRowForm') }}
</div>
</div> </div>
</div> </div>
</div> </div>

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

@ -860,13 +860,12 @@ export function useData(args: {
) { ) {
isPaginationLoading.value = true isPaginationLoading.value = true
try { try {
const bulkDeletedRowsData = await $api.dbDataTableRow.delete(metaValue?.id as string, rows, { const bulkDeletedRowsData = await $api.dbDataTableRow.delete(metaValue?.id as string, rows.length === 1 ? rows[0] : rows, {
viewId: viewMetaValue?.id as string, viewId: viewMetaValue?.id as string,
}) })
await callbacks?.syncCount?.() await callbacks?.syncCount?.()
return rows.length === 1 && bulkDeletedRowsData ? [bulkDeletedRowsData] : bulkDeletedRowsData
return bulkDeletedRowsData
} catch (error: any) { } catch (error: any) {
message.error(await extractSdkResponseErrorMsg(error)) message.error(await extractSdkResponseErrorMsg(error))
} finally { } finally {

76
tests/playwright/tests/db/features/webhook.spec.ts

@ -759,9 +759,79 @@ test.describe.serial('Webhook', () => {
} }
} }
console.log('rsp', rsp[0]);
console.log('expectedData', expectedData);
expect(isSubset(rsp[0], expectedData)).toBe(true); expect(isSubset(rsp[0], expectedData)).toBe(true);
}); });
test('Delete operations', async ({ request }) => {
async function verifyDeleteOperation(rsp, type, deleteCount) {
// kludge- add delay to allow server to process webhook
await new Promise(resolve => setTimeout(resolve, 1000));
expect(rsp[rsp.length - 1].type).toBe(type);
expect(rsp[rsp.length - 1].data.table_name).toBe('Test');
if (deleteCount !== null) {
expect(rsp[rsp.length - 1].data.rows.length).toBe(deleteCount);
}
}
test.slow();
await clearServerData({ request });
// close 'Team & Auth' tab
await dashboard.closeTab({ title: 'Team & Auth' });
await dashboard.treeView.createTable({ title: 'Test', baseTitle: context.base.title });
// after insert hook
await webhook.create({
title: 'hook-1',
event: 'After Delete',
});
// after insert hook
await webhook.create({
title: 'hook-2',
event: 'After Bulk Delete',
});
const titles = ['Poole', 'Delaware', 'Pabalo', 'John', 'Vicky', 'Tom'];
for (let i = 0; i < titles.length; i++) {
await dashboard.grid.addNewRow({
index: i,
columnHeader: 'Title',
value: titles[i],
});
}
// Select one record and delete
await dashboard.grid.selectRow(0);
await dashboard.grid.deleteSelectedRows();
let rsp = await getWebhookResponses({ request, count: 1 });
await verifyDeleteOperation(rsp, 'records.after.delete', null);
// Select multiple records and delete
await dashboard.grid.selectRow(0);
await dashboard.grid.selectRow(1);
await dashboard.grid.deleteSelectedRows();
rsp = await getWebhookResponses({ request, count: 2 });
await verifyDeleteOperation(rsp, 'records.after.bulkDelete', 2);
// Right click and delete record
await dashboard.grid.deleteRow(0);
rsp = await getWebhookResponses({ request, count: 3 });
await verifyDeleteOperation(rsp, 'records.after.delete', null);
// Select range and delete records
await dashboard.grid.selectRange({
start: { index: 0, columnHeader: 'Title' },
end: { index: 1, columnHeader: 'Title' },
});
await dashboard.grid.deleteRow(0);
rsp = await getWebhookResponses({ request, count: 4 });
await verifyDeleteOperation(rsp, 'records.after.bulkDelete', 2);
});
}); });

Loading…
Cancel
Save