Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0eb4611ea6 | |||
| 5d7f39695a | |||
| 3f5cb4570b | |||
| 428d2741d1 | |||
| 2f4c47f0d2 | |||
| 2be1ce6908 |
22
changelog.md
22
changelog.md
@@ -1,5 +1,27 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-17 - 3.81.0 - feat(dees-updater)
|
||||||
|
enhance updater progress and completion views with version metadata cards
|
||||||
|
|
||||||
|
- add reusable version metadata rendering for current, incoming, and installed versions
|
||||||
|
- refresh progress and ready states with clearer headings, descriptive copy, and automatic action summary
|
||||||
|
- apply monospace styling to displayed version numbers for improved readability
|
||||||
|
|
||||||
|
## 2026-04-16 - 3.80.0 - feat(stepper,updater)
|
||||||
|
add progress-aware stepper flows and updater countdown states
|
||||||
|
|
||||||
|
- extend dees-stepper with embedded progressbar rendering, progress state helpers, and automatic progression for async validation steps
|
||||||
|
- rework dees-updater to run as a non-cancelable two-step flow with live progress updates, optional external links, and configurable close or reload completion actions
|
||||||
|
- refresh stepper and updater demos plus documentation to showcase auto-advancing progress steps and ready-state countdown behavior
|
||||||
|
|
||||||
|
## 2026-04-16 - 3.79.0 - feat(dees-progressbar)
|
||||||
|
add status panels, terminal output, and legacy progress input support
|
||||||
|
|
||||||
|
- Extend dees-progressbar with label, statusText, terminalLines, statusRows, indeterminate, and showPercentage properties.
|
||||||
|
- Support legacy value input and normalized progress values while clamping and formatting percentages consistently.
|
||||||
|
- Add fixed-height status and terminal-style output with spinner animation and auto-scroll behavior for live activity updates.
|
||||||
|
- Refresh the progressbar demo and readme examples to showcase determinate, indeterminate, terminal, and compatibility usage patterns.
|
||||||
|
|
||||||
## 2026-04-14 - 3.78.3 - fix(dees-table)
|
## 2026-04-14 - 3.78.3 - fix(dees-table)
|
||||||
stabilize live updates by reusing row DOM and avoiding redundant layout recalculations
|
stabilize live updates by reusing row DOM and avoiding redundant layout recalculations
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "3.78.3",
|
"version": "3.81.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
|
|||||||
76
readme.md
76
readme.md
@@ -1508,31 +1508,56 @@ const layer = await DeesWindowLayer.createAndShow({
|
|||||||
### Navigation Components
|
### Navigation Components
|
||||||
|
|
||||||
#### `DeesStepper`
|
#### `DeesStepper`
|
||||||
Multi-step navigation component for guided user flows.
|
Multi-step navigation component for guided user flows, including optional auto-advancing progress steps that can render `dees-progressbar` status output between form steps.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
<dees-stepper
|
<dees-stepper
|
||||||
.steps=${[
|
.steps=${[
|
||||||
{ key: 'personal', label: 'Personal Info', content: html`<div>Form 1</div>` },
|
{
|
||||||
{ key: 'address', label: 'Address', content: html`<div>Form 2</div>` },
|
title: 'Account Setup',
|
||||||
{ key: 'confirm', label: 'Confirmation', content: html`<div>Review</div>` }
|
content: html`<dees-form>...</dees-form>`,
|
||||||
|
menuOptions: [{ name: 'Continue', action: async (stepper) => stepper?.goNext() }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Provision Workspace',
|
||||||
|
content: html`<p>Preparing your environment...</p>`,
|
||||||
|
progressStep: {
|
||||||
|
label: 'Workspace setup',
|
||||||
|
indeterminate: true,
|
||||||
|
statusRows: 4,
|
||||||
|
terminalLines: ['Allocating workspace']
|
||||||
|
},
|
||||||
|
validationFunc: async (stepper, _element, signal) => {
|
||||||
|
stepper.updateProgressStep({ percentage: 35, statusText: 'Installing dependencies...' });
|
||||||
|
stepper.appendProgressStepLine('Installing dependencies');
|
||||||
|
if (signal?.aborted) return;
|
||||||
|
stepper.updateProgressStep({ percentage: 100, indeterminate: false, statusText: 'Workspace ready.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
]}
|
]}
|
||||||
currentStep="personal"
|
|
||||||
@step-change=${handleStepChange}
|
|
||||||
@complete=${handleComplete}
|
|
||||||
></dees-stepper>
|
></dees-stepper>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `DeesProgressbar`
|
#### `DeesProgressbar`
|
||||||
Progress indicator component for tracking completion status.
|
Progress indicator component for tracking completion status, with optional fixed-height status text or terminal-style recent activity output.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
<dees-progressbar
|
<dees-progressbar
|
||||||
value={75}
|
.percentage=${75}
|
||||||
label="Uploading"
|
label="Uploading"
|
||||||
showPercentage
|
statusText="Uploading thumbnails to edge cache..."
|
||||||
type="determinate" // Options: determinate, indeterminate
|
.statusRows=${2}
|
||||||
status="normal" // Options: normal, success, warning, error
|
></dees-progressbar>
|
||||||
|
|
||||||
|
<dees-progressbar
|
||||||
|
label="Installing dependencies"
|
||||||
|
.indeterminate=${true}
|
||||||
|
.statusRows=${4}
|
||||||
|
.terminalLines=${[
|
||||||
|
'Resolving workspace packages',
|
||||||
|
'Downloading tarballs',
|
||||||
|
'Linking local binaries'
|
||||||
|
]}
|
||||||
></dees-progressbar>
|
></dees-progressbar>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1570,6 +1595,33 @@ Theme provider component that wraps children and provides CSS custom properties
|
|||||||
- Works with dark/light mode
|
- Works with dark/light mode
|
||||||
- Overrides cascade to all child components
|
- Overrides cascade to all child components
|
||||||
|
|
||||||
|
#### `DeesUpdater`
|
||||||
|
Updater controller that opens a non-cancelable `dees-stepper` flow with a progress step and a ready step.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const updater = await DeesUpdater.createAndShow({
|
||||||
|
currentVersion: '3.79.0',
|
||||||
|
updatedVersion: '3.80.0',
|
||||||
|
moreInfoUrl: 'https://code.foss.global/design.estate/dees-catalog',
|
||||||
|
changelogUrl: 'https://code.foss.global/design.estate/dees-catalog/-/blob/main/changelog.md',
|
||||||
|
successAction: 'reload',
|
||||||
|
successDelayMs: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
updater.updateProgress({
|
||||||
|
percentage: 35,
|
||||||
|
statusText: 'Downloading signed bundle...',
|
||||||
|
terminalLines: ['Checking release manifest', 'Downloading signed bundle']
|
||||||
|
});
|
||||||
|
|
||||||
|
updater.appendProgressLine('Verifying checksum');
|
||||||
|
updater.updateProgress({ percentage: 72, statusText: 'Verifying checksum...' });
|
||||||
|
|
||||||
|
await updater.markUpdateReady();
|
||||||
|
```
|
||||||
|
|
||||||
|
After `markUpdateReady()`, the updater switches to a second countdown step with a determinate progress bar and runs the configured success action when the timer reaches zero.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Workspace / IDE Components 💻
|
### Workspace / IDE Components 💻
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.78.3',
|
version: '3.81.0',
|
||||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,245 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
import '@design.estate/dees-wcctools/demotools';
|
||||||
import { DeesProgressbar } from '../dees-progressbar/dees-progressbar.js';
|
import type { DeesProgressbar } from './dees-progressbar.js';
|
||||||
|
|
||||||
export const demoFunc = () => {
|
export const demoFunc = () => {
|
||||||
return html`
|
const terminalSnapshots = [
|
||||||
<dees-progressbar
|
['Resolving workspace packages'],
|
||||||
.percentage=${50}
|
['Resolving workspace packages', 'Downloading ui-assets.tar.gz'],
|
||||||
></dees-progressbar>
|
['Resolving workspace packages', 'Downloading ui-assets.tar.gz', 'Verifying checksum'],
|
||||||
`;
|
['Resolving workspace packages', 'Downloading ui-assets.tar.gz', 'Verifying checksum', 'Extracting release bundle'],
|
||||||
|
['Resolving workspace packages', 'Downloading ui-assets.tar.gz', 'Verifying checksum', 'Extracting release bundle', 'Restarting application'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const getUploadStatus = (percentage: number): string => {
|
||||||
|
if (percentage >= 100) {
|
||||||
|
return 'Upload complete. Finalizing package manifest...';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (percentage >= 82) {
|
||||||
|
return 'Verifying checksums before handoff...';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (percentage >= 55) {
|
||||||
|
return 'Uploading thumbnails to edge cache...';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (percentage >= 25) {
|
||||||
|
return 'Streaming source files to the remote worker...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Preparing archive and dependency graph...';
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||||
|
const liveProgressbar = elementArg.querySelector('#live-progress') as DeesProgressbar | null;
|
||||||
|
const terminalProgressbar = elementArg.querySelector('#terminal-progress') as DeesProgressbar | null;
|
||||||
|
const demoElement = elementArg as HTMLElement & {
|
||||||
|
__progressbarDemoIntervalId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!liveProgressbar || !terminalProgressbar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (demoElement.__progressbarDemoIntervalId) {
|
||||||
|
window.clearInterval(demoElement.__progressbarDemoIntervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let livePercentage = 12;
|
||||||
|
let terminalSnapshotIndex = 0;
|
||||||
|
|
||||||
|
const updateDemo = () => {
|
||||||
|
liveProgressbar.percentage = livePercentage;
|
||||||
|
liveProgressbar.statusText = getUploadStatus(livePercentage);
|
||||||
|
|
||||||
|
terminalProgressbar.terminalLines = [...terminalSnapshots[terminalSnapshotIndex]];
|
||||||
|
terminalProgressbar.percentage = Math.min(100, (terminalSnapshotIndex + 1) * 20);
|
||||||
|
terminalProgressbar.indeterminate = terminalSnapshotIndex < terminalSnapshots.length - 1;
|
||||||
|
|
||||||
|
livePercentage = livePercentage >= 100 ? 12 : Math.min(100, livePercentage + 11);
|
||||||
|
terminalSnapshotIndex = terminalSnapshotIndex >= terminalSnapshots.length - 1 ? 0 : terminalSnapshotIndex + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDemo();
|
||||||
|
demoElement.__progressbarDemoIntervalId = window.setInterval(updateDemo, 1400);
|
||||||
|
}}>
|
||||||
|
<style>
|
||||||
|
${css`
|
||||||
|
.demoBox {
|
||||||
|
position: relative;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoIntro {
|
||||||
|
max-width: 720px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 20% 30%)', 'hsl(215 18% 76%)')};
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseCard {
|
||||||
|
background: ${cssManager.bdTheme('rgba(255,255,255,0.78)', 'rgba(255,255,255,0.04)')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('hsl(210 22% 86%)', 'hsl(210 10% 18%)')};
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseCard.wide {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseCard h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseCard p {
|
||||||
|
margin: 0;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 14% 40%)', 'hsl(215 10% 66%)')};
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressStack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeLabel {
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 14% 44%)', 'hsl(215 10% 70%)')};
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.demoBox {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcaseCard.wide {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<div class="demoBox">
|
||||||
|
<div class="demoIntro">
|
||||||
|
<code>dees-progressbar</code> can now pair a classic progress bar with a fixed-height status area. Use simple status text for clear user-facing updates or switch to terminal-like lines when you want recent steps to stay visible without causing layout jumps.
|
||||||
|
</div>
|
||||||
|
<div class="showcaseGrid">
|
||||||
|
<section class="showcaseCard">
|
||||||
|
<div class="codeLabel">Determinate</div>
|
||||||
|
<h3>Percentage plus current task</h3>
|
||||||
|
<p>Use a label, a percentage, and one short status line when the work is measurable.</p>
|
||||||
|
<div class="progressStack">
|
||||||
|
<dees-progressbar
|
||||||
|
label="Media upload"
|
||||||
|
.percentage=${68}
|
||||||
|
statusText="Uploading thumbnails to edge cache..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
<dees-progressbar
|
||||||
|
label="Asset sync"
|
||||||
|
.percentage=${100}
|
||||||
|
statusText="All files are synced and available."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="showcaseCard">
|
||||||
|
<div class="codeLabel">Indeterminate</div>
|
||||||
|
<h3>Spinner-style text indicator</h3>
|
||||||
|
<p>When there is no trustworthy percentage yet, keep the bar moving and let the text explain what is happening.</p>
|
||||||
|
<div class="progressStack">
|
||||||
|
<dees-progressbar
|
||||||
|
label="Dependency install"
|
||||||
|
.indeterminate=${true}
|
||||||
|
statusText="Downloading package metadata..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
<dees-progressbar
|
||||||
|
label="Queued job"
|
||||||
|
.percentage=${32}
|
||||||
|
.showPercentage=${false}
|
||||||
|
statusText="Waiting for a worker slot to become available..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="showcaseCard wide">
|
||||||
|
<div class="codeLabel">Terminal Lines</div>
|
||||||
|
<h3>Fixed-height terminal-style status output</h3>
|
||||||
|
<p>The panel stays the same height while the latest step stays visible. This is useful for update flows, downloads, and staged background work.</p>
|
||||||
|
<dees-progressbar
|
||||||
|
id="terminal-progress"
|
||||||
|
label="Release bundle"
|
||||||
|
.percentage=${20}
|
||||||
|
.indeterminate=${true}
|
||||||
|
.statusRows=${4}
|
||||||
|
.terminalLines=${terminalSnapshots[0]}
|
||||||
|
></dees-progressbar>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="showcaseCard">
|
||||||
|
<div class="codeLabel">Live Demo</div>
|
||||||
|
<h3>Updating percentage and text together</h3>
|
||||||
|
<p>A single component can express both how far the job is and which phase is currently active.</p>
|
||||||
|
<dees-progressbar
|
||||||
|
id="live-progress"
|
||||||
|
label="Customer export"
|
||||||
|
.percentage=${12}
|
||||||
|
statusText="Preparing archive and dependency graph..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="showcaseCard">
|
||||||
|
<div class="codeLabel">Compatibility</div>
|
||||||
|
<h3>Legacy <code>value</code> and <code>progress</code> inputs</h3>
|
||||||
|
<p>Existing usages can keep passing percentages directly or normalized progress values from 0 to 1.</p>
|
||||||
|
<div class="progressStack">
|
||||||
|
<dees-progressbar
|
||||||
|
label="From value"
|
||||||
|
.value=${75}
|
||||||
|
statusText="Migrating existing readme-style usage..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
<dees-progressbar
|
||||||
|
label="From progress"
|
||||||
|
.progress=${0.42}
|
||||||
|
.showPercentage=${false}
|
||||||
|
statusText="Rendering normalized progress input..."
|
||||||
|
.statusRows=${2}
|
||||||
|
></dees-progressbar>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dees-demowrapper>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as plugins from '../../00plugins.js';
|
|
||||||
import * as colors from '../../00colors.js';
|
import * as colors from '../../00colors.js';
|
||||||
import { demoFunc } from './dees-progressbar.demo.js';
|
import { demoFunc } from './dees-progressbar.demo.js';
|
||||||
import {
|
import {
|
||||||
@@ -6,94 +5,342 @@ import {
|
|||||||
html,
|
html,
|
||||||
DeesElement,
|
DeesElement,
|
||||||
property,
|
property,
|
||||||
type TemplateResult,
|
|
||||||
cssManager,
|
cssManager,
|
||||||
css,
|
css,
|
||||||
type CSSResult,
|
|
||||||
unsafeCSS,
|
|
||||||
unsafeHTML,
|
|
||||||
state,
|
state,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
|
||||||
import { themeDefaultStyles } from '../../00theme.js';
|
import { themeDefaultStyles } from '../../00theme.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-progressbar': DeesProgressbar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('dees-progressbar')
|
@customElement('dees-progressbar')
|
||||||
export class DeesProgressbar extends DeesElement {
|
export class DeesProgressbar extends DeesElement {
|
||||||
// STATIC
|
|
||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
public static demoGroups = ['Feedback'];
|
public static demoGroups = ['Feedback'];
|
||||||
|
|
||||||
// INSTANCE
|
|
||||||
@property({
|
@property({
|
||||||
type: Number,
|
type: Number,
|
||||||
})
|
})
|
||||||
accessor percentage = 0;
|
accessor percentage = 0;
|
||||||
|
|
||||||
|
// `value` and `progress` keep existing readme/internal usages working.
|
||||||
|
@property({
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
accessor value: number | null = null;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
accessor progress: number | null = null;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
accessor label = '';
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
accessor statusText = '';
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Array,
|
||||||
|
})
|
||||||
|
accessor terminalLines: string[] = [];
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
accessor statusRows = 3;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
})
|
||||||
|
accessor indeterminate = false;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
})
|
||||||
|
accessor showPercentage = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor activeSpinnerFrame = 0;
|
||||||
|
|
||||||
|
private spinnerIntervalId: number | null = null;
|
||||||
|
private readonly spinnerFrames = ['|', '/', '-', '\\'];
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
themeDefaultStyles,
|
themeDefaultStyles,
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
|
||||||
:host {
|
:host {
|
||||||
|
display: block;
|
||||||
color: ${cssManager.bdTheme(colors.bright.text, colors.dark.text)};
|
color: ${cssManager.bdTheme(colors.bright.text, colors.dark.text)};
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressBarContainer {
|
.progressBarContainer {
|
||||||
padding: 8px;
|
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressLabel {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressValue {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 15% 40%)', 'hsl(215 15% 70%)')};
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressBar {
|
.progressBar {
|
||||||
background: ${cssManager.bdTheme('#eeeeeb', '#444')};
|
position: relative;
|
||||||
height: 8px;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 4px;
|
height: 8px;
|
||||||
border-top: 0.5px solid ${cssManager.bdTheme('none', '#555')};
|
border-radius: 999px;
|
||||||
|
background: ${cssManager.bdTheme('#eeeeeb', '#444')};
|
||||||
|
border-top: 0.5px solid ${cssManager.bdTheme('transparent', '#555')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressBarFill {
|
.progressBarFill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
background: ${cssManager.bdTheme(colors.dark.blueActive, colors.bright.blueActive)};
|
background: ${cssManager.bdTheme(colors.dark.blueActive, colors.bright.blueActive)};
|
||||||
height: 8px;
|
transition: width 0.2s ease;
|
||||||
margin-top: -0.5px;
|
|
||||||
transition: 0.2s width;
|
|
||||||
border-radius: 4px;
|
|
||||||
width: 0px;
|
|
||||||
border-top: 0.5 solid ${cssManager.bdTheme('none', '#398fff')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progressText {
|
.progressBarFill.indeterminate {
|
||||||
padding: 8px;
|
width: 34%;
|
||||||
|
transition: none;
|
||||||
|
animation: indeterminateSlide 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusPanel {
|
||||||
|
margin-top: 10px;
|
||||||
|
height: calc(var(--status-rows, 3) * 1.35em + 16px);
|
||||||
|
min-height: calc(var(--status-rows, 3) * 1.35em + 16px);
|
||||||
|
padding: 8px 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('hsl(210 20% 86%)', 'hsl(210 10% 26%)')};
|
||||||
|
background: ${cssManager.bdTheme('hsl(210 33% 98%)', 'hsl(220 20% 10%)')};
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusTextRow,
|
||||||
|
.terminalLine {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 1.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalScroller {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalScroller::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalScroller::-webkit-scrollbar-thumb {
|
||||||
|
background: ${cssManager.bdTheme('hsl(215 18% 78%)', 'hsl(215 10% 34%)')};
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalScroller::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linePrefix {
|
||||||
|
width: 1ch;
|
||||||
|
flex: 0 0 1ch;
|
||||||
|
color: ${cssManager.bdTheme(colors.dark.blueActive, colors.bright.blueActive)};
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
`
|
|
||||||
|
.lineText {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: ${cssManager.bdTheme('hsl(220 15% 25%)', 'hsl(210 15% 86%)')};
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminalLine:not(.current) .lineText {
|
||||||
|
color: ${cssManager.bdTheme('hsl(215 12% 42%)', 'hsl(215 12% 63%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes indeterminateSlide {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-120%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(320%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public async connectedCallback(): Promise<void> {
|
||||||
|
await super.connectedCallback();
|
||||||
|
this.syncSpinnerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnectedCallback(): Promise<void> {
|
||||||
|
this.stopSpinner();
|
||||||
|
await super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public updated(changedProperties: Map<string | number | symbol, unknown>): void {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
this.syncSpinnerState();
|
||||||
|
|
||||||
|
if (changedProperties.has('terminalLines') && this.terminalLines.length > 0) {
|
||||||
|
this.scrollTerminalToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const effectivePercentage = this.getEffectivePercentage();
|
||||||
|
const showHeader = Boolean(this.label) || (this.showPercentage && !this.indeterminate);
|
||||||
|
const hasTerminalLines = this.terminalLines.length > 0;
|
||||||
|
const hasStatusContent = hasTerminalLines || this.statusText.trim().length > 0;
|
||||||
|
const renderedRows = this.getRenderedStatusRows();
|
||||||
|
const spinnerFrame = this.spinnerFrames[this.activeSpinnerFrame] ?? this.spinnerFrames[0];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="progressBarContainer">
|
<div class="progressBarContainer">
|
||||||
|
${showHeader ? html`
|
||||||
|
<div class="progressHeader">
|
||||||
|
<div class="progressLabel">${this.label}</div>
|
||||||
|
${this.showPercentage && !this.indeterminate ? html`
|
||||||
|
<div class="progressValue">${this.formatPercentage(effectivePercentage)}%</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
<div class="progressBar">
|
<div class="progressBar">
|
||||||
<div class="progressBarFill"></div>
|
<div
|
||||||
<div class="progressText">
|
class="progressBarFill ${this.indeterminate ? 'indeterminate' : ''}"
|
||||||
${this.percentage}%
|
style="${this.indeterminate ? '' : `width: ${effectivePercentage}%;`}"
|
||||||
<div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
${hasStatusContent ? html`
|
||||||
|
<div
|
||||||
|
class="statusPanel"
|
||||||
|
style="--status-rows: ${renderedRows};"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="true"
|
||||||
|
>
|
||||||
|
${hasTerminalLines ? html`
|
||||||
|
<div class="terminalScroller">
|
||||||
|
${this.terminalLines.map((line, index) => {
|
||||||
|
const isCurrentLine = index === this.terminalLines.length - 1;
|
||||||
|
const prefix = this.indeterminate && isCurrentLine ? spinnerFrame : '>';
|
||||||
|
return html`
|
||||||
|
<div class="terminalLine ${isCurrentLine ? 'current' : ''}">
|
||||||
|
<span class="linePrefix">${prefix}</span>
|
||||||
|
<span class="lineText">${line}</span>
|
||||||
</div>
|
</div>
|
||||||
`
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
` : html`
|
||||||
|
<div class="statusTextRow">
|
||||||
|
<span class="linePrefix">${this.indeterminate ? spinnerFrame : '>'}</span>
|
||||||
|
<span class="lineText">${this.statusText}</span>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated (_changedProperties: Map<string | number | symbol, unknown>): void {
|
private getEffectivePercentage(): number {
|
||||||
super.firstUpdated(_changedProperties);
|
if (typeof this.value === 'number' && Number.isFinite(this.value)) {
|
||||||
this.updateComplete.then(() => {
|
return this.clampPercentage(this.value);
|
||||||
this.updatePercentage();
|
}
|
||||||
|
|
||||||
|
if (typeof this.progress === 'number' && Number.isFinite(this.progress)) {
|
||||||
|
const normalizedProgress = this.progress >= 0 && this.progress <= 1
|
||||||
|
? this.progress * 100
|
||||||
|
: this.progress;
|
||||||
|
return this.clampPercentage(normalizedProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.clampPercentage(this.percentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRenderedStatusRows(): number {
|
||||||
|
const rows = Number.isFinite(this.statusRows) ? Math.floor(this.statusRows) : 3;
|
||||||
|
return Math.max(1, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clampPercentage(input: number): number {
|
||||||
|
return Math.max(0, Math.min(100, input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatPercentage(input: number): string {
|
||||||
|
return Number.isInteger(input) ? `${input}` : input.toFixed(1).replace(/\.0$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncSpinnerState(): void {
|
||||||
|
const shouldAnimate = this.indeterminate && (this.statusText.trim().length > 0 || this.terminalLines.length > 0);
|
||||||
|
|
||||||
|
if (shouldAnimate && this.spinnerIntervalId === null) {
|
||||||
|
this.spinnerIntervalId = window.setInterval(() => {
|
||||||
|
this.activeSpinnerFrame = (this.activeSpinnerFrame + 1) % this.spinnerFrames.length;
|
||||||
|
}, 120);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldAnimate) {
|
||||||
|
this.stopSpinner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopSpinner(): void {
|
||||||
|
if (this.spinnerIntervalId !== null) {
|
||||||
|
window.clearInterval(this.spinnerIntervalId);
|
||||||
|
this.spinnerIntervalId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeSpinnerFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollTerminalToBottom(): void {
|
||||||
|
const terminalScroller = this.shadowRoot?.querySelector('.terminalScroller') as HTMLElement | null;
|
||||||
|
|
||||||
|
if (!terminalScroller) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
terminalScroller.scrollTop = terminalScroller.scrollHeight;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updatePercentage() {
|
|
||||||
const progressBarFill = this.shadowRoot!.querySelector('.progressBarFill') as HTMLElement;
|
|
||||||
progressBarFill.style.width = `${this.percentage}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
updated(){
|
|
||||||
this.updatePercentage();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,45 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html } from '@design.estate/dees-element';
|
||||||
import { DeesStepper, type IStep } from './dees-stepper.js';
|
import { DeesStepper, type IStep } from './dees-stepper.js';
|
||||||
|
|
||||||
const demoSteps: IStep[] = [
|
const waitForProgressTick = async (timeoutArg: number, signal?: AbortSignal): Promise<boolean> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let completed = false;
|
||||||
|
|
||||||
|
const finish = (result: boolean) => {
|
||||||
|
if (completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
completed = true;
|
||||||
|
if (signal) {
|
||||||
|
signal.removeEventListener('abort', handleAbort);
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAbort = () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
finish(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
finish(true);
|
||||||
|
}, timeoutArg);
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
signal.addEventListener('abort', handleAbort, { once: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createContinueMenuOptions = (labelArg = 'Continue') => [
|
||||||
|
{
|
||||||
|
name: labelArg,
|
||||||
|
action: async (stepper?: DeesStepper) => stepper?.goNext(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const createDemoSteps = (): IStep[] => [
|
||||||
{
|
{
|
||||||
title: 'Account Setup',
|
title: 'Account Setup',
|
||||||
content: html`
|
content: html`
|
||||||
@@ -10,9 +48,7 @@ const demoSteps: IStep[] = [
|
|||||||
<dees-input-text key="password" label="Create Password" type="password" required></dees-input-text>
|
<dees-input-text key="password" label="Create Password" type="password" required></dees-input-text>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: createContinueMenuOptions(),
|
||||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Profile Details',
|
title: 'Profile Details',
|
||||||
@@ -22,9 +58,7 @@ const demoSteps: IStep[] = [
|
|||||||
<dees-input-text key="lastName" label="Last Name" required></dees-input-text>
|
<dees-input-text key="lastName" label="Last Name" required></dees-input-text>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: createContinueMenuOptions(),
|
||||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Contact Information',
|
title: 'Contact Information',
|
||||||
@@ -34,9 +68,74 @@ const demoSteps: IStep[] = [
|
|||||||
<dees-input-text key="company" label="Company"></dees-input-text>
|
<dees-input-text key="company" label="Company"></dees-input-text>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: createContinueMenuOptions(),
|
||||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
},
|
||||||
],
|
{
|
||||||
|
title: 'Provision Workspace',
|
||||||
|
content: html`
|
||||||
|
<dees-panel>
|
||||||
|
<p>
|
||||||
|
We are creating your starter workspace, applying your onboarding choices,
|
||||||
|
and preparing a live preview. This step moves forward automatically when
|
||||||
|
the environment is ready.
|
||||||
|
</p>
|
||||||
|
</dees-panel>
|
||||||
|
`,
|
||||||
|
progressStep: {
|
||||||
|
label: 'Workspace setup',
|
||||||
|
percentage: 8,
|
||||||
|
indeterminate: true,
|
||||||
|
statusRows: 4,
|
||||||
|
statusText: 'Allocating a clean workspace...',
|
||||||
|
terminalLines: ['Allocating a clean workspace'],
|
||||||
|
},
|
||||||
|
validationFunc: async (stepper, _htmlElement, signal) => {
|
||||||
|
const progressFrames = [
|
||||||
|
{ line: 'Allocating a clean workspace', percentage: 8, delay: 500 },
|
||||||
|
{ line: 'Syncing account preferences', percentage: 24, delay: 650 },
|
||||||
|
{ line: 'Installing selected integrations', percentage: 47, delay: 700 },
|
||||||
|
{ line: 'Generating starter project files', percentage: 71, delay: 650 },
|
||||||
|
{ line: 'Booting the live preview environment', percentage: 92, delay: 700 },
|
||||||
|
];
|
||||||
|
|
||||||
|
stepper.resetProgressStep();
|
||||||
|
|
||||||
|
for (const [index, progressFrame] of progressFrames.entries()) {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
stepper.updateProgressStep({
|
||||||
|
percentage: progressFrame.percentage,
|
||||||
|
indeterminate: true,
|
||||||
|
statusText: `${progressFrame.line}...`,
|
||||||
|
terminalLines: [progressFrame.line],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
stepper.appendProgressStepLine(progressFrame.line);
|
||||||
|
stepper.updateProgressStep({
|
||||||
|
percentage: progressFrame.percentage,
|
||||||
|
indeterminate: true,
|
||||||
|
statusText: `${progressFrame.line}...`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const completed = await waitForProgressTick(progressFrame.delay, signal);
|
||||||
|
if (!completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stepper.appendProgressStepLine('Workspace ready');
|
||||||
|
stepper.updateProgressStep({
|
||||||
|
percentage: 100,
|
||||||
|
indeterminate: false,
|
||||||
|
statusText: 'Workspace ready.',
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForProgressTick(350, signal);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Team Size',
|
title: 'Team Size',
|
||||||
@@ -55,9 +154,7 @@ const demoSteps: IStep[] = [
|
|||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: createContinueMenuOptions(),
|
||||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Goals',
|
title: 'Goals',
|
||||||
@@ -75,52 +172,31 @@ const demoSteps: IStep[] = [
|
|||||||
></dees-input-multitoggle>
|
></dees-input-multitoggle>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: createContinueMenuOptions(),
|
||||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Brand Preferences',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text key="brandColor" label="Primary brand color"></dees-input-text>
|
|
||||||
<dees-input-text key="tone" label="Preferred tone (e.g. friendly, formal)"></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Integrations',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-list
|
|
||||||
key="integrations"
|
|
||||||
label="Integrations in use"
|
|
||||||
placeholder="Add integration"
|
|
||||||
></dees-input-list>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Review & Launch',
|
title: 'Review & Launch',
|
||||||
content: html`
|
content: html`
|
||||||
<dees-panel>
|
<dees-panel>
|
||||||
<p>Almost there! Review your selections and launch whenever you're ready.</p>
|
<p>
|
||||||
|
Your workspace is ready. Review the collected details and launch when
|
||||||
|
you are ready to start.
|
||||||
|
</p>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{ name: 'Launch', action: async (stepper) => stepper!.goNext() },
|
{
|
||||||
|
name: 'Launch',
|
||||||
|
action: async (stepper?: DeesStepper) => {
|
||||||
|
if (stepper?.overlay) {
|
||||||
|
await stepper.destroy();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const cloneSteps = (): IStep[] => demoSteps.map((step) => ({ ...step }));
|
|
||||||
|
|
||||||
export const stepperDemo = () => html`
|
export const stepperDemo = () => html`
|
||||||
<div style="position: absolute; inset: 0;">
|
<div style="position: absolute; inset: 0;">
|
||||||
<div
|
<div
|
||||||
@@ -128,10 +204,10 @@ export const stepperDemo = () => html`
|
|||||||
>
|
>
|
||||||
<dees-button
|
<dees-button
|
||||||
@click=${async () => {
|
@click=${async () => {
|
||||||
await DeesStepper.createAndShow({ steps: cloneSteps() });
|
await DeesStepper.createAndShow({ steps: createDemoSteps() });
|
||||||
}}
|
}}
|
||||||
>Open stepper as overlay</dees-button>
|
>Open stepper as overlay</dees-button>
|
||||||
</div>
|
</div>
|
||||||
<dees-stepper .steps=${cloneSteps()}></dees-stepper>
|
<dees-stepper .steps=${createDemoSteps()}></dees-stepper>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import {
|
|||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
css,
|
css,
|
||||||
unsafeCSS,
|
|
||||||
type CSSResult,
|
|
||||||
cssManager,
|
cssManager,
|
||||||
property,
|
property,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
@@ -20,16 +18,33 @@ import { zIndexRegistry } from '../../00zindex.js';
|
|||||||
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
||||||
import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
|
import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
|
||||||
import type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
|
import type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
|
||||||
|
import '../../00group-feedback/dees-progressbar/dees-progressbar.js';
|
||||||
import '../dees-tile/dees-tile.js';
|
import '../dees-tile/dees-tile.js';
|
||||||
|
|
||||||
|
export interface IStepProgressState {
|
||||||
|
label?: string;
|
||||||
|
percentage?: number;
|
||||||
|
indeterminate?: boolean;
|
||||||
|
showPercentage?: boolean;
|
||||||
|
statusText?: string;
|
||||||
|
terminalLines?: string[];
|
||||||
|
statusRows?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IStepProgress extends IStepProgressState {
|
||||||
|
autoAdvance?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IStep {
|
export interface IStep {
|
||||||
title: string;
|
title: string;
|
||||||
content: TemplateResult;
|
content: TemplateResult;
|
||||||
|
progressStep?: IStepProgress;
|
||||||
menuOptions?: plugins.tsclass.website.IMenuItem<DeesStepper>[];
|
menuOptions?: plugins.tsclass.website.IMenuItem<DeesStepper>[];
|
||||||
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
|
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
|
||||||
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
|
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
|
||||||
validationFuncCalled?: boolean;
|
validationFuncCalled?: boolean;
|
||||||
abortController?: AbortController;
|
abortController?: AbortController;
|
||||||
|
progressStepState?: IStepProgressState;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -276,6 +291,14 @@ export class DeesStepper extends DeesElement {
|
|||||||
padding: 32px;
|
padding: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.step-body .content.withProgressStep {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressStep {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Footer: modal-style bottom buttons --- */
|
/* --- Footer: modal-style bottom buttons --- */
|
||||||
.bottomButtons {
|
.bottomButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -375,6 +398,7 @@ export class DeesStepper extends DeesElement {
|
|||||||
const isHidden =
|
const isHidden =
|
||||||
this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
|
this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
|
||||||
const isFirst = stepIndex === 0;
|
const isFirst = stepIndex === 0;
|
||||||
|
const progressStepState = stepArg.progressStep ? this.getProgressStepState(stepArg) : null;
|
||||||
return html`<dees-tile
|
return html`<dees-tile
|
||||||
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}"
|
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}"
|
||||||
>
|
>
|
||||||
@@ -390,7 +414,20 @@ export class DeesStepper extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
<div class="step-body">
|
<div class="step-body">
|
||||||
<div class="title">${stepArg.title}</div>
|
<div class="title">${stepArg.title}</div>
|
||||||
<div class="content">${stepArg.content}</div>
|
${stepArg.progressStep && progressStepState ? html`
|
||||||
|
<div class="progressStep">
|
||||||
|
<dees-progressbar
|
||||||
|
.label=${progressStepState.label ?? stepArg.title}
|
||||||
|
.percentage=${progressStepState.percentage ?? 0}
|
||||||
|
.indeterminate=${progressStepState.indeterminate ?? false}
|
||||||
|
.showPercentage=${progressStepState.showPercentage ?? true}
|
||||||
|
.statusText=${progressStepState.statusText ?? ''}
|
||||||
|
.terminalLines=${progressStepState.terminalLines ?? []}
|
||||||
|
.statusRows=${progressStepState.statusRows ?? 3}
|
||||||
|
></dees-progressbar>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="content ${stepArg.progressStep ? 'withProgressStep' : ''}">${stepArg.content}</div>
|
||||||
</div>
|
</div>
|
||||||
<div slot="footer" class="bottomButtons">
|
<div slot="footer" class="bottomButtons">
|
||||||
${isSelected && this.activeForm !== null && !this.activeFormValid
|
${isSelected && this.activeForm !== null && !this.activeFormValid
|
||||||
@@ -426,22 +463,30 @@ export class DeesStepper extends DeesElement {
|
|||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
await this.domtools.convenience.smartdelay.delayFor(0);
|
await this.domtools.convenience.smartdelay.delayFor(0);
|
||||||
|
if (!this.steps.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.prepareStepForActivation(this.steps[0]);
|
||||||
this.selectedStep = this.steps[0];
|
this.selectedStep = this.steps[0];
|
||||||
this.setScrollStatus();
|
await this.updateComplete;
|
||||||
|
await this.setScrollStatus();
|
||||||
// Remove entrance class after initial animation completes
|
// Remove entrance class after initial animation completes
|
||||||
await this.domtools.convenience.smartdelay.delayFor(350);
|
await this.domtools.convenience.smartdelay.delayFor(350);
|
||||||
this.shadowRoot!.querySelector('.step.entrance')?.classList.remove('entrance');
|
this.shadowRoot!.querySelector('.step.entrance')?.classList.remove('entrance');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updated() {
|
public async updated(changedProperties: Map<string | number | symbol, unknown>) {
|
||||||
this.setScrollStatus();
|
if (!changedProperties.has('selectedStep') && !changedProperties.has('steps')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.setScrollStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public scroller!: typeof domtools.plugins.SweetScroll.prototype;
|
public scroller!: typeof domtools.plugins.SweetScroll.prototype;
|
||||||
|
|
||||||
public async setScrollStatus() {
|
public async setScrollStatus() {
|
||||||
const stepperContainer = this.shadowRoot!.querySelector('.stepperContainer') as HTMLElement;
|
const stepperContainer = this.shadowRoot!.querySelector('.stepperContainer') as HTMLElement;
|
||||||
const firstStepElement = this.shadowRoot!.querySelector('.step') as HTMLElement;
|
|
||||||
const selectedStepElement = this.shadowRoot!.querySelector('.selected') as HTMLElement;
|
const selectedStepElement = this.shadowRoot!.querySelector('.selected') as HTMLElement;
|
||||||
if (!selectedStepElement) {
|
if (!selectedStepElement) {
|
||||||
return;
|
return;
|
||||||
@@ -452,14 +497,11 @@ export class DeesStepper extends DeesElement {
|
|||||||
stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2
|
stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2
|
||||||
}px`;
|
}px`;
|
||||||
}
|
}
|
||||||
console.log('Setting scroll status');
|
|
||||||
console.log(selectedStepElement);
|
|
||||||
const scrollPosition =
|
const scrollPosition =
|
||||||
selectedStepElement.offsetTop -
|
selectedStepElement.offsetTop -
|
||||||
stepperContainer.offsetHeight / 2 +
|
stepperContainer.offsetHeight / 2 +
|
||||||
selectedStepElement.offsetHeight / 2;
|
selectedStepElement.offsetHeight / 2;
|
||||||
console.log(scrollPosition);
|
await domtools.DomTools.setupDomTools();
|
||||||
const domtoolsInstance = await domtools.DomTools.setupDomTools();
|
|
||||||
if (!this.scroller) {
|
if (!this.scroller) {
|
||||||
this.scroller = new domtools.plugins.SweetScroll(
|
this.scroller = new domtools.plugins.SweetScroll(
|
||||||
{
|
{
|
||||||
@@ -474,7 +516,11 @@ export class DeesStepper extends DeesElement {
|
|||||||
if (!this.selectedStep.validationFuncCalled && this.selectedStep.validationFunc) {
|
if (!this.selectedStep.validationFuncCalled && this.selectedStep.validationFunc) {
|
||||||
this.selectedStep.abortController = new AbortController();
|
this.selectedStep.abortController = new AbortController();
|
||||||
this.selectedStep.validationFuncCalled = true;
|
this.selectedStep.validationFuncCalled = true;
|
||||||
await this.selectedStep.validationFunc(this, selectedStepElement, this.selectedStep.abortController.signal);
|
void this.runStepValidation(
|
||||||
|
this.selectedStep,
|
||||||
|
selectedStepElement,
|
||||||
|
this.selectedStep.abortController.signal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.scroller.to(scrollPosition);
|
this.scroller.to(scrollPosition);
|
||||||
}
|
}
|
||||||
@@ -492,6 +538,7 @@ export class DeesStepper extends DeesElement {
|
|||||||
currentStep.validationFuncCalled = false;
|
currentStep.validationFuncCalled = false;
|
||||||
const previousStep = this.steps[currentIndex - 1];
|
const previousStep = this.steps[currentIndex - 1];
|
||||||
previousStep.validationFuncCalled = false;
|
previousStep.validationFuncCalled = false;
|
||||||
|
this.prepareStepForActivation(previousStep);
|
||||||
this.selectedStep = previousStep;
|
this.selectedStep = previousStep;
|
||||||
await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
await this.domtools.convenience.smartdelay.delayFor(100);
|
await this.domtools.convenience.smartdelay.delayFor(100);
|
||||||
@@ -511,9 +558,52 @@ export class DeesStepper extends DeesElement {
|
|||||||
currentStep.validationFuncCalled = false;
|
currentStep.validationFuncCalled = false;
|
||||||
const nextStep = this.steps[currentIndex + 1];
|
const nextStep = this.steps[currentIndex + 1];
|
||||||
nextStep.validationFuncCalled = false;
|
nextStep.validationFuncCalled = false;
|
||||||
|
this.prepareStepForActivation(nextStep);
|
||||||
this.selectedStep = nextStep;
|
this.selectedStep = nextStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public resetProgressStep(stepArg: IStep = this.selectedStep) {
|
||||||
|
if (!stepArg?.progressStep) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stepArg.progressStepState = this.createInitialProgressStepState(stepArg);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateProgressStep(
|
||||||
|
progressStateArg: Partial<IStepProgressState>,
|
||||||
|
stepArg: IStep = this.selectedStep,
|
||||||
|
) {
|
||||||
|
if (!stepArg?.progressStep) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentProgressState = this.getProgressStepState(stepArg);
|
||||||
|
stepArg.progressStepState = {
|
||||||
|
...currentProgressState,
|
||||||
|
...progressStateArg,
|
||||||
|
terminalLines: progressStateArg.terminalLines
|
||||||
|
? [...progressStateArg.terminalLines]
|
||||||
|
: [...(currentProgressState.terminalLines ?? [])],
|
||||||
|
};
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendProgressStepLine(lineArg: string, stepArg: IStep = this.selectedStep) {
|
||||||
|
if (!stepArg?.progressStep) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentProgressState = this.getProgressStepState(stepArg);
|
||||||
|
this.updateProgressStep(
|
||||||
|
{
|
||||||
|
terminalLines: [...(currentProgressState.terminalLines ?? []), lineArg],
|
||||||
|
},
|
||||||
|
stepArg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scans the currently selected step for a <dees-form> in its content. When
|
* Scans the currently selected step for a <dees-form> in its content. When
|
||||||
* found, subscribes to the form's RxJS changeSubject so the primary
|
* found, subscribes to the form's RxJS changeSubject so the primary
|
||||||
@@ -582,6 +672,74 @@ export class DeesStepper extends DeesElement {
|
|||||||
await optionArg.action(this);
|
await optionArg.action(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getProgressStepState(stepArg: IStep): IStepProgressState {
|
||||||
|
if (!stepArg.progressStep) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stepArg.progressStepState) {
|
||||||
|
stepArg.progressStepState = this.createInitialProgressStepState(stepArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stepArg.progressStepState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createInitialProgressStepState(stepArg: IStep): IStepProgressState {
|
||||||
|
return {
|
||||||
|
label: stepArg.progressStep?.label ?? stepArg.title,
|
||||||
|
percentage: stepArg.progressStep?.percentage ?? 0,
|
||||||
|
indeterminate: stepArg.progressStep?.indeterminate ?? false,
|
||||||
|
showPercentage: stepArg.progressStep?.showPercentage ?? true,
|
||||||
|
statusText: stepArg.progressStep?.statusText ?? '',
|
||||||
|
terminalLines: [...(stepArg.progressStep?.terminalLines ?? [])],
|
||||||
|
statusRows: stepArg.progressStep?.statusRows ?? 3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareStepForActivation(stepArg?: IStep) {
|
||||||
|
if (!stepArg?.progressStep) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stepArg.progressStepState = this.createInitialProgressStepState(stepArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runStepValidation(
|
||||||
|
stepArg: IStep,
|
||||||
|
selectedStepElement: HTMLElement,
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await stepArg.validationFunc?.(this, selectedStepElement, signal);
|
||||||
|
|
||||||
|
if (signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepArg.progressStep && stepArg.progressStep.autoAdvance !== false && this.selectedStep === stepArg) {
|
||||||
|
this.goNext();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepArg.progressStep) {
|
||||||
|
const errorText = error instanceof Error ? error.message : 'Unexpected error';
|
||||||
|
this.appendProgressStepLine(`Error: ${errorText}`, stepArg);
|
||||||
|
this.updateProgressStep(
|
||||||
|
{
|
||||||
|
indeterminate: false,
|
||||||
|
statusText: errorText,
|
||||||
|
},
|
||||||
|
stepArg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currently-open confirmation modal (if any). Prevents double-stacking when
|
* Currently-open confirmation modal (if any). Prevents double-stacking when
|
||||||
* the user clicks the backdrop or the Cancel button while a confirm modal
|
* the user clicks the backdrop or the Cancel button while a confirm modal
|
||||||
@@ -657,6 +815,9 @@ export class DeesStepper extends DeesElement {
|
|||||||
public async destroy() {
|
public async destroy() {
|
||||||
const domtools = await this.domtoolsPromise;
|
const domtools = await this.domtoolsPromise;
|
||||||
const container = this.shadowRoot!.querySelector('.stepperContainer');
|
const container = this.shadowRoot!.querySelector('.stepperContainer');
|
||||||
|
if (this.selectedStep?.abortController) {
|
||||||
|
this.selectedStep.abortController.abort();
|
||||||
|
}
|
||||||
container?.classList.add('predestroy');
|
container?.classList.add('predestroy');
|
||||||
await domtools.convenience.smartdelay.delayFor(250);
|
await domtools.convenience.smartdelay.delayFor(250);
|
||||||
if (this.parentElement) {
|
if (this.parentElement) {
|
||||||
|
|||||||
@@ -2,9 +2,73 @@ import { html } from '@design.estate/dees-element';
|
|||||||
|
|
||||||
import { DeesUpdater } from '../dees-updater/dees-updater.js';
|
import { DeesUpdater } from '../dees-updater/dees-updater.js';
|
||||||
|
|
||||||
export const demoFunc = async () => {
|
const waitForDemoStep = async (timeoutArg: number): Promise<void> => {
|
||||||
const updater = await DeesUpdater.createAndShow();
|
await new Promise<void>((resolve) => {
|
||||||
setTimeout(async () => {
|
window.setTimeout(() => resolve(), timeoutArg);
|
||||||
await updater.destroy();
|
});
|
||||||
}, 10000);
|
};
|
||||||
|
|
||||||
|
export const demoFunc = () => {
|
||||||
|
let updaterRunning = false;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div style="display: grid; gap: 16px; place-items: center; padding: 32px; text-align: center;">
|
||||||
|
<p style="margin: 0; max-width: 540px; line-height: 1.6; color: var(--dees-color-text-secondary);">
|
||||||
|
Launches the updater as a stepper flow. The first step streams terminal-style
|
||||||
|
progress updates and then moves automatically to the ready step.
|
||||||
|
</p>
|
||||||
|
<dees-button
|
||||||
|
@click=${async () => {
|
||||||
|
if (updaterRunning) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updaterRunning = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updater = await DeesUpdater.createAndShow({
|
||||||
|
currentVersion: '3.79.0',
|
||||||
|
updatedVersion: '3.80.0',
|
||||||
|
moreInfoUrl: 'https://code.foss.global/design.estate/dees-catalog',
|
||||||
|
changelogUrl: 'https://code.foss.global/design.estate/dees-catalog/-/blob/main/changelog.md',
|
||||||
|
successAction: 'close',
|
||||||
|
successDelayMs: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const progressFrames = [
|
||||||
|
{ line: 'Checking release manifest', percentage: 12, delay: 550 },
|
||||||
|
{ line: 'Downloading signed bundle', percentage: 33, delay: 700 },
|
||||||
|
{ line: 'Verifying checksum', percentage: 51, delay: 650 },
|
||||||
|
{ line: 'Applying update files', percentage: 74, delay: 800 },
|
||||||
|
{ line: 'Cleaning up previous release', percentage: 91, delay: 600 },
|
||||||
|
];
|
||||||
|
|
||||||
|
updater.updateProgress({
|
||||||
|
statusText: 'Checking release manifest...',
|
||||||
|
terminalLines: ['Checking release manifest'],
|
||||||
|
percentage: 12,
|
||||||
|
indeterminate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [index, progressFrame] of progressFrames.entries()) {
|
||||||
|
if (index > 0) {
|
||||||
|
updater.appendProgressLine(progressFrame.line);
|
||||||
|
updater.updateProgress({
|
||||||
|
percentage: progressFrame.percentage,
|
||||||
|
statusText: `${progressFrame.line}...`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForDemoStep(progressFrame.delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
await updater.markUpdateReady();
|
||||||
|
await waitForDemoStep(10500);
|
||||||
|
} finally {
|
||||||
|
updaterRunning = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>Show updater flow</dees-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,14 +4,28 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
html,
|
html,
|
||||||
property,
|
property,
|
||||||
type CSSResult,
|
css,
|
||||||
domtools,
|
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import { demoFunc } from './dees-updater.demo.js';
|
import { demoFunc } from './dees-updater.demo.js';
|
||||||
|
import {
|
||||||
|
DeesStepper,
|
||||||
|
type IStep,
|
||||||
|
type IStepProgressState,
|
||||||
|
} from '../../00group-layout/dees-stepper/dees-stepper.js';
|
||||||
|
import { monoFontFamily } from '../../00fonts.js';
|
||||||
|
|
||||||
import '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
export type TDeesUpdaterSuccessAction = 'close' | 'reload';
|
||||||
import { css, cssManager } from '@design.estate/dees-element';
|
|
||||||
import { themeDefaultStyles } from '../../00theme.js';
|
export interface IDeesUpdaterOptions {
|
||||||
|
currentVersion?: string;
|
||||||
|
updatedVersion?: string;
|
||||||
|
moreInfoUrl?: string;
|
||||||
|
changelogUrl?: string;
|
||||||
|
successAction?: TDeesUpdaterSuccessAction;
|
||||||
|
successDelayMs?: number;
|
||||||
|
successActionLabel?: string;
|
||||||
|
onSuccessAction?: () => Promise<void> | void;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -24,91 +38,475 @@ export class DeesUpdater extends DeesElement {
|
|||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
public static demoGroups = ['Utility'];
|
public static demoGroups = ['Utility'];
|
||||||
|
|
||||||
public static async createAndShow() {
|
public static async createAndShow(optionsArg: IDeesUpdaterOptions = {}) {
|
||||||
const updater = new DeesUpdater();
|
const updater = new DeesUpdater();
|
||||||
|
updater.currentVersion = optionsArg.currentVersion ?? '';
|
||||||
|
updater.updatedVersion = optionsArg.updatedVersion ?? '';
|
||||||
|
updater.moreInfoUrl = optionsArg.moreInfoUrl ?? '';
|
||||||
|
updater.changelogUrl = optionsArg.changelogUrl ?? '';
|
||||||
|
updater.successAction = optionsArg.successAction ?? 'close';
|
||||||
|
updater.successDelayMs = optionsArg.successDelayMs ?? 10000;
|
||||||
|
updater.successActionLabel = optionsArg.successActionLabel ?? '';
|
||||||
|
updater.onSuccessAction = optionsArg.onSuccessAction ?? null;
|
||||||
document.body.appendChild(updater);
|
document.body.appendChild(updater);
|
||||||
|
await updater.show();
|
||||||
return updater;
|
return updater;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
type: String,
|
type: String,
|
||||||
})
|
})
|
||||||
accessor currentVersion!: string;
|
accessor currentVersion = '';
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
type: String,
|
type: String,
|
||||||
})
|
})
|
||||||
accessor updatedVersion!: string;
|
accessor updatedVersion = '';
|
||||||
|
|
||||||
constructor() {
|
@property({
|
||||||
super();
|
type: String,
|
||||||
domtools.elementBasic.setup();
|
})
|
||||||
}
|
accessor moreInfoUrl = '';
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
accessor changelogUrl = '';
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
accessor successAction: TDeesUpdaterSuccessAction = 'close';
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
accessor successDelayMs = 10000;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
accessor successActionLabel = '';
|
||||||
|
|
||||||
|
private stepper: DeesStepper | null = null;
|
||||||
|
private progressStep: IStep | null = null;
|
||||||
|
private showPromise: Promise<void> | null = null;
|
||||||
|
private onSuccessAction: (() => Promise<void> | void) | null = null;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
themeDefaultStyles,
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
css`
|
||||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
:host {
|
||||||
.modalContainer {
|
display: none;
|
||||||
will-change: transform;
|
|
||||||
position: relative;
|
|
||||||
background: ${cssManager.bdTheme('#eeeeeb', '#222')};
|
|
||||||
max-width: 800px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-top: 1px solid ${cssManager.bdTheme('#eeeeeb', '#333')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.headingContainer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 40px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: none;
|
|
||||||
font-size: 20px;
|
|
||||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
|
||||||
margin-left: 20px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonContainer {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 50% 50%;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html``;
|
||||||
<dees-windowlayer
|
}
|
||||||
@clicked="${this.windowLayerClicked}"
|
|
||||||
.options=${{
|
public async connectedCallback(): Promise<void> {
|
||||||
blur: true,
|
await super.connectedCallback();
|
||||||
}}
|
void this.show();
|
||||||
>
|
}
|
||||||
<div class="modalContainer">
|
|
||||||
<div class="headingContainer">
|
public async show(): Promise<void> {
|
||||||
<dees-spinner .size=${60}></dees-spinner>
|
if (this.stepper?.isConnected) {
|
||||||
<h1>Updating the application...</h1>
|
return;
|
||||||
</div>
|
}
|
||||||
<div class="progress">
|
|
||||||
<dees-progressbar .progress=${0.5}></dees-progressbar>
|
if (this.showPromise) {
|
||||||
</div>
|
return this.showPromise;
|
||||||
<div class="buttonContainer">
|
}
|
||||||
<dees-button>More info</dees-button>
|
|
||||||
<dees-button>Changelog</dees-button>
|
this.showPromise = this.openStepperFlow();
|
||||||
</div>
|
|
||||||
</div> </dees-windowlayer
|
try {
|
||||||
>>
|
await this.showPromise;
|
||||||
`;
|
} finally {
|
||||||
|
this.showPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateProgress(progressStateArg: Partial<IStepProgressState>) {
|
||||||
|
if (!this.stepper || !this.progressStep) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stepper.updateProgressStep(progressStateArg, this.progressStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
public appendProgressLine(lineArg: string) {
|
||||||
|
if (!this.stepper || !this.progressStep) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stepper.appendProgressStepLine(lineArg, this.progressStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
public markUpdateError(messageArg: string) {
|
||||||
|
this.appendProgressLine(`Error: ${messageArg}`);
|
||||||
|
this.updateProgress({
|
||||||
|
indeterminate: false,
|
||||||
|
statusText: messageArg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async markUpdateReady() {
|
||||||
|
if (!this.stepper || !this.progressStep) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stepper.updateProgressStep(
|
||||||
|
{
|
||||||
|
percentage: 100,
|
||||||
|
indeterminate: false,
|
||||||
|
statusText: 'Update ready.',
|
||||||
|
},
|
||||||
|
this.progressStep,
|
||||||
|
);
|
||||||
|
this.stepper.appendProgressStepLine('Update ready', this.progressStep);
|
||||||
|
|
||||||
|
if (this.stepper.selectedStep === this.progressStep) {
|
||||||
|
this.stepper.goNext();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async destroy() {
|
public async destroy() {
|
||||||
this.parentElement!.removeChild(this);
|
const stepper = this.stepper;
|
||||||
|
this.stepper = null;
|
||||||
|
this.progressStep = null;
|
||||||
|
|
||||||
|
if (stepper?.isConnected) {
|
||||||
|
await stepper.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private windowLayerClicked() {}
|
if (this.parentElement) {
|
||||||
|
this.parentElement.removeChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openStepperFlow() {
|
||||||
|
const { steps, progressStep } = this.createUpdaterSteps();
|
||||||
|
this.progressStep = progressStep;
|
||||||
|
this.stepper = await DeesStepper.createAndShow({
|
||||||
|
steps,
|
||||||
|
cancelable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createUpdaterSteps(): { steps: IStep[]; progressStep: IStep } {
|
||||||
|
const infoMenuOptions = this.getLinkMenuOptions();
|
||||||
|
const progressStep: IStep = {
|
||||||
|
title: 'Updating the application',
|
||||||
|
content: this.renderProgressContent(),
|
||||||
|
progressStep: {
|
||||||
|
label: this.getProgressLabel(),
|
||||||
|
percentage: 5,
|
||||||
|
indeterminate: true,
|
||||||
|
statusRows: 4,
|
||||||
|
statusText: 'Preparing update...',
|
||||||
|
terminalLines: ['Preparing update'],
|
||||||
|
},
|
||||||
|
menuOptions: infoMenuOptions.length > 0 ? infoMenuOptions : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const readyStep: IStep = {
|
||||||
|
title: this.updatedVersion ? `Version ${this.updatedVersion} ready` : 'Update ready',
|
||||||
|
content: this.renderReadyContent(),
|
||||||
|
progressStep: {
|
||||||
|
label: this.getSuccessCountdownLabel(this.getSuccessDelaySeconds()),
|
||||||
|
percentage: 0,
|
||||||
|
indeterminate: false,
|
||||||
|
showPercentage: false,
|
||||||
|
statusRows: 2,
|
||||||
|
statusText: this.getSuccessCountdownStatus(this.getSuccessDelaySeconds()),
|
||||||
|
},
|
||||||
|
validationFunc: async (stepper, _htmlElement, signal) => {
|
||||||
|
await this.runSuccessCountdown(stepper, readyStep, signal);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
steps: [progressStep, readyStep],
|
||||||
|
progressStep,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getProgressLabel(): string {
|
||||||
|
if (this.currentVersion && this.updatedVersion) {
|
||||||
|
return `${this.currentVersion} -> ${this.updatedVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.updatedVersion) {
|
||||||
|
return `Preparing ${this.updatedVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Application update';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSuccessDelaySeconds(): number {
|
||||||
|
return Math.max(1, Math.ceil(this.successDelayMs / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSuccessActionDisplayLabel(): string {
|
||||||
|
if (this.successActionLabel) {
|
||||||
|
return this.successActionLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onSuccessAction) {
|
||||||
|
return 'Continuing automatically';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.successAction === 'reload') {
|
||||||
|
return 'Reloading application';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Closing updater';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSuccessCountdownLabel(secondsArg: number): string {
|
||||||
|
return `${this.getSuccessActionDisplayLabel()} in ${secondsArg}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSuccessCountdownStatus(secondsArg: number): string {
|
||||||
|
const secondLabel = secondsArg === 1 ? 'second' : 'seconds';
|
||||||
|
return `${this.getSuccessActionDisplayLabel()} in ${secondsArg} ${secondLabel}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSuccessActionNowLabel(): string {
|
||||||
|
return `${this.getSuccessActionDisplayLabel()} now...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLinkMenuOptions() {
|
||||||
|
const menuOptions: Array<{ name: string; action: () => Promise<void> }> = [];
|
||||||
|
|
||||||
|
if (this.moreInfoUrl) {
|
||||||
|
menuOptions.push({
|
||||||
|
name: 'More info',
|
||||||
|
action: async () => {
|
||||||
|
this.openExternalUrl(this.moreInfoUrl);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changelogUrl) {
|
||||||
|
menuOptions.push({
|
||||||
|
name: 'Changelog',
|
||||||
|
action: async () => {
|
||||||
|
this.openExternalUrl(this.changelogUrl);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return menuOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderVersionMeta(labelArg: string, valueArg: string): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
style="display: grid; gap: 4px; min-width: 132px; padding: 12px 14px; border: 1px solid var(--dees-color-border-subtle); border-radius: 8px; background: var(--dees-color-hover);"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--dees-color-text-muted);"
|
||||||
|
>
|
||||||
|
${labelArg}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style=${`font-family: ${monoFontFamily}; font-size: 13px; font-weight: 600; letter-spacing: -0.01em; color: var(--dees-color-text-primary);`}
|
||||||
|
>
|
||||||
|
${valueArg}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderProgressContent(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div style="display: grid; gap: 18px; color: var(--dees-color-text-secondary);">
|
||||||
|
<div style="display: grid; gap: 6px;">
|
||||||
|
<div
|
||||||
|
style="font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dees-color-text-muted);"
|
||||||
|
>
|
||||||
|
Application update
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="font-size: 18px; line-height: 1.35; font-weight: 600; letter-spacing: -0.02em; color: var(--dees-color-text-primary);"
|
||||||
|
>
|
||||||
|
Downloading and applying the latest release.
|
||||||
|
</div>
|
||||||
|
<p style="margin: 0; font-size: 14px; line-height: 1.65; color: var(--dees-color-text-secondary);">
|
||||||
|
${this.currentVersion && this.updatedVersion
|
||||||
|
? html`Moving from <strong>${this.currentVersion}</strong> to <strong>${this.updatedVersion}</strong>.`
|
||||||
|
: this.updatedVersion
|
||||||
|
? html`Preparing <strong>${this.updatedVersion}</strong> for installation.`
|
||||||
|
: 'The updater is fetching the newest build and preparing it for installation.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.currentVersion || this.updatedVersion
|
||||||
|
? html`
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||||
|
${this.currentVersion
|
||||||
|
? this.renderVersionMeta('Current version', this.currentVersion)
|
||||||
|
: ''}
|
||||||
|
${this.updatedVersion
|
||||||
|
? this.renderVersionMeta('Incoming version', this.updatedVersion)
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="display: grid; gap: 4px; padding: 14px 16px; border: 1px solid var(--dees-color-border-subtle); border-radius: 8px; background: var(--dees-color-hover);"
|
||||||
|
>
|
||||||
|
<div style="font-size: 12px; font-weight: 600; color: var(--dees-color-text-primary);">
|
||||||
|
Automatic flow
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 13px; line-height: 1.6; color: var(--dees-color-text-muted);">
|
||||||
|
The updater continues on its own once the new build is installed and verified.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderReadyContent(): TemplateResult {
|
||||||
|
const successDelaySeconds = this.getSuccessDelaySeconds();
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div style="display: grid; gap: 18px; color: var(--dees-color-text-secondary);">
|
||||||
|
<div style="display: grid; gap: 6px;">
|
||||||
|
<div
|
||||||
|
style="font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--dees-color-text-muted);"
|
||||||
|
>
|
||||||
|
Update complete
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="font-size: 18px; line-height: 1.35; font-weight: 600; letter-spacing: -0.02em; color: var(--dees-color-text-primary);"
|
||||||
|
>
|
||||||
|
${this.updatedVersion
|
||||||
|
? html`Version <span style=${`font-family: ${monoFontFamily}; font-size: 0.95em;`}>${this.updatedVersion}</span> is ready to use.`
|
||||||
|
: 'The new version is ready to use.'}
|
||||||
|
</div>
|
||||||
|
<p style="margin: 0; font-size: 14px; line-height: 1.65; color: var(--dees-color-text-secondary);">
|
||||||
|
The new build has been installed and verified. You can review release details below while the next action runs automatically.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.updatedVersion
|
||||||
|
? html`
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
|
||||||
|
${this.renderVersionMeta('Installed version', this.updatedVersion)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="display: grid; gap: 4px; padding: 14px 16px; border: 1px solid var(--dees-color-border-subtle); border-radius: 8px; background: var(--dees-color-hover);"
|
||||||
|
>
|
||||||
|
<div style="font-size: 12px; font-weight: 600; color: var(--dees-color-text-primary);">
|
||||||
|
Next action
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 14px; line-height: 1.5; font-weight: 600; color: var(--dees-color-text-primary);">
|
||||||
|
${this.getSuccessActionDisplayLabel()}
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 13px; line-height: 1.6; color: var(--dees-color-text-muted);">
|
||||||
|
Runs automatically in ${successDelaySeconds} seconds.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runSuccessCountdown(
|
||||||
|
stepperArg: DeesStepper,
|
||||||
|
stepArg: IStep,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<void> {
|
||||||
|
const totalDuration = Math.max(1000, this.successDelayMs);
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (!signal?.aborted) {
|
||||||
|
const elapsed = Math.min(totalDuration, Date.now() - startTime);
|
||||||
|
const remainingMilliseconds = Math.max(0, totalDuration - elapsed);
|
||||||
|
const remainingSeconds = Math.max(0, Math.ceil(remainingMilliseconds / 1000));
|
||||||
|
|
||||||
|
stepperArg.updateProgressStep(
|
||||||
|
{
|
||||||
|
label: remainingMilliseconds > 0
|
||||||
|
? this.getSuccessCountdownLabel(remainingSeconds)
|
||||||
|
: this.getSuccessActionNowLabel(),
|
||||||
|
percentage: (elapsed / totalDuration) * 100,
|
||||||
|
indeterminate: false,
|
||||||
|
showPercentage: false,
|
||||||
|
statusText: remainingMilliseconds > 0
|
||||||
|
? this.getSuccessCountdownStatus(remainingSeconds)
|
||||||
|
: this.getSuccessActionNowLabel(),
|
||||||
|
},
|
||||||
|
stepArg,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (remainingMilliseconds <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const completed = await this.waitForCountdownTick(100, signal);
|
||||||
|
if (!completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.runConfiguredSuccessAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForCountdownTick(timeoutArg: number, signal?: AbortSignal): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let completed = false;
|
||||||
|
|
||||||
|
const finish = (result: boolean) => {
|
||||||
|
if (completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
completed = true;
|
||||||
|
if (signal) {
|
||||||
|
signal.removeEventListener('abort', handleAbort);
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAbort = () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
finish(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
finish(true);
|
||||||
|
}, timeoutArg);
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
signal.addEventListener('abort', handleAbort, { once: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runConfiguredSuccessAction(): Promise<void> {
|
||||||
|
if (this.onSuccessAction) {
|
||||||
|
await this.onSuccessAction();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.successAction === 'reload') {
|
||||||
|
await this.destroy();
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private openExternalUrl(urlArg: string) {
|
||||||
|
window.open(urlArg, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user