From 63f985aa0e2f859bd7b8ed6aaf74ce1c83d7346f Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 17 Jan 2026 23:14:05 -0700 Subject: [PATCH] Refactor the database logic for the apps into it's own mixin for reuse --- eslint.config.mjs | 11 +++- langs/en-ca.json | 2 +- module/apps/Artist.mjs | 98 ++++++++---------------------- module/apps/mixins/DBConnector.mjs | 89 +++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 75 deletions(-) create mode 100644 module/apps/mixins/DBConnector.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs index 690dd8d..3bf08fc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -75,7 +75,16 @@ export default [ "@stylistic/space-infix-ops": `warn`, "@stylistic/eol-last": `warn`, "@stylistic/operator-linebreak": [`warn`, `before`], - "@stylistic/indent": [`warn`, `tab`], + "@stylistic/indent": [ + `warn`, + `tab`, + { + SwitchCase: 1, + ignoredNodes: [ + `> .superClass`, + ], + }, + ], "@stylistic/brace-style": [`off`], "@stylistic/quotes": [`warn`, `backtick`, { "avoidEscape": true }], "@stylistic/comma-dangle": [`warn`, { arrays: `always-multiline`, objects: `always-multiline`, imports: `always-multiline`, exports: `always-multiline`, functions: `always-multiline` }], diff --git a/langs/en-ca.json b/langs/en-ca.json index 06cfedd..1664eba 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -22,7 +22,7 @@ "notifs": { "error": { "db-out-of-date": "Database out of date, please try again.", - "artist-ID-404": "An artist cannot be found with ID: {id}" + "document-ID-404": "Cannot find {dbType} with ID: {id}" } } } diff --git a/module/apps/Artist.mjs b/module/apps/Artist.mjs index 72bcb4d..a179561 100644 --- a/module/apps/Artist.mjs +++ b/module/apps/Artist.mjs @@ -1,11 +1,16 @@ import { __ID__, filePath } from "../consts.mjs"; -import { getFile, lastModifiedAt, uploadJson } from "../utils/fs.mjs"; +import { getFile, uploadJson } from "../utils/fs.mjs"; import { promptViaTemplate } from "../utils/dialogs.mjs"; +import { DBConnectorMixin } from "./mixins/DBConnector.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { randomID } = foundry.utils; -export class ArtistApp extends HandlebarsApplicationMixin(ApplicationV2) { +export class ArtistApp extends + DBConnectorMixin( + HandlebarsApplicationMixin( + ApplicationV2 +)) { // #region Options static DEFAULT_OPTIONS = { @@ -47,25 +52,19 @@ export class ArtistApp extends HandlebarsApplicationMixin(ApplicationV2) { // #endregion Options // #region Instance Data - /** @type { null | string } */ - #artistID = null; + static dbType = `Artist`; + static dbPath = `storage/db/artists.json`; - /** The artist that is being edited, or the default artist values */ - #artist = { name: ``, links: [] }; - - /** @type { null | string } */ - #lastModified = null; - - constructor({ artistID = null, ...opts } = {}) { - super(opts); - this.#artistID = artistID; - }; + /** + * The existing artist data that is being edited, or the default values + */ + _doc = { name: ``, links: [] }; get title() { - if (this.#artistID && this.#artist.name) { + if (this._docID && this._doc.name) { return game.i18n.format( `TB.apps.ArtistApp.title.edit`, - { name: this.#artist.name }, + { name: this._doc.name }, ); }; return game.i18n.localize(`TB.apps.ArtistApp.title.create`); @@ -73,50 +72,12 @@ export class ArtistApp extends HandlebarsApplicationMixin(ApplicationV2) { // #endregion Instance Data // #region Lifecycle - /** Whether or not the database values have been initialized */ - #connected = false; - - /** - * This fetches the DB data that we care about for the purposes of being able - * to create/edit entries in the Artists DB - */ - async #connectToDB() { - if (this.#connected) { return true }; - this.#lastModified ??= await lastModifiedAt(`storage/db/artists.json`); - - if (this.#artistID) { - const artists = await getFile(`storage/db/artists.json`); - - if (artists[this.#artistID] == null) { - ui.notifications.error(_loc( - `TB.notifs.error.artist-ID-404`, - { id: this.#artistID }, - )); - return false; - }; - - Object.assign(this.#artist, artists[this.#artistID]); - }; - this.#connected = true; - return true; - }; - - /** - * Ensures that the app is in a state where it is allowed to render - * before actually letting it render at all. - */ - async render(options, ...args) { - const allowed = await this.#connectToDB(); - if (!allowed) { return this }; - return super.render(options, ...args); - }; - /** * Makes it so that we update the frame's title to include the Artist name if * we're editing an existing artist instead of creating a new one. */ _onFirstRender() { - if (this.#artist.name) { + if (this._doc.name) { this._updateFrame({ window: { title: this.title } }); }; }; @@ -126,27 +87,18 @@ export class ArtistApp extends HandlebarsApplicationMixin(ApplicationV2) { const artist = formData.object; if (artist.name.length === 0) { return }; - artist.links = this.#artist.links; + artist.links = this._doc.links; artist.links.forEach(link => { delete link.isNew; }); - // Validate the DB hasn't been updated since - if (this.#artistID) { - const newLastModified = await lastModifiedAt(`storage/db/artists.json`); - if (newLastModified !== this.#lastModified) { - ui.notifications.error( - `TB.notifs.error.db-out-of-date`, - { localize: true }, - ); - return; - }; - }; + // Validate the DB hasn't been updated since opening + if (!(await this.isAbleToSave())) { return }; - const artists = getFile(`storage/db/artists.json`); + const artists = await getFile(`storage/db/artists.json`); - let id = this.#artistID; - if (!this.#artistID) { + let id = this._docID; + if (!this._docID) { do { id = randomID(); } while (artists[id] != null); @@ -163,7 +115,7 @@ export class ArtistApp extends HandlebarsApplicationMixin(ApplicationV2) { meta: { idp: this.id, }, - artist: this.#artist, + artist: this._doc, }; return ctx; @@ -178,7 +130,7 @@ export class ArtistApp extends HandlebarsApplicationMixin(ApplicationV2) { `templates/Dialogs/Link.hbs`, ); link.isNew = true; - this.#artist.links.push(link); + this._doc.links.push(link); this.render({ parts: [ `linkList` ] }); }; @@ -186,7 +138,7 @@ export class ArtistApp extends HandlebarsApplicationMixin(ApplicationV2) { static async #deleteLink(event, element) { const index = element.closest(`[data-index]`)?.dataset.index; if (index == null) { return }; - this.#artist.links.splice(index, 1); + this._doc.links.splice(index, 1); this.render({ parts: [ `linkList` ] }); }; // #endregion Actions diff --git a/module/apps/mixins/DBConnector.mjs b/module/apps/mixins/DBConnector.mjs new file mode 100644 index 0000000..e8baabf --- /dev/null +++ b/module/apps/mixins/DBConnector.mjs @@ -0,0 +1,89 @@ +import { getFile, lastModifiedAt } from "../../utils/fs.mjs"; +import { __ID__ } from "../../consts.mjs"; + + +export function DBConnectorMixin(HandlebarsApp) { + class DBConnectorApp extends HandlebarsApp { + // #region Instance Data + #lastModified = null; + + #connected = false; + + _docID; + _doc; + + static dbType = `Unknown`; + static dbPath; + + constructor({ docID, ...opts } = {}) { + super(opts); + if (this.constructor.dbPath == null) { + throw `dbPath must be defined on a DB-connected app`; + }; + this._docID = docID; + }; + + get dbPath() { + return this.constructor.dbPath; + }; + + get dbType() { + return this.constructor.dbType; + }; + // #endregion Instance Data + + // #region Lifecycle + /** + * Ensures that the app is in a state where it is allowed to render + * before actually letting it render at all. + */ + async render(options, ...args) { + const allowed = await this.#connectToDB(); + if (!allowed) { return this }; + return super.render(options, ...args); + }; + + /** + * This fetches the DB data that we care about for the purposes of being able + * to create/edit entries in the Artists DB + */ + async #connectToDB() { + if (this.#connected) { return true }; + this.#lastModified ??= await lastModifiedAt(this.dbPath); + + if (this._docID) { + const documents = await getFile(this.dbPath); + + if (documents[this._docID] == null) { + ui.notifications.error(_loc( + `TB.notifs.error.document-ID-404`, + { id: this._docID, dbType: this.dbType }, + )); + return false; + }; + + Object.assign(this._doc, documents[this._docID]); + }; + this.#connected = true; + return true; + }; + + async isAbleToSave() { + if (!this._docID) { return true }; + + const newLastModified = await lastModifiedAt(this.dbPath); + if (newLastModified !== this.#lastModified) { + ui.notifications.error( + `TB.notifs.error.db-out-of-date`, + { localize: true }, + ); + return false; + }; + + return true; + }; + // #endregion Lifecycle + }; + + return DBConnectorApp; +};