Files
authentik/web/src/elements/buttons/SpinnerButton/BaseTaskButton.ts
Ken Sternberg 35f96df66e web: Testing and documenting the simple things
This commit adds unit tests for the Alerts and EmptyState elements. It includes a new
test/documentation feature; elements can now be fully documented with text description and active
controls.

It *removes* the `babel` imports entirely.  Either we don't need them, or the components that do
need them are importing them automatically.

[An outstanding bug in WebDriverIO](https://github.com/webdriverio/webdriverio/issues/12056)
unfortunately means that the tests cannot be run in parallel for the time being.While one test is
running, the compiler for other tests becomes unreliable. They're currently working on this issue.
I have set the `maxInstances` to **1**.

I have updated the `<ak-alert>` component just a bit, providing an attribute alternative to the
`Level` property; now instead of passing it a `<ak-alert level=${Levels.Warning}>` properties, you
can just say `<ak-alert warning>` and it'll work just fine. The old way is still the default
behavior.

The default behavior for `EmptyState` was a little confusing; I've re-arranged it for clarity. Since
I touched it, I also added the `interface` and `HTMLElementTagNameMap` declarations.

Added documentation to all the elements I've touched (so far).
2024-05-16 15:26:35 -07:00

140 lines
4.0 KiB
TypeScript

import { ERROR_CLASS, PROGRESS_CLASS, SUCCESS_CLASS } from "@goauthentik/common/constants";
import { PFSize } from "@goauthentik/common/enums.js";
import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/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
export interface IBaseTaskButton {
disabled?: boolean;
}
/**
* 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 get styles() {
return 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 = new Task(this, {
task: () => this.callAction(),
args: () => [],
autoRun: false,
onComplete: (r: unknown) => this.onSuccess(r),
onError: (r: unknown) => this.onError(r),
});
}
onComplete() {
setTimeout(() => {
this.actionTask.status = TaskStatus.INITIAL;
this.dispatchCustomEvent(`${this.eventPrefix}-reset`);
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() {
if (this.actionTask.status !== TaskStatus.INITIAL) {
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;