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