mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
20241127.1 (#23045)
This commit is contained in:
commit
6e003907fb
@ -9,7 +9,6 @@ const outDir = join(paths.build_dir, "locale-data");
|
|||||||
|
|
||||||
const INTL_POLYFILLS = {
|
const INTL_POLYFILLS = {
|
||||||
"intl-datetimeformat": "DateTimeFormat",
|
"intl-datetimeformat": "DateTimeFormat",
|
||||||
"intl-durationFormat": "DurationFormat",
|
|
||||||
"intl-displaynames": "DisplayNames",
|
"intl-displaynames": "DisplayNames",
|
||||||
"intl-listformat": "ListFormat",
|
"intl-listformat": "ListFormat",
|
||||||
"intl-numberformat": "NumberFormat",
|
"intl-numberformat": "NumberFormat",
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20241127.0"
|
version = "20241127.1"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { DurationFormat } from "@formatjs/intl-durationformat";
|
|
||||||
import type { DurationInput } from "@formatjs/intl-durationformat/src/types";
|
import type { DurationInput } from "@formatjs/intl-durationformat/src/types";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import type { HaDurationData } from "../../components/ha-duration-input";
|
import type { HaDurationData } from "../../components/ha-duration-input";
|
||||||
@ -49,7 +48,7 @@ export const formatNumericDuration = (
|
|||||||
|
|
||||||
const formatDurationLongMem = memoizeOne(
|
const formatDurationLongMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new DurationFormat(locale.language, {
|
new Intl.DurationFormat(locale.language, {
|
||||||
style: "long",
|
style: "long",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -61,7 +60,7 @@ export const formatDurationLong = (
|
|||||||
|
|
||||||
const formatDigitalDurationMem = memoizeOne(
|
const formatDigitalDurationMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new DurationFormat(locale.language, {
|
new Intl.DurationFormat(locale.language, {
|
||||||
style: "digital",
|
style: "digital",
|
||||||
hoursDisplay: "auto",
|
hoursDisplay: "auto",
|
||||||
})
|
})
|
||||||
@ -72,13 +71,13 @@ export const formatDurationDigital = (
|
|||||||
duration: HaDurationData
|
duration: HaDurationData
|
||||||
) => formatDigitalDurationMem(locale).format(duration);
|
) => formatDigitalDurationMem(locale).format(duration);
|
||||||
|
|
||||||
export const DURATION_UNITS = ["ms", "s", "min", "h", "d"] as const;
|
export const DURATION_UNITS = ["s", "min", "h", "d"] as const;
|
||||||
|
|
||||||
type DurationUnit = (typeof DURATION_UNITS)[number];
|
type DurationUnit = (typeof DURATION_UNITS)[number];
|
||||||
|
|
||||||
const formatDurationDayMem = memoizeOne(
|
const formatDurationDayMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new DurationFormat(locale.language, {
|
new Intl.DurationFormat(locale.language, {
|
||||||
style: "narrow",
|
style: "narrow",
|
||||||
daysDisplay: "always",
|
daysDisplay: "always",
|
||||||
})
|
})
|
||||||
@ -86,7 +85,7 @@ const formatDurationDayMem = memoizeOne(
|
|||||||
|
|
||||||
const formatDurationHourMem = memoizeOne(
|
const formatDurationHourMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new DurationFormat(locale.language, {
|
new Intl.DurationFormat(locale.language, {
|
||||||
style: "narrow",
|
style: "narrow",
|
||||||
hoursDisplay: "always",
|
hoursDisplay: "always",
|
||||||
})
|
})
|
||||||
@ -94,7 +93,7 @@ const formatDurationHourMem = memoizeOne(
|
|||||||
|
|
||||||
const formatDurationMinuteMem = memoizeOne(
|
const formatDurationMinuteMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new DurationFormat(locale.language, {
|
new Intl.DurationFormat(locale.language, {
|
||||||
style: "narrow",
|
style: "narrow",
|
||||||
minutesDisplay: "always",
|
minutesDisplay: "always",
|
||||||
})
|
})
|
||||||
@ -102,20 +101,12 @@ const formatDurationMinuteMem = memoizeOne(
|
|||||||
|
|
||||||
const formatDurationSecondMem = memoizeOne(
|
const formatDurationSecondMem = memoizeOne(
|
||||||
(locale: FrontendLocaleData) =>
|
(locale: FrontendLocaleData) =>
|
||||||
new DurationFormat(locale.language, {
|
new Intl.DurationFormat(locale.language, {
|
||||||
style: "narrow",
|
style: "narrow",
|
||||||
secondsDisplay: "always",
|
secondsDisplay: "always",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatDurationMillisecondMem = memoizeOne(
|
|
||||||
(locale: FrontendLocaleData) =>
|
|
||||||
new DurationFormat(locale.language, {
|
|
||||||
style: "narrow",
|
|
||||||
millisecondsDisplay: "always",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const formatDuration = (
|
export const formatDuration = (
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
duration: string,
|
duration: string,
|
||||||
@ -164,13 +155,6 @@ export const formatDuration = (
|
|||||||
};
|
};
|
||||||
return formatDurationSecondMem(locale).format(input);
|
return formatDurationSecondMem(locale).format(input);
|
||||||
}
|
}
|
||||||
case "ms": {
|
|
||||||
const milliseconds = Math.floor(value);
|
|
||||||
const input: DurationInput = {
|
|
||||||
milliseconds,
|
|
||||||
};
|
|
||||||
return formatDurationMillisecondMem(locale).format(input);
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid duration unit");
|
throw new Error("Invalid duration unit");
|
||||||
}
|
}
|
||||||
|
@ -40,12 +40,13 @@ export interface IntegrationManifest {
|
|||||||
loggers?: string[];
|
loggers?: string[];
|
||||||
quality_scale?:
|
quality_scale?:
|
||||||
| "bronze"
|
| "bronze"
|
||||||
| "gold"
|
|
||||||
| "internal"
|
|
||||||
| "platinum"
|
|
||||||
| "silver"
|
| "silver"
|
||||||
| "custom"
|
| "gold"
|
||||||
| "no_score";
|
| "platinum"
|
||||||
|
| "no_score"
|
||||||
|
| "internal"
|
||||||
|
| "legacy"
|
||||||
|
| "custom";
|
||||||
iot_class:
|
iot_class:
|
||||||
| "assumed_state"
|
| "assumed_state"
|
||||||
| "cloud_polling"
|
| "cloud_polling"
|
||||||
|
39
src/data/integration_quality_scale.ts
Normal file
39
src/data/integration_quality_scale.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { mdiContentSave, mdiMedal, mdiTrophy } from "@mdi/js";
|
||||||
|
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||||
|
import type { LocalizeKeys } from "../common/translations/localize";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map integration quality scale to icon and translation key.
|
||||||
|
*/
|
||||||
|
export const QUALITY_SCALE_MAP: Record<
|
||||||
|
string,
|
||||||
|
{ icon: string; translationKey: LocalizeKeys }
|
||||||
|
> = {
|
||||||
|
bronze: {
|
||||||
|
icon: mdiMedal,
|
||||||
|
translationKey: "ui.panel.config.integrations.config_entry.bronze_quality",
|
||||||
|
},
|
||||||
|
silver: {
|
||||||
|
icon: mdiMedal,
|
||||||
|
translationKey: "ui.panel.config.integrations.config_entry.silver_quality",
|
||||||
|
},
|
||||||
|
gold: {
|
||||||
|
icon: mdiMedal,
|
||||||
|
translationKey: "ui.panel.config.integrations.config_entry.gold_quality",
|
||||||
|
},
|
||||||
|
platinum: {
|
||||||
|
icon: mdiTrophy,
|
||||||
|
translationKey:
|
||||||
|
"ui.panel.config.integrations.config_entry.platinum_quality",
|
||||||
|
},
|
||||||
|
internal: {
|
||||||
|
icon: mdiHomeAssistant,
|
||||||
|
translationKey:
|
||||||
|
"ui.panel.config.integrations.config_entry.internal_integration",
|
||||||
|
},
|
||||||
|
legacy: {
|
||||||
|
icon: mdiContentSave,
|
||||||
|
translationKey:
|
||||||
|
"ui.panel.config.integrations.config_entry.legacy_integration",
|
||||||
|
},
|
||||||
|
};
|
@ -13,7 +13,6 @@ import {
|
|||||||
mdiDownload,
|
mdiDownload,
|
||||||
mdiFileCodeOutline,
|
mdiFileCodeOutline,
|
||||||
mdiHandExtendedOutline,
|
mdiHandExtendedOutline,
|
||||||
mdiMedal,
|
|
||||||
mdiOpenInNew,
|
mdiOpenInNew,
|
||||||
mdiPackageVariant,
|
mdiPackageVariant,
|
||||||
mdiPlayCircleOutline,
|
mdiPlayCircleOutline,
|
||||||
@ -23,7 +22,6 @@ import {
|
|||||||
mdiRenameBox,
|
mdiRenameBox,
|
||||||
mdiShapeOutline,
|
mdiShapeOutline,
|
||||||
mdiStopCircleOutline,
|
mdiStopCircleOutline,
|
||||||
mdiTrophy,
|
|
||||||
mdiWeb,
|
mdiWeb,
|
||||||
mdiWrench,
|
mdiWrench,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
@ -107,9 +105,7 @@ import { documentationUrl } from "../../../util/documentation-url";
|
|||||||
import { fileDownload } from "../../../util/file_download";
|
import { fileDownload } from "../../../util/file_download";
|
||||||
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||||
|
import { QUALITY_SCALE_MAP } from "../../../data/integration_quality_scale";
|
||||||
type MedalColor = "gold" | "silver" | "bronze" | "platinum";
|
|
||||||
const MEDAL_COLORS = ["bronze", "silver", "gold", "platinum"];
|
|
||||||
|
|
||||||
export const renderConfigEntryError = (
|
export const renderConfigEntryError = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -344,36 +340,30 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
? html`<div class="version">${this._manifest.version}</div>`
|
? html`<div class="version">${this._manifest.version}</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this._manifest?.quality_scale &&
|
${this._manifest?.quality_scale &&
|
||||||
MEDAL_COLORS.includes(this._manifest.quality_scale)
|
Object.keys(QUALITY_SCALE_MAP).includes(
|
||||||
|
this._manifest.quality_scale
|
||||||
|
)
|
||||||
? html`
|
? html`
|
||||||
<div class="quality-scale integration-info">
|
<div class="quality-scale integration-info">
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
class=${`${this._manifest.quality_scale}-medal`}
|
class=${`${this._manifest.quality_scale}-quality`}
|
||||||
.path=${this._manifest.quality_scale === "platinum"
|
.path=${QUALITY_SCALE_MAP[
|
||||||
? mdiTrophy
|
this._manifest.quality_scale
|
||||||
: mdiMedal}
|
].icon}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
<span>
|
<a
|
||||||
${this.hass.localize(
|
href=${documentationUrl(
|
||||||
`ui.panel.config.integrations.config_entry.${this._manifest.quality_scale as MedalColor}_quality`,
|
this.hass,
|
||||||
{
|
`/docs/quality_scale/#-${this._manifest.quality_scale}`
|
||||||
quality_scale: html`
|
|
||||||
<a
|
|
||||||
href=${documentationUrl(
|
|
||||||
this.hass,
|
|
||||||
`/docs/quality_scale/#${this._manifest.quality_scale}-`
|
|
||||||
)}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.quality_scale"
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
`,
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
</span>
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
QUALITY_SCALE_MAP[this._manifest.quality_scale]
|
||||||
|
.translationKey
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
@ -383,9 +373,18 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
class="warning"
|
class="warning"
|
||||||
path=${mdiPackageVariant}
|
path=${mdiPackageVariant}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
${this.hass.localize(
|
<a
|
||||||
"ui.panel.config.integrations.config_entry.custom_integration"
|
href=${documentationUrl(
|
||||||
)}
|
this.hass,
|
||||||
|
`/docs/quality_scale/#-custom`
|
||||||
|
)}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.custom_integration"
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this._manifest?.iot_class?.startsWith("cloud_")
|
${this._manifest?.iot_class?.startsWith("cloud_")
|
||||||
@ -1496,6 +1495,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
.logo-container {
|
.logo-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.version {
|
.version {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
@ -1538,17 +1538,24 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
100%;
|
100%;
|
||||||
animation: shimmer 2.5s infinite;
|
animation: shimmer 2.5s infinite;
|
||||||
}
|
}
|
||||||
ha-svg-icon.bronze-medal {
|
ha-svg-icon.bronze-quality {
|
||||||
color: #cd7f32;
|
color: #cd7f32;
|
||||||
}
|
}
|
||||||
ha-svg-icon.silver-medal {
|
ha-svg-icon.silver-quality {
|
||||||
color: silver;
|
color: silver;
|
||||||
}
|
}
|
||||||
ha-svg-icon.gold-medal {
|
ha-svg-icon.gold-quality {
|
||||||
color: gold;
|
color: gold;
|
||||||
}
|
}
|
||||||
ha-svg-icon.platinum-medal {
|
ha-svg-icon.platinum-quality {
|
||||||
color: #d9d9d9;
|
color: #727272;
|
||||||
|
}
|
||||||
|
ha-svg-icon.internal-quality {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
ha-svg-icon.legacy-quality {
|
||||||
|
color: var(--mdc-theme-text-icon-on-background, rgba(0, 0, 0, 0.38));
|
||||||
|
animation: unset;
|
||||||
}
|
}
|
||||||
ha-md-list-item {
|
ha-md-list-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -4505,8 +4505,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"custom_integration": "Custom integration",
|
"custom_integration": "Custom integration",
|
||||||
|
"internal_integration": "Internal integration",
|
||||||
|
"legacy_integration": "Legacy integration",
|
||||||
"custom_overwrites_core": "Custom integration that replaces a core component",
|
"custom_overwrites_core": "Custom integration that replaces a core component",
|
||||||
"depends_on_cloud": "Depends on Internet connection",
|
"depends_on_cloud": "Requires Internet",
|
||||||
"yaml_only": "This integration cannot be setup from the UI",
|
"yaml_only": "This integration cannot be setup from the UI",
|
||||||
"no_config_flow": "This integration was not set up from the UI",
|
"no_config_flow": "This integration was not set up from the UI",
|
||||||
"disabled_polling": "Automatic polling for updated data disabled",
|
"disabled_polling": "Automatic polling for updated data disabled",
|
||||||
@ -4521,11 +4523,10 @@
|
|||||||
"setup_in_progress": "Initializing"
|
"setup_in_progress": "Initializing"
|
||||||
},
|
},
|
||||||
"open_configuration_url": "Visit device",
|
"open_configuration_url": "Visit device",
|
||||||
"bronze_quality": "Bronze on our {quality_scale}",
|
"bronze_quality": "Bronze quality",
|
||||||
"silver_quality": "Silver on our {quality_scale}",
|
"silver_quality": "Silver quality",
|
||||||
"gold_quality": "Gold on our {quality_scale}",
|
"gold_quality": "Gold quality",
|
||||||
"platinum_quality": "Platinum on our {quality_scale}",
|
"platinum_quality": "Platinum quality"
|
||||||
"quality_scale": "quality scale"
|
|
||||||
},
|
},
|
||||||
"config_flow": {
|
"config_flow": {
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
|
11
src/types.ts
11
src/types.ts
@ -1,3 +1,4 @@
|
|||||||
|
import type { DurationFormatConstructor } from "@formatjs/intl-durationformat/src/types";
|
||||||
import type {
|
import type {
|
||||||
Auth,
|
Auth,
|
||||||
Connection,
|
Connection,
|
||||||
@ -22,7 +23,7 @@ import type { Themes } from "./data/ws-themes";
|
|||||||
import type { ExternalMessaging } from "./external_app/external_messaging";
|
import type { ExternalMessaging } from "./external_app/external_messaging";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/* eslint-disable no-var, no-redeclare */
|
/* eslint-disable no-var */
|
||||||
var __DEV__: boolean;
|
var __DEV__: boolean;
|
||||||
var __DEMO__: boolean;
|
var __DEMO__: boolean;
|
||||||
var __BUILD__: "modern" | "legacy";
|
var __BUILD__: "modern" | "legacy";
|
||||||
@ -30,7 +31,7 @@ declare global {
|
|||||||
var __STATIC_PATH__: string;
|
var __STATIC_PATH__: string;
|
||||||
var __BACKWARDS_COMPAT__: boolean;
|
var __BACKWARDS_COMPAT__: boolean;
|
||||||
var __SUPERVISOR__: boolean;
|
var __SUPERVISOR__: boolean;
|
||||||
/* eslint-enable no-var, no-redeclare */
|
/* eslint-enable no-var */
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
// Custom panel entry point url
|
// Custom panel entry point url
|
||||||
@ -64,6 +65,12 @@ declare global {
|
|||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intl.DurationFormat is not yet part of the TypeScript standard
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace Intl {
|
||||||
|
const DurationFormat: DurationFormatConstructor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValueChangedEvent<T> extends CustomEvent {
|
export interface ValueChangedEvent<T> extends CustomEvent {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import "@formatjs/intl-durationformat/polyfill-force";
|
||||||
import { assert, describe, it } from "vitest";
|
import { assert, describe, it } from "vitest";
|
||||||
|
|
||||||
import { formatDuration } from "../../../src/common/datetime/format_duration";
|
import { formatDuration } from "../../../src/common/datetime/format_duration";
|
||||||
import type { FrontendLocaleData } from "../../../src/data/translation";
|
import type { FrontendLocaleData } from "../../../src/data/translation";
|
||||||
import {
|
import {
|
||||||
@ -21,14 +21,6 @@ const LOCALE: FrontendLocaleData = {
|
|||||||
|
|
||||||
describe("formatDuration", () => {
|
describe("formatDuration", () => {
|
||||||
it("works", () => {
|
it("works", () => {
|
||||||
assert.strictEqual(formatDuration(LOCALE, "0", "ms"), "0ms");
|
|
||||||
assert.strictEqual(formatDuration(LOCALE, "1", "ms"), "1ms");
|
|
||||||
assert.strictEqual(formatDuration(LOCALE, "10", "ms"), "10ms");
|
|
||||||
assert.strictEqual(formatDuration(LOCALE, "100", "ms"), "100ms");
|
|
||||||
assert.strictEqual(formatDuration(LOCALE, "1000", "ms"), "1,000ms");
|
|
||||||
assert.strictEqual(formatDuration(LOCALE, "1001", "ms"), "1,001ms");
|
|
||||||
assert.strictEqual(formatDuration(LOCALE, "65000", "ms"), "65,000ms");
|
|
||||||
|
|
||||||
assert.strictEqual(formatDuration(LOCALE, "0.5", "s"), "0s 500ms");
|
assert.strictEqual(formatDuration(LOCALE, "0.5", "s"), "0s 500ms");
|
||||||
assert.strictEqual(formatDuration(LOCALE, "1", "s"), "1s");
|
assert.strictEqual(formatDuration(LOCALE, "1", "s"), "1s");
|
||||||
assert.strictEqual(formatDuration(LOCALE, "1.1", "s"), "1s 100ms");
|
assert.strictEqual(formatDuration(LOCALE, "1.1", "s"), "1s 100ms");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user