20241127.1 (#23045)

This commit is contained in:
Bram Kragten 2024-11-28 17:01:24 +01:00 committed by GitHub
commit 6e003907fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 115 additions and 85 deletions

View File

@ -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",

View File

@ -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"

View File

@ -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");
} }

View File

@ -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"

View 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",
},
};

View File

@ -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;

View File

@ -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",

View File

@ -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 {

View File

@ -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");