feat: Migrazione della struttura dei model JS da Coloquent a Spraypaint

Il commit riguarda la rimozione di Coloquent e l'adozione del framework Spraypaint per la struttura dei model. Contiene variazioni nell'implementazione dei metodi associati ai modelli e una modifica delle chiamate per l'ottenimento dei dati. Inoltre, le modifiche aiuteranno a potenziare le prestazioni delle applicazioni.
This commit is contained in:
Maicol Battistini 2023-12-10 17:23:48 +01:00
parent b448da234e
commit 71b03c14b6
No known key found for this signature in database
16 changed files with 112 additions and 348 deletions

View File

@ -11,3 +11,7 @@ rules:
- error - error
- except-parens - except-parens
no-plusplus: off no-plusplus: off
new-cap:
- error
- capIsNewExceptions: ['Model', 'Attr', 'HasMany', 'HasOne', 'BelongsTo', 'Link', 'Stream']
properties: false

View File

@ -18,11 +18,10 @@
"@maicol07/material-web-additions": "^1.4.4", "@maicol07/material-web-additions": "^1.4.4",
"@material/mwc-snackbar": "^0.27.0", "@material/mwc-snackbar": "^0.27.0",
"@material/mwc-top-app-bar": "^0.27.0", "@material/mwc-top-app-bar": "^0.27.0",
"@material/web": "1.0.1", "@material/web": "^1.0.1",
"@mdi/js": "^7.3.67", "@mdi/js": "^7.3.67",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"collect.js": "^4.36.1", "collect.js": "^4.36.1",
"coloquent": "npm:@maicol07/coloquent@3.0.1-beta",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"include-media": "^2.0.0", "include-media": "^2.0.0",
"lit": "^3.1.0", "lit": "^3.1.0",
@ -32,6 +31,7 @@
"postcss-scss": "^4.0.9", "postcss-scss": "^4.0.9",
"prntr": "^2.0.18", "prntr": "^2.0.18",
"readable-http-codes": "^1.1.1", "readable-http-codes": "^1.1.1",
"spraypaint": "^0.10.24",
"ts-pattern": "^5.0.6", "ts-pattern": "^5.0.6",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript-cookie": "^1.0.6" "typescript-cookie": "^1.0.6"

View File

@ -14,7 +14,7 @@ import DataTable, {DataTableAttributes} from '@osm/Components/DataTable/DataTabl
import DataTableColumn, {DataTableColumnAttributes} from '@osm/Components/DataTable/DataTableColumn'; import DataTableColumn, {DataTableColumnAttributes} from '@osm/Components/DataTable/DataTableColumn';
import RecordsTableColumn from '@osm/Components/DataTable/RecordsTableColumn'; import RecordsTableColumn from '@osm/Components/DataTable/RecordsTableColumn';
import MdIcon from '@osm/Components/MdIcon'; import MdIcon from '@osm/Components/MdIcon';
import Model from '@osm/Models/Model'; import Model from '@osm/Models/Record';
import {isVnode} from '@osm/utils/misc'; import {isVnode} from '@osm/utils/misc';
import collect, {Collection} from 'collect.js'; import collect, {Collection} from 'collect.js';
import { import {
@ -28,7 +28,7 @@ export interface RecordsTableColumnAttributes extends DataTableColumnAttributes
label?: string; label?: string;
} }
export interface RecordsTableAttributes<M extends Model<any, any>> extends DataTableAttributes { export interface RecordsTableAttributes<M extends Model> extends DataTableAttributes {
cols: Collection<Children> | Collection<RecordsTableColumnAttributes> | Collection<Children | RecordsTableColumnAttributes>; cols: Collection<Children> | Collection<RecordsTableColumnAttributes> | Collection<Children | RecordsTableColumnAttributes>;
records: Map<string, M>; records: Map<string, M>;
readonly?: boolean; readonly?: boolean;
@ -42,7 +42,7 @@ export interface RecordsTableAttributes<M extends Model<any, any>> extends DataT
valueModifier?(value: any, attribute: string, record: M): any; valueModifier?(value: any, attribute: string, record: M): any;
} }
export default class RecordsTable<M extends Model<any, any>, A extends RecordsTableAttributes<M> = RecordsTableAttributes<M>> extends DataTable<A> { export default class RecordsTable<M extends Model, A extends RecordsTableAttributes<M> = RecordsTableAttributes<M>> extends DataTable<A> {
element!: MdDataTable; element!: MdDataTable;
selectedRecordsIds: string[] = []; selectedRecordsIds: string[] = [];
@ -162,7 +162,7 @@ export default class RecordsTable<M extends Model<any, any>, A extends RecordsTa
cells.put('actions', this.tableRowActions(vnode, record).values<Children>().all()); cells.put('actions', this.tableRowActions(vnode, record).values<Children>().all());
} }
rows.set(record.getId()!, cells); rows.set(record.id!, cells);
} }
return rows; return rows;
@ -238,7 +238,7 @@ export default class RecordsTable<M extends Model<any, any>, A extends RecordsTa
protected onDeleteRecordButtonClicked(vnode: Vnode<A>, record: M, event: MouseEvent) { protected onDeleteRecordButtonClicked(vnode: Vnode<A>, record: M, event: MouseEvent) {
event.stopPropagation(); event.stopPropagation();
vnode.attrs.onDeleteRecordButtonClick?.(record.getId()!, event); vnode.attrs.onDeleteRecordButtonClick?.(record.id!, event);
} }
protected onDeleteSelectedRecordsButtonClicked(vnode: Vnode<A>, event: MouseEvent) { protected onDeleteSelectedRecordsButtonClicked(vnode: Vnode<A>, event: MouseEvent) {
@ -247,9 +247,10 @@ export default class RecordsTable<M extends Model<any, any>, A extends RecordsTa
protected getModelValue(record: M, attribute: string, vnode: Vnode<A>): unknown { protected getModelValue(record: M, attribute: string, vnode: Vnode<A>): unknown {
// Check if is a relation // Check if is a relation
let value: unknown = record.getAttribute(attribute); // @ts-expect-error
let value: unknown = record[attribute];
if (attribute === 'id') { if (attribute === 'id') {
value = record.getId(); value = record.id;
} }
return vnode.attrs.valueModifier?.(value, attribute, record) ?? value; return vnode.attrs.valueModifier?.(value, attribute, record) ?? value;

View File

@ -1,7 +1,7 @@
import {mdiFloppy} from '@mdi/js'; import {mdiFloppy} from '@mdi/js';
import RecordDialog, {RecordDialogAttributes} from '@osm/Components/Dialogs/RecordDialog'; import RecordDialog, {RecordDialogAttributes} from '@osm/Components/Dialogs/RecordDialog';
import MdIcon from '@osm/Components/MdIcon'; import MdIcon from '@osm/Components/MdIcon';
import Model from '@osm/Models/Model'; import Model from '@osm/Models/Record';
import { import {
VnodeCollection, VnodeCollection,
VnodeCollectionItem VnodeCollectionItem
@ -13,7 +13,6 @@ import {
showSnackbar showSnackbar
} from '@osm/utils/misc'; } from '@osm/utils/misc';
import collect, {Collection} from 'collect.js'; import collect, {Collection} from 'collect.js';
import {SaveResponse} from 'coloquent';
import { import {
Children, Children,
Vnode, Vnode,
@ -23,7 +22,7 @@ import Stream from 'mithril/stream';
import {Form} from 'mithril-utilities'; import {Form} from 'mithril-utilities';
import {Class} from 'type-fest'; import {Class} from 'type-fest';
export default abstract class AddEditRecordDialog<M extends Model<any, any>> extends RecordDialog<M> { export default abstract class AddEditRecordDialog<M extends Model> extends RecordDialog<M> {
// eslint-disable-next-line unicorn/no-null // eslint-disable-next-line unicorn/no-null
protected formElement: HTMLFormElement | null = null; protected formElement: HTMLFormElement | null = null;
protected abstract formState: Map<string, Stream<any>>; protected abstract formState: Map<string, Stream<any>>;
@ -47,12 +46,13 @@ export default abstract class AddEditRecordDialog<M extends Model<any, any>> ext
super.oncreate(vnode); super.oncreate(vnode);
this.formElement = this.element.querySelector('form'); this.formElement = this.element.querySelector('form');
this.element.querySelector(`#saveBtn${this.formId}`)?.setAttribute('form', this.formId!); this.element.querySelector(`#saveBtn${this.formId!}`)?.setAttribute('form', this.formId!);
} }
fillForm() { fillForm() {
for (const [key, value] of this.formState) { for (const [key, value] of this.formState) {
value(this.record.getAttribute(key) ?? value()); // @ts-ignore
value(this.record[key] ?? value());
} }
} }
@ -126,27 +126,25 @@ export default abstract class AddEditRecordDialog<M extends Model<any, any>> ext
} }
async save(): Promise<boolean> { async save(): Promise<boolean> {
this.record.setAttributes(this.modelAttributesFromFormState); this.record.assignAttributes(this.modelAttributesFromFormState);
try { try {
const response = await this.record.save(); const result = await this.record.save();
this.afterSave(response); this.afterSave(result);
return response.getModelId() !== undefined; return result;
} catch (error) { } catch (error) {
this.onSaveError(error as JSONAPI.RequestError); this.onSaveError(error as JSONAPI.RequestError);
return false; return false;
} }
} }
afterSave(response: SaveResponse<M>): void { afterSave(result: boolean): void {
const responseModel = response.getModel() as M; if (result) {
if (responseModel !== undefined) {
this.record = responseModel;
void showSnackbar(__('Record salvato con successo')); void showSnackbar(__('Record salvato con successo'));
} }
} }
onSaveError(error: JSONAPI.RequestError): void { onSaveError(error: JSONAPI.RequestError): void {
const message = error.response.data.message; const {message} = error.response.data;
void showSnackbar(message, false); void showSnackbar(message, false);
} }

View File

@ -1,6 +1,6 @@
import {mdiDelete} from '@mdi/js'; import {mdiDelete} from '@mdi/js';
import MdIcon from '@osm/Components/MdIcon'; import MdIcon from '@osm/Components/MdIcon';
import Model from '@osm/Models/Model'; import Model from '@osm/Models/Record';
import {VnodeCollection} from '@osm/typings/jsx'; import {VnodeCollection} from '@osm/typings/jsx';
import {showSnackbar} from '@osm/utils/misc'; import {showSnackbar} from '@osm/utils/misc';
import collect from 'collect.js'; import collect from 'collect.js';
@ -12,11 +12,11 @@ import {RequestError} from 'mithril-utilities';
import RecordDialog, {RecordDialogAttributes} from './RecordDialog'; import RecordDialog, {RecordDialogAttributes} from './RecordDialog';
export interface DeleteRecordDialogAttributes<M extends Model<any, any>> extends RecordDialogAttributes<M> { export interface DeleteRecordDialogAttributes<M extends Model> extends RecordDialogAttributes<M> {
records: M | M[]; records: M | M[];
} }
export default class DeleteRecordDialog<M extends Model<any, any>, A extends DeleteRecordDialogAttributes<M> = DeleteRecordDialogAttributes<M>> extends RecordDialog<M, A> { export default class DeleteRecordDialog<M extends Model, A extends DeleteRecordDialogAttributes<M> = DeleteRecordDialogAttributes<M>> extends RecordDialog<M, A> {
records!: M[]; records!: M[];
oninit(vnode: Vnode<A, this>) { oninit(vnode: Vnode<A, this>) {
@ -32,7 +32,7 @@ export default class DeleteRecordDialog<M extends Model<any, any>, A extends Del
return ( return (
<> <>
<p>{text}</p> <p>{text}</p>
<ul>{this.records.map((record) => <li key={record.getId()}>{this.recordSummary(record, vnode)}</li>)}</ul> <ul>{this.records.map((record) => <li key={record.id}>{this.recordSummary(record, vnode)}</li>)}</ul>
</> </>
); );
} }
@ -61,7 +61,7 @@ export default class DeleteRecordDialog<M extends Model<any, any>, A extends Del
} }
recordSummary(record: M, vnode: Vnode<A, this>): Children { recordSummary(record: M, vnode: Vnode<A, this>): Children {
return __('ID: :recordId', {recordId: record.getId()!}); return __('ID: :recordId', {recordId: record.id!});
} }
async onConfirmButtonClicked() { async onConfirmButtonClicked() {
@ -74,12 +74,12 @@ export default class DeleteRecordDialog<M extends Model<any, any>, A extends Del
async deleteRecord() { async deleteRecord() {
try { try {
const promises = this.records.map((record) => record.delete()); const promises = this.records.map((record) => record.destroy());
await Promise.all(promises); await Promise.all(promises);
// TODO: Better way for pluralization in i18n // TODO: Better way for pluralization in i18n
void showSnackbar(this.records.length > 1 ? __('Record eliminati!') : __('Record eliminato!')); void showSnackbar(this.records.length > 1 ? __('Record eliminati!') : __('Record eliminato!'));
this.close('deleted'); void this.close('deleted');
} catch (error) { } catch (error) {
void showSnackbar(__('Errore durante l\'eliminazione del record! :error', {error: (error as RequestError<{message: string}>).response.message}), false); void showSnackbar(__('Errore durante l\'eliminazione del record! :error', {error: (error as RequestError<{message: string}>).response.message}), false);
} }

View File

@ -2,7 +2,7 @@ import '@maicol07/material-web-additions/layout-grid/layout-grid.js';
import '@material/web/button/text-button.js'; import '@material/web/button/text-button.js';
import Dialog, {DialogAttributes} from '@osm/Components/Dialogs/Dialog'; import Dialog, {DialogAttributes} from '@osm/Components/Dialogs/Dialog';
import Model from '@osm/Models/Model'; import Model from '@osm/Models/Record';
import {Vnode} from 'mithril'; import {Vnode} from 'mithril';
export interface RecordDialogAttributes<M extends Model<any, any>> extends DialogAttributes { export interface RecordDialogAttributes<M extends Model<any, any>> extends DialogAttributes {

View File

@ -3,27 +3,27 @@ import '@material/web/button/outlined-button.js';
import {mdiChevronLeft} from '@mdi/js'; import {mdiChevronLeft} from '@mdi/js';
import MdIcon from '@osm/Components/MdIcon'; import MdIcon from '@osm/Components/MdIcon';
import Page, {PageAttributes} from '@osm/Components/Page'; import Page, {PageAttributes} from '@osm/Components/Page';
import Model from '@osm/Models/Model'; import Model from '@osm/Models/Record';
import {showSnackbar} from '@osm/utils/misc'; import {showSnackbar} from '@osm/utils/misc';
import {Builder} from 'coloquent';
import { import {
Children, Children,
Vnode Vnode
} from 'mithril'; } from 'mithril';
import {Scope} from 'spraypaint';
import {Class} from 'type-fest'; import {Class} from 'type-fest';
export interface RecordPageAttributes<M extends Model<any, any>> extends PageAttributes { export interface RecordPageAttributes<M extends Model> extends PageAttributes {
record: M; record: M;
} }
export default abstract class RecordPage<M extends Model<any, any>, A extends RecordPageAttributes<M> = RecordPageAttributes<M>> extends Page<A> { export default abstract class RecordPage<M extends Model, A extends RecordPageAttributes<M> = RecordPageAttributes<M>> extends Page<A> {
abstract recordType: Class<M> & typeof Model<any, any>; abstract recordType: Class<M> & typeof Model;
record?: M; record?: M;
async oninit(vnode: Vnode<A, this>) { async oninit(vnode: Vnode<A, this>) {
super.oninit(vnode); super.oninit(vnode);
const {id: recordId} = route().params as {id: number | string}; const {id: recordId} = route().params as {id: number | string};
if (recordId !== this.record?.getId()) { if (recordId !== this.record?.id) {
await this.loadRecord(recordId); await this.loadRecord(recordId);
} }
} }
@ -32,24 +32,23 @@ export default abstract class RecordPage<M extends Model<any, any>, A extends Re
if (recordId && recordId !== 'new' && !this.record) { if (recordId && recordId !== 'new' && !this.record) {
try { try {
const response = await this.modelQuery().find(recordId); const response = await this.modelQuery().find(recordId);
this.record = response.getData() || undefined; this.record = response.data || undefined;
} catch (e) { } catch (error) {
console.error(e); // eslint-disable-next-line no-console
console.error(error);
void showSnackbar(__('Errore durante il caricamento del record')); void showSnackbar(__('Errore durante il caricamento del record'));
// Do nothing // Do nothing
} }
} }
if (!this.record) { if (!this.record) {
// @ts-expect-error — This won't be abstract when implemented
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.record = new this.recordType(); this.record = new this.recordType();
} }
m.redraw(); m.redraw();
} }
modelQuery(): Builder<M> { modelQuery(): Scope<M> {
return this.recordType.query(); return this.recordType as unknown as Scope<M>;
} }
contents(vnode: Vnode<A>): Children { contents(vnode: Vnode<A>): Children {

View File

@ -21,9 +21,8 @@ import DeleteRecordDialog, {DeleteRecordDialogAttributes} from '@osm/Components/
import RecordDialog, {RecordDialogAttributes} from '@osm/Components/Dialogs/RecordDialog'; import RecordDialog, {RecordDialogAttributes} from '@osm/Components/Dialogs/RecordDialog';
import MdIcon from '@osm/Components/MdIcon'; import MdIcon from '@osm/Components/MdIcon';
import Page, {PageAttributes} from '@osm/Components/Page'; import Page, {PageAttributes} from '@osm/Components/Page';
import Model from '@osm/Models/Model'; import Record from '@osm/Models/Record';
import collect, {type Collection} from 'collect.js'; import collect, {type Collection} from 'collect.js';
import {SortDirection} from 'coloquent';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import type { import type {
Children, Children,
@ -31,38 +30,25 @@ import type {
VnodeDOM VnodeDOM
} from 'mithril'; } from 'mithril';
import Stream from 'mithril/stream'; import Stream from 'mithril/stream';
import {Scope} from 'spraypaint';
import {SortDir} from 'spraypaint/lib-esm/scope';
import {match} from 'ts-pattern'; import {match} from 'ts-pattern';
import {Match} from 'ts-pattern/dist/types/Match'; import {Match} from 'ts-pattern/dist/types/Match';
import type {Class} from 'type-fest'; import type {Class} from 'type-fest';
export interface Meta { type RecordDialogVnode<M extends Record, D extends RecordDialog<M>> = Vnode<RecordDialogAttributes<M>, D>;
current_page: number; type DeleteRecordDialogVnode<M extends Record, D extends DeleteRecordDialog<M>> = Vnode<DeleteRecordDialogAttributes<M>, D>;
from: number;
last_page: number;
path: string;
per_page: number;
to: number;
total: number;
}
export interface JSONAPIResponse {
meta: Meta;
}
type RecordDialogVnode<M extends Model<any, any>, D extends RecordDialog<M>> = Vnode<RecordDialogAttributes<M>, D>;
type DeleteRecordDialogVnode<M extends Model<any, any>, D extends DeleteRecordDialog<M>> = Vnode<DeleteRecordDialogAttributes<M>, D>;
// noinspection JSUnusedLocalSymbols // noinspection JSUnusedLocalSymbols
/** /**
* @abstract * @abstract
*/ */
export default abstract class RecordsPage< export default abstract class RecordsPage<
M extends Model<any, any>, M extends Record,
D extends AddEditRecordDialog<M> = AddEditRecordDialog<M>, D extends AddEditRecordDialog<M> = AddEditRecordDialog<M>,
DRD extends DeleteRecordDialog<M> = DeleteRecordDialog<M> DRD extends DeleteRecordDialog<M> = DeleteRecordDialog<M>
> extends Page { > extends Page {
abstract modelType: Class<M> & typeof Model<any, any>; abstract modelType: Class<M> & typeof Record;
recordDialogType?: Class<D>; recordDialogType?: Class<D>;
deleteRecordDialogType?: Class<DRD>; deleteRecordDialogType?: Class<DRD>;
@ -86,15 +72,13 @@ export default abstract class RecordsPage<
protected lastRowOfPage = this.totalRecords; protected lastRowOfPage = this.totalRecords;
protected filters: Map<string, string> = new Map(); protected filters: Map<string, string> = new Map();
protected sort: Map<string, SortDirection> = new Map([['id', SortDirection.ASC]]); protected sort: Map<string, SortDir> = new Map();
protected relatedFilters: Map<string, string> = new Map(); protected relatedFilters: Map<string, string> = new Map();
private listenedFilterColumns: string[] = []; private listenedFilterColumns: string[] = [];
private listenedSortedColumns: string[] = []; private listenedSortedColumns: string[] = [];
oninit(vnode: Vnode<PageAttributes, this>) { oninit(vnode: Vnode<PageAttributes, this>) {
super.oninit(vnode); super.oninit(vnode);
// @ts-ignore
this.modelType.pageSize = this.currentPageSize;
// Redraw on a first load to call onbeforeupdate // Redraw on a first load to call onbeforeupdate
m.redraw(); m.redraw();
} }
@ -111,24 +95,21 @@ export default abstract class RecordsPage<
async loadRecords() { async loadRecords() {
this.isTableLoading = true; this.isTableLoading = true;
let query = this.modelQuery(); const query = this.modelQuery();
// Fix Restify when filtering relations const response = await query.page(this.currentPage).all();
query = query.option('related', query.getQuery().getInclude().join(',')); const rawResponse = response.raw;
this.lastPage = rawResponse.meta?.last_page as number;
const response = await query.get(this.currentPage); this.firstRowOfPage = rawResponse.meta?.from as number;
const rawResponse = response.getHttpClientResponse().getData() as JSONAPIResponse; this.lastRowOfPage = rawResponse.meta?.to as number;
this.lastPage = rawResponse.meta.last_page; this.currentPageSize = rawResponse.meta?.per_page as number;
this.firstRowOfPage = rawResponse.meta.from; this.totalRecords = rawResponse.meta?.total as number;
this.lastRowOfPage = rawResponse.meta.to; const {data} = response;
this.currentPageSize = rawResponse.meta.per_page;
this.totalRecords = rawResponse.meta.total;
const data = response.getData();
this.records.clear(); this.records.clear();
if (data.length > 0) { if (data.length > 0) {
for (const record of data) { for (const record of data) {
this.records.set(record.getId()!, record); this.records.set(record.id, record);
} }
} }
@ -144,31 +125,29 @@ export default abstract class RecordsPage<
*/ */
private static convertToSnakeCase(string_: string, trim = false, removeSpecials = false, underscoredNumbers = false) { private static convertToSnakeCase(string_: string, trim = false, removeSpecials = false, underscoredNumbers = false) {
return string_.replace(removeSpecials ? /[^\w ]/g : '', '') return string_.replace(removeSpecials ? /[^\w ]/g : '', '')
.replace(/([ _]+)/g, '_') .replace(/[ _]+/g, '_')
.replace(trim ? /(^_|_$)/gm : '', '') .replace(trim ? /(^_|_$)/gm : '', '')
.replace(underscoredNumbers ? /([^\dA-Z_])([^_a-z])/g : /([^\dA-Z_])([^\d_a-z])/g, (m, preUpper, upper) => `${preUpper}_${upper}`) .replace(underscoredNumbers ? /([^\dA-Z_])([^_a-z])/g : /([^\dA-Z_])([^\d_a-z])/g, (m, preUpper: string, upper: string) => `${preUpper}_${upper}`)
.replace(underscoredNumbers ? /([^\d_]\d|\d[^\d_])/g : '', (m, index) => (index ? index.split('').join('_') : '')) .replace(underscoredNumbers ? /([^\d_]\d|\d[^\d_])/g : '', (m, index: string) => (index ? [...index].join('_') : ''))
.replace(/([A-Z])([A-Z])([^\dA-Z_])/g, (m, previousUpper, upper, lower) => `${previousUpper}_${upper}${lower}`) .replace(/([A-Z])([A-Z])([^\dA-Z_])/g, (m, previousUpper: string, upper: string, lower: string) => `${previousUpper}_${upper}${lower}`)
.replaceAll('_.', '.') // remove redundant underscores .replaceAll('_.', '.') // Remove redundant underscores
.toLowerCase(); .toLowerCase();
} }
modelQuery() { modelQuery() {
// @ts-ignore let query: Scope<M> = this.modelType.per(this.currentPageSize);
let query = this.modelType.query<M>();
for (const [attribute, value] of this.filters) { for (const [attribute, value] of this.filters) {
// Query = query.where(attribute, value); TODO: Revert when Restify uses JSONAPI syntax query = query.where({[RecordsPage.convertToSnakeCase(attribute)]: value});
query = query.option(RecordsPage.convertToSnakeCase(attribute), value);
} }
for (const [relation, value] of this.relatedFilters) { for (const [relation, value] of this.relatedFilters) {
query = query.option('related', relation) query = query.where({related: relation, search: value}); // TODO: Check
.option('search', value); // .where('search', value);
} }
for (const [attribute, value] of this.sort) { for (const [attribute, value] of this.sort) {
query = query.orderBy(RecordsPage.convertToSnakeCase(attribute), value); query = query.order({[RecordsPage.convertToSnakeCase(attribute)]: value});
} }
return query; return query;
@ -244,7 +223,7 @@ export default abstract class RecordsPage<
updateRecord(model: M) { updateRecord(model: M) {
if (this.recordPageRouteName) { if (this.recordPageRouteName) {
router.visit(route(this.recordPageRouteName, {id: model.getId()!})); router.visit(route(this.recordPageRouteName, {id: model.id}));
return; return;
} }
@ -258,8 +237,8 @@ export default abstract class RecordsPage<
for (const [key, state] of this.recordDialogsStates) { for (const [key, state] of this.recordDialogsStates) {
// noinspection LocalVariableNamingConventionJS // noinspection LocalVariableNamingConventionJS
const RD = this.recordDialogType!; const RD = this.recordDialogType!;
const record = key instanceof Model ? key : this.records.get(key); const record = key instanceof Record ? key : this.records.get(key);
const vnodeKey = record?.getId() ?? (key as string); const vnodeKey = record?.id ?? (key as string);
collection.put(vnodeKey, <RD key={vnodeKey} record={record} open={state}/>); collection.put(vnodeKey, <RD key={vnodeKey} record={record} open={state}/>);
} }
@ -303,14 +282,16 @@ export default abstract class RecordsPage<
onTablePageChange(event: CustomEvent<PaginateDetail>) { onTablePageChange(event: CustomEvent<PaginateDetail>) {
const {pageSize, action} = event.detail; const {pageSize, action} = event.detail;
this.currentPageSize = pageSize; this.currentPageSize = pageSize;
const {currentPage} = this; const {currentPage, lastPage} = this;
match(action) match(action)
.with('first', () => (this.currentPage = 1)) .with('first', () => (this.currentPage = 1))
.with('previous', () => (this.currentPage--)) .with('previous', () => (this.currentPage--))
.with('next', () => (this.currentPage++)) .with('next', () => (this.currentPage++))
.with('last', () => (this.currentPage = this.lastPage)) .with('last', () => (this.currentPage = lastPage))
.with('current', () => {}) .with('current', () => {})
.run(); .run();
// We need to check if the page has changed
// eslint-disable-next-line unicorn/consistent-destructuring
if (currentPage !== this.currentPage) { if (currentPage !== this.currentPage) {
this.refreshRecords = true; this.refreshRecords = true;
m.redraw(); m.redraw();
@ -356,13 +337,13 @@ export default abstract class RecordsPage<
const {column, isDescending} = (event as CustomEvent<SortButtonClickedEventDetail>).detail; const {column, isDescending} = (event as CustomEvent<SortButtonClickedEventDetail>).detail;
const modelAttribute = column.dataset.sortAttribute ?? column.dataset.modelAttribute!; const modelAttribute = column.dataset.sortAttribute ?? column.dataset.modelAttribute!;
this.sort.clear(); this.sort.clear();
this.sort.set(modelAttribute, isDescending ? SortDirection.DESC : SortDirection.ASC); this.sort.set(modelAttribute, isDescending ? 'desc' : 'asc');
this.refreshRecords = true; this.refreshRecords = true;
m.redraw(); m.redraw();
} }
openDeleteRecordsDialog(records: M | M[]) { openDeleteRecordsDialog(records: M | M[]) {
const key = records instanceof Model ? records.getId()! : records.map((r) => r.getId()).join(','); const key = records instanceof Record ? records.id! : records.map((r) => r.id).join(',');
let state = this.deleteRecordsDialogStates.get(key); let state = this.deleteRecordsDialogStates.get(key);
if (!state) { if (!state) {
@ -386,7 +367,7 @@ export default abstract class RecordsPage<
} }
protected getRecordDialogState(record?: M, slug?: string) { protected getRecordDialogState(record?: M, slug?: string) {
const key: string = slug ?? record?.getId() ?? ''; const key: string = slug ?? record?.id ?? '';
if (!this.recordDialogsStates.has(key)) { if (!this.recordDialogsStates.has(key)) {
const state = Stream<boolean>(false); const state = Stream<boolean>(false);

View File

@ -1,74 +0,0 @@
import type {
HttpClient,
HttpClientPromise
} from 'coloquent';
import {Request} from 'mithril-utilities';
import RequestHttpClientPromise from './RequestHttpClientPromise';
/**
* @class RequestHttpClient
*
* NOTE: This class is not meant to be used directly, but only for Models.
* You should use the {@link Request} class instead.
*/
export default class RequestHttpClient implements HttpClient {
request: Request;
constructor(requestInstance?: Request) {
this.request = requestInstance ?? new Request({
background: true,
headers: {
Accept: 'application/vnd.api+json',
'Content-Type': 'application/vnd.api+json'
}
});
}
delete(url: string): HttpClientPromise {
this.request.options.method = 'delete';
this.request.options.url = url;
return new RequestHttpClientPromise(this.request.send());
}
get(url: string): HttpClientPromise {
this.request.options.method = 'get';
this.request.options.url = url;
return new RequestHttpClientPromise(this.request.send());
}
getImplementingClient() {
return this.request;
}
head(url: string): HttpClientPromise {
this.request.options.method = 'head';
this.request.options.url = url;
return new RequestHttpClientPromise(this.request.send());
}
patch(url: string, data?: Record<string, any>): HttpClientPromise {
this.request.options.method = 'patch';
this.request.options.url = url;
this.request.options.body = data;
return new RequestHttpClientPromise(this.request.send());
}
post(url: string, data?: Record<string, any>): HttpClientPromise {
this.request.options.method = 'post';
this.request.options.url = url;
this.request.options.body = data;
return new RequestHttpClientPromise(this.request.send());
}
put(url: string, data?: Record<string, any>): HttpClientPromise {
this.request.options.method = 'put';
this.request.options.url = url;
this.request.options.body = data;
return new RequestHttpClientPromise(this.request.send());
}
setWithCredentials(withCredientials: boolean): void {
this.request.options.withCredentials = withCredientials;
}
}

View File

@ -1,29 +0,0 @@
import RequestHttpClientResponse from '@osm/Models/Http/RequestHttpClientResponse';
import type {
HttpClientPromise,
HttpClientResponse
} from 'coloquent';
import type {Thenable} from 'coloquent/dist/httpclient/Types';
export default class RequestHttpClientPromise implements HttpClientPromise {
constructor(private response: Promise<any>) {}
catch<U>(onRejected?: (error: any) => (Thenable<U> | U)): Promise<U> {
return this.response.catch(onRejected) as Promise<U>;
}
// eslint-disable-next-line unicorn/no-thenable
then<U>(
onFulfilled?: (value: HttpClientResponse) => (Thenable<U> | U),
onRejected?: (error: any) => void | (Thenable<U> | U)
): Promise<U> {
const wrappedOnFulfilled = onFulfilled === undefined
? undefined
: ((responsePromise: any) => onFulfilled(new RequestHttpClientResponse(responsePromise)));
return this.response.then<U>(
wrappedOnFulfilled,
// @ts-ignore
onRejected
);
}
}

View File

@ -1,13 +0,0 @@
import type {HttpClientResponse} from 'coloquent';
export default class RequestHttpClientResponse implements HttpClientResponse {
constructor(private response: any) {}
getData(): any {
return this.response;
}
getUnderlying(): any {
return this.response;
}
}

View File

@ -1,113 +0,0 @@
import RequestHttpClient from '@osm/Models/Http/RequestHttpClient';
import {
Model as BaseModel,
PaginationStrategy,
PluralResponse
} from 'coloquent';
import dayjs from 'dayjs';
import type {ValueOf} from 'type-fest';
export interface ModelAttributes {
id: number;
createdAt: Date;
updatedAt: Date;
[key: string]: unknown;
}
export interface ModelRelations {
[key: string]: unknown;
}
/**
* The base model for all models.
*/
export default abstract class Model<A extends ModelAttributes, R extends ModelRelations> extends BaseModel {
protected static paginationStrategy = PaginationStrategy.PageBased;
protected static jsonApiBaseUrl = '/api/restify';
getHttpClient() { return new RequestHttpClient(); }
static dates: Record<string, string> = {
createdAt: 'YYYY-MM-DDTHH:mm:ss.ssssssZ',
updatedAt: 'YYYY-MM-DDTHH:mm:ss.ssssssZ'
};
protected static get jsonApiType() {
return `${this.name.toLowerCase()}s`;
}
/**
* Returns all the instances of the model. (Alias of {@link Model.get}).
*/
static all<M extends typeof Model<any, any> & {
new (): InstanceType<M>;
// @ts-ignore
}>(this: M): Promise<PluralResponse<InstanceType<M>>> {
// @ts-expect-error
return this.get();
}
/**
* Set multiple attributes on the model.
*/
setAttributes(attributes: Partial<A> | Map<keyof A, ValueOf<A>>) {
// Record to map
if (!(attributes instanceof Map)) {
// eslint-disable-next-line no-param-reassign
attributes = new Map(Object.entries(attributes) as [keyof A, ValueOf<A>][]);
}
for (const [attribute, value] of attributes) {
this.setAttribute(attribute, value);
}
}
getAttribute<AN extends keyof A = keyof A>(attributeName: AN) {
return super.getAttribute(attributeName as string) as ValueOf<A, AN>;
}
getAttributes() {
return super.getAttributes() as ModelAttributes;
}
protected getAttributeAsDate(attributeName: string) {
// @ts-ignore
let attribute: string | Date = (this.attributes as Map<string, string | null>).get(attributeName);
if (attribute && dayjs(attribute).isValid()) {
attribute = super.getAttributeAsDate(attributeName) as Date;
}
return attribute;
}
setAttribute<AN extends keyof A = keyof A>(attributeName: AN, value: ValueOf<A, AN>) {
const date = dayjs(value as string | Date | undefined);
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (this.isDateAttribute(attributeName) && date.isValid()) {
const format = this.constructor.dates[attributeName as string];
// eslint-disable-next-line no-param-reassign
value = (format === 'YYYY-MM-DDTHH:mm:ss.ssssssZ' ? date.toISOString() : date.format(format)) as ValueOf<A, AN>;
}
// @ts-expect-error — This is needed to parse the dates correctly.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
this.attributes.set(attributeName as string, value);
}
getRelation<RN extends keyof R = keyof R>(relationName: RN) {
return super.getRelation(relationName as string) as ValueOf<R, RN>;
}
/**
* Get model ID.
*/
getId() {
return this.getApiId();
}
/**
* Check if the model is new (not already saved).
*/
isNew() {
return this.getId() === undefined;
}
}

View File

@ -0,0 +1,15 @@
import {Attr, Model, SpraypaintBase} from 'spraypaint';
@Model()
export default class Record extends SpraypaintBase {
static baseUrl = '';
static apiNamespace = '/api/restify';
static clientApplication = 'OpenSTAManager';
@Attr({persist: false}) createdAt!: string;
@Attr({persist: false}) updatedAt!: string;
isNew(): boolean {
return this.id === undefined;
}
}

View File

@ -1,15 +1,10 @@
import Model, { import Record from '@osm/Models/Record';
ModelAttributes, import {Attr, Model} from 'spraypaint';
ModelRelations
} from '@osm/Models/Model';
export interface UserAttributes extends ModelAttributes { @Model()
username: string; export default class User extends Record {
email: string; static jsonapiType = 'users';
@Attr() username!: string;
@Attr() email!: string;
} }
export interface UserRelations extends ModelRelations {
// Notifications: DatabaseNotifications
}
export default class User extends Model<UserAttributes, UserRelations> {}

View File

@ -12,9 +12,9 @@ export default class UserRecord extends RecordPage<User> {
return ( return (
<> <>
{this.backButton(vnode)} {this.backButton(vnode)}
<h1>{this.record?.getAttribute('username')}</h1> <h1>{this.record?.username}</h1>
<code> <code>
{JSON.stringify(this.record?.getAttributes())} {JSON.stringify(this.record?.attributes)}
</code> </code>
</> </>
); );

View File

@ -35,9 +35,9 @@ export default class UsersRecordDialog extends AddEditRecordDialog<User> {
} }
async save() { async save() {
if (this.record.isNew()) { // if (this.record.isNew()) {
this.record.setAttribute('password', 'default'); // this.record.password = 'default';
} // }
return super.save(); return super.save();
} }
} }