Files
authentik/web/src/elements/buttons/SpinnerButton/BaseTaskButton.ts
T
Teffen Ellis 4335498ac5 web: Import organization (#14696)
* web: Clean up locale.

* web: Clean ambiguous imports.

* web: Clean up entrypoint imports.

* web: Format imports.

* web: Normalize extensions.

* web: Tidy order.

* web: Remove TS aliases.
2025-07-10 20:36:56 +00:00

142 lines
4.1 KiB
TypeScript

import { ERROR_CLASS, PROGRESS_CLASS, SUCCESS_CLASS } from "#common/constants";
import { PFSize } from "#common/enums";
import { AKElement } from "#elements/Base";
import { CustomEmitterElement } from "#elements/utils/eventEmitter";
import { Task, TaskStatus } from "@lit/task";
import { css, html } from "lit";
import { property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
// `pointer-events: none` makes the button inaccessible during the processing phase.
const buttonStyles = [
PFBase,
PFButton,
PFSpinner,
css`
#spinner-button {
transition: all var(--pf-c-button--m-progress--TransitionDuration) ease 0s;
}
#spinner-button.working {
pointer-events: none;
}
`,
];
const StatusMap = new Map<TaskStatus, string>([
[TaskStatus.INITIAL, ""],
[TaskStatus.PENDING, PROGRESS_CLASS],
[TaskStatus.COMPLETE, SUCCESS_CLASS],
[TaskStatus.ERROR, ERROR_CLASS],
]);
const SPINNER_TIMEOUT = 1000 * 1.5; // milliseconds
/**
* BaseTaskButton
*
* This is an abstract base class for our four-state buttons. It provides the four basic events of
* this class: click, success, failure, reset. Subclasses can override any of these event handlers,
* but overriding onSuccess() or onFailure() means that you must either call `onComplete` if you
* want to preserve the TaskButton's "reset after completion" semantics, or inside `onSuccess` and
* `onFailure` call their `super.` equivalents.
*
*/
export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
eventPrefix = "ak-button";
static styles = [...buttonStyles];
callAction!: () => Promise<unknown>;
actionTask: Task;
@property({ type: Boolean })
disabled = false;
constructor() {
super();
this.onSuccess = this.onSuccess.bind(this);
this.onError = this.onError.bind(this);
this.onClick = this.onClick.bind(this);
this.actionTask = this.buildTask();
}
buildTask() {
return new Task(this, {
task: () => this.callAction(),
args: () => [],
autoRun: false,
onComplete: (r: unknown) => this.onSuccess(r),
onError: (r: unknown) => this.onError(r),
});
}
onComplete() {
setTimeout(() => {
this.dispatchCustomEvent(`${this.eventPrefix}-reset`);
// set-up for the next task...
this.actionTask = this.buildTask();
this.requestUpdate();
}, SPINNER_TIMEOUT);
}
onSuccess(r: unknown) {
this.dispatchCustomEvent(`${this.eventPrefix}-success`, {
result: r,
});
this.onComplete();
}
onError(error: unknown) {
this.dispatchCustomEvent(`${this.eventPrefix}-failure`, {
error,
});
this.onComplete();
}
onClick() {
// Don't accept clicks when a task is in progress..
if (this.actionTask.status === TaskStatus.PENDING) {
return;
}
this.dispatchCustomEvent(`${this.eventPrefix}-click`);
this.actionTask.run();
}
private spinner = html`<span class="pf-c-button__progress">
<ak-spinner size=${PFSize.Medium}></ak-spinner>
</span>`;
get buttonClasses() {
return [
...this.classList,
StatusMap.get(this.actionTask.status),
this.actionTask.status === TaskStatus.INITIAL ? "" : "working",
]
.join(" ")
.trim();
}
render() {
return html`<button
id="spinner-button"
part="spinner-button"
class="pf-c-button pf-m-progress ${this.buttonClasses}"
@click=${this.onClick}
?disabled=${this.disabled}
>
${this.actionTask.render({ pending: () => this.spinner })}
<slot></slot>
</button>`;
}
}
export default BaseTaskButton;