mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Sketch out strategies (#8959)
Co-authored-by: Zack Arnett <arnett.zackary@gmail.com>
This commit is contained in:
parent
b599417a37
commit
63e10314bd
@ -35,6 +35,7 @@ class HcLovelace extends LitElement {
|
||||
}
|
||||
const lovelace: Lovelace = {
|
||||
config: this.lovelaceConfig,
|
||||
rawConfig: this.lovelaceConfig,
|
||||
editMode: false,
|
||||
urlPath: this.urlPath!,
|
||||
enableFullEditMode: () => undefined,
|
||||
|
@ -221,11 +221,17 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
|
||||
private async _generateLovelaceConfig() {
|
||||
const { generateLovelaceConfigFromHass } = await import(
|
||||
"../../../../src/panels/lovelace/common/generate-lovelace-config"
|
||||
const { generateLovelaceDashboardStrategy } = await import(
|
||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
||||
);
|
||||
this._handleNewLovelaceConfig(
|
||||
await generateLovelaceConfigFromHass(this.hass!)
|
||||
await generateLovelaceDashboardStrategy(
|
||||
{
|
||||
hass: this.hass!,
|
||||
narrow: false,
|
||||
},
|
||||
"original-states"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,10 @@ export interface LovelacePanelConfig {
|
||||
|
||||
export interface LovelaceConfig {
|
||||
title?: string;
|
||||
strategy?: {
|
||||
name: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
views: LovelaceViewConfig[];
|
||||
background?: string;
|
||||
}
|
||||
@ -77,6 +81,10 @@ export interface LovelaceViewConfig {
|
||||
index?: number;
|
||||
title?: string;
|
||||
type?: string;
|
||||
strategy?: {
|
||||
name: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
badges?: Array<string | LovelaceBadgeConfig>;
|
||||
cards?: LovelaceCardConfig[];
|
||||
path?: string;
|
||||
@ -94,6 +102,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
index?: number;
|
||||
cards?: Array<LovelaceCard | HuiErrorCard>;
|
||||
badges?: LovelaceBadge[];
|
||||
isStrategy: boolean;
|
||||
setConfig(config: LovelaceViewConfig): void;
|
||||
}
|
||||
|
||||
|
@ -31,11 +31,18 @@ export class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
return html``;
|
||||
}
|
||||
|
||||
let dumped: string | undefined;
|
||||
|
||||
if (this._config.origConfig) {
|
||||
try {
|
||||
dumped = safeDump(this._config.origConfig);
|
||||
} catch (err) {
|
||||
dumped = `[Error dumping ${this._config.origConfig}]`;
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
${this._config.error}
|
||||
${this._config.origConfig
|
||||
? html`<pre>${safeDump(this._config.origConfig)}</pre>`
|
||||
: ""}
|
||||
${this._config.error}${dumped ? html`<pre>${dumped}</pre>` : ""}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -1,41 +1,16 @@
|
||||
import {
|
||||
HassEntities,
|
||||
HassEntity,
|
||||
STATE_NOT_RUNNING,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { DEFAULT_VIEW_ENTITY_ID } from "../../../common/const";
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { extractViews } from "../../../common/entity/extract_views";
|
||||
import { getViewEntities } from "../../../common/entity/get_view_entities";
|
||||
import { splitByGroups } from "../../../common/entity/split_by_groups";
|
||||
import { compare } from "../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../../data/area_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { GroupEntity } from "../../../data/group";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
LovelaceCardConfig,
|
||||
LovelaceConfig,
|
||||
LovelaceViewConfig,
|
||||
} from "../../../data/lovelace";
|
||||
import { LovelaceCardConfig, LovelaceViewConfig } from "../../../data/lovelace";
|
||||
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../../data/sensor";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
AlarmPanelCardConfig,
|
||||
EntitiesCardConfig,
|
||||
@ -57,8 +32,6 @@ const HIDE_DOMAIN = new Set([
|
||||
|
||||
const HIDE_PLATFORM = new Set(["mobile_app"]);
|
||||
|
||||
let subscribedRegistries = false;
|
||||
|
||||
interface SplittedByAreas {
|
||||
areasWithEntities: Array<[AreaRegistryEntry, HassEntity[]]>;
|
||||
otherEntities: HassEntities;
|
||||
@ -239,7 +212,7 @@ const computeDefaultViewStates = (
|
||||
return states;
|
||||
};
|
||||
|
||||
const generateViewConfig = (
|
||||
export const generateViewConfig = (
|
||||
localize: LocalizeFunc,
|
||||
path: string,
|
||||
title: string | undefined,
|
||||
@ -373,141 +346,3 @@ export const generateDefaultViewConfig = (
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export const generateLovelaceConfigFromData = async (
|
||||
hass: HomeAssistant,
|
||||
areaEntries: AreaRegistryEntry[],
|
||||
deviceEntries: DeviceRegistryEntry[],
|
||||
entityEntries: EntityRegistryEntry[],
|
||||
entities: HassEntities,
|
||||
localize: LocalizeFunc
|
||||
): Promise<LovelaceConfig> => {
|
||||
if (hass.config.safe_mode) {
|
||||
return {
|
||||
title: hass.config.location_name,
|
||||
views: [
|
||||
{
|
||||
cards: [{ type: "safe-mode" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const viewEntities = extractViews(entities);
|
||||
|
||||
const views = viewEntities.map((viewEntity: GroupEntity) => {
|
||||
const states = getViewEntities(entities, viewEntity);
|
||||
|
||||
// In the case of a normal view, we use group order as specified in view
|
||||
const groupOrders = {};
|
||||
Object.keys(states).forEach((entityId, idx) => {
|
||||
groupOrders[entityId] = idx;
|
||||
});
|
||||
|
||||
return generateViewConfig(
|
||||
localize,
|
||||
computeObjectId(viewEntity.entity_id),
|
||||
computeStateName(viewEntity),
|
||||
viewEntity.attributes.icon,
|
||||
states,
|
||||
groupOrders
|
||||
);
|
||||
});
|
||||
|
||||
let title = hass.config.location_name;
|
||||
|
||||
// User can override default view. If they didn't, we will add one
|
||||
// that contains all entities.
|
||||
if (
|
||||
viewEntities.length === 0 ||
|
||||
viewEntities[0].entity_id !== DEFAULT_VIEW_ENTITY_ID
|
||||
) {
|
||||
views.unshift(
|
||||
generateDefaultViewConfig(
|
||||
areaEntries,
|
||||
deviceEntries,
|
||||
entityEntries,
|
||||
entities,
|
||||
localize
|
||||
)
|
||||
);
|
||||
|
||||
// Add map of geo locations to default view if loaded
|
||||
if (isComponentLoaded(hass, "geo_location")) {
|
||||
if (views[0] && views[0].cards) {
|
||||
views[0].cards.push({
|
||||
type: "map",
|
||||
geo_location_sources: ["all"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we don't have Home as title and first tab.
|
||||
if (views.length > 1 && title === "Home") {
|
||||
title = "Home Assistant";
|
||||
}
|
||||
}
|
||||
|
||||
// User has no entities
|
||||
if (views.length === 1 && views[0].cards!.length === 0) {
|
||||
views[0].cards!.push({
|
||||
type: "empty-state",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
views,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateLovelaceConfigFromHass = async (
|
||||
hass: HomeAssistant,
|
||||
localize?: LocalizeFunc
|
||||
): Promise<LovelaceConfig> => {
|
||||
if (hass.config.state === STATE_NOT_RUNNING) {
|
||||
return {
|
||||
title: hass.config.location_name,
|
||||
views: [
|
||||
{
|
||||
cards: [{ type: "starting" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (hass.config.safe_mode) {
|
||||
return {
|
||||
title: hass.config.location_name,
|
||||
views: [
|
||||
{
|
||||
cards: [{ type: "safe-mode" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// We want to keep the registry subscriptions alive after generating the UI
|
||||
// so that we don't serve up stale data after changing areas.
|
||||
if (!subscribedRegistries) {
|
||||
subscribedRegistries = true;
|
||||
subscribeAreaRegistry(hass.connection, () => undefined);
|
||||
subscribeDeviceRegistry(hass.connection, () => undefined);
|
||||
subscribeEntityRegistry(hass.connection, () => undefined);
|
||||
}
|
||||
|
||||
const [areaEntries, deviceEntries, entityEntries] = await Promise.all([
|
||||
subscribeOne(hass.connection, subscribeAreaRegistry),
|
||||
subscribeOne(hass.connection, subscribeDeviceRegistry),
|
||||
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||
]);
|
||||
|
||||
return generateLovelaceConfigFromData(
|
||||
hass,
|
||||
areaEntries,
|
||||
deviceEntries,
|
||||
entityEntries,
|
||||
hass.states,
|
||||
localize || hass.localize
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
LovelaceViewConfig,
|
||||
LovelaceViewElement,
|
||||
} from "../../../data/lovelace";
|
||||
import { HuiErrorCard } from "../cards/hui-error-card";
|
||||
import "../views/hui-masonry-view";
|
||||
import { createLovelaceElement } from "./create-element-base";
|
||||
|
||||
@ -13,7 +14,7 @@ const LAZY_LOAD_LAYOUTS = {
|
||||
|
||||
export const createViewElement = (
|
||||
config: LovelaceViewConfig
|
||||
): LovelaceViewElement => {
|
||||
): LovelaceViewElement | HuiErrorCard => {
|
||||
return createLovelaceElement(
|
||||
"view",
|
||||
config,
|
||||
|
@ -19,13 +19,15 @@ import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import type { LovelaceConfig } from "../../../data/lovelace";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { expandLovelaceConfigStrategies } from "../strategies/get-strategy";
|
||||
import type { SaveDialogParams } from "./show-save-config-dialog";
|
||||
|
||||
const EMPTY_CONFIG = { views: [] };
|
||||
const EMPTY_CONFIG: LovelaceConfig = { views: [{ title: "Home" }] };
|
||||
|
||||
@customElement("hui-dialog-save-config")
|
||||
export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
@ -125,14 +127,17 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
</div>
|
||||
${this._params.mode === "storage"
|
||||
? html`
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}
|
||||
>${this.hass!.localize(
|
||||
"ui.common.cancel"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.label=${this.hass!.localize("ui.common.cancel")}
|
||||
@click=${this.closeDialog}
|
||||
></mwc-button>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
?disabled=${this._saving}
|
||||
aria-label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.save"
|
||||
)}
|
||||
@click=${this._saveConfig}
|
||||
>
|
||||
${this._saving
|
||||
@ -148,11 +153,13 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}
|
||||
>${this.hass!.localize(
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.close"
|
||||
)}
|
||||
</mwc-button>
|
||||
@click=${this.closeDialog}
|
||||
></mwc-button>
|
||||
`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
@ -177,7 +184,13 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
try {
|
||||
const lovelace = this._params!.lovelace;
|
||||
await lovelace.saveConfig(
|
||||
this._emptyConfig ? EMPTY_CONFIG : lovelace.config
|
||||
this._emptyConfig
|
||||
? EMPTY_CONFIG
|
||||
: await expandLovelaceConfigStrategies({
|
||||
config: lovelace.config,
|
||||
hass: this.hass!,
|
||||
narrow: this._params!.narrow,
|
||||
})
|
||||
);
|
||||
lovelace.setEditMode(true);
|
||||
this._saving = false;
|
||||
|
@ -14,6 +14,7 @@ const dialogTag = "hui-dialog-save-config";
|
||||
export interface SaveDialogParams {
|
||||
lovelace: Lovelace;
|
||||
mode: "yaml" | "storage";
|
||||
narrow: boolean;
|
||||
}
|
||||
|
||||
let registeredDialog = false;
|
||||
|
@ -7,6 +7,11 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
addSearchParam,
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
deleteConfig,
|
||||
@ -21,14 +26,16 @@ import "../../layouts/hass-error-screen";
|
||||
import "../../layouts/hass-loading-screen";
|
||||
import { HomeAssistant, PanelInfo, Route } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import { generateLovelaceConfigFromHass } from "./common/generate-lovelace-config";
|
||||
import { loadLovelaceResources } from "./common/load-resources";
|
||||
import { showSaveDialog } from "./editor/show-save-config-dialog";
|
||||
import "./hui-root";
|
||||
import { generateLovelaceDashboardStrategy } from "./strategies/get-strategy";
|
||||
import { Lovelace } from "./types";
|
||||
|
||||
(window as any).loadCardHelpers = () => import("./custom-card-helpers");
|
||||
|
||||
const DEFAULT_STRATEGY = "original-states";
|
||||
|
||||
interface LovelacePanelConfig {
|
||||
mode: "yaml" | "storage";
|
||||
}
|
||||
@ -71,7 +78,11 @@ class LovelacePanel extends LitElement {
|
||||
this.lovelace.locale !== this.hass.locale
|
||||
) {
|
||||
// language has been changed, rebuild UI
|
||||
this._setLovelaceConfig(this.lovelace.config, this.lovelace.mode);
|
||||
this._setLovelaceConfig(
|
||||
this.lovelace.config,
|
||||
this.lovelace.rawConfig,
|
||||
this.lovelace.mode
|
||||
);
|
||||
} else if (this.lovelace && this.lovelace.mode === "generated") {
|
||||
// When lovelace is generated, we re-generate each time a user goes
|
||||
// to the states panel to make sure new entities are shown.
|
||||
@ -139,7 +150,9 @@ class LovelacePanel extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._fetchConfig(false);
|
||||
if (!this._unsubUpdates) {
|
||||
this._subscribeUpdates();
|
||||
@ -153,8 +166,14 @@ class LovelacePanel extends LitElement {
|
||||
}
|
||||
|
||||
private async _regenerateConfig() {
|
||||
const conf = await generateLovelaceConfigFromHass(this.hass!);
|
||||
this._setLovelaceConfig(conf, "generated");
|
||||
const conf = await generateLovelaceDashboardStrategy(
|
||||
{
|
||||
hass: this.hass!,
|
||||
narrow: this.narrow,
|
||||
},
|
||||
DEFAULT_STRATEGY
|
||||
);
|
||||
this._setLovelaceConfig(conf, undefined, "generated");
|
||||
this._state = "loaded";
|
||||
}
|
||||
|
||||
@ -202,6 +221,7 @@ class LovelacePanel extends LitElement {
|
||||
|
||||
private async _fetchConfig(forceDiskRefresh: boolean) {
|
||||
let conf: LovelaceConfig;
|
||||
let rawConf: LovelaceConfig | undefined;
|
||||
let confMode: Lovelace["mode"] = this.panel!.config.mode;
|
||||
let confProm: Promise<LovelaceConfig> | undefined;
|
||||
const llWindow = window as WindowWithLovelaceProm;
|
||||
@ -236,7 +256,18 @@ class LovelacePanel extends LitElement {
|
||||
}
|
||||
|
||||
try {
|
||||
conf = await confProm!;
|
||||
rawConf = await confProm!;
|
||||
|
||||
// If strategy defined, apply it here.
|
||||
if (rawConf.strategy) {
|
||||
conf = await generateLovelaceDashboardStrategy({
|
||||
config: rawConf,
|
||||
hass: this.hass!,
|
||||
narrow: this.narrow,
|
||||
});
|
||||
} else {
|
||||
conf = rawConf;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code !== "config_not_found") {
|
||||
// eslint-disable-next-line
|
||||
@ -245,8 +276,13 @@ class LovelacePanel extends LitElement {
|
||||
this._errorMsg = err.message;
|
||||
return;
|
||||
}
|
||||
const localize = await this.hass!.loadBackendTranslation("title");
|
||||
conf = await generateLovelaceConfigFromHass(this.hass!, localize);
|
||||
conf = await generateLovelaceDashboardStrategy(
|
||||
{
|
||||
hass: this.hass!,
|
||||
narrow: this.narrow,
|
||||
},
|
||||
DEFAULT_STRATEGY
|
||||
);
|
||||
confMode = "generated";
|
||||
} finally {
|
||||
// Ignore updates for another 2 seconds.
|
||||
@ -258,7 +294,7 @@ class LovelacePanel extends LitElement {
|
||||
}
|
||||
|
||||
this._state = this._state === "yaml-editor" ? this._state : "loaded";
|
||||
this._setLovelaceConfig(conf, confMode);
|
||||
this._setLovelaceConfig(conf, rawConf, confMode);
|
||||
}
|
||||
|
||||
private _checkLovelaceConfig(config: LovelaceConfig) {
|
||||
@ -277,11 +313,16 @@ class LovelacePanel extends LitElement {
|
||||
return checkedConfig ? deepFreeze(checkedConfig) : config;
|
||||
}
|
||||
|
||||
private _setLovelaceConfig(config: LovelaceConfig, mode: Lovelace["mode"]) {
|
||||
private _setLovelaceConfig(
|
||||
config: LovelaceConfig,
|
||||
rawConfig: LovelaceConfig | undefined,
|
||||
mode: Lovelace["mode"]
|
||||
) {
|
||||
config = this._checkLovelaceConfig(config);
|
||||
const urlPath = this.urlPath;
|
||||
this.lovelace = {
|
||||
config,
|
||||
rawConfig,
|
||||
mode,
|
||||
urlPath: this.urlPath,
|
||||
editMode: this.lovelace ? this.lovelace.editMode : false,
|
||||
@ -294,22 +335,39 @@ class LovelacePanel extends LitElement {
|
||||
this._state = "yaml-editor";
|
||||
},
|
||||
setEditMode: (editMode: boolean) => {
|
||||
// If we use a strategy for dashboard, we cannot show the edit UI
|
||||
// So go straight to the YAML editor
|
||||
if (
|
||||
this.lovelace!.rawConfig &&
|
||||
this.lovelace!.rawConfig !== this.lovelace!.config
|
||||
) {
|
||||
this.lovelace!.enableFullEditMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editMode || this.lovelace!.mode !== "generated") {
|
||||
this._updateLovelace({ editMode });
|
||||
return;
|
||||
}
|
||||
|
||||
showSaveDialog(this, {
|
||||
lovelace: this.lovelace!,
|
||||
mode: this.panel!.config.mode,
|
||||
narrow: this.narrow!,
|
||||
});
|
||||
},
|
||||
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
||||
const { config: previousConfig, mode: previousMode } = this.lovelace!;
|
||||
const {
|
||||
config: previousConfig,
|
||||
rawConfig: previousRawConfig,
|
||||
mode: previousMode,
|
||||
} = this.lovelace!;
|
||||
newConfig = this._checkLovelaceConfig(newConfig);
|
||||
try {
|
||||
// Optimistic update
|
||||
this._updateLovelace({
|
||||
config: newConfig,
|
||||
rawConfig: undefined,
|
||||
mode: "storage",
|
||||
});
|
||||
this._ignoreNextUpdateEvent = true;
|
||||
@ -320,18 +378,30 @@ class LovelacePanel extends LitElement {
|
||||
// Rollback the optimistic update
|
||||
this._updateLovelace({
|
||||
config: previousConfig,
|
||||
rawConfig: previousRawConfig,
|
||||
mode: previousMode,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
deleteConfig: async (): Promise<void> => {
|
||||
const { config: previousConfig, mode: previousMode } = this.lovelace!;
|
||||
const {
|
||||
config: previousConfig,
|
||||
rawConfig: previousRawConfig,
|
||||
mode: previousMode,
|
||||
} = this.lovelace!;
|
||||
try {
|
||||
// Optimistic update
|
||||
const localize = await this.hass!.loadBackendTranslation("title");
|
||||
const generatedConf = await generateLovelaceDashboardStrategy(
|
||||
{
|
||||
hass: this.hass!,
|
||||
narrow: this.narrow,
|
||||
},
|
||||
DEFAULT_STRATEGY
|
||||
);
|
||||
this._updateLovelace({
|
||||
config: await generateLovelaceConfigFromHass(this.hass!, localize),
|
||||
config: generatedConf,
|
||||
rawConfig: undefined,
|
||||
mode: "generated",
|
||||
editMode: false,
|
||||
});
|
||||
@ -343,6 +413,7 @@ class LovelacePanel extends LitElement {
|
||||
// Rollback the optimistic update
|
||||
this._updateLovelace({
|
||||
config: previousConfig,
|
||||
rawConfig: previousRawConfig,
|
||||
mode: previousMode,
|
||||
});
|
||||
throw err;
|
||||
@ -356,6 +427,18 @@ class LovelacePanel extends LitElement {
|
||||
...this.lovelace!,
|
||||
...props,
|
||||
};
|
||||
|
||||
if ("editMode" in props) {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(
|
||||
props.editMode
|
||||
? addSearchParam({ edit: "1" })
|
||||
: removeSearchParam("edit")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.yamlEditor.value = safeDump(this.lovelace!.config);
|
||||
this.yamlEditor.value = safeDump(this.lovelace!.rawConfig);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
|
@ -43,9 +43,7 @@ import { navigate } from "../../common/navigate";
|
||||
import {
|
||||
addSearchParam,
|
||||
extractSearchParam,
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import { computeRTLDirection } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
@ -539,7 +537,7 @@ class HUIRoot extends LitElement {
|
||||
protected firstUpdated() {
|
||||
// Check for requested edit mode
|
||||
if (extractSearchParam("edit") === "1") {
|
||||
this._enableEditMode();
|
||||
this.lovelace!.setEditMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -715,25 +713,11 @@ class HUIRoot extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._enableEditMode();
|
||||
}
|
||||
|
||||
private _enableEditMode(): void {
|
||||
this.lovelace!.setEditMode(true);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(addSearchParam({ edit: "1" }))
|
||||
);
|
||||
}
|
||||
|
||||
private _editModeDisable(): void {
|
||||
this.lovelace!.setEditMode(false);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
constructUrlCurrentPath(removeSearchParam("edit"))
|
||||
);
|
||||
}
|
||||
|
||||
private _editLovelace() {
|
||||
@ -837,7 +821,7 @@ class HUIRoot extends LitElement {
|
||||
const viewConfig = this.config.views[viewIndex];
|
||||
|
||||
if (!viewConfig) {
|
||||
this._enableEditMode();
|
||||
this.lovelace!.setEditMode(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
158
src/panels/lovelace/strategies/get-strategy.ts
Normal file
158
src/panels/lovelace/strategies/get-strategy.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { LovelaceConfig, LovelaceViewConfig } from "../../../data/lovelace";
|
||||
import { AsyncReturnType, HomeAssistant } from "../../../types";
|
||||
import { OriginalStatesStrategy } from "./original-states-strategy";
|
||||
|
||||
const MAX_WAIT_STRATEGY_LOAD = 5000;
|
||||
const CUSTOM_PREFIX = "custom:";
|
||||
|
||||
export interface LovelaceDashboardStrategy {
|
||||
generateDashboard(info: {
|
||||
config?: LovelaceConfig;
|
||||
hass: HomeAssistant;
|
||||
narrow: boolean | undefined;
|
||||
}): Promise<LovelaceConfig>;
|
||||
}
|
||||
|
||||
export interface LovelaceViewStrategy {
|
||||
generateView(info: {
|
||||
view: LovelaceViewConfig;
|
||||
config: LovelaceConfig;
|
||||
hass: HomeAssistant;
|
||||
narrow: boolean | undefined;
|
||||
}): Promise<LovelaceViewConfig>;
|
||||
}
|
||||
|
||||
const strategies: Record<
|
||||
string,
|
||||
LovelaceDashboardStrategy & LovelaceViewStrategy
|
||||
> = {
|
||||
"original-states": OriginalStatesStrategy,
|
||||
};
|
||||
|
||||
const getLovelaceStrategy = async <
|
||||
T extends LovelaceDashboardStrategy | LovelaceViewStrategy
|
||||
>(
|
||||
name: string
|
||||
): Promise<T> => {
|
||||
if (name in strategies) {
|
||||
return strategies[name] as T;
|
||||
}
|
||||
|
||||
if (!name.startsWith(CUSTOM_PREFIX)) {
|
||||
throw new Error("Unknown strategy");
|
||||
}
|
||||
|
||||
const tag = `ll-strategy-${name.substr(CUSTOM_PREFIX.length)}`;
|
||||
|
||||
if (
|
||||
(await Promise.race([
|
||||
customElements.whenDefined(tag),
|
||||
new Promise((resolve) =>
|
||||
setTimeout(() => resolve(true), MAX_WAIT_STRATEGY_LOAD)
|
||||
),
|
||||
])) === true
|
||||
) {
|
||||
throw new Error(
|
||||
`Timeout waiting for strategy element ${tag} to be registered`
|
||||
);
|
||||
}
|
||||
|
||||
return customElements.get(tag);
|
||||
};
|
||||
|
||||
interface GenerateMethods {
|
||||
generateDashboard: LovelaceDashboardStrategy["generateDashboard"];
|
||||
generateView: LovelaceViewStrategy["generateView"];
|
||||
}
|
||||
|
||||
const generateStrategy = async <T extends keyof GenerateMethods>(
|
||||
generateMethod: T,
|
||||
renderError: (err: string | Error) => AsyncReturnType<GenerateMethods[T]>,
|
||||
info: Parameters<GenerateMethods[T]>[0],
|
||||
name: string | undefined
|
||||
): Promise<ReturnType<GenerateMethods[T]>> => {
|
||||
if (!name) {
|
||||
return renderError("No strategy name found");
|
||||
}
|
||||
|
||||
try {
|
||||
const strategy = (await getLovelaceStrategy(name)) as any;
|
||||
return await strategy[generateMethod](info);
|
||||
} catch (err) {
|
||||
if (err.message !== "timeout") {
|
||||
// eslint-disable-next-line
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return renderError(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const generateLovelaceDashboardStrategy = async (
|
||||
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0],
|
||||
name?: string
|
||||
): ReturnType<LovelaceDashboardStrategy["generateDashboard"]> =>
|
||||
generateStrategy(
|
||||
"generateDashboard",
|
||||
(err) => ({
|
||||
views: [
|
||||
{
|
||||
title: "Error",
|
||||
cards: [
|
||||
{
|
||||
type: "markdown",
|
||||
content: `Error loading the dashboard strategy:\n> ${err}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
info,
|
||||
name || info.config?.strategy?.name
|
||||
);
|
||||
|
||||
export const generateLovelaceViewStrategy = async (
|
||||
info: Parameters<LovelaceViewStrategy["generateView"]>[0],
|
||||
name?: string
|
||||
): ReturnType<LovelaceViewStrategy["generateView"]> =>
|
||||
generateStrategy(
|
||||
"generateView",
|
||||
(err) => ({
|
||||
cards: [
|
||||
{
|
||||
type: "markdown",
|
||||
content: `Error loading the view strategy:\n> ${err}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
info,
|
||||
name || info.view?.strategy?.name
|
||||
);
|
||||
|
||||
/**
|
||||
* Find all references to strategies and replaces them with the generated output
|
||||
*/
|
||||
export const expandLovelaceConfigStrategies = async (
|
||||
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0] & {
|
||||
config: LovelaceConfig;
|
||||
}
|
||||
): Promise<LovelaceConfig> => {
|
||||
const config = info.config.strategy
|
||||
? await generateLovelaceDashboardStrategy(info)
|
||||
: { ...info.config };
|
||||
|
||||
config.views = await Promise.all(
|
||||
config.views.map((view) =>
|
||||
view.strategy
|
||||
? generateLovelaceViewStrategy({
|
||||
hass: info.hass,
|
||||
narrow: info.narrow,
|
||||
config,
|
||||
view,
|
||||
})
|
||||
: view
|
||||
)
|
||||
);
|
||||
|
||||
return config;
|
||||
};
|
94
src/panels/lovelace/strategies/original-states-strategy.ts
Normal file
94
src/panels/lovelace/strategies/original-states-strategy.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { STATE_NOT_RUNNING } from "home-assistant-js-websocket";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import { subscribeAreaRegistry } from "../../../data/area_registry";
|
||||
import { subscribeDeviceRegistry } from "../../../data/device_registry";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
|
||||
import {
|
||||
LovelaceViewStrategy,
|
||||
LovelaceDashboardStrategy,
|
||||
} from "./get-strategy";
|
||||
|
||||
let subscribedRegistries = false;
|
||||
|
||||
export class OriginalStatesStrategy {
|
||||
static async generateView(
|
||||
info: Parameters<LovelaceViewStrategy["generateView"]>[0]
|
||||
): ReturnType<LovelaceViewStrategy["generateView"]> {
|
||||
const hass = info.hass;
|
||||
|
||||
if (hass.config.state === STATE_NOT_RUNNING) {
|
||||
return {
|
||||
cards: [{ type: "starting" }],
|
||||
};
|
||||
}
|
||||
|
||||
if (hass.config.safe_mode) {
|
||||
return {
|
||||
cards: [{ type: "safe-mode" }],
|
||||
};
|
||||
}
|
||||
|
||||
// We leave this here so we always have the freshest data.
|
||||
if (!subscribedRegistries) {
|
||||
subscribedRegistries = true;
|
||||
subscribeAreaRegistry(hass.connection, () => undefined);
|
||||
subscribeDeviceRegistry(hass.connection, () => undefined);
|
||||
subscribeEntityRegistry(hass.connection, () => undefined);
|
||||
}
|
||||
|
||||
const [
|
||||
areaEntries,
|
||||
deviceEntries,
|
||||
entityEntries,
|
||||
localize,
|
||||
] = await Promise.all([
|
||||
subscribeOne(hass.connection, subscribeAreaRegistry),
|
||||
subscribeOne(hass.connection, subscribeDeviceRegistry),
|
||||
subscribeOne(hass.connection, subscribeEntityRegistry),
|
||||
hass.loadBackendTranslation("title"),
|
||||
]);
|
||||
|
||||
// User can override default view. If they didn't, we will add one
|
||||
// that contains all entities.
|
||||
const view = generateDefaultViewConfig(
|
||||
areaEntries,
|
||||
deviceEntries,
|
||||
entityEntries,
|
||||
hass.states,
|
||||
localize
|
||||
);
|
||||
|
||||
// Add map of geo locations to default view if loaded
|
||||
if (hass.config.components.includes("geo_location")) {
|
||||
if (view && view.cards) {
|
||||
view.cards.push({
|
||||
type: "map",
|
||||
geo_location_sources: ["all"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// User has no entities
|
||||
if (view.cards!.length === 0) {
|
||||
view.cards!.push({
|
||||
type: "empty-state",
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
static async generateDashboard(
|
||||
info: Parameters<LovelaceDashboardStrategy["generateDashboard"]>[0]
|
||||
): ReturnType<LovelaceDashboardStrategy["generateDashboard"]> {
|
||||
return {
|
||||
views: [
|
||||
{
|
||||
strategy: { name: "original-states" },
|
||||
title: info.hass.config.location_name,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
@ -18,6 +18,8 @@ declare global {
|
||||
|
||||
export interface Lovelace {
|
||||
config: LovelaceConfig;
|
||||
// If not set, a strategy was used to generate everything
|
||||
rawConfig: LovelaceConfig | undefined;
|
||||
editMode: boolean;
|
||||
urlPath: string | null;
|
||||
mode: "generated" | "yaml" | "storage";
|
||||
|
@ -53,6 +53,8 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
|
||||
@property({ type: Boolean }) public isStrategy = false;
|
||||
|
||||
@property({ attribute: false }) public cards: Array<
|
||||
LovelaceCard | HuiErrorCard
|
||||
> = [];
|
||||
@ -228,7 +230,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
private _addCardToColumn(columnEl, index, editMode) {
|
||||
const card: LovelaceCard = this.cards[index];
|
||||
if (!editMode) {
|
||||
if (!editMode || this.isStrategy) {
|
||||
card.editMode = false;
|
||||
columnEl.appendChild(card);
|
||||
} else {
|
||||
|
@ -31,6 +31,8 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
|
||||
@property({ type: Boolean }) public isStrategy = false;
|
||||
|
||||
@property({ attribute: false }) public cards: Array<
|
||||
LovelaceCard | HuiErrorCard
|
||||
> = [];
|
||||
@ -109,7 +111,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
const card: LovelaceCard = this.cards[0];
|
||||
card.isPanel = true;
|
||||
|
||||
if (!this.lovelace?.editMode) {
|
||||
if (this.isStrategy || !this.lovelace?.editMode) {
|
||||
card.editMode = false;
|
||||
this._card = card;
|
||||
return;
|
||||
|
@ -23,6 +23,7 @@ import { createViewElement } from "../create-element/create-view-element";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { confDeleteCard } from "../editor/delete-card";
|
||||
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
|
||||
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
||||
|
||||
const DEFAULT_VIEW_LAYOUT = "masonry";
|
||||
@ -55,6 +56,8 @@ export class HUIView extends UpdatingElement {
|
||||
|
||||
private _layoutElement?: LovelaceViewElement;
|
||||
|
||||
private _viewConfigTheme?: string;
|
||||
|
||||
// Public to make demo happy
|
||||
public createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||
@ -100,51 +103,21 @@ export class HUIView extends UpdatingElement {
|
||||
*/
|
||||
|
||||
const oldLovelace = changedProperties.get("lovelace") as this["lovelace"];
|
||||
const configChanged =
|
||||
|
||||
// If config has changed, create element if necessary and set all values.
|
||||
if (
|
||||
changedProperties.has("index") ||
|
||||
(changedProperties.has("lovelace") &&
|
||||
(!oldLovelace ||
|
||||
this.lovelace.config.views[this.index] !==
|
||||
oldLovelace.config.views[this.index]));
|
||||
oldLovelace.config.views[this.index]))
|
||||
) {
|
||||
this._initializeConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
// If config has changed, create element if necessary and set all values.
|
||||
if (configChanged) {
|
||||
let viewConfig = this.lovelace.config.views[this.index];
|
||||
viewConfig = {
|
||||
...viewConfig,
|
||||
type: viewConfig.panel
|
||||
? PANEL_VIEW_LAYOUT
|
||||
: viewConfig.type || DEFAULT_VIEW_LAYOUT,
|
||||
};
|
||||
|
||||
this._createBadges(viewConfig!);
|
||||
this._createCards(viewConfig!);
|
||||
|
||||
// Create a new layout element if necessary.
|
||||
let addLayoutElement = false;
|
||||
|
||||
if (
|
||||
!this._layoutElement ||
|
||||
this._layoutElementType !== viewConfig!.type
|
||||
) {
|
||||
addLayoutElement = true;
|
||||
this._createLayoutElement(viewConfig!);
|
||||
}
|
||||
|
||||
this._layoutElement!.hass = this.hass;
|
||||
this._layoutElement!.narrow = this.narrow;
|
||||
this._layoutElement!.lovelace = this.lovelace;
|
||||
this._layoutElement!.index = this.index;
|
||||
this._layoutElement!.cards = this._cards;
|
||||
this._layoutElement!.badges = this._badges;
|
||||
|
||||
if (addLayoutElement) {
|
||||
while (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this.appendChild(this._layoutElement!);
|
||||
}
|
||||
} else {
|
||||
// If no layout element, we're still creating one
|
||||
if (this._layoutElement) {
|
||||
// Config has not changed. Just props
|
||||
if (changedProperties.has("hass")) {
|
||||
this._badges.forEach((badge) => {
|
||||
@ -155,56 +128,98 @@ export class HUIView extends UpdatingElement {
|
||||
element.hass = this.hass;
|
||||
});
|
||||
|
||||
this._layoutElement!.hass = this.hass;
|
||||
this._layoutElement.hass = this.hass;
|
||||
}
|
||||
if (changedProperties.has("narrow")) {
|
||||
this._layoutElement!.narrow = this.narrow;
|
||||
this._layoutElement.narrow = this.narrow;
|
||||
}
|
||||
if (changedProperties.has("lovelace")) {
|
||||
this._layoutElement!.lovelace = this.lovelace;
|
||||
this._layoutElement.lovelace = this.lovelace;
|
||||
}
|
||||
if (changedProperties.has("_cards")) {
|
||||
this._layoutElement!.cards = this._cards;
|
||||
this._layoutElement.cards = this._cards;
|
||||
}
|
||||
if (changedProperties.has("_badges")) {
|
||||
this._layoutElement!.badges = this._badges;
|
||||
this._layoutElement.badges = this._badges;
|
||||
}
|
||||
}
|
||||
|
||||
const oldHass = changedProperties.get("hass") as this["hass"] | undefined;
|
||||
|
||||
// Update theme if necessary:
|
||||
// - If config changed, the theme could have changed
|
||||
// - if hass themes preferences have changed
|
||||
if (
|
||||
configChanged ||
|
||||
(changedProperties.has("hass") &&
|
||||
(!oldHass ||
|
||||
this.hass.themes !== oldHass.themes ||
|
||||
this.hass.selectedTheme !== oldHass.selectedTheme))
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass ||
|
||||
this.hass.themes !== oldHass.themes ||
|
||||
this.hass.selectedTheme !== oldHass.selectedTheme)
|
||||
) {
|
||||
applyThemesOnElement(
|
||||
this,
|
||||
this.hass.themes,
|
||||
this.lovelace.config.views[this.index].theme
|
||||
);
|
||||
applyThemesOnElement(this, this.hass.themes, this._viewConfigTheme);
|
||||
}
|
||||
}
|
||||
|
||||
private async _initializeConfig() {
|
||||
let viewConfig = this.lovelace.config.views[this.index];
|
||||
let isStrategy = false;
|
||||
|
||||
if (viewConfig.strategy) {
|
||||
isStrategy = true;
|
||||
viewConfig = await generateLovelaceViewStrategy({
|
||||
hass: this.hass,
|
||||
config: this.lovelace.config,
|
||||
narrow: this.narrow,
|
||||
view: viewConfig,
|
||||
});
|
||||
}
|
||||
|
||||
viewConfig = {
|
||||
...viewConfig,
|
||||
type: viewConfig.panel
|
||||
? PANEL_VIEW_LAYOUT
|
||||
: viewConfig.type || DEFAULT_VIEW_LAYOUT,
|
||||
};
|
||||
|
||||
// Create a new layout element if necessary.
|
||||
let addLayoutElement = false;
|
||||
|
||||
if (!this._layoutElement || this._layoutElementType !== viewConfig.type) {
|
||||
addLayoutElement = true;
|
||||
this._createLayoutElement(viewConfig);
|
||||
}
|
||||
|
||||
this._createBadges(viewConfig);
|
||||
this._createCards(viewConfig);
|
||||
this._layoutElement!.isStrategy = isStrategy;
|
||||
this._layoutElement!.hass = this.hass;
|
||||
this._layoutElement!.narrow = this.narrow;
|
||||
this._layoutElement!.lovelace = this.lovelace;
|
||||
this._layoutElement!.index = this.index;
|
||||
this._layoutElement!.cards = this._cards;
|
||||
this._layoutElement!.badges = this._badges;
|
||||
|
||||
applyThemesOnElement(this, this.hass.themes, viewConfig.theme);
|
||||
this._viewConfigTheme = viewConfig.theme;
|
||||
|
||||
if (addLayoutElement) {
|
||||
while (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this.appendChild(this._layoutElement!);
|
||||
}
|
||||
}
|
||||
|
||||
private _createLayoutElement(config: LovelaceViewConfig): void {
|
||||
this._layoutElement = createViewElement(config);
|
||||
this._layoutElement = createViewElement(config) as LovelaceViewElement;
|
||||
this._layoutElementType = config.type;
|
||||
this._layoutElement.addEventListener("ll-create-card", () => {
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: ev.detail.path,
|
||||
});
|
||||
});
|
||||
|
@ -256,3 +256,12 @@ export interface LocalizeMixin {
|
||||
hass?: HomeAssistant;
|
||||
localize: LocalizeFunc;
|
||||
}
|
||||
|
||||
// https://www.jpwilliams.dev/how-to-unpack-the-return-type-of-a-promise-in-typescript
|
||||
export type AsyncReturnType<T extends (...args: any) => any> = T extends (
|
||||
...args: any
|
||||
) => Promise<infer U>
|
||||
? U
|
||||
: T extends (...args: any) => infer U
|
||||
? U
|
||||
: never;
|
||||
|
Loading…
x
Reference in New Issue
Block a user