mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 14:39:38 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			hack_safar
			...
			fix-backgr
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | cc1a0b24f0 | 
| @@ -115,7 +115,6 @@ | ||||
|       } | ||||
|     ], | ||||
|     "unused-imports/no-unused-imports": "error", | ||||
|     "lit/attribute-names": "warn", | ||||
|     "lit/attribute-value-entities": "off", | ||||
|     "lit/no-template-map": "off", | ||||
|     "lit/no-native-attributes": "warn", | ||||
| @@ -126,5 +125,6 @@ | ||||
|     "lit-a11y/anchor-is-valid": "warn", | ||||
|     "lit-a11y/role-has-required-aria-attrs": "warn" | ||||
|   }, | ||||
|   "plugins": ["unused-imports"] | ||||
|   "plugins": ["disable", "unused-imports"], | ||||
|   "processor": "disable/disable" | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
| @@ -57,7 +57,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|   | ||||
							
								
								
									
										12
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -24,7 +24,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -58,7 +58,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -76,7 +76,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -89,7 +89,7 @@ jobs: | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@v4.3.3 | ||||
|         uses: actions/upload-artifact@v4.3.1 | ||||
|         with: | ||||
|           name: frontend-bundle-stats | ||||
|           path: build/stats/*.json | ||||
| @@ -100,7 +100,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -113,7 +113,7 @@ jobs: | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@v4.3.3 | ||||
|         uses: actions/upload-artifact@v4.3.1 | ||||
|         with: | ||||
|           name: supervisor-bundle-stats | ||||
|           path: build/stats/*.json | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|         with: | ||||
|           # We must fetch at least the immediate parents so that if this is | ||||
|           # a pull request then we can checkout the head. | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -22,7 +22,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
| @@ -58,7 +58,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ 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@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -20,7 +20,7 @@ jobs: | ||||
|       contents: write | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|  | ||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||
|         uses: actions/setup-python@v5 | ||||
| @@ -57,14 +57,14 @@ jobs: | ||||
|         run: tar -czvf translations.tar.gz translations | ||||
|  | ||||
|       - name: Upload build artifacts | ||||
|         uses: actions/upload-artifact@v4.3.3 | ||||
|         uses: actions/upload-artifact@v4.3.1 | ||||
|         with: | ||||
|           name: wheels | ||||
|           path: dist/home_assistant_frontend*.whl | ||||
|           if-no-files-found: error | ||||
|  | ||||
|       - name: Upload translations | ||||
|         uses: actions/upload-artifact@v4.3.3 | ||||
|         uses: actions/upload-artifact@v4.3.1 | ||||
|         with: | ||||
|           name: translations | ||||
|           path: translations.tar.gz | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Send bundle stats and build information to RelativeCI | ||||
|         uses: relative-ci/agent-action@v2.1.11 | ||||
|         uses: relative-ci/agent-action@v2.1.10 | ||||
|         with: | ||||
|           key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} | ||||
|           token: ${{ github.token }} | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | ||||
|       contents: write # Required to upload release assets | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|  | ||||
|       - name: Verify version | ||||
|         uses: home-assistant/actions/helpers/verify-version@master | ||||
| @@ -55,7 +55,7 @@ jobs: | ||||
|           script/release | ||||
|  | ||||
|       - name: Upload release assets | ||||
|         uses: softprops/action-gh-release@v2.0.5 | ||||
|         uses: softprops/action-gh-release@v2.0.4 | ||||
|         with: | ||||
|           files: | | ||||
|             dist/*.whl | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -13,7 +13,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v4.1.6 | ||||
|         uses: actions/checkout@v4.1.2 | ||||
|  | ||||
|       - name: Upload Translations | ||||
|         run: | | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -6,4 +6,4 @@ enableGlobalCache: false | ||||
|  | ||||
| nodeLinker: node-modules | ||||
|  | ||||
| yarnPath: .yarn/releases/yarn-4.2.2.cjs | ||||
| yarnPath: .yarn/releases/yarn-4.1.1.cjs | ||||
|   | ||||
| @@ -1,56 +1,7 @@ | ||||
| import defineProvider from "@babel/helper-define-polyfill-provider"; | ||||
| import { join } from "node:path"; | ||||
| import paths from "../paths.cjs"; | ||||
|  | ||||
| const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills"); | ||||
|  | ||||
| // List of polyfill keys with supported browser targets for the functionality | ||||
| const PolyfillSupport = { | ||||
|   // Note states and shadowRoot properties should be supported. | ||||
|   "element-internals": { | ||||
|     android: 90, | ||||
|     chrome: 90, | ||||
|     edge: 90, | ||||
|     firefox: 126, | ||||
|     ios: 17.4, | ||||
|     opera: 76, | ||||
|     opera_mobile: 64, | ||||
|     safari: 17.4, | ||||
|     samsung: 15.0, | ||||
|   }, | ||||
|   "element-append": { | ||||
|     android: 54, | ||||
|     chrome: 54, | ||||
|     edge: 17, | ||||
|     firefox: 49, | ||||
|     ios: 10.0, | ||||
|     opera: 41, | ||||
|     opera_mobile: 41, | ||||
|     safari: 10.0, | ||||
|     samsung: 6.0, | ||||
|   }, | ||||
|   "element-getattributenames": { | ||||
|     android: 61, | ||||
|     chrome: 61, | ||||
|     edge: 18, | ||||
|     firefox: 45, | ||||
|     ios: 10.3, | ||||
|     opera: 48, | ||||
|     opera_mobile: 45, | ||||
|     safari: 10.1, | ||||
|     samsung: 8.0, | ||||
|   }, | ||||
|   "element-toggleattribute": { | ||||
|     android: 69, | ||||
|     chrome: 69, | ||||
|     edge: 18, | ||||
|     firefox: 63, | ||||
|     ios: 12.0, | ||||
|     opera: 56, | ||||
|     opera_mobile: 48, | ||||
|     safari: 12.0, | ||||
|     samsung: 10.0, | ||||
|   }, | ||||
|   fetch: { | ||||
|     android: 42, | ||||
|     chrome: 42, | ||||
| @@ -62,31 +13,6 @@ const PolyfillSupport = { | ||||
|     safari: 10.1, | ||||
|     samsung: 4.0, | ||||
|   }, | ||||
|   "intl-getcanonicallocales": { | ||||
|     android: 54, | ||||
|     chrome: 54, | ||||
|     edge: 16, | ||||
|     firefox: 48, | ||||
|     ios: 10.3, | ||||
|     opera: 41, | ||||
|     opera_mobile: 41, | ||||
|     safari: 10.1, | ||||
|     samsung: 6.0, | ||||
|   }, | ||||
|   "intl-locale": { | ||||
|     android: 74, | ||||
|     chrome: 74, | ||||
|     edge: 79, | ||||
|     firefox: 75, | ||||
|     ios: 14.0, | ||||
|     opera: 62, | ||||
|     opera_mobile: 53, | ||||
|     safari: 14.0, | ||||
|     samsung: 11.0, | ||||
|   }, | ||||
|   "intl-other": { | ||||
|     // Not specified (i.e. always try polyfill) since compatibility depends on supported locales | ||||
|   }, | ||||
|   proxy: { | ||||
|     android: 49, | ||||
|     chrome: 49, | ||||
| @@ -98,67 +24,17 @@ const PolyfillSupport = { | ||||
|     safari: 10.0, | ||||
|     samsung: 5.0, | ||||
|   }, | ||||
|   "resize-observer": { | ||||
|     android: 64, | ||||
|     chrome: 64, | ||||
|     edge: 79, | ||||
|     firefox: 69, | ||||
|     ios: 13.4, | ||||
|     opera: 51, | ||||
|     opera_mobile: 47, | ||||
|     safari: 13.1, | ||||
|     samsung: 9.0, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| // Map of global variables and/or instance and static properties to the | ||||
| // corresponding polyfill key and actual module to import | ||||
| const polyfillMap = { | ||||
|   global: { | ||||
|     fetch: { key: "fetch", module: "unfetch/polyfill" }, | ||||
|     Proxy: { key: "proxy", module: "proxy-polyfill" }, | ||||
|     ResizeObserver: { | ||||
|       key: "resize-observer", | ||||
|       module: join(POLYFILL_DIR, "resize-observer.ts"), | ||||
|     }, | ||||
|   }, | ||||
|   instance: { | ||||
|     attachInternals: { | ||||
|       key: "element-internals", | ||||
|       module: "element-internals-polyfill", | ||||
|     }, | ||||
|     ...Object.fromEntries( | ||||
|       ["append", "getAttributeNames", "toggleAttribute"].map((prop) => { | ||||
|         const key = `element-${prop.toLowerCase()}`; | ||||
|         return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }]; | ||||
|       }) | ||||
|     ), | ||||
|   }, | ||||
|   static: { | ||||
|     Intl: { | ||||
|       getCanonicalLocales: { | ||||
|         key: "intl-getcanonicallocales", | ||||
|         module: join(POLYFILL_DIR, "intl-polyfill.ts"), | ||||
|       }, | ||||
|       Locale: { | ||||
|         key: "intl-locale", | ||||
|         module: join(POLYFILL_DIR, "intl-polyfill.ts"), | ||||
|       }, | ||||
|       ...Object.fromEntries( | ||||
|         [ | ||||
|           "DateTimeFormat", | ||||
|           "DisplayNames", | ||||
|           "ListFormat", | ||||
|           "NumberFormat", | ||||
|           "PluralRules", | ||||
|           "RelativeTimeFormat", | ||||
|         ].map((obj) => [ | ||||
|           obj, | ||||
|           { key: "intl-other", module: join(POLYFILL_DIR, "intl-polyfill.ts") }, | ||||
|         ]) | ||||
|       ), | ||||
|     }, | ||||
|     fetch: { key: "fetch", module: "unfetch/polyfill" }, | ||||
|   }, | ||||
|   instance: {}, | ||||
|   static: {}, | ||||
| }; | ||||
|  | ||||
| // Create plugin using the same factory as for CoreJS | ||||
| @@ -166,16 +42,14 @@ export default defineProvider( | ||||
|   ({ createMetaResolver, debug, shouldInjectPolyfill }) => { | ||||
|     const resolvePolyfill = createMetaResolver(polyfillMap); | ||||
|     return { | ||||
|       name: "custom-polyfill", | ||||
|       name: "HA Custom", | ||||
|       polyfills: PolyfillSupport, | ||||
|       usageGlobal(meta, utils) { | ||||
|         const polyfill = resolvePolyfill(meta); | ||||
|         if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) { | ||||
|           debug(polyfill.desc.key); | ||||
|           utils.injectGlobalImport(polyfill.desc.module); | ||||
|           return true; | ||||
|         } | ||||
|         return false; | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
|   | ||||
| @@ -3,8 +3,6 @@ const env = require("./env.cjs"); | ||||
| const paths = require("./paths.cjs"); | ||||
| const { dependencies } = require("../package.json"); | ||||
|  | ||||
| const BABEL_PLUGINS = path.join(__dirname, "babel-plugins"); | ||||
|  | ||||
| // GitHub base URL to use for production source maps | ||||
| // Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version | ||||
| module.exports.sourceMapURL = () => { | ||||
| @@ -102,12 +100,22 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ | ||||
|   ], | ||||
|   plugins: [ | ||||
|     [ | ||||
|       path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"), | ||||
|       path.resolve( | ||||
|         paths.polymer_dir, | ||||
|         "build-scripts/babel-plugins/inline-constants-plugin.cjs" | ||||
|       ), | ||||
|       { | ||||
|         modules: ["@mdi/js"], | ||||
|         ignoreModuleNotFound: true, | ||||
|       }, | ||||
|     ], | ||||
|     [ | ||||
|       path.resolve( | ||||
|         paths.polymer_dir, | ||||
|         "build-scripts/babel-plugins/custom-polyfill-plugin.js" | ||||
|       ), | ||||
|       { method: "usage-global" }, | ||||
|     ], | ||||
|     // Minify template literals for production | ||||
|     isProdBuild && [ | ||||
|       "template-html-minifier", | ||||
| @@ -145,27 +153,6 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ | ||||
|   ], | ||||
|   sourceMaps: !isTestBuild, | ||||
|   overrides: [ | ||||
|     { | ||||
|       // Add plugin to inject various polyfills, excluding the polyfills | ||||
|       // themselves to prevent self-injection. | ||||
|       plugins: [ | ||||
|         [ | ||||
|           path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"), | ||||
|           { method: "usage-global" }, | ||||
|         ], | ||||
|       ], | ||||
|       exclude: [ | ||||
|         path.join(paths.polymer_dir, "src/resources/polyfills"), | ||||
|         ...[ | ||||
|           "@formatjs/intl-\\w+", | ||||
|           "@lit-labs/virtualizer/polyfills", | ||||
|           "@webcomponents/scoped-custom-element-registry", | ||||
|           "element-internals-polyfill", | ||||
|           "proxy-polyfill", | ||||
|           "unfetch", | ||||
|         ].map((p) => new RegExp(`/node_modules/${p}/`)), | ||||
|       ], | ||||
|     }, | ||||
|     { | ||||
|       // Use unambiguous for dependencies so that require() is correctly injected into CommonJS files | ||||
|       // Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import gulp from "gulp"; | ||||
| import jszip from "jszip"; | ||||
| import path from "path"; | ||||
| import process from "process"; | ||||
| import { extract } from "tar"; | ||||
| import tar from "tar"; | ||||
|  | ||||
| const MAX_AGE = 24; // hours | ||||
| const OWNER = "home-assistant"; | ||||
| @@ -156,7 +156,7 @@ gulp.task("fetch-nightly-translations", async function () { | ||||
|   console.log("Unpacking downloaded translations..."); | ||||
|   const zip = await jszip.loadAsync(downloadResponse.data); | ||||
|   await deleteCurrent; | ||||
|   const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract()); | ||||
|   const extractStream = zip.file(/.*/)[0].nodeStream().pipe(tar.extract()); | ||||
|   await new Promise((resolve, reject) => { | ||||
|     extractStream.on("close", resolve).on("error", reject); | ||||
|   }); | ||||
|   | ||||
| @@ -1,112 +1,92 @@ | ||||
| /* eslint-disable max-classes-per-file */ | ||||
|  | ||||
| import { deleteAsync } from "del"; | ||||
| import { glob } from "glob"; | ||||
| import { createHash } from "crypto"; | ||||
| import { deleteSync } from "del"; | ||||
| import { mkdirSync, readdirSync, readFileSync, renameSync } from "fs"; | ||||
| import { writeFile } from "node:fs/promises"; | ||||
| import gulp from "gulp"; | ||||
| import flatmap from "gulp-flatmap"; | ||||
| import transform from "gulp-json-transform"; | ||||
| import merge from "gulp-merge-json"; | ||||
| import rename from "gulp-rename"; | ||||
| import merge from "lodash.merge"; | ||||
| import { createHash } from "node:crypto"; | ||||
| import { mkdir, readFile } from "node:fs/promises"; | ||||
| import { basename, join } from "node:path"; | ||||
| import { PassThrough, Transform } from "node:stream"; | ||||
| import { finished } from "node:stream/promises"; | ||||
| import path from "path"; | ||||
| import vinylBuffer from "vinyl-buffer"; | ||||
| import source from "vinyl-source-stream"; | ||||
| import env from "../env.cjs"; | ||||
| import paths from "../paths.cjs"; | ||||
| import { mapFiles } from "../util.cjs"; | ||||
| import "./fetch-nightly-translations.js"; | ||||
|  | ||||
| const inFrontendDir = "translations/frontend"; | ||||
| const inBackendDir = "translations/backend"; | ||||
| const workDir = "build/translations"; | ||||
| const outDir = join(workDir, "output"); | ||||
| const EN_SRC = join(paths.translations_src, "en.json"); | ||||
| const TEST_LOCALE = "en-x-test"; | ||||
|  | ||||
| const fullDir = workDir + "/full"; | ||||
| const coreDir = workDir + "/core"; | ||||
| const outDir = workDir + "/output"; | ||||
| let mergeBackend = false; | ||||
|  | ||||
| gulp.task( | ||||
|   "translations-enable-merge-backend", | ||||
|   gulp.parallel(async () => { | ||||
|   gulp.parallel((done) => { | ||||
|     mergeBackend = true; | ||||
|     done(); | ||||
|   }, "allow-setup-fetch-nightly-translations") | ||||
| ); | ||||
|  | ||||
| // Transform stream to apply a function on Vinyl JSON files (buffer mode only). | ||||
| // The provided function can either return a new object, or an array of | ||||
| // [object, subdirectory] pairs for fragmentizing the JSON. | ||||
| class CustomJSON extends Transform { | ||||
|   constructor(func, reviver = null) { | ||||
|     super({ objectMode: true }); | ||||
|     this._func = func; | ||||
|     this._reviver = reviver; | ||||
|   } | ||||
| // Panel translations which should be split from the core translations. | ||||
| const TRANSLATION_FRAGMENTS = Object.keys( | ||||
|   JSON.parse( | ||||
|     readFileSync( | ||||
|       path.resolve(paths.polymer_dir, "src/translations/en.json"), | ||||
|       "utf-8" | ||||
|     ) | ||||
|   ).ui.panel | ||||
| ); | ||||
|  | ||||
|   async _transform(file, _, callback) { | ||||
|     try { | ||||
|       let obj = JSON.parse(file.contents.toString(), this._reviver); | ||||
|       if (this._func) obj = this._func(obj, file.path); | ||||
|       for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) { | ||||
|         const outFile = file.clone({ contents: false }); | ||||
|         outFile.contents = Buffer.from(JSON.stringify(outObj)); | ||||
|         outFile.dirname += `/${dir}`; | ||||
|         this.push(outFile); | ||||
|       } | ||||
|       callback(null); | ||||
|     } catch (err) { | ||||
|       callback(err); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Transform stream to merge Vinyl JSON files (buffer mode only). | ||||
| class MergeJSON extends Transform { | ||||
|   _objects = []; | ||||
|  | ||||
|   constructor(stem, startObj = {}, reviver = null) { | ||||
|     super({ objectMode: true, allowHalfOpen: false }); | ||||
|     this._stem = stem; | ||||
|     this._startObj = structuredClone(startObj); | ||||
|     this._reviver = reviver; | ||||
|   } | ||||
|  | ||||
|   async _transform(file, _, callback) { | ||||
|     try { | ||||
|       this._objects.push(JSON.parse(file.contents.toString(), this._reviver)); | ||||
|       if (!this._outFile) this._outFile = file.clone({ contents: false }); | ||||
|       callback(null); | ||||
|     } catch (err) { | ||||
|       callback(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async _flush(callback) { | ||||
|     try { | ||||
|       const mergedObj = merge(this._startObj, ...this._objects); | ||||
|       this._outFile.contents = Buffer.from(JSON.stringify(mergedObj)); | ||||
|       this._outFile.stem = this._stem; | ||||
|       callback(null, this._outFile); | ||||
|     } catch (err) { | ||||
|       callback(err); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Utility to flatten object keys to single level using separator | ||||
| const flatten = (data, prefix = "", sep = ".") => { | ||||
|   const output = {}; | ||||
|   for (const [key, value] of Object.entries(data)) { | ||||
|     if (typeof value === "object") { | ||||
|       Object.assign(output, flatten(value, prefix + key + sep, sep)); | ||||
|     } else { | ||||
|       output[prefix + key] = value; | ||||
|     } | ||||
|   } | ||||
|   return output; | ||||
| function recursiveFlatten(prefix, data) { | ||||
|   let output = {}; | ||||
|   Object.keys(data).forEach((key) => { | ||||
|     if (typeof data[key] === "object") { | ||||
|       output = { | ||||
|         ...output, | ||||
|         ...recursiveFlatten(prefix + key + ".", data[key]), | ||||
|       }; | ||||
|     } else { | ||||
|       output[prefix + key] = data[key]; | ||||
|     } | ||||
|   }); | ||||
|   return output; | ||||
| } | ||||
|  | ||||
| // Filter functions that can be passed directly to JSON.parse() | ||||
| const emptyReviver = (_key, value) => value || undefined; | ||||
| const testReviver = (_key, value) => | ||||
|   value && typeof value === "string" ? "TRANSLATED" : value; | ||||
| function flatten(data) { | ||||
|   return recursiveFlatten("", data); | ||||
| } | ||||
|  | ||||
| function emptyFilter(data) { | ||||
|   const newData = {}; | ||||
|   Object.keys(data).forEach((key) => { | ||||
|     if (data[key]) { | ||||
|       if (typeof data[key] === "object") { | ||||
|         newData[key] = emptyFilter(data[key]); | ||||
|       } else { | ||||
|         newData[key] = data[key]; | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|   return newData; | ||||
| } | ||||
|  | ||||
| function recursiveEmpty(data) { | ||||
|   const newData = {}; | ||||
|   Object.keys(data).forEach((key) => { | ||||
|     if (data[key]) { | ||||
|       if (typeof data[key] === "object") { | ||||
|         newData[key] = recursiveEmpty(data[key]); | ||||
|       } else { | ||||
|         newData[key] = "TRANSLATED"; | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|   return newData; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Replace Lokalise key placeholders with their actual values. | ||||
| @@ -115,44 +95,60 @@ const testReviver = (_key, value) => | ||||
|  * be included in src/translations/en.json, but still be usable while | ||||
|  * developing locally. | ||||
|  * | ||||
|  * @link https://docs.lokalise.com/en/articles/1400528-key-referencing | ||||
|  * @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing | ||||
|  */ | ||||
| const KEY_REFERENCE = /\[%key:([^%]+)%\]/; | ||||
| const lokaliseTransform = (data, path, original = data) => { | ||||
| const re_key_reference = /\[%key:([^%]+)%\]/; | ||||
| function lokaliseTransform(data, original, file) { | ||||
|   const output = {}; | ||||
|   for (const [key, value] of Object.entries(data)) { | ||||
|     if (typeof value === "object") { | ||||
|       output[key] = lokaliseTransform(value, path, original); | ||||
|   Object.entries(data).forEach(([key, value]) => { | ||||
|     if (value instanceof Object) { | ||||
|       output[key] = lokaliseTransform(value, original, file); | ||||
|     } else { | ||||
|       output[key] = value.replace(KEY_REFERENCE, (_match, lokalise_key) => { | ||||
|       output[key] = value.replace(re_key_reference, (_match, lokalise_key) => { | ||||
|         const replace = lokalise_key.split("::").reduce((tr, k) => { | ||||
|           if (!tr) { | ||||
|             throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`); | ||||
|             throw Error( | ||||
|               `Invalid key placeholder ${lokalise_key} in ${file.path}` | ||||
|             ); | ||||
|           } | ||||
|           return tr[k]; | ||||
|         }, original); | ||||
|         if (typeof replace !== "string") { | ||||
|           throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`); | ||||
|           throw Error( | ||||
|             `Invalid key placeholder ${lokalise_key} in ${file.path}` | ||||
|           ); | ||||
|         } | ||||
|         return replace; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|   }); | ||||
|   return output; | ||||
| }; | ||||
| } | ||||
|  | ||||
| gulp.task("clean-translations", () => deleteAsync([workDir])); | ||||
| gulp.task("clean-translations", async () => deleteSync([workDir])); | ||||
|  | ||||
| const makeWorkDir = () => mkdir(workDir, { recursive: true }); | ||||
| gulp.task("ensure-translations-build-dir", async () => { | ||||
|   mkdirSync(workDir, { recursive: true }); | ||||
| }); | ||||
|  | ||||
| const createTestTranslation = () => | ||||
| gulp.task("create-test-metadata", () => | ||||
|   env.isProdBuild() | ||||
|     ? Promise.resolve() | ||||
|     : writeFile( | ||||
|         workDir + "/testMetadata.json", | ||||
|         JSON.stringify({ test: { nativeName: "Test" } }) | ||||
|       ) | ||||
| ); | ||||
|  | ||||
| gulp.task("create-test-translation", () => | ||||
|   env.isProdBuild() | ||||
|     ? Promise.resolve() | ||||
|     : gulp | ||||
|         .src(EN_SRC) | ||||
|         .pipe(new CustomJSON(null, testReviver)) | ||||
|         .pipe(rename(`${TEST_LOCALE}.json`)) | ||||
|         .pipe(gulp.dest(workDir)); | ||||
|         .src(path.join(paths.translations_src, "en.json")) | ||||
|         .pipe(transform((data, _file) => recursiveEmpty(data))) | ||||
|         .pipe(rename("test.json")) | ||||
|         .pipe(gulp.dest(workDir)) | ||||
| ); | ||||
|  | ||||
| /** | ||||
|  * This task will build a master translation file, to be used as the base for | ||||
| @@ -163,164 +159,279 @@ const createTestTranslation = () => | ||||
|  * project is buildable immediately after merging new translation keys, since | ||||
|  * the Lokalise update to translations/en.json will not happen immediately. | ||||
|  */ | ||||
| const createMasterTranslation = () => | ||||
|   gulp | ||||
|     .src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])]) | ||||
|     .pipe(new CustomJSON(lokaliseTransform)) | ||||
|     .pipe(new MergeJSON("en")) | ||||
|     .pipe(gulp.dest(workDir)); | ||||
| gulp.task("build-master-translation", () => { | ||||
|   const src = [path.join(paths.translations_src, "en.json")]; | ||||
|  | ||||
| const FRAGMENTS = ["base"]; | ||||
|  | ||||
| const toggleSupervisorFragment = async () => { | ||||
|   FRAGMENTS[0] = "supervisor"; | ||||
| }; | ||||
|  | ||||
| const panelFragment = (fragment) => | ||||
|   fragment !== "base" && fragment !== "supervisor"; | ||||
|  | ||||
| const HASHES = new Map(); | ||||
|  | ||||
| const createTranslations = async () => { | ||||
|   // Parse and store the master to avoid repeating this for each locale, then | ||||
|   // add the panel fragments when processing the app. | ||||
|   const enMaster = JSON.parse(await readFile(`${workDir}/en.json`, "utf-8")); | ||||
|   if (FRAGMENTS[0] === "base") { | ||||
|     FRAGMENTS.push(...Object.keys(enMaster.ui.panel)); | ||||
|   if (mergeBackend) { | ||||
|     src.push(path.join(inBackendDir, "en.json")); | ||||
|   } | ||||
|  | ||||
|   // The downstream pipeline is setup first.  It hashes the merged data for | ||||
|   // each locale, then fragmentizes and flattens the data for final output. | ||||
|   const translationFiles = await glob([ | ||||
|     `${inFrontendDir}/!(en).json`, | ||||
|     ...(env.isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]), | ||||
|   ]); | ||||
|   const hashStream = new Transform({ | ||||
|     objectMode: true, | ||||
|     transform: async (file, _, callback) => { | ||||
|       const hash = env.isProdBuild() | ||||
|         ? createHash("md5").update(file.contents).digest("hex") | ||||
|         : "dev"; | ||||
|       HASHES.set(file.stem, hash); | ||||
|       file.stem += `-${hash}`; | ||||
|       callback(null, file); | ||||
|     }, | ||||
|   }).setMaxListeners(translationFiles.length + 1); | ||||
|   const fragmentsStream = hashStream | ||||
|   return gulp | ||||
|     .src(src) | ||||
|     .pipe(transform((data, file) => lokaliseTransform(data, data, file))) | ||||
|     .pipe( | ||||
|       new CustomJSON((data) => | ||||
|         FRAGMENTS.map((fragment) => { | ||||
|           switch (fragment) { | ||||
|             case "base": | ||||
|               // Remove the panels and supervisor to create the base translations | ||||
|               return [ | ||||
|                 flatten({ | ||||
|                   ...data, | ||||
|                   ui: { ...data.ui, panel: undefined }, | ||||
|                   supervisor: undefined, | ||||
|                 }), | ||||
|                 "", | ||||
|               ]; | ||||
|             case "supervisor": | ||||
|               // Supervisor key is at the top level | ||||
|               return [flatten(data.supervisor), ""]; | ||||
|             default: | ||||
|               // Create a fragment with only the given panel | ||||
|               return [ | ||||
|                 flatten(data.ui.panel[fragment], `ui.panel.${fragment}.`), | ||||
|                 fragment, | ||||
|               ]; | ||||
|           } | ||||
|       merge({ | ||||
|         fileName: "en.json", | ||||
|       }) | ||||
|     ) | ||||
|     ) | ||||
|     .pipe(gulp.dest(outDir)); | ||||
|     .pipe(gulp.dest(fullDir)); | ||||
| }); | ||||
|  | ||||
|   // Send the English master downstream first, then for each other locale | ||||
|   // generate merged JSON data to continue piping. It begins with the master | ||||
| gulp.task("build-merged-translations", () => | ||||
|   gulp | ||||
|     .src([ | ||||
|       inFrontendDir + "/*.json", | ||||
|       "!" + inFrontendDir + "/en.json", | ||||
|       ...(env.isProdBuild() ? [] : [workDir + "/test.json"]), | ||||
|     ]) | ||||
|     .pipe(transform((data, file) => lokaliseTransform(data, data, file))) | ||||
|     .pipe( | ||||
|       flatmap((stream, file) => { | ||||
|         // For each language generate a merged json file. It begins with the master | ||||
|         // translation as a failsafe for untranslated strings, and merges all parent | ||||
|         // tags into one file for each specific subtag | ||||
|         // | ||||
|         // TODO: This is a naive interpretation of BCP47 that should be improved. | ||||
|         //       Will be OK for now as long as we don't have anything more complicated | ||||
|         //       than a base translation + region. | ||||
|   gulp | ||||
|     .src(`${workDir}/en.json`) | ||||
|     .pipe(new PassThrough({ objectMode: true })) | ||||
|     .pipe(hashStream, { end: false }); | ||||
|   const mergesFinished = []; | ||||
|   for (const translationFile of translationFiles) { | ||||
|     const locale = basename(translationFile, ".json"); | ||||
|     const subtags = locale.split("-"); | ||||
|     const mergeFiles = []; | ||||
|         const tr = path.basename(file.history[0], ".json"); | ||||
|         const subtags = tr.split("-"); | ||||
|         const src = [fullDir + "/en.json"]; | ||||
|         for (let i = 1; i <= subtags.length; i++) { | ||||
|           const lang = subtags.slice(0, i).join("-"); | ||||
|       if (lang === TEST_LOCALE) { | ||||
|         mergeFiles.push(`${workDir}/${TEST_LOCALE}.json`); | ||||
|           if (lang === "test") { | ||||
|             src.push(workDir + "/test.json"); | ||||
|           } else if (lang !== "en") { | ||||
|         mergeFiles.push(`${inFrontendDir}/${lang}.json`); | ||||
|             src.push(inFrontendDir + "/" + lang + ".json"); | ||||
|             if (mergeBackend) { | ||||
|           mergeFiles.push(`${inBackendDir}/${lang}.json`); | ||||
|               src.push(inBackendDir + "/" + lang + ".json"); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|     const mergeStream = gulp | ||||
|       .src(mergeFiles, { allowEmpty: true }) | ||||
|       .pipe(new MergeJSON(locale, enMaster, emptyReviver)); | ||||
|     mergesFinished.push(finished(mergeStream)); | ||||
|     mergeStream.pipe(hashStream, { end: false }); | ||||
|   } | ||||
|  | ||||
|   // Wait for all merges to finish, then it's safe to end writing to the | ||||
|   // downstream pipeline and wait for all fragments to finish writing. | ||||
|   await Promise.all(mergesFinished); | ||||
|   hashStream.end(); | ||||
|   await finished(fragmentsStream); | ||||
| }; | ||||
|  | ||||
| const writeTranslationMetaData = () => | ||||
|   gulp | ||||
|     .src([`${paths.translations_src}/translationMetadata.json`]) | ||||
|         return gulp | ||||
|           .src(src, { allowEmpty: true }) | ||||
|           .pipe(transform((data) => emptyFilter(data))) | ||||
|           .pipe( | ||||
|       new CustomJSON((meta) => { | ||||
|         // Add the test translation in development. | ||||
|         if (!env.isProdBuild()) { | ||||
|           meta[TEST_LOCALE] = { nativeName: "Translation Test" }; | ||||
|         } | ||||
|         // Filter out locales without a native name, and add the hashes. | ||||
|         for (const locale of Object.keys(meta)) { | ||||
|           if (!meta[locale].nativeName) { | ||||
|             meta[locale] = undefined; | ||||
|             console.warn( | ||||
|               `Skipping locale ${locale} because native name is not translated.` | ||||
|             ); | ||||
|           } else { | ||||
|             meta[locale].hash = HASHES.get(locale); | ||||
|           } | ||||
|         } | ||||
|         return { | ||||
|           fragments: FRAGMENTS.filter(panelFragment), | ||||
|           translations: meta, | ||||
|         }; | ||||
|             merge({ | ||||
|               fileName: tr + ".json", | ||||
|             }) | ||||
|           ) | ||||
|     .pipe(gulp.dest(workDir)); | ||||
|           .pipe(gulp.dest(fullDir)); | ||||
|       }) | ||||
|     ) | ||||
| ); | ||||
|  | ||||
| let taskName; | ||||
|  | ||||
| const splitTasks = []; | ||||
| TRANSLATION_FRAGMENTS.forEach((fragment) => { | ||||
|   taskName = "build-translation-fragment-" + fragment; | ||||
|   gulp.task(taskName, () => | ||||
|     // Return only the translations for this fragment. | ||||
|     gulp | ||||
|       .src(fullDir + "/*.json") | ||||
|       .pipe( | ||||
|         transform((data) => ({ | ||||
|           ui: { | ||||
|             panel: { | ||||
|               [fragment]: data.ui.panel[fragment], | ||||
|             }, | ||||
|           }, | ||||
|         })) | ||||
|       ) | ||||
|       .pipe(gulp.dest(workDir + "/" + fragment)) | ||||
|   ); | ||||
|   splitTasks.push(taskName); | ||||
| }); | ||||
|  | ||||
| taskName = "build-translation-core"; | ||||
| gulp.task(taskName, () => | ||||
|   // Remove the fragment translations from the core translation. | ||||
|   gulp | ||||
|     .src(fullDir + "/*.json") | ||||
|     .pipe( | ||||
|       transform((data, _file) => { | ||||
|         TRANSLATION_FRAGMENTS.forEach((fragment) => { | ||||
|           delete data.ui.panel[fragment]; | ||||
|         }); | ||||
|         delete data.supervisor; | ||||
|         return data; | ||||
|       }) | ||||
|     ) | ||||
|     .pipe(gulp.dest(coreDir)) | ||||
| ); | ||||
|  | ||||
| splitTasks.push(taskName); | ||||
|  | ||||
| gulp.task("build-flattened-translations", () => | ||||
|   // Flatten the split versions of our translations, and move them into outDir | ||||
|   gulp | ||||
|     .src( | ||||
|       TRANSLATION_FRAGMENTS.map( | ||||
|         (fragment) => workDir + "/" + fragment + "/*.json" | ||||
|       ).concat(coreDir + "/*.json"), | ||||
|       { base: workDir } | ||||
|     ) | ||||
|     .pipe( | ||||
|       transform((data) => | ||||
|         // Polymer.AppLocalizeBehavior requires flattened json | ||||
|         flatten(data) | ||||
|       ) | ||||
|     ) | ||||
|     .pipe( | ||||
|       rename((filePath) => { | ||||
|         if (filePath.dirname === "core") { | ||||
|           filePath.dirname = ""; | ||||
|         } | ||||
|         // In dev we create the file with the fake hash in the filename | ||||
|         if (!env.isProdBuild()) { | ||||
|           filePath.basename += "-dev"; | ||||
|         } | ||||
|       }) | ||||
|     ) | ||||
|     .pipe(gulp.dest(outDir)) | ||||
| ); | ||||
|  | ||||
| const fingerprints = {}; | ||||
|  | ||||
| gulp.task("build-translation-fingerprints", () => { | ||||
|   // Fingerprint full file of each language | ||||
|   const files = readdirSync(fullDir); | ||||
|  | ||||
|   for (let i = 0; i < files.length; i++) { | ||||
|     fingerprints[files[i].split(".")[0]] = { | ||||
|       // In dev we create fake hashes | ||||
|       hash: env.isProdBuild() | ||||
|         ? createHash("md5") | ||||
|             .update(readFileSync(path.join(fullDir, files[i]), "utf-8")) | ||||
|             .digest("hex") | ||||
|         : "dev", | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // In dev we create the file with the fake hash in the filename | ||||
|   if (env.isProdBuild()) { | ||||
|     mapFiles(outDir, ".json", (filename) => { | ||||
|       const parsed = path.parse(filename); | ||||
|  | ||||
|       // nl.json -> nl-<hash>.json | ||||
|       if (!(parsed.name in fingerprints)) { | ||||
|         throw new Error(`Unable to find hash for ${filename}`); | ||||
|       } | ||||
|  | ||||
|       renameSync( | ||||
|         filename, | ||||
|         `${parsed.dir}/${parsed.name}-${fingerprints[parsed.name].hash}${ | ||||
|           parsed.ext | ||||
|         }` | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   const stream = source("translationFingerprints.json"); | ||||
|   stream.write(JSON.stringify(fingerprints)); | ||||
|   process.nextTick(() => stream.end()); | ||||
|   return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir)); | ||||
| }); | ||||
|  | ||||
| gulp.task("build-translation-fragment-supervisor", () => | ||||
|   gulp | ||||
|     .src(fullDir + "/*.json") | ||||
|     .pipe(transform((data) => data.supervisor)) | ||||
|     .pipe( | ||||
|       rename((filePath) => { | ||||
|         // In dev we create the file with the fake hash in the filename | ||||
|         if (!env.isProdBuild()) { | ||||
|           filePath.basename += "-dev"; | ||||
|         } | ||||
|       }) | ||||
|     ) | ||||
|     .pipe(gulp.dest(workDir + "/supervisor")) | ||||
| ); | ||||
|  | ||||
| gulp.task("build-translation-flatten-supervisor", () => | ||||
|   gulp | ||||
|     .src(workDir + "/supervisor/*.json") | ||||
|     .pipe( | ||||
|       transform((data) => | ||||
|         // Polymer.AppLocalizeBehavior requires flattened json | ||||
|         flatten(data) | ||||
|       ) | ||||
|     ) | ||||
|     .pipe(gulp.dest(outDir)) | ||||
| ); | ||||
|  | ||||
| gulp.task("build-translation-write-metadata", () => | ||||
|   gulp | ||||
|     .src([ | ||||
|       path.join(paths.translations_src, "translationMetadata.json"), | ||||
|       ...(env.isProdBuild() ? [] : [workDir + "/testMetadata.json"]), | ||||
|       workDir + "/translationFingerprints.json", | ||||
|     ]) | ||||
|     .pipe(merge({})) | ||||
|     .pipe( | ||||
|       transform((data) => { | ||||
|         const newData = {}; | ||||
|         Object.entries(data).forEach(([key, value]) => { | ||||
|           // Filter out translations without native name. | ||||
|           if (value.nativeName) { | ||||
|             newData[key] = value; | ||||
|           } else { | ||||
|             console.warn( | ||||
|               `Skipping language ${key}. Native name was not translated.` | ||||
|             ); | ||||
|           } | ||||
|         }); | ||||
|         return newData; | ||||
|       }) | ||||
|     ) | ||||
|     .pipe( | ||||
|       transform((data) => ({ | ||||
|         fragments: TRANSLATION_FRAGMENTS, | ||||
|         translations: data, | ||||
|       })) | ||||
|     ) | ||||
|     .pipe(rename("translationMetadata.json")) | ||||
|     .pipe(gulp.dest(workDir)) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "create-translations", | ||||
|   gulp.series( | ||||
|     gulp.parallel("create-test-metadata", "create-test-translation"), | ||||
|     "build-master-translation", | ||||
|     "build-merged-translations", | ||||
|     gulp.parallel(...splitTasks), | ||||
|     "build-flattened-translations" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "build-translations", | ||||
|   gulp.series( | ||||
|     gulp.parallel( | ||||
|       "fetch-nightly-translations", | ||||
|       gulp.series("clean-translations", makeWorkDir) | ||||
|       gulp.series("clean-translations", "ensure-translations-build-dir") | ||||
|     ), | ||||
|     createTestTranslation, | ||||
|     createMasterTranslation, | ||||
|     createTranslations, | ||||
|     writeTranslationMetaData | ||||
|     "create-translations", | ||||
|     "build-translation-fingerprints", | ||||
|     "build-translation-write-metadata" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task( | ||||
|   "build-supervisor-translations", | ||||
|   gulp.series(toggleSupervisorFragment, "build-translations") | ||||
|   gulp.series( | ||||
|     gulp.parallel( | ||||
|       "fetch-nightly-translations", | ||||
|       gulp.series("clean-translations", "ensure-translations-build-dir") | ||||
|     ), | ||||
|     gulp.parallel("create-test-metadata", "create-test-translation"), | ||||
|     "build-master-translation", | ||||
|     "build-merged-translations", | ||||
|     "build-translation-fragment-supervisor", | ||||
|     "build-translation-flatten-supervisor", | ||||
|     "build-translation-fingerprints", | ||||
|     "build-translation-write-metadata" | ||||
|   ) | ||||
| ); | ||||
|   | ||||
| @@ -99,7 +99,7 @@ gulp.task("webpack-watch-app", () => { | ||||
|   ).watch({ poll: isWsl }, doneHandler()); | ||||
|   gulp.watch( | ||||
|     path.join(paths.translations_src, "en.json"), | ||||
|     gulp.series("build-translations", "copy-translations-app") | ||||
|     gulp.series("create-translations", "copy-translations-app") | ||||
|   ); | ||||
| }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										16
									
								
								build-scripts/util.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								build-scripts/util.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| const path = require("path"); | ||||
| const fs = require("fs"); | ||||
|  | ||||
| // Helper function to map recursively over files in a folder and it's subfolders | ||||
| module.exports.mapFiles = function mapFiles(startPath, filter, mapFunc) { | ||||
|   const files = fs.readdirSync(startPath); | ||||
|   for (let i = 0; i < files.length; i++) { | ||||
|     const filename = path.join(startPath, files[i]); | ||||
|     const stat = fs.lstatSync(filename); | ||||
|     if (stat.isDirectory()) { | ||||
|       mapFiles(filename, filter, mapFunc); | ||||
|     } else if (filename.indexOf(filter) >= 0) { | ||||
|       mapFunc(filename); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| @@ -10,7 +10,6 @@ const WebpackBar = require("webpackbar"); | ||||
| const { | ||||
|   TransformAsyncModulesPlugin, | ||||
| } = require("transform-async-modules-webpack-plugin"); | ||||
| const { dependencies } = require("../package.json"); | ||||
| const paths = require("./paths.cjs"); | ||||
| const bundle = require("./bundle.cjs"); | ||||
|  | ||||
| @@ -157,15 +156,11 @@ const createWebpackConfig = ({ | ||||
|           transform: (stats) => JSON.stringify(filterStats(stats)), | ||||
|         }), | ||||
|       !latestBuild && | ||||
|         new TransformAsyncModulesPlugin({ | ||||
|           browserslistEnv: "legacy", | ||||
|           runtime: { version: dependencies["@babel/runtime"] }, | ||||
|         }), | ||||
|         new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }), | ||||
|     ].filter(Boolean), | ||||
|     resolve: { | ||||
|       extensions: [".ts", ".js", ".json"], | ||||
|       alias: { | ||||
|         "lit/static-html$": "lit/static-html.js", | ||||
|         "lit/decorators$": "lit/decorators.js", | ||||
|         "lit/directive$": "lit/directive.js", | ||||
|         "lit/directives/until$": "lit/directives/until.js", | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js"; | ||||
| import { mdiCast, mdiCastConnected } from "@mdi/js"; | ||||
| import "@polymer/paper-item/paper-icon-item"; | ||||
| import "@polymer/paper-listbox/paper-listbox"; | ||||
| import { Auth, Connection } from "home-assistant-js-websocket"; | ||||
| @@ -104,11 +104,8 @@ class HcCast extends LitElement { | ||||
|                                 slot="item-icon" | ||||
|                               ></ha-icon> | ||||
|                             ` | ||||
|                           : html`<ha-svg-icon | ||||
|                               slot="item-icon" | ||||
|                               .path=${mdiViewDashboard} | ||||
|                             ></ha-svg-icon>`} | ||||
|                         ${view.title || view.path || "Unnamed view"} | ||||
|                           : ""} | ||||
|                         ${view.title || view.path} | ||||
|                       </paper-icon-item> | ||||
|                     ` | ||||
|                   )} | ||||
| @@ -253,8 +250,7 @@ class HcCast extends LitElement { | ||||
|         padding-top: 0; | ||||
|       } | ||||
|  | ||||
|       paper-listbox ha-icon, | ||||
|       paper-listbox ha-svg-icon { | ||||
|       paper-listbox ha-icon { | ||||
|         padding: 12px; | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||
| import { LovelaceConfig } from "../../../../src/data/lovelace/config/types"; | ||||
| import { getPanelTitleFromUrlPath } from "../../../../src/data/panel"; | ||||
| import { Lovelace } from "../../../../src/panels/lovelace/types"; | ||||
| import "../../../../src/panels/lovelace/views/hui-view"; | ||||
| import { HomeAssistant } from "../../../../src/types"; | ||||
| @@ -62,12 +61,7 @@ class HcLovelace extends LitElement { | ||||
|       const index = this._viewIndex; | ||||
|  | ||||
|       if (index !== undefined) { | ||||
|         const title = getPanelTitleFromUrlPath( | ||||
|           this.hass, | ||||
|           this.urlPath || "lovelace" | ||||
|         ); | ||||
|  | ||||
|         const dashboardTitle = title || this.urlPath; | ||||
|         const dashboardTitle = this.lovelaceConfig.title || this.urlPath; | ||||
|  | ||||
|         const viewTitle = | ||||
|           this.lovelaceConfig.views[index].title || | ||||
| @@ -86,17 +80,10 @@ class HcLovelace extends LitElement { | ||||
|           this.lovelaceConfig.views[index].background || | ||||
|           this.lovelaceConfig.background; | ||||
|  | ||||
|         const backgroundStyle = | ||||
|           typeof configBackground === "string" | ||||
|             ? configBackground | ||||
|             : configBackground?.image | ||||
|               ? `center / cover no-repeat url('${configBackground.image}')` | ||||
|               : undefined; | ||||
|  | ||||
|         if (backgroundStyle) { | ||||
|         if (configBackground) { | ||||
|           this._huiView!.style.setProperty( | ||||
|             "--lovelace-background", | ||||
|             backgroundStyle | ||||
|             configBackground | ||||
|           ); | ||||
|         } else { | ||||
|           this._huiView!.style.removeProperty("--lovelace-background"); | ||||
|   | ||||
| @@ -35,7 +35,6 @@ import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/lo | ||||
| import { HassElement } from "../../../../src/state/hass-element"; | ||||
| import { castContext } from "../cast_context"; | ||||
| import "./hc-launch-screen"; | ||||
| import { getPanelTitleFromUrlPath } from "../../../../src/data/panel"; | ||||
|  | ||||
| const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = { | ||||
|   strategy: { | ||||
| @@ -360,11 +359,7 @@ export class HcMain extends HassElement { | ||||
|   } | ||||
|  | ||||
|   private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) { | ||||
|     const title = getPanelTitleFromUrlPath( | ||||
|       this.hass!, | ||||
|       this._urlPath || "lovelace" | ||||
|     ); | ||||
|     castContext.setApplicationState(title || ""); | ||||
|     castContext.setApplicationState(lovelaceConfig.title || ""); | ||||
|     this._lovelaceConfig = lovelaceConfig; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,6 @@ import { | ||||
| import { HomeAssistantAppEl } from "../../src/layouts/home-assistant"; | ||||
| import { HomeAssistant } from "../../src/types"; | ||||
| import { selectedDemoConfig } from "./configs/demo-configs"; | ||||
| import { mockAreaRegistry } from "./stubs/area_registry"; | ||||
| import { mockAuth } from "./stubs/auth"; | ||||
| import { mockConfigEntries } from "./stubs/config_entries"; | ||||
| import { mockEnergy } from "./stubs/energy"; | ||||
| @@ -24,10 +23,10 @@ import { mockLovelace } from "./stubs/lovelace"; | ||||
| import { mockMediaPlayer } from "./stubs/media_player"; | ||||
| import { mockPersistentNotification } from "./stubs/persistent_notification"; | ||||
| import { mockRecorder } from "./stubs/recorder"; | ||||
| import { mockTodo } from "./stubs/todo"; | ||||
| import { mockSensor } from "./stubs/sensor"; | ||||
| import { mockSystemLog } from "./stubs/system_log"; | ||||
| import { mockTemplate } from "./stubs/template"; | ||||
| import { mockTodo } from "./stubs/todo"; | ||||
| import { mockTranslations } from "./stubs/translations"; | ||||
|  | ||||
| @customElement("ha-demo") | ||||
| @@ -63,7 +62,6 @@ export class HaDemo extends HomeAssistantAppEl { | ||||
|     mockEnergy(hass); | ||||
|     mockPersistentNotification(hass); | ||||
|     mockConfigEntries(hass); | ||||
|     mockAreaRegistry(hass); | ||||
|     mockEntityRegistry(hass, [ | ||||
|       { | ||||
|         config_entry_id: "co2signal", | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { format, startOfToday, startOfTomorrow } from "date-fns"; | ||||
| import { format, startOfToday, startOfTomorrow } from "date-fns/esm"; | ||||
| import { | ||||
|   EnergyInfo, | ||||
|   EnergyPreferences, | ||||
|   | ||||
| @@ -19,15 +19,15 @@ export const mockLovelace = ( | ||||
|   hass.mockWS("lovelace/resources", () => Promise.resolve([])); | ||||
| }; | ||||
|  | ||||
| customElements.whenDefined("hui-card").then(() => { | ||||
| customElements.whenDefined("hui-view").then(() => { | ||||
|   // eslint-disable-next-line | ||||
|   const HUIView = customElements.get("hui-card"); | ||||
|   const HUIView = customElements.get("hui-view"); | ||||
|   // Patch HUI-VIEW to make the lovelace object available to the demo card | ||||
|   const oldCreateCard = HUIView!.prototype.createElement; | ||||
|   const oldCreateCard = HUIView!.prototype.createCardElement; | ||||
|  | ||||
|   HUIView!.prototype.createElement = function (config) { | ||||
|   HUIView!.prototype.createCardElement = function (config) { | ||||
|     const el = oldCreateCard.call(this, config); | ||||
|     if (config.type === "custom:ha-demo-card") { | ||||
|     if (el.tagName === "HA-DEMO-CARD") { | ||||
|       (el as HADemoCard).lovelace = this.lovelace; | ||||
|     } | ||||
|     return el; | ||||
|   | ||||
| @@ -64,12 +64,6 @@ const ACTIONS = [ | ||||
|       entity_id: "input_boolean.toggle_4", | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     sequence: [ | ||||
|       { scene: "scene.kitchen_morning" }, | ||||
|       { service: "light.turn_off", target: { entity_id: "light.kitchen" } }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     parallel: [ | ||||
|       { scene: "scene.kitchen_morning" }, | ||||
| @@ -142,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement { | ||||
|         <div class="action"> | ||||
|           <span> | ||||
|             ${this._action | ||||
|               ? describeAction(this.hass, [], [], [], this._action) | ||||
|               ? describeAction(this.hass, [], this._action) | ||||
|               : "<invalid YAML>"} | ||||
|           </span> | ||||
|           <ha-yaml-editor | ||||
| @@ -155,7 +149,7 @@ export class DemoAutomationDescribeAction extends LitElement { | ||||
|         ${ACTIONS.map( | ||||
|           (conf) => html` | ||||
|             <div class="action"> | ||||
|               <span>${describeAction(this.hass, [], [], [], conf as any)}</span> | ||||
|               <span>${describeAction(this.hass, [], conf as any)}</span> | ||||
|               <pre>${dump(conf)}</pre> | ||||
|             </div> | ||||
|           ` | ||||
|   | ||||
| @@ -20,7 +20,6 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation | ||||
| import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; | ||||
| import { Action } from "../../../../src/data/script"; | ||||
| import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition"; | ||||
| import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence"; | ||||
| import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel"; | ||||
| import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if"; | ||||
| import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop"; | ||||
| @@ -40,7 +39,6 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [ | ||||
|   { name: "If-Then", actions: [HaIfAction.defaultConfig] }, | ||||
|   { name: "Choose", actions: [HaChooseAction.defaultConfig] }, | ||||
|   { name: "Variables", actions: [{ variables: { hello: "1" } }] }, | ||||
|   { name: "Sequence", actions: [HaSequenceAction.defaultConfig] }, | ||||
|   { name: "Parallel", actions: [HaParallelAction.defaultConfig] }, | ||||
|   { name: "Stop", actions: [HaStopAction.defaultConfig] }, | ||||
| ]; | ||||
|   | ||||
| @@ -161,14 +161,12 @@ const LABELS: LabelRegistryEntry[] = [ | ||||
|     name: "Energy", | ||||
|     icon: null, | ||||
|     color: "yellow", | ||||
|     description: null, | ||||
|   }, | ||||
|   { | ||||
|     label_id: "entertainment", | ||||
|     name: "Entertainment", | ||||
|     icon: "mdi:popcorn", | ||||
|     color: "blue", | ||||
|     description: null, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
|   | ||||
| @@ -56,7 +56,9 @@ export class DemoDateTimeDateTimeNumeric extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|   | ||||
| @@ -56,7 +56,9 @@ export class DemoDateTimeDateTimeSeconds extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|   | ||||
| @@ -56,7 +56,9 @@ export class DemoDateTimeDateTimeShortYear extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|   | ||||
| @@ -56,7 +56,9 @@ export class DemoDateTimeDateTimeShort extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|   | ||||
| @@ -56,7 +56,9 @@ export class DemoDateTimeDateTime extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|   | ||||
| @@ -35,7 +35,9 @@ export class DemoDateTimeDate extends LitElement { | ||||
|           <div class="center">Month-Day-Year</div> | ||||
|           <div class="center">Year-Month-Day</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|   | ||||
| @@ -56,7 +56,9 @@ export class DemoDateTimeTimeSeconds extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|   | ||||
| @@ -56,7 +56,9 @@ export class DemoDateTimeTimeWeekday extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|   | ||||
| @@ -56,7 +56,9 @@ export class DemoDateTimeTime extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit"; | ||||
| import { customElement, query } from "lit/decorators"; | ||||
| import { CoverEntityFeature } from "../../../../src/data/cover"; | ||||
| import { LightColorMode } from "../../../../src/data/light"; | ||||
| import { LockEntityFeature } from "../../../../src/data/lock"; | ||||
| import { VacuumEntityFeature } from "../../../../src/data/vacuum"; | ||||
| import { getEntity } from "../../../../src/fake_data/entity"; | ||||
| import { provideHass } from "../../../../src/fake_data/provide_hass"; | ||||
| @@ -21,11 +20,6 @@ const ENTITIES = [ | ||||
|   getEntity("light", "unavailable", "unavailable", { | ||||
|     friendly_name: "Unavailable entity", | ||||
|   }), | ||||
|   getEntity("lock", "front_door", "locked", { | ||||
|     friendly_name: "Front Door Lock", | ||||
|     device_class: "lock", | ||||
|     supported_features: LockEntityFeature.OPEN, | ||||
|   }), | ||||
|   getEntity("climate", "thermostat", "heat", { | ||||
|     current_temperature: 73, | ||||
|     min_temp: 45, | ||||
| @@ -144,24 +138,6 @@ const CONFIGS = [ | ||||
|     - type: "color-temp" | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Lock commands feature", | ||||
|     config: ` | ||||
| - type: tile | ||||
|   entity: lock.front_door | ||||
|   features: | ||||
|     - type: "lock-commands" | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Lock open door feature", | ||||
|     config: ` | ||||
| - type: tile | ||||
|   entity: lock.front_door | ||||
|   features: | ||||
|     - type: "lock-open-door" | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Vacuum commands feature", | ||||
|     config: ` | ||||
|   | ||||
| @@ -368,7 +368,6 @@ export class DemoEntityState extends LitElement { | ||||
|               hass.localize, | ||||
|               entry.stateObj, | ||||
|               hass.locale, | ||||
|               [], // numericDeviceClasses | ||||
|               hass.config, | ||||
|               hass.entities | ||||
|             )}`, | ||||
|   | ||||
| @@ -36,8 +36,6 @@ const createConfigEntry = ( | ||||
|   pref_disable_new_entities: false, | ||||
|   pref_disable_polling: false, | ||||
|   reason: null, | ||||
|   error_reason_translation_key: null, | ||||
|   error_reason_translation_placeholders: null, | ||||
|   ...override, | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,4 @@ | ||||
| import { globIterate } from "glob"; | ||||
| import { availableParallelism } from "node:os"; | ||||
|  | ||||
| process.env.UV_THREADPOOL_SIZE = availableParallelism(); | ||||
|  | ||||
| const gulpImports = []; | ||||
|  | ||||
|   | ||||
| @@ -1,19 +1,19 @@ | ||||
| import { mdiRefresh, mdiStorePlus } from "@mdi/js"; | ||||
| import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; | ||||
| import { mdiStorePlus, mdiUpdate } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { atLeastVersion } from "../../../src/common/config/version"; | ||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||
| import "../../../src/components/ha-fab"; | ||||
| import { reloadHassioAddons } from "../../../src/data/hassio/addon"; | ||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; | ||||
| import "../../../src/layouts/hass-subpage"; | ||||
| import "../../../src/layouts/hass-tabs-subpage"; | ||||
| import { haStyle } from "../../../src/resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../src/types"; | ||||
| import { supervisorTabs } from "../hassio-tabs"; | ||||
| import "./hassio-addons"; | ||||
| import "../../../src/layouts/hass-subpage"; | ||||
| import { reloadHassioAddons } from "../../../src/data/hassio/addon"; | ||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; | ||||
| import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; | ||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||
|  | ||||
| @customElement("hassio-dashboard") | ||||
| class HassioDashboard extends LitElement { | ||||
| @@ -43,7 +43,7 @@ class HassioDashboard extends LitElement { | ||||
|         <ha-icon-button | ||||
|           slot="toolbar-icon" | ||||
|           @click=${this._handleCheckUpdates} | ||||
|           .path=${mdiRefresh} | ||||
|           .path=${mdiUpdate} | ||||
|           .label=${this.supervisor.localize("store.check_updates")} | ||||
|         ></ha-icon-button> | ||||
|         <hassio-addons | ||||
|   | ||||
							
								
								
									
										151
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										151
									
								
								package.json
									
									
									
									
									
								
							| @@ -25,24 +25,24 @@ | ||||
|   "license": "Apache-2.0", | ||||
|   "type": "module", | ||||
|   "dependencies": { | ||||
|     "@babel/runtime": "7.24.7", | ||||
|     "@braintree/sanitize-url": "7.0.2", | ||||
|     "@codemirror/autocomplete": "6.16.2", | ||||
|     "@codemirror/commands": "6.6.0", | ||||
|     "@codemirror/language": "6.10.2", | ||||
|     "@codemirror/legacy-modes": "6.4.0", | ||||
|     "@babel/runtime": "7.24.1", | ||||
|     "@braintree/sanitize-url": "7.0.1", | ||||
|     "@codemirror/autocomplete": "6.15.0", | ||||
|     "@codemirror/commands": "6.3.3", | ||||
|     "@codemirror/language": "6.10.1", | ||||
|     "@codemirror/legacy-modes": "6.3.3", | ||||
|     "@codemirror/search": "6.5.6", | ||||
|     "@codemirror/state": "6.4.1", | ||||
|     "@codemirror/view": "6.27.0", | ||||
|     "@codemirror/view": "6.26.1", | ||||
|     "@egjs/hammerjs": "2.0.17", | ||||
|     "@formatjs/intl-datetimeformat": "6.12.5", | ||||
|     "@formatjs/intl-displaynames": "6.6.8", | ||||
|     "@formatjs/intl-datetimeformat": "6.12.3", | ||||
|     "@formatjs/intl-displaynames": "6.6.6", | ||||
|     "@formatjs/intl-getcanonicallocales": "2.3.0", | ||||
|     "@formatjs/intl-listformat": "7.5.7", | ||||
|     "@formatjs/intl-locale": "4.0.0", | ||||
|     "@formatjs/intl-numberformat": "8.10.3", | ||||
|     "@formatjs/intl-pluralrules": "5.2.14", | ||||
|     "@formatjs/intl-relativetimeformat": "11.2.14", | ||||
|     "@formatjs/intl-listformat": "7.5.5", | ||||
|     "@formatjs/intl-locale": "3.4.5", | ||||
|     "@formatjs/intl-numberformat": "8.10.1", | ||||
|     "@formatjs/intl-pluralrules": "5.2.12", | ||||
|     "@formatjs/intl-relativetimeformat": "11.2.12", | ||||
|     "@fullcalendar/core": "6.1.11", | ||||
|     "@fullcalendar/daygrid": "6.1.11", | ||||
|     "@fullcalendar/interaction": "6.1.11", | ||||
| @@ -53,7 +53,7 @@ | ||||
|     "@lit-labs/context": "0.4.1", | ||||
|     "@lit-labs/motion": "1.0.7", | ||||
|     "@lit-labs/observers": "2.0.2", | ||||
|     "@lit-labs/virtualizer": "2.0.13", | ||||
|     "@lit-labs/virtualizer": "2.0.12", | ||||
|     "@lrnwebcomponents/simple-tooltip": "8.0.2", | ||||
|     "@material/chips": "=14.0.0-canary.53b3cad2f.0", | ||||
|     "@material/data-table": "=14.0.0-canary.53b3cad2f.0", | ||||
| @@ -70,6 +70,7 @@ | ||||
|     "@material/mwc-list": "0.27.0", | ||||
|     "@material/mwc-menu": "0.27.0", | ||||
|     "@material/mwc-radio": "0.27.0", | ||||
|     "@material/mwc-ripple": "0.27.0", | ||||
|     "@material/mwc-select": "0.27.0", | ||||
|     "@material/mwc-snackbar": "0.27.0", | ||||
|     "@material/mwc-switch": "0.27.0", | ||||
| @@ -80,7 +81,7 @@ | ||||
|     "@material/mwc-top-app-bar": "0.27.0", | ||||
|     "@material/mwc-top-app-bar-fixed": "0.27.0", | ||||
|     "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", | ||||
|     "@material/web": "1.5.0", | ||||
|     "@material/web": "=1.3.0", | ||||
|     "@mdi/js": "7.4.47", | ||||
|     "@mdi/svg": "7.4.47", | ||||
|     "@polymer/paper-item": "3.0.1", | ||||
| @@ -88,8 +89,8 @@ | ||||
|     "@polymer/paper-tabs": "3.1.0", | ||||
|     "@polymer/polymer": "3.5.1", | ||||
|     "@thomasloven/round-slider": "0.6.0", | ||||
|     "@vaadin/combo-box": "24.3.13", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.3.13", | ||||
|     "@vaadin/combo-box": "24.3.10", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.3.10", | ||||
|     "@vibrant/color": "3.2.1-alpha.1", | ||||
|     "@vibrant/core": "3.2.1-alpha.1", | ||||
|     "@vibrant/quantizer-mmcq": "3.2.1-alpha.1", | ||||
| @@ -97,28 +98,28 @@ | ||||
|     "@webcomponents/scoped-custom-element-registry": "0.0.9", | ||||
|     "@webcomponents/webcomponentsjs": "2.8.0", | ||||
|     "app-datepicker": "5.1.1", | ||||
|     "chart.js": "4.4.3", | ||||
|     "chart.js": "4.4.2", | ||||
|     "color-name": "2.0.0", | ||||
|     "comlink": "4.4.1", | ||||
|     "core-js": "3.37.1", | ||||
|     "cropperjs": "1.6.2", | ||||
|     "date-fns": "3.6.0", | ||||
|     "date-fns-tz": "3.1.3", | ||||
|     "core-js": "3.36.1", | ||||
|     "cropperjs": "1.6.1", | ||||
|     "date-fns": "2.30.0", | ||||
|     "date-fns-tz": "2.0.1", | ||||
|     "deep-clone-simple": "1.1.1", | ||||
|     "deep-freeze": "0.0.1", | ||||
|     "element-internals-polyfill": "1.3.11", | ||||
|     "element-internals-polyfill": "1.3.10", | ||||
|     "fuse.js": "7.0.0", | ||||
|     "google-timezones-json": "1.2.0", | ||||
|     "hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch", | ||||
|     "home-assistant-js-websocket": "9.3.0", | ||||
|     "home-assistant-js-websocket": "9.2.1", | ||||
|     "idb-keyval": "6.2.1", | ||||
|     "intl-messageformat": "10.5.14", | ||||
|     "intl-messageformat": "10.5.11", | ||||
|     "js-yaml": "4.1.0", | ||||
|     "leaflet": "1.9.4", | ||||
|     "leaflet-draw": "1.0.4", | ||||
|     "lit": "2.8.0", | ||||
|     "luxon": "3.4.4", | ||||
|     "marked": "12.0.2", | ||||
|     "marked": "12.0.1", | ||||
|     "memoize-one": "6.0.0", | ||||
|     "node-vibrant": "3.2.1-alpha.1", | ||||
|     "proxy-polyfill": "0.3.2", | ||||
| @@ -133,118 +134,121 @@ | ||||
|     "tinykeys": "2.1.0", | ||||
|     "tsparticles-engine": "2.12.0", | ||||
|     "tsparticles-preset-links": "2.12.0", | ||||
|     "ua-parser-js": "1.0.38", | ||||
|     "ua-parser-js": "1.0.37", | ||||
|     "unfetch": "5.0.0", | ||||
|     "vis-data": "7.1.9", | ||||
|     "vis-network": "9.1.9", | ||||
|     "vue": "2.7.16", | ||||
|     "vue2-daterange-picker": "0.6.8", | ||||
|     "weekstart": "2.0.0", | ||||
|     "workbox-cacheable-response": "7.1.0", | ||||
|     "workbox-core": "7.1.0", | ||||
|     "workbox-expiration": "7.1.0", | ||||
|     "workbox-precaching": "7.1.0", | ||||
|     "workbox-routing": "7.1.0", | ||||
|     "workbox-strategies": "7.1.0", | ||||
|     "workbox-cacheable-response": "7.0.0", | ||||
|     "workbox-core": "7.0.0", | ||||
|     "workbox-expiration": "7.0.0", | ||||
|     "workbox-precaching": "7.0.0", | ||||
|     "workbox-routing": "7.0.0", | ||||
|     "workbox-strategies": "7.0.0", | ||||
|     "xss": "1.0.15" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "7.24.7", | ||||
|     "@babel/helper-define-polyfill-provider": "0.6.2", | ||||
|     "@babel/plugin-proposal-decorators": "7.24.7", | ||||
|     "@babel/plugin-transform-runtime": "7.24.7", | ||||
|     "@babel/preset-env": "7.24.7", | ||||
|     "@babel/preset-typescript": "7.24.7", | ||||
|     "@bundle-stats/plugin-webpack-filter": "4.13.2", | ||||
|     "@babel/core": "7.24.3", | ||||
|     "@babel/helper-define-polyfill-provider": "0.6.1", | ||||
|     "@babel/plugin-proposal-decorators": "7.24.1", | ||||
|     "@babel/plugin-transform-runtime": "7.24.3", | ||||
|     "@babel/preset-env": "7.24.3", | ||||
|     "@babel/preset-typescript": "7.24.1", | ||||
|     "@bundle-stats/plugin-webpack-filter": "4.12.2", | ||||
|     "@koa/cors": "5.0.0", | ||||
|     "@lokalise/node-api": "12.5.0", | ||||
|     "@octokit/auth-oauth-device": "7.1.1", | ||||
|     "@octokit/plugin-retry": "7.1.1", | ||||
|     "@octokit/rest": "20.1.1", | ||||
|     "@lokalise/node-api": "12.3.0", | ||||
|     "@octokit/auth-oauth-device": "7.0.1", | ||||
|     "@octokit/plugin-retry": "7.0.3", | ||||
|     "@octokit/rest": "20.0.2", | ||||
|     "@open-wc/dev-server-hmr": "0.1.4", | ||||
|     "@rollup/plugin-babel": "6.0.4", | ||||
|     "@rollup/plugin-commonjs": "26.0.1", | ||||
|     "@rollup/plugin-commonjs": "25.0.7", | ||||
|     "@rollup/plugin-json": "6.1.0", | ||||
|     "@rollup/plugin-node-resolve": "15.2.3", | ||||
|     "@rollup/plugin-replace": "5.0.7", | ||||
|     "@rollup/plugin-replace": "5.0.5", | ||||
|     "@types/babel__plugin-transform-runtime": "7.9.5", | ||||
|     "@types/chromecast-caf-receiver": "6.0.14", | ||||
|     "@types/chromecast-caf-sender": "1.0.10", | ||||
|     "@types/color-name": "1.1.4", | ||||
|     "@types/chromecast-caf-receiver": "6.0.13", | ||||
|     "@types/chromecast-caf-sender": "1.0.9", | ||||
|     "@types/color-name": "1.1.3", | ||||
|     "@types/glob": "8.1.0", | ||||
|     "@types/html-minifier-terser": "7.0.2", | ||||
|     "@types/js-yaml": "4.0.9", | ||||
|     "@types/leaflet": "1.9.12", | ||||
|     "@types/leaflet": "1.9.8", | ||||
|     "@types/leaflet-draw": "1.0.11", | ||||
|     "@types/lodash.merge": "4.6.9", | ||||
|     "@types/luxon": "3.4.2", | ||||
|     "@types/mocha": "10.0.6", | ||||
|     "@types/qrcode": "1.5.5", | ||||
|     "@types/serve-handler": "6.1.4", | ||||
|     "@types/sortablejs": "1.15.8", | ||||
|     "@types/tar": "6.1.13", | ||||
|     "@types/tar": "6.1.11", | ||||
|     "@types/ua-parser-js": "0.7.39", | ||||
|     "@types/webspeechapi": "0.0.29", | ||||
|     "@typescript-eslint/eslint-plugin": "7.12.0", | ||||
|     "@typescript-eslint/parser": "7.12.0", | ||||
|     "@typescript-eslint/eslint-plugin": "7.4.0", | ||||
|     "@typescript-eslint/parser": "7.4.0", | ||||
|     "@web/dev-server": "0.1.38", | ||||
|     "@web/dev-server-rollup": "0.4.1", | ||||
|     "babel-loader": "9.1.3", | ||||
|     "babel-plugin-template-html-minifier": "4.1.0", | ||||
|     "chai": "5.1.1", | ||||
|     "chai": "5.1.0", | ||||
|     "del": "7.1.0", | ||||
|     "eslint": "8.57.0", | ||||
|     "eslint-config-airbnb-base": "15.0.0", | ||||
|     "eslint-config-airbnb-typescript": "18.0.0", | ||||
|     "eslint-config-prettier": "9.1.0", | ||||
|     "eslint-import-resolver-webpack": "0.13.8", | ||||
|     "eslint-plugin-disable": "2.0.3", | ||||
|     "eslint-plugin-import": "2.29.1", | ||||
|     "eslint-plugin-lit": "1.14.0", | ||||
|     "eslint-plugin-lit": "1.11.0", | ||||
|     "eslint-plugin-lit-a11y": "4.1.2", | ||||
|     "eslint-plugin-unused-imports": "4.0.0", | ||||
|     "eslint-plugin-wc": "2.1.0", | ||||
|     "eslint-plugin-unused-imports": "3.1.0", | ||||
|     "eslint-plugin-wc": "2.0.4", | ||||
|     "fancy-log": "2.0.0", | ||||
|     "fs-extra": "11.2.0", | ||||
|     "glob": "10.4.1", | ||||
|     "gulp": "5.0.0", | ||||
|     "glob": "10.3.10", | ||||
|     "gulp": "4.0.2", | ||||
|     "gulp-flatmap": "1.0.2", | ||||
|     "gulp-json-transform": "0.5.0", | ||||
|     "gulp-merge-json": "2.2.1", | ||||
|     "gulp-rename": "2.0.0", | ||||
|     "gulp-zopfli-green": "6.0.1", | ||||
|     "html-minifier-terser": "7.2.0", | ||||
|     "husky": "9.0.11", | ||||
|     "instant-mocha": "1.5.2", | ||||
|     "jszip": "3.10.1", | ||||
|     "lint-staged": "15.2.5", | ||||
|     "lint-staged": "15.2.2", | ||||
|     "lit-analyzer": "2.0.3", | ||||
|     "lodash.merge": "4.6.2", | ||||
|     "lodash.template": "4.5.0", | ||||
|     "magic-string": "0.30.10", | ||||
|     "magic-string": "0.30.8", | ||||
|     "map-stream": "0.0.7", | ||||
|     "mocha": "10.4.0", | ||||
|     "mocha": "10.3.0", | ||||
|     "object-hash": "3.0.0", | ||||
|     "open": "10.1.0", | ||||
|     "pinst": "3.0.0", | ||||
|     "prettier": "3.3.1", | ||||
|     "prettier": "3.2.5", | ||||
|     "rollup": "2.79.1", | ||||
|     "rollup-plugin-string": "3.0.0", | ||||
|     "rollup-plugin-terser": "7.0.2", | ||||
|     "rollup-plugin-visualizer": "5.12.0", | ||||
|     "serve-handler": "6.1.5", | ||||
|     "sinon": "18.0.0", | ||||
|     "sinon": "17.0.1", | ||||
|     "source-map-url": "0.4.1", | ||||
|     "systemjs": "6.15.1", | ||||
|     "tar": "7.2.0", | ||||
|     "systemjs": "6.14.3", | ||||
|     "tar": "6.2.1", | ||||
|     "terser-webpack-plugin": "5.3.10", | ||||
|     "transform-async-modules-webpack-plugin": "1.1.1", | ||||
|     "transform-async-modules-webpack-plugin": "1.0.4", | ||||
|     "ts-lit-plugin": "2.0.2", | ||||
|     "typescript": "5.4.5", | ||||
|     "typescript": "5.4.3", | ||||
|     "vinyl-buffer": "1.0.1", | ||||
|     "vinyl-source-stream": "2.0.0", | ||||
|     "webpack": "5.91.0", | ||||
|     "webpack-cli": "5.1.4", | ||||
|     "webpack-dev-server": "5.0.4", | ||||
|     "webpack-manifest-plugin": "5.0.0", | ||||
|     "webpack-stats-plugin": "1.1.3", | ||||
|     "webpackbar": "6.0.1", | ||||
|     "workbox-build": "7.1.1" | ||||
|     "workbox-build": "7.0.0" | ||||
|   }, | ||||
|   "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", | ||||
|   "resolutions": { | ||||
| @@ -253,9 +257,8 @@ | ||||
|     "lit": "2.8.0", | ||||
|     "clean-css": "5.3.3", | ||||
|     "@lit/reactive-element": "1.6.3", | ||||
|     "@fullcalendar/daygrid": "6.1.11", | ||||
|     "sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch", | ||||
|     "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch" | ||||
|   }, | ||||
|   "packageManager": "yarn@4.2.2" | ||||
|   "packageManager": "yarn@4.1.1" | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [project] | ||||
| name         = "home-assistant-frontend" | ||||
| version      = "20240610.0" | ||||
| version      = "20240403.1" | ||||
| license      = {text = "Apache-2.0"} | ||||
| description  = "The Home Assistant frontend" | ||||
| readme       = "README.md" | ||||
|   | ||||
| @@ -40,11 +40,6 @@ | ||||
|       "matchPackageNames": ["tsparticles-engine"], | ||||
|       "matchPackagePrefixes": ["tsparticles-preset-"] | ||||
|     }, | ||||
|     { | ||||
|       "description": "Group date-fns with dependent timezone package", | ||||
|       "groupName": "date-fns", | ||||
|       "matchPackageNames": ["date-fns", "date-fns-tz"] | ||||
|     }, | ||||
|     { | ||||
|       "description": "Group and temporarily disable WDS packages", | ||||
|       "groupName": "Web Dev Server", | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { toZonedTime, fromZonedTime } from "date-fns-tz"; | ||||
| import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz"; | ||||
| import { HassConfig } from "home-assistant-js-websocket"; | ||||
| import { FrontendLocaleData, TimeZone } from "../../data/translation"; | ||||
|  | ||||
| @@ -8,10 +8,10 @@ const calcZonedDate = ( | ||||
|   fn: (date: Date, options?: any) => Date | number | boolean, | ||||
|   options? | ||||
| ) => { | ||||
|   const inputZoned = toZonedTime(date, tz); | ||||
|   const inputZoned = utcToZonedTime(date, tz); | ||||
|   const fnZoned = fn(inputZoned, options); | ||||
|   if (fnZoned instanceof Date) { | ||||
|     return fromZonedTime(fnZoned, tz) as Date; | ||||
|     return zonedTimeToUtc(fnZoned, tz) as Date; | ||||
|   } | ||||
|   return fnZoned; | ||||
| }; | ||||
| @@ -51,6 +51,6 @@ export const calcDateDifferenceProperty = ( | ||||
|     locale, | ||||
|     config, | ||||
|     locale.time_zone === TimeZone.server | ||||
|       ? toZonedTime(startDate, config.time_zone) | ||||
|       ? utcToZonedTime(startDate, config.time_zone) | ||||
|       : startDate | ||||
|   ); | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import { getWeekStartByLocale } from "weekstart"; | ||||
| import { FrontendLocaleData, FirstWeekday } from "../../data/translation"; | ||||
|  | ||||
| import "../../resources/intl-polyfill"; | ||||
|  | ||||
| export const weekdays = [ | ||||
|   "sunday", | ||||
|   "monday", | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { HassConfig } from "home-assistant-js-websocket"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { DateFormat, FrontendLocaleData } from "../../data/translation"; | ||||
| import "../../resources/intl-polyfill"; | ||||
| import { resolveTimeZone } from "./resolve-time-zone"; | ||||
|  | ||||
| // Tuesday, August 10 | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { HassConfig } from "home-assistant-js-websocket"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
| import "../../resources/intl-polyfill"; | ||||
| import { formatDateNumeric } from "./format_date"; | ||||
| import { formatTime } from "./format_time"; | ||||
| import { resolveTimeZone } from "./resolve-time-zone"; | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { HaDurationData } from "../../components/ha-duration-input"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
| import "../../resources/intl-polyfill"; | ||||
|  | ||||
| const leftPad = (num: number) => (num < 10 ? `0${num}` : num); | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { HassConfig } from "home-assistant-js-websocket"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
| import "../../resources/intl-polyfill"; | ||||
| import { resolveTimeZone } from "./resolve-time-zone"; | ||||
| import { useAmPm } from "./use_am_pm"; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import memoizeOne from "memoize-one"; | ||||
| import "../../resources/intl-polyfill"; | ||||
|  | ||||
| export const localizeWeekdays = memoizeOne( | ||||
|   (language: string, short: boolean): string[] => { | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
| import "../../resources/intl-polyfill"; | ||||
| import { selectUnit } from "../util/select-unit"; | ||||
|  | ||||
| const formatRelTimeMem = memoizeOne( | ||||
|   | ||||
| @@ -108,8 +108,6 @@ export const storage = | ||||
|     subscribe?: boolean; | ||||
|     state?: boolean; | ||||
|     stateOptions?: InternalPropertyDeclaration; | ||||
|     serializer?: (value: any) => any; | ||||
|     deserializer?: (value: any) => any; | ||||
|   }): any => | ||||
|   (clsElement: ClassElement) => { | ||||
|     const storageName = options.storage || "localStorage"; | ||||
| @@ -143,9 +141,7 @@ export const storage = | ||||
|  | ||||
|     const getValue = (): any => | ||||
|       storageInstance.hasKey(storageKey!) | ||||
|         ? options.deserializer | ||||
|           ? options.deserializer(storageInstance.getValue(storageKey!)) | ||||
|           : storageInstance.getValue(storageKey!) | ||||
|         ? storageInstance.getValue(storageKey!) | ||||
|         : initVal; | ||||
|  | ||||
|     const setValue = (el: ReactiveElement, value: any) => { | ||||
| @@ -153,10 +149,7 @@ export const storage = | ||||
|       if (options.state) { | ||||
|         oldValue = getValue(); | ||||
|       } | ||||
|       storageInstance.setValue( | ||||
|         storageKey!, | ||||
|         options.serializer ? options.serializer(value) : value | ||||
|       ); | ||||
|       storageInstance.setValue(storageKey!, value); | ||||
|       if (options.state) { | ||||
|         el.requestUpdate(clsElement.key, oldValue); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| export type MediaQueriesListener = () => void; | ||||
|  | ||||
| /** | ||||
|  * Attach a media query. Listener is called right away and when it matches. | ||||
|  * @param mediaQuery media query to match. | ||||
| @@ -9,7 +7,7 @@ export type MediaQueriesListener = () => void; | ||||
| export const listenMediaQuery = ( | ||||
|   mediaQuery: string, | ||||
|   matchesChanged: (matches: boolean) => void | ||||
| ): MediaQueriesListener => { | ||||
| ) => { | ||||
|   const mql = matchMedia(mediaQuery); | ||||
|   const listener = (e) => matchesChanged(e.matches); | ||||
|   mql.addListener(listener); | ||||
|   | ||||
| @@ -19,11 +19,28 @@ import { blankBeforeUnit } from "../translations/blank_before_unit"; | ||||
| import { LocalizeFunc } from "../translations/localize"; | ||||
| import { computeDomain } from "./compute_domain"; | ||||
|  | ||||
| export const computeStateDisplaySingleEntity = ( | ||||
|   localize: LocalizeFunc, | ||||
|   stateObj: HassEntity, | ||||
|   locale: FrontendLocaleData, | ||||
|   config: HassConfig, | ||||
|   entity: EntityRegistryDisplayEntry | undefined, | ||||
|   state?: string | ||||
| ): string => | ||||
|   computeStateDisplayFromEntityAttributes( | ||||
|     localize, | ||||
|     locale, | ||||
|     config, | ||||
|     entity, | ||||
|     stateObj.entity_id, | ||||
|     stateObj.attributes, | ||||
|     state !== undefined ? state : stateObj.state | ||||
|   ); | ||||
|  | ||||
| export const computeStateDisplay = ( | ||||
|   localize: LocalizeFunc, | ||||
|   stateObj: HassEntity, | ||||
|   locale: FrontendLocaleData, | ||||
|   sensorNumericDeviceClasses: string[], | ||||
|   config: HassConfig, | ||||
|   entities: HomeAssistant["entities"], | ||||
|   state?: string | ||||
| @@ -35,7 +52,6 @@ export const computeStateDisplay = ( | ||||
|   return computeStateDisplayFromEntityAttributes( | ||||
|     localize, | ||||
|     locale, | ||||
|     sensorNumericDeviceClasses, | ||||
|     config, | ||||
|     entity, | ||||
|     stateObj.entity_id, | ||||
| @@ -47,7 +63,6 @@ export const computeStateDisplay = ( | ||||
| export const computeStateDisplayFromEntityAttributes = ( | ||||
|   localize: LocalizeFunc, | ||||
|   locale: FrontendLocaleData, | ||||
|   sensorNumericDeviceClasses: string[], | ||||
|   config: HassConfig, | ||||
|   entity: EntityRegistryDisplayEntry | undefined, | ||||
|   entityId: string, | ||||
| @@ -58,15 +73,8 @@ export const computeStateDisplayFromEntityAttributes = ( | ||||
|     return localize(`state.default.${state}`); | ||||
|   } | ||||
|  | ||||
|   const domain = computeDomain(entityId); | ||||
|  | ||||
|   // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` | ||||
|   if ( | ||||
|     isNumericFromAttributes( | ||||
|       attributes, | ||||
|       domain === "sensor" ? sensorNumericDeviceClasses : [] | ||||
|     ) | ||||
|   ) { | ||||
|   if (isNumericFromAttributes(attributes)) { | ||||
|     // state is duration | ||||
|     if ( | ||||
|       attributes.device_class === "duration" && | ||||
| @@ -112,6 +120,8 @@ export const computeStateDisplayFromEntityAttributes = ( | ||||
|     return value; | ||||
|   } | ||||
|  | ||||
|   const domain = computeDomain(entityId); | ||||
|  | ||||
|   if (domain === "datetime") { | ||||
|     const time = new Date(state); | ||||
|     return formatDateTime(time, locale, config); | ||||
| @@ -177,14 +187,11 @@ export const computeStateDisplayFromEntityAttributes = ( | ||||
|   if ( | ||||
|     [ | ||||
|       "button", | ||||
|       "conversation", | ||||
|       "event", | ||||
|       "image", | ||||
|       "input_button", | ||||
|       "notify", | ||||
|       "scene", | ||||
|       "stt", | ||||
|       "tag", | ||||
|       "tts", | ||||
|       "wake_word", | ||||
|     ].includes(domain) || | ||||
|   | ||||
| @@ -28,15 +28,7 @@ export const FIXED_DOMAIN_STATES = { | ||||
|   input_button: [], | ||||
|   lawn_mower: ["error", "paused", "mowing", "docked"], | ||||
|   light: ["on", "off"], | ||||
|   lock: [ | ||||
|     "jammed", | ||||
|     "locked", | ||||
|     "locking", | ||||
|     "unlocked", | ||||
|     "unlocking", | ||||
|     "opening", | ||||
|     "open", | ||||
|   ], | ||||
|   lock: ["jammed", "locked", "locking", "unlocked", "unlocking"], | ||||
|   media_player: [ | ||||
|     "off", | ||||
|     "on", | ||||
|   | ||||
| @@ -12,10 +12,11 @@ export const formatLanguageCode = ( | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const formatLanguageCodeMem = memoizeOne( | ||||
|   (locale: FrontendLocaleData) => | ||||
|     new Intl.DisplayNames(locale.language, { | ||||
| const formatLanguageCodeMem = memoizeOne((locale: FrontendLocaleData) => | ||||
|   Intl && "DisplayNames" in Intl | ||||
|     ? new Intl.DisplayNames(locale.language, { | ||||
|         type: "language", | ||||
|         fallback: "code", | ||||
|       }) | ||||
|     : undefined | ||||
| ); | ||||
|   | ||||
| @@ -11,7 +11,6 @@ declare global { | ||||
|  | ||||
| export interface NavigateOptions { | ||||
|   replace?: boolean; | ||||
|   data?: any; | ||||
| } | ||||
|  | ||||
| export const navigate = (path: string, options?: NavigateOptions) => { | ||||
| @@ -25,7 +24,7 @@ export const navigate = (path: string, options?: NavigateOptions) => { | ||||
|   if (__DEMO__) { | ||||
|     if (replace) { | ||||
|       mainWindow.history.replaceState( | ||||
|         mainWindow.history.state?.root ? { root: true } : options?.data ?? null, | ||||
|         mainWindow.history.state?.root ? { root: true } : null, | ||||
|         "", | ||||
|         `${mainWindow.location.pathname}#${path}` | ||||
|       ); | ||||
| @@ -34,12 +33,12 @@ export const navigate = (path: string, options?: NavigateOptions) => { | ||||
|     } | ||||
|   } else if (replace) { | ||||
|     mainWindow.history.replaceState( | ||||
|       mainWindow.history.state?.root ? { root: true } : options?.data ?? null, | ||||
|       mainWindow.history.state?.root ? { root: true } : null, | ||||
|       "", | ||||
|       path | ||||
|     ); | ||||
|   } else { | ||||
|     mainWindow.history.pushState(options?.data ?? null, "", path); | ||||
|     mainWindow.history.pushState(null, "", path); | ||||
|   } | ||||
|   fireEvent(mainWindow, "location-changed", { | ||||
|     replace, | ||||
|   | ||||
| @@ -14,12 +14,8 @@ export const isNumericState = (stateObj: HassEntity): boolean => | ||||
|   isNumericFromAttributes(stateObj.attributes); | ||||
|  | ||||
| export const isNumericFromAttributes = ( | ||||
|   attributes: HassEntityAttributeBase, | ||||
|   numericDeviceClasses?: string[] | ||||
| ): boolean => | ||||
|   !!attributes.unit_of_measurement || | ||||
|   !!attributes.state_class || | ||||
|   (numericDeviceClasses || []).includes(attributes.device_class || ""); | ||||
|   attributes: HassEntityAttributeBase | ||||
| ): boolean => !!attributes.unit_of_measurement || !!attributes.state_class; | ||||
|  | ||||
| export const numberFormatToLocale = ( | ||||
|   localeOptions: FrontendLocaleData | ||||
| @@ -63,18 +59,30 @@ export const formatNumber = ( | ||||
|  | ||||
|   if ( | ||||
|     localeOptions?.number_format !== NumberFormat.none && | ||||
|     !Number.isNaN(Number(num)) | ||||
|     !Number.isNaN(Number(num)) && | ||||
|     Intl | ||||
|   ) { | ||||
|     try { | ||||
|       return new Intl.NumberFormat( | ||||
|         locale, | ||||
|         getDefaultFormatOptions(num, options) | ||||
|       ).format(Number(num)); | ||||
|     } catch (err: any) { | ||||
|       // Don't fail when using "TEST" language | ||||
|       // eslint-disable-next-line no-console | ||||
|       console.error(err); | ||||
|       return new Intl.NumberFormat( | ||||
|         undefined, | ||||
|         getDefaultFormatOptions(num, options) | ||||
|       ).format(Number(num)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if ( | ||||
|     !Number.isNaN(Number(num)) && | ||||
|     num !== "" && | ||||
|     localeOptions?.number_format === NumberFormat.none | ||||
|     localeOptions?.number_format === NumberFormat.none && | ||||
|     Intl | ||||
|   ) { | ||||
|     // If NumberFormat is none, use en-US format without grouping. | ||||
|     return new Intl.NumberFormat( | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import memoizeOne from "memoize-one"; | ||||
| import "../../resources/intl-polyfill"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
|  | ||||
| export const formatListWithAnds = ( | ||||
|   | ||||
| @@ -21,8 +21,7 @@ export const computeFormatFunctions = async ( | ||||
|   localize: LocalizeFunc, | ||||
|   locale: FrontendLocaleData, | ||||
|   config: HassConfig, | ||||
|   entities: HomeAssistant["entities"], | ||||
|   sensorNumericDeviceClasses: string[] | ||||
|   entities: HomeAssistant["entities"] | ||||
| ): Promise<{ | ||||
|   formatEntityState: FormatEntityStateFunc; | ||||
|   formatEntityAttributeValue: FormatEntityAttributeValueFunc; | ||||
| @@ -36,15 +35,7 @@ export const computeFormatFunctions = async ( | ||||
|  | ||||
|   return { | ||||
|     formatEntityState: (stateObj, state) => | ||||
|       computeStateDisplay( | ||||
|         localize, | ||||
|         stateObj, | ||||
|         locale, | ||||
|         sensorNumericDeviceClasses, | ||||
|         config, | ||||
|         entities, | ||||
|         state | ||||
|       ), | ||||
|       computeStateDisplay(localize, stateObj, locale, config, entities, state), | ||||
|     formatEntityAttributeValue: (stateObj, attribute, value) => | ||||
|       computeAttributeValueDisplay( | ||||
|         localize, | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import type { IntlMessageFormat } from "intl-messageformat"; | ||||
| import IntlMessageFormat from "intl-messageformat"; | ||||
| import type { HTMLTemplateResult } from "lit"; | ||||
| import { polyfillLocaleData } from "../../resources/polyfills/locale-data-polyfill"; | ||||
| import { polyfillLocaleData } from "../../resources/locale-data-polyfill"; | ||||
| import { Resources, TranslationDict } from "../../types"; | ||||
| import { fireEvent } from "../dom/fire_event"; | ||||
|  | ||||
| // Exclude some patterns from key type checking for now | ||||
| // These are intended to be removed as errors are fixed | ||||
| @@ -82,15 +81,14 @@ export interface FormatsType { | ||||
|  */ | ||||
|  | ||||
| export const computeLocalize = async <Keys extends string = LocalizeKeys>( | ||||
|   cache: HTMLElement & { | ||||
|     _localizationCache?: Record<string, IntlMessageFormat>; | ||||
|   }, | ||||
|   cache: any, | ||||
|   language: string, | ||||
|   resources: Resources, | ||||
|   formats?: FormatsType | ||||
| ): Promise<LocalizeFunc<Keys>> => { | ||||
|   const { IntlMessageFormat } = await import("intl-messageformat"); | ||||
|   await polyfillLocaleData(language); | ||||
|   await import("../../resources/intl-polyfill").then(() => | ||||
|     polyfillLocaleData(language) | ||||
|   ); | ||||
|  | ||||
|   // Every time any of the parameters change, invalidate the strings cache. | ||||
|   cache._localizationCache = {}; | ||||
| @@ -109,7 +107,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>( | ||||
|     } | ||||
|  | ||||
|     const messageKey = key + translatedValue; | ||||
|     let translatedMessage = cache._localizationCache![messageKey] as | ||||
|     let translatedMessage = cache._localizationCache[messageKey] as | ||||
|       | IntlMessageFormat | ||||
|       | undefined; | ||||
|  | ||||
| @@ -123,7 +121,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>( | ||||
|       } catch (err: any) { | ||||
|         return "Translation error: " + err.message; | ||||
|       } | ||||
|       cache._localizationCache![messageKey] = translatedMessage; | ||||
|       cache._localizationCache[messageKey] = translatedMessage; | ||||
|     } | ||||
|  | ||||
|     let argObject = {}; | ||||
| @@ -139,12 +137,6 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>( | ||||
|     try { | ||||
|       return translatedMessage.format<string>(argObject) as string; | ||||
|     } catch (err: any) { | ||||
|       // eslint-disable-next-line no-console | ||||
|       console.error("Translation error", key, language, err); | ||||
|       fireEvent(cache, "write_log", { | ||||
|         level: "error", | ||||
|         message: `Failed to format translation for key '${key}' in language '${language}'. ${err}`, | ||||
|       }); | ||||
|       return "Translation " + err; | ||||
|     } | ||||
|   }; | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| export const hasRejectedItems = <T = any>(results: PromiseSettledResult<T>[]) => | ||||
|   results.some((result) => result.status === "rejected"); | ||||
|  | ||||
| export const rejectedItems = <T = any>( | ||||
|   results: PromiseSettledResult<T>[] | ||||
| ): PromiseRejectedResult[] => | ||||
|   results.filter( | ||||
|     (result) => result.status === "rejected" | ||||
|   ) as PromiseRejectedResult[]; | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns"; | ||||
| import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns/esm"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
| import { firstWeekdayIndex } from "../datetime/first_weekday"; | ||||
|  | ||||
|   | ||||
| @@ -34,7 +34,7 @@ import { | ||||
|   endOfMonth, | ||||
|   endOfQuarter, | ||||
|   endOfYear, | ||||
| } from "date-fns"; | ||||
| } from "date-fns/esm"; | ||||
| import { | ||||
|   formatDate, | ||||
|   formatDateMonth, | ||||
|   | ||||
| @@ -313,15 +313,11 @@ export class HaChartBase extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _loading = false; | ||||
|  | ||||
|   private async _setupChart() { | ||||
|     if (this._loading) return; | ||||
|     const ctx: CanvasRenderingContext2D = this.renderRoot | ||||
|       .querySelector("canvas")! | ||||
|       .getContext("2d")!; | ||||
|     this._loading = true; | ||||
|     try { | ||||
|  | ||||
|     const ChartConstructor = (await import("../../resources/chartjs")).Chart; | ||||
|  | ||||
|     const computedStyles = getComputedStyle(this); | ||||
| @@ -342,9 +338,6 @@ export class HaChartBase extends LitElement { | ||||
|       options: this._createOptions(), | ||||
|       plugins: this._createPlugins(), | ||||
|     }); | ||||
|     } finally { | ||||
|       this._loading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _createOptions() { | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import "element-internals-polyfill"; | ||||
| import { MdAssistChip } from "@material/web/chips/assist-chip"; | ||||
| import { css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import "element-internals-polyfill"; | ||||
| import { MdChipSet } from "@material/web/chips/chip-set"; | ||||
| import { customElement } from "lit/decorators"; | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import "element-internals-polyfill"; | ||||
| import { MdFilterChip } from "@material/web/chips/filter-chip"; | ||||
| import { css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import "element-internals-polyfill"; | ||||
| import { MdInputChip } from "@material/web/chips/input-chip"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| @@ -19,7 +20,6 @@ export class HaInputChip extends MdInputChip { | ||||
|           0.15 | ||||
|         ); | ||||
|         --ha-input-chip-selected-container-opacity: 1; | ||||
|         --md-input-chip-label-text-font: Roboto, sans-serif; | ||||
|       } | ||||
|       /** Set the size of mdc icons **/ | ||||
|       ::slotted([slot="icon"]) { | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js"; | ||||
| import { mdiArrowDown, mdiArrowUp } from "@mdi/js"; | ||||
| import deepClone from "deep-clone-simple"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   nothing, | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { | ||||
|   customElement, | ||||
| @@ -22,9 +22,7 @@ import { styleMap } from "lit/directives/style-map"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { restoreScroll } from "../../common/decorators/restore-scroll"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { stringCompare } from "../../common/string/compare"; | ||||
| import { debounce } from "../../common/util/debounce"; | ||||
| import { groupBy } from "../../common/util/group-by"; | ||||
| import { nextRender } from "../../common/util/render-status"; | ||||
| import { haStyleScrollbar } from "../../resources/styles"; | ||||
| import { loadVirtualizer } from "../../resources/virtualizer"; | ||||
| @@ -34,6 +32,17 @@ import type { HaCheckbox } from "../ha-checkbox"; | ||||
| import "../ha-svg-icon"; | ||||
| import "../search-input"; | ||||
| import { filterData, sortData } from "./sort-filter"; | ||||
| import { groupBy } from "../../common/util/group-by"; | ||||
| import { stringCompare } from "../../common/string/compare"; | ||||
|  | ||||
| declare global { | ||||
|   // for fire event | ||||
|   interface HASSDomEvents { | ||||
|     "selection-changed": SelectionChangedEvent; | ||||
|     "row-click": RowClickedEvent; | ||||
|     "sorting-changed": SortingChangedEvent; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export interface RowClickedEvent { | ||||
|   id: string; | ||||
| @@ -43,10 +52,6 @@ export interface SelectionChangedEvent { | ||||
|   value: string[]; | ||||
| } | ||||
|  | ||||
| export interface CollapsedChangedEvent { | ||||
|   value: string[]; | ||||
| } | ||||
|  | ||||
| export interface SortingChangedEvent { | ||||
|   column: string; | ||||
|   direction: SortingDirection; | ||||
| @@ -137,14 +142,10 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   @property() public groupColumn?: string; | ||||
|  | ||||
|   @property({ attribute: false }) public groupOrder?: string[]; | ||||
|  | ||||
|   @property() public sortColumn?: string; | ||||
|  | ||||
|   @property() public sortDirection: SortingDirection = null; | ||||
|  | ||||
|   @property({ attribute: false }) public initialCollapsedGroups?: string[]; | ||||
|  | ||||
|   @state() private _filterable = false; | ||||
|  | ||||
|   @state() private _filter = ""; | ||||
| @@ -157,8 +158,6 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   @state() private _items: DataTableRowData[] = []; | ||||
|  | ||||
|   @state() private _collapsedGroups: string[] = []; | ||||
|  | ||||
|   private _checkableRowsCount?: number; | ||||
|  | ||||
|   private _checkedRows: string[] = []; | ||||
| @@ -214,7 +213,6 @@ export class HaDataTable extends LitElement { | ||||
|         (column) => column.filterable | ||||
|       ); | ||||
|  | ||||
|       if (!this.sortColumn) { | ||||
|       for (const columnId in this.columns) { | ||||
|         if (this.columns[columnId].direction) { | ||||
|           this.sortDirection = this.columns[columnId].direction!; | ||||
| @@ -228,7 +226,6 @@ export class HaDataTable extends LitElement { | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|       } | ||||
|  | ||||
|       const clonedColumns: DataTableColumnContainer = deepClone(this.columns); | ||||
|       Object.values(clonedColumns).forEach( | ||||
| @@ -251,23 +248,13 @@ export class HaDataTable extends LitElement { | ||||
|       ).length; | ||||
|     } | ||||
|  | ||||
|     if (!this.hasUpdated && this.initialCollapsedGroups) { | ||||
|       this._collapsedGroups = this.initialCollapsedGroups; | ||||
|       fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); | ||||
|     } else if (properties.has("groupColumn")) { | ||||
|       this._collapsedGroups = []; | ||||
|       fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); | ||||
|     } | ||||
|  | ||||
|     if ( | ||||
|       properties.has("data") || | ||||
|       properties.has("columns") || | ||||
|       properties.has("_filter") || | ||||
|       properties.has("sortColumn") || | ||||
|       properties.has("sortDirection") || | ||||
|       properties.has("groupColumn") || | ||||
|       properties.has("groupOrder") || | ||||
|       properties.has("_collapsedGroups") | ||||
|       properties.has("groupColumn") | ||||
|     ) { | ||||
|       this._sortFilterData(); | ||||
|     } | ||||
| @@ -460,8 +447,6 @@ export class HaDataTable extends LitElement { | ||||
|           } | ||||
|           return html` | ||||
|             <div | ||||
|               @mouseover=${this._setTitle} | ||||
|               @focus=${this._setTitle} | ||||
|               role=${column.main ? "rowheader" : "cell"} | ||||
|               class="mdc-data-table__cell ${classMap({ | ||||
|                 "mdc-data-table__cell--flex": column.type === "flex", | ||||
| @@ -529,7 +514,11 @@ export class HaDataTable extends LitElement { | ||||
|     } | ||||
|  | ||||
|     if (this.appendRow || this.hasFab || this.groupColumn) { | ||||
|       let items = [...data]; | ||||
|       const items = [...data]; | ||||
|  | ||||
|       if (this.appendRow) { | ||||
|         items.push({ append: true, content: this.appendRow }); | ||||
|       } | ||||
|  | ||||
|       if (this.groupColumn) { | ||||
|         const grouped = groupBy(items, (item) => item[this.groupColumn!]); | ||||
| @@ -541,66 +530,45 @@ export class HaDataTable extends LitElement { | ||||
|         const sorted: { | ||||
|           [key: string]: DataTableRowData[]; | ||||
|         } = Object.keys(grouped) | ||||
|           .sort((a, b) => { | ||||
|             const orderA = this.groupOrder?.indexOf(a) ?? -1; | ||||
|             const orderB = this.groupOrder?.indexOf(b) ?? -1; | ||||
|             if (orderA !== orderB) { | ||||
|               if (orderA === -1) { | ||||
|                 return 1; | ||||
|               } | ||||
|               if (orderB === -1) { | ||||
|                 return -1; | ||||
|               } | ||||
|               return orderA - orderB; | ||||
|             } | ||||
|             return stringCompare( | ||||
|           .sort((a, b) => | ||||
|             stringCompare( | ||||
|               ["", "-", "—"].includes(a) ? "zzz" : a, | ||||
|               ["", "-", "—"].includes(b) ? "zzz" : b, | ||||
|               this.hass.locale.language | ||||
|             ); | ||||
|           }) | ||||
|             ) | ||||
|           ) | ||||
|           .reduce((obj, key) => { | ||||
|             obj[key] = grouped[key]; | ||||
|             return obj; | ||||
|           }, {}); | ||||
|         const groupedItems: DataTableRowData[] = []; | ||||
|         Object.entries(sorted).forEach(([groupName, rows]) => { | ||||
|           if ( | ||||
|             groupName !== UNDEFINED_GROUP_KEY || | ||||
|             Object.keys(sorted).length > 1 | ||||
|           ) { | ||||
|             groupedItems.push({ | ||||
|               append: true, | ||||
|               content: html`<div | ||||
|                 class="mdc-data-table__cell group-header" | ||||
|                 role="cell" | ||||
|               .group=${groupName} | ||||
|               @click=${this._collapseGroup} | ||||
|               > | ||||
|               <ha-icon-button | ||||
|                 .path=${mdiChevronUp} | ||||
|                 class=${this._collapsedGroups.includes(groupName) | ||||
|                   ? "collapsed" | ||||
|                   : ""} | ||||
|               > | ||||
|               </ha-icon-button> | ||||
|               ${groupName === UNDEFINED_GROUP_KEY | ||||
|                 ? this.hass.localize("ui.components.data-table.ungrouped") | ||||
|                 : groupName || ""} | ||||
|                 ${groupName === UNDEFINED_GROUP_KEY ? "" : groupName || ""} | ||||
|               </div>`, | ||||
|             }); | ||||
|           if (!this._collapsedGroups.includes(groupName)) { | ||||
|             groupedItems.push(...rows); | ||||
|           } | ||||
|         }); | ||||
|         items = groupedItems; | ||||
|           } | ||||
|  | ||||
|       if (this.appendRow) { | ||||
|         items.push({ append: true, content: this.appendRow }); | ||||
|           groupedItems.push(...rows); | ||||
|         }); | ||||
|  | ||||
|         this._items = groupedItems; | ||||
|       } else { | ||||
|         this._items = items; | ||||
|       } | ||||
|  | ||||
|       if (this.hasFab) { | ||||
|         items.push({ empty: true }); | ||||
|         this._items = [...this._items, { empty: true }]; | ||||
|       } | ||||
|  | ||||
|       this._items = items; | ||||
|     } else { | ||||
|       this._items = data; | ||||
|     } | ||||
| @@ -681,13 +649,6 @@ export class HaDataTable extends LitElement { | ||||
|     fireEvent(this, "row-click", { id: rowId }, { bubbles: false }); | ||||
|   }; | ||||
|  | ||||
|   private _setTitle(ev: Event) { | ||||
|     const target = ev.currentTarget as HTMLElement; | ||||
|     if (target.scrollWidth > target.offsetWidth) { | ||||
|       target.setAttribute("title", target.innerText); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _checkedRowsChanged() { | ||||
|     // force scroller to update, change it's items | ||||
|     if (this._items.length) { | ||||
| @@ -718,40 +679,6 @@ export class HaDataTable extends LitElement { | ||||
|     this._savedScrollPos = (e.target as HTMLDivElement).scrollTop; | ||||
|   } | ||||
|  | ||||
|   private _collapseGroup = (ev: Event) => { | ||||
|     const groupName = (ev.currentTarget as any).group; | ||||
|     if (this._collapsedGroups.includes(groupName)) { | ||||
|       this._collapsedGroups = this._collapsedGroups.filter( | ||||
|         (grp) => grp !== groupName | ||||
|       ); | ||||
|     } else { | ||||
|       this._collapsedGroups = [...this._collapsedGroups, groupName]; | ||||
|     } | ||||
|     fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); | ||||
|   }; | ||||
|  | ||||
|   public expandAllGroups() { | ||||
|     this._collapsedGroups = []; | ||||
|     fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); | ||||
|   } | ||||
|  | ||||
|   public collapseAllGroups() { | ||||
|     if ( | ||||
|       !this.groupColumn || | ||||
|       !this.data.some((item) => item[this.groupColumn!]) | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|     const grouped = groupBy(this.data, (item) => item[this.groupColumn!]); | ||||
|     if (grouped.undefined) { | ||||
|       // undefined is a reserved group name | ||||
|       grouped[UNDEFINED_GROUP_KEY] = grouped.undefined; | ||||
|       delete grouped.undefined; | ||||
|     } | ||||
|     this._collapsedGroups = Object.keys(grouped); | ||||
|     fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
| @@ -1004,22 +931,8 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|         .group-header { | ||||
|           padding-top: 12px; | ||||
|           padding-left: 12px; | ||||
|           padding-inline-start: 12px; | ||||
|           padding-inline-end: initial; | ||||
|           width: 100%; | ||||
|           font-weight: 500; | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|           cursor: pointer; | ||||
|         } | ||||
|  | ||||
|         .group-header ha-icon-button { | ||||
|           transition: transform 0.2s ease; | ||||
|         } | ||||
|  | ||||
|         .group-header ha-icon-button.collapsed { | ||||
|           transform: rotate(180deg); | ||||
|         } | ||||
|  | ||||
|         :host { | ||||
| @@ -1118,12 +1031,4 @@ declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-data-table": HaDataTable; | ||||
|   } | ||||
|  | ||||
|   // for fire event | ||||
|   interface HASSDomEvents { | ||||
|     "selection-changed": SelectionChangedEvent; | ||||
|     "row-click": RowClickedEvent; | ||||
|     "sorting-changed": SortingChangedEvent; | ||||
|     "collapsed-changed": CollapsedChangedEvent; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -11,10 +11,10 @@ import { | ||||
| } from "../common/datetime/localize_date"; | ||||
| import { mainWindow } from "../common/dom/get_main_window"; | ||||
|  | ||||
| // Set the current date to the left picker instead of the right picker because the right is hidden | ||||
| const CustomDateRangePicker = Vue.extend({ | ||||
|   mixins: [DateRangePicker], | ||||
|   methods: { | ||||
|     // Set the current date to the left picker instead of the right picker because the right is hidden | ||||
|     selectMonthDate() { | ||||
|       const dt: Date = this.end || new Date(); | ||||
|       // @ts-ignore | ||||
| @@ -23,33 +23,6 @@ const CustomDateRangePicker = Vue.extend({ | ||||
|         month: dt.getMonth() + 1, | ||||
|       }); | ||||
|     }, | ||||
|     // Fix the start/end date calculation when selecting a date range. The | ||||
|     // original code keeps track of the first clicked date (in_selection) but it | ||||
|     // never sets it to either the start or end date variables, so if the | ||||
|     // in_selection date is between the start and end date that were set by the | ||||
|     // hover the selection will enter a broken state that's counter-intuitive | ||||
|     // when hovering between weeks and leads to a random date when selecting a | ||||
|     // range across months. This bug doesn't seem to be present on v0.6.7 of the | ||||
|     // lib | ||||
|     hoverDate(value: Date) { | ||||
|       if (this.readonly) return; | ||||
|  | ||||
|       if (this.in_selection) { | ||||
|         const pickA = this.in_selection as Date; | ||||
|         const pickB = value; | ||||
|  | ||||
|         this.start = this.normalizeDatetime( | ||||
|           Math.min(pickA.valueOf(), pickB.valueOf()), | ||||
|           this.start | ||||
|         ); | ||||
|         this.end = this.normalizeDatetime( | ||||
|           Math.max(pickA.valueOf(), pickB.valueOf()), | ||||
|           this.end | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       this.$emit("hover-date", value); | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -76,8 +76,6 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|   @property({ attribute: false }) | ||||
|   public entityFilter?: HaEntityPickerEntityFilterFunc; | ||||
|  | ||||
|   @property({ type: Array }) public createDomains?: string[]; | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this.hass) { | ||||
|       return nothing; | ||||
| @@ -105,7 +103,6 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|               .value=${entityId} | ||||
|               .label=${this.pickedEntityLabel} | ||||
|               .disabled=${this.disabled} | ||||
|               .createDomains=${this.createDomains} | ||||
|               @value-changed=${this._entityChanged} | ||||
|             ></ha-entity-picker> | ||||
|           </div> | ||||
| @@ -125,7 +122,6 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|           .label=${this.pickEntityLabel} | ||||
|           .helper=${this.helper} | ||||
|           .disabled=${this.disabled} | ||||
|           .createDomains=${this.createDomains} | ||||
|           .required=${this.required && !currentEntities.length} | ||||
|           @value-changed=${this._addEntity} | ||||
|         ></ha-entity-picker> | ||||
|   | ||||
| @@ -18,12 +18,6 @@ import "../ha-icon-button"; | ||||
| import "../ha-svg-icon"; | ||||
| import "./state-badge"; | ||||
| import { caseInsensitiveStringCompare } from "../../common/string/compare"; | ||||
| import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail"; | ||||
| import { domainToName } from "../../data/integration"; | ||||
| import { | ||||
|   isHelperDomain, | ||||
|   HelperDomain, | ||||
| } from "../../panels/config/helpers/const"; | ||||
|  | ||||
| interface HassEntityWithCachedName extends HassEntity, ScorableTextItem { | ||||
|   friendly_name: string; | ||||
| @@ -31,8 +25,6 @@ interface HassEntityWithCachedName extends HassEntity, ScorableTextItem { | ||||
|  | ||||
| export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean; | ||||
|  | ||||
| const CREATE_ID = "___create-new-entity___"; | ||||
|  | ||||
| @customElement("ha-entity-picker") | ||||
| export class HaEntityPicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -52,8 +44,6 @@ export class HaEntityPicker extends LitElement { | ||||
|  | ||||
|   @property() public helper?: string; | ||||
|  | ||||
|   @property({ type: Array }) public createDomains?: string[]; | ||||
|  | ||||
|   /** | ||||
|    * Show entities from specific domains. | ||||
|    * @type {Array} | ||||
| @@ -140,11 +130,7 @@ export class HaEntityPicker extends LitElement { | ||||
|           ></state-badge>` | ||||
|         : ""} | ||||
|       <span>${item.friendly_name}</span> | ||||
|       <span slot="secondary" | ||||
|         >${item.entity_id.startsWith(CREATE_ID) | ||||
|           ? this.hass.localize("ui.components.entity.entity-picker.new_entity") | ||||
|           : item.entity_id}</span | ||||
|       > | ||||
|       <span slot="secondary">${item.entity_id}</span> | ||||
|     </ha-list-item>`; | ||||
|  | ||||
|   private _getStates = memoizeOne( | ||||
| @@ -157,8 +143,7 @@ export class HaEntityPicker extends LitElement { | ||||
|       includeDeviceClasses: this["includeDeviceClasses"], | ||||
|       includeUnitOfMeasurement: this["includeUnitOfMeasurement"], | ||||
|       includeEntities: this["includeEntities"], | ||||
|       excludeEntities: this["excludeEntities"], | ||||
|       createDomains: this["createDomains"] | ||||
|       excludeEntities: this["excludeEntities"] | ||||
|     ): HassEntityWithCachedName[] => { | ||||
|       let states: HassEntityWithCachedName[] = []; | ||||
|  | ||||
| @@ -167,34 +152,6 @@ export class HaEntityPicker extends LitElement { | ||||
|       } | ||||
|       let entityIds = Object.keys(hass.states); | ||||
|  | ||||
|       const createItems = createDomains?.length | ||||
|         ? createDomains.map((domain) => { | ||||
|             const newFriendlyName = hass.localize( | ||||
|               "ui.components.entity.entity-picker.create_helper", | ||||
|               { | ||||
|                 domain: isHelperDomain(domain) | ||||
|                   ? hass.localize( | ||||
|                       `ui.panel.config.helpers.types.${domain as HelperDomain}` | ||||
|                     ) | ||||
|                   : domainToName(hass.localize, domain), | ||||
|               } | ||||
|             ); | ||||
|  | ||||
|             return { | ||||
|               entity_id: CREATE_ID + domain, | ||||
|               state: "on", | ||||
|               last_changed: "", | ||||
|               last_updated: "", | ||||
|               context: { id: "", user_id: null, parent_id: null }, | ||||
|               friendly_name: newFriendlyName, | ||||
|               attributes: { | ||||
|                 icon: "mdi:plus", | ||||
|               }, | ||||
|               strings: [domain, newFriendlyName], | ||||
|             }; | ||||
|           }) | ||||
|         : []; | ||||
|  | ||||
|       if (!entityIds.length) { | ||||
|         return [ | ||||
|           { | ||||
| @@ -214,7 +171,6 @@ export class HaEntityPicker extends LitElement { | ||||
|             }, | ||||
|             strings: [], | ||||
|           }, | ||||
|           ...createItems, | ||||
|         ]; | ||||
|       } | ||||
|  | ||||
| @@ -325,14 +281,9 @@ export class HaEntityPicker extends LitElement { | ||||
|             }, | ||||
|             strings: [], | ||||
|           }, | ||||
|           ...createItems, | ||||
|         ]; | ||||
|       } | ||||
|  | ||||
|       if (createItems?.length) { | ||||
|         states.push(...createItems); | ||||
|       } | ||||
|  | ||||
|       return states; | ||||
|     } | ||||
|   ); | ||||
| @@ -359,18 +310,13 @@ export class HaEntityPicker extends LitElement { | ||||
|         this.includeDeviceClasses, | ||||
|         this.includeUnitOfMeasurement, | ||||
|         this.includeEntities, | ||||
|         this.excludeEntities, | ||||
|         this.createDomains | ||||
|         this.excludeEntities | ||||
|       ); | ||||
|       if (this._initedStates) { | ||||
|         this.comboBox.filteredItems = this._states; | ||||
|       } | ||||
|       this._initedStates = true; | ||||
|     } | ||||
|  | ||||
|     if (changedProps.has("createDomains") && this.createDomains?.length) { | ||||
|       this.hass.loadFragmentTranslation("config"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
| @@ -405,21 +351,9 @@ export class HaEntityPicker extends LitElement { | ||||
|     this._opened = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: ValueChangedEvent<string | undefined>) { | ||||
|   private _valueChanged(ev: ValueChangedEvent<string>) { | ||||
|     ev.stopPropagation(); | ||||
|     const newValue = ev.detail.value?.trim(); | ||||
|  | ||||
|     if (newValue && newValue.startsWith(CREATE_ID)) { | ||||
|       const domain = newValue.substring(CREATE_ID.length); | ||||
|       showHelperDetailDialog(this, { | ||||
|         domain, | ||||
|         dialogClosedCallback: (item) => { | ||||
|           if (item.entityId) this._setValue(item.entityId); | ||||
|         }, | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const newValue = ev.detail.value; | ||||
|     if (newValue !== this._value) { | ||||
|       this._setValue(newValue); | ||||
|     } | ||||
| @@ -427,13 +361,13 @@ export class HaEntityPicker extends LitElement { | ||||
|  | ||||
|   private _filterChanged(ev: CustomEvent): void { | ||||
|     const target = ev.target as HaComboBox; | ||||
|     const filterString = ev.detail.value.trim().toLowerCase(); | ||||
|     const filterString = ev.detail.value.toLowerCase(); | ||||
|     target.filteredItems = filterString.length | ||||
|       ? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states) | ||||
|       : this._states; | ||||
|   } | ||||
|  | ||||
|   private _setValue(value: string | undefined) { | ||||
|   private _setValue(value: string) { | ||||
|     this.value = value; | ||||
|     setTimeout(() => { | ||||
|       fireEvent(this, "value-changed", { value }); | ||||
|   | ||||
| @@ -14,8 +14,6 @@ export class HaCard extends LitElement { | ||||
|           --ha-card-background, | ||||
|           var(--card-background-color, white) | ||||
|         ); | ||||
|         -webkit-backdrop-filter: var(--ha-card-backdrop-filter, none); | ||||
|         backdrop-filter: var(--ha-card-backdrop-filter, none); | ||||
|         box-shadow: var(--ha-card-box-shadow, none); | ||||
|         box-sizing: border-box; | ||||
|         border-radius: var(--ha-card-border-radius, 12px); | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import "element-internals-polyfill"; | ||||
| import { MdCircularProgress } from "@material/web/progress/circular-progress"; | ||||
| import { PropertyValues, css } from "lit"; | ||||
| import { CSSResult, PropertyValues, css } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
|  | ||||
| @customElement("ha-circular-progress") | ||||
| @@ -31,7 +32,8 @@ export class HaCircularProgress extends MdCircularProgress { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static override styles = [ | ||||
|   static get styles(): CSSResult[] { | ||||
|     return [ | ||||
|       ...super.styles, | ||||
|       css` | ||||
|         :host { | ||||
| @@ -41,6 +43,7 @@ export class HaCircularProgress extends MdCircularProgress { | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|   | ||||
| @@ -1,7 +1,14 @@ | ||||
| import { Ripple } from "@material/mwc-ripple"; | ||||
| import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { | ||||
|   customElement, | ||||
|   eventOptions, | ||||
|   property, | ||||
|   queryAsync, | ||||
|   state, | ||||
| } from "lit/decorators"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import "./ha-ripple"; | ||||
|  | ||||
| @customElement("ha-control-button") | ||||
| export class HaControlButton extends LitElement { | ||||
| @@ -9,6 +16,10 @@ export class HaControlButton extends LitElement { | ||||
|  | ||||
|   @property() public label?: string; | ||||
|  | ||||
|   @queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>; | ||||
|  | ||||
|   @state() private _shouldRenderRipple = false; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <button | ||||
| @@ -17,13 +28,54 @@ export class HaControlButton extends LitElement { | ||||
|         aria-label=${ifDefined(this.label)} | ||||
|         title=${ifDefined(this.label)} | ||||
|         .disabled=${Boolean(this.disabled)} | ||||
|         @focus=${this.handleRippleFocus} | ||||
|         @blur=${this.handleRippleBlur} | ||||
|         @mousedown=${this.handleRippleActivate} | ||||
|         @mouseup=${this.handleRippleDeactivate} | ||||
|         @mouseenter=${this.handleRippleMouseEnter} | ||||
|         @mouseleave=${this.handleRippleMouseLeave} | ||||
|         @touchstart=${this.handleRippleActivate} | ||||
|         @touchend=${this.handleRippleDeactivate} | ||||
|         @touchcancel=${this.handleRippleDeactivate} | ||||
|       > | ||||
|         <slot></slot> | ||||
|         <ha-ripple .disabled=${this.disabled}></ha-ripple> | ||||
|         ${this._shouldRenderRipple && !this.disabled | ||||
|           ? html`<mwc-ripple></mwc-ripple>` | ||||
|           : ""} | ||||
|       </button> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _rippleHandlers: RippleHandlers = new RippleHandlers(() => { | ||||
|     this._shouldRenderRipple = true; | ||||
|     return this._ripple; | ||||
|   }); | ||||
|  | ||||
|   @eventOptions({ passive: true }) | ||||
|   private handleRippleActivate(evt?: Event) { | ||||
|     this._rippleHandlers.startPress(evt); | ||||
|   } | ||||
|  | ||||
|   private handleRippleDeactivate() { | ||||
|     this._rippleHandlers.endPress(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleMouseEnter() { | ||||
|     this._rippleHandlers.startHover(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleMouseLeave() { | ||||
|     this._rippleHandlers.endHover(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleFocus() { | ||||
|     this._rippleHandlers.startFocus(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleBlur() { | ||||
|     this._rippleHandlers.endFocus(); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
| @@ -34,7 +86,6 @@ export class HaControlButton extends LitElement { | ||||
|         --control-button-border-radius: 10px; | ||||
|         --control-button-padding: 8px; | ||||
|         --mdc-icon-size: 20px; | ||||
|         --ha-ripple-color: var(--secondary-text-color); | ||||
|         color: var(--primary-text-color); | ||||
|         width: 40px; | ||||
|         height: 40px; | ||||
| @@ -62,14 +113,12 @@ export class HaControlButton extends LitElement { | ||||
|         outline: none; | ||||
|         overflow: hidden; | ||||
|         background: none; | ||||
|         --mdc-ripple-color: var(--control-button-background-color); | ||||
|         /* For safari border-radius overflow */ | ||||
|         z-index: 0; | ||||
|         font-size: inherit; | ||||
|         color: inherit; | ||||
|       } | ||||
|       .button:focus-visible { | ||||
|         --control-button-background-opacity: 0.4; | ||||
|       } | ||||
|       .button::before { | ||||
|         content: ""; | ||||
|         position: absolute; | ||||
|   | ||||
| @@ -1,14 +1,22 @@ | ||||
| import { Ripple } from "@material/mwc-ripple"; | ||||
| import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; | ||||
| import { SelectBase } from "@material/mwc-select/mwc-select-base"; | ||||
| import { mdiMenuDown } from "@mdi/js"; | ||||
| import { css, html, nothing } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { | ||||
|   customElement, | ||||
|   eventOptions, | ||||
|   property, | ||||
|   query, | ||||
|   queryAsync, | ||||
|   state, | ||||
| } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import { debounce } from "../common/util/debounce"; | ||||
| import { nextRender } from "../common/util/render-status"; | ||||
| import "./ha-icon"; | ||||
| import type { HaIcon } from "./ha-icon"; | ||||
| import "./ha-ripple"; | ||||
| import "./ha-svg-icon"; | ||||
| import type { HaSvgIcon } from "./ha-svg-icon"; | ||||
|  | ||||
| @@ -24,6 +32,10 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|   @property({ type: Boolean, attribute: "hide-label" }) | ||||
|   public hideLabel = false; | ||||
|  | ||||
|   @queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>; | ||||
|  | ||||
|   @state() private _shouldRenderRipple = false; | ||||
|  | ||||
|   public override render() { | ||||
|     const classes = { | ||||
|       "select-disabled": this.disabled, | ||||
| @@ -57,10 +69,17 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|           aria-labelledby=${ifDefined(labelledby)} | ||||
|           aria-label=${ifDefined(labelAttribute)} | ||||
|           aria-required=${this.required} | ||||
|           @click=${this.onClick} | ||||
|           @focus=${this.onFocus} | ||||
|           @blur=${this.onBlur} | ||||
|           @click=${this.onClick} | ||||
|           @keydown=${this.onKeydown} | ||||
|           @mousedown=${this.handleRippleActivate} | ||||
|           @mouseup=${this.handleRippleDeactivate} | ||||
|           @mouseenter=${this.handleRippleMouseEnter} | ||||
|           @mouseleave=${this.handleRippleMouseLeave} | ||||
|           @touchstart=${this.handleRippleActivate} | ||||
|           @touchend=${this.handleRippleDeactivate} | ||||
|           @touchcancel=${this.handleRippleDeactivate} | ||||
|         > | ||||
|           ${this.renderIcon()} | ||||
|           <div class="content"> | ||||
| @@ -72,7 +91,9 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|               : nothing} | ||||
|           </div> | ||||
|           ${this.renderArrow()} | ||||
|           <ha-ripple .disabled=${this.disabled}></ha-ripple> | ||||
|           ${this._shouldRenderRipple && !this.disabled | ||||
|             ? html` <mwc-ripple></mwc-ripple> ` | ||||
|             : nothing} | ||||
|         </div> | ||||
|         ${this.renderMenu()} | ||||
|       </div> | ||||
| @@ -114,6 +135,46 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   protected onFocus() { | ||||
|     this.handleRippleFocus(); | ||||
|     super.onFocus(); | ||||
|   } | ||||
|  | ||||
|   protected onBlur() { | ||||
|     this.handleRippleBlur(); | ||||
|     super.onBlur(); | ||||
|   } | ||||
|  | ||||
|   private _rippleHandlers: RippleHandlers = new RippleHandlers(() => { | ||||
|     this._shouldRenderRipple = true; | ||||
|     return this._ripple; | ||||
|   }); | ||||
|  | ||||
|   @eventOptions({ passive: true }) | ||||
|   private handleRippleActivate(evt?: Event) { | ||||
|     this._rippleHandlers.startPress(evt); | ||||
|   } | ||||
|  | ||||
|   private handleRippleDeactivate() { | ||||
|     this._rippleHandlers.endPress(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleMouseEnter() { | ||||
|     this._rippleHandlers.startHover(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleMouseLeave() { | ||||
|     this._rippleHandlers.endHover(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleFocus() { | ||||
|     this._rippleHandlers.startFocus(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleBlur() { | ||||
|     this._rippleHandlers.endFocus(); | ||||
|   } | ||||
|  | ||||
|   connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     window.addEventListener("translations-updated", this._translationsUpdated); | ||||
| @@ -143,7 +204,6 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|         --control-select-menu-height: 48px; | ||||
|         --control-select-menu-padding: 6px 10px; | ||||
|         --mdc-icon-size: 20px; | ||||
|         --ha-ripple-color: var(--secondary-text-color); | ||||
|         font-size: 14px; | ||||
|         line-height: 1.4; | ||||
|         width: auto; | ||||
| @@ -164,6 +224,7 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|         outline: none; | ||||
|         overflow: hidden; | ||||
|         background: none; | ||||
|         --mdc-ripple-color: var(--control-select-menu-background-color); | ||||
|         /* For safari border-radius overflow */ | ||||
|         z-index: 0; | ||||
|         transition: color 180ms ease-in-out; | ||||
| @@ -203,10 +264,6 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|         letter-spacing: inherit; | ||||
|       } | ||||
|  | ||||
|       .select-anchor:focus-visible { | ||||
|         --control-select-menu-background-opacity: 0.4; | ||||
|       } | ||||
|  | ||||
|       .select-anchor::before { | ||||
|         content: ""; | ||||
|         position: absolute; | ||||
|   | ||||
| @@ -67,9 +67,6 @@ export class HaControlSlider extends LitElement { | ||||
|   @property({ attribute: "tooltip-mode" }) | ||||
|   public tooltipMode: TooltipMode = "interaction"; | ||||
|  | ||||
|   @property({ attribute: "touch-action" }) | ||||
|   public touchAction?: string; | ||||
|  | ||||
|   @property({ type: Number }) | ||||
|   public value?: number; | ||||
|  | ||||
| @@ -155,7 +152,7 @@ export class HaControlSlider extends LitElement { | ||||
|   setupListeners() { | ||||
|     if (this.slider && !this._mc) { | ||||
|       this._mc = new Manager(this.slider, { | ||||
|         touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"), | ||||
|         touchAction: this.vertical ? "pan-x" : "pan-y", | ||||
|       }); | ||||
|       this._mc.add( | ||||
|         new Pan({ | ||||
|   | ||||
| @@ -33,9 +33,6 @@ export class HaControlSwitch extends LitElement { | ||||
|   // SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in) | ||||
|   @property({ type: String }) pathOff?: string; | ||||
|  | ||||
|   @property({ attribute: "touch-action" }) | ||||
|   public touchAction?: string; | ||||
|  | ||||
|   private _mc?: HammerManager; | ||||
|  | ||||
|   protected firstUpdated(changedProperties: PropertyValues): void { | ||||
| @@ -76,7 +73,7 @@ export class HaControlSwitch extends LitElement { | ||||
|   setupListeners() { | ||||
|     if (this.switch && !this._mc) { | ||||
|       this._mc = new Manager(this.switch, { | ||||
|         touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"), | ||||
|         touchAction: this.vertical ? "pan-x" : "pan-y", | ||||
|       }); | ||||
|       this._mc.add( | ||||
|         new Swipe({ | ||||
|   | ||||
| @@ -19,7 +19,6 @@ import { HomeAssistant } from "../types"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-select"; | ||||
| import type { HaSelect } from "./ha-select"; | ||||
| import { getExtendedEntityRegistryEntry } from "../data/entity_registry"; | ||||
|  | ||||
| const NONE = "__NONE_OPTION__"; | ||||
|  | ||||
| @@ -108,23 +107,13 @@ export class HaConversationAgentPicker extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private async _maybeFetchConfigEntry() { | ||||
|     if (!this.value || !(this.value in this.hass.entities)) { | ||||
|     if (!this.value || this.value === "homeassistant") { | ||||
|       this._configEntry = undefined; | ||||
|       return; | ||||
|     } | ||||
|     try { | ||||
|       const regEntry = await getExtendedEntityRegistryEntry( | ||||
|         this.hass, | ||||
|         this.value | ||||
|       ); | ||||
|  | ||||
|       if (!regEntry.config_entry_id) { | ||||
|         this._configEntry = undefined; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       this._configEntry = ( | ||||
|         await getConfigEntry(this.hass, regEntry.config_entry_id) | ||||
|         await getConfigEntry(this.hass, this.value) | ||||
|       ).config_entry; | ||||
|     } catch (err) { | ||||
|       this._configEntry = undefined; | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../common/dom/stop_propagation"; | ||||
| import { caseInsensitiveStringCompare } from "../common/string/compare"; | ||||
| import "../resources/intl-polyfill"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-select"; | ||||
| import type { HaSelect } from "./ha-select"; | ||||
| @@ -281,10 +282,14 @@ export class HaCountryPicker extends LitElement { | ||||
|   private _getOptions = memoizeOne( | ||||
|     (language?: string, countries?: string[]) => { | ||||
|       let options: { label: string; value: string }[] = []; | ||||
|       const countryDisplayNames = new Intl.DisplayNames(language, { | ||||
|       const countryDisplayNames = | ||||
|         Intl && "DisplayNames" in Intl | ||||
|           ? new Intl.DisplayNames(language, { | ||||
|               type: "region", | ||||
|               fallback: "code", | ||||
|       }); | ||||
|             }) | ||||
|           : undefined; | ||||
|  | ||||
|       if (countries) { | ||||
|         options = countries.map((country) => ({ | ||||
|           value: country, | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../common/dom/stop_propagation"; | ||||
| import { caseInsensitiveStringCompare } from "../common/string/compare"; | ||||
| import "../resources/intl-polyfill"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-select"; | ||||
| import type { HaSelect } from "./ha-select"; | ||||
| @@ -169,9 +170,12 @@ const CURRENCIES = [ | ||||
| ]; | ||||
|  | ||||
| const curSymbol = (currency: string, locale?: string) => | ||||
|   new Intl.NumberFormat(locale, { style: "currency", currency }) | ||||
|   Intl && "NumberFormat" in Intl | ||||
|     ? new Intl.NumberFormat(locale, { style: "currency", currency }) | ||||
|         .formatToParts(1) | ||||
|     .find((x) => x.type === "currency")?.value; | ||||
|         .find((x) => x.type === "currency")?.value | ||||
|     : currency; | ||||
|  | ||||
| @customElement("ha-currency-picker") | ||||
| export class HaCurrencyPicker extends LitElement { | ||||
|   @property() public language = "en"; | ||||
| @@ -185,10 +189,13 @@ export class HaCurrencyPicker extends LitElement { | ||||
|   @property({ type: Boolean, reflect: true }) public disabled = false; | ||||
|  | ||||
|   private _getOptions = memoizeOne((language?: string) => { | ||||
|     const currencyDisplayNames = new Intl.DisplayNames(language, { | ||||
|     const currencyDisplayNames = | ||||
|       Intl && "DisplayNames" in Intl | ||||
|         ? new Intl.DisplayNames(language, { | ||||
|             type: "currency", | ||||
|             fallback: "code", | ||||
|     }); | ||||
|           }) | ||||
|         : undefined; | ||||
|     const options = CURRENCIES.map((currency) => ({ | ||||
|       value: currency, | ||||
|       label: `${ | ||||
|   | ||||
| @@ -75,14 +75,8 @@ export class HaDialog extends DialogBase { | ||||
|           var(--divider-color) | ||||
|         ); | ||||
|         z-index: var(--dialog-z-index, 8); | ||||
|         -webkit-backdrop-filter: var( | ||||
|           --ha-dialog-scrim-backdrop-filter, | ||||
|           var(--dialog-backdrop-filter, none) | ||||
|         ); | ||||
|         backdrop-filter: var( | ||||
|           --ha-dialog-scrim-backdrop-filter, | ||||
|           var(--dialog-backdrop-filter, none) | ||||
|         ); | ||||
|         -webkit-backdrop-filter: var(--dialog-backdrop-filter, none); | ||||
|         backdrop-filter: var(--dialog-backdrop-filter, none); | ||||
|         --mdc-dialog-box-shadow: var(--dialog-box-shadow, none); | ||||
|         --mdc-typography-headline6-font-weight: 400; | ||||
|         --mdc-typography-headline6-font-size: 1.574rem; | ||||
| @@ -125,12 +119,6 @@ export class HaDialog extends DialogBase { | ||||
|         margin-top: var(--dialog-surface-margin-top); | ||||
|         min-height: var(--mdc-dialog-min-height, auto); | ||||
|         border-radius: var(--ha-dialog-border-radius, 28px); | ||||
|         -webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none); | ||||
|         backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none); | ||||
|         background: var( | ||||
|           --ha-dialog-surface-background, | ||||
|           var(--mdc-theme-surface, #fff) | ||||
|         ); | ||||
|       } | ||||
|       :host([flexContent]) .mdc-dialog .mdc-dialog__content { | ||||
|         display: flex; | ||||
|   | ||||
| @@ -21,8 +21,6 @@ export class HaExpansionPanel extends LitElement { | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) leftChevron = false; | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) noCollapse = false; | ||||
|  | ||||
|   @property() header?: string; | ||||
|  | ||||
|   @property() secondary?: string; | ||||
| @@ -36,17 +34,16 @@ export class HaExpansionPanel extends LitElement { | ||||
|       <div class="top ${classMap({ expanded: this.expanded })}"> | ||||
|         <div | ||||
|           id="summary" | ||||
|           class=${classMap({ noCollapse: this.noCollapse })} | ||||
|           @click=${this._toggleContainer} | ||||
|           @keydown=${this._toggleContainer} | ||||
|           @focus=${this._focusChanged} | ||||
|           @blur=${this._focusChanged} | ||||
|           role="button" | ||||
|           tabindex=${this.noCollapse ? -1 : 0} | ||||
|           tabindex="0" | ||||
|           aria-expanded=${this.expanded} | ||||
|           aria-controls="sect1" | ||||
|         > | ||||
|           ${this.leftChevron && !this.noCollapse | ||||
|           ${this.leftChevron | ||||
|             ? html` | ||||
|                 <ha-svg-icon | ||||
|                   .path=${mdiChevronDown} | ||||
| @@ -60,7 +57,7 @@ export class HaExpansionPanel extends LitElement { | ||||
|               <slot class="secondary" name="secondary">${this.secondary}</slot> | ||||
|             </div> | ||||
|           </slot> | ||||
|           ${!this.leftChevron && !this.noCollapse | ||||
|           ${!this.leftChevron | ||||
|             ? html` | ||||
|                 <ha-svg-icon | ||||
|                   .path=${mdiChevronDown} | ||||
| @@ -109,9 +106,6 @@ export class HaExpansionPanel extends LitElement { | ||||
|       return; | ||||
|     } | ||||
|     ev.preventDefault(); | ||||
|     if (this.noCollapse) { | ||||
|       return; | ||||
|     } | ||||
|     const newExpanded = !this.expanded; | ||||
|     fireEvent(this, "expanded-will-change", { expanded: newExpanded }); | ||||
|     this._container.style.overflow = "hidden"; | ||||
| @@ -136,9 +130,6 @@ export class HaExpansionPanel extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _focusChanged(ev) { | ||||
|     if (this.noCollapse) { | ||||
|       return; | ||||
|     } | ||||
|     this.shadowRoot!.querySelector(".top")!.classList.toggle( | ||||
|       "focused", | ||||
|       ev.type === "focus" | ||||
| @@ -200,9 +191,6 @@ export class HaExpansionPanel extends LitElement { | ||||
|         font-weight: 500; | ||||
|         outline: none; | ||||
|       } | ||||
|       #summary.noCollapse { | ||||
|         cursor: default; | ||||
|       } | ||||
|  | ||||
|       .summary-icon.expanded { | ||||
|         transform: rotate(180deg); | ||||
|   | ||||
| @@ -1,14 +1,7 @@ | ||||
| import { SelectedDetail } from "@material/mwc-list"; | ||||
| import "@material/mwc-menu/mwc-menu-surface"; | ||||
| import { mdiFilterVariantRemove } from "@mdi/js"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   nothing, | ||||
|   PropertyValues, | ||||
| } from "lit"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { Blueprints, fetchBlueprints } from "../data/blueprint"; | ||||
| @@ -32,16 +25,6 @@ export class HaFilterBlueprints extends LitElement { | ||||
|  | ||||
|   @state() private _blueprints?: Blueprints; | ||||
|  | ||||
|   public willUpdate(properties: PropertyValues) { | ||||
|     super.willUpdate(properties); | ||||
|  | ||||
|     if (!this.hasUpdated) { | ||||
|       if (this.value?.length) { | ||||
|         this._findRelated(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <ha-expansion-panel | ||||
| @@ -113,6 +96,7 @@ export class HaFilterBlueprints extends LitElement { | ||||
|     ev: CustomEvent<SelectedDetail<Set<number>>> | ||||
|   ) { | ||||
|     const blueprints = this._blueprints!; | ||||
|     const relatedPromises: Promise<RelatedResult>[] = []; | ||||
|  | ||||
|     if (!ev.detail.index.size) { | ||||
|       fireEvent(this, "data-table-filter-changed", { | ||||
| @@ -128,33 +112,13 @@ export class HaFilterBlueprints extends LitElement { | ||||
|     for (const index of ev.detail.index) { | ||||
|       const blueprintId = Object.keys(blueprints)[index]; | ||||
|       value.push(blueprintId); | ||||
|     } | ||||
|  | ||||
|     this.value = value; | ||||
|  | ||||
|     this._findRelated(); | ||||
|   } | ||||
|  | ||||
|   private async _findRelated() { | ||||
|     if (!this.value?.length) { | ||||
|       fireEvent(this, "data-table-filter-changed", { | ||||
|         value: [], | ||||
|         items: undefined, | ||||
|       }); | ||||
|       this.value = []; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const relatedPromises: Promise<RelatedResult>[] = []; | ||||
|  | ||||
|     for (const blueprintId of this.value) { | ||||
|       if (this.type) { | ||||
|         relatedPromises.push( | ||||
|           findRelated(this.hass, `${this.type}_blueprint`, blueprintId) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     this.value = value; | ||||
|     const results = await Promise.all(relatedPromises); | ||||
|     const items: Set<string> = new Set(); | ||||
|     for (const result of results) { | ||||
| @@ -164,7 +128,7 @@ export class HaFilterBlueprints extends LitElement { | ||||
|     } | ||||
|  | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: this.value, | ||||
|       value, | ||||
|       items: this.type ? items : undefined, | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -41,9 +41,6 @@ export class HaFilterDevices extends LitElement { | ||||
|  | ||||
|     if (!this.hasUpdated) { | ||||
|       loadVirtualizer(); | ||||
|       if (this.value?.length) { | ||||
|         this._findRelated(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -72,7 +69,7 @@ export class HaFilterDevices extends LitElement { | ||||
|                 @value-changed=${this._handleSearchChange} | ||||
|               > | ||||
|               </search-input-outlined> | ||||
|               <mwc-list class="ha-scrollbar" multi> | ||||
|               <mwc-list class="ha-scrollbar"> | ||||
|                 <lit-virtualizer | ||||
|                   .items=${this._devices( | ||||
|                     this.hass.devices, | ||||
| @@ -97,7 +94,7 @@ export class HaFilterDevices extends LitElement { | ||||
|       ? nothing | ||||
|       : html`<ha-check-list-item | ||||
|           .value=${device.id} | ||||
|           .selected=${this.value?.includes(device.id) ?? false} | ||||
|           .selected=${this.value?.includes(device.id)} | ||||
|         > | ||||
|           ${computeDeviceName(device, this.hass)} | ||||
|         </ha-check-list-item>`; | ||||
|   | ||||
| @@ -1,204 +0,0 @@ | ||||
| import { mdiFilterVariantRemove } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { stringCompare } from "../common/string/compare"; | ||||
| import { domainToName } from "../data/integration"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import "./ha-domain-icon"; | ||||
| import "./search-input-outlined"; | ||||
| import { computeDomain } from "../common/entity/compute_domain"; | ||||
|  | ||||
| @customElement("ha-filter-domains") | ||||
| export class HaFilterDomains extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public value?: string[]; | ||||
|  | ||||
|   @property({ type: Boolean }) public narrow = false; | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) public expanded = false; | ||||
|  | ||||
|   @state() private _shouldRender = false; | ||||
|  | ||||
|   @state() private _filter?: string; | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <ha-expansion-panel | ||||
|         leftChevron | ||||
|         .expanded=${this.expanded} | ||||
|         @expanded-will-change=${this._expandedWillChange} | ||||
|         @expanded-changed=${this._expandedChanged} | ||||
|       > | ||||
|         <div slot="header" class="header"> | ||||
|           ${this.hass.localize( | ||||
|             "ui.panel.config.entities.picker.headers.domain" | ||||
|           )} | ||||
|           ${this.value?.length | ||||
|             ? html`<div class="badge">${this.value?.length}</div> | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiFilterVariantRemove} | ||||
|                   @click=${this._clearFilter} | ||||
|                 ></ha-icon-button>` | ||||
|             : nothing} | ||||
|         </div> | ||||
|         ${this._shouldRender | ||||
|           ? html`<search-input-outlined | ||||
|                 .hass=${this.hass} | ||||
|                 .filter=${this._filter} | ||||
|                 @value-changed=${this._handleSearchChange} | ||||
|               > | ||||
|               </search-input-outlined> | ||||
|               <mwc-list | ||||
|                 class="ha-scrollbar" | ||||
|                 @click=${this._handleItemClick} | ||||
|                 multi | ||||
|               > | ||||
|                 ${repeat( | ||||
|                   this._domains(this.hass.states, this._filter), | ||||
|                   (i) => i, | ||||
|                   (domain) => | ||||
|                     html`<ha-check-list-item | ||||
|                       .value=${domain} | ||||
|                       .selected=${(this.value || []).includes(domain)} | ||||
|                       graphic="icon" | ||||
|                     > | ||||
|                       <ha-domain-icon | ||||
|                         slot="graphic" | ||||
|                         .hass=${this.hass} | ||||
|                         .domain=${domain} | ||||
|                         brandFallback | ||||
|                       ></ha-domain-icon> | ||||
|                       ${domainToName(this.hass.localize, domain)} | ||||
|                     </ha-check-list-item>` | ||||
|                 )} | ||||
|               </mwc-list> ` | ||||
|           : nothing} | ||||
|       </ha-expansion-panel> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _domains = memoizeOne((states, filter) => { | ||||
|     const domains = new Set<string>(); | ||||
|     Object.keys(states).forEach((entityId) => { | ||||
|       domains.add(computeDomain(entityId)); | ||||
|     }); | ||||
|  | ||||
|     return Array.from(domains.values()) | ||||
|       .filter( | ||||
|         (entry) => | ||||
|           !filter || | ||||
|           entry.toLowerCase().includes(filter) || | ||||
|           domainToName(this.hass.localize, entry).toLowerCase().includes(filter) | ||||
|       ) | ||||
|       .sort((a, b) => stringCompare(a, b, this.hass.locale.language)); | ||||
|   }); | ||||
|  | ||||
|   protected updated(changed) { | ||||
|     if (changed.has("expanded") && this.expanded) { | ||||
|       setTimeout(() => { | ||||
|         if (!this.expanded) return; | ||||
|         this.renderRoot.querySelector("mwc-list")!.style.height = | ||||
|           `${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input | ||||
|       }, 300); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _expandedWillChange(ev) { | ||||
|     this._shouldRender = ev.detail.expanded; | ||||
|   } | ||||
|  | ||||
|   private _expandedChanged(ev) { | ||||
|     this.expanded = ev.detail.expanded; | ||||
|   } | ||||
|  | ||||
|   private _handleItemClick(ev) { | ||||
|     const listItem = ev.target.closest("ha-check-list-item"); | ||||
|     const value = listItem?.value; | ||||
|     if (!value) { | ||||
|       return; | ||||
|     } | ||||
|     if (this.value?.includes(value)) { | ||||
|       this.value = this.value?.filter((val) => val !== value); | ||||
|     } else { | ||||
|       this.value = [...(this.value || []), value]; | ||||
|     } | ||||
|  | ||||
|     listItem.selected = this.value.includes(value); | ||||
|  | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: this.value, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _clearFilter(ev) { | ||||
|     ev.preventDefault(); | ||||
|     this.value = undefined; | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: undefined, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _handleSearchChange(ev: CustomEvent) { | ||||
|     this._filter = ev.detail.value.toLowerCase(); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
|       css` | ||||
|         :host { | ||||
|           border-bottom: 1px solid var(--divider-color); | ||||
|         } | ||||
|         :host([expanded]) { | ||||
|           flex: 1; | ||||
|           height: 0; | ||||
|         } | ||||
|         ha-expansion-panel { | ||||
|           --ha-card-border-radius: 0; | ||||
|           --expansion-panel-content-padding: 0; | ||||
|         } | ||||
|         .header { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|         } | ||||
|         .header ha-icon-button { | ||||
|           margin-inline-start: initial; | ||||
|           margin-inline-end: 8px; | ||||
|         } | ||||
|         .badge { | ||||
|           display: inline-block; | ||||
|           margin-left: 8px; | ||||
|           margin-inline-start: 8px; | ||||
|           margin-inline-end: initial; | ||||
|           min-width: 16px; | ||||
|           box-sizing: border-box; | ||||
|           border-radius: 50%; | ||||
|           font-weight: 400; | ||||
|           font-size: 11px; | ||||
|           background-color: var(--primary-color); | ||||
|           line-height: 16px; | ||||
|           text-align: center; | ||||
|           padding: 0px 2px; | ||||
|           color: var(--text-primary-color); | ||||
|         } | ||||
|         search-input-outlined { | ||||
|           display: block; | ||||
|           padding: 0 8px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-filter-domains": HaFilterDomains; | ||||
|   } | ||||
| } | ||||
| @@ -42,9 +42,6 @@ export class HaFilterEntities extends LitElement { | ||||
|  | ||||
|     if (!this.hasUpdated) { | ||||
|       loadVirtualizer(); | ||||
|       if (this.value?.length) { | ||||
|         this._findRelated(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -74,7 +71,7 @@ export class HaFilterEntities extends LitElement { | ||||
|                 @value-changed=${this._handleSearchChange} | ||||
|               > | ||||
|               </search-input-outlined> | ||||
|               <mwc-list class="ha-scrollbar" multi> | ||||
|               <mwc-list class="ha-scrollbar"> | ||||
|                 <lit-virtualizer | ||||
|                   .items=${this._entities( | ||||
|                     this.hass.states, | ||||
| @@ -111,7 +108,7 @@ export class HaFilterEntities extends LitElement { | ||||
|       ? nothing | ||||
|       : html`<ha-check-list-item | ||||
|           .value=${entity.entity_id} | ||||
|           .selected=${this.value?.includes(entity.entity_id) ?? false} | ||||
|           .selected=${this.value?.includes(entity.entity_id)} | ||||
|           graphic="icon" | ||||
|         > | ||||
|           <ha-state-icon | ||||
| @@ -189,12 +186,15 @@ export class HaFilterEntities extends LitElement { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const value: string[] = []; | ||||
|  | ||||
|     for (const entityId of this.value) { | ||||
|       value.push(entityId); | ||||
|       if (this.type) { | ||||
|         relatedPromises.push(findRelated(this.hass, "entity", entityId)); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     this.value = value; | ||||
|     const results = await Promise.all(relatedPromises); | ||||
|     const items: Set<string> = new Set(); | ||||
|     for (const result of results) { | ||||
| @@ -204,7 +204,7 @@ export class HaFilterEntities extends LitElement { | ||||
|     } | ||||
|  | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: this.value, | ||||
|       value, | ||||
|       items: this.type ? items : undefined, | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -1,14 +1,7 @@ | ||||
| import "@material/mwc-menu/mwc-menu-surface"; | ||||
| import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js"; | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| @@ -49,16 +42,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _floors?: FloorRegistryEntry[]; | ||||
|  | ||||
|   public willUpdate(properties: PropertyValues) { | ||||
|     super.willUpdate(properties); | ||||
|  | ||||
|     if (!this.hasUpdated) { | ||||
|       if (this.value?.floors?.length || this.value?.areas?.length) { | ||||
|         this._findRelated(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     const areas = this._areas(this.hass.areas, this._floors); | ||||
|  | ||||
| @@ -207,10 +190,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected firstUpdated() { | ||||
|     this._findRelated(); | ||||
|   } | ||||
|  | ||||
|   private _expandedWillChange(ev) { | ||||
|     this._shouldRender = ev.detail.expanded; | ||||
|   } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import { SelectedDetail } from "@material/mwc-list"; | ||||
| import { mdiFilterVariantRemove } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| @@ -56,9 +57,9 @@ export class HaFilterIntegrations extends LitElement { | ||||
|               > | ||||
|               </search-input-outlined> | ||||
|               <mwc-list | ||||
|                 class="ha-scrollbar" | ||||
|                 @click=${this._handleItemClick} | ||||
|                 @selected=${this._integrationsSelected} | ||||
|                 multi | ||||
|                 class="ha-scrollbar" | ||||
|               > | ||||
|                 ${repeat( | ||||
|                   this._integrations(this._manifests, this._filter, this.value), | ||||
| @@ -130,21 +131,34 @@ export class HaFilterIntegrations extends LitElement { | ||||
|         ) | ||||
|   ); | ||||
|  | ||||
|   private _handleItemClick(ev) { | ||||
|     const listItem = ev.target.closest("ha-check-list-item"); | ||||
|     const value = listItem?.value; | ||||
|     if (!value) { | ||||
|   private async _integrationsSelected( | ||||
|     ev: CustomEvent<SelectedDetail<Set<number>>> | ||||
|   ) { | ||||
|     const integrations = this._integrations( | ||||
|       this._manifests!, | ||||
|       this._filter, | ||||
|       this.value | ||||
|     ); | ||||
|  | ||||
|     if (!ev.detail.index.size) { | ||||
|       fireEvent(this, "data-table-filter-changed", { | ||||
|         value: [], | ||||
|         items: undefined, | ||||
|       }); | ||||
|       this.value = []; | ||||
|       return; | ||||
|     } | ||||
|     if (this.value?.includes(value)) { | ||||
|       this.value = this.value?.filter((val) => val !== value); | ||||
|     } else { | ||||
|       this.value = [...(this.value || []), value]; | ||||
|  | ||||
|     const value: string[] = []; | ||||
|  | ||||
|     for (const index of ev.detail.index) { | ||||
|       const domain = integrations[index].domain; | ||||
|       value.push(domain); | ||||
|     } | ||||
|     listItem.selected = this.value?.includes(value); | ||||
|     this.value = value; | ||||
|  | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: this.value, | ||||
|       value, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -62,8 +62,8 @@ export class HaFilterStates extends LitElement { | ||||
|                   (item) => | ||||
|                     html`<ha-check-list-item | ||||
|                       .value=${item.value} | ||||
|                       .selected=${this.value?.includes(item.value) ?? false} | ||||
|                       .graphic=${hasIcon ? "icon" : null} | ||||
|                       .selected=${this.value?.includes(item.value)} | ||||
|                       .graphic=${hasIcon ? "icon" : undefined} | ||||
|                     > | ||||
|                       ${item.icon | ||||
|                         ? html`<ha-icon | ||||
|   | ||||
| @@ -71,10 +71,6 @@ export const computeInitialHaFormData = ( | ||||
|         if (selector.country?.countries?.length) { | ||||
|           data[field.name] = selector.country.countries[0]; | ||||
|         } | ||||
|       } else if ("language" in selector) { | ||||
|         if (selector.language?.languages?.length) { | ||||
|           data[field.name] = selector.language.languages[0]; | ||||
|         } | ||||
|       } else if ("duration" in selector) { | ||||
|         data[field.name] = { | ||||
|           hours: 0, | ||||
| @@ -97,9 +93,7 @@ export const computeInitialHaFormData = ( | ||||
|       ) { | ||||
|         data[field.name] = {}; | ||||
|       } else { | ||||
|         throw new Error( | ||||
|           `Selector ${Object.keys(selector)[0]} not supported in initial form data` | ||||
|         ); | ||||
|         throw new Error("Selector not supported in initial form data"); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|   | ||||
| @@ -1,29 +1,13 @@ | ||||
| import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base"; | ||||
| import { styles } from "@material/mwc-formfield/mwc-formfield.css"; | ||||
| import { css, html } from "lit"; | ||||
| import { css } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
|  | ||||
| @customElement("ha-formfield") | ||||
| export class HaFormfield extends FormfieldBase { | ||||
|   @property({ type: Boolean, reflect: true }) public disabled = false; | ||||
|  | ||||
|   protected override render() { | ||||
|     const classes = { | ||||
|       "mdc-form-field--align-end": this.alignEnd, | ||||
|       "mdc-form-field--space-between": this.spaceBetween, | ||||
|       "mdc-form-field--nowrap": this.nowrap, | ||||
|     }; | ||||
|  | ||||
|     return html` <div class="mdc-form-field ${classMap(classes)}"> | ||||
|       <slot></slot> | ||||
|       <label class="mdc-label" @click=${this._labelClick} | ||||
|         ><slot name="label">${this.label}</slot></label | ||||
|       > | ||||
|     </div>`; | ||||
|   } | ||||
|  | ||||
|   protected _labelClick() { | ||||
|     const input = this.input as HTMLInputElement | undefined; | ||||
|     if (!input) return; | ||||
| @@ -55,9 +39,6 @@ export class HaFormfield extends FormfieldBase { | ||||
|         margin-inline-end: 10px; | ||||
|         margin-inline-start: inline; | ||||
|       } | ||||
|       .mdc-form-field { | ||||
|         align-items: var(--ha-formfield-align-items, center); | ||||
|       } | ||||
|       .mdc-form-field > label { | ||||
|         direction: var(--direction); | ||||
|         margin-inline-start: 0; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | ||||
| import { | ||||
|   ComboBoxDataProviderCallback, | ||||
| @@ -10,7 +11,6 @@ import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { customIcons } from "../data/custom_icons"; | ||||
| import { HomeAssistant, ValueChangedEvent } from "../types"; | ||||
| import "./ha-combo-box"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-icon"; | ||||
|  | ||||
| type IconItem = { | ||||
| @@ -67,10 +67,10 @@ const loadCustomIconItems = async (iconsetPrefix: string) => { | ||||
| }; | ||||
|  | ||||
| const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => | ||||
|   html`<ha-list-item graphic="avatar"> | ||||
|   html`<mwc-list-item graphic="avatar"> | ||||
|     <ha-icon .icon=${item.icon} slot="graphic"></ha-icon> | ||||
|     ${item.icon} | ||||
|   </ha-list-item>`; | ||||
|   </mwc-list-item>`; | ||||
|  | ||||
| @customElement("ha-icon-picker") | ||||
| export class HaIconPicker extends LitElement { | ||||
| @@ -198,7 +198,8 @@ export class HaIconPicker extends LitElement { | ||||
|  | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       *[slot="icon"] { | ||||
|       ha-icon, | ||||
|       ha-svg-icon { | ||||
|         color: var(--primary-text-color); | ||||
|         position: relative; | ||||
|         bottom: 2px; | ||||
|   | ||||
| @@ -302,7 +302,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { | ||||
|             name: this.hass.localize("ui.components.label-picker.no_match"), | ||||
|             icon: null, | ||||
|             color: null, | ||||
|             description: null, | ||||
|           }, | ||||
|         ]; | ||||
|       } | ||||
| @@ -316,7 +315,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { | ||||
|               name: this.hass.localize("ui.components.label-picker.add_new"), | ||||
|               icon: "mdi:plus", | ||||
|               color: null, | ||||
|               description: null, | ||||
|             }, | ||||
|           ]; | ||||
|     } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import "@material/web/ripple/ripple"; | ||||
|  | ||||
| @customElement("ha-label") | ||||
| class HaLabel extends LitElement { | ||||
| @@ -10,6 +11,7 @@ class HaLabel extends LitElement { | ||||
|       <span class="content"> | ||||
|         <slot name="icon"></slot> | ||||
|         <slot></slot> | ||||
|         <md-ripple></md-ripple> | ||||
|       </span> | ||||
|     `; | ||||
|   } | ||||
| @@ -25,6 +27,7 @@ class HaLabel extends LitElement { | ||||
|             0.15 | ||||
|           ); | ||||
|           --ha-label-background-opacity: 1; | ||||
|  | ||||
|           position: relative; | ||||
|           box-sizing: border-box; | ||||
|           display: inline-flex; | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { stopPropagation } from "../common/dom/stop_propagation"; | ||||
| import { formatLanguageCode } from "../common/language/format_language"; | ||||
| import { caseInsensitiveStringCompare } from "../common/string/compare"; | ||||
| import { FrontendLocaleData } from "../data/translation"; | ||||
| import "../resources/intl-polyfill"; | ||||
| import { translationMetadata } from "../resources/translations-metadata"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import "./ha-list-item"; | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user