Archived
fix(actions): avoid redundant action panel rerenders
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web-actions": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Prevent redundant workspace action panel re-renders from resetting mobile scroll position or replacing action buttons mid-click, and show feedback for stale, cancelled, or already-starting actions.
|
||||||
@@ -15,6 +15,12 @@ type ConfigState =
|
|||||||
| { kind: "loading" }
|
| { kind: "loading" }
|
||||||
| WorkspaceActionsConfigLoadResult;
|
| WorkspaceActionsConfigLoadResult;
|
||||||
|
|
||||||
|
interface ActionStatus {
|
||||||
|
kind: "info" | "success" | "error";
|
||||||
|
message: string;
|
||||||
|
detail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const configCache = new Map<string, ConfigState>();
|
const configCache = new Map<string, ConfigState>();
|
||||||
|
|
||||||
export function defineActionsPanelElement(): void {
|
export function defineActionsPanelElement(): void {
|
||||||
@@ -33,7 +39,7 @@ class PiWebActionsPanel extends HTMLElement {
|
|||||||
private openTerminalValue: OpenTerminal | undefined;
|
private openTerminalValue: OpenTerminal | undefined;
|
||||||
private terminalCommandRunsValue: InternalTerminalCommandRunsRuntime | undefined;
|
private terminalCommandRunsValue: InternalTerminalCommandRunsRuntime | undefined;
|
||||||
private runningActionId: string | undefined;
|
private runningActionId: string | undefined;
|
||||||
private status: { kind: "info" | "success" | "error"; message: string; detail?: string } | undefined;
|
private status: ActionStatus | undefined;
|
||||||
private readonly root: ShadowRoot;
|
private readonly root: ShadowRoot;
|
||||||
private readonly onConfigChanged = () => {
|
private readonly onConfigChanged = () => {
|
||||||
this.render();
|
this.render();
|
||||||
@@ -45,7 +51,14 @@ class PiWebActionsPanel extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set workspace(value: Workspace | undefined) {
|
set workspace(value: Workspace | undefined) {
|
||||||
|
const previousKey = this.workspaceValue === undefined ? undefined : cacheKeyForWorkspace(this.workspaceValue);
|
||||||
|
const nextKey = value === undefined ? undefined : cacheKeyForWorkspace(value);
|
||||||
this.workspaceValue = value;
|
this.workspaceValue = value;
|
||||||
|
// Parent app updates should not rebuild this shadow DOM for the same workspace:
|
||||||
|
// doing so resets the mobile scroll position and can replace buttons mid-click.
|
||||||
|
if (previousKey === nextKey) return;
|
||||||
|
this.runningActionId = undefined;
|
||||||
|
this.status = undefined;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +96,7 @@ class PiWebActionsPanel extends HTMLElement {
|
|||||||
<button class="secondary" data-open-terminal>Open Terminal</button>
|
<button class="secondary" data-open-terminal>Open Terminal</button>
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
${this.renderStatus()}
|
||||||
<section class="viewer actions-viewer">
|
<section class="viewer actions-viewer">
|
||||||
${this.renderConfigState(state)}
|
${this.renderConfigState(state)}
|
||||||
</section>
|
</section>
|
||||||
@@ -94,8 +108,7 @@ class PiWebActionsPanel extends HTMLElement {
|
|||||||
|
|
||||||
for (const button of this.root.querySelectorAll("button[data-action-id]")) {
|
for (const button of this.root.querySelectorAll("button[data-action-id]")) {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
const action = actionFromConfigState(state, button.getAttribute("data-action-id"));
|
void this.dispatchActionById(workspace, button.getAttribute("data-action-id"));
|
||||||
if (action !== undefined) void this.dispatchAction(workspace, action);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,22 +117,36 @@ class PiWebActionsPanel extends HTMLElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private dispatchActionById(workspace: Workspace, actionId: string | null): Promise<void> {
|
||||||
|
if (!this.isCurrentWorkspace(workspace)) return Promise.resolve();
|
||||||
|
const action = actionFromConfigState(getCachedWorkspaceConfig(workspace), actionId);
|
||||||
|
if (action === undefined) {
|
||||||
|
this.status = { kind: "error", message: "That action is no longer available. Click Refresh, then try again." };
|
||||||
|
this.render();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return this.dispatchAction(workspace, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isCurrentWorkspace(workspace: Workspace): boolean {
|
||||||
|
return this.workspaceValue !== undefined && cacheKeyForWorkspace(this.workspaceValue) === cacheKeyForWorkspace(workspace);
|
||||||
|
}
|
||||||
|
|
||||||
private renderConfigState(state: ConfigState): string {
|
private renderConfigState(state: ConfigState): string {
|
||||||
if (state.kind === "loading") return `<p class="muted">Loading ${escapeHtml(ACTIONS_CONFIG_PATH)}…</p>${this.renderStatus()}`;
|
if (state.kind === "loading") return `<p class="muted">Loading ${escapeHtml(ACTIONS_CONFIG_PATH)}…</p>`;
|
||||||
if (state.kind === "missing") return `${renderMissingState(state)}${this.renderStatus()}`;
|
if (state.kind === "missing") return renderMissingState(state);
|
||||||
if (state.kind === "unavailable") return `${renderUnavailableState(state)}${this.renderStatus()}`;
|
if (state.kind === "unavailable") return renderUnavailableState(state);
|
||||||
if (state.config.actions.length === 0) return `<p class="muted">No actions are defined in ${escapeHtml(ACTIONS_CONFIG_PATH)}. Add actions to the file, then click Refresh.</p>${this.renderStatus()}`;
|
if (state.config.actions.length === 0) return `<p class="muted">No actions are defined in ${escapeHtml(ACTIONS_CONFIG_PATH)}. Add actions to the file, then click Refresh.</p>`;
|
||||||
return `
|
return `
|
||||||
<p class="muted">Actions run in a dedicated workspace terminal, then switch to that terminal. Edit ${escapeHtml(ACTIONS_CONFIG_PATH)} and click Refresh to reload.</p>
|
<p class="muted">Actions run in a dedicated workspace terminal, then switch to that terminal. Edit ${escapeHtml(ACTIONS_CONFIG_PATH)} and click Refresh to reload.</p>
|
||||||
${renderActionGroups(state.config.actions, this.runningActionId)}
|
${renderActionGroups(state.config.actions, this.runningActionId)}
|
||||||
${this.renderStatus()}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderStatus(): string {
|
private renderStatus(): string {
|
||||||
if (this.status === undefined) return "";
|
if (this.status === undefined) return "";
|
||||||
const detail = this.status.detail === undefined ? "" : `<pre>${escapeHtml(this.status.detail)}</pre>`;
|
const detail = this.status.detail === undefined ? "" : `<pre>${escapeHtml(this.status.detail)}</pre>`;
|
||||||
return `<div class="status ${escapeAttr(this.status.kind)}">${escapeHtml(this.status.message)}${detail}</div>`;
|
return `<div class="status panel-status ${escapeAttr(this.status.kind)}">${escapeHtml(this.status.message)}${detail}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshConfig(workspace: Workspace): Promise<void> {
|
private async refreshConfig(workspace: Workspace): Promise<void> {
|
||||||
@@ -128,6 +155,7 @@ class PiWebActionsPanel extends HTMLElement {
|
|||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
const state = await refreshWorkspaceConfig(workspace);
|
const state = await refreshWorkspaceConfig(workspace);
|
||||||
|
if (!this.isCurrentWorkspace(workspace)) return;
|
||||||
this.status = state.kind === "loaded"
|
this.status = state.kind === "loaded"
|
||||||
? { kind: "success", message: `Loaded ${String(state.config.actions.length)} action${state.config.actions.length === 1 ? "" : "s"}.` }
|
? { kind: "success", message: `Loaded ${String(state.config.actions.length)} action${state.config.actions.length === 1 ? "" : "s"}.` }
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -135,8 +163,16 @@ class PiWebActionsPanel extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async dispatchAction(workspace: Workspace, action: WorkspaceAction): Promise<void> {
|
private async dispatchAction(workspace: Workspace, action: WorkspaceAction): Promise<void> {
|
||||||
if (this.runningActionId !== undefined) return;
|
if (this.runningActionId !== undefined) {
|
||||||
if (action.confirm && !window.confirm(`Run ${action.title}?\n\n${action.command}`)) return;
|
this.status = { kind: "info", message: "Another action is already starting. Wait for it to finish dispatching, then try again." };
|
||||||
|
this.render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.confirm && !window.confirm(`Run ${action.title}?\n\n${action.command}`)) {
|
||||||
|
this.status = { kind: "info", message: `Cancelled ${action.title}.` };
|
||||||
|
this.render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const terminal = this.terminalCommandRunsValue;
|
const terminal = this.terminalCommandRunsValue;
|
||||||
if (terminal === undefined) {
|
if (terminal === undefined) {
|
||||||
@@ -151,6 +187,7 @@ class PiWebActionsPanel extends HTMLElement {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const handle = await runWorkspaceActionInTerminal(terminal, workspace, action);
|
const handle = await runWorkspaceActionInTerminal(terminal, workspace, action);
|
||||||
|
if (!this.isCurrentWorkspace(workspace)) return;
|
||||||
this.status = {
|
this.status = {
|
||||||
kind: "success",
|
kind: "success",
|
||||||
message: `Started terminal command “${handle.run.title}”.`,
|
message: `Started terminal command “${handle.run.title}”.`,
|
||||||
@@ -159,6 +196,7 @@ class PiWebActionsPanel extends HTMLElement {
|
|||||||
this.runningActionId = undefined;
|
this.runningActionId = undefined;
|
||||||
this.render();
|
this.render();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (!this.isCurrentWorkspace(workspace)) return;
|
||||||
this.runningActionId = undefined;
|
this.runningActionId = undefined;
|
||||||
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
this.status = { kind: "error", message: error instanceof Error ? error.message : String(error) };
|
||||||
this.render();
|
this.render();
|
||||||
@@ -260,8 +298,8 @@ function renderAction(action: WorkspaceAction, runningActionId: string | undefin
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionFromConfigState(state: ConfigState, actionId: string | null): WorkspaceAction | undefined {
|
function actionFromConfigState(state: ConfigState | undefined, actionId: string | null): WorkspaceAction | undefined {
|
||||||
if (state.kind !== "loaded" || actionId === null) return undefined;
|
if (state?.kind !== "loaded" || actionId === null) return undefined;
|
||||||
return state.config.actions.find((action) => action.id === actionId);
|
return state.config.actions.find((action) => action.id === actionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +325,7 @@ function actionStyles(): string {
|
|||||||
button:disabled { cursor: wait; opacity: 0.65; }
|
button:disabled { cursor: wait; opacity: 0.65; }
|
||||||
.empty-state { border: 1px dashed var(--pi-border-muted); border-radius: 8px; color: var(--pi-muted); padding: 12px; }
|
.empty-state { border: 1px dashed var(--pi-border-muted); border-radius: 8px; color: var(--pi-muted); padding: 12px; }
|
||||||
.empty-state p { margin: 6px 0 0; }
|
.empty-state p { margin: 6px 0 0; }
|
||||||
|
.panel-status { margin: 12px 12px 0; }
|
||||||
.status { border: 1px solid var(--pi-border); border-radius: 8px; padding: 10px; }
|
.status { border: 1px solid var(--pi-border); border-radius: 8px; padding: 10px; }
|
||||||
.status.info { border-color: var(--pi-accent-border); background: var(--pi-bg-overlay-soft); }
|
.status.info { border-color: var(--pi-accent-border); background: var(--pi-bg-overlay-soft); }
|
||||||
.status.success { border-color: var(--pi-success-border); background: var(--pi-success-surface); color: var(--pi-success); }
|
.status.success { border-color: var(--pi-success-border); background: var(--pi-success-surface); color: var(--pi-success); }
|
||||||
|
|||||||
Reference in New Issue
Block a user