mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-31 19:36:33 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5830e7418 | |||
| f391a7bb06 | |||
| f890b504a8 | |||
| c8a29ae2f2 | |||
| b1ccb6355d | |||
| 5f2086765a |
@@ -62,7 +62,7 @@ Managed app, demo, gallery, and E2E app workflows share one lifetime lock, so on
|
||||
|
||||
`yarn dev:serve` also serves locally and supports `-c` for the core URL and `-p` for the port. The default is 8124, or 8123 in a devcontainer.
|
||||
|
||||
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal. `yarn dev` and `yarn dev:serve` share one managed process slot because both write the app output.
|
||||
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. `yarn dev`, `yarn dev:serve`, `yarn dev:demo`, and `yarn dev:gallery` also support `--fetch-translations`; this runs translation fetching, including first-time GitHub device authentication, under the workflow lock before starting the watcher. It works in foreground and background modes. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal. `yarn dev` and `yarn dev:serve` share one managed process slot because both write the app output.
|
||||
|
||||
## Playwright E2E
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
// --status Report whether the suite's dev server is running.
|
||||
// --stop Stop a running background dev server.
|
||||
// --logs [--follow] Print (or follow) the background dev server log.
|
||||
// --fetch-translations
|
||||
// Fetch nightly translations before starting (app,
|
||||
// app-serve, demo, and gallery only).
|
||||
//
|
||||
// Extra args (for example -p or -c on app-serve) are forwarded to the underlying
|
||||
// script. Suites use one of two liveness models:
|
||||
@@ -73,6 +76,7 @@ const SUITES = new Map([
|
||||
"demo",
|
||||
{
|
||||
alias: "dev:demo",
|
||||
fetchTranslations: true,
|
||||
liveness: "health",
|
||||
port: 8090,
|
||||
spawn: { cmd: gulpBin, args: ["develop-demo"] },
|
||||
@@ -82,6 +86,7 @@ const SUITES = new Map([
|
||||
"gallery",
|
||||
{
|
||||
alias: "dev:gallery",
|
||||
fetchTranslations: true,
|
||||
liveness: "health",
|
||||
port: 8100,
|
||||
spawn: { cmd: gulpBin, args: ["develop-gallery"] },
|
||||
@@ -91,6 +96,7 @@ const SUITES = new Map([
|
||||
"app",
|
||||
{
|
||||
alias: "dev",
|
||||
fetchTranslations: true,
|
||||
liveness: "process",
|
||||
readyLog: /Build done @/,
|
||||
spawn: { cmd: gulpBin, args: ["develop-app"] },
|
||||
@@ -102,6 +108,7 @@ const SUITES = new Map([
|
||||
alias: "dev:serve",
|
||||
liveness: "process",
|
||||
acceptsArgs: true,
|
||||
fetchTranslations: true,
|
||||
readyLog: /Build done @/,
|
||||
spawn: { cmd: developAndServeScript, args: [] },
|
||||
},
|
||||
@@ -144,12 +151,14 @@ const usage = () => {
|
||||
const suites = [...SUITES.keys()].join("|");
|
||||
process.stderr.write(
|
||||
`Usage: node build-scripts/dev-server.mjs --suite <${suites}> ` +
|
||||
`[--background | --status | --stop | --logs [--follow]]\n`
|
||||
`[--background | --status | --stop | --logs [--follow]] ` +
|
||||
`[--fetch-translations]\n`
|
||||
);
|
||||
};
|
||||
|
||||
const parseArgs = (argv) => {
|
||||
const args = {
|
||||
fetchTranslations: false,
|
||||
mode: "foreground",
|
||||
follow: false,
|
||||
modes: [],
|
||||
@@ -165,6 +174,9 @@ const parseArgs = (argv) => {
|
||||
case "--follow":
|
||||
args.follow = true;
|
||||
break;
|
||||
case "--fetch-translations":
|
||||
args.fetchTranslations = true;
|
||||
break;
|
||||
default:
|
||||
if (LIFECYCLE_MODE_FLAGS.has(arg)) {
|
||||
args.mode = LIFECYCLE_MODE_FLAGS.get(arg);
|
||||
@@ -178,6 +190,28 @@ const parseArgs = (argv) => {
|
||||
return args;
|
||||
};
|
||||
|
||||
const translationPrebuildEnv = (token) => {
|
||||
const env = workflowLockEnv(token);
|
||||
delete env.SKIP_FETCH_NIGHTLY_TRANSLATIONS;
|
||||
return env;
|
||||
};
|
||||
|
||||
const suiteEnv = (token, fetchTranslations = false) => ({
|
||||
...workflowLockEnv(token),
|
||||
...(fetchTranslations && { SKIP_FETCH_NIGHTLY_TRANSLATIONS: "1" }),
|
||||
});
|
||||
|
||||
const runPrebuild = (token, fetchTranslations = false) =>
|
||||
fetchTranslations
|
||||
? spawnForeground({
|
||||
cmd: gulpBin,
|
||||
args: ["setup-and-fetch-nightly-translations"],
|
||||
cwd: repoRoot,
|
||||
env: translationPrebuildEnv(token),
|
||||
processGroup: true,
|
||||
})
|
||||
: Promise.resolve(0);
|
||||
|
||||
const logFileFor = (suite) => path.join(logDir, `${suite}.log`);
|
||||
const acquireSuite = (suite) => {
|
||||
const token = `${process.pid}-${Date.now()}-${Math.random()}`;
|
||||
@@ -405,7 +439,7 @@ const isHttpServing = async (port, timeoutMs = 1000) => {
|
||||
return probeHost(0);
|
||||
};
|
||||
|
||||
const runForegroundHealth = async (suite, cfg) => {
|
||||
const runForegroundHealth = async (suite, cfg, fetchTranslations = false) => {
|
||||
const { port } = cfg;
|
||||
const lock = acquireSuiteForStart(suite);
|
||||
if (!lock.token) {
|
||||
@@ -427,11 +461,15 @@ const runForegroundHealth = async (suite, cfg) => {
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
|
||||
if (prebuildCode !== 0) {
|
||||
return prebuildCode;
|
||||
}
|
||||
return await spawnForeground({
|
||||
cmd: cfg.spawn.cmd,
|
||||
args: cfg.spawn.args,
|
||||
cwd: repoRoot,
|
||||
env: workflowLockEnv(lock.token),
|
||||
env: suiteEnv(lock.token, fetchTranslations),
|
||||
processGroup: true,
|
||||
onSpawn: (child) => updateSuite(suite, lock.token, child, port),
|
||||
});
|
||||
@@ -440,7 +478,7 @@ const runForegroundHealth = async (suite, cfg) => {
|
||||
}
|
||||
};
|
||||
|
||||
const runBackgroundHealth = async (suite, cfg) => {
|
||||
const runBackgroundHealth = async (suite, cfg, fetchTranslations = false) => {
|
||||
const { port } = cfg;
|
||||
const lock = acquireSuiteForStart(suite);
|
||||
if (!lock.token) {
|
||||
@@ -463,12 +501,17 @@ const runBackgroundHealth = async (suite, cfg) => {
|
||||
}
|
||||
let child;
|
||||
try {
|
||||
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
|
||||
if (prebuildCode !== 0) {
|
||||
releaseSuite(lock.token);
|
||||
return prebuildCode;
|
||||
}
|
||||
const logFile = logFileFor(suite);
|
||||
child = await spawnDetachedToLog({
|
||||
cmd: cfg.spawn.cmd,
|
||||
args: cfg.spawn.args,
|
||||
cwd: repoRoot,
|
||||
env: workflowLockEnv(lock.token),
|
||||
env: suiteEnv(lock.token, fetchTranslations),
|
||||
logFile,
|
||||
});
|
||||
updateSuite(suite, lock.token, child, port);
|
||||
@@ -520,17 +563,26 @@ const spawnArgs = (cfg, passthrough) => [
|
||||
...(cfg.acceptsArgs ? passthrough : []),
|
||||
];
|
||||
|
||||
const runForegroundProcess = async (suite, cfg, passthrough) => {
|
||||
const runForegroundProcess = async (
|
||||
suite,
|
||||
cfg,
|
||||
passthrough,
|
||||
fetchTranslations = false
|
||||
) => {
|
||||
const lock = acquireSuiteForStart(suite);
|
||||
if (!lock.token) {
|
||||
return lock.code;
|
||||
}
|
||||
try {
|
||||
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
|
||||
if (prebuildCode !== 0) {
|
||||
return prebuildCode;
|
||||
}
|
||||
return await spawnForeground({
|
||||
cmd: cfg.spawn.cmd,
|
||||
args: spawnArgs(cfg, passthrough),
|
||||
cwd: repoRoot,
|
||||
env: workflowLockEnv(lock.token),
|
||||
env: suiteEnv(lock.token, fetchTranslations),
|
||||
processGroup: true,
|
||||
onSpawn: (child) => updateSuite(suite, lock.token, child),
|
||||
});
|
||||
@@ -539,7 +591,12 @@ const runForegroundProcess = async (suite, cfg, passthrough) => {
|
||||
}
|
||||
};
|
||||
|
||||
const runBackgroundProcess = async (suite, cfg, passthrough) => {
|
||||
const runBackgroundProcess = async (
|
||||
suite,
|
||||
cfg,
|
||||
passthrough,
|
||||
fetchTranslations = false
|
||||
) => {
|
||||
const lock = acquireSuiteForStart(suite);
|
||||
if (!lock.token) {
|
||||
return lock.code;
|
||||
@@ -547,12 +604,17 @@ const runBackgroundProcess = async (suite, cfg, passthrough) => {
|
||||
|
||||
let child;
|
||||
try {
|
||||
const prebuildCode = await runPrebuild(lock.token, fetchTranslations);
|
||||
if (prebuildCode !== 0) {
|
||||
releaseSuite(lock.token);
|
||||
return prebuildCode;
|
||||
}
|
||||
const logFile = logFileFor(suite);
|
||||
child = await spawnDetachedToLog({
|
||||
cmd: cfg.spawn.cmd,
|
||||
args: spawnArgs(cfg, passthrough),
|
||||
cwd: repoRoot,
|
||||
env: workflowLockEnv(lock.token),
|
||||
env: suiteEnv(lock.token, fetchTranslations),
|
||||
logFile,
|
||||
});
|
||||
|
||||
@@ -630,6 +692,17 @@ const main = async () => {
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
if (
|
||||
args.fetchTranslations &&
|
||||
(!["foreground", "background"].includes(args.mode) ||
|
||||
!cfg.fetchTranslations)
|
||||
) {
|
||||
process.stderr.write(
|
||||
"--fetch-translations is only supported when starting app, app-serve, demo, or gallery.\n"
|
||||
);
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
if (args.passthrough.length && !cfg.acceptsArgs) {
|
||||
process.stderr.write(
|
||||
`Ignoring unexpected arguments: ${args.passthrough.join(" ")}\n`
|
||||
@@ -665,14 +738,26 @@ const main = async () => {
|
||||
const handlers =
|
||||
cfg.liveness === "health"
|
||||
? {
|
||||
foreground: () => runForegroundHealth(args.suite, cfg),
|
||||
background: () => runBackgroundHealth(args.suite, cfg),
|
||||
foreground: () =>
|
||||
runForegroundHealth(args.suite, cfg, args.fetchTranslations),
|
||||
background: () =>
|
||||
runBackgroundHealth(args.suite, cfg, args.fetchTranslations),
|
||||
}
|
||||
: {
|
||||
foreground: () =>
|
||||
runForegroundProcess(args.suite, cfg, args.passthrough),
|
||||
runForegroundProcess(
|
||||
args.suite,
|
||||
cfg,
|
||||
args.passthrough,
|
||||
args.fetchTranslations
|
||||
),
|
||||
background: () =>
|
||||
runBackgroundProcess(args.suite, cfg, args.passthrough),
|
||||
runBackgroundProcess(
|
||||
args.suite,
|
||||
cfg,
|
||||
args.passthrough,
|
||||
args.fetchTranslations
|
||||
),
|
||||
};
|
||||
return handlers[mode]();
|
||||
};
|
||||
|
||||
@@ -118,10 +118,15 @@ export class HaProgressButton extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Fade the content out rather than hiding it, so the button keeps its
|
||||
accessible name while the result icon covers it. */
|
||||
ha-button.result::part(start),
|
||||
ha-button.result::part(end),
|
||||
ha-button.result::part(label),
|
||||
ha-button.result::part(caret),
|
||||
ha-button.result::part(caret) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
ha-button.result::part(spinner) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ContextProvider, createContext } from "@lit/context";
|
||||
import type { ReactiveController, ReactiveControllerHost } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@@ -15,6 +17,61 @@ export const panelIsReady = async (element: HTMLElement) => {
|
||||
fireEvent(element, "hass-panel-ready");
|
||||
};
|
||||
|
||||
export type RegisterChildPanelReady = (ready: Promise<void>) => void;
|
||||
|
||||
export const childPanelReadyContext =
|
||||
createContext<RegisterChildPanelReady>("child-panel-ready");
|
||||
|
||||
export class ChildPanelReady implements ReactiveController {
|
||||
private _promises: Promise<void>[] = [];
|
||||
|
||||
private _host: ReactiveControllerHost & HTMLElement;
|
||||
|
||||
private _resolveReady?: () => void;
|
||||
|
||||
private _completing = false;
|
||||
|
||||
public ready = new Promise<void>((resolve) => {
|
||||
this._resolveReady = resolve;
|
||||
});
|
||||
|
||||
public constructor(host: ReactiveControllerHost & HTMLElement) {
|
||||
this._host = host;
|
||||
host.addController(this);
|
||||
new ContextProvider(host, {
|
||||
context: childPanelReadyContext,
|
||||
initialValue: (ready) => this._promises.push(ready),
|
||||
});
|
||||
}
|
||||
|
||||
public hostUpdated() {
|
||||
if (this._completing) {
|
||||
return;
|
||||
}
|
||||
this._completing = true;
|
||||
this._host.removeController(this);
|
||||
void this._complete();
|
||||
}
|
||||
|
||||
private async _complete() {
|
||||
// Children created/updated during this render register after the host's
|
||||
// update commits. Wait for that before snapshotting readiness promises.
|
||||
await this._host.updateComplete;
|
||||
|
||||
await this._waitForPromises();
|
||||
|
||||
this._resolveReady?.();
|
||||
await panelIsReady(this._host);
|
||||
}
|
||||
|
||||
private _waitForPromises(): Promise<void> {
|
||||
const count = this._promises.length;
|
||||
return Promise.allSettled(this._promises).then(() =>
|
||||
this._promises.length > count ? this._waitForPromises() : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class PanelReady {
|
||||
public ready?: Promise<void>;
|
||||
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-switch";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomCurrentTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../components/radio/ha-radio-group";
|
||||
import "../../components/radio/ha-radio-option";
|
||||
import { RecurrenceRange } from "../../data/calendar";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ConfirmEventDialogBoxParams } from "./show-confirm-event-dialog-box";
|
||||
import "../../components/ha-button";
|
||||
|
||||
// `RecurrenceRange.THISEVENT` is "", which the radio group treats as unselected
|
||||
// on its value-attribute and form-reset paths, so keep the options on their own
|
||||
// literals and map them when confirming.
|
||||
type RecurrenceScope = "this" | "future";
|
||||
|
||||
@customElement("confirm-event-dialog-box")
|
||||
class ConfirmEventDialogBox extends LitElement {
|
||||
@@ -18,11 +27,14 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state()
|
||||
private _closeState?: "canceled" | "confirmed" | "confirmedFuture";
|
||||
@state() private _closeState?: "canceled" | "confirmed";
|
||||
|
||||
@state() private _scope: RecurrenceScope = "this";
|
||||
|
||||
public async showDialog(params: ConfirmEventDialogBoxParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._scope = "this";
|
||||
this._closeState = undefined;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -39,20 +51,27 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const { destructive, recurring } = this._params;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
width="small"
|
||||
type="alert"
|
||||
aria-describedby=${recurring ? nothing : "description"}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
<p>${this._params.text}</p>
|
||||
</div>
|
||||
${
|
||||
recurring
|
||||
? this._renderRecurrenceRange()
|
||||
: html`<p id="description">${this._params.text}</p>`
|
||||
}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._dismiss}
|
||||
?autofocus=${!recurring && destructive}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
@@ -60,29 +79,41 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._confirm}
|
||||
autofocus
|
||||
variant="danger"
|
||||
?autofocus=${!recurring && !destructive}
|
||||
variant=${destructive ? "danger" : "brand"}
|
||||
>
|
||||
${this._params.confirmText}
|
||||
</ha-button>
|
||||
${
|
||||
this._params.confirmFutureText
|
||||
? html`
|
||||
<ha-button
|
||||
@click=${this._confirmFuture}
|
||||
slot="primaryAction"
|
||||
variant="danger"
|
||||
>
|
||||
${this._params.confirmFutureText}
|
||||
</ha-button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRecurrenceRange() {
|
||||
const thisEvent = this.hass.localize(
|
||||
"ui.components.calendar.event.recurrence_range.this_event"
|
||||
);
|
||||
const thisAndFuture = this.hass.localize(
|
||||
"ui.components.calendar.event.recurrence_range.this_and_future"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-radio-group
|
||||
name="recurrence_range"
|
||||
.label=${this._params!.text ?? this._params!.title}
|
||||
.value=${this._scope}
|
||||
@change=${this._scopeChanged}
|
||||
>
|
||||
<ha-radio-option value="this" autofocus>${thisEvent}</ha-radio-option>
|
||||
<ha-radio-option value="future">${thisAndFuture}</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
`;
|
||||
}
|
||||
|
||||
private _scopeChanged(ev: HASSDomCurrentTargetEvent<HaRadioGroup>): void {
|
||||
this._scope = ev.currentTarget.value as RecurrenceScope;
|
||||
}
|
||||
|
||||
private _dismiss(): void {
|
||||
this._closeState = "canceled";
|
||||
this.closeDialog();
|
||||
@@ -90,17 +121,11 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
|
||||
private _confirm(): void {
|
||||
this._closeState = "confirmed";
|
||||
if (this._params!.confirm) {
|
||||
this._params!.confirm(RecurrenceRange.THISEVENT);
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _confirmFuture(): void {
|
||||
this._closeState = "confirmedFuture";
|
||||
if (this._params!.confirm) {
|
||||
this._params!.confirm(RecurrenceRange.THISANDFUTURE);
|
||||
}
|
||||
this._params!.confirm?.(
|
||||
this._scope === "future"
|
||||
? RecurrenceRange.THISANDFUTURE
|
||||
: RecurrenceRange.THISEVENT
|
||||
);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
@@ -108,10 +133,7 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this._closeState !== "confirmed" &&
|
||||
this._closeState !== "confirmedFuture"
|
||||
) {
|
||||
if (this._closeState !== "confirmed") {
|
||||
this._params.cancel?.();
|
||||
}
|
||||
this._params = undefined;
|
||||
@@ -125,18 +147,12 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
pointer-events: initial !important;
|
||||
cursor: initial !important;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.no-bottom-padding {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
ha-radio-group::part(form-control-label) {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
ha-dialog {
|
||||
/* Place above other dialogs */
|
||||
|
||||
@@ -224,18 +224,9 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.prompt"
|
||||
),
|
||||
confirmText: entry.recurrence_id
|
||||
? this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete_this"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete"
|
||||
),
|
||||
confirmFutureText: entry.recurrence_id
|
||||
? this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete_future"
|
||||
)
|
||||
: undefined,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
recurring: !!entry.recurrence_id,
|
||||
destructive: true,
|
||||
});
|
||||
if (range === undefined) {
|
||||
// Cancel
|
||||
|
||||
@@ -569,12 +569,8 @@ class DialogCalendarEventEditor extends DirtyStateProviderMixin<CalendarEventFor
|
||||
text: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_update.recurring_prompt"
|
||||
),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_update.update_this"
|
||||
),
|
||||
confirmFutureText: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_update.update_future"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.update"),
|
||||
recurring: true,
|
||||
});
|
||||
}
|
||||
if (range === undefined) {
|
||||
@@ -625,18 +621,9 @@ class DialogCalendarEventEditor extends DirtyStateProviderMixin<CalendarEventFor
|
||||
: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.prompt"
|
||||
),
|
||||
confirmText: entry.recurrence_id
|
||||
? this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete_this"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete"
|
||||
),
|
||||
confirmFutureText: entry.recurrence_id
|
||||
? this.hass.localize(
|
||||
"ui.components.calendar.event.confirm_delete.delete_future"
|
||||
)
|
||||
: undefined,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
recurring: !!entry.recurrence_id,
|
||||
destructive: true,
|
||||
});
|
||||
if (range === undefined) {
|
||||
// Cancel
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { RecurrenceRange } from "../../data/calendar";
|
||||
|
||||
export interface ConfirmEventDialogBoxParams {
|
||||
title: string;
|
||||
// Labels the recurrence range options when `recurring` is set
|
||||
text?: string;
|
||||
confirmText?: string;
|
||||
confirmFutureText?: string; // Prompt for future recurring events
|
||||
// Let the user pick which occurrences of a recurring event are affected
|
||||
recurring?: boolean;
|
||||
destructive?: boolean;
|
||||
confirm?: (recurrenceRange: RecurrenceRange) => void;
|
||||
cancel?: () => void;
|
||||
text?: string | TemplateResult;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const loadGenericDialog = () => import("./confirm-event-dialog-box");
|
||||
|
||||
@@ -36,6 +36,7 @@ import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import { ChildPanelReady } from "../../../layouts/panel-ready";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -157,6 +158,11 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
total: 0,
|
||||
};
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
new ChildPanelReady(this);
|
||||
}
|
||||
|
||||
private _pages = memoizeOne(
|
||||
(
|
||||
cloudStatus,
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { consume } from "@lit/context";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { filterNavigationPages } from "../../../common/config/filter_navigation_pages";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import {
|
||||
childPanelReadyContext,
|
||||
type RegisterChildPanelReady,
|
||||
} from "../../../layouts/panel-ready";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-config-navigation-list";
|
||||
|
||||
@@ -18,21 +24,53 @@ class HaConfigNavigation extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public pages!: PageNavigation[];
|
||||
|
||||
@state() private _hasBluetoothConfigEntries = false;
|
||||
@state() private _visiblePages?: PageNavigation[];
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
getConfigEntries(this.hass, {
|
||||
domain: "bluetooth",
|
||||
}).then((bluetoothEntries) => {
|
||||
this._hasBluetoothConfigEntries = bluetoothEntries.length > 0;
|
||||
});
|
||||
private _hasBluetoothConfigEntries = false;
|
||||
|
||||
private _bluetoothEntriesLoaded?: Promise<void>;
|
||||
|
||||
private _childReadyRegistered = false;
|
||||
|
||||
private _filterNavigationPages = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
pages: PageNavigation[],
|
||||
hasBluetoothConfigEntries: boolean
|
||||
) => filterNavigationPages(hass, pages, { hasBluetoothConfigEntries })
|
||||
);
|
||||
|
||||
@consume({ context: childPanelReadyContext, subscribe: true })
|
||||
private _registerChildPanelReady?: RegisterChildPanelReady;
|
||||
|
||||
protected override updated(changedProps: Map<PropertyKey, unknown>) {
|
||||
super.updated(changedProps);
|
||||
|
||||
const pagesOrHassChanged =
|
||||
changedProps.has("pages") || changedProps.has("hass");
|
||||
|
||||
if (pagesOrHassChanged) {
|
||||
const ready = this._resolveVisiblePages();
|
||||
if (!this._childReadyRegistered && this._registerChildPanelReady) {
|
||||
this._registerChildPanelReady(ready);
|
||||
this._childReadyRegistered = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._childReadyRegistered &&
|
||||
this._registerChildPanelReady &&
|
||||
this.pages &&
|
||||
this.hass
|
||||
) {
|
||||
this._registerChildPanelReady(this._resolveVisiblePages());
|
||||
this._childReadyRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const pages = filterNavigationPages(this.hass, this.pages, {
|
||||
hasBluetoothConfigEntries: this._hasBluetoothConfigEntries,
|
||||
}).map((page) => ({
|
||||
const pages = (this._visiblePages ?? []).map((page) => ({
|
||||
...page,
|
||||
name:
|
||||
page.name ||
|
||||
@@ -75,6 +113,42 @@ class HaConfigNavigation extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _loadBluetoothEntries(): Promise<void> {
|
||||
if (!this._bluetoothEntriesLoaded) {
|
||||
this._bluetoothEntriesLoaded = getConfigEntries(this.hass, {
|
||||
domain: "bluetooth",
|
||||
})
|
||||
.then((entries) => {
|
||||
this._hasBluetoothConfigEntries = entries.length > 0;
|
||||
})
|
||||
.catch(() => {
|
||||
this._hasBluetoothConfigEntries = false;
|
||||
});
|
||||
}
|
||||
return this._bluetoothEntriesLoaded;
|
||||
}
|
||||
|
||||
private async _resolveVisiblePages(): Promise<void> {
|
||||
if (this.pages.some((page) => page.component === "bluetooth")) {
|
||||
await this._loadBluetoothEntries();
|
||||
}
|
||||
|
||||
const visiblePages = this._filterNavigationPages(
|
||||
this.hass,
|
||||
this.pages,
|
||||
this._hasBluetoothConfigEntries
|
||||
);
|
||||
const currentVisiblePages = this._visiblePages;
|
||||
if (
|
||||
!currentVisiblePages ||
|
||||
visiblePages.length !== currentVisiblePages.length ||
|
||||
visiblePages.some((page, index) => page !== currentVisiblePages[index])
|
||||
) {
|
||||
this._visiblePages = visiblePages;
|
||||
}
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
|
||||
@@ -89,6 +89,7 @@ class HaPanelConfig extends HassRouterPage {
|
||||
dashboard: {
|
||||
tag: "ha-config-dashboard",
|
||||
load: () => import("./dashboard/ha-config-dashboard"),
|
||||
waitForReady: true,
|
||||
},
|
||||
entities: {
|
||||
tag: "ha-config-entities",
|
||||
|
||||
@@ -1312,18 +1312,18 @@
|
||||
"invalid_duration": "The duration of the event is not valid. Please check start and end date.",
|
||||
"not_all_required_fields": "Not all required fields are filled in",
|
||||
"end_auto_adjusted": "Event end was adjusted to prevent negative duration",
|
||||
"recurrence_range": {
|
||||
"this_event": "Only this event",
|
||||
"this_and_future": "This and all future events"
|
||||
},
|
||||
"confirm_delete": {
|
||||
"delete": "Delete event",
|
||||
"delete_this": "Delete only this event",
|
||||
"delete_future": "Delete all future events",
|
||||
"prompt": "Do you want to delete this event?",
|
||||
"recurring_prompt": "Do you want to delete only this event, or this and all future occurrences of the event?"
|
||||
"recurring_prompt": "Which events do you want to delete?"
|
||||
},
|
||||
"confirm_update": {
|
||||
"update": "Update event",
|
||||
"update_this": "Update only this event",
|
||||
"update_future": "Update all future events",
|
||||
"recurring_prompt": "Do you want to update only this event, or this and all future occurrences of the event?"
|
||||
"recurring_prompt": "Which events do you want to update?"
|
||||
},
|
||||
"repeat": {
|
||||
"label": "Repeat",
|
||||
|
||||
Reference in New Issue
Block a user