mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-25 20:12:48 +00:00
Compare commits
58 Commits
20210930.0
...
ha-alert-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
605b43aa3a | ||
|
|
d3e2cc9128 | ||
|
|
b766f2f337 | ||
|
|
843aa3acb0 | ||
|
|
6f6fc759cc | ||
|
|
4358b7f924 | ||
|
|
2841369d3d | ||
|
|
ad031d4bda | ||
|
|
588ee2c3b1 | ||
|
|
038033cf27 | ||
|
|
84c4bbd380 | ||
|
|
807ce468d6 | ||
|
|
a839494a1e | ||
|
|
fa52442c1c | ||
|
|
919ce2afb1 | ||
|
|
db55be6d33 | ||
|
|
2dc7c1afed | ||
|
|
85956dc7fd | ||
|
|
910cd98a38 | ||
|
|
8022bd2868 | ||
|
|
d5ca7e1719 | ||
|
|
066a0771b3 | ||
|
|
9e35c1ab68 | ||
|
|
fb1deb838c | ||
|
|
8e010618bb | ||
|
|
365cf1f7ef | ||
|
|
b226b20e3d | ||
|
|
ec21f4c2c6 | ||
|
|
a696d849b2 | ||
|
|
ea3fae2ce4 | ||
|
|
2fb3ac74eb | ||
|
|
2d5c8ec3e9 | ||
|
|
25c1156c88 | ||
|
|
c44624282c | ||
|
|
370f2eb9e4 | ||
|
|
1793c68aae | ||
|
|
cba6bbdc74 | ||
|
|
6f4593508b | ||
|
|
dc3bad56f2 | ||
|
|
784e5e6e39 | ||
|
|
13fe62975d | ||
|
|
b97fd9918a | ||
|
|
dc56c2de52 | ||
|
|
375a5323d5 | ||
|
|
8e3011807d | ||
|
|
ec7c6ab96c | ||
|
|
8a4097a366 | ||
|
|
792a736e48 | ||
|
|
cce0a02ebb | ||
|
|
2ddab4eecc | ||
|
|
f66755cbf1 | ||
|
|
257e60a2b1 | ||
|
|
75a3566760 | ||
|
|
7a9f17e059 | ||
|
|
abbfe7200a | ||
|
|
419942112b | ||
|
|
597d4a0426 | ||
|
|
e023d60be7 |
@@ -29,6 +29,7 @@
|
||||
"__BUILD__": false,
|
||||
"__VERSION__": false,
|
||||
"__STATIC_PATH__": false,
|
||||
"__SUPERVISOR__": false,
|
||||
"Polymer": true
|
||||
},
|
||||
"env": {
|
||||
@@ -111,8 +112,7 @@
|
||||
],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"lit/attribute-value-entities": "off",
|
||||
"lit/no-template-map": "off",
|
||||
"lit/no-template-arrow": "warn"
|
||||
"lit/no-template-map": "off"
|
||||
},
|
||||
"plugins": ["disable", "unused-imports"],
|
||||
"processor": "disable/disable"
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations gather-gallery-demos
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-demos
|
||||
- name: Run eslint
|
||||
run: yarn run lint:eslint
|
||||
- name: Run tsc
|
||||
@@ -53,6 +53,8 @@ jobs:
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp build-translations build-locale-data
|
||||
- name: Run Tests
|
||||
run: yarn run test
|
||||
build:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,7 +3,6 @@
|
||||
|
||||
# build
|
||||
build
|
||||
build-translations/*
|
||||
hass_frontend/*
|
||||
dist
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
build
|
||||
build-translations/*
|
||||
translations/*
|
||||
node_modules/*
|
||||
hass_frontend/*
|
||||
|
||||
@@ -35,6 +35,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__VERSION__: JSON.stringify(env.version()),
|
||||
__DEMO__: false,
|
||||
__SUPERVISOR__: false,
|
||||
__BACKWARDS_COMPAT__: false,
|
||||
__STATIC_PATH__: "/static/",
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
@@ -194,6 +195,9 @@ module.exports.config = {
|
||||
publicPath: publicPath(latestBuild, paths.hassio_publicPath),
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
defineOverlay: {
|
||||
__SUPERVISOR__: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -206,6 +210,9 @@ module.exports.config = {
|
||||
publicPath: publicPath(latestBuild),
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
defineOverlay: {
|
||||
__DEMO__: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ const env = require("../env");
|
||||
|
||||
require("./clean.js");
|
||||
require("./translations.js");
|
||||
require("./locale-data.js");
|
||||
require("./gen-icons-json.js");
|
||||
require("./gather-static.js");
|
||||
require("./compress.js");
|
||||
@@ -26,7 +27,8 @@ gulp.task(
|
||||
"gen-icons-json",
|
||||
"gen-pages-dev",
|
||||
"gen-index-app-dev",
|
||||
"build-translations"
|
||||
"build-translations",
|
||||
"build-locale-data"
|
||||
),
|
||||
"copy-static-app",
|
||||
env.useWDS()
|
||||
@@ -44,7 +46,7 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean",
|
||||
gulp.parallel("gen-icons-json", "build-translations"),
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-app",
|
||||
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
|
||||
// Don't compress running tests
|
||||
|
||||
@@ -18,7 +18,7 @@ gulp.task(
|
||||
},
|
||||
"clean-cast",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations"),
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-cast",
|
||||
"gen-index-cast-dev",
|
||||
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
|
||||
@@ -33,7 +33,7 @@ gulp.task(
|
||||
},
|
||||
"clean-cast",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations"),
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-cast",
|
||||
env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast",
|
||||
"gen-index-cast-prod"
|
||||
|
||||
@@ -20,7 +20,12 @@ gulp.task(
|
||||
},
|
||||
"clean-demo",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "gen-index-demo-dev", "build-translations"),
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
"gen-index-demo-dev",
|
||||
"build-translations",
|
||||
"build-locale-data"
|
||||
),
|
||||
"copy-static-demo",
|
||||
env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo"
|
||||
)
|
||||
@@ -35,7 +40,7 @@ gulp.task(
|
||||
"clean-demo",
|
||||
// Cast needs to be backwards compatible and older HA has no translations
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations"),
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-demo",
|
||||
env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo",
|
||||
"gen-index-demo-prod"
|
||||
|
||||
@@ -51,6 +51,7 @@ gulp.task(
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
"build-translations",
|
||||
"build-locale-data",
|
||||
"gather-gallery-demos"
|
||||
),
|
||||
"copy-static-gallery",
|
||||
@@ -70,6 +71,7 @@ gulp.task(
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
"build-translations",
|
||||
"build-locale-data",
|
||||
"gather-gallery-demos"
|
||||
),
|
||||
"copy-static-gallery",
|
||||
|
||||
@@ -22,11 +22,18 @@ function copyTranslations(staticDir) {
|
||||
|
||||
// Translation output
|
||||
fs.copySync(
|
||||
polyPath("build-translations/output"),
|
||||
polyPath("build/translations/output"),
|
||||
staticPath("translations")
|
||||
);
|
||||
}
|
||||
|
||||
function copyLocaleData(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
// Locale data output
|
||||
fs.copySync(polyPath("build/locale-data"), staticPath("locale-data"));
|
||||
}
|
||||
|
||||
function copyMdiIcons(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
|
||||
@@ -84,6 +91,11 @@ function copyMapPanel(staticDir) {
|
||||
);
|
||||
}
|
||||
|
||||
gulp.task("copy-locale-data", async () => {
|
||||
const staticDir = paths.app_output_static;
|
||||
copyLocaleData(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-translations-app", async () => {
|
||||
const staticDir = paths.app_output_static;
|
||||
copyTranslations(staticDir);
|
||||
@@ -94,6 +106,11 @@ gulp.task("copy-translations-supervisor", async () => {
|
||||
copyTranslations(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-locale-data-supervisor", async () => {
|
||||
const staticDir = paths.hassio_output_static;
|
||||
copyLocaleData(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-app", async () => {
|
||||
const staticDir = paths.app_output_static;
|
||||
// Basic static files
|
||||
@@ -103,6 +120,7 @@ gulp.task("copy-static-app", async () => {
|
||||
copyPolyfills(staticDir);
|
||||
copyFonts(staticDir);
|
||||
copyTranslations(staticDir);
|
||||
copyLocaleData(staticDir);
|
||||
copyMdiIcons(staticDir);
|
||||
|
||||
// Panel assets
|
||||
@@ -123,6 +141,7 @@ gulp.task("copy-static-demo", async () => {
|
||||
copyMapPanel(paths.demo_output_static);
|
||||
copyFonts(paths.demo_output_static);
|
||||
copyTranslations(paths.demo_output_static);
|
||||
copyLocaleData(paths.demo_output_static);
|
||||
copyMdiIcons(paths.demo_output_static);
|
||||
});
|
||||
|
||||
@@ -137,6 +156,7 @@ gulp.task("copy-static-cast", async () => {
|
||||
copyMapPanel(paths.cast_output_static);
|
||||
copyFonts(paths.cast_output_static);
|
||||
copyTranslations(paths.cast_output_static);
|
||||
copyLocaleData(paths.cast_output_static);
|
||||
copyMdiIcons(paths.cast_output_static);
|
||||
});
|
||||
|
||||
@@ -152,5 +172,6 @@ gulp.task("copy-static-gallery", async () => {
|
||||
copyMapPanel(paths.gallery_output_static);
|
||||
copyFonts(paths.gallery_output_static);
|
||||
copyTranslations(paths.gallery_output_static);
|
||||
copyLocaleData(paths.gallery_output_static);
|
||||
copyMdiIcons(paths.gallery_output_static);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const env = require("../env");
|
||||
const paths = require("../paths");
|
||||
|
||||
require("./clean.js");
|
||||
require("./gen-icons-json.js");
|
||||
@@ -20,10 +17,11 @@ gulp.task(
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-icons-json",
|
||||
"gen-index-hassio-dev",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
"copy-locale-data-supervisor",
|
||||
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
|
||||
)
|
||||
);
|
||||
@@ -35,9 +33,10 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-icons-json",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
"copy-locale-data-supervisor",
|
||||
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
|
||||
"gen-index-hassio-prod",
|
||||
...// Don't compress running tests
|
||||
|
||||
74
build-scripts/gulp/locale-data.js
Executable file
74
build-scripts/gulp/locale-data.js
Executable file
@@ -0,0 +1,74 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const paths = require("../paths");
|
||||
|
||||
const outDir = "build/locale-data";
|
||||
|
||||
gulp.task("clean-locale-data", () => del([outDir]));
|
||||
|
||||
gulp.task("ensure-locale-data-build-dir", (done) => {
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
const modules = {
|
||||
"intl-relativetimeformat": "RelativeTimeFormat",
|
||||
"intl-datetimeformat": "DateTimeFormat",
|
||||
"intl-numberformat": "NumberFormat",
|
||||
};
|
||||
|
||||
gulp.task("create-locale-data", (done) => {
|
||||
const translationMeta = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(paths.translations_src, "translationMetadata.json")
|
||||
)
|
||||
);
|
||||
Object.entries(modules).forEach(([module, className]) => {
|
||||
Object.keys(translationMeta).forEach((lang) => {
|
||||
try {
|
||||
const localeData = String(
|
||||
fs.readFileSync(
|
||||
require.resolve(`@formatjs/${module}/locale-data/${lang}.js`)
|
||||
)
|
||||
)
|
||||
.replace(
|
||||
new RegExp(
|
||||
`\\/\\*\\s*@generated\\s*\\*\\/\\s*\\/\\/\\s*prettier-ignore\\s*if\\s*\\(Intl\\.${className}\\s*&&\\s*typeof\\s*Intl\\.${className}\\.__addLocaleData\\s*===\\s*'function'\\)\\s*{\\s*Intl\\.${className}\\.__addLocaleData\\(`,
|
||||
"im"
|
||||
),
|
||||
""
|
||||
)
|
||||
.replace(/\)\s*}/im, "");
|
||||
// make sure we have valid JSON
|
||||
JSON.parse(localeData);
|
||||
if (!fs.existsSync(path.join(outDir, module))) {
|
||||
fs.mkdirSync(path.join(outDir, module), { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, `${module}/${lang}.json`),
|
||||
localeData
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.code !== "MODULE_NOT_FOUND") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"build-locale-data",
|
||||
gulp.series(
|
||||
"clean-locale-data",
|
||||
"ensure-locale-data-build-dir",
|
||||
"create-locale-data"
|
||||
)
|
||||
);
|
||||
@@ -17,7 +17,7 @@ const paths = require("../paths");
|
||||
|
||||
const inFrontendDir = "translations/frontend";
|
||||
const inBackendDir = "translations/backend";
|
||||
const workDir = "build-translations";
|
||||
const workDir = "build/translations";
|
||||
const fullDir = workDir + "/full";
|
||||
const coreDir = workDir + "/core";
|
||||
const outDir = workDir + "/output";
|
||||
@@ -121,7 +121,7 @@ gulp.task("clean-translations", () => del([workDir]));
|
||||
|
||||
gulp.task("ensure-translations-build-dir", (done) => {
|
||||
if (!fs.existsSync(workDir)) {
|
||||
fs.mkdirSync(workDir);
|
||||
fs.mkdirSync(workDir, { recursive: true });
|
||||
}
|
||||
done();
|
||||
});
|
||||
@@ -336,6 +336,14 @@ 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"))
|
||||
);
|
||||
|
||||
|
||||
@@ -35,26 +35,29 @@ const isWsl =
|
||||
* listenHost?: string
|
||||
* }}
|
||||
*/
|
||||
const runDevServer = ({
|
||||
const runDevServer = async ({
|
||||
compiler,
|
||||
contentBase,
|
||||
port,
|
||||
listenHost = "localhost",
|
||||
}) =>
|
||||
new WebpackDevServer(compiler, {
|
||||
open: true,
|
||||
watchContentBase: true,
|
||||
contentBase,
|
||||
}).listen(port, listenHost, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// Server listening
|
||||
log(
|
||||
"[webpack-dev-server]",
|
||||
`Project is running at http://localhost:${port}`
|
||||
);
|
||||
});
|
||||
}) => {
|
||||
const server = new WebpackDevServer(
|
||||
{
|
||||
open: true,
|
||||
host: listenHost,
|
||||
port,
|
||||
static: {
|
||||
directory: contentBase,
|
||||
watch: true,
|
||||
},
|
||||
},
|
||||
compiler
|
||||
);
|
||||
|
||||
await server.start();
|
||||
// Server listening
|
||||
log("[webpack-dev-server]", `Project is running at http://localhost:${port}`);
|
||||
};
|
||||
|
||||
const doneHandler = (done) => (err, stats) => {
|
||||
if (err) {
|
||||
@@ -107,13 +110,13 @@ gulp.task("webpack-prod-app", () =>
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-demo", () => {
|
||||
gulp.task("webpack-dev-server-demo", () =>
|
||||
runDevServer({
|
||||
compiler: webpack(bothBuilds(createDemoConfig, { isProdBuild: false })),
|
||||
contentBase: paths.demo_output_root,
|
||||
port: 8090,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task("webpack-prod-demo", () =>
|
||||
prodBuild(
|
||||
@@ -123,15 +126,15 @@ gulp.task("webpack-prod-demo", () =>
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-cast", () => {
|
||||
gulp.task("webpack-dev-server-cast", () =>
|
||||
runDevServer({
|
||||
compiler: webpack(bothBuilds(createCastConfig, { isProdBuild: false })),
|
||||
contentBase: paths.cast_output_root,
|
||||
port: 8080,
|
||||
// Accessible from the network, because that's how Cast hits it.
|
||||
listenHost: "0.0.0.0",
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task("webpack-prod-cast", () =>
|
||||
prodBuild(
|
||||
@@ -148,7 +151,7 @@ gulp.task("webpack-watch-hassio", () => {
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
})
|
||||
).watch({ ignored: /build-translations/, poll: isWsl }, doneHandler());
|
||||
).watch({ ignored: /build/, poll: isWsl }, doneHandler());
|
||||
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
@@ -164,14 +167,15 @@ gulp.task("webpack-prod-hassio", () =>
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-dev-server-gallery", () => {
|
||||
gulp.task("webpack-dev-server-gallery", () =>
|
||||
runDevServer({
|
||||
// We don't use the es5 build, but the dev server will fuck up the publicPath if we don't
|
||||
compiler: webpack(bothBuilds(createGalleryConfig, { isProdBuild: false })),
|
||||
contentBase: paths.gallery_output_root,
|
||||
port: 8100,
|
||||
});
|
||||
});
|
||||
listenHost: "0.0.0.0",
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task("webpack-prod-gallery", () =>
|
||||
prodBuild(
|
||||
|
||||
7
demo/src/stubs/area_registry.ts
Normal file
7
demo/src/stubs/area_registry.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AreaRegistryEntry } from "../../../src/data/area_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAreaRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: AreaRegistryEntry[] = []
|
||||
) => hass.mockWS("config/area_registry/list", () => data);
|
||||
7
demo/src/stubs/device_registry.ts
Normal file
7
demo/src/stubs/device_registry.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { DeviceRegistryEntry } from "../../../src/data/device_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockDeviceRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: DeviceRegistryEntry[] = []
|
||||
) => hass.mockWS("config/device_registry/list", () => data);
|
||||
7
demo/src/stubs/entity_registry.ts
Normal file
7
demo/src/stubs/entity_registry.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { EntityRegistryEntry } from "../../../src/data/entity_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockEntityRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: EntityRegistryEntry[] = []
|
||||
) => hass.mockWS("config/entity_registry/list", () => data);
|
||||
59
demo/src/stubs/hassio_supervisor.ts
Normal file
59
demo/src/stubs/hassio_supervisor.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockHassioSupervisor = (hass: MockHomeAssistant) => {
|
||||
hass.config.components.push("hassio");
|
||||
hass.mockWS("supervisor/api", (msg) => {
|
||||
if (msg.endpoint === "/supervisor/info") {
|
||||
const data: HassioSupervisorInfo = {
|
||||
version: "2021.10.dev0805",
|
||||
version_latest: "2021.10.dev0806",
|
||||
update_available: true,
|
||||
channel: "dev",
|
||||
arch: "aarch64",
|
||||
supported: true,
|
||||
healthy: true,
|
||||
ip_address: "172.30.32.2",
|
||||
wait_boot: 5,
|
||||
timezone: "America/Los_Angeles",
|
||||
logging: "info",
|
||||
debug: false,
|
||||
debug_block: false,
|
||||
diagnostics: true,
|
||||
addons: [
|
||||
{
|
||||
name: "Visual Studio Code",
|
||||
slug: "a0d7b954_vscode",
|
||||
description:
|
||||
"Fully featured VSCode experience, to edit your HA config in the browser, including auto-completion!",
|
||||
state: "started",
|
||||
version: "3.6.2",
|
||||
version_latest: "3.6.2",
|
||||
update_available: false,
|
||||
repository: "a0d7b954",
|
||||
icon: true,
|
||||
logo: true,
|
||||
},
|
||||
{
|
||||
name: "Z-Wave JS",
|
||||
slug: "core_zwave_js",
|
||||
description:
|
||||
"Control a ZWave network with Home Assistant Z-Wave JS",
|
||||
state: "started",
|
||||
version: "0.1.45",
|
||||
version_latest: "0.1.45",
|
||||
update_available: false,
|
||||
repository: "core",
|
||||
icon: true,
|
||||
logo: true,
|
||||
},
|
||||
] as any,
|
||||
addons_repositories: [
|
||||
"https://github.com/hassio-addons/repository",
|
||||
] as any,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
return Promise.reject(`${msg.method} ${msg.endpoint} is not implemented`);
|
||||
});
|
||||
};
|
||||
122
gallery/src/components/demo-black-white-row.ts
Normal file
122
gallery/src/components/demo-black-white-row.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { html, LitElement, css, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
|
||||
@customElement("demo-black-white-row")
|
||||
class DemoBlackWhiteRow extends LitElement {
|
||||
@property() title!: string;
|
||||
|
||||
@property() value!: any;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="row">
|
||||
<div class="content light">
|
||||
<ha-card .header=${this.title}>
|
||||
<div class="card-content">
|
||||
<slot name="light"></slot>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button>Submit</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
<div class="content dark">
|
||||
<ha-card .header=${this.title}>
|
||||
<div class="card-content">
|
||||
<slot name="dark"></slot>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button>Submit</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<pre>${JSON.stringify(this.value, undefined, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.row {
|
||||
display: flex;
|
||||
}
|
||||
.content {
|
||||
padding: 50px 0;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
.light {
|
||||
flex: 1;
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.light ha-card {
|
||||
margin-left: auto;
|
||||
}
|
||||
.dark {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding-left: 50px;
|
||||
box-sizing: border-box;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
ha-card {
|
||||
width: 400px;
|
||||
}
|
||||
pre {
|
||||
width: 300px;
|
||||
margin: 0 16px 0;
|
||||
overflow: auto;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
border-top: none;
|
||||
}
|
||||
@media only screen and (max-width: 1500px) {
|
||||
.light {
|
||||
flex: initial;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.light,
|
||||
.dark {
|
||||
padding: 16px;
|
||||
}
|
||||
.row,
|
||||
.dark {
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-card {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
pre {
|
||||
margin: 16px auto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-black-white-row": DemoBlackWhiteRow;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import { html, css, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../src/components/ha-card";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import { html, css, LitElement, TemplateResult } from "lit";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/trace/hat-script-graph";
|
||||
|
||||
@@ -107,19 +107,21 @@ export class DemoHaAlert extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="ha-alert demo">
|
||||
${alerts.map(
|
||||
(alert) => html`
|
||||
<ha-alert
|
||||
.title=${alert.title || ""}
|
||||
.alertType=${alert.type}
|
||||
.dismissable=${alert.dismissable || false}
|
||||
.actionText=${alert.action || ""}
|
||||
.rtl=${alert.rtl || false}
|
||||
>
|
||||
${alert.description}
|
||||
</ha-alert>
|
||||
`
|
||||
)}
|
||||
<div class="card-content">
|
||||
${alerts.map(
|
||||
(alert) => html`
|
||||
<ha-alert
|
||||
.title=${alert.title || ""}
|
||||
.alertType=${alert.type}
|
||||
.dismissable=${alert.dismissable || false}
|
||||
.actionText=${alert.action || ""}
|
||||
.rtl=${alert.rtl || false}
|
||||
>
|
||||
${alert.description}
|
||||
</ha-alert>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@@ -130,6 +132,10 @@ export class DemoHaAlert extends LitElement {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin: 24px 0;
|
||||
}
|
||||
.condition {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
|
||||
267
gallery/src/demos/demo-ha-form.ts
Normal file
267
gallery/src/demos/demo-ha-form.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import "@material/mwc-button";
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { computeInitialHaFormData } from "../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import type { HaFormSchema } from "../../../src/components/ha-form/types";
|
||||
import "../../../src/components/ha-form/ha-form";
|
||||
import "../components/demo-black-white-row";
|
||||
|
||||
const SCHEMAS: {
|
||||
title: string;
|
||||
translations?: Record<string, string>;
|
||||
error?: Record<string, string>;
|
||||
schema: HaFormSchema[];
|
||||
data?: Record<string, any>;
|
||||
}[] = [
|
||||
{
|
||||
title: "Authentication",
|
||||
translations: {
|
||||
username: "Username",
|
||||
password: "Password",
|
||||
invalid_login: "Invalid username or password",
|
||||
},
|
||||
error: {
|
||||
base: "invalid_login",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "string",
|
||||
name: "username",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
name: "password",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: "One of each",
|
||||
schema: [
|
||||
{
|
||||
type: "constant",
|
||||
value: "Constant Value",
|
||||
name: "constant",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: "boolean",
|
||||
name: "bool",
|
||||
optional: true,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
type: "integer",
|
||||
name: "int",
|
||||
optional: true,
|
||||
default: 10,
|
||||
},
|
||||
{
|
||||
type: "float",
|
||||
name: "float",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
name: "string",
|
||||
optional: true,
|
||||
default: "Default",
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
options: [
|
||||
["default", "default"],
|
||||
["other", "other"],
|
||||
],
|
||||
name: "select",
|
||||
optional: true,
|
||||
default: "default",
|
||||
},
|
||||
{
|
||||
type: "multi_select",
|
||||
options: {
|
||||
default: "Default",
|
||||
other: "Other",
|
||||
},
|
||||
name: "multi",
|
||||
optional: true,
|
||||
default: ["default"],
|
||||
},
|
||||
{
|
||||
type: "positive_time_period_dict",
|
||||
name: "time",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Numbers",
|
||||
schema: [
|
||||
{
|
||||
type: "integer",
|
||||
name: "int",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: "integer",
|
||||
name: "int with default",
|
||||
optional: true,
|
||||
default: 10,
|
||||
},
|
||||
{
|
||||
type: "integer",
|
||||
name: "int range required",
|
||||
required: true,
|
||||
default: 5,
|
||||
valueMin: 0,
|
||||
valueMax: 10,
|
||||
},
|
||||
{
|
||||
type: "integer",
|
||||
name: "int range optional",
|
||||
optional: true,
|
||||
valueMin: 0,
|
||||
valueMax: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "select",
|
||||
schema: [
|
||||
{
|
||||
type: "select",
|
||||
options: [
|
||||
["default", "Default"],
|
||||
["other", "Other"],
|
||||
],
|
||||
name: "select",
|
||||
required: true,
|
||||
default: "default",
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
options: [
|
||||
["default", "Default"],
|
||||
["other", "Other"],
|
||||
],
|
||||
name: "select optional",
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
options: [
|
||||
["default", "Default"],
|
||||
["other", "Other"],
|
||||
["uno", "mas"],
|
||||
["one", "more"],
|
||||
["and", "another_one"],
|
||||
["option", "1000"],
|
||||
],
|
||||
name: "select many otions",
|
||||
optional: true,
|
||||
default: "default",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Multi select",
|
||||
schema: [
|
||||
{
|
||||
type: "multi_select",
|
||||
options: {
|
||||
default: "Default",
|
||||
other: "Other",
|
||||
},
|
||||
name: "multi",
|
||||
required: true,
|
||||
default: ["default"],
|
||||
},
|
||||
{
|
||||
type: "multi_select",
|
||||
options: {
|
||||
default: "Default",
|
||||
other: "Other",
|
||||
uno: "mas",
|
||||
one: "more",
|
||||
and: "another_one",
|
||||
option: "1000",
|
||||
},
|
||||
name: "multi many otions",
|
||||
optional: true,
|
||||
default: ["default"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Field specific error",
|
||||
data: {
|
||||
new_password: "hello",
|
||||
new_password_2: "bye",
|
||||
},
|
||||
translations: {
|
||||
new_password: "New Password",
|
||||
new_password_2: "Re-type Password",
|
||||
not_match: "The passwords do not match",
|
||||
},
|
||||
error: {
|
||||
new_password_2: "not_match",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "string",
|
||||
name: "new_password",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
name: "new_password_2",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-ha-form")
|
||||
class DemoHaForm extends LitElement {
|
||||
private data = SCHEMAS.map(
|
||||
({ schema, data }) => data || computeInitialHaFormData(schema)
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${SCHEMAS.map((info, idx) => {
|
||||
const translations = info.translations || {};
|
||||
return html`
|
||||
<demo-black-white-row .title=${info.title} .value=${this.data[idx]}>
|
||||
${["light", "dark"].map(
|
||||
(slot) => html`
|
||||
<ha-form
|
||||
slot=${slot}
|
||||
.data=${this.data[idx]}
|
||||
.schema=${info.schema}
|
||||
.error=${info.error}
|
||||
.computeError=${(error) => translations[error] || error}
|
||||
.computeLabel=${(schema) =>
|
||||
translations[schema.name] || schema.name}
|
||||
@value-changed=${(e) => {
|
||||
this.data[idx] = e.detail.value;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
></ha-form>
|
||||
`
|
||||
)}
|
||||
</demo-black-white-row>
|
||||
`;
|
||||
})}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-ha-form": DemoHaForm;
|
||||
}
|
||||
}
|
||||
131
gallery/src/demos/demo-ha-selector.ts
Normal file
131
gallery/src/demos/demo-ha-selector.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import "@material/mwc-button";
|
||||
import { LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import "../components/demo-black-white-row";
|
||||
import { BlueprintInput } from "../../../src/data/blueprint";
|
||||
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
|
||||
|
||||
const SCHEMAS: {
|
||||
name: string;
|
||||
input: Record<string, BlueprintInput | null>;
|
||||
}[] = [
|
||||
{
|
||||
name: "One of each",
|
||||
input: {
|
||||
entity: { name: "Entity", selector: { entity: {} } },
|
||||
device: { name: "Device", selector: { device: {} } },
|
||||
addon: { name: "Addon", selector: { addon: {} } },
|
||||
area: { name: "Area", selector: { area: {} } },
|
||||
target: { name: "Target", selector: { target: {} } },
|
||||
number_box: {
|
||||
name: "Number Box",
|
||||
selector: {
|
||||
number: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
mode: "box",
|
||||
},
|
||||
},
|
||||
},
|
||||
number_slider: {
|
||||
name: "Number Slider",
|
||||
selector: {
|
||||
number: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
mode: "slider",
|
||||
},
|
||||
},
|
||||
},
|
||||
boolean: { name: "Boolean", selector: { boolean: {} } },
|
||||
time: { name: "Time", selector: { time: {} } },
|
||||
action: { name: "Action", selector: { action: {} } },
|
||||
text: { name: "Text", selector: { text: { multiline: false } } },
|
||||
text_multiline: {
|
||||
name: "Text multiline",
|
||||
selector: { text: { multiline: true } },
|
||||
},
|
||||
object: { name: "Object", selector: { object: {} } },
|
||||
select: {
|
||||
name: "Select",
|
||||
selector: { select: { options: ["Option 1", "Option 2"] } },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-ha-selector")
|
||||
class DemoHaSelector extends LitElement {
|
||||
@state() private hass!: HomeAssistant;
|
||||
|
||||
private data = SCHEMAS.map(() => ({}));
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("config", "en");
|
||||
mockEntityRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${SCHEMAS.map((info, idx) => {
|
||||
const data = this.data[idx];
|
||||
const valueChanged = (ev) => {
|
||||
this.data[idx] = {
|
||||
...data,
|
||||
[ev.target.key]: ev.detail.value,
|
||||
};
|
||||
this.requestUpdate();
|
||||
};
|
||||
return html`
|
||||
<demo-black-white-row .title=${info.name} .value=${this.data[idx]}>
|
||||
${["light", "dark"].map((slot) =>
|
||||
Object.entries(info.input).map(
|
||||
([key, value]) =>
|
||||
html`
|
||||
<ha-settings-row narrow slot=${slot}>
|
||||
<span slot="heading">${value?.name || key}</span>
|
||||
<span slot="description">${value?.description}</span>
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${value!.selector}
|
||||
.key=${key}
|
||||
.value=${data[key] ?? value!.default}
|
||||
@value-changed=${valueChanged}
|
||||
></ha-selector>
|
||||
</ha-settings-row>
|
||||
`
|
||||
)
|
||||
)}
|
||||
</demo-black-white-row>
|
||||
`;
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
paper-input,
|
||||
ha-selector {
|
||||
width: 60;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-ha-selector": DemoHaSelector;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
|
||||
import "../../../src/components/ha-card";
|
||||
import {
|
||||
HassioAddonInfo,
|
||||
@@ -32,7 +33,7 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
return filterAndSort(addons, filter);
|
||||
}
|
||||
return addons.sort((a, b) =>
|
||||
a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1
|
||||
caseInsensitiveStringCompare(a.name, b.name)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -55,7 +55,9 @@ class HassioAddonAudio extends LitElement {
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
|
||||
<paper-dropdown-menu
|
||||
|
||||
@@ -19,7 +19,7 @@ import "../../../../src/components/ha-button-menu";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-switch";
|
||||
import "../../../../src/components/ha-yaml-editor";
|
||||
@@ -137,14 +137,16 @@ class HassioAddonConfig extends LitElement {
|
||||
.yamlSchema=${ADDON_YAML_SCHEMA}
|
||||
></ha-yaml-editor>`}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${!this._yamlMode ||
|
||||
(this._canShowSchema && this.addon.schema) ||
|
||||
this._valid
|
||||
? ""
|
||||
: html`
|
||||
<ha-alert alert-type="error">
|
||||
<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this.supervisor.localize(
|
||||
"addon.configuration.options.invalid_yaml"
|
||||
)}
|
||||
|
||||
@@ -64,7 +64,9 @@ class HassioAddonNetwork extends LitElement {
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
|
||||
<table>
|
||||
|
||||
@@ -40,7 +40,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<div class="card-content">
|
||||
${this._content
|
||||
|
||||
@@ -144,14 +144,14 @@ class HassioAddonInfo extends LitElement {
|
||||
this.addon.arch
|
||||
)
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
<ha-alert .hass=${this.hass} alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: html`
|
||||
<ha-alert alert-type="warning">
|
||||
<ha-alert .hass=${this.hass} alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch",
|
||||
"core_version_installed",
|
||||
@@ -297,10 +297,11 @@ class HassioAddonInfo extends LitElement {
|
||||
})}
|
||||
@click=${this._showMoreInfo}
|
||||
id="rating"
|
||||
.value=${this.addon.rating}
|
||||
label="rating"
|
||||
description=""
|
||||
></ha-label-badge>
|
||||
>
|
||||
${this.addon.rating}
|
||||
</ha-label-badge>
|
||||
${this.addon.host_network
|
||||
? html`
|
||||
<ha-label-badge
|
||||
@@ -571,7 +572,9 @@ class HassioAddonInfo extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${!this.addon.version && addonStoreInfo && !this.addon.available
|
||||
? !addonArchIsSupported(
|
||||
@@ -579,14 +582,14 @@ class HassioAddonInfo extends LitElement {
|
||||
this.addon.arch
|
||||
)
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
<ha-alert .hass=${this.hass} alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: html`
|
||||
<ha-alert alert-type="warning">
|
||||
<ha-alert .hass=${this.hass} alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_version",
|
||||
"core_version_installed",
|
||||
|
||||
@@ -36,7 +36,9 @@ class HassioAddonLogs extends LitElement {
|
||||
<h1>${this.addon.name}</h1>
|
||||
<ha-card>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<div class="card-content">
|
||||
${this._content
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../src/components/ha-relative-time";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
@@ -19,8 +18,6 @@ class HassioCardContent extends LitElement {
|
||||
|
||||
@property() public topbarClass?: string;
|
||||
|
||||
@property() public datetime?: string;
|
||||
|
||||
@property() public iconTitle?: string;
|
||||
|
||||
@property() public iconClass?: string;
|
||||
@@ -56,15 +53,6 @@ class HassioCardContent extends LitElement {
|
||||
/* treat as available when undefined */
|
||||
this.available === false ? " (Not available)" : ""
|
||||
}
|
||||
${this.datetime
|
||||
? html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
class="addition"
|
||||
.datetime=${this.datetime}
|
||||
></ha-relative-time>
|
||||
`
|
||||
: undefined}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -106,9 +94,6 @@ class HassioCardContent extends LitElement {
|
||||
height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
ha-relative-time {
|
||||
display: block;
|
||||
}
|
||||
.icon_image img {
|
||||
max-height: 40px;
|
||||
max-width: 40px;
|
||||
|
||||
@@ -181,9 +181,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.homeAssistant}
|
||||
@click=${() => {
|
||||
this.homeAssistant = !this.homeAssistant;
|
||||
}}
|
||||
@click=${this.toggleHomeAssistant}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
@@ -272,6 +270,10 @@ export class SupervisorBackupContent extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private toggleHomeAssistant() {
|
||||
this.homeAssistant = !this.homeAssistant;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.partial-picker ha-formfield {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { stringCompare } from "../../../src/common/string/compare";
|
||||
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
|
||||
import "../../../src/components/ha-card";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
@@ -33,7 +33,7 @@ class HassioAddons extends LitElement {
|
||||
</ha-card>
|
||||
`
|
||||
: this.supervisor.supervisor.addons
|
||||
.sort((a, b) => stringCompare(a.name, b.name))
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
||||
.map(
|
||||
(addon) => html`
|
||||
<ha-card .addon=${addon} @click=${this._addonTapped}>
|
||||
|
||||
@@ -28,6 +28,7 @@ import "../../components/supervisor-backup-content";
|
||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||
import { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
||||
import { atLeastVersion } from "../../../../src/common/config/version";
|
||||
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
|
||||
|
||||
@customElement("dialog-hassio-backup")
|
||||
class HassioBackupDialog
|
||||
@@ -91,7 +92,9 @@ class HassioBackupDialog
|
||||
>
|
||||
</supervisor-backup-content>`}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
|
||||
<mwc-button
|
||||
@@ -107,7 +110,7 @@ class HassioBackupDialog
|
||||
fixed
|
||||
slot="primaryAction"
|
||||
@action=${this._handleMenuAction}
|
||||
@closed=${(ev: Event) => ev.stopPropagation()}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<mwc-icon-button slot="trigger" alt="menu">
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
|
||||
@@ -64,7 +64,9 @@ class HassioCreateBackupDialog extends LitElement {
|
||||
>
|
||||
</supervisor-backup-content>`}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||
${this._dialogParams.supervisor.localize("common.close")}
|
||||
|
||||
@@ -65,32 +65,21 @@ class HassioDatadiskDialog extends LitElement {
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${this.moving
|
||||
? this.dialogParams.supervisor.localize("dialog.datadisk_move.moving")
|
||||
: this.dialogParams.supervisor.localize("dialog.datadisk_move.title")}
|
||||
@closed=${this.closeDialog}
|
||||
?hideActions=${this.moving}
|
||||
>
|
||||
${this.moving
|
||||
? html`<slot name="heading">
|
||||
<h2 id="title" class="header_title">
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.moving"
|
||||
)}
|
||||
</h2>
|
||||
</slot>
|
||||
<ha-circular-progress alt="Moving" size="large" active>
|
||||
? html` <ha-circular-progress alt="Moving" size="large" active>
|
||||
</ha-circular-progress>
|
||||
<p class="progress-text">
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.moving_desc"
|
||||
)}
|
||||
</p>`
|
||||
: html`<slot name="heading">
|
||||
<h2 id="title" class="header_title">
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.title"
|
||||
)}
|
||||
</h2>
|
||||
</slot>
|
||||
${this.devices?.length
|
||||
: html` ${this.devices?.length
|
||||
? html`
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.description",
|
||||
|
||||
@@ -252,7 +252,7 @@ export class DialogHassioNetwork
|
||||
`
|
||||
: ""}
|
||||
${this._dirty
|
||||
? html`<ha-alert alert-type="warning">
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="warning">
|
||||
${this.supervisor.localize("dialog.network.warning")}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
@@ -57,7 +58,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
|
||||
repos
|
||||
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -77,7 +78,9 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
)}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
${repositories.length
|
||||
|
||||
@@ -130,7 +130,9 @@ class DialogSupervisorUpdate extends LitElement {
|
||||
)}
|
||||
</p>`}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
|
||||
@@ -184,23 +184,34 @@ class HassioHostInfo extends LitElement {
|
||||
<mwc-icon-button slot="trigger">
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item @click=${() => this._handleMenuAction("hardware")}>
|
||||
<mwc-list-item
|
||||
.action=${"hardware"}
|
||||
@click=${this._handleMenuAction}
|
||||
>
|
||||
${this.supervisor.localize("system.host.hardware")}
|
||||
</mwc-list-item>
|
||||
${this.supervisor.host.features.includes("haos")
|
||||
? html`<mwc-list-item
|
||||
@click=${() => this._handleMenuAction("import_from_usb")}
|
||||
? html`
|
||||
<mwc-list-item
|
||||
.action=${"import_from_usb"}
|
||||
@click=${this._handleMenuAction}
|
||||
>
|
||||
${this.supervisor.localize("system.host.import_from_usb")}
|
||||
</mwc-list-item>
|
||||
${this.supervisor.host.features.includes("os_agent") &&
|
||||
atLeastVersion(this.supervisor.host.agent_version, 1, 2, 0)
|
||||
? html`<mwc-list-item
|
||||
@click=${() => this._handleMenuAction("move_datadisk")}
|
||||
>
|
||||
${this.supervisor.localize("system.host.move_datadisk")}
|
||||
</mwc-list-item>`
|
||||
: ""} `
|
||||
? html`
|
||||
<mwc-list-item
|
||||
.action=${"move_datadisk"}
|
||||
@click=${this._handleMenuAction}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"system.host.move_datadisk"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
@@ -223,8 +234,8 @@ class HassioHostInfo extends LitElement {
|
||||
return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0];
|
||||
});
|
||||
|
||||
private async _handleMenuAction(action: string) {
|
||||
switch (action) {
|
||||
private async _handleMenuAction(ev) {
|
||||
switch ((ev.target as any).action) {
|
||||
case "hardware":
|
||||
await this._showHardware();
|
||||
break;
|
||||
|
||||
@@ -174,6 +174,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</ha-settings-row>`
|
||||
: ""
|
||||
: html`<ha-alert
|
||||
.hass=${this.hass}
|
||||
alert-type="warning"
|
||||
.actionText=${this.supervisor.localize("common.learn_more")}
|
||||
@alert-action-clicked=${this._unsupportedDialog}
|
||||
@@ -184,6 +185,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</ha-alert>`}
|
||||
${!this.supervisor.supervisor.healthy
|
||||
? html`<ha-alert
|
||||
.hass=${this.hass}
|
||||
alert-type="error"
|
||||
.actionText=${this.supervisor.localize("common.learn_more")}
|
||||
@alert-action-clicked=${this._unhealthyDialog}
|
||||
|
||||
@@ -69,7 +69,9 @@ class HassioSupervisorLog extends LitElement {
|
||||
return html`
|
||||
<ha-card>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
? html`<ha-alert .hass=${this.hass} alert-type="error">
|
||||
${this._error}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${this.hass.userData?.showAdvanced
|
||||
? html`
|
||||
|
||||
@@ -60,12 +60,15 @@
|
||||
"@material/mwc-menu": "0.25.1",
|
||||
"@material/mwc-radio": "0.25.1",
|
||||
"@material/mwc-ripple": "0.25.1",
|
||||
"@material/mwc-select": "^0.25.1",
|
||||
"@material/mwc-slider": "^0.25.1",
|
||||
"@material/mwc-switch": "0.25.1",
|
||||
"@material/mwc-tab": "0.25.1",
|
||||
"@material/mwc-tab-bar": "0.25.1",
|
||||
"@material/mwc-textfield": "^0.25.1",
|
||||
"@material/top-app-bar": "13.0.0-canary.65125b3a6.0",
|
||||
"@mdi/js": "6.1.95",
|
||||
"@mdi/svg": "6.1.95",
|
||||
"@mdi/js": "6.2.95",
|
||||
"@mdi/svg": "6.2.95",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
@@ -108,7 +111,7 @@
|
||||
"deep-freeze": "^0.0.1",
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.0.10",
|
||||
"hls.js": "^1.0.11",
|
||||
"home-assistant-js-websocket": "^5.11.1",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
|
||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20210930.0",
|
||||
version="20211007.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/frontend",
|
||||
author="The Home Assistant Authors",
|
||||
|
||||
@@ -11,12 +11,14 @@ import "./ha-password-manager-polyfill";
|
||||
import { property, state } from "lit/decorators";
|
||||
import "../components/ha-form/ha-form";
|
||||
import "../components/ha-markdown";
|
||||
import "../components/ha-alert";
|
||||
import { AuthProvider } from "../data/auth";
|
||||
import {
|
||||
DataEntryFlowStep,
|
||||
DataEntryFlowStepForm,
|
||||
} from "../data/data_entry_flow";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
|
||||
@@ -31,12 +33,40 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
|
||||
@state() private _state: State = "loading";
|
||||
|
||||
@state() private _stepData: any = {};
|
||||
@state() private _stepData?: Record<string, any>;
|
||||
|
||||
@state() private _step?: DataEntryFlowStep;
|
||||
|
||||
@state() private _errorMessage?: string;
|
||||
|
||||
willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!changedProps.has("_step")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._step) {
|
||||
this._stepData = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const oldStep = changedProps.get("_step") as HaAuthFlow["_step"];
|
||||
|
||||
if (
|
||||
!oldStep ||
|
||||
this._step.flow_id !== oldStep.flow_id ||
|
||||
(this._step.type === "form" &&
|
||||
oldStep.type === "form" &&
|
||||
this._step.step_id !== oldStep.step_id)
|
||||
) {
|
||||
this._stepData =
|
||||
this._step.type === "form"
|
||||
? computeInitialHaFormData(this._step.data_schema)
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<form>${this._renderForm()}</form>
|
||||
@@ -76,6 +106,24 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
if (changedProps.has("authProvider")) {
|
||||
this._providerChanged(this.authProvider);
|
||||
}
|
||||
|
||||
if (!changedProps.has("_step") || this._step?.type !== "form") {
|
||||
return;
|
||||
}
|
||||
|
||||
// 100ms to give all the form elements time to initialize.
|
||||
setTimeout(() => {
|
||||
const form = this.renderRoot.querySelector("ha-form");
|
||||
if (form) {
|
||||
(form as any).focus();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
this.renderRoot.querySelector(
|
||||
"ha-password-manager-polyfill"
|
||||
)!.boundingRect = this.getBoundingClientRect();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _renderForm(): TemplateResult {
|
||||
@@ -98,16 +146,20 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
`;
|
||||
case "error":
|
||||
return html`
|
||||
<div class="error">
|
||||
<ha-alert alert-type="error">
|
||||
${this.localize(
|
||||
"ui.panel.page-authorize.form.error",
|
||||
"error",
|
||||
this._errorMessage
|
||||
)}
|
||||
</div>
|
||||
</ha-alert>
|
||||
`;
|
||||
case "loading":
|
||||
return html` ${this.localize("ui.panel.page-authorize.form.working")} `;
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.localize("ui.panel.page-authorize.form.working")}
|
||||
</ha-alert>
|
||||
`;
|
||||
default:
|
||||
return html``;
|
||||
}
|
||||
@@ -189,7 +241,8 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._updateStep(data);
|
||||
this._step = data;
|
||||
this._state = "step";
|
||||
} else {
|
||||
this._state = "error";
|
||||
this._errorMessage = data.message;
|
||||
@@ -220,39 +273,6 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
document.location.assign(url);
|
||||
}
|
||||
|
||||
private async _updateStep(step: DataEntryFlowStep) {
|
||||
let stepData: any = null;
|
||||
if (
|
||||
this._step &&
|
||||
(step.flow_id !== this._step.flow_id ||
|
||||
(step.type === "form" &&
|
||||
this._step.type === "form" &&
|
||||
step.step_id !== this._step.step_id))
|
||||
) {
|
||||
stepData = {};
|
||||
}
|
||||
this._step = step;
|
||||
this._state = "step";
|
||||
if (stepData != null) {
|
||||
this._stepData = stepData;
|
||||
}
|
||||
|
||||
await this.updateComplete;
|
||||
// 100ms to give all the form elements time to initialize.
|
||||
setTimeout(() => {
|
||||
const form = this.renderRoot.querySelector("ha-form");
|
||||
if (form) {
|
||||
(form as any).focus();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
this.renderRoot.querySelector(
|
||||
"ha-password-manager-polyfill"
|
||||
)!.boundingRect = this.getBoundingClientRect();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _stepDataChanged(ev: CustomEvent) {
|
||||
this._stepData = ev.detail.value;
|
||||
}
|
||||
@@ -316,7 +336,8 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
this._redirect(newStep.result);
|
||||
return;
|
||||
}
|
||||
await this._updateStep(newStep);
|
||||
this._step = newStep;
|
||||
this._state = "step";
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error submitting step", err);
|
||||
@@ -337,9 +358,6 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
margin: 24px 0 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,10 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
display: block;
|
||||
margin-top: 48px;
|
||||
}
|
||||
ha-auth-flow {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HaFormSchema } from "../components/ha-form/ha-form";
|
||||
import { DataEntryFlowStep } from "../data/data_entry_flow";
|
||||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import type { DataEntryFlowStep } from "../data/data_entry_flow";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -4,6 +4,10 @@ export const atLeastVersion = (
|
||||
minor: number,
|
||||
patch?: number
|
||||
): boolean => {
|
||||
if (__DEMO__) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [haMajor, haMinor, haPatch] = version.split(".", 3);
|
||||
|
||||
return (
|
||||
|
||||
@@ -57,28 +57,29 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
|
||||
export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
aqi: "hass:air-filter",
|
||||
current: "hass:current-ac",
|
||||
battery: "hass:battery",
|
||||
carbon_dioxide: "mdi:molecule-co2",
|
||||
carbon_monoxide: "mdi:molecule-co",
|
||||
current: "hass:current-ac",
|
||||
date: "hass:calendar",
|
||||
energy: "hass:lightning-bolt",
|
||||
gas: "hass:gas-cylinder",
|
||||
humidity: "hass:water-percent",
|
||||
illuminance: "hass:brightness-5",
|
||||
monetary: "mdi:cash",
|
||||
nitrogen_dioxide: "mdi:molecule",
|
||||
nitrogen_monoxide: "mdi:molecule",
|
||||
nitrous_oxide: "mdi:molecule",
|
||||
ozone: "mdi:molecule",
|
||||
temperature: "hass:thermometer",
|
||||
monetary: "mdi:cash",
|
||||
pm25: "mdi:molecule",
|
||||
pm1: "mdi:molecule",
|
||||
pm10: "mdi:molecule",
|
||||
pressure: "hass:gauge",
|
||||
pm25: "mdi:molecule",
|
||||
power: "hass:flash",
|
||||
power_factor: "hass:angle-acute",
|
||||
pressure: "hass:gauge",
|
||||
signal_strength: "hass:wifi",
|
||||
sulphur_dioxide: "mdi:molecule",
|
||||
temperature: "hass:thermometer",
|
||||
timestamp: "hass:clock",
|
||||
volatile_organic_compounds: "mdi:molecule",
|
||||
voltage: "hass:sine-wave",
|
||||
|
||||
@@ -115,7 +115,7 @@ export const applyThemesOnElement = (
|
||||
}
|
||||
|
||||
const newTheme =
|
||||
themeRules && cacheKey
|
||||
Object.keys(themeRules).length && cacheKey
|
||||
? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules)
|
||||
: undefined;
|
||||
|
||||
|
||||
2
src/common/string/capitalize-first-letter.ts
Normal file
2
src/common/string/capitalize-first-letter.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const capitalizeFirstLetter = (str: string) =>
|
||||
str.charAt(0).toUpperCase() + str.slice(1);
|
||||
@@ -180,10 +180,10 @@ export function fuzzyScore(
|
||||
wordLow
|
||||
);
|
||||
|
||||
let row = 1;
|
||||
let row: number;
|
||||
let column = 1;
|
||||
let patternPos = patternStart;
|
||||
let wordPos = wordStart;
|
||||
let patternPos: number;
|
||||
let wordPos: number;
|
||||
|
||||
const hasStrongFirstMatch = [false];
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-rel
|
||||
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
|
||||
import IntlMessageFormat from "intl-messageformat";
|
||||
import { Resources } from "../../types";
|
||||
import { getLocalLanguage } from "../../util/common-translation";
|
||||
|
||||
export type LocalizeFunc = (key: string, ...args: any[]) => string;
|
||||
interface FormatType {
|
||||
@@ -15,37 +16,32 @@ export interface FormatsType {
|
||||
time: FormatType;
|
||||
}
|
||||
|
||||
let loadedPolyfillLocale: Set<string> | undefined;
|
||||
|
||||
const polyfillPluralRules = shouldPolyfillPluralRules();
|
||||
const polyfillRelativeTime = shouldPolyfillRelativeTime();
|
||||
const polyfillDateTime = shouldPolyfillDateTime();
|
||||
const loadedPolyfillLocale = new Set();
|
||||
|
||||
const polyfills: Promise<any>[] = [];
|
||||
if (__BUILD__ === "latest") {
|
||||
if (shouldPolyfillLocale()) {
|
||||
polyfills.push(import("@formatjs/intl-locale/polyfill"));
|
||||
}
|
||||
if (polyfillPluralRules) {
|
||||
if (shouldPolyfillPluralRules()) {
|
||||
polyfills.push(import("@formatjs/intl-pluralrules/polyfill"));
|
||||
polyfills.push(import("@formatjs/intl-pluralrules/locale-data/en"));
|
||||
}
|
||||
if (polyfillRelativeTime) {
|
||||
if (shouldPolyfillRelativeTime()) {
|
||||
polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill"));
|
||||
}
|
||||
if (polyfillDateTime) {
|
||||
if (shouldPolyfillDateTime()) {
|
||||
polyfills.push(import("@formatjs/intl-datetimeformat/polyfill"));
|
||||
}
|
||||
}
|
||||
|
||||
let polyfillLoaded = polyfills.length === 0;
|
||||
export const polyfillsLoaded = polyfillLoaded
|
||||
? undefined
|
||||
: Promise.all(polyfills).then(() => {
|
||||
loadedPolyfillLocale = new Set();
|
||||
polyfillLoaded = true;
|
||||
// Load English so it becomes the default
|
||||
return loadPolyfillLocales("en");
|
||||
});
|
||||
export const polyfillsLoaded =
|
||||
polyfills.length === 0
|
||||
? undefined
|
||||
: Promise.all(polyfills).then(() =>
|
||||
// Load the default language
|
||||
loadPolyfillLocales(getLocalLanguage())
|
||||
);
|
||||
|
||||
/**
|
||||
* Adapted from Polymer app-localize-behavior.
|
||||
@@ -74,11 +70,11 @@ export const computeLocalize = async (
|
||||
resources: Resources,
|
||||
formats?: FormatsType
|
||||
): Promise<LocalizeFunc> => {
|
||||
if (!polyfillLoaded) {
|
||||
if (polyfillsLoaded) {
|
||||
await polyfillsLoaded;
|
||||
}
|
||||
|
||||
loadPolyfillLocales(language);
|
||||
await loadPolyfillLocales(language);
|
||||
|
||||
// Everytime any of the parameters change, invalidate the strings cache.
|
||||
cache._localizationCache = {};
|
||||
@@ -132,19 +128,44 @@ export const computeLocalize = async (
|
||||
};
|
||||
|
||||
export const loadPolyfillLocales = async (language: string) => {
|
||||
if (!loadedPolyfillLocale || loadedPolyfillLocale.has(language)) {
|
||||
if (loadedPolyfillLocale.has(language)) {
|
||||
return;
|
||||
}
|
||||
loadedPolyfillLocale.add(language);
|
||||
try {
|
||||
if (polyfillPluralRules) {
|
||||
await import(`@formatjs/intl-pluralrules/locale-data/${language}`);
|
||||
if (
|
||||
Intl.NumberFormat &&
|
||||
// @ts-ignore
|
||||
typeof Intl.NumberFormat.__addLocaleData === "function"
|
||||
) {
|
||||
const result = await fetch(
|
||||
`/static/locale-data/intl-numberformat/${language}.json`
|
||||
);
|
||||
// @ts-ignore
|
||||
Intl.NumberFormat.__addLocaleData(await result.json());
|
||||
}
|
||||
if (polyfillRelativeTime) {
|
||||
await import(`@formatjs/intl-relativetimeformat/locale-data/${language}`);
|
||||
if (
|
||||
// @ts-expect-error
|
||||
Intl.RelativeTimeFormat &&
|
||||
// @ts-ignore
|
||||
typeof Intl.RelativeTimeFormat.__addLocaleData === "function"
|
||||
) {
|
||||
const result = await fetch(
|
||||
`/static/locale-data/intl-relativetimeformat/${language}.json`
|
||||
);
|
||||
// @ts-ignore
|
||||
Intl.RelativeTimeFormat.__addLocaleData(await result.json());
|
||||
}
|
||||
if (polyfillDateTime) {
|
||||
await import(`@formatjs/intl-datetimeformat/locale-data/${language}`);
|
||||
if (
|
||||
Intl.DateTimeFormat &&
|
||||
// @ts-ignore
|
||||
typeof Intl.DateTimeFormat.__addLocaleData === "function"
|
||||
) {
|
||||
const result = await fetch(
|
||||
`/static/locale-data/intl-datetimeformat/${language}.json`
|
||||
);
|
||||
// @ts-ignore
|
||||
Intl.DateTimeFormat.__addLocaleData(await result.json());
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore
|
||||
|
||||
@@ -86,6 +86,7 @@ export default class HaChartBase extends LitElement {
|
||||
class=${classMap({
|
||||
hidden: this._hiddenDatasets.has(index),
|
||||
})}
|
||||
.title=${dataset.label}
|
||||
>
|
||||
<div
|
||||
class="bullet"
|
||||
|
||||
@@ -21,6 +21,7 @@ const BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED = new Set([
|
||||
"garage_door",
|
||||
"gas",
|
||||
"lock",
|
||||
"motion",
|
||||
"opening",
|
||||
"problem",
|
||||
"safety",
|
||||
@@ -55,7 +56,11 @@ const getColor = (
|
||||
entityState: HassEntity,
|
||||
computedStyles: CSSStyleDeclaration
|
||||
) => {
|
||||
if (invertOnOff(entityState)) {
|
||||
// Inversion is only valid for "on" or "off" state
|
||||
if (
|
||||
(stateString === "on" || stateString === "off") &&
|
||||
invertOnOff(entityState)
|
||||
) {
|
||||
stateString = stateString === "on" ? "off" : "on";
|
||||
}
|
||||
if (stateColorMap.has(stateString)) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Layout1d, scroll } from "@lit-labs/virtualizer";
|
||||
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
css,
|
||||
@@ -27,7 +28,7 @@ import { nextRender } from "../../common/util/render-status";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-icon";
|
||||
import "../ha-svg-icon";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
|
||||
declare global {
|
||||
@@ -311,11 +312,11 @@ export class HaDataTable extends LitElement {
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${sorted && this._sortDirection === "desc"
|
||||
? "hass:arrow-down"
|
||||
: "hass:arrow-up"}
|
||||
></ha-icon>
|
||||
<ha-svg-icon
|
||||
.path=${sorted && this._sortDirection === "desc"
|
||||
? mdiArrowDown
|
||||
: mdiArrowUp}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
<span>${column.title}</span>
|
||||
@@ -863,14 +864,14 @@ export class HaDataTable extends LitElement {
|
||||
:host([dir="rtl"]) .mdc-data-table__header-cell > * {
|
||||
transition: right 0.2s ease;
|
||||
}
|
||||
.mdc-data-table__header-cell ha-icon {
|
||||
.mdc-data-table__header-cell ha-svg-icon {
|
||||
top: -3px;
|
||||
position: absolute;
|
||||
}
|
||||
.mdc-data-table__header-cell.not-sorted ha-icon {
|
||||
.mdc-data-table__header-cell.not-sorted ha-svg-icon {
|
||||
left: -20px;
|
||||
}
|
||||
:host([dir="rtl"]) .mdc-data-table__header-cell.not-sorted ha-icon {
|
||||
:host([dir="rtl"]) .mdc-data-table__header-cell.not-sorted ha-svg-icon {
|
||||
right: -20px;
|
||||
}
|
||||
.mdc-data-table__header-cell.sortable:not(.not-sorted) span,
|
||||
@@ -886,16 +887,16 @@ export class HaDataTable extends LitElement {
|
||||
left: auto;
|
||||
right: 24px;
|
||||
}
|
||||
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-icon,
|
||||
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-icon {
|
||||
.mdc-data-table__header-cell.sortable:not(.not-sorted) ha-svg-icon,
|
||||
.mdc-data-table__header-cell.sortable:hover.not-sorted ha-svg-icon {
|
||||
left: 12px;
|
||||
}
|
||||
:host([dir="rtl"])
|
||||
.mdc-data-table__header-cell.sortable:not(.not-sorted)
|
||||
ha-icon,
|
||||
ha-svg-icon,
|
||||
:host([dir="rtl"])
|
||||
.mdc-data-table__header-cell.sortable:hover.not-sorted
|
||||
ha-icon {
|
||||
ha-svg-icon {
|
||||
left: auto;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mdiAlert } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -14,11 +15,12 @@ import { computeStateDisplay } from "../../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { stateIcon } from "../../common/entity/state_icon";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import { formatNumber } from "../../common/number/format_number";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-label-badge";
|
||||
import "../ha-icon";
|
||||
|
||||
@customElement("ha-state-label-badge")
|
||||
export class HaStateLabelBadge extends LitElement {
|
||||
@@ -58,16 +60,20 @@ export class HaStateLabelBadge extends LitElement {
|
||||
<ha-label-badge
|
||||
class="warning"
|
||||
label=${this.hass!.localize("state_badge.default.error")}
|
||||
icon="hass:alert"
|
||||
description=${this.hass!.localize(
|
||||
"state_badge.default.entity_not_found"
|
||||
)}
|
||||
></ha-label-badge>
|
||||
>
|
||||
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
`;
|
||||
}
|
||||
|
||||
const domain = computeStateDomain(entityState);
|
||||
|
||||
const value = this._computeValue(domain, entityState);
|
||||
const icon = this.icon ? this.icon : this._computeIcon(domain, entityState);
|
||||
|
||||
return html`
|
||||
<ha-label-badge
|
||||
class=${classMap({
|
||||
@@ -75,8 +81,6 @@ export class HaStateLabelBadge extends LitElement {
|
||||
"has-unit_of_measurement":
|
||||
"unit_of_measurement" in entityState.attributes,
|
||||
})}
|
||||
.value=${this._computeValue(domain, entityState)}
|
||||
.icon=${this.icon ? this.icon : this._computeIcon(domain, entityState)}
|
||||
.image=${this.icon
|
||||
? ""
|
||||
: this.image
|
||||
@@ -88,8 +92,15 @@ export class HaStateLabelBadge extends LitElement {
|
||||
entityState,
|
||||
this._timerTimeRemaining
|
||||
)}
|
||||
.description=${this.name ? this.name : computeStateName(entityState)}
|
||||
></ha-label-badge>
|
||||
.description=${this.name ?? computeStateName(entityState)}
|
||||
>
|
||||
${icon ? html`<ha-icon .icon=${icon}></ha-icon>` : ""}
|
||||
${value && (this.icon || !this.image)
|
||||
? html`<span class=${value && value.length > 4 ? "big" : ""}
|
||||
>${value}</span
|
||||
>`
|
||||
: ""}
|
||||
</ha-label-badge>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -208,7 +219,9 @@ export class HaStateLabelBadge extends LitElement {
|
||||
:host {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: 70%;
|
||||
}
|
||||
ha-label-badge {
|
||||
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
|
||||
}
|
||||
|
||||
@@ -24,13 +24,15 @@ class StateInfo extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const name = computeStateName(this.stateObj);
|
||||
|
||||
return html`<state-badge
|
||||
.stateObj=${this.stateObj}
|
||||
.stateColor=${true}
|
||||
></state-badge>
|
||||
<div class="info">
|
||||
<div class="name" .inDialog=${this.inDialog}>
|
||||
${computeStateName(this.stateObj)}
|
||||
<div class="name" .title=${name} .inDialog=${this.inDialog}>
|
||||
${name}
|
||||
</div>
|
||||
${this.inDialog
|
||||
? html`<div class="time-ago">
|
||||
@@ -38,6 +40,7 @@ class StateInfo extends LitElement {
|
||||
id="last_changed"
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
<paper-tooltip animation-delay="0" for="last_changed">
|
||||
<div>
|
||||
@@ -92,7 +95,6 @@ class StateInfo extends LitElement {
|
||||
state-badge {
|
||||
float: left;
|
||||
}
|
||||
|
||||
:host([rtl]) state-badge {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
mdiClose,
|
||||
mdiInformationOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
const ALERT_ICONS = {
|
||||
@@ -29,6 +31,8 @@ declare global {
|
||||
|
||||
@customElement("ha-alert")
|
||||
class HaAlert extends LitElement {
|
||||
@property({ type: Object }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public title = "";
|
||||
|
||||
@property({ attribute: "alert-type" }) public alertType:
|
||||
@@ -37,29 +41,37 @@ class HaAlert extends LitElement {
|
||||
| "error"
|
||||
| "success" = "info";
|
||||
|
||||
@property({ attribute: "action-text" }) public actionText = "";
|
||||
@property({ attribute: "action-text" }) public actionText?: string;
|
||||
|
||||
@property({ type: Boolean }) public dismissable = false;
|
||||
|
||||
@property({ type: Boolean }) public rtl = false;
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.size !== 1 || !changedProps.has("hass")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant;
|
||||
if (!oldHass || !this.hass) {
|
||||
return true;
|
||||
}
|
||||
return this.hass.language !== oldHass.language;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div
|
||||
class="issue-type ${classMap({
|
||||
rtl: this.rtl,
|
||||
rtl: this.hass ? computeRTL(this.hass) : this.rtl,
|
||||
[this.alertType]: true,
|
||||
})}"
|
||||
>
|
||||
<div class="icon">
|
||||
<div class="icon ${this.title ? "" : "no-title"}">
|
||||
<ha-svg-icon .path=${ALERT_ICONS[this.alertType]}></ha-svg-icon>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div
|
||||
class="main-content ${classMap({
|
||||
"no-title": !this.title,
|
||||
})}"
|
||||
>
|
||||
<div class="main-content">
|
||||
${this.title ? html`<div class="title">${this.title}</div>` : ""}
|
||||
<slot></slot>
|
||||
</div>
|
||||
@@ -94,7 +106,7 @@ class HaAlert extends LitElement {
|
||||
static styles = css`
|
||||
.issue-type {
|
||||
position: relative;
|
||||
padding: 4px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
margin: 4px 0;
|
||||
}
|
||||
@@ -113,11 +125,16 @@ class HaAlert extends LitElement {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.icon {
|
||||
margin: 4px 8px;
|
||||
margin-right: 8px;
|
||||
width: 24px;
|
||||
}
|
||||
.main-content.no-title {
|
||||
margin-top: 6px;
|
||||
.icon.no-title {
|
||||
align-self: center;
|
||||
}
|
||||
.issue-type.rtl > .icon {
|
||||
margin-right: 0px;
|
||||
margin-left: 8px;
|
||||
width: 24px;
|
||||
}
|
||||
.issue-type.rtl > .content {
|
||||
flex-direction: row-reverse;
|
||||
@@ -126,24 +143,22 @@ class HaAlert extends LitElement {
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.main-content {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.title {
|
||||
margin-top: 2px;
|
||||
font-weight: bold;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
mwc-button {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.action {
|
||||
align-self: center;
|
||||
mwc-icon-button {
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
|
||||
.issue-type.info > .icon {
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
unsafeCSS,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-icon";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
|
||||
@@ -4,7 +4,8 @@ import { css, CSSResultGroup, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
export const createCloseHeading = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -16,8 +16,6 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public suffix?: string;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public enableMillisecond?: boolean;
|
||||
|
||||
37
src/components/ha-form/compute-initial-ha-form-data.ts
Normal file
37
src/components/ha-form/compute-initial-ha-form-data.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { HaFormSchema } from "./types";
|
||||
|
||||
export const computeInitialHaFormData = (
|
||||
schema: HaFormSchema[]
|
||||
): Record<string, any> => {
|
||||
const data = {};
|
||||
schema.forEach((field) => {
|
||||
if (field.description?.suggested_value) {
|
||||
data[field.name] = field.description.suggested_value;
|
||||
} else if ("default" in field) {
|
||||
data[field.name] = field.default;
|
||||
} else if (!field.required) {
|
||||
// Do nothing.
|
||||
} else if (field.type === "boolean") {
|
||||
data[field.name] = false;
|
||||
} else if (field.type === "string") {
|
||||
data[field.name] = "";
|
||||
} else if (field.type === "integer") {
|
||||
data[field.name] = "valueMin" in field ? field.valueMin : 0;
|
||||
} else if (field.type === "constant") {
|
||||
data[field.name] = field.value;
|
||||
} else if (field.type === "float") {
|
||||
data[field.name] = 0.0;
|
||||
} else if (field.type === "select") {
|
||||
if (field.options.length) {
|
||||
data[field.name] = field.options[0][0];
|
||||
}
|
||||
} else if (field.type === "positive_time_period_dict") {
|
||||
data[field.name] = {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
return data;
|
||||
};
|
||||
@@ -1,13 +1,14 @@
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import "@material/mwc-formfield";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HaFormBooleanData,
|
||||
HaFormBooleanSchema,
|
||||
HaFormElement,
|
||||
} from "./ha-form";
|
||||
} from "./types";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-checkbox";
|
||||
|
||||
@customElement("ha-form-boolean")
|
||||
export class HaFormBoolean extends LitElement implements HaFormElement {
|
||||
@@ -17,8 +18,6 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public suffix!: string;
|
||||
|
||||
@query("paper-checkbox", true) private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
@@ -29,26 +28,20 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<paper-checkbox .checked=${this.data} @change=${this._valueChanged}>
|
||||
${this.label}
|
||||
</paper-checkbox>
|
||||
<mwc-formfield .label=${this.label}>
|
||||
<ha-checkbox
|
||||
.checked=${this.data}
|
||||
@change=${this._valueChanged}
|
||||
></ha-checkbox>
|
||||
</mwc-formfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: (ev.target as PaperCheckboxElement).checked,
|
||||
value: (ev.target as HaCheckbox).checked,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
paper-checkbox {
|
||||
display: block;
|
||||
padding: 22px 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaFormConstantSchema, HaFormElement } from "./ha-form";
|
||||
import { HaFormConstantSchema, HaFormElement } from "./types";
|
||||
|
||||
@customElement("ha-form-constant")
|
||||
export class HaFormConstant extends LitElement implements HaFormElement {
|
||||
@@ -16,13 +8,6 @@ export class HaFormConstant extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this.schema.value,
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<span class="label">${this.label}</span>: ${this.schema.value}`;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import "@material/mwc-textfield";
|
||||
import type { TextField } from "@material/mwc-textfield";
|
||||
import { css, html, LitElement, TemplateResult, PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./ha-form";
|
||||
import { HaFormElement, HaFormFloatData, HaFormFloatSchema } from "./types";
|
||||
|
||||
@customElement("ha-form-float")
|
||||
export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
@@ -13,9 +13,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public suffix!: string;
|
||||
|
||||
@query("paper-input", true) private _input?: HTMLElement;
|
||||
@query("mwc-textfield") private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
@@ -25,33 +23,59 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<paper-input
|
||||
<mwc-textfield
|
||||
inputMode="decimal"
|
||||
.label=${this.label}
|
||||
.value=${this._value}
|
||||
.value=${this.data !== undefined ? this.data : ""}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
<span suffix slot="suffix">${this.suffix}</span>
|
||||
</paper-input>
|
||||
.suffix=${this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required ? "Required" : undefined}
|
||||
@input=${this._valueChanged}
|
||||
></mwc-textfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.data;
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("schema")) {
|
||||
this.toggleAttribute("own-margin", !!this.schema.required);
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
const value: number | undefined = (ev.target as PaperInputElement).value
|
||||
? Number((ev.target as PaperInputElement).value)
|
||||
: undefined;
|
||||
if (this._value === value) {
|
||||
const source = ev.target as TextField;
|
||||
const rawValue = source.value;
|
||||
|
||||
let value: number | undefined;
|
||||
|
||||
if (rawValue !== "") {
|
||||
value = parseFloat(rawValue);
|
||||
}
|
||||
|
||||
// Detect anything changed
|
||||
if (this.data === value) {
|
||||
// parseFloat will drop invalid text at the end, in that case update textfield
|
||||
const newRawValue = value === undefined ? "" : String(value);
|
||||
if (source.value !== newRawValue) {
|
||||
source.value = newRawValue;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host([own-margin]) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
mwc-textfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import "@material/mwc-textfield";
|
||||
import type { TextField } from "@material/mwc-textfield";
|
||||
import "@material/mwc-slider";
|
||||
import type { Slider } from "@material/mwc-slider";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-slider";
|
||||
import type { HaSlider } from "../ha-slider";
|
||||
import {
|
||||
HaFormElement,
|
||||
HaFormIntegerData,
|
||||
HaFormIntegerSchema,
|
||||
} from "./ha-form";
|
||||
import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types";
|
||||
|
||||
@customElement("ha-form-integer")
|
||||
export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
@@ -20,10 +23,10 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public suffix?: string;
|
||||
|
||||
@query("paper-input ha-slider") private _input?: HTMLElement;
|
||||
|
||||
private _lastValue?: HaFormIntegerData;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
@@ -31,66 +34,113 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return "valueMin" in this.schema && "valueMax" in this.schema
|
||||
? html`
|
||||
<div>
|
||||
${this.label}
|
||||
<div class="flex">
|
||||
${this.schema.optional && this.schema.default === undefined
|
||||
? html`
|
||||
<ha-checkbox
|
||||
@change=${this._handleCheckboxChange}
|
||||
.checked=${this.data !== undefined}
|
||||
></ha-checkbox>
|
||||
`
|
||||
: ""}
|
||||
<ha-slider
|
||||
pin
|
||||
editable
|
||||
.value=${this._value}
|
||||
.min=${this.schema.valueMin}
|
||||
.max=${this.schema.valueMax}
|
||||
.disabled=${this.data === undefined &&
|
||||
this.schema.optional &&
|
||||
this.schema.default === undefined}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-slider>
|
||||
</div>
|
||||
if ("valueMin" in this.schema && "valueMax" in this.schema) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
<div class="flex">
|
||||
${this.schema.optional
|
||||
? html`
|
||||
<ha-checkbox
|
||||
@change=${this._handleCheckboxChange}
|
||||
.checked=${this.data !== undefined}
|
||||
></ha-checkbox>
|
||||
`
|
||||
: ""}
|
||||
<mwc-slider
|
||||
discrete
|
||||
.value=${this._value}
|
||||
.min=${this.schema.valueMin}
|
||||
.max=${this.schema.valueMax}
|
||||
.disabled=${this.data === undefined && this.schema.optional}
|
||||
@change=${this._valueChanged}
|
||||
></mwc-slider>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<paper-input
|
||||
type="number"
|
||||
.label=${this.label}
|
||||
.value=${this._value}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
`;
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<mwc-textfield
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
.label=${this.label}
|
||||
.value=${this.data !== undefined ? this.data : ""}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
.suffix=${this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required ? "Required" : undefined}
|
||||
@input=${this._valueChanged}
|
||||
></mwc-textfield>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("schema")) {
|
||||
this.toggleAttribute(
|
||||
"own-margin",
|
||||
!("valueMin" in this.schema && "valueMax" in this.schema) &&
|
||||
!!this.schema.required
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return (
|
||||
this.data ||
|
||||
this.schema.description?.suggested_value ||
|
||||
this.schema.default ||
|
||||
0
|
||||
);
|
||||
if (this.data !== undefined) {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
if (this.schema.optional) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.schema.description?.suggested_value || this.schema.default || 0;
|
||||
}
|
||||
|
||||
private _handleCheckboxChange(ev: Event) {
|
||||
const checked = (ev.target as HaCheckbox).checked;
|
||||
let value: HaFormIntegerData | undefined;
|
||||
if (checked) {
|
||||
for (const candidate of [
|
||||
this._lastValue,
|
||||
this.schema.description?.suggested_value as HaFormIntegerData,
|
||||
this.schema.default,
|
||||
0,
|
||||
]) {
|
||||
if (candidate !== undefined) {
|
||||
value = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We track last value so user can disable and enable a field without losing
|
||||
// their value.
|
||||
this._lastValue = this.data;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: checked ? this._value : undefined,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
const value = Number((ev.target as PaperInputElement | HaSlider).value);
|
||||
if (this._value === value) {
|
||||
const source = ev.target as TextField | Slider;
|
||||
const rawValue = source.value;
|
||||
|
||||
let value: number | undefined;
|
||||
|
||||
if (rawValue !== "") {
|
||||
value = parseInt(String(rawValue));
|
||||
}
|
||||
|
||||
if (this.data === value) {
|
||||
// parseInt will drop invalid text at the end, in that case update textfield
|
||||
const newRawValue = value === undefined ? "" : String(value);
|
||||
if (source.value !== newRawValue) {
|
||||
source.value = newRawValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
@@ -98,12 +148,17 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host([own-margin]) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
ha-slider {
|
||||
width: 100%;
|
||||
margin-right: 16px;
|
||||
mwc-slider {
|
||||
flex: 1;
|
||||
}
|
||||
mwc-textfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-menu-button/paper-menu-button";
|
||||
import "@polymer/paper-ripple/paper-ripple";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||
import "@material/mwc-textfield";
|
||||
import "@material/mwc-formfield";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-icon";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-svg-icon";
|
||||
import {
|
||||
HaFormElement,
|
||||
HaFormMultiSelectData,
|
||||
HaFormMultiSelectSchema,
|
||||
} from "./ha-form";
|
||||
} from "./types";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
|
||||
function optionValue(item: string | string[]): string {
|
||||
return Array.isArray(item) ? item[0] : item;
|
||||
}
|
||||
|
||||
function optionLabel(item: string | string[]): string {
|
||||
return Array.isArray(item) ? item[1] || item[0] : item;
|
||||
}
|
||||
|
||||
const SHOW_ALL_ENTRIES_LIMIT = 6;
|
||||
|
||||
@customElement("ha-form-multi_select")
|
||||
export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
@@ -22,9 +39,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public suffix!: string;
|
||||
|
||||
@state() private _init = false;
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("paper-menu-button", true) private _input?: HTMLElement;
|
||||
|
||||
@@ -35,118 +50,141 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const options = Array.isArray(this.schema.options)
|
||||
? this.schema.options
|
||||
: Object.entries(this.schema.options!);
|
||||
|
||||
const options = Object.entries(this.schema.options);
|
||||
const data = this.data || [];
|
||||
|
||||
const renderedOptions = options.map((item: string | [string, string]) => {
|
||||
const value = optionValue(item);
|
||||
return html`
|
||||
<mwc-formfield .label=${optionLabel(item)}>
|
||||
<ha-checkbox
|
||||
.checked=${data.includes(value)}
|
||||
.value=${value}
|
||||
@change=${this._valueChanged}
|
||||
></ha-checkbox>
|
||||
</mwc-formfield>
|
||||
`;
|
||||
});
|
||||
|
||||
// We will just render all checkboxes.
|
||||
if (options.length < SHOW_ALL_ENTRIES_LIMIT) {
|
||||
return html`<div>${this.label}${renderedOptions}</div> `;
|
||||
}
|
||||
|
||||
return html`
|
||||
<paper-menu-button horizontal-align="right" vertical-offset="8">
|
||||
<div class="dropdown-trigger" slot="dropdown-trigger">
|
||||
<paper-ripple></paper-ripple>
|
||||
<paper-input
|
||||
id="input"
|
||||
type="text"
|
||||
readonly
|
||||
value=${data
|
||||
.map((value) => this.schema.options![value] || value)
|
||||
.join(", ")}
|
||||
label=${this.label}
|
||||
input-role="button"
|
||||
input-aria-haspopup="listbox"
|
||||
autocomplete="off"
|
||||
>
|
||||
<ha-icon
|
||||
icon="paper-dropdown-menu:arrow-drop-down"
|
||||
suffix
|
||||
slot="suffix"
|
||||
></ha-icon>
|
||||
</paper-input>
|
||||
</div>
|
||||
<paper-listbox
|
||||
multi
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-value"
|
||||
.selectedValues=${data}
|
||||
@selected-items-changed=${this._valueChanged}
|
||||
@iron-select=${this._onSelect}
|
||||
>
|
||||
${
|
||||
// TS doesn't work with union array types https://github.com/microsoft/TypeScript/issues/36390
|
||||
// @ts-ignore
|
||||
options.map((item: string | [string, string]) => {
|
||||
const value = this._optionValue(item);
|
||||
return html`
|
||||
<paper-icon-item .itemValue=${value}>
|
||||
<paper-checkbox
|
||||
.checked=${data.includes(value)}
|
||||
slot="item-icon"
|
||||
></paper-checkbox>
|
||||
${this._optionLabel(item)}
|
||||
</paper-icon-item>
|
||||
`;
|
||||
})
|
||||
}
|
||||
</paper-listbox>
|
||||
</paper-menu-button>
|
||||
<ha-button-menu
|
||||
fixed
|
||||
corner="BOTTOM_START"
|
||||
@opened=${this._handleOpen}
|
||||
@closed=${this._handleClose}
|
||||
>
|
||||
<mwc-textfield
|
||||
slot="trigger"
|
||||
.label=${this.label}
|
||||
.value=${data
|
||||
.map((value) => this.schema.options![value] || value)
|
||||
.join(", ")}
|
||||
tabindex="-1"
|
||||
></mwc-textfield>
|
||||
<ha-svg-icon
|
||||
slot="trigger"
|
||||
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
${renderedOptions}
|
||||
</ha-button-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this.updateComplete.then(() => {
|
||||
const input = (
|
||||
this.shadowRoot?.querySelector("paper-input")?.inputElement as any
|
||||
)?.inputElement;
|
||||
if (input) {
|
||||
input.style.textOverflow = "ellipsis";
|
||||
const { formElement, mdcRoot } =
|
||||
this.shadowRoot?.querySelector("mwc-textfield") || ({} as any);
|
||||
if (formElement) {
|
||||
formElement.style.textOverflow = "ellipsis";
|
||||
}
|
||||
if (mdcRoot) {
|
||||
mdcRoot.style.cursor = "pointer";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _optionValue(item: string | string[]): string {
|
||||
return Array.isArray(item) ? item[0] : item;
|
||||
}
|
||||
|
||||
private _optionLabel(item: string | string[]): string {
|
||||
return Array.isArray(item) ? item[1] || item[0] : item;
|
||||
}
|
||||
|
||||
private _onSelect(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("schema")) {
|
||||
this.toggleAttribute(
|
||||
"own-margin",
|
||||
Object.keys(this.schema.options).length >= SHOW_ALL_ENTRIES_LIMIT &&
|
||||
!!this.schema.required
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
if (!ev.detail.value || !this._init) {
|
||||
// ignore first call because that is the init of the component
|
||||
this._init = true;
|
||||
return;
|
||||
const { value, checked } = ev.target as HaCheckbox;
|
||||
|
||||
let newValue: string[];
|
||||
|
||||
if (checked) {
|
||||
if (!this.data) {
|
||||
newValue = [value];
|
||||
} else if (this.data.includes(value)) {
|
||||
return;
|
||||
} else {
|
||||
newValue = [...this.data, value];
|
||||
}
|
||||
} else {
|
||||
if (!this.data.includes(value)) {
|
||||
return;
|
||||
}
|
||||
newValue = this.data.filter((v) => v !== value);
|
||||
}
|
||||
|
||||
fireEvent(
|
||||
this,
|
||||
"value-changed",
|
||||
{
|
||||
value: ev.detail.value.map((element) => element.itemValue),
|
||||
},
|
||||
{ bubbles: false }
|
||||
);
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleOpen(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
this._opened = true;
|
||||
this.toggleAttribute("opened", true);
|
||||
}
|
||||
|
||||
private _handleClose(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
this._opened = false;
|
||||
this.toggleAttribute("opened", false);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
paper-menu-button {
|
||||
:host([own-margin]) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
ha-button-menu {
|
||||
display: block;
|
||||
padding: 0;
|
||||
--paper-item-icon-width: 34px;
|
||||
cursor: pointer;
|
||||
}
|
||||
paper-ripple {
|
||||
top: 12px;
|
||||
left: 0px;
|
||||
bottom: 8px;
|
||||
right: 0px;
|
||||
mwc-formfield {
|
||||
display: block;
|
||||
padding-right: 16px;
|
||||
}
|
||||
paper-input {
|
||||
text-overflow: ellipsis;
|
||||
mwc-textfield {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--input-dropdown-icon-color);
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
top: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([opened]) ha-svg-icon {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
:host([opened]) ha-button-menu {
|
||||
--mdc-text-field-idle-line-color: var(--input-hover-line-color);
|
||||
--mdc-text-field-label-ink-color: var(--primary-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../ha-duration-input";
|
||||
import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./ha-form";
|
||||
import { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./types";
|
||||
|
||||
@customElement("ha-form-positive_time_period_dict")
|
||||
export class HaFormTimePeriod extends LitElement implements HaFormElement {
|
||||
@@ -11,8 +11,6 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public suffix!: string;
|
||||
|
||||
@query("ha-time-input", true) private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { mdiClose, mdiMenuDown } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@polymer/paper-menu-button/paper-menu-button";
|
||||
import "@polymer/paper-ripple/paper-ripple";
|
||||
import "@material/mwc-select";
|
||||
import type { Select } from "@material/mwc-select";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-svg-icon";
|
||||
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
|
||||
import "../ha-radio";
|
||||
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./types";
|
||||
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import type { HaRadio } from "../ha-radio";
|
||||
|
||||
@customElement("ha-form-select")
|
||||
export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
@@ -19,9 +19,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public suffix!: string;
|
||||
|
||||
@query("ha-paper-dropdown-menu", true) private _input?: HTMLElement;
|
||||
@query("mwc-select", true) private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
@@ -30,90 +28,67 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<paper-menu-button horizontal-align="right" vertical-offset="8">
|
||||
<div class="dropdown-trigger" slot="dropdown-trigger">
|
||||
<paper-ripple></paper-ripple>
|
||||
<paper-input
|
||||
id="input"
|
||||
type="text"
|
||||
readonly
|
||||
value=${this.data}
|
||||
label=${this.label}
|
||||
input-role="button"
|
||||
input-aria-haspopup="listbox"
|
||||
autocomplete="off"
|
||||
>
|
||||
${this.data && this.schema.optional
|
||||
? html`<mwc-icon-button
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
@click=${this._clearValue}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: ""}
|
||||
<mwc-icon-button slot="suffix">
|
||||
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</paper-input>
|
||||
if (!this.schema.optional && this.schema.options!.length < 6) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
${this.schema.options.map(
|
||||
([value, label]) => html`
|
||||
<mwc-formfield .label=${label}>
|
||||
<ha-radio
|
||||
.checked=${value === this.data}
|
||||
.value=${value}
|
||||
@change=${this._valueChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-value"
|
||||
.selected=${this.data}
|
||||
@selected-item-changed=${this._valueChanged}
|
||||
>
|
||||
${
|
||||
// TS doesn't work with union array types https://github.com/microsoft/TypeScript/issues/36390
|
||||
// @ts-ignore
|
||||
this.schema.options!.map(
|
||||
(item: string | [string, string]) => html`
|
||||
<paper-item .itemValue=${this._optionValue(item)}>
|
||||
${this._optionLabel(item)}
|
||||
</paper-item>
|
||||
`
|
||||
)
|
||||
}
|
||||
</paper-listbox>
|
||||
</paper-menu-button>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<mwc-select
|
||||
fixedMenuPosition
|
||||
.label=${this.label}
|
||||
.value=${this.data}
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
${this.schema.optional
|
||||
? html`<mwc-list-item value=""></mwc-list-item>`
|
||||
: ""}
|
||||
${this.schema.options!.map(
|
||||
([value, label]) => html`
|
||||
<mwc-list-item .value=${value}>${label}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private _optionValue(item: string | [string, string]) {
|
||||
return Array.isArray(item) ? item[0] : item;
|
||||
}
|
||||
|
||||
private _optionLabel(item: string | [string, string]) {
|
||||
return Array.isArray(item) ? item[1] || item[0] : item;
|
||||
}
|
||||
|
||||
private _clearValue(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", { value: undefined });
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
if (!ev.detail.value) {
|
||||
ev.stopPropagation();
|
||||
let value: string | undefined = (ev.target as Select | HaRadio).value;
|
||||
|
||||
if (value === this.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === "") {
|
||||
value = undefined;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: ev.detail.value.itemValue,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
paper-menu-button {
|
||||
mwc-select,
|
||||
mwc-formfield {
|
||||
display: block;
|
||||
padding: 0;
|
||||
}
|
||||
paper-input > mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
padding: 2px;
|
||||
}
|
||||
.clear-button {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import "@material/mwc-textfield";
|
||||
import type { TextField } from "@material/mwc-textfield";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-svg-icon";
|
||||
@@ -10,7 +16,9 @@ import type {
|
||||
HaFormElement,
|
||||
HaFormStringData,
|
||||
HaFormStringSchema,
|
||||
} from "./ha-form";
|
||||
} from "./types";
|
||||
|
||||
const MASKED_FIELDS = ["password", "secret", "token"];
|
||||
|
||||
@customElement("ha-form-string")
|
||||
export class HaFormString extends LitElement implements HaFormElement {
|
||||
@@ -20,11 +28,9 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public suffix!: string;
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
@query("paper-input") private _input?: HTMLElement;
|
||||
@query("mwc-textfield") private _input?: HTMLElement;
|
||||
|
||||
public focus(): void {
|
||||
if (this._input) {
|
||||
@@ -33,20 +39,31 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this.schema.name.includes("password")
|
||||
? html`
|
||||
<paper-input
|
||||
.type=${this._unmaskedPassword ? "text" : "password"}
|
||||
.label=${this.label}
|
||||
.value=${this.data}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
const isPassword = MASKED_FIELDS.some((field) =>
|
||||
this.schema.name.includes(field)
|
||||
);
|
||||
return html`
|
||||
<mwc-textfield
|
||||
.type=${!isPassword
|
||||
? this._stringType
|
||||
: this._unmaskedPassword
|
||||
? "text"
|
||||
: "password"}
|
||||
.label=${this.label}
|
||||
.value=${this.data || ""}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
.suffix=${isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required ? "Required" : undefined}
|
||||
@input=${this._valueChanged}
|
||||
></mwc-textfield>
|
||||
${isPassword
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
toggles
|
||||
slot="suffix"
|
||||
id="iconButton"
|
||||
title="Click to toggle between masked and clear password"
|
||||
@click=${this._toggleUnmaskedPassword}
|
||||
tabindex="-1"
|
||||
@@ -54,19 +71,15 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</paper-input>
|
||||
`
|
||||
: html`
|
||||
<paper-input
|
||||
.type=${this._stringType}
|
||||
.label=${this.label}
|
||||
.value=${this.data}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
error-message="Required"
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
`;
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("schema")) {
|
||||
this.toggleAttribute("own-margin", !!this.schema.required);
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleUnmaskedPassword(): void {
|
||||
@@ -74,10 +87,13 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event): void {
|
||||
const value = (ev.target as PaperInputElement).value;
|
||||
let value: string | undefined = (ev.target as TextField).value;
|
||||
if (this.data === value) {
|
||||
return;
|
||||
}
|
||||
if (value === "" && this.schema.optional) {
|
||||
value = undefined;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
@@ -97,7 +113,20 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
:host([own-margin]) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
mwc-textfield {
|
||||
display: block;
|
||||
}
|
||||
mwc-icon-button {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 12px;
|
||||
--mdc-icon-button-size: 24px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaDurationData } from "../ha-duration-input";
|
||||
import "../ha-alert";
|
||||
import "./ha-form-boolean";
|
||||
import "./ha-form-constant";
|
||||
import "./ha-form-float";
|
||||
@@ -11,160 +11,79 @@ import "./ha-form-multi_select";
|
||||
import "./ha-form-positive_time_period_dict";
|
||||
import "./ha-form-select";
|
||||
import "./ha-form-string";
|
||||
import { HaFormElement, HaFormDataContainer, HaFormSchema } from "./types";
|
||||
|
||||
export type HaFormSchema =
|
||||
| HaFormConstantSchema
|
||||
| HaFormStringSchema
|
||||
| HaFormIntegerSchema
|
||||
| HaFormFloatSchema
|
||||
| HaFormBooleanSchema
|
||||
| HaFormSelectSchema
|
||||
| HaFormMultiSelectSchema
|
||||
| HaFormTimeSchema;
|
||||
|
||||
export interface HaFormBaseSchema {
|
||||
name: string;
|
||||
default?: HaFormData;
|
||||
required?: boolean;
|
||||
optional?: boolean;
|
||||
description?: { suffix?: string; suggested_value?: HaFormData };
|
||||
}
|
||||
|
||||
export interface HaFormConstantSchema extends HaFormBaseSchema {
|
||||
type: "constant";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface HaFormIntegerSchema extends HaFormBaseSchema {
|
||||
type: "integer";
|
||||
default?: HaFormIntegerData;
|
||||
valueMin?: number;
|
||||
valueMax?: number;
|
||||
}
|
||||
|
||||
export interface HaFormSelectSchema extends HaFormBaseSchema {
|
||||
type: "select";
|
||||
options?: string[] | Array<[string, string]>;
|
||||
}
|
||||
|
||||
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
|
||||
type: "multi_select";
|
||||
options?: Record<string, string> | string[] | Array<[string, string]>;
|
||||
}
|
||||
|
||||
export interface HaFormFloatSchema extends HaFormBaseSchema {
|
||||
type: "float";
|
||||
}
|
||||
|
||||
export interface HaFormStringSchema extends HaFormBaseSchema {
|
||||
type: "string";
|
||||
format?: string;
|
||||
}
|
||||
|
||||
export interface HaFormBooleanSchema extends HaFormBaseSchema {
|
||||
type: "boolean";
|
||||
}
|
||||
|
||||
export interface HaFormTimeSchema extends HaFormBaseSchema {
|
||||
type: "positive_time_period_dict";
|
||||
}
|
||||
|
||||
export interface HaFormDataContainer {
|
||||
[key: string]: HaFormData;
|
||||
}
|
||||
|
||||
export type HaFormData =
|
||||
| HaFormStringData
|
||||
| HaFormIntegerData
|
||||
| HaFormFloatData
|
||||
| HaFormBooleanData
|
||||
| HaFormSelectData
|
||||
| HaFormMultiSelectData
|
||||
| HaFormTimeData;
|
||||
|
||||
export type HaFormStringData = string;
|
||||
export type HaFormIntegerData = number;
|
||||
export type HaFormFloatData = number;
|
||||
export type HaFormBooleanData = boolean;
|
||||
export type HaFormSelectData = string;
|
||||
export type HaFormMultiSelectData = string[];
|
||||
export type HaFormTimeData = HaDurationData;
|
||||
|
||||
export interface HaFormElement extends LitElement {
|
||||
schema: HaFormSchema | HaFormSchema[];
|
||||
data?: HaFormDataContainer | HaFormData;
|
||||
label?: string;
|
||||
suffix?: string;
|
||||
}
|
||||
const getValue = (obj, item) => (obj ? obj[item.name] : null);
|
||||
|
||||
@customElement("ha-form")
|
||||
export class HaForm extends LitElement implements HaFormElement {
|
||||
@property() public data!: HaFormDataContainer | HaFormData;
|
||||
@property() public data!: HaFormDataContainer;
|
||||
|
||||
@property() public schema!: HaFormSchema | HaFormSchema[];
|
||||
@property() public schema!: HaFormSchema[];
|
||||
|
||||
@property() public error;
|
||||
@property() public error?: Record<string, string>;
|
||||
|
||||
@property() public computeError?: (schema: HaFormSchema, error) => string;
|
||||
|
||||
@property() public computeLabel?: (schema: HaFormSchema) => string;
|
||||
|
||||
@property() public computeSuffix?: (schema: HaFormSchema) => string;
|
||||
|
||||
public focus() {
|
||||
const input =
|
||||
this.shadowRoot!.getElementById("child-form") ||
|
||||
this.shadowRoot!.querySelector("ha-form");
|
||||
if (!input) {
|
||||
const root = this.shadowRoot?.querySelector(".root");
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
(input as HTMLElement).focus();
|
||||
for (const child of root.children) {
|
||||
if (child.tagName !== "HA-ALERT") {
|
||||
(child as HTMLElement).focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (Array.isArray(this.schema)) {
|
||||
return html`
|
||||
return html`
|
||||
<div class="root">
|
||||
${this.error && this.error.base
|
||||
? html`
|
||||
<div class="error">
|
||||
<ha-alert alert-type="error">
|
||||
${this._computeError(this.error.base, this.schema)}
|
||||
</div>
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
${this.schema.map(
|
||||
(item) => html`
|
||||
<ha-form
|
||||
.data=${this._getValue(this.data, item)}
|
||||
.schema=${item}
|
||||
.error=${this._getValue(this.error, item)}
|
||||
@value-changed=${this._valueChanged}
|
||||
.computeError=${this.computeError}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeSuffix=${this.computeSuffix}
|
||||
></ha-form>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.error
|
||||
? html`
|
||||
<div class="error">
|
||||
${this._computeError(this.error, this.schema)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${dynamicElement(`ha-form-${this.schema.type}`, {
|
||||
schema: this.schema,
|
||||
data: this.data,
|
||||
label: this._computeLabel(this.schema),
|
||||
suffix: this._computeSuffix(this.schema),
|
||||
id: "child-form",
|
||||
})}
|
||||
${this.schema.map((item) => {
|
||||
const error = getValue(this.error, item);
|
||||
return html`
|
||||
${error
|
||||
? html`
|
||||
<ha-alert own-margin alert-type="error">
|
||||
${this._computeError(error, item)}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
${dynamicElement(`ha-form-${item.type}`, {
|
||||
schema: item,
|
||||
data: getValue(this.data, item),
|
||||
label: this._computeLabel(item),
|
||||
})}
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
const root = super.createRenderRoot();
|
||||
// attach it as soon as possible to make sure we fetch all events.
|
||||
root.addEventListener("value-changed", (ev) => {
|
||||
ev.stopPropagation();
|
||||
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.data, [schema.name]: ev.detail.value },
|
||||
});
|
||||
});
|
||||
return root;
|
||||
}
|
||||
|
||||
private _computeLabel(schema: HaFormSchema) {
|
||||
return this.computeLabel
|
||||
? this.computeLabel(schema)
|
||||
@@ -173,38 +92,25 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
: "";
|
||||
}
|
||||
|
||||
private _computeSuffix(schema: HaFormSchema) {
|
||||
return this.computeSuffix
|
||||
? this.computeSuffix(schema)
|
||||
: schema && schema.description
|
||||
? schema.description.suffix
|
||||
: "";
|
||||
}
|
||||
|
||||
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
|
||||
return this.computeError ? this.computeError(error, schema) : error;
|
||||
}
|
||||
|
||||
private _getValue(obj, item) {
|
||||
if (obj) {
|
||||
return obj[item.name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
|
||||
const data = this.data as HaFormDataContainer;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...data, [schema.name]: ev.detail.value },
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
// .root has overflow: auto to avoid margin collapse
|
||||
return css`
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
.root {
|
||||
margin-bottom: -24px;
|
||||
overflow: auto;
|
||||
}
|
||||
.root > * {
|
||||
display: block;
|
||||
}
|
||||
.root > *:not([own-margin]) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
ha-alert[own-margin] {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
86
src/components/ha-form/types.ts
Normal file
86
src/components/ha-form/types.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { LitElement } from "lit";
|
||||
import type { HaDurationData } from "../ha-duration-input";
|
||||
|
||||
export type HaFormSchema =
|
||||
| HaFormConstantSchema
|
||||
| HaFormStringSchema
|
||||
| HaFormIntegerSchema
|
||||
| HaFormFloatSchema
|
||||
| HaFormBooleanSchema
|
||||
| HaFormSelectSchema
|
||||
| HaFormMultiSelectSchema
|
||||
| HaFormTimeSchema;
|
||||
|
||||
export interface HaFormBaseSchema {
|
||||
name: string;
|
||||
default?: HaFormData;
|
||||
required?: boolean;
|
||||
optional?: boolean;
|
||||
description?: { suffix?: string; suggested_value?: HaFormData };
|
||||
}
|
||||
|
||||
export interface HaFormConstantSchema extends HaFormBaseSchema {
|
||||
type: "constant";
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface HaFormIntegerSchema extends HaFormBaseSchema {
|
||||
type: "integer";
|
||||
default?: HaFormIntegerData;
|
||||
valueMin?: number;
|
||||
valueMax?: number;
|
||||
}
|
||||
|
||||
export interface HaFormSelectSchema extends HaFormBaseSchema {
|
||||
type: "select";
|
||||
options: Array<[string, string]>;
|
||||
}
|
||||
|
||||
export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
|
||||
type: "multi_select";
|
||||
options: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface HaFormFloatSchema extends HaFormBaseSchema {
|
||||
type: "float";
|
||||
}
|
||||
|
||||
export interface HaFormStringSchema extends HaFormBaseSchema {
|
||||
type: "string";
|
||||
format?: string;
|
||||
}
|
||||
|
||||
export interface HaFormBooleanSchema extends HaFormBaseSchema {
|
||||
type: "boolean";
|
||||
}
|
||||
|
||||
export interface HaFormTimeSchema extends HaFormBaseSchema {
|
||||
type: "positive_time_period_dict";
|
||||
}
|
||||
|
||||
export interface HaFormDataContainer {
|
||||
[key: string]: HaFormData;
|
||||
}
|
||||
|
||||
export type HaFormData =
|
||||
| HaFormStringData
|
||||
| HaFormIntegerData
|
||||
| HaFormFloatData
|
||||
| HaFormBooleanData
|
||||
| HaFormSelectData
|
||||
| HaFormMultiSelectData
|
||||
| HaFormTimeData;
|
||||
|
||||
export type HaFormStringData = string;
|
||||
export type HaFormIntegerData = number;
|
||||
export type HaFormFloatData = number;
|
||||
export type HaFormBooleanData = boolean;
|
||||
export type HaFormSelectData = string;
|
||||
export type HaFormMultiSelectData = string[];
|
||||
export type HaFormTimeData = HaDurationData;
|
||||
|
||||
export interface HaFormElement extends LitElement {
|
||||
schema: HaFormSchema | HaFormSchema[];
|
||||
data?: HaFormDataContainer | HaFormData;
|
||||
label?: string;
|
||||
}
|
||||
@@ -361,7 +361,10 @@ const mdiDeprecatedIcons: DeprecatedIcon = {
|
||||
|
||||
const chunks: Chunks = {};
|
||||
|
||||
checkCacheVersion();
|
||||
// Supervisor doesn't use icons, and should not update/downgrade the icon DB.
|
||||
if (!__SUPERVISOR__) {
|
||||
checkCacheVersion();
|
||||
}
|
||||
|
||||
const debouncedWriteCache = debounce(() => writeCache(chunks), 2000);
|
||||
|
||||
|
||||
@@ -8,13 +8,9 @@ import {
|
||||
} from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
class HaLabelBadge extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public description?: string;
|
||||
@@ -25,20 +21,8 @@ class HaLabelBadge extends LitElement {
|
||||
return html`
|
||||
<div class="badge-container">
|
||||
<div class="label-badge" id="badge">
|
||||
<div
|
||||
class=${classMap({
|
||||
value: true,
|
||||
big: Boolean(this.value && this.value.length > 4),
|
||||
})}
|
||||
>
|
||||
<slot>
|
||||
${this.icon && !this.value && !this.image
|
||||
? html`<ha-icon .icon=${this.icon}></ha-icon>`
|
||||
: ""}
|
||||
${this.value && !this.image
|
||||
? html`<span>${this.value}</span>`
|
||||
: ""}
|
||||
</slot>
|
||||
<div class="value">
|
||||
<slot></slot>
|
||||
</div>
|
||||
${this.label
|
||||
? html`
|
||||
@@ -54,7 +38,7 @@ class HaLabelBadge extends LitElement {
|
||||
: ""}
|
||||
</div>
|
||||
${this.description
|
||||
? html` <div class="title">${this.description}</div> `
|
||||
? html`<div class="title">${this.description}</div>`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
@@ -87,14 +71,15 @@ class HaLabelBadge extends LitElement {
|
||||
background-size: cover;
|
||||
transition: border 0.3s ease-in-out;
|
||||
}
|
||||
.label-badge .label.big span {
|
||||
font-size: 90%;
|
||||
padding: 10% 12% 7% 12%; /* push smaller text a bit down to center vertically */
|
||||
}
|
||||
.label-badge .value {
|
||||
font-size: 90%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.label-badge .value.big {
|
||||
font-size: 70%;
|
||||
}
|
||||
.label-badge .label {
|
||||
position: absolute;
|
||||
bottom: -1em;
|
||||
@@ -119,10 +104,6 @@ class HaLabelBadge extends LitElement {
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
text-transform: var(--ha-label-badge-label-text-transform, uppercase);
|
||||
}
|
||||
.label-badge .label.big span {
|
||||
font-size: 90%;
|
||||
padding: 10% 12% 7% 12%; /* push smaller text a bit down to center vertically */
|
||||
}
|
||||
.badge-container .title {
|
||||
margin-top: 1em;
|
||||
font-size: var(--ha-label-badge-title-font-size, 0.9em);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { relativeTime } from "../common/datetime/relative_time";
|
||||
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-relative-time")
|
||||
@@ -9,6 +10,8 @@ class HaRelativeTime extends ReactiveElement {
|
||||
|
||||
@property({ attribute: false }) public datetime?: string | Date;
|
||||
|
||||
@property({ type: Boolean }) public capitalize = false;
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
@@ -55,7 +58,10 @@ class HaRelativeTime extends ReactiveElement {
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
|
||||
} else {
|
||||
this.innerHTML = relativeTime(new Date(this.datetime), this.hass.locale);
|
||||
const relTime = relativeTime(new Date(this.datetime), this.hass.locale);
|
||||
this.innerHTML = this.capitalize
|
||||
? capitalizeFirstLetter(relTime)
|
||||
: relTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ export class HaAreaSelector extends LitElement {
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
.entityFilter=${(entity) => this._filterEntities(entity)}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
@@ -51,16 +51,16 @@ export class HaAreaSelector extends LitElement {
|
||||
></ha-area-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities(entity: EntityRegistryEntry): boolean {
|
||||
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
|
||||
if (this.selector.area.entity?.integration) {
|
||||
if (entity.platform !== this.selector.area.entity.integration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (
|
||||
this.selector.area.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.area.device.manufacturer
|
||||
@@ -84,7 +84,7 @@ export class HaAreaSelector extends LitElement {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
|
||||
@@ -34,7 +34,7 @@ export class HaDeviceSelector extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.includeDeviceClasses=${this.selector.device.entity?.device_class
|
||||
? [this.selector.device.entity.device_class]
|
||||
: undefined}
|
||||
@@ -46,7 +46,7 @@ export class HaDeviceSelector extends LitElement {
|
||||
></ha-device-picker>`;
|
||||
}
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (
|
||||
this.selector.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.device.manufacturer
|
||||
@@ -70,7 +70,7 @@ export class HaDeviceSelector extends LitElement {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
|
||||
@@ -27,7 +27,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.entityFilter=${(entity) => this._filterEntities(entity)}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`;
|
||||
@@ -48,7 +48,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _filterEntities(entity: HassEntity): boolean {
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (this.selector.entity?.domain) {
|
||||
if (computeStateDomain(entity) !== this.selector.entity.domain) {
|
||||
return false;
|
||||
@@ -72,7 +72,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -69,10 +69,9 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
return html`<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.deviceFilter=${(device) => this._filterDevices(device)}
|
||||
.entityRegFilter=${(entity: EntityRegistryEntry) =>
|
||||
this._filterRegEntities(entity)}
|
||||
.entityFilter=${(entity: HassEntity) => this._filterEntities(entity)}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityRegFilter=${this._filterRegEntities}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.target.entity?.device_class
|
||||
? [this.selector.target.entity.device_class]
|
||||
: undefined}
|
||||
@@ -83,7 +82,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities(entity: HassEntity): boolean {
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (
|
||||
this.selector.target.entity?.integration ||
|
||||
this.selector.target.device?.integration
|
||||
@@ -98,18 +97,18 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private _filterRegEntities(entity: EntityRegistryEntry): boolean {
|
||||
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => {
|
||||
if (this.selector.target.entity?.integration) {
|
||||
if (entity.platform !== this.selector.target.entity.integration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (
|
||||
this.selector.target.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.target.device.manufacturer
|
||||
@@ -135,7 +134,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-tab")
|
||||
export class HaTab extends LitElement {
|
||||
|
||||
@@ -484,6 +484,7 @@ export class HaMap extends ReactiveElement {
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ export class PaperTimeInput extends PolymerElement {
|
||||
<paper-input
|
||||
id="hour"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
value="{{hour}}"
|
||||
label="[[hourLabel]]"
|
||||
on-change="_shouldFormatHour"
|
||||
@@ -141,6 +142,7 @@ export class PaperTimeInput extends PolymerElement {
|
||||
class$="[[_computeClassNames(enableSecond)]]"
|
||||
id="min"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
value="{{min}}"
|
||||
label="[[minLabel]]"
|
||||
on-change="_formatMin"
|
||||
@@ -163,6 +165,7 @@ export class PaperTimeInput extends PolymerElement {
|
||||
class$="[[_computeClassNames(enableMillisecond)]]"
|
||||
id="sec"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
value="{{sec}}"
|
||||
label="[[secLabel]]"
|
||||
on-change="_formatSec"
|
||||
|
||||
@@ -511,18 +511,16 @@ export class HaAutomationTracer extends LitElement {
|
||||
className: isError ? "error" : undefined,
|
||||
};
|
||||
}
|
||||
// null means it was stopped by a condition
|
||||
if (entry) {
|
||||
entries.push(html`
|
||||
<ha-timeline
|
||||
lastItem
|
||||
.icon=${entry.icon}
|
||||
class=${ifDefined(entry.className)}
|
||||
>
|
||||
${entry.description}
|
||||
</ha-timeline>
|
||||
`);
|
||||
}
|
||||
|
||||
entries.push(html`
|
||||
<ha-timeline
|
||||
lastItem
|
||||
.icon=${entry.icon}
|
||||
class=${ifDefined(entry.className)}
|
||||
>
|
||||
${entry.description}
|
||||
</ha-timeline>
|
||||
`);
|
||||
|
||||
return html`${entries}`;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import { fetchUsers, User } from "../../data/user";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-button";
|
||||
import "./ha-user-badge";
|
||||
|
||||
class HaUserPicker extends LitElement {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Connection } from "home-assistant-js-websocket";
|
||||
import { HaFormSchema } from "../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import { ConfigEntry } from "./config_entries";
|
||||
|
||||
export interface DataEntryFlowProgressedEvent {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { HaFormSchema } from "../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { BaseTrigger } from "./automation";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import { HaFormSchema } from "../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../components/ha-form/types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { SupervisorArch } from "../supervisor/supervisor";
|
||||
import {
|
||||
|
||||
@@ -77,18 +77,42 @@ export interface StatisticsMetaData {
|
||||
}
|
||||
|
||||
export type StatisticsValidationResult =
|
||||
| StatisticsValidationResultUnsupportedUnit
|
||||
| StatisticsValidationResultUnitsChanged;
|
||||
| StatisticsValidationResultEntityNotRecorded
|
||||
| StatisticsValidationResultUnsupportedStateClass
|
||||
| StatisticsValidationResultUnitsChanged
|
||||
| StatisticsValidationResultUnsupportedUnitMetadata
|
||||
| StatisticsValidationResultUnsupportedUnitState;
|
||||
|
||||
export interface StatisticsValidationResultUnsupportedUnit {
|
||||
type: "unsupported_unit";
|
||||
data: { statistic_id: string; device_class: string; state_unit: string };
|
||||
export interface StatisticsValidationResultEntityNotRecorded {
|
||||
type: "entity_not_recorded";
|
||||
data: { statistic_id: string };
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResultUnsupportedStateClass {
|
||||
type: "unsupported_state_class";
|
||||
data: { statistic_id: string; state_class: string };
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResultUnitsChanged {
|
||||
type: "units_changed";
|
||||
data: { statistic_id: string; state_unit: string; metadata_unit: string };
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResultUnsupportedUnitMetadata {
|
||||
type: "unsupported_unit_metadata";
|
||||
data: {
|
||||
statistic_id: string;
|
||||
device_class: string;
|
||||
metadata_unit: string;
|
||||
supported_unit: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResultUnsupportedUnitState {
|
||||
type: "unsupported_unit_state";
|
||||
data: { statistic_id: string; device_class: string; metadata_unit: string };
|
||||
}
|
||||
|
||||
export interface StatisticsValidationResults {
|
||||
[statisticId: string]: StatisticsValidationResult[];
|
||||
}
|
||||
@@ -466,7 +490,7 @@ export const reduceSumStatisticsByDay = (
|
||||
// add init value if the first value isn't end of previous period
|
||||
result.push({
|
||||
...values[0]!,
|
||||
start: startOfMonth(addDays(new Date(values[0].start), -1)).toISOString(),
|
||||
start: startOfDay(addDays(new Date(values[0].start), -1)).toISOString(),
|
||||
});
|
||||
}
|
||||
let lastValue: StatisticValue;
|
||||
@@ -522,7 +546,7 @@ export const reduceSumStatisticsByMonth = (
|
||||
prevMonth = month;
|
||||
}
|
||||
if (prevMonth !== month) {
|
||||
// Last value of the day
|
||||
// Last value of the month
|
||||
result.push({
|
||||
...lastValue!,
|
||||
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export enum LightColorModes {
|
||||
export const enum LightColorModes {
|
||||
UNKNOWN = "unknown",
|
||||
ONOFF = "onoff",
|
||||
BRIGHTNESS = "brightness",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HaFormSchema } from "../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ZHAEntityReference extends HassEntity {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
|
||||
export enum InclusionStrategy {
|
||||
export const enum InclusionStrategy {
|
||||
/**
|
||||
* Always uses Security S2 if supported, otherwise uses Security S0 for certain devices which don't work without encryption and uses no encryption otherwise.
|
||||
*
|
||||
@@ -83,6 +83,7 @@ export interface ZWaveJSNodeStatus {
|
||||
node_id: number;
|
||||
ready: boolean;
|
||||
status: number;
|
||||
is_secure: boolean | string;
|
||||
}
|
||||
|
||||
export interface ZwaveJSNodeMetadata {
|
||||
@@ -154,7 +155,7 @@ export interface ZWaveJSRemovedNode {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export enum NodeStatus {
|
||||
export const enum NodeStatus {
|
||||
Unknown,
|
||||
Asleep,
|
||||
Awake,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TemplateResult } from "lit";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaFormSchema } from "../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../components/ha-form/types";
|
||||
import {
|
||||
DataEntryFlowStep,
|
||||
DataEntryFlowStepAbort,
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-circular-progress";
|
||||
import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data";
|
||||
import type { HaFormSchema } from "../../components/ha-form/types";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../components/ha-form/ha-form";
|
||||
import "../../components/ha-markdown";
|
||||
import "../../components/ha-alert";
|
||||
import type { DataEntryFlowStepForm } from "../../data/data_entry_flow";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
@@ -37,24 +39,13 @@ class StepFlowForm extends LitElement {
|
||||
const step = this.step;
|
||||
const stepData = this._stepDataProcessed;
|
||||
|
||||
const allRequiredInfoFilledIn =
|
||||
stepData === undefined
|
||||
? // If no data filled in, just check that any field is required
|
||||
step.data_schema.find((field) => !field.optional) === undefined
|
||||
: // If data is filled in, make sure all required fields are
|
||||
stepData &&
|
||||
step.data_schema.every(
|
||||
(field) =>
|
||||
field.optional || !["", undefined].includes(stepData![field.name])
|
||||
);
|
||||
|
||||
return html`
|
||||
<h2>${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)}</h2>
|
||||
<div class="content">
|
||||
${this._errorMsg
|
||||
? html` <div class="error">${this._errorMsg}</div> `
|
||||
: ""}
|
||||
${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)}
|
||||
${this._errorMsg
|
||||
? html`<ha-alert alert-type="error">${this._errorMsg}</ha-alert>`
|
||||
: ""}
|
||||
<ha-form
|
||||
.data=${stepData}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
@@ -73,25 +64,13 @@ class StepFlowForm extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<div>
|
||||
<mwc-button
|
||||
@click=${this._submitStep}
|
||||
.disabled=${!allRequiredInfoFilledIn}
|
||||
>${this.hass.localize(
|
||||
<mwc-button @click=${this._submitStep}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_flow.${
|
||||
this.step.last_step === false ? "next" : "submit"
|
||||
}`
|
||||
)}
|
||||
</mwc-button>
|
||||
|
||||
${!allRequiredInfoFilledIn
|
||||
? html`
|
||||
<paper-tooltip animation-delay="0" position="left"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.not_all_required_fields"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`
|
||||
: html``}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
@@ -113,25 +92,35 @@ class StepFlowForm extends LitElement {
|
||||
return this._stepData;
|
||||
}
|
||||
|
||||
const data = {};
|
||||
this.step.data_schema.forEach((field) => {
|
||||
if (field.description?.suggested_value) {
|
||||
data[field.name] = field.description.suggested_value;
|
||||
} else if ("default" in field) {
|
||||
data[field.name] = field.default;
|
||||
}
|
||||
});
|
||||
|
||||
this._stepData = data;
|
||||
return data;
|
||||
this._stepData = computeInitialHaFormData(this.step.data_schema);
|
||||
return this._stepData;
|
||||
}
|
||||
|
||||
private async _submitStep(): Promise<void> {
|
||||
const stepData = this._stepData || {};
|
||||
|
||||
const allRequiredInfoFilledIn =
|
||||
stepData === undefined
|
||||
? // If no data filled in, just check that any field is required
|
||||
this.step.data_schema.find((field) => !field.optional) === undefined
|
||||
: // If data is filled in, make sure all required fields are
|
||||
stepData &&
|
||||
this.step.data_schema.every(
|
||||
(field) =>
|
||||
field.optional || !["", undefined].includes(stepData![field.name])
|
||||
);
|
||||
|
||||
if (!allRequiredInfoFilledIn) {
|
||||
this._errorMsg = this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.not_all_required_fields"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
this._errorMsg = undefined;
|
||||
|
||||
const flowId = this.step.flow_id;
|
||||
const stepData = this._stepData || {};
|
||||
|
||||
const toSendData = {};
|
||||
Object.keys(stepData).forEach((key) => {
|
||||
@@ -188,6 +177,12 @@ class StepFlowForm extends LitElement {
|
||||
.submit-spinner {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
ha-alert,
|
||||
ha-form {
|
||||
margin-top: 24px;
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../common/search/search-input";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../../components/ha-icon-next";
|
||||
import { domainToName } from "../../data/integration";
|
||||
@@ -59,7 +60,7 @@ class StepFlowPickHandler extends LitElement {
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
}
|
||||
return handlers.sort((a, b) =>
|
||||
a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1
|
||||
caseInsensitiveStringCompare(a.name, b.name)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -26,8 +26,8 @@ export const configFlowContentStyles = css`
|
||||
|
||||
.buttons {
|
||||
position: relative;
|
||||
padding: 8px 8px 8px 24px;
|
||||
margin: 0;
|
||||
padding: 8px 16px 8px 24px;
|
||||
margin: 8px 0 0;
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -25,6 +25,7 @@ class MoreInfoAutomation extends LitElement {
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.attributes.last_triggered}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ class MoreInfoScript extends LitElement {
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.attributes.last_triggered}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`
|
||||
: this.hass.localize("ui.components.relative_time.never")}
|
||||
|
||||
@@ -73,9 +73,6 @@ class MoreInfoSun extends LitElement {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
ha-relative-time::first-letter {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user