Data Request API helper #10

Merged
Oliver merged 94 commits from feat/data-requests into main 2025-11-22 02:51:15 +00:00
5 changed files with 148 additions and 36 deletions
Showing only changes of commit bb095a9b4e - Show all commits

View file

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

View file

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

View file

@ -1,3 +1,9 @@
/**
* An object containing information about the current status for all users involved
* with the data request.
* @typedef {Record<string, "finished"|"waiting"|"cancelled"|"disconnected"|"unprompted">} UserStatus
*/
/**
* @typedef QueryData
* @property {string[]} users
@ -5,6 +11,9 @@
* @property {Record<string, object>} responses
* @property {(() => Promise<void>)|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<QueryData, "resolve"|"onSubmit"|"app">} */
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;
Oliver marked this conversation as resolved Outdated

Add check to ensure that the query isn't undefined

Add check to ensure that the query isn't undefined
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` ] });
};
};
};

View file

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

View file

@ -1,30 +1,47 @@
<ul class="user-list">
{{#each users as | user |}}
<li style="--spinner-inner-colour: var(--user-color-{{user.id}})">
<div class="grow">
{{ user.name }}
<li
style="--spinner-inner-colour: var(--user-color-{{user.id}})"
data-user-id="{{ user.id }}"
>
<div class="user-summary">
<div class="grow">
{{ user.name }}
</div>
{{#if (eq user.status "cancelled")}}
Oliver marked this conversation as resolved Outdated

Cancelled is no longer a user status supported

Cancelled is no longer a user status supported
<span>Cancelled by User</span>
{{else if (eq user.status "waiting")}}
<span class="loader"></span>
{{else if (eq user.status "disconnected")}}
<taf-icon
data-tooltip="taf.Apps.QueryStatus.user-disconnected-tooltip"
name="icons/disconnected"
var:size="40px"
var:stroke="currentColor"
var:fill="currentColor"
></taf-icon>
{{else if (eq user.status "unprompted")}}
<button
type="button"
data-action="promptUser"
>
Send Request
Oliver marked this conversation as resolved

Localize

Localize
</button>
{{/if}}
</div>
{{#if user.answers}}
{{#if (eq user.status "finished")}}
<div class="chip-list">
{{#each user.answers as | answer |}}
<span class="chip">
{{ @key }}
<span class="key">
{{ @key }}
</span>
<span class="value">
{{ answer }}
</span>
</span>
{{/each}}
</div>
{{else if user.active}}
<span class="loader"></span>
{{else}}
<taf-icon
data-tooltip="taf.Apps.QueryStatus.user-disconnected-tooltip"
name="icons/disconnected"
var:size="40px"
var:stroke="currentColor"
var:fill="currentColor"
></taf-icon>
{{/if}}
</li>
{{/each}}