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

Basic implementation of batch actions #14

Merged
merged 1 commit into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/lib/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,19 @@ export abstract class DefaultAction implements Action {

/** */
export class CallbackAction extends DefaultAction {
private readonly _callback: (item?: object | undefined) => void;
private readonly _callback: (item?: unknown) => void;

constructor(
label: string,
icon: Optional<ActionIcon>,
callback: (item?: object | undefined) => void,
callback: (item?: unknown) => void,
options?: ActionOptions
) {
super(label, icon, options);
this._callback = callback;
}

public call(item?: object | undefined): unknown {
public call(item?: unknown): unknown {
return this._callback.call(null, item);
}
}
Expand All @@ -79,6 +79,13 @@ export class UrlAction extends DefaultAction {
item: object & { [key: string]: string | number | boolean } = {},
identifierFieldName: string = 'id'
): string {
if (Array.isArray(item)) {
console.warn(
'Provided item for UrlAction is an array, and arrays are not supported. Using the first item of the array, or an empty object if not set.'
);
item = item[0] ?? {};
}

let url = this._url || '';

const mightNeedId = item[identifierFieldName] !== undefined;
Expand Down
8 changes: 4 additions & 4 deletions src/lib/Crud/Operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface CrudOperation {
/** */ readonly label: string;
/** */ readonly displayComponentName: CrudTheme;
/** */ readonly fields: Array<FieldInterface<FieldOptions>>;
/** */ readonly actions: Array<Action>;
/** */ readonly contextActions: Array<Action>;
/** */ readonly options: Record<string, string | unknown>;

/** */
Expand Down Expand Up @@ -75,7 +75,7 @@ export abstract class BaseCrudOperation implements CrudOperation {
/** */ public readonly label: string,
/** */ public readonly displayComponentName: CrudTheme,
/** */ public readonly fields: Array<FieldInterface<FieldOptions>>,
/** */ public readonly actions: Array<Action>,
/** */ public readonly contextActions: Array<Action>,
/** */ public readonly options: Record<string, string | unknown> = {}
) {}

Expand Down Expand Up @@ -175,14 +175,14 @@ export class List extends BaseCrudOperation {
/** */
constructor(
fields: Array<FieldInterface<FieldOptions>>,
actions: Array<Action> = [],
itemsActions: Array<Action> = [],
options: Partial<ListOperationOptions> = {}
) {
options.globalActions ??= [];
options.batchActions ??= [];
options.pagination = { ...defaultPaginationOptions(), ...(options.pagination || {}) };
options.filters ??= [];
super('list', 'crud.list.label', 'list', fields, actions, options);
super('list', 'crud.list.label', 'list', fields, itemsActions, options);
this.options = options as ListOperationOptions;
}
}
Expand Down
12 changes: 5 additions & 7 deletions src/lib/themes/svelte/carbon/Crud/CrudList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
let page: number | undefined;

const configuredFilters = operation.options?.filters || [];
const actions = operation.actions;
const actions = operation.contextActions;
const sortableDataTable =
operation.fields.filter((field: BaseField<FieldOptions>) => !field.options?.sortable).length >
0;
Expand All @@ -42,13 +42,14 @@
let showPagination = operation.options.pagination.enabled;
let rows: Promise<unknown>;
let paginator: PaginatedResults<unknown> | undefined;
let globalActions: Array<Action> = [];
let globalActions: Array<Action> = operation.options.globalActions || [];
let batchActions: Array<Action> = operation.options.batchActions || [];

if (!crud.options.stateProvider) {
throw new Error(`No StateProvider was given to the "${crud.name}" CRUD.`);
}

if ((!operation) instanceof List) {
if (!(operation instanceof List)) {
throw new Error(
'CrudList view can only accept operations that are instances of the List operation.'
);
Expand Down Expand Up @@ -113,10 +114,6 @@
});
}

if (operation instanceof List<unknown> || operation.options?.globalActions?.length) {
globalActions = operation.options.globalActions;
}

function onPaginationUpdate(event: CustomEvent<{ page: number; pageSize: number }>) {
page = event.detail.page;
requestParameters.page = event.detail.page;
Expand Down Expand Up @@ -159,6 +156,7 @@
{rows}
{actions}
{globalActions}
{batchActions}
{page}
{operation}
{onSort}
Expand Down
40 changes: 35 additions & 5 deletions src/lib/themes/svelte/carbon/DataTable/DataTable.svelte
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { _ } from 'svelte-i18n';
import DataTable from 'carbon-components-svelte/src/DataTable/DataTable.svelte';
import DataTable, {
type DataTableRowId
} from 'carbon-components-svelte/src/DataTable/DataTable.svelte';
import DataTableSkeleton from 'carbon-components-svelte/src/DataTable/DataTableSkeleton.svelte';
import InlineNotification from 'carbon-components-svelte/src/Notification/InlineNotification.svelte';
import Loading from 'carbon-components-svelte/src/Loading/Loading.svelte';
import Toolbar from 'carbon-components-svelte/src/DataTable/Toolbar.svelte';
import ToolbarBatchActions from 'carbon-components-svelte/src/DataTable/ToolbarBatchActions.svelte';

import DataTableToolbar from '$lib/themes/svelte/carbon/DataTable/Toolbar/DataTableToolbar.svelte';
import ItemActions from '$lib/themes/svelte/carbon/DataTable/actions/ItemActions.svelte';

import type { Headers, Row, Rows } from '$lib/DataTable';
import type { Action } from '$lib/Actions';
import type { FilterInterface, FilterOptions } from '$lib/Filter';
import type { ThemeConfig } from '$lib/types';
import type { SubmittedData } from '$lib/Crud/Form';
import { type FieldInterface, type FieldOptions, TextField } from '$lib';
import ToolbarAction from '$lib/themes/svelte/carbon/DataTable/Toolbar/ToolbarAction.svelte';

export let headers: Headers = [];
export let rows: Promise<Rows>;
export let actions: Action[] = [];
export let globalActions: Array<Action> = [];
export let batchActions: Action[] = [];
export let filters: Array<FilterInterface<FilterOptions>> = [];
export let page: number | undefined;
export let theme: ThemeConfig;
export let sortable: boolean;
export let onSort: () => unknown | undefined;

let actionsCellIndex = -1;
let batchSelectionIsActive = false;
let selectedRowIds: ReadonlyArray<DataTableRowId> = [];

if (actions.length) {
headers.push({
Expand All @@ -34,17 +44,16 @@
actionsCellIndex = headers.length - 1;
}

function getFieldFromRow(fieldName: string, row: Row) {
function getFieldFromRow(fieldName: string, row: Row): FieldInterface<FieldOptions> {
if (!row.__crud_operation) {
console.error('Internal "__crud_operation" property isn\'t properly injected.');
return theme.viewFields.default;
throw new Error('Internal "__crud_operation" property isn\'t properly injected.');
}

const matchingFields = row.__crud_operation.fields.filter((f) => f.name === fieldName);

if (!matchingFields.length) {
console.warn(`Field "${fieldName}" was not found in current operation.`);
return theme.viewFields.default;
return new TextField(fieldName, fieldName);
}

if (matchingFields.length > 1) {
Expand All @@ -71,6 +80,11 @@
function onFiltersSubmit(event: CustomEvent<SubmittedData>) {
dispatchEvent('submitFilters', event.detail);
}

function onCancelSelection(event: CustomEvent<null>) {
event.preventDefault();
batchSelectionIsActive = false;
}
</script>

{#await rows}
Expand All @@ -81,9 +95,12 @@
{page}
{sortable}
zebra
selectable={batchActions.length > 0}
batchSelection={batchSelectionIsActive}
rows={resolvedRows}
size="short"
on:click:header={onSort}
bind:selectedRowIds
{...$$restProps}
>
<svelte:fragment slot="title">
Expand All @@ -100,6 +117,19 @@
on:submitFilters={onFiltersSubmit}
/>
{/if}
{#if batchActions.length > 0}
<Toolbar>
<ToolbarBatchActions bind:active={batchSelectionIsActive} on:cancel={onCancelSelection}>
{#each batchActions as action}
<ToolbarAction {action} action_arguments={[selectedRowIds]} />
{/each}
</ToolbarBatchActions>
<!--<ToolbarContent>-->
<!-- <Button on:click={() => (active = true)}>Edit rows</Button>-->
<!--</ToolbarContent>-->
</Toolbar>
{/if}

{#if !resolvedRows.length}
<InlineNotification kind="warning" hideCloseButton={true} lowContrast={true}>
{$_('error.crud.list.no_elements')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import FilterReset from 'carbon-icons-svelte/lib/FilterReset.svelte';

import type { Action } from '$lib/Actions';
import ActionComponent from '$lib/themes/svelte/carbon/DataTable/Toolbar/ToolbarAction.svelte';
import ToolbarAction from '$lib/themes/svelte/carbon/DataTable/Toolbar/ToolbarAction.svelte';
import FilterComponent from '$lib/themes/svelte/carbon/DataTable/Toolbar/ToolbarFilter.svelte';
import type { Filter, FilterOptions } from '$lib/Filter';
import type { ThemeConfig } from '$lib/types';
Expand Down Expand Up @@ -58,7 +58,7 @@
<Toolbar>
<ToolbarContent>
{#each actions as action}
<ActionComponent {action} />
<ToolbarAction {action} />
{/each}
</ToolbarContent>
</Toolbar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
import { type Action, CallbackAction, UrlAction } from '$lib/Actions';

export let action: Action;
export let action_arguments: Array<unknown> = [];
</script>

{#if action instanceof UrlAction}
<Button
href={action.url()}
href={action.url(...action_arguments) || ''}
icon={action.icon}
kind={action.options?.buttonKind}
{...action.options.htmlAttributes}
Expand All @@ -20,7 +21,7 @@
</Button>
{:else if action instanceof CallbackAction}
<Button
on:click={async () => await action.call()}
on:click={async () => await action.call(...action_arguments)}
icon={action.icon}
kind={action.options?.buttonKind}
{...action.options.htmlAttributes}
Expand Down
14 changes: 14 additions & 0 deletions src/testApp/TestCrud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ export const testCrud = new CrudDefinition<Test>({
),
new UrlAction('New', '/admin/tests/new', Pen)
],
batchActions: [
new CallbackAction('Testing batch actions', ViewIcon, function (item?: unknown) {
if (!Array.isArray(item)) {
if (item) {
console.warn('Item used for Callback action is not an array.');
}
item = [];
}
if (!item) {
item = [];
}
success('Selected IDs to process:\n' + (item as Array<string>).join('\n'));
})
],
filters: [
new TextFilter('text_field', 'Filter text'),
new BooleanFilter('checkbox_field', 'Filter checkbox'),
Expand Down
Loading