From bb095a9b4eec2c88ed90868667e6a4ca4a456f94 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 19 Nov 2025 21:02:49 -0700 Subject: [PATCH] Get user re-querying working when they disconnect and improve the user status --- module/apps/QueryStatus.mjs | 22 +++++-- module/hooks/userConnected.mjs | 5 +- module/utils/QueryManager.mjs | 101 ++++++++++++++++++++++++++++---- styles/Apps/QueryStatus.css | 9 ++- templates/QueryStatus/users.hbs | 47 ++++++++++----- 5 files changed, 148 insertions(+), 36 deletions(-) diff --git a/module/apps/QueryStatus.mjs b/module/apps/QueryStatus.mjs index e5ce09b..cc75d95 100644 --- a/module/apps/QueryStatus.mjs +++ b/module/apps/QueryStatus.mjs @@ -18,6 +18,9 @@ export class QueryStatus extends HandlebarsApplicationMixin(ApplicationV2) { window: { resizable: true, }, + actions: { + promptUser: this.promptUser, + }, }; static PARTS = { @@ -53,10 +56,6 @@ export class QueryStatus extends HandlebarsApplicationMixin(ApplicationV2) { this._prepareUsers(ctx); break; }; - case `controls`: { - this._prepareControls(ctx); - break; - }; }; return ctx; @@ -74,14 +73,25 @@ export class QueryStatus extends HandlebarsApplicationMixin(ApplicationV2) { name: user.name, active: user.active, answers: query.responses[userID] ?? null, + status: query.status[userID], }); }; ctx.users = users; }; - - async _prepareControls(ctx) {}; // #endregion Lifecycle // #region Actions + /** @this {QueryStatus} */ + static async promptUser($e, element) { + const userID = element.closest(`[data-user-id]`)?.dataset.userId; + if (!userID) { return }; + QueryManager.requery(this.requestID, [ userID ]); + }; + + /** @this {QueryStatus} */ + static async cancelRequest() {}; + + /** @this {QueryStatus} */ + static async finishEarly() {}; // #endregion Actions }; diff --git a/module/hooks/userConnected.mjs b/module/hooks/userConnected.mjs index 750417e..ed23837 100644 --- a/module/hooks/userConnected.mjs +++ b/module/hooks/userConnected.mjs @@ -1,5 +1,6 @@ import { QueryManager } from "../utils/QueryManager.mjs"; -Hooks.on(`userConnected`, (user) => { - QueryManager.userActivity(user.id); +Hooks.on(`userConnected`, (user, connected) => { + if (user.isSelf) { return }; + QueryManager.userActivity(user.id, connected); }); diff --git a/module/utils/QueryManager.mjs b/module/utils/QueryManager.mjs index 9adce82..9a4f892 100644 --- a/module/utils/QueryManager.mjs +++ b/module/utils/QueryManager.mjs @@ -1,3 +1,9 @@ +/** + * An object containing information about the current status for all users involved + * with the data request. + * @typedef {Record} UserStatus + */ + /** * @typedef QueryData * @property {string[]} users @@ -5,6 +11,9 @@ * @property {Record} responses * @property {(() => Promise)|null} onSubmit * @property {QueryStatus|null} app + * @property {UserStatus} status + * @property {object} request The data used to form the initial request + * @property {object} config The data used to create the initial config */ import { filePath } from "../consts.mjs"; @@ -29,6 +38,7 @@ export class QueryManager { return this.#queries.has(requestID); }; + /** @returns {Omit} */ static get(requestID) { if (!this.#queries.has(requestID)) { return null }; const query = this.#queries.get(requestID); @@ -36,6 +46,7 @@ export class QueryManager { delete cloned.onSubmit; delete cloned.resolve; + delete cloned.app; return foundry.utils.deepFreeze(cloned); }; @@ -68,15 +79,29 @@ export class QueryManager { return null; }; + users ??= game.users + .filter(u => u.id !== game.user.id) + .map(u => u.id); + const promise = new Promise((resolve) => { + + /** @type {UserStatus} */ + const status = {}; + for (const user of users) { + status[user] = game.users.get(user).active ? `waiting` : `unprompted`; + }; + this.#queries.set( request.id, { - users: users ?? game.users.filter(u => u.id !== game.user.id).map(u => u.id), - resolve, + users, + request, + config, responses: {}, + resolve, onSubmit, app: null, + status, }, ); }); @@ -90,15 +115,62 @@ export class QueryManager { return promise; }; + static async requery(requestID, users) { + const query = this.#queries.get(requestID); + if (!query) { return }; + + game.socket.emit(`system.taf`, { + event: `query.prompt`, + payload: { + id: requestID, + users, + request: query.request, + config: query.config, + }, + }); + + for (const user of users) { + query.status[user] = `waiting`; + }; + query.app?.render({ parts: [ `users` ] }); + }; + static async addResponse(requestID, userID, answers) { const data = this.#queries.get(requestID); data.responses[userID] = answers; + data.status[userID] = `finished`; await data.onSubmit?.(userID, answers); + this.maybeResolve(requestID); + }; - // Validate for responses from everyone - if (data.users.length === Object.keys(data.responses).length) { - data.app.close(); + static async maybeResolve(requestID) { + const data = this.#queries.get(requestID); + + // Determine how many users are considered "finished" + let finishedUserCount = 0; + for (const user of data.users) { + const hasApp = data.app != null; + + switch (data.status[user]) { + case `finished`: { + finishedUserCount++; + break; + }; + case `cancelled`: + case `disconnected`: + case `unprompted`: { + if (!hasApp) { + finishedUserCount++; + }; + break; + }; + }; + }; + + // Ensure that we have a finished response from everyone prompted + if (data.users.length === finishedUserCount) { + data.app?.close(); data.resolve(data.responses); } else { data.app?.render({ parts: [ `users` ] }); @@ -137,14 +209,21 @@ export class QueryManager { query.app = app; }; - static async userActivity(userID) { - for (const query of this.#queries.values()) { + static async userActivity(userID, connected) { + for (const [id, query] of this.#queries.entries()) { if (query.users.includes(userID)) { - query.app.render({ parts: [ `users` ] }); - // TODO: if the user is connecting, we want to open - // the ask modal on their browser so that they can - // actually fill in the data + // Update the user's status to allow for the app to re-prompt them + if (query.status[userID] !== `finished`) { + if (connected) { + query.status[userID] = `unprompted`; + } else { + query.status[userID] = `disconnected`; + }; + this.maybeResolve(id); + }; + + query.app?.render({ parts: [ `users` ] }); }; }; }; diff --git a/styles/Apps/QueryStatus.css b/styles/Apps/QueryStatus.css index fbbc60e..5532505 100644 --- a/styles/Apps/QueryStatus.css +++ b/styles/Apps/QueryStatus.css @@ -9,12 +9,17 @@ li { display: flex; - flex-direction: row; - align-items: center; + flex-direction: column; margin: 0; border: 1px solid yellowgreen; border-radius: 4px; padding: 4px 8px; + + > .user-summary { + display: flex; + flex-direction: row; + align-items: center; + } } } } diff --git a/templates/QueryStatus/users.hbs b/templates/QueryStatus/users.hbs index 640bfdd..4ade68d 100644 --- a/templates/QueryStatus/users.hbs +++ b/templates/QueryStatus/users.hbs @@ -1,30 +1,47 @@
    {{#each users as | user |}} -
  • -
    - {{ user.name }} +
  • +
    +
    + {{ user.name }} +
    + {{#if (eq user.status "cancelled")}} + Cancelled by User + {{else if (eq user.status "waiting")}} + + {{else if (eq user.status "disconnected")}} + + {{else if (eq user.status "unprompted")}} + + {{/if}}
    - {{#if user.answers}} + {{#if (eq user.status "finished")}}
    {{#each user.answers as | answer |}} - {{ @key }} + + {{ @key }} + {{ answer }} {{/each}}
    - {{else if user.active}} - - {{else}} - {{/if}}
  • {{/each}}