diff --git a/mathesar_ui/src/systems/table-view/context-menu/contextMenu.ts b/mathesar_ui/src/systems/table-view/context-menu/contextMenu.ts index 907505d1db..bc1f6b0019 100644 --- a/mathesar_ui/src/systems/table-view/context-menu/contextMenu.ts +++ b/mathesar_ui/src/systems/table-view/context-menu/contextMenu.ts @@ -5,6 +5,7 @@ import { type ClientPosition, type ContextMenuController, type ModalController, + buttonMenuEntry, menuSection, subMenu, } from '@mathesar/component-library'; @@ -18,10 +19,10 @@ import type RecordStore from '@mathesar/systems/record-view/RecordStore'; import { takeFirstAndOnly } from '@mathesar/utils/iterUtils'; import { match } from '@mathesar/utils/patternMatching'; +import { getRowActionsData } from '../row-actions/RowActionsDataProvider'; + import { copyCells } from './entries/copyCells'; import { deleteColumn } from './entries/deleteColumn'; -import { deleteRecords } from './entries/deleteRecords'; -import { duplicateRecord } from './entries/duplicateRecord'; import { modifyFilters } from './entries/modifyFilters'; import { modifyGrouping } from './entries/modifyGrouping'; import { modifySorting } from './entries/modifySorting'; @@ -55,15 +56,50 @@ export function openTableCellContextMenu({ const { selection } = tabularData; function* getEntriesForMultipleRows(rowIds: string[]) { - yield* deleteRecords({ tabularData, rowIds }); + const rowActionsData = getRowActionsData({ + rowIds, + tabularData, + modalRecordView, + }); + + for (const action of rowActionsData.actions) { + if (action.id === 'delete-records') { + yield buttonMenuEntry({ + icon: action.icon, + label: action.label, + danger: action.danger, + onClick: action.onClick, + }); + } + } } function* getEntriesForOneRow(rowId: string) { const recordId = tabularData.getRecordIdFromRowId(rowId); yield* viewRowRecord({ tabularData, recordId, modalRecordView }); - yield* duplicateRecord({ tabularData, rowId }); - yield* getEntriesForMultipleRows([rowId]); + const rowActionsData = getRowActionsData({ + rowIds: [rowId], + tabularData, + modalRecordView, + }); + + for (const action of rowActionsData.actions) { + if (action.id === 'duplicate-record') { + yield buttonMenuEntry({ + icon: action.icon, + label: action.label, + onClick: action.onClick, + }); + } else if (action.id === 'delete-records') { + yield buttonMenuEntry({ + icon: action.icon, + label: action.label, + danger: action.danger, + onClick: action.onClick, + }); + } + } } function* getEntriesForArbitraryRows(rowIds: Iterable) { @@ -109,7 +145,6 @@ export function openTableCellContextMenu({ } function* getEntriesForMultipleCells(cellIds: string[]) { - // Check if any of the selected cells are joined columns const allColumns = get(tabularData.allColumns); const hasJoinedColumn = cellIds.some((cellId) => { const { columnId } = parseCellId(cellId); diff --git a/mathesar_ui/src/systems/table-view/row-actions/RowActionsDataProvider.ts b/mathesar_ui/src/systems/table-view/row-actions/RowActionsDataProvider.ts new file mode 100644 index 0000000000..9b23bb1778 --- /dev/null +++ b/mathesar_ui/src/systems/table-view/row-actions/RowActionsDataProvider.ts @@ -0,0 +1,156 @@ +import { get } from 'svelte/store'; +import { _ } from 'svelte-i18n'; + +import { + iconDeleteMajor, + iconDuplicateRecord, + iconLinkToRecordPage, + iconModalRecordView, +} from '@mathesar/icons'; +import { confirm } from '@mathesar/stores/confirmation'; +import { storeToGetRecordPageUrl } from '@mathesar/stores/storeBasedUrls'; +import type { TabularData } from '@mathesar/stores/table-data'; +import { currentTablesMap } from '@mathesar/stores/tables'; +import { toast } from '@mathesar/stores/toast'; +import RecordStore from '@mathesar/systems/record-view/RecordStore'; +import type { ModalController } from '@mathesar-component-library'; +import type { IconProps } from '@mathesar-component-library/types'; + +export type RowIdentifier = string | number; + +export interface RowAction { + id: string; + label: string; + onClick: () => void; + icon?: IconProps; + href?: string; + danger?: boolean; + disabled?: boolean; +} + +export interface RowActionsData { + actions: RowAction[]; +} + +export function getRowActionsData(params: { + rowIds: RowIdentifier[]; + tabularData: TabularData; + modalRecordView?: ModalController; +}): RowActionsData { + const { rowIds, tabularData, modalRecordView } = params; + const actions: RowAction[] = []; + + const canInsertRecords = get(tabularData.canInsertRecords); + const canDeleteRecords = get(tabularData.canDeleteRecords); + const canViewLinkedEntities = get(tabularData.canViewLinkedEntities); + + const rows = get(tabularData.recordsData.selectableRowsMap); + + const firstRowId = rowIds[0]; + const firstRow = + firstRowId !== undefined ? rows.get(String(firstRowId)) : undefined; + + let recordId: string | number | undefined; + if (firstRow) { + try { + const result = tabularData.getRecordIdFromRowId(String(firstRowId)); + recordId = + typeof result === 'string' || typeof result === 'number' + ? result + : undefined; + } catch { + recordId = undefined; + } + } + + if ( + canViewLinkedEntities && + rowIds.length === 1 && + recordId !== undefined && + modalRecordView + ) { + actions.push({ + id: 'quick-view-record', + label: get(_)('quick_view_record'), + icon: iconModalRecordView, + onClick: () => { + const containingTable = get(currentTablesMap).get( + tabularData.table.oid, + ); + if (!containingTable) return; + + const recordStore = new RecordStore({ + table: containingTable, + recordPk: String(recordId), + }); + + modalRecordView.open(recordStore); + }, + }); + } + + if (canViewLinkedEntities && rowIds.length === 1 && recordId !== undefined) { + const getRecordPageUrl = get(storeToGetRecordPageUrl); + const recordPageUrl = getRecordPageUrl({ + tableId: tabularData.table.oid, + recordId, + }); + + if (recordPageUrl) { + actions.push({ + id: 'open-record', + label: get(_)('open_record'), + icon: iconLinkToRecordPage, + href: recordPageUrl, + onClick: () => {}, + }); + } + } + + if (canInsertRecords && rowIds.length === 1 && firstRow) { + actions.push({ + id: 'duplicate-record', + label: get(_)('duplicate_record'), + icon: iconDuplicateRecord, + onClick: () => { + void tabularData.recordsData.duplicateRecord(firstRow); + }, + }); + } + + if (canDeleteRecords && rowIds.length > 0) { + actions.push({ + id: 'delete-records', + label: get(_)('delete_records', { + values: { count: rowIds.length }, + }), + icon: iconDeleteMajor, + danger: true, + onClick: () => { + void confirm({ + title: get(_)('delete_records_question', { + values: { count: rowIds.length }, + }), + body: [ + get(_)('deleted_records_cannot_be_recovered', { + values: { count: rowIds.length }, + }), + get(_)('are_you_sure_to_proceed'), + ], + onProceed: () => + tabularData.recordsData.deleteSelected(rowIds.map(String)), + onError: (e) => toast.fromError(e), + onSuccess: (count) => { + toast.success({ + title: get(_)('count_records_deleted_successfully', { + values: { count }, + }), + }); + }, + }); + }, + }); + } + + return { actions }; +} diff --git a/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte b/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte index 53a0e5fdb6..e0aca4976b 100644 --- a/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte +++ b/mathesar_ui/src/systems/table-view/table-inspector/record/RowActions.svelte @@ -1,108 +1,65 @@
- {#if recordPageLink} - - - - - {$_('open_record')} - - {/if} - - + {#each rowActionsData.actions as action} + {@const actionIcon = getActionIcon(action)} + {#if action.href} + + + {action.label} + + {:else if actionIcon} + + {/if} + {/each}