From d068283acc610dcde18af5c236b656339cd22ca7 Mon Sep 17 00:00:00 2001 From: Alex Rock Ancelet Date: Mon, 25 Mar 2024 09:38:55 +0100 Subject: [PATCH] Add back some admin files still needed for now --- src/lib/admin/src/ActionParams.ts | 27 +++++++++++ src/lib/admin/src/AssociatedField.ts | 12 +++++ src/lib/admin/src/AssociatedItem.ts | 11 +++++ src/lib/admin/src/CallbackAction.ts | 14 ++++++ src/lib/admin/src/Collection.ts | 10 ++++ src/lib/admin/src/CollectionField.ts | 23 +++++++++ src/lib/admin/src/ConfigFilter.ts | 34 ++++++++++++++ src/lib/admin/src/DefaultAction.ts | 13 ++++++ src/lib/admin/src/Field.ts | 45 ++++++++++++++++++ src/lib/admin/src/FieldHtmlProperties.ts | 11 +++++ src/lib/admin/src/FieldOptions.ts | 34 ++++++++++++++ src/lib/admin/src/FilterType.ts | 9 ++++ src/lib/admin/src/FilterWithValue.ts | 28 +++++++++++ src/lib/admin/src/ItemAction.ts | 3 ++ src/lib/admin/src/OrderBy.ts | 15 ++++++ src/lib/admin/src/PageHooks.ts | 39 ++++++++++++++++ src/lib/admin/src/SavedFilter.ts | 28 +++++++++++ src/lib/admin/src/SortableField.ts | 21 +++++++++ src/lib/admin/src/UrlAction.ts | 24 ++++++++++ src/lib/admin/src/config.ts | 23 +++++++++ src/lib/admin/src/filters.ts | 59 ++++++++++++++++++++++++ 21 files changed, 483 insertions(+) create mode 100644 src/lib/admin/src/ActionParams.ts create mode 100644 src/lib/admin/src/AssociatedField.ts create mode 100644 src/lib/admin/src/AssociatedItem.ts create mode 100644 src/lib/admin/src/CallbackAction.ts create mode 100644 src/lib/admin/src/Collection.ts create mode 100644 src/lib/admin/src/CollectionField.ts create mode 100644 src/lib/admin/src/ConfigFilter.ts create mode 100644 src/lib/admin/src/DefaultAction.ts create mode 100644 src/lib/admin/src/Field.ts create mode 100644 src/lib/admin/src/FieldHtmlProperties.ts create mode 100644 src/lib/admin/src/FieldOptions.ts create mode 100644 src/lib/admin/src/FilterType.ts create mode 100644 src/lib/admin/src/FilterWithValue.ts create mode 100644 src/lib/admin/src/ItemAction.ts create mode 100644 src/lib/admin/src/OrderBy.ts create mode 100644 src/lib/admin/src/PageHooks.ts create mode 100644 src/lib/admin/src/SavedFilter.ts create mode 100644 src/lib/admin/src/SortableField.ts create mode 100644 src/lib/admin/src/UrlAction.ts create mode 100644 src/lib/admin/src/config.ts create mode 100644 src/lib/admin/src/filters.ts diff --git a/src/lib/admin/src/ActionParams.ts b/src/lib/admin/src/ActionParams.ts new file mode 100644 index 00000000..29d10dc3 --- /dev/null +++ b/src/lib/admin/src/ActionParams.ts @@ -0,0 +1,27 @@ +import Field from './Field'; + +export default class ActionParams { + private _params: Array = []; + + get params(): Array { + return this._params; + } + + public static id(): ActionParams { + return new ActionParams().and('id'); + } + + public static with(field: string | Field): ActionParams { + return new ActionParams().and(field); + } + + public and(field: string | Field): ActionParams { + if (field instanceof Field) { + this._params.push(field); + } else { + this._params.push(new Field(field)); + } + + return this; + } +} diff --git a/src/lib/admin/src/AssociatedField.ts b/src/lib/admin/src/AssociatedField.ts new file mode 100644 index 00000000..aa9f6dc6 --- /dev/null +++ b/src/lib/admin/src/AssociatedField.ts @@ -0,0 +1,12 @@ +import Field from './Field'; +import FieldHtmlProperties from './FieldHtmlProperties'; + +export default class AssociatedField extends Field { + constructor( + name: string, + associated_field: Field, + field_html_properties: FieldHtmlProperties = FieldHtmlProperties.defaults() + ) { + super(name, '', associated_field, field_html_properties); + } +} diff --git a/src/lib/admin/src/AssociatedItem.ts b/src/lib/admin/src/AssociatedItem.ts new file mode 100644 index 00000000..999b0364 --- /dev/null +++ b/src/lib/admin/src/AssociatedItem.ts @@ -0,0 +1,11 @@ +import type Field from './Field'; + +export default class AssociatedItem { + public readonly item: T; + public readonly fields: Field[]; + + constructor(item: T, fields: Field[]) { + this.item = item; + this.fields = fields; + } +} diff --git a/src/lib/admin/src/CallbackAction.ts b/src/lib/admin/src/CallbackAction.ts new file mode 100644 index 00000000..f84687fd --- /dev/null +++ b/src/lib/admin/src/CallbackAction.ts @@ -0,0 +1,14 @@ +import DefaultAction from './DefaultAction'; + +export default class CallbackAction extends DefaultAction { + private readonly _callback: Function; + + constructor(name: string, callback: Function) { + super(name); + this._callback = callback; + } + + public call(item: object): string { + return this._callback.call(null, item); + } +} diff --git a/src/lib/admin/src/Collection.ts b/src/lib/admin/src/Collection.ts new file mode 100644 index 00000000..cd925819 --- /dev/null +++ b/src/lib/admin/src/Collection.ts @@ -0,0 +1,10 @@ +export default class Collection extends Array { + constructor(items: T[] = []) { + super(); + this.addItems(items); + } + + private addItems(items: T[] = []) { + items.forEach((item) => this.push(item)); + } +} diff --git a/src/lib/admin/src/CollectionField.ts b/src/lib/admin/src/CollectionField.ts new file mode 100644 index 00000000..12ddb6ef --- /dev/null +++ b/src/lib/admin/src/CollectionField.ts @@ -0,0 +1,23 @@ +import type AssociatedField from './AssociatedField'; +import Field from './Field'; +import FieldOptions from './FieldOptions'; + +export default class CollectionField extends Field { + constructor(name: string, text: string, item_field: Field) { + super(name, text, new FieldOptions(item_field)); + } + + public displayFromItem(item: object | any): any { + let field: Field | AssociatedField; + + const items = item[this.name]; + + if (items.length === 0 || !(items instanceof Array)) { + return null; + } + + field = this._associated_field; + + return items.map((singleItem) => field.displayFromItem(singleItem)); + } +} diff --git a/src/lib/admin/src/ConfigFilter.ts b/src/lib/admin/src/ConfigFilter.ts new file mode 100644 index 00000000..774c5285 --- /dev/null +++ b/src/lib/admin/src/ConfigFilter.ts @@ -0,0 +1,34 @@ +import FilterType from './FilterType'; +import type FilterWithValue from './FilterWithValue'; +import type Filter from '../components/PaginatedTable/Filter.svelte'; + +export default class ConfigFilter { + public readonly name: string; + public readonly title: string; + public readonly type: FilterType; + public readonly options: { [key: string]: any }; + public element: Filter | null; + public value: FilterWithValue | null; + + constructor(name: string, title: string, type: FilterType, options?: { [key: string]: any }) { + this.name = name; + this.title = title; + this.type = type; + this.options = options || {}; + this.validateOptions(); + } + + private validateOptions() { + switch (this.type) { + case FilterType.entity: + if (!this.options.entities) { + throw new Error( + 'To define a filter of type "entity", you must also specify the "entities" option. This must contain an array that contains elements of type "{name: string, value: string}", or a callable that returns such array, or a promise that resolves to such array.' + ); + } + break; + default: + break; + } + } +} diff --git a/src/lib/admin/src/DefaultAction.ts b/src/lib/admin/src/DefaultAction.ts new file mode 100644 index 00000000..bad02248 --- /dev/null +++ b/src/lib/admin/src/DefaultAction.ts @@ -0,0 +1,13 @@ +import type ItemAction from './ItemAction'; + +export default abstract class DefaultAction implements ItemAction { + protected readonly _name: string; + + protected constructor(name: string) { + this._name = name; + } + + get name(): string { + return this._name; + } +} diff --git a/src/lib/admin/src/Field.ts b/src/lib/admin/src/Field.ts new file mode 100644 index 00000000..caa309f2 --- /dev/null +++ b/src/lib/admin/src/Field.ts @@ -0,0 +1,45 @@ +import type AssociatedField from './AssociatedField'; +import type FieldHtmlProperties from './FieldHtmlProperties'; +import FieldOptions from './FieldOptions'; +import SortableField from './SortableField'; + +export const Sortable = true; + +export default class Field { + public readonly name: string; + public readonly text: string; + public readonly field_html_properties: FieldHtmlProperties; + public readonly sortable_field: SortableField | null; + protected readonly _associated_field: null | AssociatedField; + + constructor( + name: string, + text: string = '', + options: FieldOptions | null = null, + sortable: boolean = false + ) { + if (!options) { + options = FieldOptions.defaults(); + } + this.name = name; + this.text = text === '' ? name : text; + this.field_html_properties = options.field_html_properties; + this._associated_field = options.associated_field; + this.sortable_field = sortable + ? new SortableField(name, null, options.sortable_property_name || name) + : null; + } + + public displayFromItem(item: object | any): any { + let field: Field | AssociatedField; + + if (this._associated_field) { + item = item[this.name]; + field = this._associated_field; + } else { + field = this; + } + + return item[field.name]; + } +} diff --git a/src/lib/admin/src/FieldHtmlProperties.ts b/src/lib/admin/src/FieldHtmlProperties.ts new file mode 100644 index 00000000..aa3e3256 --- /dev/null +++ b/src/lib/admin/src/FieldHtmlProperties.ts @@ -0,0 +1,11 @@ +export default class FieldHtmlProperties { + public readonly html_class: string; + + constructor(html_class: string = '') { + this.html_class = html_class; + } + + static defaults() { + return new FieldHtmlProperties(); + } +} diff --git a/src/lib/admin/src/FieldOptions.ts b/src/lib/admin/src/FieldOptions.ts new file mode 100644 index 00000000..4829b5c7 --- /dev/null +++ b/src/lib/admin/src/FieldOptions.ts @@ -0,0 +1,34 @@ +import type AssociatedField from './AssociatedField'; +import FieldHtmlProperties from './FieldHtmlProperties'; + +export default class FieldOptions { + public readonly associated_field: null | AssociatedField; + public readonly field_html_properties: FieldHtmlProperties; + public readonly sortable_property_name: string | null; + + constructor( + associated_field: AssociatedField | null = null, + field_html_properties: FieldHtmlProperties | null = null, + sortable_property_name: string | null = null + ) { + this.associated_field = associated_field; + this.field_html_properties = field_html_properties || FieldHtmlProperties.defaults(); + this.sortable_property_name = sortable_property_name; + } + + static defaults(): FieldOptions { + return new FieldOptions(); + } + + static newWithAssociatedField(field: AssociatedField): FieldOptions { + return new FieldOptions(field); + } + + static newWithHtmlProperties(htmlProperties: FieldHtmlProperties): FieldOptions { + return new FieldOptions(null, htmlProperties); + } + + static newWithSortName(sortable_property_name: string): FieldOptions { + return new FieldOptions(null, null, sortable_property_name); + } +} diff --git a/src/lib/admin/src/FilterType.ts b/src/lib/admin/src/FilterType.ts new file mode 100644 index 00000000..3bc38c6c --- /dev/null +++ b/src/lib/admin/src/FilterType.ts @@ -0,0 +1,9 @@ +enum FilterType { + text = 'text', + date = 'date', + number = 'number', + entity = 'entity', + boolean = 'boolean' +} + +export default FilterType; diff --git a/src/lib/admin/src/FilterWithValue.ts b/src/lib/admin/src/FilterWithValue.ts new file mode 100644 index 00000000..159309e1 --- /dev/null +++ b/src/lib/admin/src/FilterWithValue.ts @@ -0,0 +1,28 @@ +import type ConfigFilter from './ConfigFilter'; +import type FilterType from './FilterType'; + +export default class FilterWithValue { + public readonly name: string; + public readonly type: FilterType; + public readonly value: string; + + constructor(name: string, type: FilterType, value: string) { + this.name = name; + this.type = type; + this.value = value; + } + + public static fromFilter(filter: ConfigFilter, value: string): FilterWithValue { + return new FilterWithValue(filter.name, filter.type, value); + } + + public static fromSerialized(filter: Partial): FilterWithValue { + if (!filter.name || !filter.type || !filter.value) { + console.error('Serialized filter is incomplete', filter); + + throw new Error('Serialized filter is incomplete'); + } + + return new FilterWithValue(filter.name, filter.type, filter.value); + } +} diff --git a/src/lib/admin/src/ItemAction.ts b/src/lib/admin/src/ItemAction.ts new file mode 100644 index 00000000..d8021455 --- /dev/null +++ b/src/lib/admin/src/ItemAction.ts @@ -0,0 +1,3 @@ +export default interface ItemAction { + get name(): string; +} diff --git a/src/lib/admin/src/OrderBy.ts b/src/lib/admin/src/OrderBy.ts new file mode 100644 index 00000000..bf0965f9 --- /dev/null +++ b/src/lib/admin/src/OrderBy.ts @@ -0,0 +1,15 @@ +export enum OrderBy { + ASC = 'ASC', + DESC = 'DESC' +} + +export function orderByToString(order_by: OrderBy): string { + switch (order_by) { + case OrderBy.ASC: + return 'ASC'; + case OrderBy.DESC: + return 'DESC'; + default: + throw new Error(`Unsupported order_by type: ${order_by}`); + } +} diff --git a/src/lib/admin/src/PageHooks.ts b/src/lib/admin/src/PageHooks.ts new file mode 100644 index 00000000..c7145bee --- /dev/null +++ b/src/lib/admin/src/PageHooks.ts @@ -0,0 +1,39 @@ +import type SortableField from './SortableField'; +import type FilterWithValue from './FilterWithValue'; + +export default class PageHooks { + private readonly _items_callback: Function; + private readonly _count_callback: Function; + + private lastPage: number = 1; + private lastField: SortableField | null; + private lastFilters: Array | null; + + constructor(items_callback: Function, count_callback: Function = null) { + this._items_callback = items_callback; + this._count_callback = count_callback || null; + } + + get hasCountCallback(): boolean { + return this._count_callback !== null; + } + + public refresh() { + this.callForItems(this.lastPage, this.lastField, this.lastFilters); + } + + public callForItems( + page: number, + field: SortableField | null, + filters: Array | null + ): void { + this.lastPage = page; + this.lastField = field; + this.lastFilters = filters; + this._items_callback(page, field, filters); + } + + public getCountCallback(): Function { + return this._count_callback; + } +} diff --git a/src/lib/admin/src/SavedFilter.ts b/src/lib/admin/src/SavedFilter.ts new file mode 100644 index 00000000..7b939729 --- /dev/null +++ b/src/lib/admin/src/SavedFilter.ts @@ -0,0 +1,28 @@ +import FilterWithValue from './FilterWithValue'; + +export default class SavedFilter { + public readonly name: string; + public readonly deserialized_filters: Array; + private readonly filters: string; + + constructor(name: string, filters_with_values: Array) { + this.name = name; + this.deserialized_filters = filters_with_values; + this.filters = JSON.stringify(filters_with_values); + } + + public static fromSerialized(name: string, filters: string): SavedFilter { + let deserialized_filters: Array = JSON.parse(filters); + + if (!Array.isArray(deserialized_filters)) { + console.error('Stored filters are not stored as an array. Resetting them.'); + deserialized_filters = []; + } + + deserialized_filters = deserialized_filters.map((f: filter) => + FilterWithValue.fromSerialized(f) + ); + + return new SavedFilter(name, deserialized_filters); + } +} diff --git a/src/lib/admin/src/SortableField.ts b/src/lib/admin/src/SortableField.ts new file mode 100644 index 00000000..d0fd03d6 --- /dev/null +++ b/src/lib/admin/src/SortableField.ts @@ -0,0 +1,21 @@ +import { OrderBy } from './OrderBy'; + +export default class SortableField { + public readonly name: string; + public readonly property_name: string; + public order_by: OrderBy; + + constructor(name: string, order: OrderBy = OrderBy.ASC, property_name: string) { + this.name = name; + this.order_by = order; + this.property_name = property_name; + } + + get is_asc(): boolean { + return this.order_by === OrderBy.ASC; + } + + swapOrder() { + this.order_by = this.order_by === OrderBy.ASC ? OrderBy.DESC : OrderBy.ASC; + } +} diff --git a/src/lib/admin/src/UrlAction.ts b/src/lib/admin/src/UrlAction.ts new file mode 100644 index 00000000..e521bd32 --- /dev/null +++ b/src/lib/admin/src/UrlAction.ts @@ -0,0 +1,24 @@ +import DefaultAction from './DefaultAction'; +import type ActionParams from './ActionParams'; +import type Field from './Field'; + +export default class UrlAction extends DefaultAction { + private readonly params: ActionParams; + private readonly _url: string; + + constructor(name: string, url: string, params: ActionParams) { + super(name); + this.params = params; + this._url = url; + } + + public url(item: object): string { + let url = this._url; + + this.params.params.forEach((field: Field) => { + url = url.replace(`:${field.name}`, field.displayFromItem(item).toString()); + }); + + return `${url}`; + } +} diff --git a/src/lib/admin/src/config.ts b/src/lib/admin/src/config.ts new file mode 100644 index 00000000..183c6834 --- /dev/null +++ b/src/lib/admin/src/config.ts @@ -0,0 +1,23 @@ +import { writable } from 'svelte/store'; +import type SavedFilter from '$lib/admin/src/SavedFilter'; + +class Config { + public spinLoaderSrc: string = ''; + public builtinFilters: { [key: string]: Array } = {}; +} + +let config = new Config(); + +export const configStore = writable(config); + +export function updateConfig(userConfig) { + for (const configKey in userConfig) { + config[configKey] = userConfig[configKey]; + } + + configStore.set(config); +} + +export function getConfig(): Config { + return config; +} diff --git a/src/lib/admin/src/filters.ts b/src/lib/admin/src/filters.ts new file mode 100644 index 00000000..3e511486 --- /dev/null +++ b/src/lib/admin/src/filters.ts @@ -0,0 +1,59 @@ +import SavedFilter from './SavedFilter'; +import { getConfig } from './config'; + +export function getByName(save_key: string, name: string): SavedFilter { + const filters = getSavedFilters(save_key); + + const filtered_by_name = filters.filter((filter: SavedFilter) => filter.name === name); + + if (!filtered_by_name.length) { + throw new Error(`Filter with name "${name}" was not found.`); + } else if (filtered_by_name.length > 1) { + throw new Error(`Found multiple filters with name "${name}"`); + } + + return filtered_by_name[0]; +} + +export function getSavedFilters( + save_key: string, + with_builtin: boolean = true +): Array { + let stored_filters = localStorage.getItem('compotes_filters_' + save_key); + + if (!stored_filters) { + stored_filters = '[]'; + } + + let deserialized_filters: Array = JSON.parse(stored_filters); + + if (!Array.isArray(deserialized_filters)) { + console.error('Stored filters are not stored as an array. Resetting them.'); + deserialized_filters = []; + } + + const builtin = with_builtin ? getConfig().builtinFilters[save_key] || [] : []; + + return [ + ...builtin, + ...deserialized_filters.map((f: SavedFilter) => { + return SavedFilter.fromSerialized(f.name, f.filters); + }) + ]; +} + +export function saveFilter(save_key: string, new_filter: SavedFilter) { + let deserialized_filters = getSavedFilters(save_key, false); + + let existing_filter_index = deserialized_filters.findIndex((filter: SavedFilter) => { + return filter.name === new_filter.name; + }); + + if (existing_filter_index >= 0) { + deserialized_filters[existing_filter_index] = new_filter; + } else { + deserialized_filters.push(new_filter); + } + + localStorage.setItem('compotes_filters_' + save_key, JSON.stringify(deserialized_filters)); +}