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:
parent
b448da234e
commit
71b03c14b6
|
@ -11,3 +11,7 @@ rules:
|
|||
- error
|
||||
- except-parens
|
||||
no-plusplus: off
|
||||
new-cap:
|
||||
- error
|
||||
- capIsNewExceptions: ['Model', 'Attr', 'HasMany', 'HasOne', 'BelongsTo', 'Link', 'Stream']
|
||||
properties: false
|
||||
|
|
|
@ -18,11 +18,10 @@
|
|||
"@maicol07/material-web-additions": "^1.4.4",
|
||||
"@material/mwc-snackbar": "^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",
|
||||
"classnames": "^2.3.2",
|
||||
"collect.js": "^4.36.1",
|
||||
"coloquent": "npm:@maicol07/coloquent@3.0.1-beta",
|
||||
"dayjs": "^1.11.10",
|
||||
"include-media": "^2.0.0",
|
||||
"lit": "^3.1.0",
|
||||
|
@ -32,6 +31,7 @@
|
|||
"postcss-scss": "^4.0.9",
|
||||
"prntr": "^2.0.18",
|
||||
"readable-http-codes": "^1.1.1",
|
||||
"spraypaint": "^0.10.24",
|
||||
"ts-pattern": "^5.0.6",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript-cookie": "^1.0.6"
|
||||
|
|
|
@ -14,7 +14,7 @@ import DataTable, {DataTableAttributes} from '@osm/Components/DataTable/DataTabl
|
|||
import DataTableColumn, {DataTableColumnAttributes} from '@osm/Components/DataTable/DataTableColumn';
|
||||
import RecordsTableColumn from '@osm/Components/DataTable/RecordsTableColumn';
|
||||
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 collect, {Collection} from 'collect.js';
|
||||
import {
|
||||
|
@ -28,7 +28,7 @@ export interface RecordsTableColumnAttributes extends DataTableColumnAttributes
|
|||
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>;
|
||||
records: Map<string, M>;
|
||||
readonly?: boolean;
|
||||
|
@ -42,7 +42,7 @@ export interface RecordsTableAttributes<M extends Model<any, any>> extends DataT
|
|||
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;
|
||||
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());
|
||||
}
|
||||
|
||||
rows.set(record.getId()!, cells);
|
||||
rows.set(record.id!, cells);
|
||||
}
|
||||
|
||||
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) {
|
||||
event.stopPropagation();
|
||||
vnode.attrs.onDeleteRecordButtonClick?.(record.getId()!, event);
|
||||
vnode.attrs.onDeleteRecordButtonClick?.(record.id!, event);
|
||||
}
|
||||
|
||||
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 {
|
||||
// Check if is a relation
|
||||
let value: unknown = record.getAttribute(attribute);
|
||||
// @ts-expect-error
|
||||
let value: unknown = record[attribute];
|
||||
if (attribute === 'id') {
|
||||
value = record.getId();
|
||||
value = record.id;
|
||||
}
|
||||
|
||||
return vnode.attrs.valueModifier?.(value, attribute, record) ?? value;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {mdiFloppy} from '@mdi/js';
|
||||
import RecordDialog, {RecordDialogAttributes} from '@osm/Components/Dialogs/RecordDialog';
|
||||
import MdIcon from '@osm/Components/MdIcon';
|
||||
import Model from '@osm/Models/Model';
|
||||
import Model from '@osm/Models/Record';
|
||||
import {
|
||||
VnodeCollection,
|
||||
VnodeCollectionItem
|
||||
|
@ -13,7 +13,6 @@ import {
|
|||
showSnackbar
|
||||
} from '@osm/utils/misc';
|
||||
import collect, {Collection} from 'collect.js';
|
||||
import {SaveResponse} from 'coloquent';
|
||||
import {
|
||||
Children,
|
||||
Vnode,
|
||||
|
@ -23,7 +22,7 @@ import Stream from 'mithril/stream';
|
|||
import {Form} from 'mithril-utilities';
|
||||
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
|
||||
protected formElement: HTMLFormElement | null = null;
|
||||
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);
|
||||
|
||||
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() {
|
||||
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> {
|
||||
this.record.setAttributes(this.modelAttributesFromFormState);
|
||||
this.record.assignAttributes(this.modelAttributesFromFormState);
|
||||
try {
|
||||
const response = await this.record.save();
|
||||
this.afterSave(response);
|
||||
return response.getModelId() !== undefined;
|
||||
const result = await this.record.save();
|
||||
this.afterSave(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.onSaveError(error as JSONAPI.RequestError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
afterSave(response: SaveResponse<M>): void {
|
||||
const responseModel = response.getModel() as M;
|
||||
if (responseModel !== undefined) {
|
||||
this.record = responseModel;
|
||||
afterSave(result: boolean): void {
|
||||
if (result) {
|
||||
void showSnackbar(__('Record salvato con successo'));
|
||||
}
|
||||
}
|
||||
|
||||
onSaveError(error: JSONAPI.RequestError): void {
|
||||
const message = error.response.data.message;
|
||||
const {message} = error.response.data;
|
||||
void showSnackbar(message, false);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {mdiDelete} from '@mdi/js';
|
||||
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 {showSnackbar} from '@osm/utils/misc';
|
||||
import collect from 'collect.js';
|
||||
|
@ -12,11 +12,11 @@ import {RequestError} from 'mithril-utilities';
|
|||
|
||||
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[];
|
||||
}
|
||||
|
||||
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[];
|
||||
|
||||
oninit(vnode: Vnode<A, this>) {
|
||||
|
@ -32,7 +32,7 @@ export default class DeleteRecordDialog<M extends Model<any, any>, A extends Del
|
|||
return (
|
||||
<>
|
||||
<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 {
|
||||
return __('ID: :recordId', {recordId: record.getId()!});
|
||||
return __('ID: :recordId', {recordId: record.id!});
|
||||
}
|
||||
|
||||
async onConfirmButtonClicked() {
|
||||
|
@ -74,12 +74,12 @@ export default class DeleteRecordDialog<M extends Model<any, any>, A extends Del
|
|||
|
||||
async deleteRecord() {
|
||||
try {
|
||||
const promises = this.records.map((record) => record.delete());
|
||||
const promises = this.records.map((record) => record.destroy());
|
||||
await Promise.all(promises);
|
||||
|
||||
// TODO: Better way for pluralization in i18n
|
||||
void showSnackbar(this.records.length > 1 ? __('Record eliminati!') : __('Record eliminato!'));
|
||||
this.close('deleted');
|
||||
void this.close('deleted');
|
||||
} catch (error) {
|
||||
void showSnackbar(__('Errore durante l\'eliminazione del record! :error', {error: (error as RequestError<{message: string}>).response.message}), false);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import '@maicol07/material-web-additions/layout-grid/layout-grid.js';
|
|||
import '@material/web/button/text-button.js';
|
||||
|
||||
import Dialog, {DialogAttributes} from '@osm/Components/Dialogs/Dialog';
|
||||
import Model from '@osm/Models/Model';
|
||||
import Model from '@osm/Models/Record';
|
||||
import {Vnode} from 'mithril';
|
||||
|
||||
export interface RecordDialogAttributes<M extends Model<any, any>> extends DialogAttributes {
|
||||
|
|
|
@ -3,27 +3,27 @@ import '@material/web/button/outlined-button.js';
|
|||
import {mdiChevronLeft} from '@mdi/js';
|
||||
import MdIcon from '@osm/Components/MdIcon';
|
||||
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 {Builder} from 'coloquent';
|
||||
import {
|
||||
Children,
|
||||
Vnode
|
||||
} from 'mithril';
|
||||
import {Scope} from 'spraypaint';
|
||||
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;
|
||||
}
|
||||
|
||||
export default abstract class RecordPage<M extends Model<any, any>, A extends RecordPageAttributes<M> = RecordPageAttributes<M>> extends Page<A> {
|
||||
abstract recordType: Class<M> & typeof Model<any, any>;
|
||||
export default abstract class RecordPage<M extends Model, A extends RecordPageAttributes<M> = RecordPageAttributes<M>> extends Page<A> {
|
||||
abstract recordType: Class<M> & typeof Model;
|
||||
record?: M;
|
||||
|
||||
async oninit(vnode: Vnode<A, this>) {
|
||||
super.oninit(vnode);
|
||||
const {id: recordId} = route().params as {id: number | string};
|
||||
if (recordId !== this.record?.getId()) {
|
||||
if (recordId !== this.record?.id) {
|
||||
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) {
|
||||
try {
|
||||
const response = await this.modelQuery().find(recordId);
|
||||
this.record = response.getData() || undefined;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.record = response.data || undefined;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
void showSnackbar(__('Errore durante il caricamento del record'));
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
modelQuery(): Builder<M> {
|
||||
return this.recordType.query();
|
||||
modelQuery(): Scope<M> {
|
||||
return this.recordType as unknown as Scope<M>;
|
||||
}
|
||||
|
||||
contents(vnode: Vnode<A>): Children {
|
||||
|
|
|
@ -21,9 +21,8 @@ import DeleteRecordDialog, {DeleteRecordDialogAttributes} from '@osm/Components/
|
|||
import RecordDialog, {RecordDialogAttributes} from '@osm/Components/Dialogs/RecordDialog';
|
||||
import MdIcon from '@osm/Components/MdIcon';
|
||||
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 {SortDirection} from 'coloquent';
|
||||
import dayjs from 'dayjs';
|
||||
import type {
|
||||
Children,
|
||||
|
@ -31,38 +30,25 @@ import type {
|
|||
VnodeDOM
|
||||
} from 'mithril';
|
||||
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/dist/types/Match';
|
||||
import type {Class} from 'type-fest';
|
||||
|
||||
export interface Meta {
|
||||
current_page: number;
|
||||
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>;
|
||||
type RecordDialogVnode<M extends Record, D extends RecordDialog<M>> = Vnode<RecordDialogAttributes<M>, D>;
|
||||
type DeleteRecordDialogVnode<M extends Record, D extends DeleteRecordDialog<M>> = Vnode<DeleteRecordDialogAttributes<M>, D>;
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
/**
|
||||
* @abstract
|
||||
*/
|
||||
export default abstract class RecordsPage<
|
||||
M extends Model<any, any>,
|
||||
M extends Record,
|
||||
D extends AddEditRecordDialog<M> = AddEditRecordDialog<M>,
|
||||
DRD extends DeleteRecordDialog<M> = DeleteRecordDialog<M>
|
||||
> extends Page {
|
||||
abstract modelType: Class<M> & typeof Model<any, any>;
|
||||
abstract modelType: Class<M> & typeof Record;
|
||||
recordDialogType?: Class<D>;
|
||||
deleteRecordDialogType?: Class<DRD>;
|
||||
|
||||
|
@ -86,15 +72,13 @@ export default abstract class RecordsPage<
|
|||
protected lastRowOfPage = this.totalRecords;
|
||||
|
||||
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();
|
||||
private listenedFilterColumns: string[] = [];
|
||||
private listenedSortedColumns: string[] = [];
|
||||
|
||||
oninit(vnode: Vnode<PageAttributes, this>) {
|
||||
super.oninit(vnode);
|
||||
// @ts-ignore
|
||||
this.modelType.pageSize = this.currentPageSize;
|
||||
// Redraw on a first load to call onbeforeupdate
|
||||
m.redraw();
|
||||
}
|
||||
|
@ -111,24 +95,21 @@ export default abstract class RecordsPage<
|
|||
async loadRecords() {
|
||||
this.isTableLoading = true;
|
||||
|
||||
let query = this.modelQuery();
|
||||
const query = this.modelQuery();
|
||||
|
||||
// Fix Restify when filtering relations
|
||||
query = query.option('related', query.getQuery().getInclude().join(','));
|
||||
|
||||
const response = await query.get(this.currentPage);
|
||||
const rawResponse = response.getHttpClientResponse().getData() as JSONAPIResponse;
|
||||
this.lastPage = rawResponse.meta.last_page;
|
||||
this.firstRowOfPage = rawResponse.meta.from;
|
||||
this.lastRowOfPage = rawResponse.meta.to;
|
||||
this.currentPageSize = rawResponse.meta.per_page;
|
||||
this.totalRecords = rawResponse.meta.total;
|
||||
const data = response.getData();
|
||||
const response = await query.page(this.currentPage).all();
|
||||
const rawResponse = response.raw;
|
||||
this.lastPage = rawResponse.meta?.last_page as number;
|
||||
this.firstRowOfPage = rawResponse.meta?.from as number;
|
||||
this.lastRowOfPage = rawResponse.meta?.to as number;
|
||||
this.currentPageSize = rawResponse.meta?.per_page as number;
|
||||
this.totalRecords = rawResponse.meta?.total as number;
|
||||
const {data} = response;
|
||||
|
||||
this.records.clear();
|
||||
if (data.length > 0) {
|
||||
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) {
|
||||
return string_.replace(removeSpecials ? /[^\w ]/g : '', '')
|
||||
.replace(/([ _]+)/g, '_')
|
||||
.replace(/[ _]+/g, '_')
|
||||
.replace(trim ? /(^_|_$)/gm : '', '')
|
||||
.replace(underscoredNumbers ? /([^\dA-Z_])([^_a-z])/g : /([^\dA-Z_])([^\d_a-z])/g, (m, preUpper, upper) => `${preUpper}_${upper}`)
|
||||
.replace(underscoredNumbers ? /([^\d_]\d|\d[^\d_])/g : '', (m, index) => (index ? index.split('').join('_') : ''))
|
||||
.replace(/([A-Z])([A-Z])([^\dA-Z_])/g, (m, previousUpper, upper, lower) => `${previousUpper}_${upper}${lower}`)
|
||||
.replaceAll('_.', '.') // remove redundant underscores
|
||||
.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: string) => (index ? [...index].join('_') : ''))
|
||||
.replace(/([A-Z])([A-Z])([^\dA-Z_])/g, (m, previousUpper: string, upper: string, lower: string) => `${previousUpper}_${upper}${lower}`)
|
||||
.replaceAll('_.', '.') // Remove redundant underscores
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
modelQuery() {
|
||||
// @ts-ignore
|
||||
let query = this.modelType.query<M>();
|
||||
let query: Scope<M> = this.modelType.per(this.currentPageSize);
|
||||
|
||||
for (const [attribute, value] of this.filters) {
|
||||
// Query = query.where(attribute, value); TODO: Revert when Restify uses JSONAPI syntax
|
||||
query = query.option(RecordsPage.convertToSnakeCase(attribute), value);
|
||||
query = query.where({[RecordsPage.convertToSnakeCase(attribute)]: value});
|
||||
}
|
||||
|
||||
for (const [relation, value] of this.relatedFilters) {
|
||||
query = query.option('related', relation)
|
||||
.option('search', value);
|
||||
query = query.where({related: relation, search: value}); // TODO: Check
|
||||
// .where('search', value);
|
||||
}
|
||||
|
||||
for (const [attribute, value] of this.sort) {
|
||||
query = query.orderBy(RecordsPage.convertToSnakeCase(attribute), value);
|
||||
query = query.order({[RecordsPage.convertToSnakeCase(attribute)]: value});
|
||||
}
|
||||
|
||||
return query;
|
||||
|
@ -244,7 +223,7 @@ export default abstract class RecordsPage<
|
|||
|
||||
updateRecord(model: M) {
|
||||
if (this.recordPageRouteName) {
|
||||
router.visit(route(this.recordPageRouteName, {id: model.getId()!}));
|
||||
router.visit(route(this.recordPageRouteName, {id: model.id}));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -258,8 +237,8 @@ export default abstract class RecordsPage<
|
|||
for (const [key, state] of this.recordDialogsStates) {
|
||||
// noinspection LocalVariableNamingConventionJS
|
||||
const RD = this.recordDialogType!;
|
||||
const record = key instanceof Model ? key : this.records.get(key);
|
||||
const vnodeKey = record?.getId() ?? (key as string);
|
||||
const record = key instanceof Record ? key : this.records.get(key);
|
||||
const vnodeKey = record?.id ?? (key as string);
|
||||
collection.put(vnodeKey, <RD key={vnodeKey} record={record} open={state}/>);
|
||||
}
|
||||
|
||||
|
@ -303,14 +282,16 @@ export default abstract class RecordsPage<
|
|||
onTablePageChange(event: CustomEvent<PaginateDetail>) {
|
||||
const {pageSize, action} = event.detail;
|
||||
this.currentPageSize = pageSize;
|
||||
const {currentPage} = this;
|
||||
const {currentPage, lastPage} = this;
|
||||
match(action)
|
||||
.with('first', () => (this.currentPage = 1))
|
||||
.with('previous', () => (this.currentPage--))
|
||||
.with('next', () => (this.currentPage++))
|
||||
.with('last', () => (this.currentPage = this.lastPage))
|
||||
.with('last', () => (this.currentPage = lastPage))
|
||||
.with('current', () => {})
|
||||
.run();
|
||||
// We need to check if the page has changed
|
||||
// eslint-disable-next-line unicorn/consistent-destructuring
|
||||
if (currentPage !== this.currentPage) {
|
||||
this.refreshRecords = true;
|
||||
m.redraw();
|
||||
|
@ -356,13 +337,13 @@ export default abstract class RecordsPage<
|
|||
const {column, isDescending} = (event as CustomEvent<SortButtonClickedEventDetail>).detail;
|
||||
const modelAttribute = column.dataset.sortAttribute ?? column.dataset.modelAttribute!;
|
||||
this.sort.clear();
|
||||
this.sort.set(modelAttribute, isDescending ? SortDirection.DESC : SortDirection.ASC);
|
||||
this.sort.set(modelAttribute, isDescending ? 'desc' : 'asc');
|
||||
this.refreshRecords = true;
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (!state) {
|
||||
|
@ -386,7 +367,7 @@ export default abstract class RecordsPage<
|
|||
}
|
||||
|
||||
protected getRecordDialogState(record?: M, slug?: string) {
|
||||
const key: string = slug ?? record?.getId() ?? '';
|
||||
const key: string = slug ?? record?.id ?? '';
|
||||
|
||||
if (!this.recordDialogsStates.has(key)) {
|
||||
const state = Stream<boolean>(false);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,15 +1,10 @@
|
|||
import Model, {
|
||||
ModelAttributes,
|
||||
ModelRelations
|
||||
} from '@osm/Models/Model';
|
||||
import Record from '@osm/Models/Record';
|
||||
import {Attr, Model} from 'spraypaint';
|
||||
|
||||
export interface UserAttributes extends ModelAttributes {
|
||||
username: string;
|
||||
email: string;
|
||||
@Model()
|
||||
export default class User extends Record {
|
||||
static jsonapiType = 'users';
|
||||
|
||||
@Attr() username!: string;
|
||||
@Attr() email!: string;
|
||||
}
|
||||
|
||||
export interface UserRelations extends ModelRelations {
|
||||
// Notifications: DatabaseNotifications
|
||||
}
|
||||
|
||||
export default class User extends Model<UserAttributes, UserRelations> {}
|
||||
|
|
|
@ -12,9 +12,9 @@ export default class UserRecord extends RecordPage<User> {
|
|||
return (
|
||||
<>
|
||||
{this.backButton(vnode)}
|
||||
<h1>{this.record?.getAttribute('username')}</h1>
|
||||
<h1>{this.record?.username}</h1>
|
||||
<code>
|
||||
{JSON.stringify(this.record?.getAttributes())}
|
||||
{JSON.stringify(this.record?.attributes)}
|
||||
</code>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -35,9 +35,9 @@ export default class UsersRecordDialog extends AddEditRecordDialog<User> {
|
|||
}
|
||||
|
||||
async save() {
|
||||
if (this.record.isNew()) {
|
||||
this.record.setAttribute('password', 'default');
|
||||
}
|
||||
// if (this.record.isNew()) {
|
||||
// this.record.password = 'default';
|
||||
// }
|
||||
return super.save();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue