diff --git a/.github/workflows/cast_deployment.yaml b/.github/workflows/cast_deployment.yaml index 8b8b0b98f7..a3ecc20db0 100644 --- a/.github/workflows/cast_deployment.yaml +++ b/.github/workflows/cast_deployment.yaml @@ -22,12 +22,12 @@ jobs: url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: ref: dev - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn @@ -60,12 +60,12 @@ jobs: url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: ref: master - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b43acab616..40ac8b224b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,13 +32,13 @@ jobs: sha: ${{ steps.get-sha.outputs.sha }} steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: # Checkout PR head instead of merge commit # Use ref, not SHA, so reruns get the dedupe commit ref: ${{ github.event.pull_request.head.ref }} - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn @@ -81,11 +81,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: ref: ${{ needs.dedupe.outputs.sha }} - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn @@ -109,11 +109,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: ref: ${{ needs.dedupe.outputs.sha }} - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn @@ -131,11 +131,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: ref: ${{ needs.dedupe.outputs.sha }} - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn @@ -153,11 +153,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: ref: ${{ needs.dedupe.outputs.sha }} - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7717444454..7e417b355b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/demo_deployment.yaml b/.github/workflows/demo_deployment.yaml index cc394b646f..d28a5ffe11 100644 --- a/.github/workflows/demo_deployment.yaml +++ b/.github/workflows/demo_deployment.yaml @@ -23,12 +23,12 @@ jobs: url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: ref: dev - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn @@ -61,12 +61,12 @@ jobs: url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: ref: master - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn diff --git a/.github/workflows/design_deployment.yaml b/.github/workflows/design_deployment.yaml index 0574dd8676..c9b37d605a 100644 --- a/.github/workflows/design_deployment.yaml +++ b/.github/workflows/design_deployment.yaml @@ -17,10 +17,10 @@ jobs: url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn diff --git a/.github/workflows/design_preview.yaml b/.github/workflows/design_preview.yaml index 084f69ad93..607838b940 100644 --- a/.github/workflows/design_preview.yaml +++ b/.github/workflows/design_preview.yaml @@ -22,10 +22,10 @@ jobs: if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') steps: - name: Check out files from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index bd54ca3026..ffadf0a483 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -21,7 +21,7 @@ jobs: contents: write steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v4 @@ -29,7 +29,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 662c7adcc6..01058a9413 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -24,7 +24,7 @@ jobs: contents: write # Required to upload release assets steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Verify version uses: home-assistant/actions/helpers/verify-version@master @@ -35,7 +35,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3.5.1 + uses: actions/setup-node@v3.6.0 with: node-version: ${{ env.NODE_VERSION }} cache: yarn diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 4d593874d7..594117661c 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Upload Translations run: | diff --git a/gallery/src/pages/components/ha-alert.ts b/gallery/src/pages/components/ha-alert.ts index ced4c5a44b..a0d3f309fb 100644 --- a/gallery/src/pages/components/ha-alert.ts +++ b/gallery/src/pages/components/ha-alert.ts @@ -98,7 +98,9 @@ const alerts: { description: "Alert with slotted image", type: "warning", iconSlot: html`Home Assistant logo`, }, { diff --git a/hassio/src/addon-store/hassio-addon-repository.ts b/hassio/src/addon-store/hassio-addon-repository.ts index a2eb5157e3..0d22c49ad2 100644 --- a/hassio/src/addon-store/hassio-addon-repository.ts +++ b/hassio/src/addon-store/hassio-addon-repository.ts @@ -29,7 +29,9 @@ class HassioAddonRepositoryEl extends LitElement { if (filter) { return filterAndSort(addons, filter); } - return addons.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)); + return addons.sort((a, b) => + caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language) + ); }); protected render(): TemplateResult { diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index 3cc3d23010..a6e8c99217 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -404,6 +404,7 @@ class HassioAddonInfo extends LitElement { ? html` ` diff --git a/hassio/src/dashboard/hassio-addons.ts b/hassio/src/dashboard/hassio-addons.ts index cc855ccc69..87aaa545a3 100644 --- a/hassio/src/dashboard/hassio-addons.ts +++ b/hassio/src/dashboard/hassio-addons.ts @@ -35,7 +35,13 @@ class HassioAddons extends LitElement { ` : this.supervisor.addon.addons - .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)) + .sort((a, b) => + caseInsensitiveStringCompare( + a.name, + b.name, + this.hass.locale.language + ) + ) .map( (addon) => html` + ( + showAdvanced: boolean, + hardware: HassioHardwareInfo, + filter: string, + language: string + ) => hardware.devices .filter( (device) => @@ -28,7 +33,7 @@ const _filterDevices = memoizeOne( .toLocaleLowerCase() .includes(filter)) ) - .sort((a, b) => stringCompare(a.name, b.name)) + .sort((a, b) => stringCompare(a.name, b.name, language)) ); @customElement("dialog-hassio-hardware") @@ -56,7 +61,8 @@ class HassioHardwareDialog extends LitElement { const devices = _filterDevices( this.hass.userData?.showAdvanced || false, this._dialogParams.hardware, - (this._filter || "").toLowerCase() + (this._filter || "").toLowerCase(), + this.hass.locale.language ); return html` diff --git a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts index 82325db788..0a11027766 100644 --- a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts +++ b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts @@ -68,7 +68,9 @@ class HassioRepositoriesDialog extends LitElement { repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons repo.slug !== "5c53de3b" // The ESPHome repository ) - .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)) + .sort((a, b) => + caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language) + ) ); private _filteredUsedRepositories = memoizeOne( diff --git a/package.json b/package.json index 531d6e4246..9fbdeabd07 100644 --- a/package.json +++ b/package.json @@ -25,18 +25,13 @@ "license": "Apache-2.0", "dependencies": { "@braintree/sanitize-url": "^6.0.0", - "@codemirror/autocomplete": "^0.19.12", - "@codemirror/commands": "^0.19.8", - "@codemirror/gutter": "^0.19.9", - "@codemirror/highlight": "^0.19.7", - "@codemirror/history": "^0.19.2", - "@codemirror/legacy-modes": "^0.19.0", - "@codemirror/rectangular-selection": "^0.19.1", - "@codemirror/search": "^0.19.6", - "@codemirror/state": "^0.19.6", - "@codemirror/stream-parser": "^0.19.5", - "@codemirror/text": "^0.19.6", - "@codemirror/view": "^0.19.40", + "@codemirror/autocomplete": "^6.4.0", + "@codemirror/commands": "^6.1.3", + "@codemirror/language": "^6.3.2", + "@codemirror/legacy-modes": "^6.3.1", + "@codemirror/search": "^6.2.3", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.7.1", "@formatjs/intl-datetimeformat": "^4.2.5", "@formatjs/intl-getcanonicallocales": "^1.8.0", "@formatjs/intl-locale": "^2.4.40", @@ -49,6 +44,7 @@ "@fullcalendar/interaction": "5.9.0", "@fullcalendar/list": "5.9.0", "@fullcalendar/timegrid": "5.9.0", + "@lezer/highlight": "^1.1.3", "@lit-labs/motion": "^1.0.2", "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch", "@material/chips": "14.0.0-canary.261f2db59.0", @@ -75,8 +71,8 @@ "@material/mwc-textfield": "0.25.3", "@material/mwc-top-app-bar-fixed": "^0.25.3", "@material/top-app-bar": "14.0.0-canary.261f2db59.0", - "@mdi/js": "7.0.96", - "@mdi/svg": "7.0.96", + "@mdi/js": "7.1.96", + "@mdi/svg": "7.1.96", "@polymer/app-layout": "^3.1.0", "@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-icon": "^3.0.1", @@ -97,7 +93,7 @@ "@vibrant/color": "^3.2.1-alpha.1", "@vibrant/core": "^3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", - "@vue/web-component-wrapper": "^1.2.0", + "@vue/web-component-wrapper": "^1.3.0", "@webcomponents/scoped-custom-element-registry": "^0.0.5", "@webcomponents/webcomponentsjs": "^2.2.10", "app-datepicker": "^5.1.0", @@ -141,12 +137,12 @@ "vue": "^2.6.12", "vue2-daterange-picker": "^0.5.1", "weekstart": "^1.1.0", - "workbox-cacheable-response": "^6.4.2", - "workbox-core": "^6.4.2", - "workbox-expiration": "^6.4.2", - "workbox-precaching": "^6.4.2", - "workbox-routing": "^6.4.2", - "workbox-strategies": "^6.4.2", + "workbox-cacheable-response": "^6.5.4", + "workbox-core": "^6.5.4", + "workbox-expiration": "^6.5.4", + "workbox-precaching": "^6.5.4", + "workbox-routing": "^6.5.4", + "workbox-strategies": "^6.5.4", "xss": "^1.0.9" }, "devDependencies": { @@ -202,7 +198,7 @@ "eslint-plugin-unused-imports": "^1.1.5", "eslint-plugin-wc": "^1.3.2", "fancy-log": "^2.0.0", - "fs-extra": "^7.0.1", + "fs-extra": "^11.1.0", "glob": "^7.2.0", "gulp": "^4.0.2", "gulp-flatmap": "^1.0.2", @@ -222,7 +218,7 @@ "merge-stream": "^1.0.1", "mocha": "^8.4.0", "object-hash": "^2.0.3", - "open": "^7.0.4", + "open": "^8.4.0", "pinst": "^3.0.0", "prettier": "^2.8.1", "require-dir": "^1.2.0", @@ -237,7 +233,7 @@ "tar": "^6.1.11", "terser-webpack-plugin": "^5.2.4", "ts-lit-plugin": "^1.2.1", - "typescript": "^4.9.3", + "typescript": "^4.9.4", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", "webpack": "^5.55.1", @@ -245,7 +241,7 @@ "webpack-dev-server": "^4.3.0", "webpack-manifest-plugin": "^4.0.2", "webpackbar": "^5.0.0-3", - "workbox-build": "^6.4.2" + "workbox-build": "^6.5.4" }, "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", "resolutions": { diff --git a/pyproject.toml b/pyproject.toml index 5641dfcf15..9ecc1b760d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20221213.0" +version = "20230104.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/common/const.ts b/src/common/const.ts index a279cfd348..07ada9c7e3 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -201,6 +201,7 @@ export const DOMAINS_WITH_CARD = [ export const SENSOR_ENTITIES = [ "sensor", "binary_sensor", + "calendar", "camera", "device_tracker", "weather", diff --git a/src/common/string/compare.ts b/src/common/string/compare.ts index 67ef4c87bd..a14e087ccb 100644 --- a/src/common/string/compare.ts +++ b/src/common/string/compare.ts @@ -1,4 +1,15 @@ -export const stringCompare = (a: string, b: string) => { +import memoizeOne from "memoize-one"; + +const collator = memoizeOne( + (language: string | undefined) => new Intl.Collator(language) +); + +const caseInsensitiveCollator = memoizeOne( + (language: string | undefined) => + new Intl.Collator(language, { sensitivity: "accent" }) +); + +const fallbackStringCompare = (a: string, b: string) => { if (a < b) { return -1; } @@ -9,5 +20,28 @@ export const stringCompare = (a: string, b: string) => { return 0; }; -export const caseInsensitiveStringCompare = (a: string, b: string) => - stringCompare(a.toLowerCase(), b.toLowerCase()); +export const stringCompare = ( + a: string, + b: string, + language: string | undefined = undefined +) => { + // @ts-ignore + if (Intl?.Collator) { + return collator(language).compare(a, b); + } + + return fallbackStringCompare(a, b); +}; + +export const caseInsensitiveStringCompare = ( + a: string, + b: string, + language: string | undefined = undefined +) => { + // @ts-ignore + if (Intl?.Collator) { + return caseInsensitiveCollator(language).compare(a, b); + } + + return fallbackStringCompare(a.toLowerCase(), b.toLowerCase()); +}; diff --git a/src/components/country-datalist.ts b/src/components/country-datalist.ts index b4295f99ad..7692e58c0c 100644 --- a/src/components/country-datalist.ts +++ b/src/components/country-datalist.ts @@ -266,14 +266,16 @@ export const getCountryOptions = memoizeOne((language?: string) => { value: country, label: countryDisplayNames ? countryDisplayNames.of(country)! : country, })); - options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label)); + options.sort((a, b) => + caseInsensitiveStringCompare(a.label, b.label, language) + ); return options; }); -export const createCountryListEl = () => { +export const createCountryListEl = (language?: string) => { const list = document.createElement("datalist"); list.id = "countries"; - const options = getCountryOptions(); + const options = getCountryOptions(language); for (const country of options) { const option = document.createElement("option"); option.value = country.value; diff --git a/src/components/currency-datalist.ts b/src/components/currency-datalist.ts index 1339c47520..ef29faf56c 100644 --- a/src/components/currency-datalist.ts +++ b/src/components/currency-datalist.ts @@ -157,7 +157,7 @@ export const CURRENCIES = [ "XPF", "YER", "ZAR", - "ZMK", + "ZMW", "ZWL", ]; @@ -173,14 +173,16 @@ export const getCurrencyOptions = memoizeOne((language?: string) => { value: currency, label: currencyDisplayNames ? currencyDisplayNames.of(currency)! : currency, })); - options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label)); + options.sort((a, b) => + caseInsensitiveStringCompare(a.label, b.label, language) + ); return options; }); -export const createCurrencyListEl = () => { +export const createCurrencyListEl = (language: string) => { const list = document.createElement("datalist"); list.id = "currencies"; - for (const currency of getCurrencyOptions()) { + for (const currency of getCurrencyOptions(language)) { const option = document.createElement("option"); option.value = currency.value; option.innerText = currency.label; diff --git a/src/components/date-range-picker.ts b/src/components/date-range-picker.ts index b6f93a9584..20a952fb6c 100644 --- a/src/components/date-range-picker.ts +++ b/src/components/date-range-picker.ts @@ -5,7 +5,6 @@ import DateRangePicker from "vue2-daterange-picker"; // @ts-ignore import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css"; import { fireEvent } from "../common/dom/fire_event"; -import { Constructor } from "../types"; const Component = Vue.extend({ props: { @@ -47,35 +46,26 @@ const Component = Vue.extend({ }, }, render(createElement) { - // @ts-ignore + // @ts-expect-error return createElement(DateRangePicker, { props: { - // @ts-ignore "time-picker": this.timePicker, - // @ts-ignore "auto-apply": this.autoApply, opens: "right", "show-dropdowns": false, - // @ts-ignore "time-picker24-hour": this.twentyfourHours, - // @ts-ignore disabled: this.disabled, - // @ts-ignore ranges: this.ranges ? {} : false, "locale-data": { - // @ts-ignore firstDay: this.firstDay, }, }, model: { value: { - // @ts-ignore startDate: this.startDate, - // @ts-ignore endDate: this.endDate, }, callback: (value) => { - // @ts-ignore fireEvent(this.$el as HTMLElement, "change", value); }, expression: "dateRange", @@ -106,7 +96,11 @@ const Component = Vue.extend({ }, }); -const WrappedElement: Constructor = wrap(Vue, Component); +// Assertion corrects HTMLElement type from package +const WrappedElement = wrap( + Vue, + Component +) as unknown as CustomElementConstructor; @customElement("date-range-picker") class DateRangePickerElement extends WrappedElement { diff --git a/src/components/device/ha-area-devices-picker.ts b/src/components/device/ha-area-devices-picker.ts index 169a49f518..3edee20f5a 100644 --- a/src/components/device/ha-area-devices-picker.ts +++ b/src/components/device/ha-area-devices-picker.ts @@ -189,7 +189,8 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { .sort((a, b) => stringCompare( devicesByArea[a].name || "", - devicesByArea[b].name || "" + devicesByArea[b].name || "", + this.hass.locale.language ) ) .map((key) => devicesByArea[key]); diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index d93e717b27..95a3332fe0 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -84,6 +84,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { @property({ type: Array, attribute: "include-device-classes" }) public includeDeviceClasses?: string[]; + /** + * List of devices to be excluded. + * @type {Array} + * @attr exclude-devices + */ + @property({ type: Array, attribute: "exclude-devices" }) + public excludeDevices?: string[]; + @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; @property({ type: Boolean }) public disabled?: boolean; @@ -104,7 +112,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { includeDomains: this["includeDomains"], excludeDomains: this["excludeDomains"], includeDeviceClasses: this["includeDeviceClasses"], - deviceFilter: this["deviceFilter"] + deviceFilter: this["deviceFilter"], + excludeDevices: this["excludeDevices"] ): Device[] => { if (!devices.length) { return [ @@ -164,6 +173,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { }); } + if (excludeDevices) { + inputDevices = inputDevices.filter( + (device) => !excludeDevices!.includes(device.id) + ); + } + if (includeDeviceClasses) { inputDevices = inputDevices.filter((device) => { const devEntities = deviceEntityLookup[device.id]; @@ -216,7 +231,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { return outputDevices; } return outputDevices.sort((a, b) => - stringCompare(a.name || "", b.name || "") + stringCompare(a.name || "", b.name || "", this.hass.locale.language) ); } ); @@ -258,7 +273,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { this.includeDomains, this.excludeDomains, this.includeDeviceClasses, - this.deviceFilter + this.deviceFilter, + this.excludeDevices ); } } diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 056918602a..e2f2339c1c 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -174,7 +174,8 @@ export class HaEntityPicker extends LitElement { .sort((entityA, entityB) => caseInsensitiveStringCompare( entityA.friendly_name, - entityB.friendly_name + entityB.friendly_name, + this.hass.locale.language ) ); } @@ -205,7 +206,8 @@ export class HaEntityPicker extends LitElement { .sort((entityA, entityB) => caseInsensitiveStringCompare( entityA.friendly_name, - entityB.friendly_name + entityB.friendly_name, + this.hass.locale.language ) ); diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 35fa54748e..c51309ccfb 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -223,6 +223,10 @@ export class HaStateLabelBadge extends LitElement { if (domainStateKey) { return this.hass!.localize(`state_badge.${domainStateKey}`); } + // Person and device tracker state can be zone name + if (domain === "person" || domain === "device_tracker") { + return entityState.state; + } if (domain === "timer") { return secondsToDuration(_timerTimeRemaining); } diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index a19ee9e011..59c942d95e 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -177,7 +177,9 @@ export class HaStatisticPicker extends LitElement { } if (output.length > 1) { - output.sort((a, b) => stringCompare(a.name || "", b.name || "")); + output.sort((a, b) => + stringCompare(a.name || "", b.name || "", this.hass.locale.language) + ); } output.push({ diff --git a/src/components/entity/state-info.ts b/src/components/entity/state-info.ts index 7ee6c6ba39..9db1bae9f7 100644 --- a/src/components/entity/state-info.ts +++ b/src/components/entity/state-info.ts @@ -28,7 +28,7 @@ class StateInfo extends LitElement { const name = computeStateName(this.stateObj); - return html` = ( ${item.name} ${item.slug} ${item.icon - ? html`` + ? html`` : ""} `; @@ -80,7 +84,9 @@ class HaAddonPicker extends LitElement { const addonsInfo = await fetchHassioAddonsInfo(this.hass); this._addons = addonsInfo.addons .filter((addon) => addon.version) - .sort((a, b) => stringCompare(a.name, b.name)); + .sort((a, b) => + stringCompare(a.name, b.name, this.hass.locale.language) + ); } else { showAlertDialog(this, { title: this.hass.localize( diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index ec242ba8cc..d45f4fdb89 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -73,6 +73,14 @@ export class HaAreaPicker extends LitElement { @property({ type: Array, attribute: "include-device-classes" }) public includeDeviceClasses?: string[]; + /** + * List of areas to be excluded. + * @type {Array} + * @attr exclude-areas + */ + @property({ type: Array, attribute: "exclude-areas" }) + public excludeAreas?: string[]; + @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; @property() public entityFilter?: (entity: EntityRegistryEntry) => boolean; @@ -109,7 +117,8 @@ export class HaAreaPicker extends LitElement { includeDeviceClasses: this["includeDeviceClasses"], deviceFilter: this["deviceFilter"], entityFilter: this["entityFilter"], - noAdd: this["noAdd"] + noAdd: this["noAdd"], + excludeAreas: this["excludeAreas"] ): AreaRegistryEntry[] => { if (!areas.length) { return [ @@ -235,6 +244,12 @@ export class HaAreaPicker extends LitElement { outputAreas = areas.filter((area) => areaIds!.includes(area.area_id)); } + if (excludeAreas) { + outputAreas = outputAreas.filter( + (area) => !excludeAreas!.includes(area.area_id) + ); + } + if (!outputAreas.length) { outputAreas = [ { @@ -264,7 +279,7 @@ export class HaAreaPicker extends LitElement { (this._init && changedProps.has("_opened") && this._opened) ) { this._init = true; - (this.comboBox as any).items = this._getAreas( + const areas = this._getAreas( Object.values(this.hass.areas), Object.values(this.hass.devices), Object.values(this.hass.entities), @@ -273,8 +288,11 @@ export class HaAreaPicker extends LitElement { this.includeDeviceClasses, this.deviceFilter, this.entityFilter, - this.noAdd + this.noAdd, + this.excludeAreas ); + (this.comboBox as any).items = areas; + (this.comboBox as any).filteredItems = areas; } } @@ -384,7 +402,8 @@ export class HaAreaPicker extends LitElement { this.includeDeviceClasses, this.deviceFilter, this.entityFilter, - this.noAdd + this.noAdd, + this.excludeAreas ); await this.updateComplete; await this.comboBox.updateComplete; diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts index 9ada34f871..8db3e132e1 100644 --- a/src/components/ha-base-time-input.ts +++ b/src/components/ha-base-time-input.ts @@ -266,6 +266,9 @@ export class HaBaseTimeInput extends LitElement { seconds: this.seconds, milliseconds: this.milliseconds, }; + if (this.enableDay) { + value.days = this.days; + } if (this.format === 12) { value.amPm = this.amPm; } diff --git a/src/components/ha-blueprint-picker.ts b/src/components/ha-blueprint-picker.ts index 58a0f03f4a..0dfc64c3a9 100644 --- a/src/components/ha-blueprint-picker.ts +++ b/src/components/ha-blueprint-picker.ts @@ -46,7 +46,9 @@ class HaBluePrintPicker extends LitElement { ...(blueprint as Blueprint).metadata, path, })); - return result.sort((a, b) => stringCompare(a.name, b.name)); + return result.sort((a, b) => + stringCompare(a.name, b.name, this.hass!.locale.language) + ); }); protected render(): TemplateResult { diff --git a/src/components/ha-code-editor.ts b/src/components/ha-code-editor.ts index a8f9938332..794c202740 100644 --- a/src/components/ha-code-editor.ts +++ b/src/components/ha-code-editor.ts @@ -4,6 +4,7 @@ import type { CompletionResult, CompletionSource, } from "@codemirror/autocomplete"; +import type { Extension } from "@codemirror/state"; import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view"; import { HassEntities } from "home-assistant-js-websocket"; import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit"; @@ -72,9 +73,9 @@ export class HaCodeEditor extends ReactiveElement { if (!this.codemirror || !this._loadedCodeMirror) { return false; } - const className = this._loadedCodeMirror.HighlightStyle.get( + const className = this._loadedCodeMirror.highlightingFor( this.codemirror.state, - this._loadedCodeMirror.tags.comment + [this._loadedCodeMirror.tags.comment] ); return !!this.shadowRoot!.querySelector(`span.${className}`); } @@ -136,7 +137,7 @@ export class HaCodeEditor extends ReactiveElement { private async _load(): Promise { this._loadedCodeMirror = await loadCodeMirror(); - const extensions = [ + const extensions: Extension[] = [ this._loadedCodeMirror.lineNumbers(), this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true), this._loadedCodeMirror.history(), @@ -152,10 +153,8 @@ export class HaCodeEditor extends ReactiveElement { saveKeyBinding, ] as KeyBinding[]), this._loadedCodeMirror.langCompartment.of(this._mode), - this._loadedCodeMirror.theme, - this._loadedCodeMirror.Prec.fallback( - this._loadedCodeMirror.highlightStyle - ), + this._loadedCodeMirror.haTheme, + this._loadedCodeMirror.haSyntaxHighlighting, this._loadedCodeMirror.readonlyCompartment.of( this._loadedCodeMirror.EditorView.editable.of(!this.readOnly) ), @@ -227,7 +226,7 @@ export class HaCodeEditor extends ReactiveElement { return { from: Number(entityWord.from), options: states, - span: /^[a-z_]{3,}\.\w*$/, + validFor: /^[a-z_]{3,}\.\w*$/, }; } @@ -268,7 +267,7 @@ export class HaCodeEditor extends ReactiveElement { return { from: Number(match.from), options: iconItems, - span: /^mdi:\S*$/, + validFor: /^mdi:\S*$/, }; } diff --git a/src/components/ha-config-entry-picker.ts b/src/components/ha-config-entry-picker.ts index 733598b003..09bfc45b4e 100644 --- a/src/components/ha-config-entry-picker.ts +++ b/src/components/ha-config-entry-picker.ts @@ -59,6 +59,7 @@ class HaConfigEntryPicker extends LitElement { > ${item.localized_domain_name} caseInsensitiveStringCompare( conf1.localized_domain_name + conf1.title, - conf2.localized_domain_name + conf2.title + conf2.localized_domain_name + conf2.title, + this.hass.locale.language ) ); }); diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index f3995a7737..1e69ad2ac4 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -3,10 +3,12 @@ import { styles } from "@material/mwc-dialog/mwc-dialog.css"; import { mdiClose } from "@mdi/js"; import { css, html, TemplateResult } from "lit"; import { customElement } from "lit/decorators"; -import type { HomeAssistant } from "../types"; import { FOCUS_TARGET } from "../dialogs/make-dialog-manager"; +import type { HomeAssistant } from "../types"; import "./ha-icon-button"; +const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"]; + export const createCloseHeading = ( hass: HomeAssistant, title: string | TemplateResult @@ -32,6 +34,14 @@ export class HaDialog extends DialogBase { return html` ${super.renderHeading()} `; } + protected firstUpdated(): void { + super.firstUpdated(); + this.suppressDefaultPressSelector = [ + this.suppressDefaultPressSelector, + SUPPRESS_DEFAULT_PRESS_SELECTOR, + ].join(", "); + } + static override styles = [ styles, css` diff --git a/src/components/ha-form/ha-form-integer.ts b/src/components/ha-form/ha-form-integer.ts index f09821e3d7..86de2d310c 100644 --- a/src/components/ha-form/ha-form-integer.ts +++ b/src/components/ha-form/ha-form-integer.ts @@ -67,6 +67,9 @@ export class HaFormInteger extends LitElement implements HaFormElement { @change=${this._valueChanged} > + ${this.helper + ? html`${this.helper}` + : ""} `; } diff --git a/src/components/ha-selector/ha-selector-target.ts b/src/components/ha-selector/ha-selector-target.ts index 55f078f6bf..e4cf47278d 100644 --- a/src/components/ha-selector/ha-selector-target.ts +++ b/src/components/ha-selector/ha-selector-target.ts @@ -1,8 +1,4 @@ -import { - HassEntity, - HassServiceTarget, - UnsubscribeFunc, -} from "home-assistant-js-websocket"; +import { HassEntity, HassServiceTarget } from "home-assistant-js-websocket"; import { css, CSSResultGroup, @@ -17,8 +13,7 @@ import { DeviceRegistryEntry, getDeviceIntegrationLookup, } from "../../data/device_registry"; -import type { EntityRegistryEntry } from "../../data/entity_registry"; -import { subscribeEntityRegistry } from "../../data/entity_registry"; +import { EntityRegistryEntry } from "../../data/entity_registry"; import { EntitySources, fetchEntitySourcesWithCache, @@ -28,12 +23,11 @@ import { filterSelectorEntities, TargetSelector, } from "../../data/selector"; -import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../types"; import "../ha-target-picker"; @customElement("ha-selector-target") -export class HaTargetSelector extends SubscribeMixin(LitElement) { +export class HaTargetSelector extends LitElement { @property() public hass!: HomeAssistant; @property() public selector!: TargetSelector; @@ -48,18 +42,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { @state() private _entitySources?: EntitySources; - @state() private _entities?: EntityRegistryEntry[]; - private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); - public hassSubscribe(): UnsubscribeFunc[] { - return [ - subscribeEntityRegistry(this.hass.connection!, (entities) => { - this._entities = entities.filter((entity) => entity.device_id !== null); - }), - ]; - } - protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties); if ( @@ -88,12 +72,19 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { .value=${this.value} .helper=${this.helper} .deviceFilter=${this._filterDevices} - .entityFilter=${this._filterEntities} + .entityFilter=${this._filterStates} + .entityRegFilter=${this._filterRegEntities} + .includeDeviceClasses=${this.selector.target?.entity?.device_class + ? [this.selector.target?.entity.device_class] + : undefined} + .includeDomains=${this.selector.target?.entity?.domain + ? [this.selector.target?.entity.domain] + : undefined} .disabled=${this.disabled} >`; } - private _filterEntities = (entity: HassEntity): boolean => { + private _filterStates = (entity: HassEntity): boolean => { if (!this.selector.target?.entity) { return true; } @@ -105,15 +96,26 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { ); }; + private _filterRegEntities = (entity: EntityRegistryEntry): boolean => { + if (this.selector.target?.entity?.integration) { + if (entity.platform !== this.selector.target.entity.integration) { + return false; + } + } + return true; + }; + private _filterDevices = (device: DeviceRegistryEntry): boolean => { if (!this.selector.target?.device) { return true; } - const deviceIntegrations = - this._entitySources && this._entities - ? this._deviceIntegrationLookup(this._entitySources, this._entities) - : undefined; + const deviceIntegrations = this._entitySources + ? this._deviceIntegrationLookup( + this._entitySources, + Object.values(this.hass.entities) + ) + : undefined; return filterSelectorDevices( this.selector.target.device, diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 30c4959335..1173b0c7a7 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -87,7 +87,8 @@ const panelSorter = ( reverseSort: string[], defaultPanel: string, a: PanelInfo, - b: PanelInfo + b: PanelInfo, + language: string ) => { const indexA = reverseSort.indexOf(a.url_path); const indexB = reverseSort.indexOf(b.url_path); @@ -97,13 +98,14 @@ const panelSorter = ( } return -1; } - return defaultPanelSorter(defaultPanel, a, b); + return defaultPanelSorter(defaultPanel, a, b, language); }; const defaultPanelSorter = ( defaultPanel: string, a: PanelInfo, - b: PanelInfo + b: PanelInfo, + language: string ) => { // Put all the Lovelace at the top. const aLovelace = a.component_name === "lovelace"; @@ -117,7 +119,7 @@ const defaultPanelSorter = ( } if (aLovelace && bLovelace) { - return stringCompare(a.title!, b.title!); + return stringCompare(a.title!, b.title!, language); } if (aLovelace && !bLovelace) { return -1; @@ -139,7 +141,7 @@ const defaultPanelSorter = ( return 1; } // both not built in, sort by title - return stringCompare(a.title!, b.title!); + return stringCompare(a.title!, b.title!, language); }; const computePanels = memoizeOne( @@ -147,7 +149,8 @@ const computePanels = memoizeOne( panels: HomeAssistant["panels"], defaultPanel: HomeAssistant["defaultPanel"], panelsOrder: string[], - hiddenPanels: string[] + hiddenPanels: string[], + locale: HomeAssistant["locale"] ): [PanelInfo[], PanelInfo[]] => { if (!panels) { return [[], []]; @@ -171,8 +174,12 @@ const computePanels = memoizeOne( const reverseSort = [...panelsOrder].reverse(); - beforeSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b)); - afterSpacer.sort((a, b) => panelSorter(reverseSort, defaultPanel, a, b)); + beforeSpacer.sort((a, b) => + panelSorter(reverseSort, defaultPanel, a, b, locale.language) + ); + afterSpacer.sort((a, b) => + panelSorter(reverseSort, defaultPanel, a, b, locale.language) + ); return [beforeSpacer, afterSpacer]; } @@ -374,7 +381,8 @@ class HaSidebar extends SubscribeMixin(LitElement) { this.hass.panels, this.hass.defaultPanel, this._panelOrder, - this._hiddenPanels + this._hiddenPanels, + this.hass.locale ); // Show the supervisor as beeing part of configuration diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index d5dd2c8630..3aa581b441 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -345,6 +345,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { .entityFilter=${this.entityRegFilter} .includeDeviceClasses=${this.includeDeviceClasses} .includeDomains=${this.includeDomains} + .excludeAreas=${ensureArray(this.value?.area_id)} @value-changed=${this._targetPicked} > `; @@ -358,9 +359,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { "ui.components.target-picker.add_device_id" )} .deviceFilter=${this.deviceFilter} - .entityFilter=${this.entityRegFilter} .includeDeviceClasses=${this.includeDeviceClasses} .includeDomains=${this.includeDomains} + .excludeDevices=${ensureArray(this.value?.device_id)} @value-changed=${this._targetPicked} > `; @@ -376,6 +377,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { .entityFilter=${this.entityFilter} .includeDeviceClasses=${this.includeDeviceClasses} .includeDomains=${this.includeDomains} + .excludeEntities=${ensureArray(this.value?.entity_id)} @value-changed=${this._targetPicked} allow-custom-entity > @@ -393,6 +395,13 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { const target = ev.currentTarget; target.value = ""; this._addMode = undefined; + if ( + this.value && + this.value[target.type] && + ensureArray(this.value[target.type]).includes(value) + ) { + return; + } fireEvent(this, "value-changed", { value: this.value ? { diff --git a/src/components/tile/ha-tile-image.ts b/src/components/tile/ha-tile-image.ts index 0990749412..783b22583b 100644 --- a/src/components/tile/ha-tile-image.ts +++ b/src/components/tile/ha-tile-image.ts @@ -1,14 +1,19 @@ import { CSSResultGroup, html, css, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; +import { ifDefined } from "lit/directives/if-defined"; @customElement("ha-tile-image") export class HaTileImage extends LitElement { @property() public imageUrl?: string; + @property() public imageAlt?: string; + protected render(): TemplateResult { return html`
- ${this.imageUrl ? html`` : null} + ${this.imageUrl + ? html`${ifDefined(this.imageAlt)}` + : null}
`; } diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts index 6bbc3c1af3..50ec6b4611 100644 --- a/src/components/user/ha-user-picker.ts +++ b/src/components/user/ha-user-picker.ts @@ -30,7 +30,9 @@ class HaUserPicker extends LitElement { return users .filter((user) => !user.system_generated) - .sort((a, b) => stringCompare(a.name, b.name)); + .sort((a, b) => + stringCompare(a.name, b.name, this.hass!.locale.language) + ); }); protected render(): TemplateResult { diff --git a/src/data/calendar.ts b/src/data/calendar.ts index 0b856de9b9..c0b9d2b072 100644 --- a/src/data/calendar.ts +++ b/src/data/calendar.ts @@ -50,6 +50,7 @@ export enum RecurrenceRange { export const enum CalendarEntityFeature { CREATE_EVENT = 1, DELETE_EVENT = 2, + UPDATE_EVENT = 4, } export const fetchCalendarEvents = async ( @@ -161,12 +162,18 @@ export const createCalendarEvent = ( export const updateCalendarEvent = ( hass: HomeAssistant, entityId: string, - event: CalendarEventMutableParams + uid: string, + event: CalendarEventMutableParams, + recurrence_id?: string, + recurrence_range?: RecurrenceRange ) => hass.callWS({ type: "calendar/event/update", entity_id: entityId, - event: event, + uid, + recurrence_id, + recurrence_range, + event, }); export const deleteCalendarEvent = ( diff --git a/src/data/conversation.ts b/src/data/conversation.ts index 4e7a504f11..b448876dfd 100644 --- a/src/data/conversation.ts +++ b/src/data/conversation.ts @@ -61,12 +61,14 @@ export const processConversationInput = ( hass: HomeAssistant, text: string, // eslint-disable-next-line: variable-name - conversation_id: string + conversation_id: string | null, + language: string ): Promise => hass.callWS({ type: "conversation/process", text, conversation_id, + language, }); export const getAgentInfo = (hass: HomeAssistant): Promise => diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts index a026221819..bc1437adfa 100644 --- a/src/data/device_registry.ts +++ b/src/data/device_registry.ts @@ -123,9 +123,12 @@ export const subscribeDeviceRegistry = ( onChange ); -export const sortDeviceRegistryByName = (entries: DeviceRegistryEntry[]) => +export const sortDeviceRegistryByName = ( + entries: DeviceRegistryEntry[], + language: string +) => entries.sort((entry1, entry2) => - caseInsensitiveStringCompare(entry1.name || "", entry2.name || "") + caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language) ); export const getDeviceEntityLookup = ( diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index a80562cb41..cbc5459b3b 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -29,6 +29,7 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry { original_icon?: string; device_class?: string; original_device_class?: string; + aliases: string[]; } export interface UpdateEntityRegistryEntryResult { @@ -63,6 +64,7 @@ export interface EntityRegistryEntryUpdateParams { new_entity_id?: string; options_domain?: string; options?: SensorEntityOptions | NumberEntityOptions | WeatherEntityOptions; + aliases?: string[]; } export const findBatteryEntity = ( @@ -109,6 +111,15 @@ export const getExtendedEntityRegistryEntry = ( entity_id: entityId, }); +export const getExtendedEntityRegistryEntries = ( + hass: HomeAssistant, + entityIds: string[] +): Promise> => + hass.callWS({ + type: "config/entity_registry/get_entries", + entity_ids: entityIds, + }); + export const updateEntityRegistryEntry = ( hass: HomeAssistant, entityId: string, @@ -162,9 +173,12 @@ export const subscribeEntityRegistry = ( onChange ); -export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) => +export const sortEntityRegistryByName = ( + entries: EntityRegistryEntry[], + language: string +) => entries.sort((entry1, entry2) => - caseInsensitiveStringCompare(entry1.name || "", entry2.name || "") + caseInsensitiveStringCompare(entry1.name || "", entry2.name || "", language) ); export const entityRegistryById = memoizeOne( diff --git a/src/data/update.ts b/src/data/update.ts index 802c1d0554..cc352c5116 100644 --- a/src/data/update.ts +++ b/src/data/update.ts @@ -68,7 +68,10 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) => entity_id: entityId, }); -export const filterUpdateEntities = (entities: HassEntities) => +export const filterUpdateEntities = ( + entities: HassEntities, + language?: string +) => ( Object.values(entities).filter( (entity) => computeStateDomain(entity) === "update" @@ -94,7 +97,8 @@ export const filterUpdateEntities = (entities: HassEntities) => } return caseInsensitiveStringCompare( a.attributes.title || a.attributes.friendly_name || "", - b.attributes.title || b.attributes.friendly_name || "" + b.attributes.title || b.attributes.friendly_name || "", + language ); }); @@ -110,7 +114,7 @@ export const checkForEntityUpdates = async ( element: HTMLElement, hass: HomeAssistant ) => { - const entities = filterUpdateEntities(hass.states).map( + const entities = filterUpdateEntities(hass.states, hass.locale.language).map( (entity) => entity.entity_id ); diff --git a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts index 74e3711235..94b6d22065 100644 --- a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts +++ b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts @@ -74,7 +74,7 @@ export class HaImagecropperDialog extends LitElement { round: Boolean(this._params?.options.round), })}" > - + ${this.hass.localize("ui.dialogs.image_cropper.crop_image")} ${this.hass.localize("ui.common.cancel")} diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 8b62007c9a..730ef8ad68 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -484,7 +484,11 @@ export class QuickBar extends LitElement { }; }) .sort((a, b) => - caseInsensitiveStringCompare(a.primaryText, b.primaryText) + caseInsensitiveStringCompare( + a.primaryText, + b.primaryText, + this.hass.locale.language + ) ); } @@ -494,7 +498,11 @@ export class QuickBar extends LitElement { ...this._generateServerControlCommands(), ...(await this._generateNavigationCommands()), ].sort((a, b) => - caseInsensitiveStringCompare(a.strings.join(" "), b.strings.join(" ")) + caseInsensitiveStringCompare( + a.strings.join(" "), + b.strings.join(" "), + this.hass.locale.language + ) ); } diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index 49c998bcb4..91577f65e8 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -13,7 +13,6 @@ import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../common/dom/fire_event"; import { SpeechRecognition } from "../../common/dom/speech-recognition"; -import { uid } from "../../common/util/uid"; import "../../components/ha-dialog"; import type { HaDialog } from "../../components/ha-dialog"; import "../../components/ha-icon-button"; @@ -60,7 +59,7 @@ export class HaVoiceCommandDialog extends LitElement { private recognition!: SpeechRecognition; - private _conversationId?: string; + private _conversationId: string | null = null; public async showDialog(): Promise { this._opened = true; @@ -175,7 +174,6 @@ export class HaVoiceCommandDialog extends LitElement { protected firstUpdated(changedProps: PropertyValues) { super.updated(changedProps); - this._conversationId = uid(); this._conversation = [ { who: "hass", @@ -211,18 +209,29 @@ export class HaVoiceCommandDialog extends LitElement { private _initRecognition() { this.recognition = new SpeechRecognition(); this.recognition.interimResults = true; - this.recognition.lang = "en-US"; + this.recognition.lang = this.hass.language; - this.recognition.onstart = () => { + this.recognition.addEventListener("start", () => { this.results = { final: false, transcript: "", }; - }; - this.recognition.onerror = (event) => { + }); + this.recognition.addEventListener("nomatch", () => { + this._addMessage({ + who: "user", + text: `<${this.hass.localize( + "ui.dialogs.voice_command.did_not_understand" + )}>`, + error: true, + }); + }); + this.recognition.addEventListener("error", (event) => { + // eslint-disable-next-line + console.error("Error recognizing text", event); this.recognition!.abort(); // @ts-ignore - if (event.error !== "aborted") { + if (event.error !== "aborted" && event.error !== "no-speech") { const text = this.results && this.results.transcript ? this.results.transcript @@ -232,8 +241,8 @@ export class HaVoiceCommandDialog extends LitElement { this._addMessage({ who: "user", text, error: true }); } this.results = null; - }; - this.recognition.onend = () => { + }); + this.recognition.addEventListener("end", () => { // Already handled by onerror if (this.results == null) { return; @@ -251,15 +260,14 @@ export class HaVoiceCommandDialog extends LitElement { error: true, }); } - }; - - this.recognition.onresult = (event) => { + }); + this.recognition.addEventListener("result", (event) => { const result = event.results[0]; this.results = { transcript: result[0].transcript, final: result.isFinal, }; - }; + }); } private async _processText(text: string) { @@ -277,8 +285,10 @@ export class HaVoiceCommandDialog extends LitElement { const response = await processConversationInput( this.hass, text, - this._conversationId! + this._conversationId, + this.hass.language ); + this._conversationId = response.conversation_id; const plain = response.response.speech?.plain; if (plain) { message.text = plain.speech; diff --git a/src/onboarding/integration-badge.ts b/src/onboarding/integration-badge.ts index 62e48d1b00..fc440b1589 100644 --- a/src/onboarding/integration-badge.ts +++ b/src/onboarding/integration-badge.ts @@ -19,6 +19,7 @@ class IntegrationBadge extends LitElement { return html`
{ - curInput.shadowRoot!.appendChild(createCurrencyListEl()); + curInput.shadowRoot!.appendChild( + createCurrencyListEl(this.hass.locale.language) + ); curInput.formElement.setAttribute("list", "currencies"); }); @@ -279,7 +281,9 @@ class OnboardingCoreConfig extends LitElement { "[name=country]" ) as HaTextField; countryInput.updateComplete.then(() => { - countryInput.shadowRoot!.appendChild(createCountryListEl()); + countryInput.shadowRoot!.appendChild( + createCountryListEl(this.hass.locale.language) + ); countryInput.formElement.setAttribute("list", "countries"); }); diff --git a/src/onboarding/onboarding-integrations.ts b/src/onboarding/onboarding-integrations.ts index 263ce23108..e35cdf15e2 100644 --- a/src/onboarding/onboarding-integrations.ts +++ b/src/onboarding/onboarding-integrations.ts @@ -117,7 +117,7 @@ class OnboardingIntegrations extends LitElement { } ); const content = [...entries, ...discovered] - .sort((a, b) => stringCompare(a[0], b[0])) + .sort((a, b) => stringCompare(a[0], b[0], this.hass.locale.language)) .map((item) => item[1]); return html` diff --git a/src/panels/calendar/dialog-calendar-event-detail.ts b/src/panels/calendar/dialog-calendar-event-detail.ts index 3f16f2c82c..8bc8f81512 100644 --- a/src/panels/calendar/dialog-calendar-event-detail.ts +++ b/src/panels/calendar/dialog-calendar-event-detail.ts @@ -4,15 +4,11 @@ import { addDays, isSameDay } from "date-fns/esm"; import { toDate } from "date-fns-tz"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property, state } from "lit/decorators"; -import { RRule, Weekday } from "rrule"; import { formatDate } from "../../common/datetime/format_date"; import { formatDateTime } from "../../common/datetime/format_date_time"; import { formatTime } from "../../common/datetime/format_time"; import { fireEvent } from "../../common/dom/fire_event"; -import { capitalizeFirstLetter } from "../../common/string/capitalize-first-letter"; import { isDate } from "../../common/string/is_date"; -import { dayNames } from "../../common/translations/day_names"; -import { monthNames } from "../../common/translations/month_names"; import "../../components/entity/state-info"; import "../../components/ha-date-input"; import "../../components/ha-time-input"; @@ -23,10 +19,10 @@ import { import { haStyleDialog } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import "../lovelace/components/hui-generic-entity-row"; -import "./ha-recurrence-rule-editor"; import { showConfirmEventDialog } from "./show-confirm-event-dialog-box"; import { CalendarEventDetailDialogParams } from "./show-dialog-calendar-event-detail"; import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor"; +import { renderRRuleAsText } from "./recurrence"; class DialogCalendarEventDetail extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -137,54 +133,16 @@ class DialogCalendarEventDetail extends LitElement { return ""; } try { - const rule = RRule.fromString(`RRULE:${value}`); - if (rule.isFullyConvertibleToText()) { - return html`
- ${capitalizeFirstLetter( - rule.toText( - this._translateRRuleElement, - { - dayNames: dayNames(this.hass.locale), - monthNames: monthNames(this.hass.locale), - tokens: {}, - }, - this._formatDate - ) - )} -
`; + const ruleText = renderRRuleAsText(this.hass, value); + if (ruleText !== undefined) { + return html`
${ruleText}
`; } - return html`
Cannot convert recurrence rule
`; } catch (e) { return "Error while processing the rule"; } } - private _translateRRuleElement = (id: string | number | Weekday): string => { - if (typeof id === "string") { - return this.hass.localize(`ui.components.calendar.event.rrule.${id}`); - } - - return ""; - }; - - private _formatDate = (year: number, month: string, day: number): string => { - if (!year || !month || !day) { - return ""; - } - - // Build date so we can then format it - const date = new Date(); - date.setFullYear(year); - // As input we already get the localized month name, so we now unfortunately - // need to convert it back to something Date can work with. The already localized - // months names are a must in the RRule.Language structure (an empty string[] would - // mean we get undefined months input in this method here). - date.setMonth(monthNames(this.hass.locale).indexOf(month)); - date.setDate(day); - return formatDate(date, this.hass.locale); - }; - private _formatDateRange() { // Parse a dates in the browser timezone const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; diff --git a/src/panels/calendar/dialog-calendar-event-editor.ts b/src/panels/calendar/dialog-calendar-event-editor.ts index 19965a748e..64a7aa3321 100644 --- a/src/panels/calendar/dialog-calendar-event-editor.ts +++ b/src/panels/calendar/dialog-calendar-event-editor.ts @@ -25,6 +25,8 @@ import { CalendarEventMutableParams, createCalendarEvent, deleteCalendarEvent, + updateCalendarEvent, + RecurrenceRange, } from "../../data/calendar"; import { haStyleDialog } from "../../resources/styles"; import { HomeAssistant } from "../../types"; @@ -49,7 +51,7 @@ class DialogCalendarEventEditor extends LitElement { @state() private _summary = ""; - @state() private _description = ""; + @state() private _description? = ""; @state() private _rrule?: string; @@ -85,12 +87,13 @@ class DialogCalendarEventEditor extends LitElement { const entry = params.entry!; this._allDay = isDate(entry.dtstart); this._summary = entry.summary; + this._description = entry.description; this._rrule = entry.rrule; if (this._allDay) { - this._dtstart = new Date(entry.dtstart); + this._dtstart = new Date(entry.dtstart + "T00:00:00"); // Calendar event end dates are exclusive, but not shown that way in the UI. The // reverse happens when persisting the event. - this._dtend = addDays(new Date(entry.dtend), -1); + this._dtend = addDays(new Date(entry.dtend + "T00:00:00"), -1); } else { this._dtstart = new Date(entry.dtstart); this._dtend = new Date(entry.dtend); @@ -168,6 +171,7 @@ class DialogCalendarEventEditor extends LitElement { class="summary" name="summary" .label=${this.hass.localize("ui.components.calendar.event.summary")} + .value=${this._summary} required @change=${this._handleSummaryChanged} error-message=${this.hass.localize("ui.common.error_required")} @@ -179,6 +183,7 @@ class DialogCalendarEventEditor extends LitElement { .label=${this.hass.localize( "ui.components.calendar.event.description" )} + .value=${this._description} @change=${this._handleDescriptionChanged} autogrow > @@ -244,6 +249,9 @@ class DialogCalendarEventEditor extends LitElement {
= this._dtstart!; + } + return this._dtend! > this._dtstart!; + } + private async _createEvent() { if (!this._summary || !this._calendarId) { this._error = this.hass.localize( @@ -420,7 +435,7 @@ class DialogCalendarEventEditor extends LitElement { return; } - if (this._dtend! <= this._dtstart!) { + if (!this._isValidStartEnd()) { this._error = this.hass.localize( "ui.components.calendar.event.invalid_duration" ); @@ -445,7 +460,61 @@ class DialogCalendarEventEditor extends LitElement { } private async _saveEvent() { - // to be implemented + if (!this._summary || !this._calendarId) { + this._error = this.hass.localize( + "ui.components.calendar.event.not_all_required_fields" + ); + return; + } + + if (!this._isValidStartEnd()) { + this._error = this.hass.localize( + "ui.components.calendar.event.invalid_duration" + ); + return; + } + + this._submitting = true; + const entry = this._params!.entry!; + let range: RecurrenceRange | undefined = RecurrenceRange.THISEVENT; + if (entry.recurrence_id) { + range = await showConfirmEventDialog(this, { + title: this.hass.localize( + "ui.components.calendar.event.confirm_update.update" + ), + 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" + ), + }); + } + if (range === undefined) { + // Cancel + this._submitting = false; + return; + } + try { + await updateCalendarEvent( + this.hass!, + this._calendarId!, + entry.uid!, + this._calculateData(), + entry.recurrence_id || "", + range! + ); + } catch (err: any) { + this._error = err ? err.message : "Unknown error"; + return; + } finally { + this._submitting = false; + } + await this._params!.updated(); + this.closeDialog(); } private async _deleteEvent() { diff --git a/src/panels/calendar/ha-full-calendar.ts b/src/panels/calendar/ha-full-calendar.ts index e0e2a114d3..9e4f3ad5e0 100644 --- a/src/panels/calendar/ha-full-calendar.ts +++ b/src/panels/calendar/ha-full-calendar.ts @@ -302,6 +302,9 @@ export class HAFullCalendar extends LitElement { private _handleEventClick(info): void { const entityStateObj = this.hass.states[info.event.extendedProps.calendar]; + const canEdit = + entityStateObj && + supportsFeature(entityStateObj, CalendarEntityFeature.UPDATE_EVENT); const canDelete = entityStateObj && supportsFeature(entityStateObj, CalendarEntityFeature.DELETE_EVENT); @@ -312,6 +315,7 @@ export class HAFullCalendar extends LitElement { updated: () => { this._fireViewChanged(); }, + canEdit: canEdit, canDelete: canDelete, }); } diff --git a/src/panels/calendar/ha-recurrence-rule-editor.ts b/src/panels/calendar/ha-recurrence-rule-editor.ts index 08e239b62a..b7b3c03f5e 100644 --- a/src/panels/calendar/ha-recurrence-rule-editor.ts +++ b/src/panels/calendar/ha-recurrence-rule-editor.ts @@ -1,11 +1,13 @@ import type { SelectedDetail } from "@material/mwc-list"; +import { formatInTimeZone, toDate } from "date-fns-tz"; import { css, html, LitElement, PropertyValues } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import type { Options, WeekdayStr } from "rrule"; import { ByWeekday, RRule, Weekday } from "rrule"; import { firstWeekdayIndex } from "../../common/datetime/first_weekday"; import { stopPropagation } from "../../common/dom/stop_propagation"; +import { LocalizeKeys } from "../../common/translations/localize"; import "../../components/ha-chip"; import "../../components/ha-list-item"; import "../../components/ha-select"; @@ -16,22 +18,31 @@ import { convertFrequency, convertRepeatFrequency, DEFAULT_COUNT, + getWeekday, getWeekdays, - intervalSuffix, + getMonthlyRepeatItems, RepeatEnd, RepeatFrequency, ruleByWeekDay, untilValue, - WEEKDAY_NAME, + MonthlyRepeatItem, + getMonthlyRepeatWeekdayFromRule, + getMonthdayRepeatFromRule, } from "./recurrence"; import "../../components/ha-date-input"; @customElement("ha-recurrence-rule-editor") export class RecurrenceRuleEditor extends LitElement { + @property() public hass!: HomeAssistant; + @property() public disabled = false; @property() public value = ""; + @property() public dtstart?: Date; + + @property() public allDay?: boolean; + @property({ attribute: false }) public locale!: HomeAssistant["locale"]; @property() public timezone?: string; @@ -44,14 +55,24 @@ export class RecurrenceRuleEditor extends LitElement { @state() private _weekday: Set = new Set(); + @state() private _monthlyRepeat?: string; + + @state() private _monthlyRepeatWeekday?: Weekday; + + @state() private _monthday?: number; + @state() private _end: RepeatEnd = "never"; @state() private _count?: number; - @state() private _until?: Date; + @state() private _untilDay?: Date; + + @query("#monthly") private _monthlyRepeatSelect!: HaSelect; private _allWeekdays?: WeekdayStr[]; + private _monthlyRepeatItems: MonthlyRepeatItem[] = []; + protected willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); @@ -61,15 +82,50 @@ export class RecurrenceRuleEditor extends LitElement { ); } - if (!changedProps.has("value") || this._computedRRule === this.value) { + if (changedProps.has("dtstart") || changedProps.has("_interval")) { + this._monthlyRepeatItems = this.dtstart + ? getMonthlyRepeatItems(this.hass, this._interval, this.dtstart) + : []; + this._computeWeekday(); + const selectElement = this._monthlyRepeatSelect; + if (selectElement) { + const oldSelected = selectElement.index; + selectElement.select(-1); + this.updateComplete.then(() => { + selectElement.select(changedProps.has("dtstart") ? 0 : oldSelected); + }); + } + } + + if ( + !changedProps.has("value") && + (changedProps.has("dtstart") || + changedProps.has("timezone") || + changedProps.has("_freq") || + changedProps.has("_interval") || + changedProps.has("_weekday") || + changedProps.has("_monthlyRepeatWeekday") || + changedProps.has("_monthday") || + changedProps.has("_end") || + changedProps.has("_count") || + changedProps.has("_untilDay")) + ) { + this._updateRule(); + return; + } + + if (this._computedRRule === this.value) { return; } this._interval = 1; this._weekday.clear(); + this._monthlyRepeat = undefined; + this._monthday = undefined; + this._monthlyRepeatWeekday = undefined; this._end = "never"; this._count = undefined; - this._until = undefined; + this._untilDay = undefined; this._computedRRule = this.value; if (this.value === "") { @@ -88,6 +144,14 @@ export class RecurrenceRuleEditor extends LitElement { if (rrule.interval) { this._interval = rrule.interval; } + this._monthlyRepeatWeekday = getMonthlyRepeatWeekdayFromRule(rrule); + if (this._monthlyRepeatWeekday) { + this._monthlyRepeat = `BYDAY=${this._monthlyRepeatWeekday.toString()}`; + } + this._monthday = getMonthdayRepeatFromRule(rrule); + if (this._monthday) { + this._monthlyRepeat = `BYMONTHDAY=${this._monthday}`; + } if ( this._freq === "weekly" && rrule.byweekday && @@ -101,7 +165,7 @@ export class RecurrenceRuleEditor extends LitElement { } if (rrule.until) { this._end = "on"; - this._until = rrule.until; + this._untilDay = toDate(rrule.until, { timeZone: this.timezone }); } else if (rrule.count) { this._end = "after"; this._count = rrule.count; @@ -112,24 +176,65 @@ export class RecurrenceRuleEditor extends LitElement { return html` - None - Yearly - Monthly - Weekly - Daily + + ${this.hass.localize("ui.components.calendar.event.repeat.freq.none")} + + + ${this.hass.localize( + "ui.components.calendar.event.repeat.freq.yearly" + )} + + + ${this.hass.localize( + "ui.components.calendar.event.repeat.freq.monthly" + )} + + + ${this.hass.localize( + "ui.components.calendar.event.repeat.freq.weekly" + )} + + + ${this.hass.localize( + "ui.components.calendar.event.repeat.freq.daily" + )} + `; } renderMonthly() { - return this.renderInterval(); + return html` + ${this.renderInterval()} + ${this._monthlyRepeatItems.length > 0 + ? html` + ${this._monthlyRepeatItems!.map( + (item) => html` + + ${item.label} + + ` + )} + ` + : html``} + `; } renderWeekly() { @@ -142,7 +247,11 @@ export class RecurrenceRuleEditor extends LitElement { .value=${item} class=${classMap({ active: this._weekday.has(item) })} @click=${this._onWeekdayToggle} - >${WEEKDAY_NAME[item]}${this.hass.localize( + `ui.components.calendar.event.repeat.weekly.weekday.${ + item.toLowerCase() as Lowercase + }` + )} ` )} @@ -158,11 +267,16 @@ export class RecurrenceRuleEditor extends LitElement { return html` `; @@ -172,26 +286,38 @@ export class RecurrenceRuleEditor extends LitElement { return html` - Never - After - On + + ${this.hass.localize("ui.components.calendar.event.repeat.end.never")} + + + ${this.hass.localize("ui.components.calendar.event.repeat.end.after")} + + + ${this.hass.localize("ui.components.calendar.event.repeat.end.on")} + ${this._end === "after" ? html` ` @@ -200,9 +326,11 @@ export class RecurrenceRuleEditor extends LitElement { ? html` ` @@ -222,7 +350,6 @@ export class RecurrenceRuleEditor extends LitElement { private _onIntervalChange(e: Event) { this._interval = (e.target! as any).value; - this._updateRule(); } private _onRepeatSelected(e: CustomEvent>) { @@ -233,9 +360,20 @@ export class RecurrenceRuleEditor extends LitElement { } if (this._freq !== "weekly") { this._weekday.clear(); + this._computeWeekday(); } e.stopPropagation(); - this._updateRule(); + } + + private _onMonthlyDetailSelected(e: CustomEvent>) { + e.stopPropagation(); + const selectedItem = this._monthlyRepeatItems[e.detail.index]; + if (!selectedItem) { + return; + } + this._monthlyRepeat = selectedItem.value; + this._monthlyRepeatWeekday = selectedItem.byday; + this._monthday = selectedItem.bymonthday; } private _onWeekdayToggle(e: MouseEvent) { @@ -246,7 +384,7 @@ export class RecurrenceRuleEditor extends LitElement { } else { this._weekday.delete(value); } - this._updateRule(); + this.requestUpdate("_weekday"); } private _onEndSelected(e: CustomEvent>) { @@ -259,44 +397,78 @@ export class RecurrenceRuleEditor extends LitElement { switch (this._end) { case "after": this._count = DEFAULT_COUNT[this._freq!]; - this._until = undefined; + this._untilDay = undefined; break; case "on": this._count = undefined; - this._until = untilValue(this._freq!); + this._untilDay = untilValue(this._freq!); break; default: this._count = undefined; - this._until = undefined; + this._untilDay = undefined; } e.stopPropagation(); - this._updateRule(); } private _onCountChange(e: Event) { this._count = (e.target! as any).value; - this._updateRule(); } private _onUntilChange(e: CustomEvent) { e.stopPropagation(); - this._until = new Date(e.detail.value); - this._updateRule(); + this._untilDay = toDate(e.detail.value + "T00:00:00", { + timeZone: this.timezone, + }); + } + + // Reset the weekday selected when there is only a single value + private _computeWeekday() { + if (this.dtstart && this._weekday.size <= 1) { + const weekdayNum = getWeekday(this.dtstart); + this._weekday.clear(); + this._weekday.add(new Weekday(weekdayNum).toString() as WeekdayStr); + } } private _computeRRule() { if (this._freq === undefined || this._freq === "none") { return ""; } - const options = { + let byweekday: Weekday[] | undefined; + let bymonthday: number | undefined; + if (this._freq === "monthly" && this._monthlyRepeatWeekday !== undefined) { + byweekday = [this._monthlyRepeatWeekday]; + } else if (this._freq === "monthly" && this._monthday !== undefined) { + bymonthday = this._monthday; + } else if (this._freq === "weekly") { + byweekday = ruleByWeekDay(this._weekday); + } + const options: Partial = { freq: convertRepeatFrequency(this._freq!)!, interval: this._interval > 1 ? this._interval : undefined, - byweekday: ruleByWeekDay(this._weekday), count: this._count, - until: this._until, - tzid: this.timezone, + byweekday: byweekday, + bymonthday: bymonthday, }; - const contentline = RRule.optionsToString(options); + let contentline = RRule.optionsToString(options); + if (this._untilDay) { + // The UNTIL value should be inclusive of the last event instance + const until = toDate( + this._formatDate(this._untilDay!) + + "T" + + this._formatTime(this.dtstart!), + { timeZone: this.timezone } + ); + // rrule.js can't compute some UNTIL variations so we compute that ourself. Must be + // in the same format as dtstart. + const format = this.allDay ? "yyyyMMdd" : "yyyyMMdd'T'HHmmss"; + const newUntilValue = formatInTimeZone( + until, + this.hass.config.time_zone, + format + ); + contentline += `;UNTIL=${newUntilValue}`; + } return contentline.slice(6); // Strip "RRULE:" prefix } @@ -315,6 +487,16 @@ export class RecurrenceRuleEditor extends LitElement { ); } + // Formats a date in browser display timezone + private _formatDate(date: Date): string { + return formatInTimeZone(date, this.timezone!, "yyyy-MM-dd"); + } + + // Formats a time in browser display timezone + private _formatTime(date: Date): string { + return formatInTimeZone(date, this.timezone!, "HH:mm:ss"); + } + static styles = css` ha-textfield, ha-select { diff --git a/src/panels/calendar/recurrence.ts b/src/panels/calendar/recurrence.ts index dce91b7d6b..ddc1d45729 100644 --- a/src/panels/calendar/recurrence.ts +++ b/src/panels/calendar/recurrence.ts @@ -1,8 +1,22 @@ // Library for converting back and forth from values use by this webcomponent // and the values defined by rrule.js. -import { RRule, Frequency, Weekday } from "rrule"; -import type { WeekdayStr } from "rrule"; -import { addDays, addMonths, addWeeks, addYears } from "date-fns"; +import { + addDays, + addMonths, + addWeeks, + addYears, + getDate, + getDay, + isLastDayOfMonth, + isSameMonth, +} from "date-fns"; +import type { Options, WeekdayStr } from "rrule"; +import { Frequency, RRule, Weekday } from "rrule"; +import { formatDate } from "../../common/datetime/format_date"; +import { capitalizeFirstLetter } from "../../common/string/capitalize-first-letter"; +import { dayNames } from "../../common/translations/day_names"; +import { monthNames } from "../../common/translations/month_names"; +import { HomeAssistant } from "../../types"; export type RepeatFrequency = | "none" @@ -21,14 +35,11 @@ export const DEFAULT_COUNT = { daily: 30, }; -export function intervalSuffix(freq: RepeatFrequency) { - if (freq === "monthly") { - return "months"; - } - if (freq === "weekly") { - return "weeks"; - } - return "days"; +export interface MonthlyRepeatItem { + value: string; + byday?: Weekday; + bymonthday?: number; + label: string; } export function untilValue(freq: RepeatFrequency): Date { @@ -81,16 +92,6 @@ export const convertRepeatFrequency = ( } }; -export const WEEKDAY_NAME = { - SU: "Sun", - MO: "Mon", - TU: "Tue", - WE: "Wed", - TH: "Thu", - FR: "Fri", - SA: "Sat", -}; - export const WEEKDAYS = [ RRule.SU, RRule.MO, @@ -101,7 +102,16 @@ export const WEEKDAYS = [ RRule.SA, ]; -export function getWeekdays(firstDay?: number) { +/** Return a weekday number compatible with rrule.js weekdays */ +export function getWeekday(dtstart: Date): number { + let weekDay = getDay(dtstart) - 1; + if (weekDay < 0) { + weekDay += 7; + } + return weekDay; +} + +export function getWeekdays(firstDay?: number): Weekday[] { if (firstDay === undefined || firstDay === 0) { return WEEKDAYS; } @@ -114,9 +124,7 @@ export function getWeekdays(firstDay?: number) { return weekDays; } -export function ruleByWeekDay( - weekdays: Set -): Weekday[] | undefined { +export function ruleByWeekDay(weekdays: Set): Weekday[] { return Array.from(weekdays).map((value: string) => { switch (value) { case "MO": @@ -138,3 +146,127 @@ export function ruleByWeekDay( } }); } + +/** + * Determine the recurrence options based on the day of the month. The + * return values are a Weekday object that represent a BYDAY for a + * particular week of the month like "first Saturday" or "last Friday". + */ +function getWeekydaysForMonth(dtstart: Date): Weekday[] { + const weekDay = getWeekday(dtstart); + const dayOfMonth = getDate(dtstart); + const nthWeekdayOfMonth = Math.floor((dayOfMonth - 1) / 7) + 1; + const isLastWeekday = !isSameMonth(dtstart, addDays(dtstart, 7)); + const byweekdays: Weekday[] = []; + if (!isLastWeekday || dayOfMonth <= 28) { + byweekdays.push(new Weekday(weekDay, nthWeekdayOfMonth)); + } + if (isLastWeekday) { + byweekdays.push(new Weekday(weekDay, -1)); + } + return byweekdays; +} + +/** + * Returns the list of repeat values available for the specified date. + */ +export function getMonthlyRepeatItems( + hass: HomeAssistant, + interval: number, + dtstart: Date +): MonthlyRepeatItem[] { + const getLabel = (repeatValue: string) => + renderRRuleAsText(hass, `FREQ=MONTHLY;INTERVAL=${interval};${repeatValue}`); + + const result: MonthlyRepeatItem[] = [ + // The default repeat rule is on day of month e.g. 3rd day of month + { + value: `BYMONTHDAY=${getDate(dtstart)}`, + label: getLabel(`BYMONTHDAY=${getDate(dtstart)}`)!, + }, + // Additional optional rules based on the week of month e.g. 2nd sunday of month + ...getWeekydaysForMonth(dtstart).map((item) => ({ + value: `BYDAY=${item.toString()}`, + byday: item, + label: getLabel(`BYDAY=${item.toString()}`)!, + })), + ]; + if (isLastDayOfMonth(dtstart)) { + result.push({ + value: "BYMONTHDAY=-1", + bymonthday: -1, + label: getLabel(`BYMONTHDAY=-1`)!, + }); + } + return result; +} + +export function getMonthlyRepeatWeekdayFromRule( + rrule: Partial +): Weekday | undefined { + if (rrule.freq !== Frequency.MONTHLY) { + return undefined; + } + if ( + rrule.byweekday && + Array.isArray(rrule.byweekday) && + rrule.byweekday.length === 1 && + rrule.byweekday[0] instanceof Weekday + ) { + return rrule.byweekday[0]; + } + return undefined; +} + +export function getMonthdayRepeatFromRule( + rrule: Partial +): number | undefined { + if (rrule.freq !== Frequency.MONTHLY || !rrule.bymonthday) { + return undefined; + } + if (Array.isArray(rrule.bymonthday)) { + return rrule.bymonthday[0]; + } + return rrule.bymonthday; +} + +/** + * A wrapper around RRule.toText that assists with translation. + */ +export function renderRRuleAsText(hass: HomeAssistant, value: string) { + const rule = RRule.fromString(`RRULE:${value}`); + if (!rule.isFullyConvertibleToText()) { + return undefined; + } + return capitalizeFirstLetter( + rule.toText( + (id: string | number | Weekday): string => { + if (typeof id === "string") { + return hass.localize(`ui.components.calendar.event.rrule.${id}`); + } + return ""; + }, + { + dayNames: dayNames(hass.locale), + monthNames: monthNames(hass.locale), + tokens: {}, + }, + // Format the date + (year: number, month: string, day: number): string => { + if (!year || !month || !day) { + return ""; + } + // Build date so we can then format it + const date = new Date(); + date.setFullYear(year); + // As input we already get the localized month name, so we now unfortunately + // need to convert it back to something Date can work with. The already localized + // months names are a must in the RRule.Language structure (an empty string[] would + // mean we get undefined months input in this method here). + date.setMonth(monthNames(hass.locale).indexOf(month)); + date.setDate(day); + return formatDate(date, hass.locale); + } + ) + ); +} diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index e4e3911d2d..c03ed944ff 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -192,13 +192,13 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { devices.forEach((entry) => { entry.name = computeDeviceName(entry, this.hass); }); - sortDeviceRegistryByName(devices); + sortDeviceRegistryByName(devices, this.hass.locale.language); } if (entities) { entities.forEach((entry) => { entry.name = computeEntityRegistryName(this.hass, entry); }); - sortEntityRegistryByName(entities); + sortEntityRegistryByName(entities, this.hass.locale.language); } // Group entities by domain @@ -258,7 +258,8 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
${area.picture ? html`
- + - caseInsensitiveStringCompare(entry1.name!, entry2.name!) + caseInsensitiveStringCompare( + entry1.name!, + entry2.name!, + this.hass.locale.language + ) ); } if (relatedEntityIds?.length) { @@ -521,7 +526,11 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { } }); relatedEntities.sort((entry1, entry2) => - caseInsensitiveStringCompare(entry1.name!, entry2.name!) + caseInsensitiveStringCompare( + entry1.name!, + entry2.name!, + this.hass.locale.language + ) ); } diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index 6c4827a633..a35fef3b91 100644 --- a/src/panels/config/automation/action/ha-automation-action.ts +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -299,7 +299,7 @@ export default class HaAutomationAction extends LitElement { icon, ] as [string, string, string] ) - .sort((a, b) => stringCompare(a[1], b[1])) + .sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language)) ); static get styles(): CSSResultGroup { diff --git a/src/panels/config/automation/action/types/ha-automation-action-condition.ts b/src/panels/config/automation/action/types/ha-automation-action-condition.ts index a21b4bb9d5..6123678139 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-condition.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-condition.ts @@ -66,7 +66,7 @@ export class HaConditionAction extends LitElement implements ActionElement { icon, ] as [string, string, string] ) - .sort((a, b) => stringCompare(a[1], b[1])) + .sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language)) ); private _conditionChanged(ev: CustomEvent) { diff --git a/src/panels/config/automation/action/types/ha-automation-action-device_id.ts b/src/panels/config/automation/action/types/ha-automation-action-device_id.ts index f4cf9cf800..37d641adba 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-device_id.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-device_id.ts @@ -158,6 +158,7 @@ export class HaDeviceAction extends LitElement { } ha-form { + display: block; margin-top: 24px; } `; diff --git a/src/panels/config/automation/condition/ha-automation-condition.ts b/src/panels/config/automation/condition/ha-automation-condition.ts index 2b0bd7e1c2..4d318d09cd 100644 --- a/src/panels/config/automation/condition/ha-automation-condition.ts +++ b/src/panels/config/automation/condition/ha-automation-condition.ts @@ -328,7 +328,7 @@ export default class HaAutomationCondition extends LitElement { icon, ] as [string, string, string] ) - .sort((a, b) => stringCompare(a[1], b[1])) + .sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language)) ); static get styles(): CSSResultGroup { diff --git a/src/panels/config/automation/ha-automation-trace.ts b/src/panels/config/automation/ha-automation-trace.ts index 8a53c9aee2..cf087b5ca1 100644 --- a/src/panels/config/automation/ha-automation-trace.ts +++ b/src/panels/config/automation/ha-automation-trace.ts @@ -541,15 +541,10 @@ export class HaAutomationTrace extends LitElement { justify-content: center; display: flex; } - .info { flex: 1; background-color: var(--card-background-color); } - - .linkButton { - color: var(--primary-text-color); - } .trace-link { text-decoration: none; } diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts index 74534e457a..c53738ef86 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts @@ -302,7 +302,7 @@ export default class HaAutomationTrigger extends LitElement { icon, ] as [string, string, string] ) - .sort((a, b) => stringCompare(a[1], b[1])) + .sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language)) ); static get styles(): CSSResultGroup { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-calendar.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-calendar.ts index c9cccc4795..846d22a5d5 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-calendar.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-calendar.ts @@ -46,7 +46,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement { ], ], }, - { name: "offset", selector: { duration: { enable_day: true } } }, + { name: "offset", selector: { duration: {} } }, { name: "offset_type", type: "select", diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts index 4d09932c75..e9c13c24fb 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts @@ -54,7 +54,11 @@ export class HaTagTrigger extends LitElement implements TriggerElement { private async _fetchTags() { this._tags = (await fetchTags(this.hass)).sort((a, b) => - caseInsensitiveStringCompare(a.name || a.id, b.name || b.id) + caseInsensitiveStringCompare( + a.name || a.id, + b.name || b.id, + this.hass.locale.language + ) ); } diff --git a/src/panels/config/cloud/alexa/cloud-alexa.ts b/src/panels/config/cloud/alexa/cloud-alexa.ts index 0ac078e360..8c666890dc 100644 --- a/src/panels/config/cloud/alexa/cloud-alexa.ts +++ b/src/panels/config/cloud/alexa/cloud-alexa.ts @@ -9,7 +9,6 @@ import { mdiFormatListChecks, mdiSync, } from "@mdi/js"; -import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; @@ -41,22 +40,17 @@ import { updateCloudAlexaEntityConfig, updateCloudPref, } from "../../../../data/cloud"; -import { - EntityRegistryEntry, - subscribeEntityRegistry, -} from "../../../../data/entity_registry"; +import { EntityRegistryEntry } from "../../../../data/entity_registry"; import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-subpage"; -import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; const DEFAULT_CONFIG_EXPOSE = true; -const IGNORE_INTERFACES = ["Alexa.EndpointHealth"]; @customElement("cloud-alexa") -class CloudAlexa extends SubscribeMixin(LitElement) { +class CloudAlexa extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() @@ -168,13 +162,8 @@ class CloudAlexa extends SubscribeMixin(LitElement) { - ${entity.interfaces - .filter((ifc) => !IGNORE_INTERFACES.includes(ifc)) - .map((ifc) => ifc.replace(/(Alexa.|Controller)/g, "")) - .join(", ")} ${!emptyFilter ? html`${iconButton}` @@ -323,23 +312,18 @@ class CloudAlexa extends SubscribeMixin(LitElement) { if (changedProps.has("cloudStatus")) { this._entityConfigs = this.cloudStatus.prefs.alexa_entity_configs; } - } + if ( + changedProps.has("hass") && + changedProps.get("hass")?.entities !== this.hass.entities + ) { + const categories = {}; - protected override hassSubscribe(): ( - | UnsubscribeFunc - | Promise - )[] { - return [ - subscribeEntityRegistry(this.hass.connection, (entries) => { - const categories = {}; + for (const entry of Object.values(this.hass.entities)) { + categories[entry.entity_id] = entry.entity_category; + } - for (const entry of entries) { - categories[entry.entity_id] = entry.entity_category; - } - - this._entityCategories = categories; - }), - ]; + this._entityCategories = categories; + } } private async _fetchData() { @@ -349,7 +333,8 @@ class CloudAlexa extends SubscribeMixin(LitElement) { const stateB = this.hass.states[b.entity_id]; return stringCompare( stateA ? computeStateName(stateA) : a.entity_id, - stateB ? computeStateName(stateB) : b.entity_id + stateB ? computeStateName(stateB) : b.entity_id, + this.hass.locale.language ); }); this._entities = entities; @@ -541,6 +526,7 @@ class CloudAlexa extends SubscribeMixin(LitElement) { } state-info { cursor: pointer; + height: 40px; } ha-switch { padding: 8px 0; diff --git a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts index da22c0fc0b..c11ad9c82f 100644 --- a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts +++ b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts @@ -9,7 +9,6 @@ import { mdiFormatListChecks, mdiSync, } from "@mdi/js"; -import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; @@ -41,7 +40,9 @@ import { } from "../../../../data/cloud"; import { EntityRegistryEntry, - subscribeEntityRegistry, + ExtEntityRegistryEntry, + getExtendedEntityRegistryEntries, + updateEntityRegistryEntry, } from "../../../../data/entity_registry"; import { fetchCloudGoogleEntities, @@ -51,15 +52,15 @@ import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-subpage"; -import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; -import { haStyle } from "../../../../resources/styles"; +import { buttonLinkStyle, haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import { showToast } from "../../../../util/toast"; +import { showEntityAliasesDialog } from "../../entities/entity-aliases/show-dialog-entity-aliases"; const DEFAULT_CONFIG_EXPOSE = true; @customElement("cloud-google-assistant") -class CloudGoogleAssistant extends SubscribeMixin(LitElement) { +class CloudGoogleAssistant extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public cloudStatus!: CloudStatusLoggedIn; @@ -68,6 +69,8 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { @state() private _entities?: GoogleEntity[]; + @state() private _entries?: { [id: string]: ExtEntityRegistryEntry }; + @state() private _syncing = false; @state() @@ -164,6 +167,8 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { : mdiCloseBoxMultiple} >`; + const aliases = this._entries?.[entity.entity_id]?.aliases; + target.push(html`
@@ -174,15 +179,57 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { secondary-line @click=${this._showMoreInfo} > - ${entity.traits - .map((trait) => trait.substr(trait.lastIndexOf(".") + 1)) - .join(", ")} + ${aliases + ? html` + + ${aliases.length > 0 + ? [...aliases] + .sort((a, b) => + stringCompare(a, b, this.hass.locale.language) + ) + .join(", ") + : this.hass.localize( + "ui.panel.config.cloud.google.no_aliases" + )} + +
+ + ` + : html` + + ${this.hass.localize( + "ui.panel.config.cloud.google.aliases_not_available" + )} + +
+ + `} ${!emptyFilter ? html`${iconButton}` : html` ${iconButton} @@ -308,7 +355,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { ${!this.narrow ? this.hass!.localize( - "ui.panel.config.cloud.alexa.exposed", + "ui.panel.config.cloud.google.exposed", "selected", selected ) @@ -329,7 +376,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { ${!this.narrow ? this.hass!.localize( - "ui.panel.config.cloud.alexa.not_exposed", + "ui.panel.config.cloud.google.not_exposed", "selected", this._entities.length - selected ) @@ -354,23 +401,38 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { if (changedProps.has("cloudStatus")) { this._entityConfigs = this.cloudStatus.prefs.google_entity_configs; } + if ( + changedProps.has("hass") && + changedProps.get("hass")?.entities !== this.hass.entities + ) { + const categories = {}; + + for (const entry of Object.values(this.hass.entities)) { + categories[entry.entity_id] = entry.entity_category; + } + + this._entityCategories = categories; + } } - protected override hassSubscribe(): ( - | UnsubscribeFunc - | Promise - )[] { - return [ - subscribeEntityRegistry(this.hass.connection, (entries) => { - const categories = {}; - - for (const entry of entries) { - categories[entry.entity_id] = entry.entity_category; - } - - this._entityCategories = categories; - }), - ]; + private async _openAliasesSettings(ev) { + ev.stopPropagation(); + const entityId = ev.target.entityId; + const entry = this._entries![entityId]; + if (!entry) { + return; + } + showEntityAliasesDialog(this, { + entity: entry, + updateEntry: async (updates) => { + const { entity_entry } = await updateEntityRegistryEntry( + this.hass, + entry.entity_id, + updates + ); + this._entries![entity_entry.entity_id] = entity_entry; + }, + }); } private _configIsDomainExposed( @@ -397,12 +459,20 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { private async _fetchData() { const entities = await fetchCloudGoogleEntities(this.hass); + this._entries = await getExtendedEntityRegistryEntries( + this.hass, + entities + .filter((ent) => this.hass.entities[ent.entity_id]) + .map((e) => e.entity_id) + ); + entities.sort((a, b) => { const stateA = this.hass.states[a.entity_id]; const stateB = this.hass.states[b.entity_id]; return stringCompare( stateA ? computeStateName(stateA) : a.entity_id, - stateB ? computeStateName(stateB) : b.entity_id + stateB ? computeStateName(stateB) : b.entity_id, + this.hass.locale.language ); }); this._entities = entities; @@ -410,7 +480,14 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { private _showMoreInfo(ev) { const entityId = ev.currentTarget.stateObj.entity_id; - fireEvent(this, "hass-more-info", { entityId }); + const moreInfoTab = ev.currentTarget.moreInfoTab; + fireEvent(this, "hass-more-info", { entityId, tab: moreInfoTab }); + } + + private _showMoreInfoSettings(ev) { + ev.stopPropagation(); + const entityId = ev.currentTarget.stateObj.entity_id; + fireEvent(this, "hass-more-info", { entityId, tab: "settings" }); } private async _exposeChanged(ev: CustomEvent) { @@ -582,6 +659,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { static get styles(): CSSResultGroup { return [ haStyle, + buttonLinkStyle, css` mwc-list-item > [slot="meta"] { margin-left: 4px; diff --git a/src/panels/config/core/ha-config-section-general.ts b/src/panels/config/core/ha-config-section-general.ts index 462f23fd9a..900b5ccdac 100644 --- a/src/panels/config/core/ha-config-section-general.ts +++ b/src/panels/config/core/ha-config-section-general.ts @@ -304,7 +304,11 @@ class HaConfigSectionGeneral extends LitElement { } this._languages = Object.entries(this.hass.translationMetadata.translations) .sort((a, b) => - caseInsensitiveStringCompare(a[1].nativeName, b[1].nativeName) + caseInsensitiveStringCompare( + a[1].nativeName, + b[1].nativeName, + this.hass.locale.language + ) ) .map(([value, metaData]) => ({ value, diff --git a/src/panels/config/devices/device-detail/ha-device-via-devices-card.ts b/src/panels/config/devices/device-detail/ha-device-via-devices-card.ts index 3a0c276d6f..b437c42938 100644 --- a/src/panels/config/devices/device-detail/ha-device-via-devices-card.ts +++ b/src/panels/config/devices/device-detail/ha-device-via-devices-card.ts @@ -31,7 +31,8 @@ export class HaDeviceViaDevicesCard extends LitElement { .sort((d1, d2) => caseInsensitiveStringCompare( computeDeviceName(d1, this.hass), - computeDeviceName(d2, this.hass) + computeDeviceName(d2, this.hass), + this.hass.locale.language ) ) ); diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 143f2a8f51..2a0cc232a6 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -157,7 +157,8 @@ export class HaConfigDevicePage extends LitElement { .sort((ent1, ent2) => stringCompare( ent1.stateName || `zzz${ent1.entity_id}`, - ent2.stateName || `zzz${ent2.entity_id}` + ent2.stateName || `zzz${ent2.entity_id}`, + this.hass.locale.language ) ) ); @@ -658,6 +659,10 @@ export class HaConfigDevicePage extends LitElement { integrations.length ? html` ${domainToName( { + this._params = params; + this._error = undefined; + this._aliases = + this._params.entity.aliases?.length > 0 + ? [...this._params.entity.aliases].sort() + : [""]; + await this.updateComplete; + } + + public closeDialog(): void { + this._error = ""; + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + + const entityId = this._params.entity.entity_id; + const stateObj = entityId ? this.hass.states[entityId] : undefined; + + const name = (stateObj && computeStateName(stateObj)) || entityId; + + return html` + +
+ ${this._error + ? html`${this._error} ` + : ""} +
+ ${this._aliases.map( + (alias, index) => html` +
+ + +
+ ` + )} +
+ + ${this.hass!.localize( + "ui.dialogs.entity_registry.editor.aliases.add_alias" + )} + + +
+
+
+ + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.aliases.save" + )} + +
+ `; + } + + private async _addAlias() { + this._aliases = [...this._aliases, ""]; + await this.updateComplete; + const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as + | HaTextField + | undefined; + field?.focus(); + } + + private async _editAlias(ev: Event) { + const index = (ev.target as any).index; + this._aliases[index] = (ev.target as any).value; + } + + private async _keyDownAlias(ev: KeyboardEvent) { + if (ev.key === "Enter") { + ev.stopPropagation(); + this._addAlias(); + } + } + + private async _removeAlias(ev: Event) { + const index = (ev.target as any).index; + const aliases = [...this._aliases]; + aliases.splice(index, 1); + this._aliases = aliases; + } + + private async _updateEntry(): Promise { + this._submitting = true; + const noEmptyAliases = this._aliases + .map((alias) => alias.trim()) + .filter((alias) => alias); + + try { + await this._params!.updateEntry({ + aliases: noEmptyAliases, + }); + this.closeDialog(); + } catch (err: any) { + this._error = + err.message || + this.hass.localize( + "ui.dialogs.entity_registry.editor.aliases.unknown_error" + ); + } finally { + this._submitting = false; + } + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + .row { + margin-bottom: 8px; + } + ha-textfield { + display: block; + } + ha-icon-button { + display: block; + } + mwc-button { + margin-left: 8px; + } + #alias_input { + margin-top: 8px; + } + .alias { + border: 1px solid var(--divider-color); + border-radius: 4px; + margin-top: 4px; + --mdc-icon-button-size: 24px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-entity-aliases": DialogEntityAliases; + } +} diff --git a/src/panels/config/entities/entity-aliases/show-dialog-entity-aliases.ts b/src/panels/config/entities/entity-aliases/show-dialog-entity-aliases.ts new file mode 100644 index 0000000000..0e9cbd9357 --- /dev/null +++ b/src/panels/config/entities/entity-aliases/show-dialog-entity-aliases.ts @@ -0,0 +1,25 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import { + EntityRegistryEntryUpdateParams, + ExtEntityRegistryEntry, +} from "../../../../data/entity_registry"; + +export interface EntityAliasesDialogParams { + entity: ExtEntityRegistryEntry; + updateEntry: ( + updates: Partial + ) => Promise; +} + +export const loadEntityAliasesDialog = () => import("./dialog-entity-aliases"); + +export const showEntityAliasesDialog = ( + element: HTMLElement, + entityAliasesParams: EntityAliasesDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-entity-aliases", + dialogImport: loadEntityAliasesDialog, + dialogParams: entityAliasesParams, + }); +}; diff --git a/src/panels/config/entities/entity-registry-basic-editor.ts b/src/panels/config/entities/entity-registry-basic-editor.ts index 3d915e2db4..680a66bf7b 100644 --- a/src/panels/config/entities/entity-registry-basic-editor.ts +++ b/src/panels/config/entities/entity-registry-basic-editor.ts @@ -1,8 +1,13 @@ import "@material/mwc-formfield/mwc-formfield"; +import "@material/mwc-list/mwc-list"; +import "@material/mwc-list/mwc-list-item"; +import { mdiPencil } from "@mdi/js"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; +import { stringCompare } from "../../../common/string/compare"; import "../../../components/ha-area-picker"; import "../../../components/ha-expansion-panel"; import "../../../components/ha-radio"; @@ -21,6 +26,7 @@ import { import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../types"; +import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases"; @customElement("ha-registry-basic-editor") export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { @@ -44,6 +50,21 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { @state() private _submitting = false; + private _handleAliasesClicked(ev: CustomEvent) { + if (ev.detail.index !== 0) return; + showEntityAliasesDialog(this, { + entity: this.entry!, + updateEntry: async (updates) => { + const result = await updateEntityRegistryEntry( + this.hass, + this.entry.entity_id, + updates + ); + fireEvent(this, "entity-entry-updated", result.entity_entry); + }, + }); + } + public async updateEntry(): Promise { this._submitting = true; const params: Partial = { @@ -247,6 +268,37 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
` : ""} + +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.aliases_section" + )} +
+ + 0} hasMeta> + + ${this.entry.aliases.length > 0 + ? this.hass.localize( + "ui.dialogs.entity_registry.editor.configured_aliases", + { count: this.entry.aliases.length } + ) + : this.hass.localize( + "ui.dialogs.entity_registry.editor.no_aliases" + )} + + + ${[...this.entry.aliases] + .sort((a, b) => stringCompare(a, b, this.hass.locale.language)) + .join(", ")} + + + + +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.aliases.description" + )} +
`; } @@ -300,6 +352,13 @@ export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) { .label { margin-top: 16px; } + .aliases { + border-radius: 4px; + margin-top: 4px; + margin-bottom: 4px; + --mdc-icon-button-size: 24px; + overflow: hidden; + } `; } } diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 21f1fbd1cd..b1061a9f43 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -1,6 +1,7 @@ import "@material/mwc-button/mwc-button"; import "@material/mwc-formfield/mwc-formfield"; import "@material/mwc-list/mwc-list-item"; +import { mdiPencil } from "@mdi/js"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, @@ -26,6 +27,7 @@ import { import "../../../components/ha-alert"; import "../../../components/ha-area-picker"; import "../../../components/ha-expansion-panel"; +import "../../../components/ha-icon"; import "../../../components/ha-icon-picker"; import "../../../components/ha-radio"; import "../../../components/ha-select"; @@ -75,6 +77,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail"; +import { showEntityAliasesDialog } from "./entity-aliases/show-dialog-entity-aliases"; const OVERRIDE_DEVICE_CLASSES = { cover: [ @@ -115,6 +118,43 @@ const OVERRIDE_NUMBER_UNITS = { }; const OVERRIDE_SENSOR_UNITS = { + current: ["A", "mA"], + data_rate: [ + "bit/s", + "kbit/s", + "Mbit/s", + "Gbit/s", + "B/s", + "kB/s", + "MB/s", + "GB/s", + "KiB/s", + "MiB/s", + "GiB/s", + ], + data_size: [ + "bit", + "kbit", + "Mbit", + "Gbit", + "B", + "kB", + "MB", + "GB", + "TB", + "PB", + "EB", + "ZB", + "YB", + "KiB", + "MiB", + "GiB", + "TiB", + "PiB", + "EiB", + "ZiB", + "YiB", + ], distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"], gas: ["CCF", "ft³", "m³"], precipitation: ["cm", "in", "mm"], @@ -122,6 +162,7 @@ const OVERRIDE_SENSOR_UNITS = { pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"], temperature: ["°C", "°F", "K"], + voltage: ["V", "mV"], volume: ["CCF", "fl. oz.", "ft³", "gal", "L", "mL", "m³"], water: ["CCF", "ft³", "gal", "L", "m³"], weight: ["g", "kg", "lb", "mg", "oz", "st", "µg"], @@ -673,7 +714,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
${this.hass.localize( "ui.dialogs.entity_registry.editor.entity_status" - )}: + )}
${this._disabledBy && @@ -760,12 +801,45 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
` : ""} + +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.aliases_section" + )} +
+ + 0} hasMeta> + + ${this.entry.aliases.length > 0 + ? this.hass.localize( + "ui.dialogs.entity_registry.editor.configured_aliases", + { count: this.entry.aliases.length } + ) + : this.hass.localize( + "ui.dialogs.entity_registry.editor.no_aliases" + )} + + + ${[...this.entry.aliases] + .sort((a, b) => + stringCompare(a, b, this.hass.locale.language) + ) + .join(", ")} + + + + +
+ ${this.hass.localize( + "ui.dialogs.entity_registry.editor.aliases.description" + )} +
${this.entry.device_id ? html`
${this.hass.localize( "ui.dialogs.entity_registry.editor.change_area" - )}: + )}
{ + const result = await updateEntityRegistryEntry( + this.hass, + this.entry.entity_id, + updates + ); + fireEvent(this, "entity-entry-updated", result.entity_entry); + }, + }); + } + private async _enableEntry() { this._error = undefined; this._submitting = true; @@ -1143,7 +1232,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { domain: entry, label: domainToName(localize, entry), })) - .sort((a, b) => stringCompare(a.label, b.label)) + .sort((a, b) => + stringCompare(a.label, b.label, this.hass.locale.language) + ) ); private _deviceClassesSorted = memoizeOne( @@ -1155,7 +1246,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { `ui.dialogs.entity_registry.editor.device_classes.${domain}.${entry}` ), })) - .sort((a, b) => stringCompare(a.label, b.label)) + .sort((a, b) => + stringCompare(a.label, b.label, this.hass.locale.language) + ) ); static get styles(): CSSResultGroup { @@ -1212,7 +1305,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { } .secondary { margin: 8px 0; - width: 340px; } li[divider] { border-bottom-color: var(--divider-color); @@ -1220,6 +1312,13 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ha-alert mwc-button { width: max-content; } + .aliases { + border-radius: 4px; + margin-top: 4px; + margin-bottom: 4px; + --mdc-icon-button-size: 24px; + overflow: hidden; + } `, ]; } diff --git a/src/panels/config/hardware/dialog-hardware-available.ts b/src/panels/config/hardware/dialog-hardware-available.ts index aee0f828e7..d99a15c723 100644 --- a/src/panels/config/hardware/dialog-hardware-available.ts +++ b/src/panels/config/hardware/dialog-hardware-available.ts @@ -20,7 +20,12 @@ import { haStyle, haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; const _filterDevices = memoizeOne( - (showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) => + ( + showAdvanced: boolean, + hardware: HassioHardwareInfo, + filter: string, + language: string + ) => hardware.devices .filter( (device) => @@ -33,7 +38,7 @@ const _filterDevices = memoizeOne( .toLocaleLowerCase() .includes(filter)) ) - .sort((a, b) => stringCompare(a.name, b.name)) + .sort((a, b) => stringCompare(a.name, b.name, language)) ); @customElement("ha-dialog-hardware-available") @@ -70,7 +75,8 @@ class DialogHardwareAvailable extends LitElement implements HassDialog { const devices = _filterDevices( this.hass.userData?.showAdvanced || false, this._hardware, - (this._filter || "").toLowerCase() + (this._filter || "").toLowerCase(), + this.hass.locale.language ); return html` diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index 386335469a..c3a0524735 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -305,7 +305,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { .twoline=${Boolean(boardId)} > ${imageURL - ? html`` + ? html`` : ""} ${boardName || diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts index fb8367ee15..15b52126d4 100644 --- a/src/panels/config/integrations/dialog-add-integration.ts +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -147,7 +147,13 @@ class AddIntegrationDialog extends LitElement { is_built_in: true, is_add: true, })) - .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)); + .sort((a, b) => + caseInsensitiveStringCompare( + a.name, + b.name, + this.hass.locale.language + ) + ); const integrations: IntegrationListItem[] = []; const yamlIntegrations: IntegrationListItem[] = []; @@ -242,7 +248,11 @@ class AddIntegrationDialog extends LitElement { return [ ...addDeviceRows, ...integrations.sort((a, b) => - caseInsensitiveStringCompare(a.name || "", b.name || "") + caseInsensitiveStringCompare( + a.name || "", + b.name || "", + this.hass.locale.language + ) ), ]; } diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 3ba289ae99..aaea690b57 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -230,7 +230,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { (conf1, conf2) => caseInsensitiveStringCompare( conf1.localized_domain_name + conf1.title, - conf2.localized_domain_name + conf2.title + conf2.localized_domain_name + conf2.title, + this.hass.locale.language ) ); }, diff --git a/src/panels/config/integrations/ha-domain-integrations.ts b/src/panels/config/integrations/ha-domain-integrations.ts index 21f75f28d3..f62461b553 100644 --- a/src/panels/config/integrations/ha-domain-integrations.ts +++ b/src/panels/config/integrations/ha-domain-integrations.ts @@ -48,6 +48,7 @@ class HaDomainIntegrations extends LitElement { hasMeta >
- +
Re-configure MQTT${this.hass.localize( + "ui.panel.config.mqtt.reconfigure" + )}
@@ -53,12 +60,12 @@ class HaPanelDevMqtt extends LitElement {
${qosLevel.map( (qos) => @@ -70,17 +77,35 @@ class HaPanelDevMqtt extends LitElement { >
-

${this.hass.localize("ui.panel.config.mqtt.payload")}

+

+ + + +

+

+ ${this._allowTemplate + ? this.hass.localize("ui.panel.config.mqtt.payload") + : this.hass.localize( + "ui.panel.config.mqtt.payload_no_template" + )} +

@@ -101,22 +126,26 @@ class HaPanelDevMqtt extends LitElement { } private _handleTopic(ev: CustomEvent) { - this.topic = (ev.target! as any).value; + this._topic = (ev.target! as any).value; } private _handlePayload(ev: CustomEvent) { - this.payload = ev.detail.value; + this._payload = ev.detail.value; } private _handleQos(ev: CustomEvent) { const newValue = (ev.target! as any).value; - if (newValue >= 0 && newValue !== this.qos) { - this.qos = newValue; + if (newValue >= 0 && newValue !== this._qos) { + this._qos = newValue; } } private _handleRetain(ev: CustomEvent) { - this.retain = (ev.target! as any).checked; + this._retain = (ev.target! as any).checked; + } + + private _handleAllowTemplate(ev: CustomEvent) { + this._allowTemplate = (ev.target! as any).checked; } private _publish(): void { @@ -124,10 +153,11 @@ class HaPanelDevMqtt extends LitElement { return; } this.hass.callService("mqtt", "publish", { - topic: this.topic, - payload_template: this.payload, - qos: parseInt(this.qos), - retain: this.retain, + topic: this._topic, + payload: !this._allowTemplate ? this._payload : undefined, + payload_template: this._allowTemplate ? this._payload : undefined, + qos: parseInt(this._qos), + retain: this._retain, }); } diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts index b953b7e4a5..ebc0e1a123 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts @@ -9,6 +9,8 @@ import { MQTTMessage, subscribeMQTTTopic } from "../../../../../data/mqtt"; import { HomeAssistant } from "../../../../../types"; import "@material/mwc-list/mwc-list-item"; import { LocalStorage } from "../../../../../common/decorators/local-storage"; +import "../../../../../components/ha-formfield"; +import "../../../../../components/ha-switch"; const qosLevel = ["0", "1", "2"]; @@ -22,6 +24,9 @@ class MqttSubscribeCard extends LitElement { @LocalStorage("panel-dev-mqtt-qos-subscribe", true, false) private _qos = "0"; + @LocalStorage("panel-dev-mqtt-json-format", true, false) + private _json_format = false; + @state() private _subscribed?: () => void; @state() private _messages: Array<{ @@ -47,6 +52,18 @@ class MqttSubscribeCard extends LitElement { header=${this.hass.localize("ui.panel.config.mqtt.description_listen")} >
+

+ + + +

{ if (this._subscribed) { this._subscribed(); @@ -132,9 +153,13 @@ class MqttSubscribeCard extends LitElement { const tail = this._messages.length > 30 ? this._messages.slice(0, 29) : this._messages; let payload: string; - try { - payload = JSON.stringify(JSON.parse(message.payload), null, 4); - } catch (err: any) { + if (this._json_format) { + try { + payload = JSON.stringify(JSON.parse(message.payload), null, 4); + } catch (err: any) { + payload = message.payload; + } + } else { payload = message.payload; } this._messages = [ diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts index f357a9bdf9..ab15c017d6 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts @@ -50,7 +50,8 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) { .sort((ent1, ent2) => stringCompare( ent1.stateName || `zzz${ent1.entity_id}`, - ent2.stateName || `zzz${ent2.entity_id}` + ent2.stateName || `zzz${ent2.entity_id}`, + this.hass.locale.language ) ) ); diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index afa5d71126..e055214469 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -230,7 +230,9 @@ export class HaConfigLovelaceDashboards extends LitElement { result.push( ...dashboards - .sort((a, b) => stringCompare(a.title, b.title)) + .sort((a, b) => + stringCompare(a.title, b.title, this.hass.locale.language) + ) .map((dashboard) => ({ filename: "", ...dashboard, @@ -342,7 +344,12 @@ export class HaConfigLovelaceDashboards extends LitElement { createDashboard: async (values: LovelaceDashboardCreateParams) => { const created = await createDashboard(this.hass!, values); this._dashboards = this._dashboards!.concat(created).sort( - (res1, res2) => stringCompare(res1.url_path, res2.url_path) + (res1, res2) => + stringCompare( + res1.url_path, + res2.url_path, + this.hass.locale.language + ) ); }, updateDashboard: async (values) => { diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts index b93afcc17d..411d288fbe 100644 --- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts +++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts @@ -143,7 +143,7 @@ export class HaConfigLovelaceRescources extends LitElement { createResource: async (values) => { const created = await createResource(this.hass!, values); this._resources = this._resources!.concat(created).sort((res1, res2) => - stringCompare(res1.url, res2.url) + stringCompare(res1.url, res2.url, this.hass!.locale.language) ); loadLovelaceResources([created], this.hass!.auth.data.hassUrl); }, diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts index 87d16caf94..ffd68efeec 100644 --- a/src/panels/config/person/ha-config-person.ts +++ b/src/panels/config/person/ha-config-person.ts @@ -156,10 +156,10 @@ class HaConfigPerson extends LitElement { const personData = await fetchPersons(this.hass!); this._storageItems = personData.storage.sort((ent1, ent2) => - stringCompare(ent1.name, ent2.name) + stringCompare(ent1.name, ent2.name, this.hass!.locale.language) ); this._configItems = personData.config.sort((ent1, ent2) => - stringCompare(ent1.name, ent2.name) + stringCompare(ent1.name, ent2.name, this.hass!.locale.language) ); this._openDialogIfPersonSpecifiedInRoute(); } @@ -221,7 +221,8 @@ class HaConfigPerson extends LitElement { createEntry: async (values) => { const created = await createPerson(this.hass!, values); this._storageItems = this._storageItems!.concat(created).sort( - (ent1, ent2) => stringCompare(ent1.name, ent2.name) + (ent1, ent2) => + stringCompare(ent1.name, ent2.name, this.hass!.locale.language) ); }, updateEntry: async (values) => { diff --git a/src/panels/config/repairs/ha-config-repairs.ts b/src/panels/config/repairs/ha-config-repairs.ts index 2cadd4a9b9..e24a5f1dfd 100644 --- a/src/panels/config/repairs/ha-config-repairs.ts +++ b/src/panels/config/repairs/ha-config-repairs.ts @@ -52,6 +52,7 @@ class HaConfigRepairs extends LitElement { @click=${this._openShowMoreDialog} > ${domainToName(this.hass.localize, `; + this._qrCode = html`${this.hass.localize(`; } static get styles(): CSSResultGroup { diff --git a/src/panels/config/zone/ha-config-zone.ts b/src/panels/config/zone/ha-config-zone.ts index 7089e7735a..d46e313ff4 100644 --- a/src/panels/config/zone/ha-config-zone.ts +++ b/src/panels/config/zone/ha-config-zone.ts @@ -296,7 +296,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { private async _fetchData() { this._storageItems = (await fetchZones(this.hass!)).sort((ent1, ent2) => - stringCompare(ent1.name, ent2.name) + stringCompare(ent1.name, ent2.name, this.hass!.locale.language) ); this._getStates(); } @@ -411,7 +411,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { private async _createEntry(values: ZoneMutableParams) { const created = await createZone(this.hass!, values); this._storageItems = this._storageItems!.concat(created).sort( - (ent1, ent2) => stringCompare(ent1.name, ent2.name) + (ent1, ent2) => + stringCompare(ent1.name, ent2.name, this.hass!.locale.language) ); if (this.narrow) { return; diff --git a/src/panels/developer-tools/event/events-list.js b/src/panels/developer-tools/event/events-list.js index 89b07d6933..a6893d269e 100644 --- a/src/panels/developer-tools/event/events-list.js +++ b/src/panels/developer-tools/event/events-list.js @@ -58,7 +58,9 @@ class EventsList extends EventsMixin(LocalizeMixin(PolymerElement)) { connectedCallback() { super.connectedCallback(); this.hass.callApi("GET", "events").then((events) => { - this.events = events.sort((e1, e2) => stringCompare(e1.event, e2.event)); + this.events = events.sort((e1, e2) => + stringCompare(e1.event, e2.event, this.hass.locale.language) + ); }); } diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index e5b7951160..412ccf8f13 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -79,7 +79,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { config.geo_location_sources && !Array.isArray(config.geo_location_sources) ) { - throw new Error("Geo_location_sources needs to be an array"); + throw new Error("Parameter geo_location_sources needs to be an array"); } this._config = config; @@ -102,6 +102,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { ratio && ratio.w > 0 && ratio.h > 0 ? `${((100 * ratio.h) / ratio.w).toFixed(2)}` : "100"; + return 1 + Math.floor(Number(ar) / 25) || 3; } @@ -185,10 +186,21 @@ class HuiMapCard extends LitElement implements LovelaceCard { return false; } - protected firstUpdated(changedProps: PropertyValues): void { - super.firstUpdated(changedProps); - const root = this.shadowRoot!.getElementById("root"); + protected updated(changedProps: PropertyValues): void { + if (this._config?.hours_to_show && this._configEntities?.length) { + if (changedProps.has("_config")) { + this._getHistory(); + } else if (Date.now() - this._date!.getTime() >= MINUTE) { + this._getHistory(); + } + } + if (changedProps.has("_config")) { + this._computePadding(); + } + } + private _computePadding(): void { + const root = this.shadowRoot!.getElementById("root"); if (!this._config || this.isPanel || !root) { return; } @@ -206,16 +218,6 @@ class HuiMapCard extends LitElement implements LovelaceCard { : (root.style.paddingBottom = "100%"); } - protected updated(changedProps: PropertyValues): void { - if (this._config?.hours_to_show && this._configEntities?.length) { - if (changedProps.has("_config")) { - this._getHistory(); - } else if (Date.now() - this._date!.getTime() >= MINUTE) { - this._getHistory(); - } - } - } - private _fitMap() { this._map?.fitMap(); } diff --git a/src/panels/lovelace/cards/hui-picture-card.ts b/src/panels/lovelace/cards/hui-picture-card.ts index 20a2a2f315..ffcf4f5009 100644 --- a/src/panels/lovelace/cards/hui-picture-card.ts +++ b/src/panels/lovelace/cards/hui-picture-card.ts @@ -101,7 +101,10 @@ export class HuiPictureCard extends LitElement implements LovelaceCard { ), })} > - + ${this._config.alt_text} `; } diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 5ab627b466..2ad69eeb94 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -332,6 +332,7 @@ export interface PictureCardConfig extends LovelaceCardConfig { hold_action?: ActionConfig; double_tap_action?: ActionConfig; theme?: string; + alt_text?: string; } export interface PictureElementsCardConfig extends LovelaceCardConfig { diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 07b99cb22d..661334392d 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -59,7 +59,7 @@ const splitByAreaDevice = ( for (const entity of Object.values(entityEntries)) { const areaId = entity.area_id || - (entity.device_id && deviceEntries[entity.device_id].area_id); + (entity.device_id && deviceEntries[entity.device_id]?.area_id); if (areaId && areaId in areaEntries && entity.entity_id in allEntities) { if (!(areaId in areasWithEntities)) { areasWithEntities[areaId] = []; diff --git a/src/panels/lovelace/common/handle-action.ts b/src/panels/lovelace/common/handle-action.ts index 6f8865f7d2..ddbbc838f3 100644 --- a/src/panels/lovelace/common/handle-action.ts +++ b/src/panels/lovelace/common/handle-action.ts @@ -46,7 +46,7 @@ export const handleAction = async ( actionConfig.confirmation && (!actionConfig.confirmation.exemptions || !actionConfig.confirmation.exemptions.some( - (e) => e.user === hass!.user!.id + (e) => e.user === hass!.user?.id )) ) { forwardHaptic("warning"); diff --git a/src/panels/lovelace/common/process-config-entities.ts b/src/panels/lovelace/common/process-config-entities.ts index 2dfa9a92a7..1d4519ea5f 100644 --- a/src/panels/lovelace/common/process-config-entities.ts +++ b/src/panels/lovelace/common/process-config-entities.ts @@ -27,13 +27,11 @@ export const processConfigEntities = < config = { entity: entityConf } as T; } else if (typeof entityConf === "object" && !Array.isArray(entityConf)) { if (!("entity" in entityConf)) { - throw new Error( - `Entity object at position ${index} is missing entity field.` - ); + throw new Error(`Object at position ${index} is missing entity field`); } config = entityConf as T; } else { - throw new Error(`Invalid entity specified at position ${index}.`); + throw new Error(`Invalid entity ID at position ${index}`); } if (checkEntityId && !isValidEntityId((config as EntityConfig).entity!)) { diff --git a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts index 97f5593057..f1c6168dbf 100644 --- a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts @@ -1,20 +1,21 @@ -import "../../../../components/ha-form/ha-form"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { array, assert, assign, object, optional, string } from "superstruct"; import memoizeOne from "memoize-one"; +import { array, assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { entityId } from "../../../../common/structs/is-entity-id"; +import type { LocalizeFunc } from "../../../../common/translations/localize"; +import "../../../../components/ha-form/ha-form"; +import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; import type { AlarmPanelCardConfig } from "../../cards/types"; import type { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; -import type { SchemaUnion } from "../../../../components/ha-form/types"; -import type { LocalizeFunc } from "../../../../common/translations/localize"; const cardConfigStruct = assign( baseLovelaceCardConfig, object({ - entity: optional(string()), + entity: optional(entityId()), name: optional(string()), states: optional(array()), theme: optional(string()), diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts index 7ac8bb39e9..da8436cf5b 100644 --- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts @@ -6,6 +6,7 @@ import { assert, assign, boolean, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeDomain } from "../../../../common/entity/compute_domain"; import { domainIcon } from "../../../../common/entity/domain_icon"; +import { entityId } from "../../../../common/structs/is-entity-id"; import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; @@ -18,7 +19,7 @@ import { configElementStyle } from "./config-elements-style"; const cardConfigStruct = assign( baseLovelaceCardConfig, object({ - entity: optional(string()), + entity: optional(entityId()), name: optional(string()), show_name: optional(boolean()), icon: optional(string()), diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts index 334e84bc06..37e3c34874 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts @@ -1,12 +1,13 @@ -import "../../../../components/ha-form/ha-form"; +import type { HassEntity } from "home-assistant-js-websocket/dist/types"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { assert, assign, boolean, object, optional, string } from "superstruct"; -import type { HassEntity } from "home-assistant-js-websocket/dist/types"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeDomain } from "../../../../common/entity/compute_domain"; import { domainIcon } from "../../../../common/entity/domain_icon"; +import { entityId } from "../../../../common/structs/is-entity-id"; +import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; import type { EntityCardConfig } from "../../cards/types"; @@ -17,7 +18,7 @@ import { baseLovelaceCardConfig } from "../structs/base-card-struct"; const cardConfigStruct = assign( baseLovelaceCardConfig, object({ - entity: optional(string()), + entity: optional(entityId()), name: optional(string()), icon: optional(string()), attribute: optional(string()), diff --git a/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts index 477b949a78..55592f9132 100644 --- a/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts @@ -1,6 +1,6 @@ -import "../../../../components/ha-form/ha-form"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { array, assert, @@ -11,8 +11,9 @@ import { optional, string, } from "superstruct"; -import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { entityId } from "../../../../common/structs/is-entity-id"; +import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; import type { GaugeCardConfig } from "../../cards/types"; @@ -29,7 +30,7 @@ const cardConfigStruct = assign( baseLovelaceCardConfig, object({ name: optional(string()), - entity: optional(string()), + entity: optional(entityId()), unit: optional(string()), min: optional(number()), max: optional(number()), diff --git a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts index 055cdf68c9..7516347005 100644 --- a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts @@ -1,8 +1,9 @@ -import "../../../../components/ha-form/ha-form"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { entityId } from "../../../../common/structs/is-entity-id"; +import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; import type { HumidifierCardConfig } from "../../cards/types"; @@ -12,7 +13,7 @@ import { baseLovelaceCardConfig } from "../structs/base-card-struct"; const cardConfigStruct = assign( baseLovelaceCardConfig, object({ - entity: optional(string()), + entity: optional(entityId()), name: optional(string()), theme: optional(string()), }) diff --git a/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts index e0e42f6fe7..e8247b9eb7 100644 --- a/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts @@ -6,6 +6,7 @@ import { assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeDomain } from "../../../../common/entity/compute_domain"; import { domainIcon } from "../../../../common/entity/domain_icon"; +import { entityId } from "../../../../common/structs/is-entity-id"; import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; @@ -19,7 +20,7 @@ const cardConfigStruct = assign( baseLovelaceCardConfig, object({ name: optional(string()), - entity: optional(string()), + entity: optional(entityId()), theme: optional(string()), icon: optional(string()), hold_action: optional(actionConfigStruct), diff --git a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts index 4c23122554..13161eac14 100644 --- a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts @@ -1,17 +1,19 @@ -import "../../../../components/ha-form/ha-form"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { array, assert, + assign, boolean, number, object, optional, string, - assign, } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { entityId } from "../../../../common/structs/is-entity-id"; +import "../../../../components/ha-form/ha-form"; +import { SchemaUnion } from "../../../../components/ha-form/types"; import "../../../../components/ha-formfield"; import "../../../../components/ha-switch"; import { PolymerChangedEvent } from "../../../../polymer-types"; @@ -22,11 +24,9 @@ import "../../components/hui-input-list-editor"; import { EntityConfig } from "../../entity-rows/types"; import { LovelaceCardEditor } from "../../types"; import { processEditorEntities } from "../process-editor-entities"; -import { entitiesConfigStruct } from "../structs/entities-struct"; +import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; -import { baseLovelaceCardConfig } from "../structs/base-card-struct"; -import { SchemaUnion } from "../../../../components/ha-form/types"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -35,9 +35,10 @@ const cardConfigStruct = assign( aspect_ratio: optional(string()), default_zoom: optional(number()), dark_mode: optional(boolean()), - entities: array(entitiesConfigStruct), + entities: array(entityId()), hours_to_show: optional(number()), geo_location_sources: optional(array(string())), + auto_fit: optional(boolean()), }) ); diff --git a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts index 9470be0a78..f7bd27249c 100644 --- a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts @@ -2,6 +2,7 @@ import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { assert, assign, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { entityId } from "../../../../common/structs/is-entity-id"; import "../../../../components/entity/ha-entity-picker"; import "../../../../components/ha-theme-picker"; import { HomeAssistant } from "../../../../types"; @@ -13,7 +14,7 @@ import { EditorTarget, EntitiesEditorEvent } from "../types"; const cardConfigStruct = assign( baseLovelaceCardConfig, object({ - entity: optional(string()), + entity: optional(entityId()), theme: optional(string()), }) ); diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts index 8174a049e2..ee75869fc6 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts @@ -20,6 +20,7 @@ const cardConfigStruct = assign( tap_action: optional(actionConfigStruct), hold_action: optional(actionConfigStruct), theme: optional(string()), + alt_text: optional(string()), }) ); @@ -53,6 +54,10 @@ export class HuiPictureCardEditor return this._config!.theme || ""; } + get _alt_text(): string { + return this._config!.alt_text || ""; + } + protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; @@ -72,6 +77,16 @@ export class HuiPictureCardEditor .configValue=${"image"} @input=${this._valueChanged} > + - users.sort((a, b) => stringCompare(a.name, b.name)) + users.sort((a, b) => + stringCompare(a.name, b.name, this.hass.locale.language) + ) ); protected firstUpdated(changedProps: PropertyValues) { diff --git a/src/panels/lovelace/header-footer/hui-picture-header-footer.ts b/src/panels/lovelace/header-footer/hui-picture-header-footer.ts index 200744f85c..169f55d576 100644 --- a/src/panels/lovelace/header-footer/hui-picture-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-picture-header-footer.ts @@ -68,6 +68,7 @@ export class HuiPictureHeaderFooter return html` ${this._config!.alt_text} - e.user === this.hass!.user!.id + e.user === this.hass!.user?.id )) || view.visible === false)) ), @@ -470,7 +470,7 @@ class HUIRoot extends LitElement { view.visible !== undefined && ((Array.isArray(view.visible) && !view.visible.some( - (e) => e.user === this.hass!.user!.id + (e) => e.user === this.hass!.user?.id )) || view.visible === false) ), diff --git a/src/panels/profile/ha-long-lived-access-token-dialog.ts b/src/panels/profile/ha-long-lived-access-token-dialog.ts index 5786a783a4..e736509843 100644 --- a/src/panels/profile/ha-long-lived-access-token-dialog.ts +++ b/src/panels/profile/ha-long-lived-access-token-dialog.ts @@ -85,7 +85,14 @@ export class HaLongLivedAccessTokenDialog extends LitElement { canvas.height / 3 ); - this._qrCode = html``; + this._qrCode = html`${this.hass.localize(`; } static get styles(): CSSResultGroup { diff --git a/src/resources/codemirror.ts b/src/resources/codemirror.ts index 3b69e471b5..f16e1f61f0 100644 --- a/src/resources/codemirror.ts +++ b/src/resources/codemirror.ts @@ -1,25 +1,29 @@ import { indentLess, indentMore } from "@codemirror/commands"; -import { HighlightStyle, tags } from "@codemirror/highlight"; +import { + HighlightStyle, + StreamLanguage, + syntaxHighlighting, +} from "@codemirror/language"; import { jinja2 } from "@codemirror/legacy-modes/mode/jinja2"; import { yaml } from "@codemirror/legacy-modes/mode/yaml"; import { Compartment } from "@codemirror/state"; -import { StreamLanguage } from "@codemirror/stream-parser"; import { EditorView, KeyBinding } from "@codemirror/view"; +import { tags } from "@lezer/highlight"; -export { defaultKeymap } from "@codemirror/commands"; -export { lineNumbers } from "@codemirror/gutter"; -export { HighlightStyle, tags } from "@codemirror/highlight"; -export { history, historyKeymap } from "@codemirror/history"; -export { rectangularSelection } from "@codemirror/rectangular-selection"; -export { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; -export { EditorState, Prec } from "@codemirror/state"; export { autocompletion } from "@codemirror/autocomplete"; +export { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; +export { highlightingFor } from "@codemirror/language"; +export { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; +export { EditorState } from "@codemirror/state"; export { drawSelection, EditorView, highlightActiveLine, keymap, + lineNumbers, + rectangularSelection, } from "@codemirror/view"; +export { tags } from "@lezer/highlight"; export const langs = { jinja2: StreamLanguage.define(jinja2), @@ -37,7 +41,7 @@ export const tabKeyBindings: KeyBinding[] = [ }, ]; -export const theme = EditorView.theme({ +export const haTheme = EditorView.theme({ "&": { color: "var(--primary-text-color)", backgroundColor: @@ -186,7 +190,7 @@ export const theme = EditorView.theme({ ".cm-gutterElement.lineNumber": { color: "inherit" }, }); -export const highlightStyle = HighlightStyle.define([ +const haHighlightStyle = HighlightStyle.define([ { tag: tags.keyword, color: "var(--codemirror-keyword, #6262FF)" }, { tag: [ @@ -259,3 +263,5 @@ export const highlightStyle = HighlightStyle.define([ { tag: tags.inserted, color: "var(--codemirror-string2, #07a)" }, { tag: tags.invalid, color: "var(--error-color)" }, ]); + +export const haSyntaxHighlighting = syntaxHighlighting(haHighlightStyle); diff --git a/src/state-summary/state-card-input_number.js b/src/state-summary/state-card-input_number.js deleted file mode 100644 index ced62e2984..0000000000 --- a/src/state-summary/state-card-input_number.js +++ /dev/null @@ -1,200 +0,0 @@ -import "@polymer/iron-flex-layout/iron-flex-layout-classes"; -import { IronResizableBehavior } from "@polymer/iron-resizable-behavior/iron-resizable-behavior"; -import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { computeStateDisplay } from "../common/entity/compute_state_display"; -import "../components/entity/state-info"; -import "../components/ha-slider"; -import "../components/ha-textfield"; - -class StateCardInputNumber extends mixinBehaviors( - [IronResizableBehavior], - PolymerElement -) { - static get template() { - return html` - - - -
- ${this.stateInfoTemplate} - - - -
- `; - } - - static get stateInfoTemplate() { - return html` - - `; - } - - ready() { - super.ready(); - if (typeof ResizeObserver === "function") { - const ro = new ResizeObserver((entries) => { - entries.forEach(() => { - this.hiddenState(); - }); - }); - ro.observe(this.$.input_number_card); - } else { - this.addEventListener("iron-resize", this.hiddenState); - } - } - - static get properties() { - return { - hass: Object, - hiddenbox: { - type: Boolean, - value: true, - }, - hiddenslider: { - type: Boolean, - value: true, - }, - inDialog: { - type: Boolean, - value: false, - }, - stateObj: { - type: Object, - observer: "stateObjectChanged", - }, - min: { - type: Number, - value: 0, - }, - max: { - type: Number, - value: 100, - }, - maxlength: { - type: Number, - value: 3, - }, - step: Number, - value: Number, - formattedState: String, - mode: String, - }; - } - - hiddenState() { - if (this.mode !== "slider") return; - const sliderwidth = this.$.slider.offsetWidth; - if (sliderwidth < 100) { - this.$.sliderstate.hidden = true; - } else if (sliderwidth >= 145) { - this.$.sliderstate.hidden = false; - } - } - - stateObjectChanged(newVal) { - const prevMode = this.mode; - this.setProperties({ - min: Number(newVal.attributes.min), - max: Number(newVal.attributes.max), - step: Number(newVal.attributes.step), - value: Number(newVal.state), - formattedState: computeStateDisplay( - this.hass.localize, - newVal, - this.hass.locale, - this.hass.entities, - newVal.state - ), - mode: String(newVal.attributes.mode), - maxlength: String(newVal.attributes.max).length, - hiddenbox: newVal.attributes.mode !== "box", - hiddenslider: newVal.attributes.mode !== "slider", - }); - if (this.mode === "slider" && prevMode !== "slider") { - this.hiddenState(); - } - } - - onInput(ev) { - this.value = ev.target.value; - } - - selectedValueChanged() { - if (this.value === Number(this.stateObj.state)) { - return; - } - this.hass.callService("input_number", "set_value", { - value: this.value, - entity_id: this.stateObj.entity_id, - }); - } - - stopPropagation(ev) { - ev.stopPropagation(); - } -} - -customElements.define("state-card-input_number", StateCardInputNumber); diff --git a/src/state-summary/state-card-input_number.ts b/src/state-summary/state-card-input_number.ts new file mode 100644 index 0000000000..02829dd96a --- /dev/null +++ b/src/state-summary/state-card-input_number.ts @@ -0,0 +1,169 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { HassEntity } from "home-assistant-js-websocket"; +import { customElement, property } from "lit/decorators"; +import { computeStateDisplay } from "../common/entity/compute_state_display"; +import { computeRTLDirection } from "../common/util/compute_rtl"; +import { debounce } from "../common/util/debounce"; +import "../components/ha-slider"; +import "../components/ha-textfield"; +import "../components/entity/state-info"; +import { isUnavailableState } from "../data/entity"; +import { setValue } from "../data/input_text"; +import { HomeAssistant } from "../types"; +import { installResizeObserver } from "../panels/lovelace/common/install-resize-observer"; + +@customElement("state-card-input_number") +class StateCardInputNumber extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public stateObj!: HassEntity; + + @property({ type: Boolean }) public inDialog = false; + + private _loaded?: boolean; + + private _updated?: boolean; + + private _resizeObserver?: ResizeObserver; + + public connectedCallback(): void { + super.connectedCallback(); + if (this._updated && !this._loaded) { + this._initialLoad(); + } + this._attachObserver(); + } + + public disconnectedCallback(): void { + this._resizeObserver?.disconnect(); + } + + protected firstUpdated(): void { + this._updated = true; + if (this.isConnected && !this._loaded) { + this._initialLoad(); + } + this._attachObserver(); + } + + protected render(): TemplateResult { + return html` + + ${this.stateObj.attributes.mode === "slider" + ? html` +
+ + + ${computeStateDisplay( + this.hass.localize, + this.stateObj, + this.hass.locale, + this.hass.entities, + this.stateObj.state + )} + +
+ ` + : html` +
+ + +
+ `} + `; + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: flex; + } + .flex { + display: flex; + align-items: center; + justify-content: flex-end; + flex-grow: 2; + } + .state { + min-width: 45px; + text-align: end; + } + ha-textfield { + text-align: end; + } + ha-slider { + width: 100%; + max-width: 200px; + } + `; + } + + private async _initialLoad(): Promise { + this._loaded = true; + await this.updateComplete; + this._measureCard(); + } + + private _measureCard() { + if (!this.isConnected) { + return; + } + const element = this.shadowRoot!.querySelector(".state") as HTMLElement; + if (!element) { + return; + } + element.hidden = this.clientWidth <= 300; + } + + private async _attachObserver(): Promise { + if (!this._resizeObserver) { + await installResizeObserver(); + this._resizeObserver = new ResizeObserver( + debounce(() => this._measureCard(), 250, false) + ); + } + if (this.isConnected) { + this._resizeObserver.observe(this); + } + } + + private _selectedValueChanged(ev: Event): void { + if ((ev.target as HTMLInputElement).value !== this.stateObj.state) { + setValue( + this.hass!, + this.stateObj.entity_id, + (ev.target as HTMLInputElement).value + ); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "state-card-input_number": StateCardInputNumber; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index f8415ad180..b9e0779322 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -623,6 +623,12 @@ "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?" }, + "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?" + }, "repeat": { "label": "Repeat", "freq": { @@ -638,6 +644,33 @@ "monthly": "months", "weekly": "weeks", "daily": "days" + }, + "monthly": { + "label": "Repeat Monthly" + }, + "weekly": { + "weekday": { + "su": "Sun", + "mo": "Mon", + "tu": "Tue", + "we": "Wed", + "th": "Thu", + "fr": "Fri", + "sa": "Sat" + } + }, + "end": { + "label": "End", + "never": "Never", + "after": "After", + "on": "On" + }, + "end_on": { + "label": "End On" + }, + "end_after": { + "label": "End After", + "ocurrences": "ocurrences" } }, "rrule": { @@ -771,6 +804,7 @@ }, "voice_command": { "did_not_hear": "Home Assistant did not hear anything", + "did_not_understand": "Didn't quite get that", "found": "I found the following for you:", "error": "Oops, an error has occurred", "how_can_i_help": "How can I help?", @@ -784,7 +818,8 @@ "close": "Close" }, "image_cropper": { - "crop": "Crop" + "crop": "Crop", + "crop_image": "Picture to crop" }, "date-picker": { "today": "Today" @@ -961,6 +996,20 @@ "stream_orientation_6": "Rotate left", "stream_orientation_7": "Rotate right and flip", "stream_orientation_8": "Rotate right" + }, + "aliases_section": "Aliases", + "no_aliases": "No configured aliases", + "configured_aliases": "{count} configured {count, plural,\n one {alias}\n other {aliases}\n}", + "aliases": { + "heading": "{name} aliases", + "description": "Aliases are alternative names used in voice assistants to refer to this entity.", + "remove_alias": "Remove alias {number}", + "input_label": "Alias {number}", + "save": "Save", + "add_alias": "Add alias", + "no_aliases": "No aliases have been added yet", + "update": "Update", + "unknown_error": "Unknown error" } } }, @@ -1415,6 +1464,7 @@ "confirm_remove_title": "Remove tag?", "confirm_remove": "Are you sure you want to remove tag {tag}?", "automation_title": "Tag {name} is scanned", + "qr_code_image": "QR code for tag {name}", "headers": { "icon": "Icon", "name": "Name", @@ -2612,7 +2662,7 @@ "enable_state_reporting": "Enable State Reporting", "info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Amazon. This allows you to always see the latest states in the Alexa app and use the state changes to create routines.", "state_reporting_error": "Unable to {enable_disable} report state.", - "manage_entities": "Manage Entities", + "manage_entities": "[%key:ui::panel::config::cloud::account::google::manage_entities%]", "enable": "enable", "disable": "disable", "not_configured_title": "Alexa is not activated", @@ -2663,6 +2713,7 @@ "follow_domain": "[%key:ui::panel::config::cloud::google::follow_domain%]", "exposed": "[%key:ui::panel::config::cloud::google::exposed%]", "not_exposed": "[%key:ui::panel::config::cloud::google::not_exposed%]", + "manage_aliases": "[%key:ui::panel::config::cloud::google::manage_aliases%]", "expose": "Expose to Alexa", "sync_entities": "Synchronize entities", "sync_entities_error": "Failed to sync entities:" @@ -2688,6 +2739,11 @@ "follow_domain": "Follow domain", "exposed": "{selected} exposed", "not_exposed": "{selected} not exposed", + "manage_aliases": "Manage aliases", + "add_aliases": "Add aliases", + "no_aliases": "No aliases", + "aliases_not_available": "Aliases not available", + "aliases_not_available_learn_more": "Learn more", "sync_to_google": "Synchronizing changes to Google.", "sync_entities": "Synchronize entities", "sync_entities_error": "Failed to sync entities:", @@ -3193,11 +3249,16 @@ }, "mqtt": { "title": "MQTT", + "settings_title": "MQTT settings", + "reconfigure": "Re-configure MQTT", "description_publish": "Publish a packet", "topic": "Topic", "payload": "Payload (template allowed)", + "payload_no_template": "Payload", + "allow_template": "Allow template", "publish": "Publish", "description_listen": "Listen to a topic", + "json_formatting": "Format JSON content", "listening_to": "Listening to", "subscribe_to": "Topic to subscribe to", "start_listening": "Start listening", @@ -4172,6 +4233,7 @@ "description": "The Light card allows you to change the brightness of the light." }, "generic": { + "alt_text": "Alternative Text", "aspect_ratio": "Aspect Ratio", "attribute": "Attribute", "camera_image": "Camera Entity", @@ -4575,7 +4637,8 @@ "name": "Name", "prompt_name": "Give the token a name", "prompt_copy_token": "Copy your access token. It will not be shown again.", - "empty_state": "You have no long-lived access tokens yet." + "empty_state": "You have no long-lived access tokens yet.", + "qr_code_image": "QR code for token {name}" } }, "shopping_list": { diff --git a/yarn.lock b/yarn.lock index c81d28f903..ebb08e619f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1365,197 +1365,84 @@ __metadata: languageName: node linkType: hard -"@codemirror/autocomplete@npm:^0.19.12": - version: 0.19.12 - resolution: "@codemirror/autocomplete@npm:0.19.12" +"@codemirror/autocomplete@npm:^6.4.0": + version: 6.4.0 + resolution: "@codemirror/autocomplete@npm:6.4.0" dependencies: - "@codemirror/language": ^0.19.0 - "@codemirror/state": ^0.19.4 - "@codemirror/text": ^0.19.2 - "@codemirror/tooltip": ^0.19.12 - "@codemirror/view": ^0.19.0 - "@lezer/common": ^0.15.0 - checksum: f57dfe7b911e9dd928a589d72d487c84f74281e3a899120f14e857a48f4c9af109ae1df9f7e0e5959c77aedcfeafa74a428c832d1cd8cb0adde2d3e2daf6fec8 + "@codemirror/language": ^6.0.0 + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.6.0 + "@lezer/common": ^1.0.0 + peerDependencies: + "@codemirror/language": ^6.0.0 + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.0.0 + "@lezer/common": ^1.0.0 + checksum: 3470fee01da60d3d71b8b4f8728629c0f0441e704b8b828592f98c000d75fdb2c9077727e82685626cf45b95cadbc0c1a03968261df2f0cfb4162418b5f4dd1f languageName: node linkType: hard -"@codemirror/commands@npm:^0.19.8": - version: 0.19.8 - resolution: "@codemirror/commands@npm:0.19.8" +"@codemirror/commands@npm:^6.1.3": + version: 6.1.3 + resolution: "@codemirror/commands@npm:6.1.3" dependencies: - "@codemirror/language": ^0.19.0 - "@codemirror/matchbrackets": ^0.19.0 - "@codemirror/state": ^0.19.2 - "@codemirror/text": ^0.19.6 - "@codemirror/view": ^0.19.22 - "@lezer/common": ^0.15.0 - checksum: 296f7564e71c07680da0ade9b73db67d68dd845ede22d2ba9a2a83f8b3a5429953ccd22a11d96cf2543425d416a15d1f026fb5c74a2e80522cf1779cc7cd13ff + "@codemirror/language": ^6.0.0 + "@codemirror/state": ^6.2.0 + "@codemirror/view": ^6.0.0 + "@lezer/common": ^1.0.0 + checksum: beca0248fa2528005e4088a46840bc2a057d34e5b51c60cb20703ca734761dc55f5e205f5dcf94280ffbb5c3366753f46f2d03a5abfd69bd95eac8a67c2e96bb languageName: node linkType: hard -"@codemirror/gutter@npm:^0.19.9": - version: 0.19.9 - resolution: "@codemirror/gutter@npm:0.19.9" +"@codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.3.2": + version: 6.3.2 + resolution: "@codemirror/language@npm:6.3.2" dependencies: - "@codemirror/rangeset": ^0.19.0 - "@codemirror/state": ^0.19.0 - "@codemirror/view": ^0.19.23 - checksum: 948e4bdeddfdd2f824412aa8a2cc43915444e948c310ee113faca4a988e98b6b02bea72f8849481adf82a5021b00d6a8ee2bdf0b105864de0e8aa417b41a9ed1 - languageName: node - linkType: hard - -"@codemirror/highlight@npm:^0.19.0, @codemirror/highlight@npm:^0.19.7": - version: 0.19.7 - resolution: "@codemirror/highlight@npm:0.19.7" - dependencies: - "@codemirror/language": ^0.19.0 - "@codemirror/rangeset": ^0.19.0 - "@codemirror/state": ^0.19.3 - "@codemirror/view": ^0.19.0 - "@lezer/common": ^0.15.0 + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.0.0 + "@lezer/common": ^1.0.0 + "@lezer/highlight": ^1.0.0 + "@lezer/lr": ^1.0.0 style-mod: ^4.0.0 - checksum: 8be9d2d900501b483aa108fbd58e4cc628d01b6b5150e4f0242c1e779fd20b930f69c2da8d2eb5468712e01135808f900e44500c76fb0a838538c69c9aa31a96 + checksum: b70ed9b85d0bea79181c86e88a1f5c0bada30680ee1fe6a68efc01bc037c3d14f94a83602fc46cc4b4393589605ef7e986ed5174443502f3365dd61f883894fa languageName: node linkType: hard -"@codemirror/history@npm:^0.19.2": - version: 0.19.2 - resolution: "@codemirror/history@npm:0.19.2" +"@codemirror/legacy-modes@npm:^6.3.1": + version: 6.3.1 + resolution: "@codemirror/legacy-modes@npm:6.3.1" dependencies: - "@codemirror/state": ^0.19.2 - "@codemirror/view": ^0.19.0 - checksum: c9d794289ea0b493b11a24df487a8de14afb7f8aef502bfaa9a8dda48e01c172c769ae76209743e4cb2d5937df0e64bea1295f07722b571a858d7417b21cc4f8 + "@codemirror/language": ^6.0.0 + checksum: 9065e521bf14e33856e9d3ea114d7b352adf341a8b8d4fb94b4c866189336a32b5ed42ffc20f5d2fa3c839f1bdf29a868bbf9b74c105ed83fa9fd6080e0429e9 languageName: node linkType: hard -"@codemirror/language@npm:^0.19.0": - version: 0.19.2 - resolution: "@codemirror/language@npm:0.19.2" +"@codemirror/search@npm:^6.2.3": + version: 6.2.3 + resolution: "@codemirror/search@npm:6.2.3" dependencies: - "@codemirror/state": ^0.19.0 - "@codemirror/text": ^0.19.0 - "@codemirror/view": ^0.19.0 - "@lezer/common": ^0.15.0 - "@lezer/lr": ^0.15.0 - checksum: f0b0e555869b17b08017a128fe07b0c3280391310b8e9bc443cecfbc054f670cf58fef8e33ed4c6b9d421acdfdc541a8149404cdbdbc66e1f5d688f90153ebc3 - languageName: node - linkType: hard - -"@codemirror/legacy-modes@npm:^0.19.0": - version: 0.19.0 - resolution: "@codemirror/legacy-modes@npm:0.19.0" - dependencies: - "@codemirror/stream-parser": ^0.19.0 - checksum: 8ad6235f443ef7218651ab21b7b407712ddceaa158c74f7698f5fe507fb1edcc60382318fe2413294716e8b395f568bbdd985436f2d3b3699abbb9e17456614a - languageName: node - linkType: hard - -"@codemirror/matchbrackets@npm:^0.19.0": - version: 0.19.1 - resolution: "@codemirror/matchbrackets@npm:0.19.1" - dependencies: - "@codemirror/language": ^0.19.0 - "@codemirror/state": ^0.19.0 - "@codemirror/view": ^0.19.0 - "@lezer/common": ^0.15.0 - checksum: 6a5a6a4fc166b3032ea0757f959263623105d1014202911016a5899b0b956493c6726f18b5689cb056f5e85108d59f93fdc7862d28d77822b1d944ac4b93efa5 - languageName: node - linkType: hard - -"@codemirror/panel@npm:^0.19.0": - version: 0.19.0 - resolution: "@codemirror/panel@npm:0.19.0" - dependencies: - "@codemirror/state": ^0.19.0 - "@codemirror/view": ^0.19.0 - checksum: 6ebc6f02fdc248812ac0ae8e510bb5ed93d636d2a60acbb78813ce4e65cde68336239973fd5456584376fb0c27f3603b6ebc37c46c28002e61d10caf6814c8bb - languageName: node - linkType: hard - -"@codemirror/rangeset@npm:^0.19.0, @codemirror/rangeset@npm:^0.19.5": - version: 0.19.6 - resolution: "@codemirror/rangeset@npm:0.19.6" - dependencies: - "@codemirror/state": ^0.19.0 - checksum: f7b9ff54ac514a5c67dea1689c7f227906b46643007da76e93045ea163bd863c823a35ded4d33ba8ab1d085cb562c67134b2bf9165ffc14a9f44fbf3d85afa43 - languageName: node - linkType: hard - -"@codemirror/rectangular-selection@npm:^0.19.1": - version: 0.19.1 - resolution: "@codemirror/rectangular-selection@npm:0.19.1" - dependencies: - "@codemirror/state": ^0.19.0 - "@codemirror/text": ^0.19.4 - "@codemirror/view": ^0.19.0 - checksum: 63b7d8d1efaa551bbb12bf8baf3218d6e00fcd62a1b9648b0c02420c1c008fc7451a3e68e881ab9ce3e1d7e76126a224a47fe67cf8461e2da6b165a93a1d7213 - languageName: node - linkType: hard - -"@codemirror/search@npm:^0.19.6": - version: 0.19.6 - resolution: "@codemirror/search@npm:0.19.6" - dependencies: - "@codemirror/panel": ^0.19.0 - "@codemirror/rangeset": ^0.19.0 - "@codemirror/state": ^0.19.3 - "@codemirror/text": ^0.19.0 - "@codemirror/view": ^0.19.34 + "@codemirror/state": ^6.0.0 + "@codemirror/view": ^6.0.0 crelt: ^1.0.5 - checksum: 1313b389b1f7b0282ab988d338fcadbd9025765d2e85d7de90dec43477241b1f31b4ab118506c2ff1821086256f3c50a570baa4a1abdfd1909c79d0f34f3776b + checksum: 7ab0ffab7992f5c6260313e06ec8935f55807b95ca86f0327154ea1ae0ab984cd22c2fc1a812bd6cace1db131785353689fbfd080d2e12c660e3db0295dec355 languageName: node linkType: hard -"@codemirror/state@npm:^0.19.0, @codemirror/state@npm:^0.19.2, @codemirror/state@npm:^0.19.3, @codemirror/state@npm:^0.19.4, @codemirror/state@npm:^0.19.6": - version: 0.19.6 - resolution: "@codemirror/state@npm:0.19.6" +"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.1.4, @codemirror/state@npm:^6.2.0": + version: 6.2.0 + resolution: "@codemirror/state@npm:6.2.0" + checksum: fdc99c773dc09c700dd02bf918f06132aa8d3069c262cc4eb6ca5c810ce24ae2d7e90719ae7630a8158fd263018de6d40bd78f312e6bfba754e737b64e6c6b3d + languageName: node + linkType: hard + +"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.6.0, @codemirror/view@npm:^6.7.1": + version: 6.7.1 + resolution: "@codemirror/view@npm:6.7.1" dependencies: - "@codemirror/text": ^0.19.0 - checksum: 65bee46d76c0b55b10ed4818cbb77267a6c75dff3c8cc04e83056a79a1d36e79d7b8bf750d4695238ac28fe792d6329939fd725839f8314eee34146941cae344 - languageName: node - linkType: hard - -"@codemirror/stream-parser@npm:^0.19.0, @codemirror/stream-parser@npm:^0.19.5": - version: 0.19.5 - resolution: "@codemirror/stream-parser@npm:0.19.5" - dependencies: - "@codemirror/highlight": ^0.19.0 - "@codemirror/language": ^0.19.0 - "@codemirror/state": ^0.19.0 - "@codemirror/text": ^0.19.0 - "@lezer/common": ^0.15.0 - "@lezer/lr": ^0.15.0 - checksum: 3a1edef98def985e31f9d1be3669bebc7bb14c41d0e69bd23e57868b67c1f473b7713cba1c45638e4453faf99e84888df2d4a3ebb183ea1db9795a367fde93bc - languageName: node - linkType: hard - -"@codemirror/text@npm:^0.19.0, @codemirror/text@npm:^0.19.2, @codemirror/text@npm:^0.19.4, @codemirror/text@npm:^0.19.6": - version: 0.19.6 - resolution: "@codemirror/text@npm:0.19.6" - checksum: 685e46c1f0114a216081b7a070460e1b0db9c51b0a2b361e9ed90e5ea2ed89d86a7a834b76f7c63b27fd192809d9414e7a15e0d186bd15cdb5d4f85639d434f0 - languageName: node - linkType: hard - -"@codemirror/tooltip@npm:^0.19.12": - version: 0.19.13 - resolution: "@codemirror/tooltip@npm:0.19.13" - dependencies: - "@codemirror/state": ^0.19.0 - "@codemirror/view": ^0.19.0 - checksum: 00e0554510aa6545efb201ce9a7925d13122c78455429ec26c220ff6c9de480728e16ad3bb7e451ceee1c1e1968e2c7168c38f7ffb64b5e096b4a504fe135494 - languageName: node - linkType: hard - -"@codemirror/view@npm:^0.19.0, @codemirror/view@npm:^0.19.22, @codemirror/view@npm:^0.19.23, @codemirror/view@npm:^0.19.34, @codemirror/view@npm:^0.19.40": - version: 0.19.40 - resolution: "@codemirror/view@npm:0.19.40" - dependencies: - "@codemirror/rangeset": ^0.19.5 - "@codemirror/state": ^0.19.3 - "@codemirror/text": ^0.19.0 + "@codemirror/state": ^6.1.4 style-mod: ^4.0.0 w3c-keyname: ^2.2.4 - checksum: bf3356a15a2bd24bdea7097483f055b5bef9ae20508639bb37d0bc33439824f093d348736d0e0da5b3e076f2fc6662437d9b5795e0668325bd6329f3f0bbd50f + checksum: 75a5846d61e63027e9bf1dfd0b507932934cb7650b7959c1191e68b161eb1756e9773f964c4331970b51864aef8f7954bc5cc8fdb51b0f6533de6c20568833ed languageName: node linkType: hard @@ -1998,19 +1885,28 @@ __metadata: languageName: node linkType: hard -"@lezer/common@npm:^0.15.0": - version: 0.15.4 - resolution: "@lezer/common@npm:0.15.4" - checksum: 567a8f947848f224231f2123f3800529396ab78336d6eba5933eeaa9d4a9ca70fae48ffee9f54c5c39485f3b1115b953ed8f7afb1f8328d2be4f0af75bc3d4a3 +"@lezer/common@npm:^1.0.0": + version: 1.0.2 + resolution: "@lezer/common@npm:1.0.2" + checksum: bbcc58e07be02652bf0700d2856042ec089d5be0b95893d628b3e18192ade864fac83b61b19653e10b9f1472261a178b12318d934e9004edd5483a577c0db56b languageName: node linkType: hard -"@lezer/lr@npm:^0.15.0": - version: 0.15.2 - resolution: "@lezer/lr@npm:0.15.2" +"@lezer/highlight@npm:^1.0.0, @lezer/highlight@npm:^1.1.3": + version: 1.1.3 + resolution: "@lezer/highlight@npm:1.1.3" dependencies: - "@lezer/common": ^0.15.0 - checksum: 62009e7587c2eea3992438076f1dd025e9ff498165b41ce3d305b872dd6410ab3f020635469a372c82bcc2304398b827fcbc6bad83d78acc81cdc7c11b698c61 + "@lezer/common": ^1.0.0 + checksum: 90ec143ce46b32f6779c3b245f1b5a540d66686939816d3daed8318821acc4bc719466dc222336cfd483bf04a8de4fdc6f279e904cf114d4d9f786f9feccbbd8 + languageName: node + linkType: hard + +"@lezer/lr@npm:^1.0.0": + version: 1.2.5 + resolution: "@lezer/lr@npm:1.2.5" + dependencies: + "@lezer/common": ^1.0.0 + checksum: 9a2fb2663dba5608c0f8a7d51b4c1beeb37d391da972fb3569fe51b637167ac4889b055ceb0c5267b8612a0aa5dfd517cbbd1349975cd662d1ca7fea374916b1 languageName: node linkType: hard @@ -3066,17 +2962,17 @@ __metadata: languageName: node linkType: hard -"@mdi/js@npm:7.0.96": - version: 7.0.96 - resolution: "@mdi/js@npm:7.0.96" - checksum: 4bcc4542a2d8ba9e706a754333e90ce2677616709d3776016b83be59c7219e7514308dbca6e62c7dd38436b3c5d251b8769679c90e354f971b090fa2ebcbc4fe +"@mdi/js@npm:7.1.96": + version: 7.1.96 + resolution: "@mdi/js@npm:7.1.96" + checksum: cd3102a0120c06b4af7a153e30ee964807746ce55f84a35479d382841d0ab1a10f986059e0c6af0c240444393679b7330d306d0688e1193a23fc03bb8cee0b82 languageName: node linkType: hard -"@mdi/svg@npm:7.0.96": - version: 7.0.96 - resolution: "@mdi/svg@npm:7.0.96" - checksum: 434994401283e4f790dd9d7061d5781fc8eec24f85b7be33d07a32a9fecb9a268437b35fa3c30ee4fe433d49f7f69674571494510aaa9efade05fb4e8e71755f +"@mdi/svg@npm:7.1.96": + version: 7.1.96 + resolution: "@mdi/svg@npm:7.1.96" + checksum: afbb0947cbf342ffb08d9b0069095db5e4d9a5dab741457eac2a0a8433217834bb96589be26b8abbf4ac315473870e07a14b0b759b5148efeedad193067ab67c languageName: node linkType: hard @@ -4797,10 +4693,10 @@ __metadata: languageName: node linkType: hard -"@vue/web-component-wrapper@npm:^1.2.0": - version: 1.2.0 - resolution: "@vue/web-component-wrapper@npm:1.2.0" - checksum: 342d56d3fcebcbd94e0be984adc1163587f2d1326dcda7727d3624800be221cf0329c1c6c3f949fdc5a00c9f487946750781cf6441d1585a4fa3800d11ef939e +"@vue/web-component-wrapper@npm:^1.3.0": + version: 1.3.0 + resolution: "@vue/web-component-wrapper@npm:1.3.0" + checksum: 8cc4d1135990e61ab9d38a7b6460b018703b38b4dd3477390083018bffb93b283fabb7d57d83b3cfb78dd44da4f863167b964fe88dfa9886a54996f308036a94 languageName: node linkType: hard @@ -8639,14 +8535,14 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^7.0.1": - version: 7.0.1 - resolution: "fs-extra@npm:7.0.1" +"fs-extra@npm:^11.1.0": + version: 11.1.0 + resolution: "fs-extra@npm:11.1.0" dependencies: - graceful-fs: ^4.1.2 - jsonfile: ^4.0.0 - universalify: ^0.1.0 - checksum: 141b9dccb23b66a66cefdd81f4cda959ff89282b1d721b98cea19ba08db3dcbe6f862f28841f3cf24bb299e0b7e6c42303908f65093cb7e201708e86ea5a8dcf + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: 5ca476103fa1f5ff4a9b3c4f331548f8a3c1881edaae323a4415d3153b5dc11dc6a981c8d1dd93eec8367ceee27b53f8bd27eecbbf66ffcdd04927510c171e7f languageName: node linkType: hard @@ -9327,18 +9223,13 @@ fsevents@^1.2.7: "@babel/preset-env": ^7.20.2 "@babel/preset-typescript": ^7.18.6 "@braintree/sanitize-url": ^6.0.0 - "@codemirror/autocomplete": ^0.19.12 - "@codemirror/commands": ^0.19.8 - "@codemirror/gutter": ^0.19.9 - "@codemirror/highlight": ^0.19.7 - "@codemirror/history": ^0.19.2 - "@codemirror/legacy-modes": ^0.19.0 - "@codemirror/rectangular-selection": ^0.19.1 - "@codemirror/search": ^0.19.6 - "@codemirror/state": ^0.19.6 - "@codemirror/stream-parser": ^0.19.5 - "@codemirror/text": ^0.19.6 - "@codemirror/view": ^0.19.40 + "@codemirror/autocomplete": ^6.4.0 + "@codemirror/commands": ^6.1.3 + "@codemirror/language": ^6.3.2 + "@codemirror/legacy-modes": ^6.3.1 + "@codemirror/search": ^6.2.3 + "@codemirror/state": ^6.2.0 + "@codemirror/view": ^6.7.1 "@formatjs/intl-datetimeformat": ^4.2.5 "@formatjs/intl-getcanonicallocales": ^1.8.0 "@formatjs/intl-locale": ^2.4.40 @@ -9352,6 +9243,7 @@ fsevents@^1.2.7: "@fullcalendar/list": 5.9.0 "@fullcalendar/timegrid": 5.9.0 "@koa/cors": ^3.1.0 + "@lezer/highlight": ^1.1.3 "@lit-labs/motion": ^1.0.2 "@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch" "@material/chips": 14.0.0-canary.261f2db59.0 @@ -9378,8 +9270,8 @@ fsevents@^1.2.7: "@material/mwc-textfield": 0.25.3 "@material/mwc-top-app-bar-fixed": ^0.25.3 "@material/top-app-bar": 14.0.0-canary.261f2db59.0 - "@mdi/js": 7.0.96 - "@mdi/svg": 7.0.96 + "@mdi/js": 7.1.96 + "@mdi/svg": 7.1.96 "@octokit/auth-oauth-device": ^4.0.2 "@octokit/rest": ^19.0.4 "@open-wc/dev-server-hmr": ^0.0.2 @@ -9423,7 +9315,7 @@ fsevents@^1.2.7: "@vibrant/color": ^3.2.1-alpha.1 "@vibrant/core": ^3.2.1-alpha.1 "@vibrant/quantizer-mmcq": ^3.2.1-alpha.1 - "@vue/web-component-wrapper": ^1.2.0 + "@vue/web-component-wrapper": ^1.3.0 "@web/dev-server": ^0.0.24 "@web/dev-server-rollup": ^0.2.11 "@webcomponents/scoped-custom-element-registry": ^0.0.5 @@ -9451,7 +9343,7 @@ fsevents@^1.2.7: eslint-plugin-unused-imports: ^1.1.5 eslint-plugin-wc: ^1.3.2 fancy-log: ^2.0.0 - fs-extra: ^7.0.1 + fs-extra: ^11.1.0 fuse.js: ^6.0.0 glob: ^7.2.0 google-timezones-json: ^1.0.2 @@ -9485,7 +9377,7 @@ fsevents@^1.2.7: mocha: ^8.4.0 node-vibrant: 3.2.1-alpha.1 object-hash: ^2.0.3 - open: ^7.0.4 + open: ^8.4.0 pinst: ^3.0.0 prettier: ^2.8.1 proxy-polyfill: ^0.3.2 @@ -9512,7 +9404,7 @@ fsevents@^1.2.7: tinykeys: ^1.1.3 ts-lit-plugin: ^1.2.1 tsparticles: ^1.34.0 - typescript: ^4.9.3 + typescript: ^4.9.4 unfetch: ^4.1.0 vinyl-buffer: ^1.0.1 vinyl-source-stream: ^2.0.0 @@ -9526,13 +9418,13 @@ fsevents@^1.2.7: webpack-manifest-plugin: ^4.0.2 webpackbar: ^5.0.0-3 weekstart: ^1.1.0 - workbox-build: ^6.4.2 - workbox-cacheable-response: ^6.4.2 - workbox-core: ^6.4.2 - workbox-expiration: ^6.4.2 - workbox-precaching: ^6.4.2 - workbox-routing: ^6.4.2 - workbox-strategies: ^6.4.2 + workbox-build: ^6.5.4 + workbox-cacheable-response: ^6.5.4 + workbox-core: ^6.5.4 + workbox-expiration: ^6.5.4 + workbox-precaching: ^6.5.4 + workbox-routing: ^6.5.4 + workbox-strategies: ^6.5.4 xss: ^1.0.9 languageName: unknown linkType: soft @@ -9782,10 +9674,10 @@ fsevents@^1.2.7: languageName: node linkType: hard -"idb@npm:^6.1.4": - version: 6.1.5 - resolution: "idb@npm:6.1.5" - checksum: 45d81be3bf5d5ae6d009d62b4a7eeb873fe2a9972d235aaa5c33cd3e27947b33a01fd3fb7bbdbe795cd608d2279c55ccd2db3f8b3f486bc74bdb5eab1c1be957 +"idb@npm:^7.0.1": + version: 7.1.1 + resolution: "idb@npm:7.1.1" + checksum: 1973c28d53c784b177bdef9f527ec89ec239ec7cf5fcbd987dae75a16c03f5b7dfcc8c6d3285716fd0309dd57739805390bd9f98ce23b1b7d8849a3b52de8d56 languageName: node linkType: hard @@ -10740,13 +10632,13 @@ fsevents@^1.2.7: linkType: hard "json5@npm:^1.0.1": - version: 1.0.1 - resolution: "json5@npm:1.0.1" + version: 1.0.2 + resolution: "json5@npm:1.0.2" dependencies: minimist: ^1.2.0 bin: json5: lib/cli.js - checksum: e76ea23dbb8fc1348c143da628134a98adf4c5a4e8ea2adaa74a80c455fc2cdf0e2e13e6398ef819bfe92306b610ebb2002668ed9fc1af386d593691ef346fc3 + checksum: 866458a8c58a95a49bef3adba929c625e82532bcff1fe93f01d29cb02cac7c3fe1f4b79951b7792c2da9de0b32871a8401a6e3c5b36778ad852bf5b8a61165d7 languageName: node linkType: hard @@ -10759,18 +10651,6 @@ fsevents@^1.2.7: languageName: node linkType: hard -"jsonfile@npm:^4.0.0": - version: 4.0.0 - resolution: "jsonfile@npm:4.0.0" - dependencies: - graceful-fs: ^4.1.6 - dependenciesMeta: - graceful-fs: - optional: true - checksum: 6447d6224f0d31623eef9b51185af03ac328a7553efcee30fa423d98a9e276ca08db87d71e17f2310b0263fd3ffa6c2a90a6308367f661dc21580f9469897c9e - languageName: node - linkType: hard - "jsonfile@npm:^6.0.1": version: 6.1.0 resolution: "jsonfile@npm:6.1.0" @@ -12409,7 +12289,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"open@npm:^7.0.3, open@npm:^7.0.4, open@npm:^7.3.0": +"open@npm:^7.0.3, open@npm:^7.3.0": version: 7.3.0 resolution: "open@npm:7.3.0" dependencies: @@ -12419,14 +12299,14 @@ fsevents@^1.2.7: languageName: node linkType: hard -"open@npm:^8.0.9": - version: 8.2.1 - resolution: "open@npm:8.2.1" +"open@npm:^8.0.9, open@npm:^8.4.0": + version: 8.4.0 + resolution: "open@npm:8.4.0" dependencies: define-lazy-prop: ^2.0.0 is-docker: ^2.1.1 is-wsl: ^2.2.0 - checksum: fcde0059188dd497e080436f81c5240dad0bebd331d1c856a532d4b870808bdc5770ef7c5c4b83143fd0c0577fe2b580e54c03357d695771259aa59f64cf0f40 + checksum: e9545bec64cdbf30a0c35c1bdc310344adf8428a117f7d8df3c0af0a0a24c513b304916a6d9b11db0190ff7225c2d578885080b761ed46a3d5f6f1eebb98b63c languageName: node linkType: hard @@ -15445,13 +15325,13 @@ typescript@^3.8.3: languageName: node linkType: hard -"typescript@npm:^4.9.3": - version: 4.9.3 - resolution: "typescript@npm:4.9.3" +"typescript@npm:^4.9.4": + version: 4.9.4 + resolution: "typescript@npm:4.9.4" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 17b8f816050b412403e38d48eef0e893deb6be522d6dc7caf105e54a72e34daf6835c447735fd2b28b66784e72bfbf87f627abb4818a8e43d1fa8106396128dc + checksum: e782fb9e0031cb258a80000f6c13530288c6d63f1177ed43f770533fdc15740d271554cdae86701c1dd2c83b082cea808b07e97fd68b38a172a83dbf9e0d0ef9 languageName: node linkType: hard @@ -15465,13 +15345,13 @@ typescript@^3.8.3: languageName: node linkType: hard -"typescript@patch:typescript@^4.9.3#~builtin": - version: 4.9.3 - resolution: "typescript@patch:typescript@npm%3A4.9.3#~builtin::version=4.9.3&hash=a1c5e5" +"typescript@patch:typescript@^4.9.4#~builtin": + version: 4.9.4 + resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin::version=4.9.4&hash=a1c5e5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: ef65c22622d864497d0a0c5db693523329b3284c15fe632e93ad9aa059e8dc38ef3bd767d6f26b1e5ecf9446f49bd0f6c4e5714a2eeaf352805dc002479843d1 + checksum: 37f6e2c3c5e2aa5934b85b0fddbf32eeac8b1bacf3a5b51d01946936d03f5377fe86255d4e5a4ae628fd0cd553386355ad362c57f13b4635064400f3e8e05b9d languageName: node linkType: hard @@ -15638,13 +15518,6 @@ typescript@^3.8.3: languageName: node linkType: hard -"universalify@npm:^0.1.0": - version: 0.1.2 - resolution: "universalify@npm:0.1.2" - checksum: 40cdc60f6e61070fe658ca36016a8f4ec216b29bf04a55dce14e3710cc84c7448538ef4dad3728d0bfe29975ccd7bfb5f414c45e7b78883567fb31b246f02dff - languageName: node - linkType: hard - "universalify@npm:^2.0.0": version: 2.0.0 resolution: "universalify@npm:2.0.0" @@ -16379,28 +16252,28 @@ typescript@^3.8.3: languageName: node linkType: hard -"workbox-background-sync@npm:6.4.2": - version: 6.4.2 - resolution: "workbox-background-sync@npm:6.4.2" +"workbox-background-sync@npm:6.5.4": + version: 6.5.4 + resolution: "workbox-background-sync@npm:6.5.4" dependencies: - idb: ^6.1.4 - workbox-core: 6.4.2 - checksum: db8c267cef752176ab34b9d863334a700f27b70daa8109ca65fade7e2ff07f7969ccc2f64c075f043e2d8e3f89787c7f46e1bcde4c8a1a682f107c36f7e75d5e + idb: ^7.0.1 + workbox-core: 6.5.4 + checksum: 60ac80275cc9083b82eb53b6034e3d555d15146927a21c6017329e2b5de12d802619cc2cc6cf023f534a1f1a51671d89cdb59b26a80587d5391e8dc4b7f7dd1d languageName: node linkType: hard -"workbox-broadcast-update@npm:6.4.2": - version: 6.4.2 - resolution: "workbox-broadcast-update@npm:6.4.2" +"workbox-broadcast-update@npm:6.5.4": + version: 6.5.4 + resolution: "workbox-broadcast-update@npm:6.5.4" dependencies: - workbox-core: 6.4.2 - checksum: cbf948c84530edce754797e205ed36a2b9db3b4a2d9a97d23cab56d84bcb880f5a9f0b22549e456199c52d2feee926a138a6b4a3982e820b4a31ed64dcdd5b7d + workbox-core: 6.5.4 + checksum: 63cbab2012456871ffeae401e10b16668a0654fa3fa311743cf14e05b8719b797ac3afb47dc8955d87e24f0f1199a547b090bcfdbddd67191b07697d24ac5746 languageName: node linkType: hard -"workbox-build@npm:^6.4.2": - version: 6.4.2 - resolution: "workbox-build@npm:6.4.2" +"workbox-build@npm:^6.5.4": + version: 6.5.4 + resolution: "workbox-build@npm:6.5.4" dependencies: "@apideck/better-ajv-errors": ^0.3.1 "@babel/core": ^7.11.1 @@ -16420,153 +16293,152 @@ typescript@^3.8.3: rollup: ^2.43.1 rollup-plugin-terser: ^7.0.0 source-map: ^0.8.0-beta.0 - source-map-url: ^0.4.0 stringify-object: ^3.3.0 strip-comments: ^2.0.1 tempy: ^0.6.0 upath: ^1.2.0 - workbox-background-sync: 6.4.2 - workbox-broadcast-update: 6.4.2 - workbox-cacheable-response: 6.4.2 - workbox-core: 6.4.2 - workbox-expiration: 6.4.2 - workbox-google-analytics: 6.4.2 - workbox-navigation-preload: 6.4.2 - workbox-precaching: 6.4.2 - workbox-range-requests: 6.4.2 - workbox-recipes: 6.4.2 - workbox-routing: 6.4.2 - workbox-strategies: 6.4.2 - workbox-streams: 6.4.2 - workbox-sw: 6.4.2 - workbox-window: 6.4.2 - checksum: 3c8d45899b11420ae2584ce39487bd4a754e7a95bd79131ef7f3b7cbdbd6482048ef178fbb741182f45bcb4e0e9d43bcf3d2600347ea5a167ca396a0ffdce2b8 + workbox-background-sync: 6.5.4 + workbox-broadcast-update: 6.5.4 + workbox-cacheable-response: 6.5.4 + workbox-core: 6.5.4 + workbox-expiration: 6.5.4 + workbox-google-analytics: 6.5.4 + workbox-navigation-preload: 6.5.4 + workbox-precaching: 6.5.4 + workbox-range-requests: 6.5.4 + workbox-recipes: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + workbox-streams: 6.5.4 + workbox-sw: 6.5.4 + workbox-window: 6.5.4 + checksum: 7336bbab4ce8e6e43a17873beedf7360ec32e72310306c670cd4d9ebd7e5a6a729257b2806e63830136a9bf01955632c96b27edf7a00d52c7744dbe875cca6c1 languageName: node linkType: hard -"workbox-cacheable-response@npm:6.4.2, workbox-cacheable-response@npm:^6.4.2": - version: 6.4.2 - resolution: "workbox-cacheable-response@npm:6.4.2" +"workbox-cacheable-response@npm:6.5.4, workbox-cacheable-response@npm:^6.5.4": + version: 6.5.4 + resolution: "workbox-cacheable-response@npm:6.5.4" dependencies: - workbox-core: 6.4.2 - checksum: ca8e1d64ec55b9be8a79cd6b5d905a963693a13d9fd4641ac529e2bd88c03b3a7429b16252cd15e7f30351a90737a4095d6c896ef4e0aafdf652426a741cebbb + workbox-core: 6.5.4 + checksum: f7545b71c1505d6f56f4ba1191989ea7af7119e67fa4eb414d80603221acd0fa31362014106c1df9b9ea0e28bdcf1e2b440859acab06a75e38e978a0d1c2e489 languageName: node linkType: hard -"workbox-core@npm:6.4.2, workbox-core@npm:^6.4.2": - version: 6.4.2 - resolution: "workbox-core@npm:6.4.2" - checksum: bbdf4346e85d775d7162a49710957083bfa2b8cfc50b475bce02fcb62879ef1619ff381b00c969553a48b0c64c8b5ef7d9fce23fd5a64df1df8ed8f78667f23a +"workbox-core@npm:6.5.4, workbox-core@npm:^6.5.4": + version: 6.5.4 + resolution: "workbox-core@npm:6.5.4" + checksum: d973cc6c1c5fdbde7f6642632384c2e0de48f08228eb234db2c97a18a7e5422b483005767e7b447ea774abc0772dfc1edef2ef2b5df174df4d40ae61d4c49719 languageName: node linkType: hard -"workbox-expiration@npm:6.4.2, workbox-expiration@npm:^6.4.2": - version: 6.4.2 - resolution: "workbox-expiration@npm:6.4.2" +"workbox-expiration@npm:6.5.4, workbox-expiration@npm:^6.5.4": + version: 6.5.4 + resolution: "workbox-expiration@npm:6.5.4" dependencies: - idb: ^6.1.4 - workbox-core: 6.4.2 - checksum: 15234417ec60af7fc6222bbf812619a35e2c4b62187f7c3777b2ebab28cf0c4de1d5e728bb380400eaa5a4f6263436b4889ba3b3fbc80bba05844094fb691316 + idb: ^7.0.1 + workbox-core: 6.5.4 + checksum: 4b012b69ceafeb5afb3dd6c5c9abe6d55f2eb70666ab603bd78ff839f602336e7493990f729d507ded1fa505b852a5f9135f63afb75b9554c8f948e571143fce languageName: node linkType: hard -"workbox-google-analytics@npm:6.4.2": - version: 6.4.2 - resolution: "workbox-google-analytics@npm:6.4.2" +"workbox-google-analytics@npm:6.5.4": + version: 6.5.4 + resolution: "workbox-google-analytics@npm:6.5.4" dependencies: - workbox-background-sync: 6.4.2 - workbox-core: 6.4.2 - workbox-routing: 6.4.2 - workbox-strategies: 6.4.2 - checksum: 69e43a18c69881b293054af3550b38b182599ae93f261d5313f4a82a20b2c0f79667cf721ee9bf32cc76b1e2e77bd8409e5c8af02c7272f4553c7a1bc727b9f4 + workbox-background-sync: 6.5.4 + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + checksum: fcce5e313780cb4f74ac962c4809fe04f9a93d3d3905d282552a2cbe6d5c6c1b8744641fe7c57d1e4b62754b90c56155e97e589712f99f6a4cab750731d60b93 languageName: node linkType: hard -"workbox-navigation-preload@npm:6.4.2": - version: 6.4.2 - resolution: "workbox-navigation-preload@npm:6.4.2" +"workbox-navigation-preload@npm:6.5.4": + version: 6.5.4 + resolution: "workbox-navigation-preload@npm:6.5.4" dependencies: - workbox-core: 6.4.2 - checksum: ab8433b12d7273057389b9ef36a8cae605ce713625a523925c14d3345be04abfa432d01206fd5f10295250e935c51a65e0284e13d99c128f0cbd22b040252358 + workbox-core: 6.5.4 + checksum: c8c341b799f328bb294de8eb9e331a55501d495153237e4ddbaa08bf8630efa700621df5d81f08fb9bffc0f40ecd191a60581f72a3cd5cc72ed2e5baa318c63a languageName: node linkType: hard -"workbox-precaching@npm:6.4.2, workbox-precaching@npm:^6.4.2": - version: 6.4.2 - resolution: "workbox-precaching@npm:6.4.2" +"workbox-precaching@npm:6.5.4, workbox-precaching@npm:^6.5.4": + version: 6.5.4 + resolution: "workbox-precaching@npm:6.5.4" dependencies: - workbox-core: 6.4.2 - workbox-routing: 6.4.2 - workbox-strategies: 6.4.2 - checksum: b1d6c6a62418b4234b5a13aa1ed643908449ed1bc4acdbc2ffcc235341c36cd6e7b4d5fcee041c833b0c4bba07413a4da3a3a505b6f04745d2c19407e84e2f82 + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + checksum: 15ef24ffb04edd13bcdfa6c4e7f64002551badce2d507031c343019b3bcdc569591fdff8f8e30cf1262d641d3eff611115bdda7b2ad0deb9d4ccef8f4be8bd20 languageName: node linkType: hard -"workbox-range-requests@npm:6.4.2": - version: 6.4.2 - resolution: "workbox-range-requests@npm:6.4.2" +"workbox-range-requests@npm:6.5.4": + version: 6.5.4 + resolution: "workbox-range-requests@npm:6.5.4" dependencies: - workbox-core: 6.4.2 - checksum: 940297ed423ac414b7edf59cf4e499230f8340713a4818de4a103296f2a1b29a52371f5f2e7bc3c41f3ea9317f974b80385e4cc58d2adeed6efc4ada251e14c0 + workbox-core: 6.5.4 + checksum: 50f144ced7af7db77b3c64c06c0f9924db5b8573ff2c50b3899fc22c4a360baaf6b332e65f47cf812adfc9dec882a94556fed1cf90ae4ef20b645caa03d1149e languageName: node linkType: hard -"workbox-recipes@npm:6.4.2": - version: 6.4.2 - resolution: "workbox-recipes@npm:6.4.2" +"workbox-recipes@npm:6.5.4": + version: 6.5.4 + resolution: "workbox-recipes@npm:6.5.4" dependencies: - workbox-cacheable-response: 6.4.2 - workbox-core: 6.4.2 - workbox-expiration: 6.4.2 - workbox-precaching: 6.4.2 - workbox-routing: 6.4.2 - workbox-strategies: 6.4.2 - checksum: 75a07ba6317f5e2fbf51b4a432914065fa8e62d232515664fc40eddc96a2c355ed03efb72411d1e73e947d40a845a2bad85c22c80e43e23fcb60b739f7869e31 + workbox-cacheable-response: 6.5.4 + workbox-core: 6.5.4 + workbox-expiration: 6.5.4 + workbox-precaching: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + checksum: 397befeb7c4c63adb0eb1913934ecaf496846844124044f0b39348288ad5950ffb45eb488cfef2504adeafe28a51cdbcc21af2a234813d81ab3da0949942c265 languageName: node linkType: hard -"workbox-routing@npm:6.4.2, workbox-routing@npm:^6.4.2": - version: 6.4.2 - resolution: "workbox-routing@npm:6.4.2" +"workbox-routing@npm:6.5.4, workbox-routing@npm:^6.5.4": + version: 6.5.4 + resolution: "workbox-routing@npm:6.5.4" dependencies: - workbox-core: 6.4.2 - checksum: 7cb503caa2c87572235b0b891a07bd9bcebd644bd8eec715982b4b5285867bf885e772feef0c2b6797c868e4d65b6d1014654afde0ba779177d683f7b44e23ac + workbox-core: 6.5.4 + checksum: 7198c50b9016d3cea0e5b51512d66f5813d6e6ad5e99c201435d6c0ab3baee1c90aa2bbdd72dd954f439267b6e6196fb04ec96e62347e6c89385db6c1a4dec79 languageName: node linkType: hard -"workbox-strategies@npm:6.4.2, workbox-strategies@npm:^6.4.2": - version: 6.4.2 - resolution: "workbox-strategies@npm:6.4.2" +"workbox-strategies@npm:6.5.4, workbox-strategies@npm:^6.5.4": + version: 6.5.4 + resolution: "workbox-strategies@npm:6.5.4" dependencies: - workbox-core: 6.4.2 - checksum: f981ab0bb103695f765cb4305d0bf35ebe347bcb19c441a58b2b48e99dc238495b6cbb1d1d3f55c89f2dee3202d9d2f8cb31f10b98120a33ed52f7e838366a98 + workbox-core: 6.5.4 + checksum: 52134ecd6c05f4edd31e7b022b33a91b7b59c215bfdfb987bc0f10be02fea4d4e6385a9638a2303ba336190c5d28f9721182cd78a6779b9c817a66ec12cb1c6b languageName: node linkType: hard -"workbox-streams@npm:6.4.2": - version: 6.4.2 - resolution: "workbox-streams@npm:6.4.2" +"workbox-streams@npm:6.5.4": + version: 6.5.4 + resolution: "workbox-streams@npm:6.5.4" dependencies: - workbox-core: 6.4.2 - workbox-routing: 6.4.2 - checksum: b17223f0a6604a869b6564ce29932146c4d8d8ec0e9f8d36cede5776ccde78fe0400598373c119209429ee01281d3e371c16e2722ae5d95dd67d8d526048ca14 + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + checksum: efd6917ead915011be2b25dc3ebbb9d051dbd10ba2d91cdaec36ca742360e2c33627564653fc40f336dee874d501e94bcc4a25d1b65eaf5a6ee5f1a8b894af44 languageName: node linkType: hard -"workbox-sw@npm:6.4.2": - version: 6.4.2 - resolution: "workbox-sw@npm:6.4.2" - checksum: c9b05dc9af1f3da1cdb1ca8a2e57d273e63c76eaf29a216669234dea6934ee547f47836dd930143f3c04b5c756b38d1aa221efdc90f82bb69287bf3664849853 +"workbox-sw@npm:6.5.4": + version: 6.5.4 + resolution: "workbox-sw@npm:6.5.4" + checksum: b95c76a74b84ff268ef7691447125697f4de85b076ebc33c9545fb7532b020b6f66b37f7a4bedbc21ab45473d1109337a5f037c45b3d99126ae8f5eeb898a687 languageName: node linkType: hard -"workbox-window@npm:6.4.2": - version: 6.4.2 - resolution: "workbox-window@npm:6.4.2" +"workbox-window@npm:6.5.4": + version: 6.5.4 + resolution: "workbox-window@npm:6.5.4" dependencies: "@types/trusted-types": ^2.0.2 - workbox-core: 6.4.2 - checksum: 811dd5cae8f493e66e39729440b36a96ca3cb91b99595fd62f151c6f92f5e658109b0444aa3b91fafd1232220798c61c2164f1f2c76e21079abed9ceebe93f22 + workbox-core: 6.5.4 + checksum: bc43c8d31908ab564d740eb1041180c0b0ca4d1f0a3ccde59c5764a8f96d7b08edb7df975360fd37c2bec9f3f57ca9de6c7e34fd252aa1a4a075b5b002f74f60 languageName: node linkType: hard