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

Support for external entities #5

Merged
merged 9 commits into from
Feb 10, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
coverage/

/web-components

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# v0.13.0

- Add support for external entities

# v0.12.0

- **BC Break**: CrudDefinition's `name` is no longer an argument, and set in the options instead.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"@vitest/coverage-v8": "^1.2.2",
"axios": "^1.6.2",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
Expand Down
16 changes: 15 additions & 1 deletion src/lib/Crud/Operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ import type { CrudTheme } from '$lib/themes/ThemeConfig';
import { defaultPaginationOptions, type PaginationOptions } from '$lib/DataTable/Pagination';
import type { FilterInterface, FilterOptions } from '$lib/Filter';

export type CrudOperationName = 'new' | 'edit' | 'view' | 'list' | 'delete' | string;
export type CrudOperationName =
| 'new'
| 'edit'
| 'view'
| 'list'
| 'delete'
| 'entity_view'
| 'entity_list'
| string;

export interface CrudOperation {
readonly name: CrudOperationName;
Expand Down Expand Up @@ -126,3 +134,9 @@ export class View extends BaseCrudOperation {
super('view', 'crud.view.label', 'view', fields, []);
}
}

export class Field extends BaseCrudOperation {
constructor(name: CrudOperationName = 'field', options: Record<string, string | unknown> = {}) {
super(name, '', 'field', [], [], options);
}
}
5 changes: 3 additions & 2 deletions src/lib/Dashboard/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ describe(
'Dashboard',
() => {
it('can be instantiated with simple config', () => {
const dashboard = new DashboardDefinition<Book>({
const dashboard = new DashboardDefinition({
adminConfig: {},
cruds: [
new CrudDefinition<Book>('books', {
new CrudDefinition<Book>({
name: 'books',
label: { singular: 'Book', plural: 'Books' },
operations: [new List([])],
stateProvider: new CallbackStateProvider<Book>(() => Promise.resolve(null)),
Expand Down
16 changes: 0 additions & 16 deletions src/lib/DataTable/DataTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import type {
} from 'carbon-components-svelte/types/DataTable/DataTable.svelte';

import type { CrudOperation } from '$lib/Crud/Operations';
import type { CommonFieldOptions, Field } from '$lib/FieldDefinitions/definition';
import type { KeyValueObject } from '$lib/genericTypes';

export type Header = DataTableHeader;
export type Headers = Array<Header>;
Expand All @@ -14,17 +12,3 @@ export type Row = DataTableRow & {
__operation: CrudOperation;
};
export type Rows = Array<Row>;

export function createEmptyRow(operation: CrudOperation): Row {
const fields: KeyValueObject = {};

operation.fields.forEach((field: Field<CommonFieldOptions>) => {
fields[field.name] = '-';
});

return {
id: 0,
__operation: operation,
...fields
};
}
39 changes: 2 additions & 37 deletions src/lib/DataTable/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { describe, it, expect, type TestOptions } from 'vitest';
import { createEmptyRow } from '$lib/DataTable/DataTable';
import {
List,
CheckboxField,
Expand All @@ -20,43 +19,9 @@ const testOpts: TestOptions = {
describe(
'DataTable',
() => {
it('can create an empty row from empty CrudOperation', () => {
const row = createEmptyRow(new List([]));

expect(row).toBeDefined();
expect(row).toStrictEqual({
id: 0
});
});

it('can create an empty row from CrudOperation with many text-based fields', () => {
const fields = [
new CheckboxField('field_1'),
new Field('field_2'),
new NumberField('field_3'),
new TextareaField('field_4'),
new TextField('field_5'),
new ToggleField('field_6'),
new UrlField('field_7')
];
const row = createEmptyRow(new List(fields));

expect(row).toBeDefined();
expect(row).toStrictEqual({
id: 0,
field_1: '-',
field_2: '-',
field_3: '-',
field_4: '-',
field_5: '-',
field_6: '-',
field_7: '-'
});
});

it('can create a Tabs configuration with 1 level of nested fields', () => {
const fields = [
new Tabs([
new Tabs('', '', [
{ name: 'tab_1', fields: [new CheckboxField('field_1')] },
{ name: 'tab_2', fields: [new Field('field_2')] },
{ name: 'tab_3', fields: [new NumberField('field_3')] },
Expand All @@ -73,7 +38,7 @@ describe(

it('can create a Columns configuration with 1 level of nested fields', () => {
const fields = [
new Columns([
new Columns('', '', [
{ name: 'column_1', fields: [new CheckboxField('field_1')] },
{ name: 'column_2', fields: [new Field('field_2')] },
{ name: 'column_3', fields: [new NumberField('field_3')] },
Expand Down
27 changes: 27 additions & 0 deletions src/lib/FieldDefinitions/CrudEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Field, type InputFieldOptions } from '$lib/FieldDefinitions/definition';
import type { FormFieldTheme, ViewFieldTheme } from '$lib/themes/ThemeConfig';
import type { KeyValueObject } from '$lib/genericTypes';

export type CrudEntityOptions = InputFieldOptions & {
crud_name: string;
list_provider_operation?: {
name?: 'entity_list' | string;
options?: KeyValueObject;
label_field: string;
value_field?: 'id' | string;
};
get_provider_operation: {
name?: 'entity_view' | string;
options?: KeyValueObject;
entity_field: string;
};
};

export class CrudEntityField extends Field<CrudEntityOptions> {
readonly formComponent: FormFieldTheme = 'crud_entity';
readonly viewComponent: ViewFieldTheme = 'crud_entity';

constructor(name: string, label: string = '', options: CrudEntityOptions) {
super(name, label, options);
}
}
77 changes: 77 additions & 0 deletions src/lib/actions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect, type TestOptions } from 'vitest';
import { CallbackAction, UrlAction } from '$lib/actions';

const testOpts: TestOptions = {
repeats: process.env.REPEAT ? parseInt(process.env.REPEAT) : undefined
};

describe(
'URL actions',
() => {
it('handles item fields in specified request parameters', () => {
const action = new UrlAction('', '/test/:field1/:field2');
const item = {
field1: 'val1',
field2: 'val2',
field3: 'val3'
};

expect(action.url(item)).toBe('/test/val1/val2');
});

it('handles item ID with default "id" when not specified', () => {
const action = new UrlAction('', '/test/:field1');
const item = {
id: 'identifier',
field1: 'val1'
};

expect(action.url(item)).toBe('/test/val1?id=identifier');
});

it('handles item ID with custom "id" property', () => {
const action = new UrlAction('', '/test/:field1');
const item = {
customId: 'custom_identifier',
field1: 'val1'
};

expect(action.url(item, 'customId')).toBe('/test/val1?id=custom_identifier');
});
},
testOpts
);

describe(
'Callback actions',
() => {
it('calls function', () => {
let called = false;
const callback = () => {
called = true;
return called;
};
const action = new CallbackAction('', null, callback);

expect(action.call()).toBe(true);
expect(called).toBe(true);
});

it('calls function with item as argument', () => {
let called = false;
const baseItem = {
field: 'value'
};
const callback = (item: typeof baseItem): boolean => {
called = true;
item.field = 'newValue';
return called;
};
const action = new CallbackAction('', null, callback);

expect(action.call(baseItem)).toBe(true);
expect(baseItem.field).toBe('newValue');
});
},
testOpts
);
2 changes: 1 addition & 1 deletion src/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class CallbackAction extends DefaultAction {
this._callback = callback;
}

public call(item?: object | undefined): void {
public call(item?: object | undefined): unknown {
return this._callback.call(null, item);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/themes/ThemeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type ThemeConfig = {
viewFields: {
checkbox: ComponentType;
column: ComponentType;
crud_entity: ComponentType;
date: ComponentType;
default: ComponentType;
label: ComponentType;
Expand All @@ -33,6 +34,7 @@ export type ThemeConfig = {
formFields: {
checkbox: ComponentType;
column: ComponentType;
crud_entity: ComponentType;
date: ComponentType;
default: ComponentType;
number: ComponentType;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/themes/carbon/Crud/CrudList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { onMount } from 'svelte';
import Pagination from 'carbon-components-svelte/src/Pagination/Pagination.svelte';

import { createEmptyRow, type Header, type Headers } from '$lib/DataTable/DataTable';
import { type Header, type Headers } from '$lib/DataTable/DataTable';
import type { Field, FieldOptions } from '$lib/FieldDefinitions/definition';
import type { CrudDefinition } from '$lib/Crud/definition';

Expand Down
3 changes: 2 additions & 1 deletion src/lib/themes/carbon/DataTable/DataTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
field={getFieldFromRow(cell.key, row)}
value={cell.display ? cell.display(cell.value) : cell.value}
operation={$$restProps.operation}
entityObject={row}
{theme}
>
{cell.display ? cell.display(cell.value) : cell.value}
Expand All @@ -146,7 +147,7 @@
</svelte:fragment>

<InlineNotification kind="error" hideCloseButton={true} lowContrast={true}>
{$_('error.crud.list.load_error')}<br>
{$_('error.crud.list.load_error')}<br />
{error.toString()}
</InlineNotification>
</DataTable>
Expand Down
61 changes: 61 additions & 0 deletions src/lib/themes/carbon/FormFieldsComponents/CrudEntityField.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script lang="ts">
import InlineNotification from 'carbon-components-svelte/src/Notification/InlineNotification.svelte';
import SelectSkeleton from 'carbon-components-svelte/src/Select/SelectSkeleton.svelte';
import Select from 'carbon-components-svelte/src/Select/Select.svelte';
import SelectItem from 'carbon-components-svelte/src/Select/SelectItem.svelte';
import { _ } from 'svelte-i18n';

import { type CrudOperation, Field } from '$lib/Crud/Operations';
import type { CrudEntityField } from '$lib/FieldDefinitions/CrudEntity';
import { CrudDefinition } from '$lib/Crud/definition';

export let field: CrudEntityField;
export let operation: CrudOperation;
export let value: unknown;

const crud: CrudDefinition<unknown> | undefined =
operation.dashboard.cruds.filter(
(def: CrudDefinition<unknown>) => def.name === field.options.crud_name
)[0] ?? undefined;

function fetchList() {
if (!crud) {
return;
}

const fieldOperation = new Field(
field.options.list_provider_operation?.name ?? 'entity_list',
field.options.list_provider_operation?.options ?? {}
);
fieldOperation.crud = crud;
fieldOperation.dashboard = operation.dashboard;

return crud.options.stateProvider.provide(fieldOperation);
}
</script>

{#if !crud}
<InlineNotification kind="error" hideCloseButton>
{$_('error.crud.could_not_find_crud_name', { values: { crud: field.options.crud_name } })}
</InlineNotification>
{:else}
{#await fetchList()}
<SelectSkeleton labelText={$_(field.label)} />
{:then data}
{@const values = data}
<Select name={field.name} labelText={$_(field.label)} selected={value}>
{#each values as itemValue}
{@const val =
itemValue[field.options.list_provider_operation.value_field ?? 'id'] ?? undefined}
{@const txt = itemValue[field.options.list_provider_operation.label_field] ?? val}
{#if val && txt}
<SelectItem value={val} text={txt} />
{/if}
{/each}
</Select>
{:catch error}
<InlineNotification kind="error" hideCloseButton>
{$_('error.crud.form.entity_field_list_fetch_error', { values: { message: error.message } })}
</InlineNotification>
{/await}
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
throw new Error('Value was expected to be an object, but "' + typeof value + '" given.');
}
if (!value) {
value = {'': ''};
value = { '': '' };
}

let valueEntries = Object.entries(value);
Expand Down
Loading
Loading