Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RPC records CRUD operations on front end #3759

Merged
merged 10 commits into from
Aug 26, 2024
26 changes: 26 additions & 0 deletions mathesar_ui/src/api/rpc/records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,31 @@ export interface RecordsResponse {
}

export const records = {
add: rpcMethodTypeContainer<
{
database_id: number;
table_oid: number;
/** Keys are stringified attnums */
record_def: Record<string, ResultValue>;
},
Pick<RecordsResponse, 'results' | 'preview_data'>
>(),

patch: rpcMethodTypeContainer<
{
database_id: number;
table_oid: number;
record_id: ResultValue;
/** Keys are stringified attnums */
record_def: Record<string, ResultValue>;
},
Pick<RecordsResponse, 'results' | 'preview_data'>
>(),

list: rpcMethodTypeContainer<RecordsListParams, RecordsResponse>(),

delete: rpcMethodTypeContainer<
{ database_id: number; table_oid: number; record_ids: ResultValue[] },
void
>(),
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
async function handleProceedButton() {
try {
allowClose = false;
await onProceed();
onSuccess();
const result = await onProceed();
onSuccess(result);
$resolve(true);
modal.close();
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ interface ButtonDetails {
icon?: IconProps;
}

export interface ConfirmationProps {
export interface ConfirmationProps<T> {
title?: string | ComponentAndProps;
/** An array of strings will be transformed into paragraphs. */
body: string | string[] | ComponentAndProps;
proceedButton: ButtonDetails;
cancelButton: ButtonDetails;
onProceed: () => Promise<void>;
onSuccess: () => void;
onProceed: () => Promise<T>;
onSuccess: (value: T) => void;
onError: (error: Error) => void;
}

const baseConfirmationProps: ConfirmationProps = {
const baseConfirmationProps: ConfirmationProps<unknown> = {
body: 'Are you sure?',
proceedButton: {
label: 'Yes',
Expand All @@ -37,23 +37,23 @@ const baseConfirmationProps: ConfirmationProps = {
export class ConfirmationController {
modal: ModalController;

confirmationProps: Writable<ConfirmationProps>;
confirmationProps: Writable<ConfirmationProps<unknown>>;

resolve = writable<(isConfirmed: boolean) => void>(() => {});

canProceed = writable(true);

constructor(
modalController: ModalController,
initialConfirmationProps: ConfirmationProps,
initialConfirmationProps: ConfirmationProps<unknown>,
) {
this.modal = modalController;
this.confirmationProps = writable(initialConfirmationProps);
}
}

interface MakeConfirm {
confirm: (props: Partial<ConfirmationProps>) => Promise<boolean>;
confirm: <T>(props: Partial<ConfirmationProps<T>>) => Promise<boolean>;
confirmationController: ConfirmationController;
}

Expand All @@ -62,7 +62,7 @@ export function makeConfirm({
defaultConfirmationProps,
}: {
confirmationModal: ModalController;
defaultConfirmationProps?: ConfirmationProps;
defaultConfirmationProps?: ConfirmationProps<unknown>;
}): MakeConfirm {
const fullDefaultConfirmationProps = {
...baseConfirmationProps,
Expand All @@ -72,16 +72,19 @@ export function makeConfirm({
confirmationModal,
fullDefaultConfirmationProps,
);
async function confirm(props: Partial<ConfirmationProps>) {
return new Promise<boolean>((resolve) => {
controller.resolve.set(resolve);
controller.canProceed.set(true);
controller.confirmationProps.set({
return {
async confirm(props) {
const fullProps = {
...fullDefaultConfirmationProps,
...props,
} as ConfirmationProps<unknown>;
return new Promise<boolean>((resolve) => {
controller.resolve.set(resolve);
controller.canProceed.set(true);
controller.confirmationProps.set(fullProps);
controller.modal.open();
});
controller.modal.open();
});
}
return { confirm, confirmationController: controller };
},
confirmationController: controller,
};
}
6 changes: 3 additions & 3 deletions mathesar_ui/src/stores/confirmation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ export const { confirm, confirmationController } = makeConfirm({
confirmationModal,
});

interface ConfirmDeleteProps extends Partial<ConfirmationProps> {
interface ConfirmDeleteProps<T> extends Partial<ConfirmationProps<T>> {
/** e.g. the name of the table, column, etc */
identifierName?: string;
/** the "thing" you're deleting, e.g. 'column', 'table', 'tables', '3 rows' etc. */
identifierType: string;
}

export function confirmDelete(
props: ConfirmDeleteProps,
export function confirmDelete<T>(
props: ConfirmDeleteProps<T>,
): ReturnType<typeof confirm> {
const type = props.identifierType;

Expand Down
177 changes: 96 additions & 81 deletions mathesar_ui/src/stores/table-data/records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,8 @@ export class RecordsData {
return undefined;
}

async deleteSelected(rowSelectionIds: Iterable<string>): Promise<void> {
/** @returns the number of selected rows deleted */
async deleteSelected(rowSelectionIds: Iterable<string>): Promise<number> {
const ids =
typeof rowSelectionIds === 'string' ? [rowSelectionIds] : rowSelectionIds;
const pkColumn = get(this.columnsDataStore.pkColumn);
Expand All @@ -457,7 +458,7 @@ export class RecordsData {
const rowKeys = [...primaryKeysOfSavedRows, ...identifiersOfUnsavedRows];

if (rowKeys.length === 0) {
return;
return 0;
}

this.meta.rowDeletionStatus.setMultiple(rowKeys, { state: 'processing' });
Expand All @@ -478,8 +479,13 @@ export class RecordsData {
if (keysToDelete.length > 0) {
const recordIds = [...keysToDelete];
try {
throw new Error('Not implemented'); // TODO_BETA
// await deleteAPI<RowKey>(bulkDeleteURL, { pks: recordIds });
await api.records
.delete({
database_id: this.apiContext.database_id,
table_oid: this.apiContext.table_oid,
record_ids: recordIds,
})
.run();
keysToDelete.forEach((key) => successRowKeys.add(key));
} catch (error) {
failures.set(keysToDelete.join(','), getErrorMessage(error));
Expand Down Expand Up @@ -535,6 +541,8 @@ export class RecordsData {
const apiMsg = [...failures.values()].join('\n');
throw new Error(`${uiMsg} ${apiMsg}`);
}

return primaryKeysOfSavedRows.length + identifiersOfUnsavedRows.length;
}

// TODO: It would be better to throw errors instead of silently failing
Expand Down Expand Up @@ -564,35 +572,39 @@ export class RecordsData {
this.meta.cellModificationStatus.set(cellKey, { state: 'processing' });
this.updatePromises?.get(cellKey)?.cancel();

throw new Error('Not implemented'); // TODO_BETA
// const promise = patchAPI<RecordsResponse>(
// `${this.url}${String(primaryKeyValue)}/`,
// { [column.id]: record[column.id] },
// );

// if (!this.updatePromises) {
// this.updatePromises = new Map();
// }
// this.updatePromises.set(cellKey, promise);

// try {
// const result = await promise;
// this.meta.cellModificationStatus.set(cellKey, { state: 'success' });
// return {
// ...row,
// record: result.results[0],
// };
// } catch (err) {
// this.meta.cellModificationStatus.set(cellKey, {
// state: 'failure',
// errors: [`Unable to save cell. ${getErrorMessage(err)}`],
// });
// } finally {
// if (this.updatePromises.get(cellKey) === promise) {
// this.updatePromises.delete(cellKey);
// }
// }
// return row;
const promise = api.records
.patch({
...this.apiContext,
record_id: primaryKeyValue,
record_def: {
[String(column.id)]: record[column.id],
},
})
.run();

if (!this.updatePromises) {
this.updatePromises = new Map();
}
this.updatePromises.set(cellKey, promise);

try {
const result = await promise;
this.meta.cellModificationStatus.set(cellKey, { state: 'success' });
return {
...row,
record: result.results[0],
};
} catch (err) {
this.meta.cellModificationStatus.set(cellKey, {
state: 'failure',
errors: [`Unable to save cell. ${getErrorMessage(err)}`],
});
} finally {
if (this.updatePromises.get(cellKey) === promise) {
this.updatePromises.delete(cellKey);
}
}
return row;
}

getNewEmptyRecord(): NewRecordRow {
Expand Down Expand Up @@ -636,55 +648,58 @@ export class RecordsData {
const rowKeyOfBlankRow = getRowKey(row, pkColumn?.id);
this.meta.rowCreationStatus.set(rowKeyOfBlankRow, { state: 'processing' });
this.createPromises?.get(rowKeyOfBlankRow)?.cancel();
const requestRecord = {
...Object.fromEntries(this.contextualFilters),
...row.record,
};

throw new Error('Not implemented'); // TODO_BETA

// const promise = postAPI<RecordsResponse>(this.url, requestRecord);
// if (!this.createPromises) {
// this.createPromises = new Map();
// }
// this.createPromises.set(rowKeyOfBlankRow, promise);

// try {
// const response = await promise;
// const record = response.results[0];
// let newRow: NewRecordRow = {
// ...row,
// record,
// };
// if (isPlaceholderRow(newRow)) {
// const { isAddPlaceholder, ...newRecordRow } = newRow;
// newRow = newRecordRow;
// }

// const rowKeyWithRecord = getRowKey(newRow, pkColumn?.id);
// this.meta.rowCreationStatus.delete(rowKeyOfBlankRow);
// this.meta.rowCreationStatus.set(rowKeyWithRecord, { state: 'success' });
// this.newRecords.update((existing) =>
// existing.map((entry) => {
// if (entry.identifier === row.identifier) {
// return newRow;
// }
// return entry;
// }),
// );
// this.totalCount.update((count) => (count ?? 0) + 1);
// return newRow;
// } catch (err) {
// this.meta.rowCreationStatus.set(rowKeyOfBlankRow, {
// state: 'failure',
// errors: [getErrorMessage(err)],
// });
// } finally {
// if (this.createPromises.get(rowKeyOfBlankRow) === promise) {
// this.createPromises.delete(rowKeyOfBlankRow);
// }
// }
// return row;
const promise = api.records
.add({
...this.apiContext,
record_def: {
...Object.fromEntries(this.contextualFilters),
...row.record,
},
})
.run();

if (!this.createPromises) {
this.createPromises = new Map();
}
this.createPromises.set(rowKeyOfBlankRow, promise);

try {
const response = await promise;
const record = response.results[0];
let newRow: NewRecordRow = {
...row,
record,
};
if (isPlaceholderRow(newRow)) {
const { isAddPlaceholder, ...newRecordRow } = newRow;
newRow = newRecordRow;
}

const rowKeyWithRecord = getRowKey(newRow, pkColumn?.id);
this.meta.rowCreationStatus.delete(rowKeyOfBlankRow);
this.meta.rowCreationStatus.set(rowKeyWithRecord, { state: 'success' });
this.newRecords.update((existing) =>
existing.map((entry) => {
if (entry.identifier === row.identifier) {
return newRow;
}
return entry;
}),
);
this.totalCount.update((count) => (count ?? 0) + 1);
return newRow;
} catch (err) {
this.meta.rowCreationStatus.set(rowKeyOfBlankRow, {
state: 'failure',
errors: [getErrorMessage(err)],
});
} finally {
if (this.createPromises.get(rowKeyOfBlankRow) === promise) {
this.createPromises.delete(rowKeyOfBlankRow);
}
}
return row;
}

async createOrUpdateRecord(
Expand Down
3 changes: 2 additions & 1 deletion mathesar_ui/src/systems/table-view/Body.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
$: ({ oid } = table);
$: ({ displayableRecords } = display);
$: ({ pkColumn } = columnsDataStore);
$: canEditTableRecords = true; // TODO_BETA: Implement permissions here

function getItemSizeFromRow(row: RowType) {
if (isHelpTextRow(row)) {
Expand Down Expand Up @@ -62,7 +63,7 @@
>
<ScrollAndResetHandler {api} />
{#each items as item (item.key)}
{#if $displayableRecords[item.index] && !isPlaceholderRow($displayableRecords[item.index])}
{#if $displayableRecords[item.index] && !(isPlaceholderRow($displayableRecords[item.index]) && !canEditTableRecords)}
<Row style={item.style} bind:row={$displayableRecords[item.index]} />
{/if}
{/each}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@
],
onProceed: () => recordsData.deleteSelected(selectedRowIds),
onError: (e) => toast.fromError(e),
onSuccess: () => {
onSuccess: (count) => {
toast.success({
title: $_('count_records_deleted_successfully', {
values: { count: selectedRowCount },
values: { count },
}),
});
},
Expand Down
Loading