Refactor the database logic for the apps into it's own mixin for reuse

This commit is contained in:
Oliver 2026-01-17 23:14:05 -07:00
parent ffa2162fbd
commit 63f985aa0e
4 changed files with 125 additions and 75 deletions

View file

@ -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` }],

View file

@ -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}"
}
}
}

View file

@ -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

View file

@ -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;
};