mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-15 21:02:10 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 900efcba6c |
@@ -30,6 +30,7 @@ jobs:
|
||||
"Do Not Review",
|
||||
"Blocked",
|
||||
"has-parent",
|
||||
"Needs Template",
|
||||
];
|
||||
const prLabels = context.payload.pull_request.labels.map(
|
||||
(l) => l.name
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: false
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -41,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -62,4 +62,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: false
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
name: Pull request standards
|
||||
|
||||
on:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] -- safe: reads PR metadata from event payload only, no PR code checkout
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- ready_for_review
|
||||
branches:
|
||||
- dev
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check pull request follows contribution standards
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write # To label and comment on pull requests
|
||||
steps:
|
||||
- name: Check pull request standards
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Exempt bots (Copilot agent, dependabot), drafts, and maintainers.
|
||||
if (pr.user.type === "Bot") {
|
||||
core.info(`Skipping bot author: ${pr.user.login}`);
|
||||
return;
|
||||
}
|
||||
if (pr.draft) {
|
||||
core.info("Skipping draft pull request");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: "home-assistant",
|
||||
username: pr.user.login,
|
||||
});
|
||||
core.info(`Skipping organization member: ${pr.user.login}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
core.info(`${pr.user.login} is not an organization member, checking standards`);
|
||||
}
|
||||
|
||||
const label = "Needs Template";
|
||||
const marker = "<!-- pr-standards-check -->";
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = pr.number;
|
||||
|
||||
const body = (pr.body || "").replace(/<!--[\s\S]*?-->/g, "");
|
||||
const normalized = body.toLowerCase();
|
||||
|
||||
// Ignore 404s from mutations that race manual edits or cancelled runs.
|
||||
const ignoreMissing = async (fn) => {
|
||||
try {
|
||||
await fn();
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info("Target already removed, nothing to do");
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Hide/restore our comment via GraphQL (REST cannot minimize).
|
||||
const setMinimized = async (subjectId, minimized) => {
|
||||
const mutation = minimized
|
||||
? `mutation($id: ID!) {
|
||||
minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`
|
||||
: `mutation($id: ID!) {
|
||||
unminimizeComment(input: { subjectId: $id }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
try {
|
||||
await github.graphql(mutation, { id: subjectId });
|
||||
} catch (error) {
|
||||
core.info(
|
||||
`Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Content of a "## <name>" section, or null when the heading is absent.
|
||||
const section = (name) => {
|
||||
const match = body.match(
|
||||
new RegExp(`##\\s*${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i")
|
||||
);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const problems = [];
|
||||
|
||||
const requiredHeadings = [
|
||||
"## proposed change",
|
||||
"## type of change",
|
||||
"## checklist",
|
||||
];
|
||||
if (requiredHeadings.some((h) => !normalized.includes(h))) {
|
||||
problems.push(
|
||||
"Use the pull request template without removing its sections."
|
||||
);
|
||||
}
|
||||
|
||||
const typeOfChange = section("type of change");
|
||||
if (typeOfChange !== null) {
|
||||
const ticked = (typeOfChange.match(/-\s*\[[xX]\]/g) || []).length;
|
||||
if (ticked !== 1) {
|
||||
problems.push(
|
||||
'Select exactly one option under "Type of change".'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const proposedChange = section("proposed change");
|
||||
if (proposedChange !== null && proposedChange.trim().length === 0) {
|
||||
problems.push('Describe your changes under "Proposed change".');
|
||||
}
|
||||
|
||||
const isValid = problems.length === 0;
|
||||
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number, per_page: 100 }
|
||||
);
|
||||
const existing = comments.find((c) => c.body.includes(marker));
|
||||
const hasLabel = pr.labels.some((l) => l.name === label);
|
||||
|
||||
if (isValid) {
|
||||
core.info("Pull request standards met");
|
||||
|
||||
if (hasLabel) {
|
||||
await ignoreMissing(() =>
|
||||
github.rest.issues.removeLabel({
|
||||
owner, repo, issue_number, name: label,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (existing) {
|
||||
await setMinimized(existing.node_id, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
|
||||
|
||||
if (!hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number, labels: [label],
|
||||
});
|
||||
}
|
||||
|
||||
const message =
|
||||
`${marker}\n` +
|
||||
`Hey @${pr.user.login}!\n\n` +
|
||||
`Thank you for your contribution! To help reviewers, please update ` +
|
||||
`this pull request to follow our pull request standards:\n\n` +
|
||||
problems.map((p) => `- ${p}`).join("\n") +
|
||||
`\n\n` +
|
||||
`Please complete the ` +
|
||||
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
|
||||
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
|
||||
`for more on creating a great pull request (see point 6).`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo, comment_id: existing.id, body: message,
|
||||
});
|
||||
await setMinimized(existing.node_id, false);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number, body: message,
|
||||
});
|
||||
}
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@e91ad1948e57189485b9c1ad608af0c303946f89 # master
|
||||
uses: home-assistant/actions/helpers/verify-version@868e6cb4607727d764341a158d98872cd63fa658 # master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
|
||||
# home-assistant/wheels doesn't support SHA pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
|
||||
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
with:
|
||||
abi: cp314
|
||||
tag: musllinux_1_2
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
+8
-8
@@ -34,13 +34,13 @@
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/lint": "6.9.7",
|
||||
"@codemirror/lint": "6.9.6",
|
||||
"@codemirror/search": "6.7.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.43.1",
|
||||
"@codemirror/view": "6.43.0",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.9",
|
||||
"@formatjs/intl-datetimeformat": "7.4.8",
|
||||
"@formatjs/intl-displaynames": "7.3.10",
|
||||
"@formatjs/intl-durationformat": "0.10.14",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.10",
|
||||
@@ -131,13 +131,13 @@
|
||||
"@babel/preset-env": "7.29.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.62.0",
|
||||
"@html-eslint/eslint-plugin": "0.61.0",
|
||||
"@lokalise/node-api": "16.0.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.13",
|
||||
"@rspack/core": "2.0.8",
|
||||
"@rsdoctor/rspack-plugin": "1.5.12",
|
||||
"@rspack/core": "2.0.6",
|
||||
"@rspack/dev-server": "2.0.3",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -186,7 +186,7 @@
|
||||
"lodash.template": "4.18.1",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.8.4",
|
||||
"prettier": "3.8.3",
|
||||
"rspack-manifest-plugin": "5.2.2",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.0.0",
|
||||
@@ -194,7 +194,7 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.61.0",
|
||||
"typescript-eslint": "8.60.1",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.8",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Load a resource and get a promise when loading done.
|
||||
// From: https://davidwalsh.name/javascript-loader
|
||||
|
||||
const _load = (tag: "link" | "script", url: string, type?: "module") =>
|
||||
const _load = (tag: "link" | "script" | "img", url: string, type?: "module") =>
|
||||
// This promise will be used by Promise.all to determine success or failure
|
||||
new Promise((resolve, reject) => {
|
||||
const element = document.createElement(tag);
|
||||
@@ -33,4 +33,5 @@ const _load = (tag: "link" | "script", url: string, type?: "module") =>
|
||||
});
|
||||
export const loadCSS = (url: string) => _load("link", url);
|
||||
export const loadJS = (url: string) => _load("script", url);
|
||||
export const loadImg = (url: string) => _load("img", url);
|
||||
export const loadModule = (url: string) => _load("script", url, "module");
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Scroll to a specific y coordinate.
|
||||
*
|
||||
* Copied from paper-scroll-header-panel.
|
||||
*
|
||||
* @method scroll
|
||||
* @param {number} top The coordinate to scroll to, along the y-axis.
|
||||
* @param {boolean} smooth true if the scroll position should be smoothly adjusted.
|
||||
*/
|
||||
export default function scrollToTarget(element, target) {
|
||||
// the scroll event will trigger _updateScrollState directly,
|
||||
// However, _updateScrollState relies on the previous `scrollTop` to update the states.
|
||||
// Calling _updateScrollState will ensure that the states are synced correctly.
|
||||
const top = 0;
|
||||
const scroller = target;
|
||||
const easingFn = function easeOutQuad(t, b, c, d) {
|
||||
t /= d;
|
||||
return -c * t * (t - 2) + b;
|
||||
};
|
||||
const animationId = Math.random();
|
||||
const duration = 200;
|
||||
const startTime = Date.now();
|
||||
const currentScrollTop = scroller.scrollTop;
|
||||
const deltaScrollTop = top - currentScrollTop;
|
||||
element._currentAnimationId = animationId;
|
||||
(function updateFrame() {
|
||||
const now = Date.now();
|
||||
const elapsedTime = now - startTime;
|
||||
if (elapsedTime > duration) {
|
||||
scroller.scrollTop = top;
|
||||
} else if (element._currentAnimationId === animationId) {
|
||||
scroller.scrollTop = easingFn(
|
||||
elapsedTime,
|
||||
currentScrollTop,
|
||||
deltaScrollTop,
|
||||
duration
|
||||
);
|
||||
requestAnimationFrame(updateFrame.bind(element));
|
||||
}
|
||||
}).call(element);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import type { Map, TileLayer } from "leaflet";
|
||||
// Sets up a Leaflet map on the provided DOM element
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
export type LeafletModuleType = typeof import("leaflet");
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
||||
|
||||
export const setupLeafletMap = async (
|
||||
mapElement: HTMLElement,
|
||||
@@ -43,6 +45,17 @@ export const setupLeafletMap = async (
|
||||
return [map, Leaflet, tileLayer];
|
||||
};
|
||||
|
||||
export const replaceTileLayer = (
|
||||
leaflet: LeafletModuleType,
|
||||
map: Map,
|
||||
tileLayer: TileLayer
|
||||
): TileLayer => {
|
||||
map.removeLayer(tileLayer);
|
||||
tileLayer = createTileLayer(leaflet);
|
||||
tileLayer.addTo(map);
|
||||
return tileLayer;
|
||||
};
|
||||
|
||||
const createTileLayer = (leaflet: LeafletModuleType): TileLayer =>
|
||||
leaflet.tileLayer(
|
||||
`https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}${
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
/** An empty image which can be set as src of an img element. */
|
||||
export const emptyImageBase64 =
|
||||
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
@@ -19,40 +19,6 @@ import type { LocalizeFunc } from "../translations/localize";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { SENSOR_TIMESTAMP_DEVICE_CLASSES } from "../../data/sensor";
|
||||
|
||||
// Domains whose state is a timezone-agnostic date and/or time string.
|
||||
const DATE_TIME_DOMAINS = new Set(["date", "input_datetime", "time"]);
|
||||
|
||||
// Domains whose state is a timestamp.
|
||||
const TIMESTAMP_DOMAINS = new Set([
|
||||
"ai_task",
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
]);
|
||||
|
||||
// Maps Intl.NumberFormat part types to ValuePart types for monetary states.
|
||||
const MONETARY_TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
minusSign: "value",
|
||||
plusSign: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
@@ -172,10 +138,21 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
}
|
||||
|
||||
if (parts.length) {
|
||||
const TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
minusSign: "value",
|
||||
plusSign: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
const type = MONETARY_TYPE_MAP[part.type];
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive value parts (e.g. "-" + "12" + "." + "00" → "-12.00")
|
||||
@@ -214,7 +191,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
return [{ type: "value", value: value }];
|
||||
}
|
||||
|
||||
if (DATE_TIME_DOMAINS.has(domain)) {
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||
// Attributes aren't available, we have to use `state`.
|
||||
|
||||
@@ -273,7 +250,23 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
|
||||
// state is a timestamp
|
||||
if (
|
||||
TIMESTAMP_DOMAINS.has(domain) ||
|
||||
[
|
||||
"ai_task",
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
].includes(domain) ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(attributes.device_class))
|
||||
) {
|
||||
|
||||
@@ -4,10 +4,9 @@ import { updateIsInstalling } from "../../data/update";
|
||||
|
||||
export const updateIcon = (stateObj: HassEntity, state?: string) => {
|
||||
const compareState = state ?? stateObj.state;
|
||||
// An install can be in progress even when the state is "off", e.g. when
|
||||
// downgrading firmware. Show the installing icon regardless of state.
|
||||
if (updateIsInstalling(stateObj as UpdateEntity)) {
|
||||
return "mdi:package-down";
|
||||
}
|
||||
return compareState === "on" ? "mdi:package-up" : "mdi:package";
|
||||
return compareState === "on"
|
||||
? updateIsInstalling(stateObj as UpdateEntity)
|
||||
? "mdi:package-down"
|
||||
: "mdi:package-up"
|
||||
: "mdi:package";
|
||||
};
|
||||
|
||||
@@ -40,25 +40,6 @@ export const numberFormatToLocale = (
|
||||
}
|
||||
};
|
||||
|
||||
// Constructing an Intl.NumberFormat is comparatively expensive, and these
|
||||
// formatters are created on every numeric state render. The number of distinct
|
||||
// (locale, options) combinations is small and bounded in practice, so cache the
|
||||
// instances instead of rebuilding them on every call.
|
||||
const numberFormatCache = new Map<string, Intl.NumberFormat>();
|
||||
|
||||
const getNumberFormatter = (
|
||||
locale: string | string[] | undefined,
|
||||
options: Intl.NumberFormatOptions
|
||||
): Intl.NumberFormat => {
|
||||
const key = JSON.stringify([locale, options]);
|
||||
let formatter = numberFormatCache.get(key);
|
||||
if (!formatter) {
|
||||
formatter = new Intl.NumberFormat(locale, options);
|
||||
numberFormatCache.set(key, formatter);
|
||||
}
|
||||
return formatter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
|
||||
*
|
||||
@@ -94,7 +75,7 @@ export const formatNumberToParts = (
|
||||
localeOptions?.number_format !== NumberFormat.none &&
|
||||
!Number.isNaN(Number(num))
|
||||
) {
|
||||
return getNumberFormatter(
|
||||
return new Intl.NumberFormat(
|
||||
locale,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).formatToParts(Number(num));
|
||||
@@ -106,7 +87,7 @@ export const formatNumberToParts = (
|
||||
localeOptions?.number_format === NumberFormat.none
|
||||
) {
|
||||
// If NumberFormat is none, use en-US format without grouping.
|
||||
return getNumberFormatter(
|
||||
return new Intl.NumberFormat(
|
||||
"en-US",
|
||||
getDefaultFormatOptions(num, {
|
||||
...options,
|
||||
|
||||
@@ -11,12 +11,6 @@ export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
|
||||
}
|
||||
|
||||
const root = rootEl || deepActiveElement()?.getRootNode() || document.body;
|
||||
// A document node cannot have a textarea appended directly (only the single
|
||||
// documentElement is allowed), so fall back to its body. Shadow roots and
|
||||
// elements can hold the textarea directly, which keeps execCommand working
|
||||
// inside dialogs that trap focus.
|
||||
const container: Node =
|
||||
root.nodeType === Node.DOCUMENT_NODE ? document.body : root;
|
||||
|
||||
const el = document.createElement("textarea");
|
||||
el.value = str;
|
||||
@@ -25,8 +19,8 @@ export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
|
||||
el.style.top = "0";
|
||||
el.style.left = "0";
|
||||
el.style.opacity = "0";
|
||||
container.appendChild(el);
|
||||
root.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
container.removeChild(el);
|
||||
root.removeChild(el);
|
||||
};
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import type { ReactiveControllerHost } from "lit";
|
||||
import { clamp } from "../number/clamp";
|
||||
|
||||
// Count columns from the container's real width (not the viewport) so a
|
||||
// docked sidebar is accounted for, like the dashboard sections view.
|
||||
const MIN_COLUMN_WIDTH = 320;
|
||||
const DEFAULT_COLUMN_GAP = 16;
|
||||
|
||||
const parsePx = (value: string) => parseInt(value, 10) || 0;
|
||||
|
||||
export const createColumnsController = (
|
||||
host: ReactiveControllerHost & Element,
|
||||
maxColumns: number
|
||||
) =>
|
||||
new ResizeController<number>(host, {
|
||||
target: null,
|
||||
skipInitial: true,
|
||||
callback: (entries) => {
|
||||
const entry = entries[0];
|
||||
if (!entry) {
|
||||
return maxColumns;
|
||||
}
|
||||
const width = entry.contentRect.width;
|
||||
const gap =
|
||||
parsePx(getComputedStyle(entry.target).columnGap) || DEFAULT_COLUMN_GAP;
|
||||
const columns = Math.floor((width + gap) / (MIN_COLUMN_WIDTH + gap));
|
||||
return clamp(columns, 1, maxColumns);
|
||||
},
|
||||
});
|
||||
@@ -1,23 +1,5 @@
|
||||
import type { LineSeriesOption } from "echarts";
|
||||
|
||||
type Point = NonNullable<LineSeriesOption["data"]>[number];
|
||||
|
||||
interface MeanFrame {
|
||||
sumX: number;
|
||||
sumY: number;
|
||||
count: number;
|
||||
isArray: boolean;
|
||||
}
|
||||
|
||||
interface MinMaxFrame {
|
||||
minPoint: Point;
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxPoint: Point;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
export function downSampleLineData<
|
||||
T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number],
|
||||
>(
|
||||
@@ -37,47 +19,11 @@ export function downSampleLineData<
|
||||
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
|
||||
const step = Math.ceil((max - min) / Math.floor(maxDetails));
|
||||
|
||||
if (useMean) {
|
||||
// Group points into frames, accumulating sums in insertion order.
|
||||
const frames = new Map<number, MeanFrame>();
|
||||
|
||||
for (const point of data) {
|
||||
const pointData = getPointData(point);
|
||||
if (!Array.isArray(pointData)) continue;
|
||||
const x = Number(pointData[0]);
|
||||
const y = Number(pointData[1]);
|
||||
if (isNaN(x) || isNaN(y)) continue;
|
||||
|
||||
const frameIndex = Math.floor((x - min) / step);
|
||||
const frame = frames.get(frameIndex);
|
||||
if (!frame) {
|
||||
frames.set(frameIndex, {
|
||||
sumX: x,
|
||||
sumY: y,
|
||||
count: 1,
|
||||
isArray: Array.isArray(pointData),
|
||||
});
|
||||
} else {
|
||||
frame.sumX += x;
|
||||
frame.sumY += y;
|
||||
frame.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const result: T[] = [];
|
||||
for (const frame of frames.values()) {
|
||||
const meanX = frame.sumX / frame.count;
|
||||
const meanY = frame.sumY / frame.count;
|
||||
const meanPoint = (
|
||||
frame.isArray ? [meanX, meanY] : { value: [meanX, meanY] }
|
||||
) as T;
|
||||
result.push(meanPoint);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Min/max mode: track the min and max point per frame in insertion order.
|
||||
const frames = new Map<number, MinMaxFrame>();
|
||||
// Group points into frames
|
||||
const frames = new Map<
|
||||
number,
|
||||
{ point: (typeof data)[number]; x: number; y: number }[]
|
||||
>();
|
||||
|
||||
for (const point of data) {
|
||||
const pointData = getPointData(point);
|
||||
@@ -89,39 +35,53 @@ export function downSampleLineData<
|
||||
const frameIndex = Math.floor((x - min) / step);
|
||||
const frame = frames.get(frameIndex);
|
||||
if (!frame) {
|
||||
frames.set(frameIndex, {
|
||||
minPoint: point,
|
||||
minX: x,
|
||||
minY: y,
|
||||
maxPoint: point,
|
||||
maxX: x,
|
||||
maxY: y,
|
||||
});
|
||||
frames.set(frameIndex, [{ point, x, y }]);
|
||||
} else {
|
||||
// Match the original strict-less / strict-greater comparisons so the
|
||||
// first occurrence wins on ties.
|
||||
if (y < frame.minY) {
|
||||
frame.minPoint = point;
|
||||
frame.minX = x;
|
||||
frame.minY = y;
|
||||
}
|
||||
if (y > frame.maxY) {
|
||||
frame.maxPoint = point;
|
||||
frame.maxX = x;
|
||||
frame.maxY = y;
|
||||
}
|
||||
frame.push({ point, x, y });
|
||||
}
|
||||
}
|
||||
|
||||
// Convert frames back to points
|
||||
const result: T[] = [];
|
||||
for (const frame of frames.values()) {
|
||||
// The order of the data must be preserved so max may be before min
|
||||
if (frame.minX > frame.maxX) {
|
||||
result.push(frame.maxPoint as T);
|
||||
|
||||
if (useMean) {
|
||||
// Use mean values for each frame
|
||||
for (const [_i, framePoints] of frames) {
|
||||
const sumY = framePoints.reduce((acc, p) => acc + p.y, 0);
|
||||
const meanY = sumY / framePoints.length;
|
||||
const sumX = framePoints.reduce((acc, p) => acc + p.x, 0);
|
||||
const meanX = sumX / framePoints.length;
|
||||
|
||||
const firstPoint = framePoints[0].point;
|
||||
const pointData = getPointData(firstPoint);
|
||||
const meanPoint = (
|
||||
Array.isArray(pointData) ? [meanX, meanY] : { value: [meanX, meanY] }
|
||||
) as T;
|
||||
result.push(meanPoint);
|
||||
}
|
||||
result.push(frame.minPoint as T);
|
||||
if (frame.minX < frame.maxX) {
|
||||
result.push(frame.maxPoint as T);
|
||||
} else {
|
||||
// Use min/max values for each frame
|
||||
for (const [_i, framePoints] of frames) {
|
||||
let minPoint = framePoints[0];
|
||||
let maxPoint = framePoints[0];
|
||||
|
||||
for (const p of framePoints) {
|
||||
if (p.y < minPoint.y) {
|
||||
minPoint = p;
|
||||
}
|
||||
if (p.y > maxPoint.y) {
|
||||
maxPoint = p;
|
||||
}
|
||||
}
|
||||
|
||||
// The order of the data must be preserved so max may be before min
|
||||
if (minPoint.x > maxPoint.x) {
|
||||
result.push(maxPoint.point);
|
||||
}
|
||||
result.push(minPoint.point);
|
||||
if (minPoint.x < maxPoint.x) {
|
||||
result.push(maxPoint.point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -394,18 +394,6 @@ export class HaChartBase extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
const datasets = ensureArray(this.data!);
|
||||
// Index datasets by id and name so each legend item is an O(1) lookup
|
||||
// instead of scanning every dataset twice. Charts can have many series.
|
||||
const datasetById = new Map<unknown, (typeof datasets)[number]>();
|
||||
const datasetByName = new Map<unknown, (typeof datasets)[number]>();
|
||||
for (const dataset of datasets) {
|
||||
if (dataset.id !== undefined && !datasetById.has(dataset.id)) {
|
||||
datasetById.set(dataset.id, dataset);
|
||||
}
|
||||
if (dataset.name !== undefined && !datasetByName.has(dataset.name)) {
|
||||
datasetByName.set(dataset.name, dataset);
|
||||
}
|
||||
}
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
@@ -425,10 +413,10 @@ export class HaChartBase extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
let itemStyle: Record<string, any> = {};
|
||||
let id = "";
|
||||
let value = "";
|
||||
let noLabelClick = false;
|
||||
const name = typeof item === "string" ? item : (item.name ?? "");
|
||||
let id: string;
|
||||
if (typeof item === "string") {
|
||||
id = item;
|
||||
} else {
|
||||
@@ -438,7 +426,9 @@ export class HaChartBase extends LitElement {
|
||||
noLabelClick = item.noLabelClick ?? false;
|
||||
}
|
||||
const labelClickable = this.clickLabelForMoreInfo && !noLabelClick;
|
||||
const dataset = datasetById.get(id) ?? datasetByName.get(id);
|
||||
const dataset =
|
||||
datasets.find((d) => d.id === id) ??
|
||||
datasets.find((d) => d.name === id);
|
||||
itemStyle = {
|
||||
color: dataset?.color as string,
|
||||
...(dataset?.itemStyle as { borderColor?: string }),
|
||||
@@ -1530,9 +1520,7 @@ export class HaChartBase extends LitElement {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
/* overflow: hidden clips descenders (e.g. "g", parentheses) with a tight
|
||||
line-height, so give the line box room to contain them */
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
line-height: 1;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.chart-legend .label.clickable:hover {
|
||||
@@ -1570,25 +1558,6 @@ export class HaChartBase extends LitElement {
|
||||
.chart-legend .legend-toggle ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
/* On touch devices, enlarge the toggle tap target via taller rows and
|
||||
leading padding (which also separates it from the previous item), while
|
||||
keeping the icon tight to its own label so the pairing stays clear.
|
||||
Drop the now-pointless row gap and li padding. */
|
||||
@media (pointer: coarse) {
|
||||
.chart-legend ul {
|
||||
row-gap: 0;
|
||||
}
|
||||
/* Only grow the toggle rows, not the expand/collapse chip's row. */
|
||||
.chart-legend li:has(.legend-toggle) {
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
.chart-legend .legend-toggle {
|
||||
padding: 11px;
|
||||
padding-inline-end: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
ha-assist-chip {
|
||||
height: 100%;
|
||||
--_label-text-weight: 500;
|
||||
|
||||
@@ -147,14 +147,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
this.hass.config
|
||||
);
|
||||
const datapoints: Record<string, any>[] = [];
|
||||
// Index the hovered points by series so the per-dataset lookup below is
|
||||
// O(1) instead of scanning `params` for every dataset on each mouse move.
|
||||
const paramsBySeriesIndex = new Map<number, Record<string, any>>();
|
||||
for (const p of params) {
|
||||
if (!paramsBySeriesIndex.has(p.seriesIndex)) {
|
||||
paramsBySeriesIndex.set(p.seriesIndex, p);
|
||||
}
|
||||
}
|
||||
this._chartData.forEach((dataset, index) => {
|
||||
if (
|
||||
dataset.tooltip?.show === false ||
|
||||
@@ -162,7 +154,9 @@ export class StateHistoryChartLine extends LitElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const param = paramsBySeriesIndex.get(index);
|
||||
const param = params.find(
|
||||
(p: Record<string, any>) => p.seriesIndex === index
|
||||
);
|
||||
if (param) {
|
||||
datapoints.push(param);
|
||||
return;
|
||||
@@ -446,10 +440,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
this._chartTime = new Date();
|
||||
const endTime = this.endTime;
|
||||
// Work with numeric epoch timestamps (ms) instead of Date objects below.
|
||||
// Charts can hold a huge number of points, and allocating a Date per point
|
||||
// is needless GC pressure; the "time" axis consumes numbers natively.
|
||||
const endTimeMs = endTime.getTime();
|
||||
const names = this.names || {};
|
||||
const colors = this.colors || {};
|
||||
entityStates.forEach((states, dataIdx) => {
|
||||
@@ -461,9 +451,9 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
const data: LineSeriesOption[] = [];
|
||||
|
||||
const pushData = (timestamp: number, datavalues: any[] | null) => {
|
||||
const pushData = (timestamp: Date, datavalues: any[] | null) => {
|
||||
if (!datavalues) return;
|
||||
if (timestamp > endTimeMs) {
|
||||
if (timestamp > endTime) {
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
return;
|
||||
@@ -634,11 +624,11 @@ export class StateHistoryChartLine extends LitElement {
|
||||
entityState.attributes.target_temp_low
|
||||
);
|
||||
series.push(targetHigh, targetLow);
|
||||
pushData(entityState.last_changed, series);
|
||||
pushData(new Date(entityState.last_changed), series);
|
||||
} else {
|
||||
const target = safeParseFloat(entityState.attributes.temperature);
|
||||
series.push(target);
|
||||
pushData(entityState.last_changed, series);
|
||||
pushData(new Date(entityState.last_changed), series);
|
||||
}
|
||||
});
|
||||
} else if (domain === "humidifier") {
|
||||
@@ -756,27 +746,31 @@ export class StateHistoryChartLine extends LitElement {
|
||||
} else {
|
||||
series.push(entityState.state === "on" ? current : null);
|
||||
}
|
||||
pushData(entityState.last_changed, series);
|
||||
pushData(new Date(entityState.last_changed), series);
|
||||
});
|
||||
} else {
|
||||
addDataSet(states.entity_id, name, color);
|
||||
|
||||
let lastValue: number;
|
||||
let lastDate: number;
|
||||
let lastNullDate: number | null = null;
|
||||
let lastDate: Date;
|
||||
let lastNullDate: Date | null = null;
|
||||
|
||||
// Process chart data.
|
||||
// When state is `unknown`, calculate the value and break the line.
|
||||
const processData = (entityState: LineChartState) => {
|
||||
const value = safeParseFloat(entityState.state);
|
||||
const date = entityState.last_changed;
|
||||
const date = new Date(entityState.last_changed);
|
||||
if (value !== null && lastNullDate) {
|
||||
const dateTime = date.getTime();
|
||||
const lastNullDateTime = lastNullDate.getTime();
|
||||
const lastDateTime = lastDate?.getTime();
|
||||
const tmpValue =
|
||||
(value - lastValue) *
|
||||
((lastNullDate - lastDate) / (date - lastDate)) +
|
||||
((lastNullDateTime - lastDateTime) /
|
||||
(dateTime - lastDateTime)) +
|
||||
lastValue;
|
||||
pushData(lastNullDate, [tmpValue]);
|
||||
pushData(lastNullDate + 1, [null]);
|
||||
pushData(new Date(lastNullDateTime + 1), [null]);
|
||||
pushData(date, [value]);
|
||||
lastDate = date;
|
||||
lastValue = value;
|
||||
@@ -815,17 +809,17 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
|
||||
// Add an entry for final values
|
||||
pushData(endTimeMs, prevValues);
|
||||
pushData(endTime, prevValues);
|
||||
|
||||
// For sensors, append current state if viewing recent data
|
||||
const nowMs = Date.now();
|
||||
const now = new Date();
|
||||
// allow 1s of leeway for "now"
|
||||
const isUpToNow = nowMs - endTimeMs <= 1000;
|
||||
const isUpToNow = now.getTime() - endTime.getTime() <= 1000;
|
||||
if (domain === "sensor" && isUpToNow && data.length === 1) {
|
||||
const stateObj = this.hass.states[states.entity_id];
|
||||
const currentValue = stateObj ? safeParseFloat(stateObj.state) : null;
|
||||
if (currentValue !== null) {
|
||||
data[0].data!.push([nowMs, currentValue]);
|
||||
data[0].data!.push([now, currentValue]);
|
||||
trackY(currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,16 +215,10 @@ export class HaDataTable extends LitElement {
|
||||
if (clear) {
|
||||
this._checkedRows = [];
|
||||
}
|
||||
// Map + Set keep a large selection O(rows + ids) instead of O(rows × ids).
|
||||
const rowLookup = new Map(
|
||||
(this._filteredData || []).map((data) => [data[this.id], data])
|
||||
);
|
||||
const checkedRows = new Set(this._checkedRows);
|
||||
ids.forEach((id) => {
|
||||
const row = rowLookup.get(id);
|
||||
if (row?.selectable !== false && !checkedRows.has(id)) {
|
||||
const row = this._filteredData?.find((data) => data[this.id] === id);
|
||||
if (row?.selectable !== false && !this._checkedRows.includes(id)) {
|
||||
this._checkedRows.push(id);
|
||||
checkedRows.add(id);
|
||||
}
|
||||
});
|
||||
this._lastSelectedRowId = null;
|
||||
|
||||
@@ -183,7 +183,6 @@ export class HaControlSelectMenu extends LitElement {
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
font-family: var(--ha-font-family-body, inherit);
|
||||
font-style: normal;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
letter-spacing: 0.25px;
|
||||
|
||||
@@ -12,20 +12,6 @@ import type {
|
||||
HaFormSelectSchema,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* The underlying select returns option values as strings. Map a selected value
|
||||
* back to its original option value so the source type is retained (for example
|
||||
* a number coming from a backend `vol.In` schema), falling back to the value
|
||||
* itself when no option matches.
|
||||
*/
|
||||
export const matchSelectOptionValue = (
|
||||
options: HaFormSelectSchema["options"],
|
||||
value: string
|
||||
): HaFormSelectData => {
|
||||
const option = options.find((opt) => String(opt[0]) === String(value));
|
||||
return option ? option[0] : value;
|
||||
};
|
||||
|
||||
@customElement("ha-form-select")
|
||||
export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -80,18 +66,16 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
let value: HaFormSelectData | undefined = ev.detail.value;
|
||||
|
||||
if (value === "") {
|
||||
value = undefined;
|
||||
} else if (value != null) {
|
||||
value = matchSelectOptionValue(this.schema.options, value);
|
||||
}
|
||||
let value: string | undefined = ev.detail.value;
|
||||
|
||||
if (value === this.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === "") {
|
||||
value = undefined;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
|
||||
@@ -41,8 +41,6 @@ const CUSTOM_ICONS: Record<string, () => Promise<string>> = {
|
||||
),
|
||||
esphome: () =>
|
||||
import("../resources/esphome-logo-svg").then((mod) => mod.mdiEsphomeLogo),
|
||||
matter: () =>
|
||||
import("../resources/matter-logo-svg").then((mod) => mod.mdiMatterLogo),
|
||||
};
|
||||
|
||||
@customElement("ha-icon")
|
||||
|
||||
@@ -354,9 +354,7 @@ export class HaSerialPortSelector extends LitElement {
|
||||
}
|
||||
|
||||
private get _selectorDomain(): string | undefined {
|
||||
// `domain` is the integration domain even in options flows, where the flow
|
||||
// handler is the config entry id instead.
|
||||
return this.context?.domain;
|
||||
return this.context?.handler;
|
||||
}
|
||||
|
||||
private _memoRecommendedDomains = memoizeOne(
|
||||
|
||||
@@ -3,7 +3,6 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { goBack } from "../common/navigate";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-icon-button-arrow-prev";
|
||||
import "./ha-menu-button";
|
||||
|
||||
@@ -12,9 +11,6 @@ const PASSIVE_EVENT_OPTIONS = { passive: true } as const;
|
||||
export const haTopAppBarFixedStyles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
--total-top-app-bar-height: calc(
|
||||
var(--header-height, 0px) + var(--sub-row-height, 0px)
|
||||
);
|
||||
@@ -25,11 +21,10 @@ export const haTopAppBarFixedStyles = css`
|
||||
box-sizing: border-box;
|
||||
color: var(--app-header-text-color, #fff);
|
||||
background-color: var(--app-header-background-color, var(--primary-color));
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
width: 100%;
|
||||
width: var(--ha-top-app-bar-width, 100%);
|
||||
z-index: 4;
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
@@ -121,17 +116,17 @@ export const haTopAppBarFixedStyles = css`
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: calc(
|
||||
height: calc(
|
||||
100vh - var(--total-top-app-bar-height, 0px) - var(
|
||||
--safe-area-inset-top,
|
||||
0px
|
||||
) - var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
padding-top: calc(
|
||||
var(--total-top-app-bar-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:host([narrow]) .top-app-bar-fixed-adjust {
|
||||
@@ -151,8 +146,6 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
|
||||
@query(".sub-row") protected _subRowElement?: HTMLElement;
|
||||
|
||||
@query(".top-app-bar-fixed-adjust") protected _scrollElement?: HTMLElement;
|
||||
|
||||
@state() private _hasSubRow = false;
|
||||
|
||||
private _scrollTarget?: HTMLElement | Window;
|
||||
@@ -161,13 +154,14 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
|
||||
@property({ attribute: false })
|
||||
public get scrollTarget(): HTMLElement | Window {
|
||||
return this._scrollTarget || this._scrollElement || window;
|
||||
return this._scrollTarget || window;
|
||||
}
|
||||
|
||||
public set scrollTarget(value: HTMLElement | Window) {
|
||||
const old = this.scrollTarget;
|
||||
this._unregisterListeners();
|
||||
this._scrollTarget = value;
|
||||
this._updateBarPosition();
|
||||
this.requestUpdate("scrollTarget", old);
|
||||
if (this.isConnected) {
|
||||
this._registerListeners();
|
||||
@@ -189,6 +183,7 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
if (this.hasUpdated) {
|
||||
this._observeSubRowHeight();
|
||||
this._updateSubRowHeight();
|
||||
this._updateBarPosition();
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
}
|
||||
@@ -248,7 +243,7 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
}
|
||||
|
||||
protected _renderContent() {
|
||||
return html`<div class="top-app-bar-fixed-adjust ha-scrollbar">
|
||||
return html`<div class="top-app-bar-fixed-adjust">
|
||||
<slot></slot>
|
||||
</div>`;
|
||||
}
|
||||
@@ -257,6 +252,7 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._observeSubRowHeight();
|
||||
this._updateSubRowHeight();
|
||||
this._updateBarPosition();
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
}
|
||||
@@ -274,6 +270,13 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
this._unregisterListeners();
|
||||
}
|
||||
|
||||
protected _updateBarPosition() {
|
||||
if (this._barElement) {
|
||||
this._barElement.style.position =
|
||||
this.scrollTarget === window ? "" : "absolute";
|
||||
}
|
||||
}
|
||||
|
||||
protected _syncScrollState = () => {
|
||||
const scrollTop =
|
||||
this.scrollTarget instanceof Window
|
||||
@@ -333,10 +336,7 @@ export class HaTopAppBarFixed extends LitElement {
|
||||
this.style.setProperty("--sub-row-height", `${subRowHeight}px`);
|
||||
};
|
||||
|
||||
static override styles: CSSResultGroup = [
|
||||
haStyleScrollbar,
|
||||
haTopAppBarFixedStyles,
|
||||
];
|
||||
static override styles: CSSResultGroup = haTopAppBarFixedStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -85,25 +85,15 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
await listTTSVoices(this.hass, this.engineId, this.language)
|
||||
).voices;
|
||||
|
||||
const valueIsValid =
|
||||
this.value &&
|
||||
this._voices?.some((voice) => voice.voice_id === this.value);
|
||||
|
||||
if (valueIsValid) {
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The current value is missing or no longer valid for the loaded voices.
|
||||
// When a voice is required, auto-select the first one (the <ha-select>
|
||||
// already displays it) so the value is propagated to the parent;
|
||||
// otherwise clear it.
|
||||
const newValue =
|
||||
this.required && this._voices?.length
|
||||
? this._voices[0].voice_id
|
||||
: undefined;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
this.value = newValue;
|
||||
if (
|
||||
!this._voices ||
|
||||
!this._voices.find((voice) => voice.voice_id === this.value)
|
||||
) {
|
||||
this.value = undefined;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
<div
|
||||
class=${classMap({
|
||||
"top-app-bar-fixed-adjust": true,
|
||||
"ha-scrollbar": true,
|
||||
"top-app-bar-fixed-adjust--pane": this.pane,
|
||||
})}
|
||||
>
|
||||
@@ -131,7 +130,12 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
|
||||
.top-app-bar-fixed-adjust--pane {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: calc(
|
||||
100vh - var(--total-top-app-bar-height, 0px) - var(
|
||||
--safe-area-inset-top,
|
||||
0px
|
||||
) - var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
.pane {
|
||||
@@ -163,7 +167,6 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust--pane .content {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import timezones from "google-timezones-json";
|
||||
|
||||
export const createTimezoneListEl = () => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "timezones";
|
||||
Object.keys(timezones).forEach((key) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = key;
|
||||
option.innerText = timezones[key];
|
||||
list.appendChild(option);
|
||||
});
|
||||
return list;
|
||||
};
|
||||
@@ -73,44 +73,30 @@ export const getEntities = (
|
||||
|
||||
let entityIds = Object.keys(hass.states);
|
||||
|
||||
// These run over every entity, so use Sets for O(1) membership instead of
|
||||
// repeated Array.includes scans.
|
||||
if (includeEntities) {
|
||||
const includeEntitiesSet = new Set(includeEntities);
|
||||
entityIds = entityIds.filter((entityId) =>
|
||||
includeEntitiesSet.has(entityId)
|
||||
includeEntities.includes(entityId)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeEntities) {
|
||||
const excludeEntitiesSet = new Set(excludeEntities);
|
||||
entityIds = entityIds.filter(
|
||||
(entityId) => !excludeEntitiesSet.has(entityId)
|
||||
(entityId) => !excludeEntities.includes(entityId)
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDomains) {
|
||||
const includeDomainsSet = new Set(includeDomains);
|
||||
entityIds = entityIds.filter((eid) =>
|
||||
includeDomainsSet.has(computeDomain(eid))
|
||||
includeDomains.includes(computeDomain(eid))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
const excludeDomainsSet = new Set(excludeDomains);
|
||||
entityIds = entityIds.filter(
|
||||
(eid) => !excludeDomainsSet.has(computeDomain(eid))
|
||||
(eid) => !excludeDomains.includes(computeDomain(eid))
|
||||
);
|
||||
}
|
||||
|
||||
// These values are the same for every entity, so compute them once instead
|
||||
// of inside the map over (potentially thousands of) entities.
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
const domainNames = new Map<string, string>();
|
||||
|
||||
items = entityIds.map<EntityComboBoxItem>((entityId) => {
|
||||
const stateObj = hass.states[entityId];
|
||||
|
||||
@@ -124,12 +110,12 @@ export const getEntities = (
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
let domainName = domainNames.get(domain);
|
||||
if (domainName === undefined) {
|
||||
domainName = domainToName(hass.localize, domain);
|
||||
domainNames.set(domain, domainName);
|
||||
}
|
||||
const domainName = domainToName(hass.localize, computeDomain(entityId));
|
||||
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
|
||||
+6
-8
@@ -725,18 +725,16 @@ export const mergeHistoryResults = (
|
||||
}
|
||||
|
||||
const newLineItem: LineChartUnit = { ...historyItem, data: [] };
|
||||
const historyDataByEntity = new Map(
|
||||
historyItem.data.map((d) => [d.entity_id, d])
|
||||
);
|
||||
const ltsDataByEntity = new Map(ltsItem.data.map((d) => [d.entity_id, d]));
|
||||
const entities = new Set([
|
||||
...historyDataByEntity.keys(),
|
||||
...ltsDataByEntity.keys(),
|
||||
...historyItem.data.map((d) => d.entity_id),
|
||||
...ltsItem.data.map((d) => d.entity_id),
|
||||
]);
|
||||
|
||||
for (const entity of entities) {
|
||||
const historyDataItem = historyDataByEntity.get(entity);
|
||||
const ltsDataItem = ltsDataByEntity.get(entity);
|
||||
const historyDataItem = historyItem.data.find(
|
||||
(d) => d.entity_id === entity
|
||||
);
|
||||
const ltsDataItem = ltsItem.data.find((d) => d.entity_id === entity);
|
||||
|
||||
if (!historyDataItem || !ltsDataItem) {
|
||||
newLineItem.data.push(historyDataItem || ltsDataItem!);
|
||||
|
||||
@@ -43,6 +43,11 @@ export const lightSupportsColorMode = (
|
||||
mode: LightColorMode
|
||||
) => entity.attributes.supported_color_modes?.includes(mode) || false;
|
||||
|
||||
export const lightIsInColorMode = (entity: LightEntity) =>
|
||||
(entity.attributes.color_mode &&
|
||||
modesSupportingColor.includes(entity.attributes.color_mode)) ||
|
||||
false;
|
||||
|
||||
export const lightSupportsColor = (entity: LightEntity) =>
|
||||
entity.attributes.supported_color_modes?.some((mode) =>
|
||||
modesSupportingColor.includes(mode)
|
||||
@@ -154,3 +159,5 @@ export const computeDefaultFavoriteColors = (
|
||||
|
||||
return colors;
|
||||
};
|
||||
|
||||
export const formatTempColor = (value: number) => `${value} K`;
|
||||
|
||||
+1
-3
@@ -128,13 +128,11 @@ export const addMatterDevice = (hass: HomeAssistant) => {
|
||||
|
||||
export const commissionMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
code: string,
|
||||
networkOnly: boolean
|
||||
code: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "matter/commission",
|
||||
code,
|
||||
network_only: networkOnly,
|
||||
});
|
||||
|
||||
export const acceptSharedMatterDevice = (
|
||||
|
||||
+17
-18
@@ -231,24 +231,6 @@ export const computeUpdateStateDisplay = (
|
||||
const state = stateObj.state;
|
||||
const attributes = stateObj.attributes;
|
||||
|
||||
// An install can be in progress even when the state is "off", e.g. when
|
||||
// downgrading firmware (installed_version is newer than latest_version).
|
||||
// Show the installing status regardless of state in that case.
|
||||
if (updateIsInstalling(stateObj)) {
|
||||
const supportsProgress =
|
||||
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
|
||||
attributes.update_percentage !== null;
|
||||
if (supportsProgress) {
|
||||
return hass.localize("ui.card.update.installing_with_progress", {
|
||||
progress: formatNumber(attributes.update_percentage!, hass.locale, {
|
||||
maximumFractionDigits: attributes.display_precision,
|
||||
minimumFractionDigits: attributes.display_precision,
|
||||
}),
|
||||
});
|
||||
}
|
||||
return hass.localize("ui.card.update.installing");
|
||||
}
|
||||
|
||||
if (state === "off") {
|
||||
const isSkipped =
|
||||
attributes.latest_version &&
|
||||
@@ -259,6 +241,23 @@ export const computeUpdateStateDisplay = (
|
||||
return hass.formatEntityState(stateObj);
|
||||
}
|
||||
|
||||
if (state === "on") {
|
||||
if (updateIsInstalling(stateObj)) {
|
||||
const supportsProgress =
|
||||
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
|
||||
attributes.update_percentage !== null;
|
||||
if (supportsProgress) {
|
||||
return hass.localize("ui.card.update.installing_with_progress", {
|
||||
progress: formatNumber(attributes.update_percentage!, hass.locale, {
|
||||
maximumFractionDigits: attributes.display_precision,
|
||||
minimumFractionDigits: attributes.display_precision,
|
||||
}),
|
||||
});
|
||||
}
|
||||
return hass.localize("ui.card.update.installing");
|
||||
}
|
||||
}
|
||||
|
||||
return hass.formatEntityState(stateObj);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,21 +10,13 @@ import "../../components/ha-button";
|
||||
import type { HaSwitch } from "../../components/ha-switch";
|
||||
import type { ConfigEntryMutableParams } from "../../data/config_entries";
|
||||
import { updateConfigEntry } from "../../data/config_entries";
|
||||
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import type { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options";
|
||||
|
||||
interface SystemOptionsState {
|
||||
disableNewEntities: boolean;
|
||||
disablePolling: boolean;
|
||||
}
|
||||
|
||||
@customElement("dialog-config-entry-system-options")
|
||||
class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptionsState>()(
|
||||
LitElement
|
||||
) {
|
||||
class DialogConfigEntrySystemOptions extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _disableNewEntities!: boolean;
|
||||
@@ -46,13 +38,6 @@ class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptio
|
||||
this._error = undefined;
|
||||
this._disableNewEntities = params.entry.pref_disable_new_entities;
|
||||
this._disablePolling = params.entry.pref_disable_polling;
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{
|
||||
disableNewEntities: this._disableNewEntities,
|
||||
disablePolling: this._disablePolling,
|
||||
}
|
||||
);
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -83,7 +68,7 @@ class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptio
|
||||
) || this._params.entry.domain,
|
||||
}
|
||||
)}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
@@ -150,7 +135,7 @@ class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptio
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${this._submitting || !this.isDirtyState}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.update"
|
||||
@@ -164,19 +149,11 @@ class DialogConfigEntrySystemOptions extends DirtyStateProviderMixin<SystemOptio
|
||||
private _disableNewEntitiesChanged(ev: Event): void {
|
||||
this._error = undefined;
|
||||
this._disableNewEntities = !(ev.target as HaSwitch).checked;
|
||||
this._updateDirtyState({
|
||||
disableNewEntities: this._disableNewEntities,
|
||||
disablePolling: this._disablePolling,
|
||||
});
|
||||
}
|
||||
|
||||
private _disablePollingChanged(ev: Event): void {
|
||||
this._error = undefined;
|
||||
this._disablePolling = !(ev.target as HaSwitch).checked;
|
||||
this._updateDirtyState({
|
||||
disableNewEntities: this._disableNewEntities,
|
||||
disablePolling: this._disablePolling,
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateEntry(): Promise<void> {
|
||||
|
||||
@@ -403,7 +403,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
.domain=${this._params.domain ?? this._step.handler}
|
||||
@flow-step-footer-state-changed=${this
|
||||
._handleFooterStateChanged}
|
||||
></step-flow-form>
|
||||
|
||||
@@ -35,10 +35,6 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// The integration domain this flow belongs to. Unlike `step.handler`, this is
|
||||
// the domain even for options flows (where the handler is the config entry id).
|
||||
@property({ attribute: false }) public domain?: string;
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _stepData?: Record<string, any>;
|
||||
@@ -112,7 +108,7 @@ class StepFlowForm extends LitElement {
|
||||
.computeHelper=${this._helperCallback}
|
||||
.computeError=${this._errorCallback}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
.context=${{ handler: step.handler, domain: this.domain }}
|
||||
.context=${{ handler: step.handler }}
|
||||
></ha-form>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
@@ -1103,7 +1103,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
.title .breadcrumb {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-family: var(--ha-font-family-heading, inherit);
|
||||
line-height: 16px;
|
||||
--mdc-icon-size: 16px;
|
||||
padding: var(--ha-space-1);
|
||||
|
||||
@@ -22,7 +22,6 @@ interface EntityInfo {
|
||||
entityId: string;
|
||||
entityName: string | undefined;
|
||||
areaId: string | undefined;
|
||||
deviceId: string | undefined;
|
||||
}
|
||||
|
||||
@customElement("more-info-content")
|
||||
@@ -121,7 +120,7 @@ class MoreInfoContent extends LitElement {
|
||||
hass.entities,
|
||||
hass.devices
|
||||
);
|
||||
const { area, device } = getEntityContext(
|
||||
const { area } = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
@@ -129,8 +128,7 @@ class MoreInfoContent extends LitElement {
|
||||
hass.floors
|
||||
);
|
||||
const areaId = area?.area_id;
|
||||
const deviceId = device?.id;
|
||||
return { entityId, entityName, areaId, deviceId };
|
||||
return { entityId, entityName, areaId };
|
||||
})
|
||||
.filter(Boolean) as EntityInfo[];
|
||||
|
||||
@@ -142,20 +140,10 @@ class MoreInfoContent extends LitElement {
|
||||
const areaIds = new Set(entityInfos.map((info) => info.areaId));
|
||||
const allSameArea = areaIds.size === 1;
|
||||
|
||||
// Check if all entities belong to the same device
|
||||
const deviceIds = new Set(entityInfos.map((info) => info.deviceId));
|
||||
const allSameDevice = deviceIds.size === 1;
|
||||
// Build name and state content config based on conditions
|
||||
const name: EntityNameItem[] = [{ type: "device" }];
|
||||
|
||||
// Build name and state content config based on conditions. The device name
|
||||
// is redundant when every member belongs to the same device, so omit it
|
||||
// (and fall back to the entity name so the tile still has a label).
|
||||
const name: EntityNameItem[] = [];
|
||||
|
||||
if (!allSameDevice) {
|
||||
name.push({ type: "device" });
|
||||
}
|
||||
|
||||
if (!allSameEntityName || allSameDevice) {
|
||||
if (!allSameEntityName) {
|
||||
name.push({ type: "entity" });
|
||||
}
|
||||
|
||||
|
||||
@@ -19,64 +19,6 @@ declare const __WB_MANIFEST__: Parameters<typeof precacheAndRoute>[0];
|
||||
const noFallBackRegEx =
|
||||
/\/(api|static|auth|frontend_latest|frontend_es5|local)\/.*/;
|
||||
|
||||
// Camera / image proxy endpoints that carry credentials in the URL.
|
||||
// We pre-validate the credential in the service worker so obviously invalid
|
||||
// requests (signature expired, token missing) never reach the server and
|
||||
// don't trigger spurious "Login attempt" warnings from http.ban after BFCache
|
||||
// restore, tab resume, network change, or any other browser-initiated replay
|
||||
// of a stale `<img>` URL.
|
||||
const proxyPathRegEx =
|
||||
/^\/api\/(camera_proxy_stream|camera_proxy|image_proxy)\//;
|
||||
|
||||
// Reject signatures this many ms before their nominal expiry to absorb small
|
||||
// client/server clock differences. Erring this direction only ever turns a
|
||||
// would-be valid request into a local 401; we cannot err the other way without
|
||||
// re-introducing the warnings this filter exists to prevent.
|
||||
const JWT_EXPIRY_SKEW_MS = 5000;
|
||||
|
||||
const base64UrlDecode = (input: string): string => {
|
||||
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
||||
return atob(padded);
|
||||
};
|
||||
|
||||
const isJwtExpired = (jwt: string): boolean => {
|
||||
try {
|
||||
const parts = jwt.split(".");
|
||||
if (parts.length !== 3) {
|
||||
return false;
|
||||
}
|
||||
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
||||
if (typeof payload.exp !== "number") {
|
||||
return false;
|
||||
}
|
||||
return payload.exp * 1000 < Date.now() + JWT_EXPIRY_SKEW_MS;
|
||||
} catch (_err) {
|
||||
// If we can't parse the JWT for any reason, defer to the server.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleProxyRequest: RouteHandler = async ({ request }) => {
|
||||
const req = request as Request;
|
||||
const url = new URL(req.url);
|
||||
|
||||
const token = url.searchParams.get("token");
|
||||
if (token === "undefined" || token === "null" || token === "") {
|
||||
return new Response(null, { status: 401, statusText: "Invalid token" });
|
||||
}
|
||||
|
||||
const authSig = url.searchParams.get("authSig");
|
||||
if (authSig && isJwtExpired(authSig)) {
|
||||
return new Response(null, {
|
||||
status: 401,
|
||||
statusText: "Signature expired",
|
||||
});
|
||||
}
|
||||
|
||||
return fetch(req);
|
||||
};
|
||||
|
||||
const initRouting = () => {
|
||||
precacheAndRoute(__WB_MANIFEST__, {
|
||||
// Ignore all URL parameters.
|
||||
@@ -117,15 +59,6 @@ const initRouting = () => {
|
||||
})
|
||||
);
|
||||
|
||||
// Short-circuit camera/image proxy requests with an expired signature or a
|
||||
// missing/undefined token so they don't hit core and get logged as invalid
|
||||
// login attempts. Registered before the generic /api route below so it wins.
|
||||
registerRoute(
|
||||
({ url, request }) =>
|
||||
proxyPathRegEx.test(url.pathname) && request.method === "GET",
|
||||
handleProxyRequest
|
||||
);
|
||||
|
||||
// Get api from network.
|
||||
registerRoute(/\/(api|auth)\/.*/, new NetworkOnly());
|
||||
|
||||
|
||||
@@ -23,28 +23,18 @@ import {
|
||||
} from "../../../data/application_credential";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import type { AddApplicationCredentialDialogParams } from "./show-dialog-add-application-credential";
|
||||
|
||||
interface CredentialFormState {
|
||||
domain: string;
|
||||
name: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
|
||||
interface Domain {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@customElement("dialog-add-application-credential")
|
||||
export class DialogAddApplicationCredential extends DirtyStateProviderMixin<CredentialFormState>()(
|
||||
LitElement
|
||||
) {
|
||||
export class DialogAddApplicationCredential extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _loading = false;
|
||||
@@ -86,7 +76,6 @@ export class DialogAddApplicationCredential extends DirtyStateProviderMixin<Cred
|
||||
this._error = undefined;
|
||||
this._loading = false;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "shallow" }, this._currentState());
|
||||
this._fetchConfig();
|
||||
}
|
||||
|
||||
@@ -111,7 +100,10 @@ export class DialogAddApplicationCredential extends DirtyStateProviderMixin<Cred
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
@closed=${this._abortDialog}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
.preventScrimClose=${!!this._domain ||
|
||||
!!this._name ||
|
||||
!!this._clientId ||
|
||||
!!this._clientSecret}
|
||||
.headerTitle=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.caption"
|
||||
)}
|
||||
@@ -292,7 +284,6 @@ export class DialogAddApplicationCredential extends DirtyStateProviderMixin<Cred
|
||||
ev.stopPropagation();
|
||||
this._domain = ev.detail.value;
|
||||
this._updateDescription();
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private async _updateDescription() {
|
||||
@@ -316,16 +307,6 @@ export class DialogAddApplicationCredential extends DirtyStateProviderMixin<Cred
|
||||
const name = (ev.target as any).name;
|
||||
const value = (ev.target as any).value;
|
||||
this[`_${name}`] = value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _currentState(): CredentialFormState {
|
||||
return {
|
||||
domain: this._domain || "",
|
||||
name: this._name || "",
|
||||
clientId: this._clientId || "",
|
||||
clientSecret: this._clientSecret || "",
|
||||
};
|
||||
}
|
||||
|
||||
private _abortDialog() {
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
|
||||
import type { ObjectSelector, Selector } from "../../../../../data/selector";
|
||||
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { DirtyStateProviderMixin } from "../../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
@@ -57,15 +56,15 @@ const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||
const MASKED_FIELDS = ["password", "secret", "token"];
|
||||
|
||||
@customElement("supervisor-app-config")
|
||||
class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
Record<string, unknown>
|
||||
>()(LitElement) {
|
||||
class SupervisorAppConfig extends LitElement {
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _configHasChanged = false;
|
||||
|
||||
@state() private _valid = true;
|
||||
|
||||
@state() private _canShowSchema = false;
|
||||
@@ -352,7 +351,9 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
<div class="card-actions right">
|
||||
<ha-progress-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${this.disabled || !this.isDirtyState || !this._valid}
|
||||
.disabled=${this.disabled ||
|
||||
!this._configHasChanged ||
|
||||
!this._valid}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-progress-button>
|
||||
@@ -376,7 +377,6 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("addon")) {
|
||||
this._options = { ...this.addon.options };
|
||||
this._initDirtyTracking({ type: "deep" }, this.addon.options);
|
||||
}
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
@@ -415,13 +415,11 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
private _configChanged(ev): void {
|
||||
if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
|
||||
this._valid = true;
|
||||
this._configHasChanged = true;
|
||||
this._options = ev.detail.value;
|
||||
this._updateDirtyState(ev.detail.value);
|
||||
} else {
|
||||
this._configHasChanged = true;
|
||||
this._valid = ev.detail.isValid;
|
||||
if (ev.detail.isValid) {
|
||||
this._updateDirtyState(ev.detail.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +450,7 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
|
||||
this._markDirtyStateClean();
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
@@ -471,7 +469,7 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
}
|
||||
|
||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||
if (this.disabled || !this.isDirtyState || !this._valid) {
|
||||
if (this.disabled || !this._configHasChanged || !this._valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -501,7 +499,7 @@ class SupervisorAppConfig extends DirtyStateProviderMixin<
|
||||
options,
|
||||
});
|
||||
|
||||
this._markDirtyStateClean();
|
||||
this._configHasChanged = false;
|
||||
if (this.addon?.state === "started") {
|
||||
await suggestSupervisorAppRestart(this, this.hass, this.addon);
|
||||
}
|
||||
|
||||
@@ -15,16 +15,13 @@ import type {
|
||||
} from "../../../../../data/hassio/addon";
|
||||
import { setHassioAddonOption } from "../../../../../data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
|
||||
import { DirtyStateProviderMixin } from "../../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
|
||||
|
||||
@customElement("supervisor-app-network")
|
||||
class SupervisorAppNetwork extends DirtyStateProviderMixin<
|
||||
Record<string, number | null>
|
||||
>()(LitElement) {
|
||||
class SupervisorAppNetwork extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
@@ -33,19 +30,19 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
|
||||
|
||||
@state() private _showOptional = false;
|
||||
|
||||
@state() private _configHasChanged = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _config?: Record<string, number | null>;
|
||||
@state() private _config?: Record<string, any>;
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const config = this._config;
|
||||
|
||||
const hasHiddenOptions = Object.keys(config).find(
|
||||
(entry) => config[entry] === null
|
||||
const hasHiddenOptions = Object.keys(this._config).find(
|
||||
(entry) => this._config![entry] === null
|
||||
);
|
||||
|
||||
return html`
|
||||
@@ -101,7 +98,7 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${!this.isDirtyState || this.disabled}
|
||||
.disabled=${!this._configHasChanged || this.disabled}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-progress-button>
|
||||
@@ -118,10 +115,7 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
|
||||
}
|
||||
|
||||
private _createSchema = memoizeOne(
|
||||
(
|
||||
config: Record<string, number | null>,
|
||||
showOptional: boolean
|
||||
): HaFormSchema[] =>
|
||||
(config: Record<string, number>, showOptional: boolean): HaFormSchema[] =>
|
||||
(showOptional
|
||||
? Object.keys(config)
|
||||
: Object.keys(config).filter((entry) => config[entry] !== null)
|
||||
@@ -147,14 +141,12 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
|
||||
item.name;
|
||||
|
||||
private _setNetworkConfig(): void {
|
||||
const config = this.addon.network || {};
|
||||
this._config = config;
|
||||
this._initDirtyTracking({ type: "shallow" }, config);
|
||||
this._config = this.addon.network || {};
|
||||
}
|
||||
|
||||
private _configChanged(ev: CustomEvent): void {
|
||||
private async _configChanged(ev: CustomEvent): Promise<void> {
|
||||
this._configHasChanged = true;
|
||||
this._config = ev.detail.value;
|
||||
this._updateDirtyState(ev.detail.value);
|
||||
}
|
||||
|
||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||
@@ -169,7 +161,7 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
|
||||
|
||||
try {
|
||||
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
|
||||
this._markDirtyStateClean();
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
@@ -196,14 +188,14 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
|
||||
}
|
||||
|
||||
private async _saveTapped(ev: CustomEvent): Promise<void> {
|
||||
if (!this.isDirtyState || this.disabled) {
|
||||
if (!this._configHasChanged || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = ev.currentTarget as any;
|
||||
|
||||
this._error = undefined;
|
||||
const networkconfiguration: Record<string, number | null> = {};
|
||||
const networkconfiguration = {};
|
||||
Object.entries(this._config!).forEach(([key, value]) => {
|
||||
networkconfiguration[key] = value ?? null;
|
||||
});
|
||||
@@ -214,7 +206,7 @@ class SupervisorAppNetwork extends DirtyStateProviderMixin<
|
||||
|
||||
try {
|
||||
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
|
||||
this._markDirtyStateClean();
|
||||
this._configHasChanged = false;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
|
||||
@@ -261,22 +261,15 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 var(--ha-space-4);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-input-search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: var(--ha-space-3) var(--ha-space-2);
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
|
||||
import "../../../components/ha-alert";
|
||||
@@ -55,20 +54,9 @@ const SENSOR_DOMAINS = ["sensor"];
|
||||
const TEMPERATURE_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_TEMPERATURE];
|
||||
const HUMIDITY_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_HUMIDITY];
|
||||
|
||||
interface AreaFormState {
|
||||
name: string;
|
||||
aliases: string[];
|
||||
labels: string[];
|
||||
picture: string | null;
|
||||
icon: string | null;
|
||||
floor: string | null;
|
||||
temperatureEntity: string | null;
|
||||
humidityEntity: string | null;
|
||||
}
|
||||
|
||||
@customElement("dialog-area-registry-detail")
|
||||
class DialogAreaDetail
|
||||
extends DirtyStateProviderMixin<AreaFormState>()(LitElement)
|
||||
extends LitElement
|
||||
implements HassDialog<AreaRegistryDetailDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -128,23 +116,9 @@ class DialogAreaDetail
|
||||
this._humidityEntity = null;
|
||||
}
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._currentState());
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
private _currentState(): AreaFormState {
|
||||
return {
|
||||
name: this._name,
|
||||
aliases: this._aliases,
|
||||
labels: this._labels,
|
||||
picture: this._picture,
|
||||
icon: this._icon,
|
||||
floor: this._floor,
|
||||
temperatureEntity: this._temperatureEntity,
|
||||
humidityEntity: this._humidityEntity,
|
||||
};
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._open = false;
|
||||
return true;
|
||||
@@ -352,8 +326,6 @@ class DialogAreaDetail
|
||||
if (processed.floor) {
|
||||
this._floor = processed.floor;
|
||||
}
|
||||
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -371,7 +343,7 @@ class DialogAreaDetail
|
||||
header-title=${entry
|
||||
? this.hass.localize("ui.panel.config.areas.editor.update_area")
|
||||
: this.hass.localize("ui.panel.config.areas.editor.create_area")}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-suggest-with-ai-button
|
||||
@@ -412,9 +384,7 @@ class DialogAreaDetail
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid ||
|
||||
this._submitting ||
|
||||
(!!this._params?.entry && !this.isDirtyState)}
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.common.save")
|
||||
@@ -448,43 +418,36 @@ class DialogAreaDetail
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _floorChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._floor = ev.detail.value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _iconChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._icon = ev.detail.value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _labelsChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._labels = ev.detail.value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
|
||||
this._error = undefined;
|
||||
this._picture = (ev.target as HaPictureUpload).value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _aliasesChanged(ev: CustomEvent): void {
|
||||
this._aliases = ev.detail.value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _sensorChanged(ev: CustomEvent): void {
|
||||
const deviceClass = (ev.target as HaEntityPicker).includeDeviceClasses![0];
|
||||
const key = `_${deviceClass}Entity`;
|
||||
this[key] = ev.detail.value || null;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
@@ -506,7 +469,6 @@ class DialogAreaDetail
|
||||
} else {
|
||||
await this._params!.updateEntry!(values);
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
|
||||
@@ -34,7 +34,6 @@ import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import { groupBy } from "../../../common/util/group-by";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import { createColumnsController } from "../../../common/util/responsive-columns";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
@@ -140,8 +139,6 @@ const NAVIGATION_ACTIONS: {
|
||||
},
|
||||
] as const;
|
||||
|
||||
const MAX_COLUMNS = 3;
|
||||
|
||||
@customElement("ha-config-area-page")
|
||||
class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -162,8 +159,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _logbookTime = { recent: 86400 };
|
||||
|
||||
private _columnsController = createColumnsController(this, MAX_COLUMNS);
|
||||
|
||||
private _memberships = memoizeOne(
|
||||
(
|
||||
areaId: string,
|
||||
@@ -293,28 +288,29 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
Object.values(this.hass.devices),
|
||||
this._entityReg
|
||||
);
|
||||
const { devices, entities } = memberships;
|
||||
const quickLinkCounts = this._getQuickLinkCounts(
|
||||
memberships,
|
||||
this._related
|
||||
);
|
||||
|
||||
// Compute the display names on shallow copies so we can sort and render by
|
||||
// them without mutating the shared registry objects.
|
||||
const devices = memberships.devices.map((entry) => ({
|
||||
...entry,
|
||||
name: computeDeviceNameDisplay(
|
||||
entry,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
),
|
||||
}));
|
||||
sortDeviceRegistryByName(devices, this.hass.locale.language);
|
||||
|
||||
const entities = memberships.entities.map((entry) => ({
|
||||
...entry,
|
||||
name: computeEntityRegistryName(this.hass, entry),
|
||||
}));
|
||||
sortEntityRegistryByName(entities, this.hass.locale.language);
|
||||
// Pre-compute the entity and device names, so we can sort by them
|
||||
if (devices) {
|
||||
devices.forEach((entry) => {
|
||||
entry.name = computeDeviceNameDisplay(
|
||||
entry,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
);
|
||||
});
|
||||
sortDeviceRegistryByName(devices, this.hass.locale.language);
|
||||
}
|
||||
if (entities) {
|
||||
entities.forEach((entry) => {
|
||||
entry.name = computeEntityRegistryName(this.hass, entry);
|
||||
});
|
||||
sortEntityRegistryByName(entities, this.hass.locale.language);
|
||||
}
|
||||
|
||||
// Group entities by domain
|
||||
const groupedEntities = groupBy(entities, (entity) =>
|
||||
@@ -362,267 +358,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
)
|
||||
);
|
||||
|
||||
const infoColumn = html`
|
||||
${area.picture
|
||||
? html`<div class="img-container">
|
||||
<img alt=${area.name} src=${area.picture} />
|
||||
<ha-icon-button
|
||||
.path=${mdiPencil}
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.areas.edit_settings"
|
||||
)}
|
||||
class="img-edit-btn"
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
${area.picture && !this._newTriggersConditions
|
||||
? nothing
|
||||
: html`<div class="action-buttons">
|
||||
${area.picture
|
||||
? nothing
|
||||
: html`<ha-button
|
||||
appearance="filled"
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiImagePlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.areas.add_picture")}
|
||||
</ha-button>`}
|
||||
${this._newTriggersConditions
|
||||
? html`<ha-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._showAddToDialog}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
)}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>`}
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
>${devices.length
|
||||
? html`<ha-list>
|
||||
${devices.map(
|
||||
(device) => html`
|
||||
<a href="/config/devices/device/${device.id}">
|
||||
<ha-list-item hasMeta>
|
||||
<span>${device.name}</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
`
|
||||
)}
|
||||
</ha-list>`
|
||||
: html`
|
||||
<div class="no-entries">
|
||||
${this.hass.localize("ui.panel.config.devices.no_devices")}
|
||||
</div>
|
||||
`}
|
||||
</ha-card>
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.linked_entities_caption"
|
||||
)}
|
||||
>
|
||||
${nonAutomatedEntities.length
|
||||
? html`<ha-list>
|
||||
${nonAutomatedEntities.map(
|
||||
(entity) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openEntity}
|
||||
.entity=${entity}
|
||||
hasMeta
|
||||
>
|
||||
<span>${entity.name}</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}</ha-list
|
||||
>`
|
||||
: html`
|
||||
<div class="no-entries">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.no_linked_entities"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</ha-card>
|
||||
`;
|
||||
|
||||
const relatedColumn = html`
|
||||
${isComponentLoaded(this.hass.config, "automation")
|
||||
? html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
>
|
||||
${groupedAutomations?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.assigned_to_area"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-list>
|
||||
${groupedAutomations.map((automation) =>
|
||||
this._renderAutomation(
|
||||
automation.name,
|
||||
automation.entity
|
||||
)
|
||||
)}</ha-list
|
||||
>`
|
||||
: ""}
|
||||
${relatedAutomations?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.targeting_area"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-list>
|
||||
${relatedAutomations.map((automation) =>
|
||||
this._renderAutomation(
|
||||
automation.name,
|
||||
automation.entity
|
||||
)
|
||||
)}</ha-list
|
||||
>`
|
||||
: ""}
|
||||
${!groupedAutomations?.length && !relatedAutomations?.length
|
||||
? html`
|
||||
<div class="no-entries">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
${isComponentLoaded(this.hass.config, "scene")
|
||||
? html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
>
|
||||
${groupedScenes?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.assigned_to_area"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-list>
|
||||
${groupedScenes.map((scene) =>
|
||||
this._renderScene(scene.name, scene.entity)
|
||||
)}</ha-list
|
||||
>`
|
||||
: ""}
|
||||
${relatedScenes?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.targeting_area"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-list>
|
||||
${relatedScenes.map((scene) =>
|
||||
this._renderScene(scene.name, scene.entity)
|
||||
)}</ha-list
|
||||
>`
|
||||
: ""}
|
||||
${!groupedScenes?.length && !relatedScenes?.length
|
||||
? html`
|
||||
<div class="no-entries">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.no_scenes"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
${isComponentLoaded(this.hass.config, "script")
|
||||
? html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
>
|
||||
${groupedScripts?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.assigned_to_area"
|
||||
)}:
|
||||
</h3>
|
||||
${groupedScripts.map((script) =>
|
||||
this._renderScript(script.name, script.entity)
|
||||
)}`
|
||||
: ""}
|
||||
${relatedScripts?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.targeting_area"
|
||||
)}:
|
||||
</h3>
|
||||
${relatedScripts.map((script) =>
|
||||
this._renderScript(script.name, script.entity)
|
||||
)}`
|
||||
: ""}
|
||||
${!groupedScripts?.length && !relatedScripts?.length
|
||||
? html`
|
||||
<div class="no-entries">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.no_scripts"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
|
||||
const logbookColumn = html`
|
||||
${isComponentLoaded(this.hass.config, "logbook")
|
||||
? html`
|
||||
<ha-card outlined .header=${this.hass.localize("panel.logbook")}>
|
||||
<ha-logbook
|
||||
.hass=${this.hass}
|
||||
.time=${this._logbookTime}
|
||||
.entityIds=${this._allEntities(memberships)}
|
||||
.deviceIds=${this._allDeviceIds(memberships.devices)}
|
||||
virtualize
|
||||
narrow
|
||||
no-icon
|
||||
></ha-logbook>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
|
||||
// In 2 columns the logbook goes on the right, under the shorter
|
||||
// automations/scenes/scripts column, to balance the column heights.
|
||||
const columns =
|
||||
this._columnsController.value ?? (this.narrow ? 1 : MAX_COLUMNS);
|
||||
|
||||
const columnContents =
|
||||
columns >= 3
|
||||
? [[infoColumn], [relatedColumn], [logbookColumn]]
|
||||
: columns === 2
|
||||
? [[infoColumn], [relatedColumn, logbookColumn]]
|
||||
: [[infoColumn, relatedColumn, logbookColumn]];
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
@@ -667,10 +402,266 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
|
||||
<div class="container" ${this._columnsController.target()}>
|
||||
${columnContents.map(
|
||||
(contents) => html`<div class="column">${contents}</div>`
|
||||
)}
|
||||
<div class="container">
|
||||
<div class="column">
|
||||
${area.picture
|
||||
? html`<div class="img-container">
|
||||
<img alt=${area.name} src=${area.picture} />
|
||||
<ha-icon-button
|
||||
.path=${mdiPencil}
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.areas.edit_settings"
|
||||
)}
|
||||
class="img-edit-btn"
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
${area.picture && !this._newTriggersConditions
|
||||
? nothing
|
||||
: html`<div class="action-buttons">
|
||||
${area.picture
|
||||
? nothing
|
||||
: html`<ha-button
|
||||
appearance="filled"
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiImagePlus}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.add_picture"
|
||||
)}
|
||||
</ha-button>`}
|
||||
${this._newTriggersConditions
|
||||
? html`<ha-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._showAddToDialog}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
)}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>`}
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
>${devices.length
|
||||
? html`<ha-list>
|
||||
${devices.map(
|
||||
(device) => html`
|
||||
<a href="/config/devices/device/${device.id}">
|
||||
<ha-list-item hasMeta>
|
||||
<span>${device.name}</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
`
|
||||
)}
|
||||
</ha-list>`
|
||||
: html`
|
||||
<div class="no-entries">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.no_devices"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</ha-card>
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.linked_entities_caption"
|
||||
)}
|
||||
>
|
||||
${nonAutomatedEntities.length
|
||||
? html`<ha-list>
|
||||
${nonAutomatedEntities.map(
|
||||
(entity) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openEntity}
|
||||
.entity=${entity}
|
||||
hasMeta
|
||||
>
|
||||
<span>${entity.name}</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}</ha-list
|
||||
>`
|
||||
: html`
|
||||
<div class="no-entries">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.no_linked_entities"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</ha-card>
|
||||
</div>
|
||||
<div class="column">
|
||||
${isComponentLoaded(this.hass.config, "automation")
|
||||
? html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
>
|
||||
${groupedAutomations?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.assigned_to_area"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-list>
|
||||
${groupedAutomations.map((automation) =>
|
||||
this._renderAutomation(
|
||||
automation.name,
|
||||
automation.entity
|
||||
)
|
||||
)}</ha-list
|
||||
>`
|
||||
: ""}
|
||||
${relatedAutomations?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.targeting_area"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-list>
|
||||
${relatedAutomations.map((automation) =>
|
||||
this._renderAutomation(
|
||||
automation.name,
|
||||
automation.entity
|
||||
)
|
||||
)}</ha-list
|
||||
>`
|
||||
: ""}
|
||||
${!groupedAutomations?.length && !relatedAutomations?.length
|
||||
? html`
|
||||
<div class="no-entries">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
${isComponentLoaded(this.hass.config, "scene")
|
||||
? html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
>
|
||||
${groupedScenes?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.assigned_to_area"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-list>
|
||||
${groupedScenes.map((scene) =>
|
||||
this._renderScene(scene.name, scene.entity)
|
||||
)}</ha-list
|
||||
>`
|
||||
: ""}
|
||||
${relatedScenes?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.targeting_area"
|
||||
)}:
|
||||
</h3>
|
||||
<ha-list>
|
||||
${relatedScenes.map((scene) =>
|
||||
this._renderScene(scene.name, scene.entity)
|
||||
)}</ha-list
|
||||
>`
|
||||
: ""}
|
||||
${!groupedScenes?.length && !relatedScenes?.length
|
||||
? html`
|
||||
<div class="no-entries">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.no_scenes"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
${isComponentLoaded(this.hass.config, "script")
|
||||
? html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
>
|
||||
${groupedScripts?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.assigned_to_area"
|
||||
)}:
|
||||
</h3>
|
||||
${groupedScripts.map((script) =>
|
||||
this._renderScript(script.name, script.entity)
|
||||
)}`
|
||||
: ""}
|
||||
${relatedScripts?.length
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.targeting_area"
|
||||
)}:
|
||||
</h3>
|
||||
${relatedScripts.map((script) =>
|
||||
this._renderScript(script.name, script.entity)
|
||||
)}`
|
||||
: ""}
|
||||
${!groupedScripts?.length && !relatedScripts?.length
|
||||
? html`
|
||||
<div class="no-entries">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.no_scripts"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="column">
|
||||
${isComponentLoaded(this.hass.config, "logbook")
|
||||
? html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize("panel.logbook")}
|
||||
>
|
||||
<ha-logbook
|
||||
.hass=${this.hass}
|
||||
.time=${this._logbookTime}
|
||||
.entityIds=${this._allEntities(memberships)}
|
||||
.deviceIds=${this._allDeviceIds(memberships.devices)}
|
||||
virtualize
|
||||
narrow
|
||||
no-icon
|
||||
></ha-logbook>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -914,31 +905,30 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-4);
|
||||
margin: auto;
|
||||
max-width: 1280px;
|
||||
box-sizing: border-box;
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
margin-top: var(--ha-space-8);
|
||||
margin-bottom: var(--ha-space-8);
|
||||
max-width: 1000px;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.column {
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
width: 33%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.fullwidth {
|
||||
padding: var(--ha-space-2);
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.column > *:not(:first-child) {
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:host([narrow]) .column {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host([narrow]) .container {
|
||||
|
||||
@@ -18,22 +18,13 @@ import {
|
||||
} from "../../../../data/automation";
|
||||
import { MODES, isMaxMode } from "../../../../data/script";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import type { AutomationModeDialog } from "./show-dialog-automation-mode";
|
||||
|
||||
interface AutomationModeState {
|
||||
mode: (typeof MODES)[number];
|
||||
max?: number;
|
||||
}
|
||||
|
||||
@customElement("ha-dialog-automation-mode")
|
||||
class DialogAutomationMode
|
||||
extends DirtyStateProviderMixin<AutomationModeState>()(LitElement)
|
||||
implements HassDialog
|
||||
{
|
||||
class DialogAutomationMode extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _open = false;
|
||||
@@ -51,10 +42,6 @@ class DialogAutomationMode
|
||||
this._newMax = isMaxMode(this._newMode)
|
||||
? params.config.max || AUTOMATION_DEFAULT_MAX
|
||||
: undefined;
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{ mode: this._newMode, max: this._newMax }
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
@@ -83,7 +70,6 @@ class DialogAutomationMode
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${title}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-icon-button
|
||||
@@ -137,11 +123,7 @@ class DialogAutomationMode
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${!this.isDirtyState}
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this._save}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.change_mode"
|
||||
)}
|
||||
@@ -159,7 +141,6 @@ class DialogAutomationMode
|
||||
} else if (!this._newMax) {
|
||||
this._newMax = AUTOMATION_DEFAULT_MAX;
|
||||
}
|
||||
this._updateDirtyState({ mode: this._newMode, max: this._newMax });
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent) {
|
||||
@@ -167,7 +148,6 @@ class DialogAutomationMode
|
||||
const target = ev.target as HaInput;
|
||||
if (target.name === "max") {
|
||||
this._newMax = Number(target.value);
|
||||
this._updateDirtyState({ mode: this._newMode, max: this._newMax });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import type { GenDataTaskResult } from "../../../../data/ai_task";
|
||||
import type { AutomationConfig } from "../../../../data/automation";
|
||||
import type { ScriptConfig } from "../../../../data/script";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
@@ -39,18 +38,8 @@ import type {
|
||||
SaveDialogParams,
|
||||
} from "./show-dialog-automation-save";
|
||||
|
||||
interface AutomationSaveState {
|
||||
name?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
entryUpdates: EntityRegistryUpdate;
|
||||
}
|
||||
|
||||
@customElement("ha-dialog-automation-save")
|
||||
class DialogAutomationSave
|
||||
extends DirtyStateProviderMixin<AutomationSaveState>()(LitElement)
|
||||
implements HassDialog
|
||||
{
|
||||
class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _open = false;
|
||||
@@ -92,16 +81,6 @@ class DialogAutomationSave
|
||||
this._entryUpdates.labels.length > 0 ? "labels" : "",
|
||||
this._entryUpdates.area ? "area" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
this._initDirtyTracking(
|
||||
{ type: "deep" },
|
||||
{
|
||||
name: this._newName,
|
||||
description: this._newDescription,
|
||||
icon: this._newIcon,
|
||||
entryUpdates: this._entryUpdates,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
@@ -273,7 +252,6 @@ class DialogAutomationSave
|
||||
.open=${this._open}
|
||||
@closed=${this._dialogClosed}
|
||||
header-title=${this._params.title || title}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
>
|
||||
${this._params.hideInputs
|
||||
? nothing
|
||||
@@ -303,11 +281,7 @@ class DialogAutomationSave
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${!!this._params.config.alias && !this.isDirtyState}
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this._save}>
|
||||
${this.hass.localize(
|
||||
this._params.config.alias && !this._params.onDiscard
|
||||
? "ui.panel.config.automation.editor.rename"
|
||||
@@ -325,28 +299,17 @@ class DialogAutomationSave
|
||||
this._visibleOptionals = [...this._visibleOptionals, option];
|
||||
}
|
||||
|
||||
private _trackDirtyState() {
|
||||
this._updateDirtyState({
|
||||
name: this._newName,
|
||||
description: this._newDescription,
|
||||
icon: this._newIcon,
|
||||
entryUpdates: this._entryUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
private _registryEntryChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const id: string = ev.target.id;
|
||||
const value = ev.detail.value;
|
||||
|
||||
this._entryUpdates = { ...this._entryUpdates, [id]: value };
|
||||
this._trackDirtyState();
|
||||
}
|
||||
|
||||
private _iconChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._newIcon = ev.detail.value || undefined;
|
||||
this._trackDirtyState();
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
@@ -357,7 +320,6 @@ class DialogAutomationSave
|
||||
} else {
|
||||
this._newName = target.value;
|
||||
}
|
||||
this._trackDirtyState();
|
||||
}
|
||||
|
||||
private _handleDiscard() {
|
||||
@@ -425,7 +387,6 @@ class DialogAutomationSave
|
||||
this._visibleOptionals = [...this._visibleOptionals, "labels"];
|
||||
}
|
||||
}
|
||||
this._trackDirtyState();
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
|
||||
@@ -164,9 +164,6 @@ export class HaPlatformCondition extends LitElement {
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircleOutline}
|
||||
class="help-icon"
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</a>`
|
||||
: nothing}
|
||||
|
||||
@@ -251,13 +251,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
if (filteredAutomations === null) {
|
||||
return [];
|
||||
}
|
||||
// Build lookups once instead of scanning the registries for every row.
|
||||
const entityRegLookup = new Map(
|
||||
entityReg.map((reg) => [reg.entity_id, reg])
|
||||
);
|
||||
const labelLookup = labelReg
|
||||
? new Map(labelReg.map((label) => [label.label_id, label]))
|
||||
: undefined;
|
||||
return (
|
||||
filteredAutomations
|
||||
? automations.filter((automation) =>
|
||||
@@ -265,11 +258,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
)
|
||||
: automations
|
||||
).map((automation) => {
|
||||
const entityRegEntry = entityRegLookup.get(automation.entity_id);
|
||||
const entityRegEntry = entityReg.find(
|
||||
(reg) => reg.entity_id === automation.entity_id
|
||||
);
|
||||
const category = entityRegEntry?.categories.automation;
|
||||
const labels = labelReg && entityRegEntry?.labels;
|
||||
const label_entries = (labels || [])
|
||||
.map((lbl) => labelLookup!.get(lbl)!)
|
||||
.map((lbl) => labelReg!.find((label) => label.label_id === lbl)!)
|
||||
.filter(Boolean);
|
||||
const assistants = getEntityVoiceAssistantsIds(
|
||||
entityReg,
|
||||
|
||||
@@ -55,9 +55,6 @@ export class HaConversationTrigger
|
||||
@click=${this._removeOption}
|
||||
slot="end"
|
||||
.path=${mdiClose}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.conversation.delete"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</ha-input>
|
||||
`
|
||||
@@ -81,9 +78,6 @@ export class HaConversationTrigger
|
||||
@click=${this._addOption}
|
||||
slot="end"
|
||||
.path=${mdiPlus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.conversation.add_sentence"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</ha-input>`;
|
||||
}
|
||||
|
||||
@@ -201,9 +201,6 @@ export class HaPlatformTrigger extends LitElement {
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircleOutline}
|
||||
class="help-icon"
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</a>`
|
||||
: nothing}
|
||||
|
||||
@@ -15,7 +15,6 @@ import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/item/ha-list-item-button";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import type {
|
||||
BackupConfig,
|
||||
BackupMutableConfig,
|
||||
@@ -83,10 +82,7 @@ const RECOMMENDED_CONFIG: BackupConfig = {
|
||||
};
|
||||
|
||||
@customElement("ha-dialog-backup-onboarding")
|
||||
class DialogBackupOnboarding
|
||||
extends DirtyStateProviderMixin<BackupConfig>()(LitElement)
|
||||
implements HassDialog
|
||||
{
|
||||
class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _open = false;
|
||||
@@ -119,7 +115,6 @@ class DialogBackupOnboarding
|
||||
}
|
||||
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._config!);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -174,7 +169,6 @@ class DialogBackupOnboarding
|
||||
try {
|
||||
await this._save(true);
|
||||
this._params?.submit!(true);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -220,7 +214,7 @@ class DialogBackupOnboarding
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this._stepTitle}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${isFirstStep
|
||||
@@ -299,7 +293,6 @@ class DialogBackupOnboarding
|
||||
password: this._config.create_backup.password,
|
||||
},
|
||||
};
|
||||
this._updateDirtyState(this._config);
|
||||
this._done();
|
||||
}
|
||||
|
||||
@@ -522,7 +515,6 @@ class DialogBackupOnboarding
|
||||
include_addons: data.include_addons || null,
|
||||
},
|
||||
};
|
||||
this._updateDirtyState(this._config);
|
||||
}
|
||||
|
||||
private _scheduleChanged(ev) {
|
||||
@@ -532,7 +524,6 @@ class DialogBackupOnboarding
|
||||
schedule: value.schedule,
|
||||
retention: value.retention,
|
||||
};
|
||||
this._updateDirtyState(this._config);
|
||||
}
|
||||
|
||||
private _agentsConfigChanged(ev) {
|
||||
@@ -544,7 +535,6 @@ class DialogBackupOnboarding
|
||||
agent_ids: agents,
|
||||
},
|
||||
};
|
||||
this._updateDirtyState(this._config);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -12,17 +12,13 @@ import {
|
||||
getPreferredAgentForDownload,
|
||||
} from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { downloadBackupFile } from "../helper/download_backup";
|
||||
import type { DownloadDecryptedBackupDialogParams } from "./show-dialog-download-decrypted-backup";
|
||||
|
||||
@customElement("ha-dialog-download-decrypted-backup")
|
||||
class DialogDownloadDecryptedBackup
|
||||
extends DirtyStateProviderMixin<string>()(LitElement)
|
||||
implements HassDialog
|
||||
{
|
||||
class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _open = false;
|
||||
@@ -36,7 +32,6 @@ class DialogDownloadDecryptedBackup
|
||||
public showDialog(params: DownloadDecryptedBackupDialogParams): void {
|
||||
this._open = true;
|
||||
this._params = params;
|
||||
this._initDirtyTracking({ type: "shallow" }, "");
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -65,7 +60,7 @@ class DialogDownloadDecryptedBackup
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.download.title"
|
||||
)}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<p>
|
||||
@@ -110,11 +105,7 @@ class DialogDownloadDecryptedBackup
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._submit}
|
||||
.disabled=${!this.isDirtyState}
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.download.download"
|
||||
)}
|
||||
@@ -145,7 +136,6 @@ class DialogDownloadDecryptedBackup
|
||||
this._agentId,
|
||||
this._encryptionKey
|
||||
);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
if (err?.code === "password_incorrect") {
|
||||
@@ -165,7 +155,6 @@ class DialogDownloadDecryptedBackup
|
||||
private _keyChanged(ev) {
|
||||
this._encryptionKey = ev.currentTarget.value;
|
||||
this._error = "";
|
||||
this._updateDirtyState(this._encryptionKey);
|
||||
}
|
||||
|
||||
private get _agentId() {
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
fetchBackupConfig,
|
||||
} from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import "../components/config/ha-backup-config-data";
|
||||
@@ -60,10 +59,7 @@ const STEPS = ["data", "sync"] as const;
|
||||
const DISALLOWED_AGENTS_NO_HA = [CLOUD_AGENT];
|
||||
|
||||
@customElement("ha-dialog-generate-backup")
|
||||
class DialogGenerateBackup
|
||||
extends DirtyStateProviderMixin<FormData>()(LitElement)
|
||||
implements HassDialog
|
||||
{
|
||||
class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _step?: "data" | "sync";
|
||||
@@ -83,8 +79,6 @@ class DialogGenerateBackup
|
||||
this._formData = INITIAL_DATA;
|
||||
this._params = _params;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, INITIAL_DATA);
|
||||
this._updateDirtyState(this._formData);
|
||||
|
||||
this._fetchAgents();
|
||||
this._fetchBackupConfig();
|
||||
@@ -166,7 +160,6 @@ class DialogGenerateBackup
|
||||
agents_mode: "custom",
|
||||
agent_ids: filteredAgents,
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,11 +180,7 @@ class DialogGenerateBackup
|
||||
const selectedAgents = this._formData.agent_ids;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog .open=${this._open} @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header slot="header">
|
||||
${isFirstStep
|
||||
? html`
|
||||
@@ -287,7 +276,6 @@ class DialogGenerateBackup
|
||||
...this._formData!,
|
||||
data,
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _renderSync() {
|
||||
@@ -382,7 +370,6 @@ class DialogGenerateBackup
|
||||
...this._formData!,
|
||||
agents_mode: value,
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _agentsChanged(ev) {
|
||||
@@ -390,7 +377,6 @@ class DialogGenerateBackup
|
||||
...this._formData!,
|
||||
agent_ids: ev.detail.value,
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
@@ -398,7 +384,6 @@ class DialogGenerateBackup
|
||||
...this._formData!,
|
||||
name: (ev.target as HaInput).value ?? "",
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _disabledAgentIds() {
|
||||
@@ -444,7 +429,6 @@ class DialogGenerateBackup
|
||||
}
|
||||
|
||||
this._params!.submit?.(params);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
} from "../../../../components/ha-form/types";
|
||||
import { extractApiErrorMessage } from "../../../../data/hassio/common";
|
||||
import { changeMountOptions } from "../../../../data/supervisor/mounts";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LocalBackupLocationDialogParams } from "./show-dialog-local-backup-location";
|
||||
@@ -26,14 +25,8 @@ const SCHEMA = [
|
||||
},
|
||||
] as const satisfies HaFormSchema[];
|
||||
|
||||
interface LocalBackupLocationFormState {
|
||||
default_backup_mount: string | null | undefined;
|
||||
}
|
||||
|
||||
@customElement("dialog-local-backup-location")
|
||||
class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocationFormState>()(
|
||||
LitElement
|
||||
) {
|
||||
class LocalBackupLocationDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: LocalBackupLocationDialogParams;
|
||||
@@ -51,10 +44,6 @@ class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocat
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
this._open = true;
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{ default_backup_mount: undefined }
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -79,7 +68,7 @@ class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocat
|
||||
header-title=${this.hass.localize(
|
||||
`ui.panel.config.backup.dialogs.local_backup_location.title`
|
||||
)}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error
|
||||
@@ -113,7 +102,7 @@ class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocat
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${this._waiting || !this.isDirtyState}
|
||||
.disabled=${this._waiting || !this._data}
|
||||
slot="primaryAction"
|
||||
@click=${this._changeMount}
|
||||
>
|
||||
@@ -136,9 +125,6 @@ class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocat
|
||||
this._data = {
|
||||
default_backup_mount: newLocation === "/backup" ? null : newLocation,
|
||||
};
|
||||
this._updateDirtyState({
|
||||
default_backup_mount: this._data.default_backup_mount,
|
||||
});
|
||||
}
|
||||
|
||||
private async _changeMount() {
|
||||
@@ -154,7 +140,6 @@ class LocalBackupLocationDialog extends DirtyStateProviderMixin<LocalBackupLocat
|
||||
this._waiting = false;
|
||||
return;
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.show_encryption_key.title"
|
||||
)}
|
||||
prevent-scrim-close
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<ha-icon-button
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
type BackupUploadFileFormData,
|
||||
} from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||
@@ -29,7 +28,7 @@ import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
|
||||
|
||||
@customElement("ha-dialog-upload-backup")
|
||||
export class DialogUploadBackup
|
||||
extends DirtyStateProviderMixin<BackupUploadFileFormData>()(LitElement)
|
||||
extends LitElement
|
||||
implements HassDialog<UploadBackupDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -48,8 +47,6 @@ export class DialogUploadBackup
|
||||
this._params = params;
|
||||
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "shallow" }, INITIAL_UPLOAD_FORM_DATA);
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
@@ -67,6 +64,10 @@ export class DialogUploadBackup
|
||||
return true;
|
||||
}
|
||||
|
||||
private _formValid() {
|
||||
return this._formData?.file !== undefined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._formData) {
|
||||
return nothing;
|
||||
@@ -78,7 +79,7 @@ export class DialogUploadBackup
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.title"
|
||||
)}
|
||||
.preventScrimClose=${this.isDirtyState || this._uploading}
|
||||
?prevent-scrim-close=${this._uploading}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error
|
||||
@@ -111,7 +112,7 @@ export class DialogUploadBackup
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._upload}
|
||||
.disabled=${!this.isDirtyState || this._uploading}
|
||||
.disabled=${!this._formValid() || this._uploading}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.upload.action"
|
||||
@@ -130,13 +131,11 @@ export class DialogUploadBackup
|
||||
...this._formData!,
|
||||
file,
|
||||
};
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private _filesCleared() {
|
||||
this._error = undefined;
|
||||
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||
this._updateDirtyState(this._formData);
|
||||
}
|
||||
|
||||
private async _upload() {
|
||||
@@ -162,7 +161,6 @@ export class DialogUploadBackup
|
||||
try {
|
||||
await uploadBackup(this.hass, file, agentIds);
|
||||
this._params!.submit?.();
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
|
||||
@@ -84,10 +84,7 @@ export class CloudRegister extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.register.information3"
|
||||
)}
|
||||
<a
|
||||
href="https://www.nabucasa.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
<a href="https://www.nabucasa.com" target="_blank"
|
||||
>Nabu Casa, Inc</a
|
||||
>
|
||||
${this.hass.localize(
|
||||
|
||||
@@ -37,12 +37,6 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public showAttributes = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
public showDevice = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
public showArea = true;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
@@ -63,15 +57,11 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
|
||||
protected render() {
|
||||
const showAttributes = !this.narrow && this.showAttributes;
|
||||
const showDevice = !this.narrow && this.showDevice;
|
||||
const showArea = !this.narrow && this.showArea;
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
entities: true,
|
||||
"hide-attributes": !showAttributes,
|
||||
"hide-device": !showDevice,
|
||||
"hide-area": !showArea,
|
||||
"hide-extra": this.narrow,
|
||||
})}
|
||||
role="table"
|
||||
@@ -91,14 +81,14 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="header" role="columnheader" ?hidden=${!showDevice}>
|
||||
<div class="header" role="columnheader">
|
||||
<span class="padded">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.entities.picker.headers.device"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="header" role="columnheader" ?hidden=${!showArea}>
|
||||
<div class="header" role="columnheader">
|
||||
<span class="padded">
|
||||
${this._i18n.localize("ui.panel.config.generic.headers.area")}
|
||||
</span>
|
||||
@@ -365,24 +355,6 @@ class HaPanelDevStateRenderer extends LitElement {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.hide-device .filter-devices {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-device .row .header:nth-child(3),
|
||||
.hide-device .row .cell:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-area .filter-areas {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-area .row .header:nth-child(4),
|
||||
.hide-area .row .cell:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-attributes .filter-attributes {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -87,18 +87,6 @@ class HaPanelDevState extends LitElement {
|
||||
})
|
||||
private _showAttributes = true;
|
||||
|
||||
@storage({
|
||||
key: "devToolsShowDevice",
|
||||
state: true,
|
||||
})
|
||||
private _showDevice = true;
|
||||
|
||||
@storage({
|
||||
key: "devToolsShowArea",
|
||||
state: true,
|
||||
})
|
||||
private _showArea = true;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@state()
|
||||
@@ -169,32 +157,14 @@ class HaPanelDevState extends LitElement {
|
||||
)}
|
||||
</h1>
|
||||
${!this.narrow
|
||||
? html`
|
||||
<div class="filters-toggles">
|
||||
<ha-checkbox
|
||||
.checked=${this._showDevice}
|
||||
@change=${this._saveDeviceCheckboxState}
|
||||
>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.entities.picker.headers.device"
|
||||
)}
|
||||
</ha-checkbox>
|
||||
<ha-checkbox
|
||||
.checked=${this._showArea}
|
||||
@change=${this._saveAreaCheckboxState}
|
||||
>
|
||||
${this._i18n.localize("ui.panel.config.generic.headers.area")}
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
<ha-checkbox
|
||||
.checked=${this._showAttributes}
|
||||
@change=${this._saveAttributeCheckboxState}
|
||||
>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.developer-tools.tabs.states.attributes"
|
||||
)}
|
||||
</ha-checkbox>
|
||||
`
|
||||
? html`<ha-checkbox
|
||||
.checked=${this._showAttributes}
|
||||
@change=${this._saveAttributeCheckboxState}
|
||||
>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.developer-tools.tabs.states.attributes"
|
||||
)}
|
||||
</ha-checkbox>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-expansion-panel
|
||||
@@ -310,8 +280,6 @@ class HaPanelDevState extends LitElement {
|
||||
.entities=${entities}
|
||||
.virtualize=${entities.length > VIRTUALIZE_THRESHOLD}
|
||||
.showAttributes=${this._showAttributes}
|
||||
.showDevice=${this._showDevice}
|
||||
.showArea=${this._showArea}
|
||||
@states-tool-entity-selected=${this._entitySelected}
|
||||
>
|
||||
<ha-input-search
|
||||
@@ -625,14 +593,6 @@ class HaPanelDevState extends LitElement {
|
||||
this._showAttributes = ev.target.checked;
|
||||
}
|
||||
|
||||
private _saveDeviceCheckboxState(ev) {
|
||||
this._showDevice = ev.target.checked;
|
||||
}
|
||||
|
||||
private _saveAreaCheckboxState(ev) {
|
||||
this._showArea = ev.target.checked;
|
||||
}
|
||||
|
||||
private _yamlChanged(ev) {
|
||||
this._stateAttributes = ev.detail.value;
|
||||
this._validJSON = ev.detail.isValid;
|
||||
@@ -657,25 +617,12 @@ class HaPanelDevState extends LitElement {
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-4);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.heading h1 {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.filters-toggles {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.heading .filters-toggles ha-checkbox {
|
||||
margin-right: 0;
|
||||
width: max-content;
|
||||
display: inline-flex;
|
||||
.heading ha-checkbox {
|
||||
margin-right: var(--ha-space-2);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.entity-id {
|
||||
|
||||
@@ -38,7 +38,6 @@ import { stringCompare } from "../../../common/string/compare";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { groupBy } from "../../../common/util/group-by";
|
||||
import { createColumnsController } from "../../../common/util/responsive-columns";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
@@ -176,8 +175,6 @@ export interface DeviceAlert {
|
||||
|
||||
const DEVICE_ALERTS_INTERVAL = 30000;
|
||||
|
||||
const MAX_COLUMNS = 3;
|
||||
|
||||
@customElement("ha-config-device-page")
|
||||
export class HaConfigDevicePage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -214,8 +211,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
private _logbookTime = { recent: 86400 };
|
||||
|
||||
private _columnsController = createColumnsController(this, MAX_COLUMNS);
|
||||
|
||||
private _integrations = memoizeOne(
|
||||
(
|
||||
device: DeviceRegistryEntry,
|
||||
@@ -754,176 +749,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
`
|
||||
: "";
|
||||
|
||||
const infoColumn = html`
|
||||
${this._deviceAlerts?.length
|
||||
? html`
|
||||
<div>
|
||||
${this._deviceAlerts.map(
|
||||
(alert) => html`
|
||||
<ha-alert .alertType=${alert.level}> ${alert.text} </ha-alert>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<ha-device-info-card .hass=${this.hass} .device=${device}>
|
||||
${deviceInfo}
|
||||
${firstDeviceAction || actions.length
|
||||
? html`
|
||||
<div class="card-actions" slot="actions">
|
||||
<ha-button
|
||||
href=${ifDefined(firstDeviceAction!.href)}
|
||||
rel=${ifDefined(
|
||||
firstDeviceAction!.target ? "noreferrer" : undefined
|
||||
)}
|
||||
appearance="plain"
|
||||
target=${ifDefined(firstDeviceAction!.target)}
|
||||
class=${ifDefined(firstDeviceAction!.classes)}
|
||||
.variant=${firstDeviceAction!.classes?.includes("warning")
|
||||
? "danger"
|
||||
: "brand"}
|
||||
.action=${firstDeviceAction!.action}
|
||||
@click=${this._deviceActionClicked}
|
||||
>
|
||||
${firstDeviceAction!.label}
|
||||
${firstDeviceAction!.icon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class=${ifDefined(firstDeviceAction!.classes)}
|
||||
.path=${firstDeviceAction!.icon}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${firstDeviceAction!.trailingIcon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${firstDeviceAction!.trailingIcon}
|
||||
slot="end"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-button>
|
||||
|
||||
${actions.length
|
||||
? html`
|
||||
<ha-dropdown
|
||||
@wa-select=${this._deviceActionSelected}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${actions.map((deviceAction, idx) => {
|
||||
const dropdownItem = html`<ha-dropdown-item
|
||||
.value=${idx}
|
||||
.data=${deviceAction}
|
||||
.variant=${deviceAction.classes?.includes("warning")
|
||||
? "danger"
|
||||
: "default"}
|
||||
>
|
||||
${deviceAction.icon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${deviceAction.icon}
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
${deviceAction.label}
|
||||
${deviceAction.trailingIcon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="details"
|
||||
.path=${deviceAction.trailingIcon}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
</ha-dropdown-item>`;
|
||||
return deviceAction.href
|
||||
? html`<a
|
||||
href=${deviceAction.href}
|
||||
target=${ifDefined(deviceAction.target)}
|
||||
rel=${ifDefined(
|
||||
deviceAction.target ? "noreferrer" : undefined
|
||||
)}
|
||||
>${dropdownItem}
|
||||
</a>`
|
||||
: dropdownItem;
|
||||
})}
|
||||
</ha-dropdown>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-device-info-card>
|
||||
`;
|
||||
|
||||
const entitiesColumn = html`
|
||||
${(
|
||||
[
|
||||
"control",
|
||||
"sensor",
|
||||
"notify",
|
||||
"event",
|
||||
"assist",
|
||||
"config",
|
||||
"diagnostic",
|
||||
] as const
|
||||
).map((category) =>
|
||||
// Make sure we render controls if no other cards will be rendered
|
||||
entitiesByCategory[category].length > 0 ||
|
||||
(entities.length === 0 && category === "control")
|
||||
? html`
|
||||
<ha-device-entities-card
|
||||
.hass=${this.hass}
|
||||
.header=${this.hass.localize(
|
||||
`ui.panel.config.devices.entities.${category}`
|
||||
)}
|
||||
.deviceName=${deviceName}
|
||||
.entities=${entitiesByCategory[category]}
|
||||
.showHidden=${device.disabled_by !== null}
|
||||
>
|
||||
</ha-device-entities-card>
|
||||
`
|
||||
: ""
|
||||
)}
|
||||
<ha-device-via-devices-card
|
||||
.hass=${this.hass}
|
||||
.deviceId=${this.deviceId}
|
||||
></ha-device-via-devices-card>
|
||||
`;
|
||||
|
||||
const logbookColumn = isComponentLoaded(this.hass.config, "logbook")
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">${this.hass.localize("panel.logbook")}</h1>
|
||||
<ha-logbook
|
||||
.hass=${this.hass}
|
||||
.time=${this._logbookTime}
|
||||
.entityIds=${this._entityIds(entities)}
|
||||
.deviceIds=${this._deviceIdInList(this.deviceId)}
|
||||
virtualize
|
||||
narrow
|
||||
no-icon
|
||||
></ha-logbook>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing;
|
||||
|
||||
const columns =
|
||||
this._columnsController.value ?? (this.narrow ? 1 : MAX_COLUMNS);
|
||||
|
||||
const columnContents =
|
||||
columns >= 3
|
||||
? [[infoColumn, relatedCard], [entitiesColumn], [logbookColumn]]
|
||||
: columns === 2
|
||||
? [[infoColumn, relatedCard, logbookColumn], [entitiesColumn]]
|
||||
: [[infoColumn, entitiesColumn, relatedCard, logbookColumn]];
|
||||
|
||||
return html`<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
@@ -971,7 +796,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
|
||||
<div class="container" ${this._columnsController.target()}>
|
||||
<div class="container">
|
||||
<div class="header fullwidth">
|
||||
${area
|
||||
? html`<div class="header-name">
|
||||
@@ -1024,9 +849,175 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
${columnContents.map(
|
||||
(contents) => html`<div class="column">${contents}</div>`
|
||||
)}
|
||||
<div class="column">
|
||||
${this._deviceAlerts?.length
|
||||
? html`
|
||||
<div>
|
||||
${this._deviceAlerts.map(
|
||||
(alert) => html`
|
||||
<ha-alert .alertType=${alert.level}>
|
||||
${alert.text}
|
||||
</ha-alert>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<ha-device-info-card .hass=${this.hass} .device=${device}>
|
||||
${deviceInfo}
|
||||
${firstDeviceAction || actions.length
|
||||
? html`
|
||||
<div class="card-actions" slot="actions">
|
||||
<ha-button
|
||||
href=${ifDefined(firstDeviceAction!.href)}
|
||||
rel=${ifDefined(
|
||||
firstDeviceAction!.target ? "noreferrer" : undefined
|
||||
)}
|
||||
appearance="plain"
|
||||
target=${ifDefined(firstDeviceAction!.target)}
|
||||
class=${ifDefined(firstDeviceAction!.classes)}
|
||||
.variant=${firstDeviceAction!.classes?.includes("warning")
|
||||
? "danger"
|
||||
: "brand"}
|
||||
.action=${firstDeviceAction!.action}
|
||||
@click=${this._deviceActionClicked}
|
||||
>
|
||||
${firstDeviceAction!.label}
|
||||
${firstDeviceAction!.icon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class=${ifDefined(firstDeviceAction!.classes)}
|
||||
.path=${firstDeviceAction!.icon}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${firstDeviceAction!.trailingIcon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${firstDeviceAction!.trailingIcon}
|
||||
slot="end"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-button>
|
||||
|
||||
${actions.length
|
||||
? html`
|
||||
<ha-dropdown
|
||||
@wa-select=${this._deviceActionSelected}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${actions.map((deviceAction, idx) => {
|
||||
const dropdownItem = html`<ha-dropdown-item
|
||||
.value=${idx}
|
||||
.data=${deviceAction}
|
||||
.variant=${deviceAction.classes?.includes(
|
||||
"warning"
|
||||
)
|
||||
? "danger"
|
||||
: "default"}
|
||||
>
|
||||
${deviceAction.icon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${deviceAction.icon}
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
${deviceAction.label}
|
||||
${deviceAction.trailingIcon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="details"
|
||||
.path=${deviceAction.trailingIcon}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
</ha-dropdown-item>`;
|
||||
return deviceAction.href
|
||||
? html`<a
|
||||
href=${deviceAction.href}
|
||||
target=${ifDefined(deviceAction.target)}
|
||||
rel=${ifDefined(
|
||||
deviceAction.target
|
||||
? "noreferrer"
|
||||
: undefined
|
||||
)}
|
||||
>${dropdownItem}
|
||||
</a>`
|
||||
: dropdownItem;
|
||||
})}
|
||||
</ha-dropdown>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-device-info-card>
|
||||
${!this.narrow ? relatedCard : ""}
|
||||
</div>
|
||||
<div class="column">
|
||||
${(
|
||||
[
|
||||
"control",
|
||||
"sensor",
|
||||
"notify",
|
||||
"event",
|
||||
"assist",
|
||||
"config",
|
||||
"diagnostic",
|
||||
] as const
|
||||
).map((category) =>
|
||||
// Make sure we render controls if no other cards will be rendered
|
||||
entitiesByCategory[category].length > 0 ||
|
||||
(entities.length === 0 && category === "control")
|
||||
? html`
|
||||
<ha-device-entities-card
|
||||
.hass=${this.hass}
|
||||
.header=${this.hass.localize(
|
||||
`ui.panel.config.devices.entities.${category}`
|
||||
)}
|
||||
.deviceName=${deviceName}
|
||||
.entities=${entitiesByCategory[category]}
|
||||
.showHidden=${device.disabled_by !== null}
|
||||
>
|
||||
</ha-device-entities-card>
|
||||
`
|
||||
: ""
|
||||
)}
|
||||
<ha-device-via-devices-card
|
||||
.hass=${this.hass}
|
||||
.deviceId=${this.deviceId}
|
||||
></ha-device-via-devices-card>
|
||||
</div>
|
||||
<div class="column">
|
||||
${this.narrow ? relatedCard : ""}
|
||||
${isComponentLoaded(this.hass.config, "logbook")
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<h1 class="card-header">
|
||||
${this.hass.localize("panel.logbook")}
|
||||
</h1>
|
||||
<ha-logbook
|
||||
.hass=${this.hass}
|
||||
.time=${this._logbookTime}
|
||||
.entityIds=${this._entityIds(entities)}
|
||||
.deviceIds=${this._deviceIdInList(this.deviceId)}
|
||||
virtualize
|
||||
narrow
|
||||
no-icon
|
||||
></ha-logbook>
|
||||
</ha-card>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</hass-subpage>`;
|
||||
}
|
||||
@@ -1636,17 +1627,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-4);
|
||||
margin: auto;
|
||||
max-width: 1280px;
|
||||
box-sizing: border-box;
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
max-width: 1000px;
|
||||
margin-top: var(--ha-space-8);
|
||||
margin-bottom: var(--ha-space-8);
|
||||
}
|
||||
@@ -1707,11 +1692,12 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
.column,
|
||||
.fullwidth {
|
||||
padding: var(--ha-space-2);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.column {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
width: 33%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.fullwidth {
|
||||
width: 100%;
|
||||
@@ -1753,6 +1739,10 @@ export class HaConfigDevicePage extends LitElement {
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
|
||||
:host([narrow]) .column {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
|
||||
@@ -452,12 +452,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
outputDevices = outputDevices.filter((device) => !device.disabled_by);
|
||||
}
|
||||
|
||||
// Build a label lookup once instead of scanning labelReg for every
|
||||
// label of every device.
|
||||
const labelLookup = labelReg
|
||||
? new Map(labelReg.map((label) => [label.label_id, label]))
|
||||
: undefined;
|
||||
|
||||
const formattedOutputDevices = outputDevices.map((device) => {
|
||||
const deviceEntries = sortConfigEntries(
|
||||
device.config_entries
|
||||
@@ -468,7 +462,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
const labels = labelReg && device?.labels;
|
||||
const labelsEntries = (labels || [])
|
||||
.map((lbl) => labelLookup!.get(lbl))
|
||||
.map((lbl) => labelReg!.find((label) => label.label_id === lbl))
|
||||
.filter((entry): entry is LabelRegistryEntry => entry !== undefined);
|
||||
|
||||
const { areaName } = computeDeviceAreaLabel(
|
||||
|
||||
@@ -894,17 +894,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
slot="end"
|
||||
@click=${this._restoreEntityId}
|
||||
.path=${mdiRestore}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.restore_entity_id"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
@click=${this._copyEntityId}
|
||||
.path=${mdiContentCopy}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.copy_entity_id"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</ha-input>
|
||||
${!this.entry.device_id
|
||||
|
||||
@@ -1178,17 +1178,9 @@ export class HaConfigEntities extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only the *set* of entity ids matters for the list below. A plain state
|
||||
// value change on an existing entity cannot add an "entity without unique
|
||||
// id", so detecting a newly added entity lets us skip the (potentially
|
||||
// large) rebuild on every state update, which fires constantly.
|
||||
const stateEntityAdded =
|
||||
changedProps.has("hass") &&
|
||||
(!oldHass ||
|
||||
Object.keys(this.hass.states).some((id) => !(id in oldHass.states)));
|
||||
|
||||
if (
|
||||
stateEntityAdded ||
|
||||
(changedProps.has("hass") &&
|
||||
(!oldHass || oldHass.states !== this.hass.states)) ||
|
||||
changedProps.has("_entities") ||
|
||||
changedProps.has("_entitySources") ||
|
||||
changedProps.has("_exposedEntities")
|
||||
|
||||
@@ -18,7 +18,6 @@ import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
||||
import { createCounter } from "../../../data/counter";
|
||||
import { createInputBoolean } from "../../../data/input_boolean";
|
||||
@@ -102,9 +101,7 @@ const HELPERS: HelperCreators = {
|
||||
};
|
||||
|
||||
@customElement("dialog-helper-detail")
|
||||
export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
Helper | undefined
|
||||
>()(LitElement) {
|
||||
export class DialogHelperDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _item?: Helper;
|
||||
@@ -140,7 +137,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
this._item = undefined;
|
||||
if (this._domain && this._domain in HELPERS) {
|
||||
await HELPERS[this._domain].import();
|
||||
this._initDirtyTracking({ type: "deep" }, undefined);
|
||||
}
|
||||
this._open = true;
|
||||
await this.updateComplete;
|
||||
@@ -297,7 +293,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
header-title=${this._domain
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.helpers.dialog.create_platform",
|
||||
@@ -369,7 +364,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
this._item = ev.detail.value;
|
||||
this._updateDirtyState(this._item);
|
||||
}
|
||||
|
||||
private async _createItem(): Promise<void> {
|
||||
@@ -389,7 +383,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
entityId: `${this._domain}.${createdEntity.id}`,
|
||||
});
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message || "Unknown error";
|
||||
@@ -417,7 +410,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
try {
|
||||
await HELPERS[domain].import();
|
||||
this._domain = domain;
|
||||
this._initDirtyTracking({ type: "deep" }, undefined);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
@@ -1259,14 +1259,11 @@ ${rejected
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a Set for O(1) lookups: this runs on every state change, and the
|
||||
// filter scans every state, so an array `includes` here is O(states ×
|
||||
// sources).
|
||||
const entityIds = new Set(Object.keys(this._entitySource));
|
||||
const entityIds = Object.keys(this._entitySource);
|
||||
|
||||
const newHelpers = Object.values(this.hass!.states).filter(
|
||||
(entity) =>
|
||||
entityIds.has(entity.entity_id) ||
|
||||
entityIds.includes(entity.entity_id) ||
|
||||
isHelperDomain(computeStateDomain(entity))
|
||||
);
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-logo-svg";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import type { HassioHassOSInfo } from "../../../data/hassio/host";
|
||||
import { fetchHassioHassOsInfo } from "../../../data/hassio/host";
|
||||
import type { HassioInfo } from "../../../data/hassio/supervisor";
|
||||
@@ -198,8 +200,8 @@ class HaConfigInfo extends LitElement {
|
||||
</ha-card>
|
||||
|
||||
<ha-card outlined class="pages">
|
||||
<ha-list-base>
|
||||
<ha-list-item-button @click=${this._showShortcuts}>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item type="button" @click=${this._showShortcuts}>
|
||||
<div
|
||||
slot="start"
|
||||
class="icon-background"
|
||||
@@ -207,14 +209,15 @@ class HaConfigInfo extends LitElement {
|
||||
>
|
||||
<ha-svg-icon .path=${mdiKeyboard}></ha-svg-icon>
|
||||
</div>
|
||||
<span slot="headline"
|
||||
<span
|
||||
>${this.hass.localize("ui.panel.config.info.shortcuts")}</span
|
||||
>
|
||||
</ha-list-item-button>
|
||||
</ha-md-list-item>
|
||||
|
||||
${PAGES.map(
|
||||
(page) => html`
|
||||
<ha-list-item-button
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
.href=${documentationUrl(this.hass, page.path)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -222,20 +225,20 @@ class HaConfigInfo extends LitElement {
|
||||
<div
|
||||
slot="start"
|
||||
class="icon-background"
|
||||
style=${`background-color: ${page.iconColor};`}
|
||||
.style="background-color: ${page.iconColor}"
|
||||
>
|
||||
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
|
||||
</div>
|
||||
<span slot="headline">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.info.items.${page.name}`
|
||||
)}
|
||||
</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list-base>
|
||||
</ha-md-list>
|
||||
${customUiList.length
|
||||
? html`
|
||||
<div class="custom-ui">
|
||||
@@ -243,9 +246,8 @@ class HaConfigInfo extends LitElement {
|
||||
${customUiList.map(
|
||||
(item) => html`
|
||||
<div>
|
||||
<a href=${item.url} target="_blank" rel="noreferrer">
|
||||
${item.name}</a
|
||||
>: ${item.version}
|
||||
<a href=${item.url} target="_blank"> ${item.name}</a>:
|
||||
${item.version}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
@@ -389,15 +391,12 @@ class HaConfigInfo extends LitElement {
|
||||
.icon-background ha-svg-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.icon-background {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
|
||||
|
||||
@@ -354,7 +354,6 @@ export class HaConfigEntryRow extends LitElement {
|
||||
<a
|
||||
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@click=${this._signUrl}
|
||||
>
|
||||
<ha-dropdown-item value="diagnostics">
|
||||
|
||||
@@ -27,6 +27,7 @@ import "../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import { fetchDiagnosticHandlers } from "../../../data/diagnostics";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity/entity_registry";
|
||||
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
|
||||
@@ -162,6 +163,8 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
|
||||
@state() private _filter: string = history.state?.filter || "";
|
||||
|
||||
@state() private _diagnosticHandlers?: Record<string, boolean>;
|
||||
|
||||
@state() private _logInfos?: Record<string, IntegrationLogInfo>;
|
||||
|
||||
@query("ha-input-search") private _searchInput!: HaInputSearch;
|
||||
@@ -383,6 +386,16 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
this._handleRouteChanged();
|
||||
this._scanUSBDevices();
|
||||
this._scanImprovDevices();
|
||||
|
||||
if (isComponentLoaded(this.hass.config, "diagnostics")) {
|
||||
fetchDiagnosticHandlers(this.hass).then((infos) => {
|
||||
const handlers = {};
|
||||
for (const info of infos) {
|
||||
handlers[info.domain] = info.handlers.config_entry;
|
||||
}
|
||||
this._diagnosticHandlers = handlers;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
@@ -637,6 +650,9 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
.manifest=${this._manifests[domain]}
|
||||
.entityRegistryEntries=${this._entityRegistryEntries}
|
||||
.domainEntities=${this._domainEntities[domain] || []}
|
||||
.supportsDiagnostics=${this._diagnosticHandlers
|
||||
? this._diagnosticHandlers[domain]
|
||||
: false}
|
||||
.logInfo=${this._logInfos
|
||||
? this._logInfos[domain]
|
||||
: nothing}
|
||||
|
||||
@@ -38,6 +38,9 @@ export class HaIntegrationCard extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public entityRegistryEntries!: EntityRegistryEntry[];
|
||||
|
||||
@property({ attribute: "supports-diagnostics", type: Boolean })
|
||||
public supportsDiagnostics = false;
|
||||
|
||||
@property({ attribute: false }) public logInfo?: IntegrationLogInfo;
|
||||
|
||||
@property({ attribute: false }) public domainEntities: string[] = [];
|
||||
|
||||
+1
-1
@@ -283,7 +283,7 @@ class DialogMatterAddDevice extends LitElement {
|
||||
const savedStep = this._step;
|
||||
try {
|
||||
this._step = "commissioning";
|
||||
await commissionMatterDevice(this.hass, code, true);
|
||||
await commissionMatterDevice(this.hass, code);
|
||||
} catch (_err) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
|
||||
@@ -211,7 +211,7 @@ class MatterOptionsPage extends LitElement {
|
||||
this._error = undefined;
|
||||
this._redirectOnNewMatterDevice();
|
||||
try {
|
||||
await commissionMatterDevice(this.hass, code, false);
|
||||
await commissionMatterDevice(this.hass, code);
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
this._stopRedirect();
|
||||
|
||||
@@ -92,7 +92,6 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
this._configEntryId || ""
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@click=${this._signUrl}
|
||||
>
|
||||
<ha-dropdown-item>
|
||||
|
||||
+2
-8
@@ -6,16 +6,13 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { LovelaceStrategyConfig } from "../../../../data/lovelace/config/strategy";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../../lovelace/editor/dashboard-strategy-editor/hui-dashboard-strategy-element-editor";
|
||||
import type { LovelaceDashboardConfigureStrategyDialogParams } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-configure-strategy")
|
||||
export class DialogLovelaceDashboardConfigureStrategy extends DirtyStateProviderMixin<LovelaceStrategyConfig>()(
|
||||
LitElement
|
||||
) {
|
||||
export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LovelaceDashboardConfigureStrategyDialogParams;
|
||||
@@ -32,7 +29,6 @@ export class DialogLovelaceDashboardConfigureStrategy extends DirtyStateProvider
|
||||
this._params = params;
|
||||
this._data = params.config.strategy;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -53,7 +49,7 @@ export class DialogLovelaceDashboardConfigureStrategy extends DirtyStateProvider
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.new_dashboard"
|
||||
)}
|
||||
@@ -84,7 +80,6 @@ export class DialogLovelaceDashboardConfigureStrategy extends DirtyStateProvider
|
||||
|
||||
private _handleConfigChanged(ev: CustomEvent): void {
|
||||
this._data = ev.detail.config;
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
@@ -97,7 +92,6 @@ export class DialogLovelaceDashboardConfigureStrategy extends DirtyStateProvider
|
||||
strategy: this._data,
|
||||
});
|
||||
this._submitting = false;
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,13 @@ import type {
|
||||
LovelaceDashboardCreateParams,
|
||||
LovelaceDashboardMutableParams,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import { pickAvailableDashboardUrlPath } from "./pick-available-dashboard-url-path";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-detail")
|
||||
export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
|
||||
Partial<LovelaceDashboard>
|
||||
>()(LitElement) {
|
||||
export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LovelaceDashboardDetailsDialogParams;
|
||||
@@ -45,7 +42,6 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
|
||||
this._open = true;
|
||||
if (this._params.dashboard) {
|
||||
this._data = this._params.dashboard;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
} else {
|
||||
const suggestions = this._params.suggestions;
|
||||
this._data = {
|
||||
@@ -55,12 +51,9 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
|
||||
require_admin: false,
|
||||
mode: "storage",
|
||||
};
|
||||
// New dashboards have no saved baseline, so track against an emptyobject to mark them dirty from the outset (keeps Create enabled).
|
||||
this._initDirtyTracking({ type: "deep" }, {});
|
||||
if (suggestions?.title) {
|
||||
this._fillUrlPath(suggestions.title);
|
||||
}
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +72,6 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const yamlMode = this._params.dashboard?.mode === "yaml";
|
||||
|
||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||
|
||||
const cancelButton = html`
|
||||
@@ -104,11 +95,11 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.new_dashboard"
|
||||
)}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${yamlMode
|
||||
${this._params.dashboard?.mode === "yaml"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
|
||||
)
|
||||
@@ -151,12 +142,10 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateDashboard}
|
||||
.disabled=${!yamlMode &&
|
||||
((this._error && "url_path" in this._error) ||
|
||||
titleInvalid ||
|
||||
this._submitting ||
|
||||
!this.isDirtyState)}
|
||||
?autofocus=${yamlMode}
|
||||
.disabled=${(this._error && "url_path" in this._error) ||
|
||||
titleInvalid ||
|
||||
this._submitting}
|
||||
?autofocus=${this._params.dashboard?.mode === "yaml"}
|
||||
>
|
||||
${this._params.urlPath
|
||||
? this._params.dashboard?.mode === "storage"
|
||||
@@ -262,7 +251,6 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
|
||||
} else {
|
||||
this._data = value;
|
||||
}
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
|
||||
private _fillUrlPath(title: string) {
|
||||
@@ -282,7 +270,6 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
|
||||
? pickAvailableDashboardUrlPath(baseSlug, taken)
|
||||
: baseSlug,
|
||||
};
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
@@ -305,7 +292,6 @@ export class DialogLovelaceDashboardDetail extends DirtyStateProviderMixin<
|
||||
this._data as LovelaceDashboardCreateParams
|
||||
);
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
let localizedErrorMessage: string | undefined;
|
||||
|
||||
@@ -13,7 +13,6 @@ import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { PanelMutableParams } from "../../../../data/panel";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { PanelDetailDialogParams } from "./show-dialog-panel-detail";
|
||||
@@ -26,9 +25,7 @@ interface PanelDetailData {
|
||||
}
|
||||
|
||||
@customElement("dialog-panel-detail")
|
||||
export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>()(
|
||||
LitElement
|
||||
) {
|
||||
export class DialogPanelDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: PanelDetailDialogParams;
|
||||
@@ -51,7 +48,6 @@ export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>(
|
||||
show_in_sidebar: params.showInSidebar,
|
||||
};
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -74,7 +70,7 @@ export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>(
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.panel_detail.edit_panel"
|
||||
)}
|
||||
@@ -118,7 +114,7 @@ export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>(
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updatePanel}
|
||||
.disabled=${titleInvalid || this._submitting || !this.isDirtyState}
|
||||
.disabled=${titleInvalid || this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.update"
|
||||
@@ -175,7 +171,6 @@ export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>(
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._error = undefined;
|
||||
this._data = ev.detail.value;
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
|
||||
private async _handleError(err: any) {
|
||||
@@ -233,7 +228,6 @@ export class DialogPanelDetail extends DirtyStateProviderMixin<PanelDetailData>(
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await this._params!.updatePanel(updates);
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._handleError(err);
|
||||
|
||||
@@ -9,7 +9,6 @@ import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-button";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { LovelaceResourcesMutableParams } from "../../../../data/lovelace/resource";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceResourceDetailsDialogParams } from "./show-dialog-lovelace-resource-detail";
|
||||
|
||||
@@ -31,9 +30,7 @@ const detectResourceType = (url?: string) => {
|
||||
};
|
||||
|
||||
@customElement("dialog-lovelace-resource-detail")
|
||||
export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
|
||||
Partial<LovelaceResourcesMutableParams>
|
||||
>()(LitElement) {
|
||||
export class DialogLovelaceResourceDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LovelaceResourceDetailsDialogParams;
|
||||
@@ -60,7 +57,6 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
|
||||
};
|
||||
}
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -87,7 +83,7 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
header-title=${dialogTitle}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
@@ -123,10 +119,7 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateResource}
|
||||
.disabled=${urlInvalid ||
|
||||
!this._data?.res_type ||
|
||||
this._submitting ||
|
||||
!this.isDirtyState}
|
||||
.disabled=${urlInvalid || !this._data?.res_type || this._submitting}
|
||||
>
|
||||
${this._params.resource
|
||||
? this.hass!.localize(
|
||||
@@ -210,7 +203,6 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
|
||||
if (!this._data!.res_type) {
|
||||
const type = detectResourceType(this._data!.url);
|
||||
if (!type) {
|
||||
this._updateDirtyState(this._data!);
|
||||
return;
|
||||
}
|
||||
this._data = {
|
||||
@@ -218,7 +210,6 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
|
||||
res_type: type,
|
||||
};
|
||||
}
|
||||
this._updateDirtyState(this._data!);
|
||||
}
|
||||
|
||||
private async _updateResource() {
|
||||
@@ -235,8 +226,7 @@ export class DialogLovelaceResourceDetail extends DirtyStateProviderMixin<
|
||||
this._data! as LovelaceResourcesMutableParams
|
||||
);
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
this._params = undefined;
|
||||
} catch (err: any) {
|
||||
this._error = { base: err?.message || "Unknown error" };
|
||||
} finally {
|
||||
|
||||
@@ -13,7 +13,6 @@ import "../../../components/ha-picture-upload";
|
||||
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
|
||||
import "../../../components/input/ha-input";
|
||||
import "../../../components/item/ha-row-item";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { adminChangeUsername } from "../../../data/auth";
|
||||
import type { PersonMutableParams } from "../../../data/person";
|
||||
import type { User } from "../../../data/user";
|
||||
@@ -45,20 +44,8 @@ const cropOptions: CropOptions = {
|
||||
aspectRatio: 1,
|
||||
};
|
||||
|
||||
interface PersonFormState {
|
||||
name: string;
|
||||
picture: string | null;
|
||||
userId: string | undefined;
|
||||
deviceTrackers: string[];
|
||||
isAdmin: boolean | undefined;
|
||||
localOnly: boolean | undefined;
|
||||
}
|
||||
|
||||
@customElement("dialog-person-detail")
|
||||
class DialogPersonDetail
|
||||
extends DirtyStateProviderMixin<PersonFormState>()(LitElement)
|
||||
implements HassDialog
|
||||
{
|
||||
class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _name!: string;
|
||||
@@ -117,21 +104,9 @@ class DialogPersonDetail
|
||||
this._picture = null;
|
||||
}
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._currentState());
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
private _currentState(): PersonFormState {
|
||||
return {
|
||||
name: this._name,
|
||||
picture: this._picture,
|
||||
userId: this._userId,
|
||||
deviceTrackers: this._deviceTrackers,
|
||||
isAdmin: this._isAdmin,
|
||||
localOnly: this._localOnly,
|
||||
};
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
@@ -159,7 +134,7 @@ class DialogPersonDetail
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
header-title=${this._params.entry
|
||||
? this._params.entry.name
|
||||
: this.hass!.localize("ui.panel.config.person.detail.new_person")}
|
||||
@@ -287,7 +262,7 @@ class DialogPersonDetail
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || this._submitting || !this.isDirtyState}
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
>
|
||||
${this._params.entry
|
||||
? this.hass!.localize("ui.common.save")
|
||||
@@ -392,17 +367,14 @@ class DialogPersonDetail
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._name = (ev.target as HTMLInputElement).value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _adminChanged(ev): void {
|
||||
this._isAdmin = ev.target.checked;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _localOnlyChanged(ev): void {
|
||||
this._localOnly = ev.target.checked;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private async _allowLoginChanged(ev): Promise<void> {
|
||||
@@ -421,7 +393,6 @@ class DialogPersonDetail
|
||||
this._userId = user.id;
|
||||
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
|
||||
this._localOnly = user.local_only;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
},
|
||||
name: this._name,
|
||||
@@ -450,20 +421,17 @@ class DialogPersonDetail
|
||||
this._user = undefined;
|
||||
this._isAdmin = undefined;
|
||||
this._localOnly = undefined;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
}
|
||||
|
||||
private _deviceTrackersChanged(ev: ValueChangedEvent<string[]>) {
|
||||
this._error = undefined;
|
||||
this._deviceTrackers = ev.detail.value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
|
||||
this._error = undefined;
|
||||
this._picture = (ev.target as HaPictureUpload).value;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
private async _changePassword() {
|
||||
@@ -559,7 +527,6 @@ class DialogPersonDetail
|
||||
await this._params!.createEntry?.(values);
|
||||
this._personExists = true;
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err ? err.message : "Unknown error";
|
||||
|
||||
@@ -22,7 +22,6 @@ import "../../category/ha-category-picker";
|
||||
|
||||
import type { GenDataTaskResult } from "../../../../data/ai_task";
|
||||
import type { SceneConfig } from "../../../../data/scene";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
@@ -43,16 +42,8 @@ const SUGGESTION_INCLUDE: MetadataSuggestionInclude = {
|
||||
labels: true,
|
||||
};
|
||||
|
||||
interface SceneSaveState {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
entryUpdates: EntityRegistryUpdate;
|
||||
}
|
||||
|
||||
@customElement("ha-dialog-scene-save")
|
||||
class DialogSceneSave extends DirtyStateProviderMixin<SceneSaveState>()(
|
||||
LitElement
|
||||
) {
|
||||
class DialogSceneSave extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _open = false;
|
||||
@@ -89,15 +80,6 @@ class DialogSceneSave extends DirtyStateProviderMixin<SceneSaveState>()(
|
||||
this._entryUpdates.category ? "category" : "",
|
||||
this._entryUpdates.labels.length > 0 ? "labels" : "",
|
||||
].filter(Boolean);
|
||||
|
||||
this._initDirtyTracking(
|
||||
{ type: "deep" },
|
||||
{
|
||||
name: this._newName,
|
||||
icon: this._newIcon,
|
||||
entryUpdates: this._entryUpdates,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -206,7 +188,6 @@ class DialogSceneSave extends DirtyStateProviderMixin<SceneSaveState>()(
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title || title}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._params.hideInputs
|
||||
@@ -249,11 +230,7 @@ class DialogSceneSave extends DirtyStateProviderMixin<SceneSaveState>()(
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${!!this._params.config.id && !this.isDirtyState}
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this._save}>
|
||||
${this.hass.localize(
|
||||
this._params.config.id && !this._params.onDiscard
|
||||
? "ui.panel.config.scene.editor.rename"
|
||||
@@ -271,27 +248,17 @@ class DialogSceneSave extends DirtyStateProviderMixin<SceneSaveState>()(
|
||||
this._visibleOptionals = [...this._visibleOptionals, option];
|
||||
}
|
||||
|
||||
private _trackDirtyState() {
|
||||
this._updateDirtyState({
|
||||
name: this._newName,
|
||||
icon: this._newIcon,
|
||||
entryUpdates: this._entryUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
private _registryEntryChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const id: string = ev.target.id;
|
||||
const value = ev.detail.value;
|
||||
|
||||
this._entryUpdates = { ...this._entryUpdates, [id]: value };
|
||||
this._trackDirtyState();
|
||||
}
|
||||
|
||||
private _iconChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._newIcon = ev.detail.value || undefined;
|
||||
this._trackDirtyState();
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
@@ -300,7 +267,6 @@ class DialogSceneSave extends DirtyStateProviderMixin<SceneSaveState>()(
|
||||
if (this._error && this._newName.trim()) {
|
||||
this._error = false;
|
||||
}
|
||||
this._trackDirtyState();
|
||||
}
|
||||
|
||||
private _handleDiscard() {
|
||||
@@ -359,7 +325,6 @@ class DialogSceneSave extends DirtyStateProviderMixin<SceneSaveState>()(
|
||||
this._visibleOptionals = [...this._visibleOptionals, "labels"];
|
||||
}
|
||||
}
|
||||
this._trackDirtyState();
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
|
||||
@@ -20,24 +20,12 @@ import {
|
||||
createUser,
|
||||
deleteUser,
|
||||
} from "../../../data/user";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { AddUserDialogParams } from "./show-dialog-add-user";
|
||||
|
||||
interface AddUserFormState {
|
||||
name?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
passwordConfirm?: string;
|
||||
isAdmin?: boolean;
|
||||
localOnly?: boolean;
|
||||
}
|
||||
|
||||
@customElement("dialog-add-user")
|
||||
export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
|
||||
LitElement
|
||||
) {
|
||||
export class DialogAddUser extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _loading = false;
|
||||
@@ -82,18 +70,6 @@ export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
|
||||
}
|
||||
|
||||
this._open = true;
|
||||
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{
|
||||
name: this._name,
|
||||
username: this._username,
|
||||
password: "",
|
||||
passwordConfirm: "",
|
||||
isAdmin: false,
|
||||
localOnly: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
@@ -113,7 +89,7 @@ export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.users.add_user.caption"
|
||||
)}
|
||||
@@ -266,7 +242,6 @@ export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
|
||||
|
||||
if (parts.length) {
|
||||
this._username = parts[0].toLowerCase();
|
||||
this._publishDirtyState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,30 +249,16 @@ export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
|
||||
this._error = undefined;
|
||||
const target = ev.target as HaInput;
|
||||
this[`_${target.name}`] = target.value;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private async _adminChanged(ev: Event): Promise<void> {
|
||||
const target = ev.target as HaSwitch;
|
||||
this._isAdmin = target.checked;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _localOnlyChanged(ev: Event): void {
|
||||
const target = ev.target as HaSwitch;
|
||||
this._localOnly = target.checked;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _publishDirtyState(): void {
|
||||
this._updateDirtyState({
|
||||
name: this._name,
|
||||
username: this._username,
|
||||
password: this._password,
|
||||
passwordConfirm: this._passwordConfirm,
|
||||
isAdmin: this._isAdmin,
|
||||
localOnly: this._localOnly,
|
||||
});
|
||||
}
|
||||
|
||||
private async _createUser(ev: Event) {
|
||||
@@ -345,7 +306,6 @@ export class DialogAddUser extends DirtyStateProviderMixin<AddUserFormState>()(
|
||||
},
|
||||
];
|
||||
this._params!.userAddedCallback(user);
|
||||
this._markDirtyStateClean();
|
||||
this._close();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import { adminChangePassword } from "../../../data/auth";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
@@ -44,9 +43,7 @@ interface FormData {
|
||||
}
|
||||
|
||||
@customElement("dialog-admin-change-password")
|
||||
class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
|
||||
LitElement
|
||||
) {
|
||||
class DialogAdminChangePassword extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: AdminChangePasswordDialogParams;
|
||||
@@ -68,10 +65,7 @@ class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
|
||||
this._userId = params.userId;
|
||||
this._data = undefined;
|
||||
this._error = undefined;
|
||||
this._submitting = false;
|
||||
this._success = false;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "shallow" }, {});
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -123,7 +117,7 @@ class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.users.change_password.caption"
|
||||
)}
|
||||
@@ -179,7 +173,6 @@ class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this._data = ev.detail.value;
|
||||
this._updateDirtyState(this._data ?? {});
|
||||
this._validate();
|
||||
}
|
||||
|
||||
@@ -192,7 +185,6 @@ class DialogAdminChangePassword extends DirtyStateProviderMixin<FormData>()(
|
||||
this._userId!,
|
||||
this._data.new_password
|
||||
);
|
||||
this._markDirtyStateClean();
|
||||
this._success = true;
|
||||
} catch (err: any) {
|
||||
showToast(this, {
|
||||
|
||||
@@ -24,23 +24,13 @@ import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showAdminChangePasswordDialog } from "./show-dialog-admin-change-password";
|
||||
import type { UserDetailDialogParams } from "./show-dialog-user-detail";
|
||||
|
||||
interface UserDetailFormState {
|
||||
name: string;
|
||||
isAdmin?: boolean;
|
||||
localOnly?: boolean;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
@customElement("dialog-user-detail")
|
||||
class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
|
||||
LitElement
|
||||
) {
|
||||
class DialogUserDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _name!: string;
|
||||
@@ -67,15 +57,6 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
|
||||
this._localOnly = params.entry.local_only;
|
||||
this._isActive = params.entry.is_active;
|
||||
this._open = true;
|
||||
this._initDirtyTracking(
|
||||
{ type: "shallow" },
|
||||
{
|
||||
name: this._name,
|
||||
isAdmin: this._isAdmin,
|
||||
localOnly: this._localOnly,
|
||||
isActive: this._isActive,
|
||||
}
|
||||
);
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
@@ -88,164 +69,177 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
header-title=${user.name}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`<div class="error">${this._error}</div>`
|
||||
: nothing}
|
||||
${
|
||||
this._error
|
||||
? html`<div class="error">${this._error}</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="secondary">
|
||||
${this.hass.localize("ui.panel.config.users.editor.id")}:
|
||||
${user.id}<br />
|
||||
</div>
|
||||
${badges.length === 0
|
||||
? nothing
|
||||
: html`
|
||||
<div class="badge-container">
|
||||
${badges.map(
|
||||
([icon, label]) => html`
|
||||
<ha-label>
|
||||
<ha-svg-icon slot="icon" .path=${icon}></ha-svg-icon>
|
||||
${label}
|
||||
</ha-label>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
<div class="form">
|
||||
${!user.system_generated
|
||||
? html`
|
||||
<ha-input
|
||||
autofocus
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.users.editor.name"
|
||||
${
|
||||
badges.length === 0
|
||||
? nothing
|
||||
: html`
|
||||
<div class="badge-container">
|
||||
${badges.map(
|
||||
([icon, label]) => html`
|
||||
<ha-label>
|
||||
<ha-svg-icon slot="icon" .path=${icon}></ha-svg-icon>
|
||||
${label}
|
||||
</ha-label>
|
||||
`
|
||||
)}
|
||||
></ha-input>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.username"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">${user.username}</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changeUsername}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-row-item>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${!user.system_generated && this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.password"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">************</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changePassword}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-row-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isActive}
|
||||
@change=${this._activeChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated}
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
${!this._isAdmin && !user.system_generated
|
||||
}
|
||||
<div class="form">
|
||||
${
|
||||
!user.system_generated
|
||||
? html`
|
||||
<ha-input
|
||||
autofocus
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.users.editor.name"
|
||||
)}
|
||||
></ha-input>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.username"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">${user.username}</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changeUsername}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-row-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!user.system_generated && this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.password"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text">************</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changePassword}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-row-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isActive}
|
||||
@change=${this._activeChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated}
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
></ha-switch>
|
||||
</ha-switch>
|
||||
</ha-row-item>
|
||||
${
|
||||
!this._isAdmin && !user.system_generated
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
user.system_generated
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
"ui.panel.config.users.editor.system_generated_read_only_users"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${user.system_generated
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated_read_only_users"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
@@ -254,19 +248,18 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
@click=${this._deleteEntry}
|
||||
.disabled=${this._submitting ||
|
||||
user.system_generated ||
|
||||
user.is_owner}
|
||||
.disabled=${
|
||||
this._submitting || user.system_generated || user.is_owner
|
||||
}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.users.editor.delete_user")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!this._name ||
|
||||
this._submitting ||
|
||||
user.system_generated ||
|
||||
!this.isDirtyState}
|
||||
.disabled=${
|
||||
!this._name || this._submitting || user.system_generated
|
||||
}
|
||||
>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
@@ -278,31 +271,18 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _adminChanged(ev): void {
|
||||
this._isAdmin = ev.target.checked;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _localOnlyChanged(ev): void {
|
||||
this._localOnly = ev.target.checked;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _activeChanged(ev): void {
|
||||
this._isActive = ev.target.checked;
|
||||
this._publishDirtyState();
|
||||
}
|
||||
|
||||
private _publishDirtyState(): void {
|
||||
this._updateDirtyState({
|
||||
name: this._name,
|
||||
isAdmin: this._isAdmin,
|
||||
localOnly: this._localOnly,
|
||||
isActive: this._isActive,
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
@@ -316,7 +296,6 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
|
||||
],
|
||||
local_only: this._localOnly,
|
||||
});
|
||||
this._markDirtyStateClean();
|
||||
this._close();
|
||||
} catch (err: any) {
|
||||
this._error = err?.message || "Unknown error";
|
||||
@@ -329,7 +308,6 @@ class DialogUserDetail extends DirtyStateProviderMixin<UserDetailFormState>()(
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry()) {
|
||||
this._markDirtyStateClean();
|
||||
this._close();
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -11,7 +11,6 @@ import "../../../components/ha-form/ha-form";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import type {
|
||||
AssistPipeline,
|
||||
AssistPipelineMutableParams,
|
||||
@@ -29,9 +28,7 @@ import type { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-vo
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("dialog-voice-assistant-pipeline-detail")
|
||||
export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
|
||||
Partial<AssistPipeline>
|
||||
>()(LitElement) {
|
||||
export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: VoiceAssistantPipelineDetailsDialogParams;
|
||||
@@ -65,7 +62,6 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
|
||||
|
||||
this._hideWakeWord =
|
||||
this._params.hideWakeWord || !this._data.wake_word_entity;
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,7 +98,6 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
|
||||
stt_engine: this._params.pipeline?.stt_engine || sstDefault,
|
||||
tts_engine: this._params.pipeline?.tts_engine || ttsDefault,
|
||||
};
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -150,7 +145,7 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${title}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${!this._hideWakeWord ||
|
||||
@@ -239,7 +234,6 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
|
||||
slot="primaryAction"
|
||||
@click=${this._updatePipeline}
|
||||
.loading=${this._submitting}
|
||||
.disabled=${!this.isDirtyState}
|
||||
>
|
||||
${isExistingPipeline
|
||||
? this.hass.localize(
|
||||
@@ -272,7 +266,6 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
|
||||
value[key] = ev.detail.value[key];
|
||||
});
|
||||
this._data = { ...this._data, ...value };
|
||||
this._updateDirtyState(this._data);
|
||||
}
|
||||
|
||||
private async _updatePipeline() {
|
||||
@@ -306,7 +299,6 @@ export class DialogVoiceAssistantPipelineDetail extends DirtyStateProviderMixin<
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("No createPipeline function provided");
|
||||
}
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err?.message || "Unknown error";
|
||||
|
||||
@@ -8,7 +8,6 @@ import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import "../../../components/ha-button";
|
||||
import type { HomeZoneMutableParams } from "../../../data/zone";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeZoneDetailDialogParams } from "./show-dialog-home-zone-detail";
|
||||
@@ -22,9 +21,7 @@ const SCHEMA = [
|
||||
];
|
||||
|
||||
@customElement("dialog-home-zone-detail")
|
||||
class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams>()(
|
||||
LitElement
|
||||
) {
|
||||
class DialogHomeZoneDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: Record<string, string>;
|
||||
@@ -46,7 +43,6 @@ class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams
|
||||
longitude: this.hass.config.longitude,
|
||||
radius: this.hass.config.radius,
|
||||
};
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -75,7 +71,7 @@ class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams
|
||||
header-title=${this.hass!.localize("ui.common.edit_item", {
|
||||
name: this._data.name,
|
||||
})}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-form
|
||||
@@ -98,7 +94,7 @@ class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!valid || this._submitting || !this.isDirtyState}
|
||||
.disabled=${!valid || this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
@@ -124,7 +120,6 @@ class DialogHomeZoneDetail extends DirtyStateProviderMixin<HomeZoneMutableParams
|
||||
value.radius = value.location.radius;
|
||||
delete value.location;
|
||||
this._data = value;
|
||||
this._updateDirtyState(value);
|
||||
}
|
||||
|
||||
private _computeLabel = (): string => "";
|
||||
|
||||
@@ -11,15 +11,12 @@ import "../../../components/ha-button";
|
||||
import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import type { ZoneMutableParams } from "../../../data/zone";
|
||||
import { getZoneEditorInitData } from "../../../data/zone";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
|
||||
|
||||
@customElement("dialog-zone-detail")
|
||||
class DialogZoneDetail extends DirtyStateProviderMixin<ZoneMutableParams>()(
|
||||
LitElement
|
||||
) {
|
||||
class DialogZoneDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: Record<string, string>;
|
||||
@@ -56,7 +53,6 @@ class DialogZoneDetail extends DirtyStateProviderMixin<ZoneMutableParams>()(
|
||||
radius: 100,
|
||||
};
|
||||
}
|
||||
this._initDirtyTracking({ type: "deep" }, this._data);
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -97,7 +93,7 @@ class DialogZoneDetail extends DirtyStateProviderMixin<ZoneMutableParams>()(
|
||||
name: this._params.entry.name,
|
||||
})
|
||||
: this.hass!.localize("ui.panel.config.zone.detail.new_zone")}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-form
|
||||
@@ -135,7 +131,7 @@ class DialogZoneDetail extends DirtyStateProviderMixin<ZoneMutableParams>()(
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!valid || this._submitting || !this.isDirtyState}
|
||||
.disabled=${!valid || this._submitting}
|
||||
>
|
||||
${this._params.entry
|
||||
? this.hass!.localize("ui.common.save")
|
||||
@@ -193,7 +189,6 @@ class DialogZoneDetail extends DirtyStateProviderMixin<ZoneMutableParams>()(
|
||||
delete value.icon;
|
||||
}
|
||||
this._data = value;
|
||||
this._updateDirtyState(value);
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
|
||||
@@ -13,7 +13,6 @@ import type { HaFormSchema } from "../../../components/ha-form/types";
|
||||
import type { HomeFrontendSystemData } from "../../../data/frontend";
|
||||
import type { ShortcutItem } from "../../../data/home_shortcuts";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import "../components/home-favorites-editor";
|
||||
@@ -38,7 +37,7 @@ const WELCOME_SCHEMA: HaFormSchema[] = [
|
||||
|
||||
@customElement("dialog-edit-home")
|
||||
export class DialogEditHome
|
||||
extends DirtyStateProviderMixin<EditorState>()(LitElement)
|
||||
extends LitElement
|
||||
implements HassDialog<EditHomeDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -61,7 +60,6 @@ export class DialogEditHome
|
||||
show_welcome_message: !params.config.hide_welcome_message,
|
||||
shortcuts: params.config.shortcuts ? [...params.config.shortcuts] : [],
|
||||
};
|
||||
this._initDirtyTracking({ type: "shallow" }, this._state);
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -89,7 +87,7 @@ export class DialogEditHome
|
||||
.headerSubtitle=${this.hass.localize(
|
||||
"ui.panel.home.editor.description"
|
||||
)}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-alert alert-type="info">
|
||||
@@ -180,7 +178,7 @@ export class DialogEditHome
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${this._submitting || !this.isDirtyState}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
@@ -224,7 +222,6 @@ export class DialogEditHome
|
||||
...this._state!,
|
||||
favorite_entities: ev.detail.value,
|
||||
};
|
||||
this._updateDirtyState(this._state);
|
||||
}
|
||||
|
||||
private _welcomeChanged(
|
||||
@@ -234,7 +231,6 @@ export class DialogEditHome
|
||||
...this._state!,
|
||||
show_welcome_message: ev.detail.value.show_welcome_message,
|
||||
};
|
||||
this._updateDirtyState(this._state);
|
||||
}
|
||||
|
||||
private _suggestedChanged(
|
||||
@@ -244,7 +240,6 @@ export class DialogEditHome
|
||||
...this._state!,
|
||||
show_suggested_entities: ev.detail.value.show_suggested_entities,
|
||||
};
|
||||
this._updateDirtyState(this._state);
|
||||
}
|
||||
|
||||
private _shortcutsChanged(ev: ValueChangedEvent<ShortcutItem[]>): void {
|
||||
@@ -252,7 +247,6 @@ export class DialogEditHome
|
||||
...this._state!,
|
||||
shortcuts: ev.detail.value,
|
||||
};
|
||||
this._updateDirtyState(this._state);
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
@@ -276,7 +270,6 @@ export class DialogEditHome
|
||||
|
||||
try {
|
||||
await this._params.saveConfig(config);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
|
||||
@@ -10,14 +10,13 @@ import type { HaFormSchema } from "../../../components/ha-form/types";
|
||||
import type { CustomShortcutItem } from "../../../data/home_shortcuts";
|
||||
import { NavigationPathInfoController } from "../../../data/navigation-path-controller";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { EditShortcutDialogParams } from "./show-dialog-edit-shortcut";
|
||||
|
||||
@customElement("dialog-edit-shortcut")
|
||||
export class DialogEditShortcut
|
||||
extends DirtyStateProviderMixin<CustomShortcutItem>()(LitElement)
|
||||
extends LitElement
|
||||
implements HassDialog<EditShortcutDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -33,7 +32,6 @@ export class DialogEditShortcut
|
||||
public showDialog(params: EditShortcutDialogParams): void {
|
||||
this._params = params;
|
||||
this._data = { ...params.item };
|
||||
this._initDirtyTracking({ type: "shallow" }, this._data);
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -91,7 +89,6 @@ export class DialogEditShortcut
|
||||
.open=${this._open}
|
||||
.headerTitle=${this.hass.localize("ui.panel.home.editor.edit_shortcut")}
|
||||
width="small"
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-form
|
||||
@@ -110,11 +107,7 @@ export class DialogEditShortcut
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${!this.isDirtyState}
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this._save}>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
@@ -131,7 +124,6 @@ export class DialogEditShortcut
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._data = ev.detail.value as CustomShortcutItem;
|
||||
this._updateDirtyState(this._data);
|
||||
}
|
||||
|
||||
private _save() {
|
||||
@@ -144,7 +136,6 @@ export class DialogEditShortcut
|
||||
icon: icon || undefined,
|
||||
color: color || undefined,
|
||||
});
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
@@ -267,6 +267,8 @@ function formatTooltip(
|
||||
|
||||
let sumPositive = 0;
|
||||
let countPositive = 0;
|
||||
let sumNegative = 0;
|
||||
let countNegative = 0;
|
||||
const rows: TemplateResult[] = [];
|
||||
for (const param of params) {
|
||||
const y = param.value?.[1] as number;
|
||||
@@ -278,12 +280,14 @@ function formatTooltip(
|
||||
if (value === "0") {
|
||||
continue;
|
||||
}
|
||||
// Only the positive bars (consumption) are summed into a total. Negative
|
||||
// bars mix unrelated categories (grid export and battery charge), so they
|
||||
// are not totaled.
|
||||
if (param.componentSubType === "bar" && y > 0) {
|
||||
sumPositive += y;
|
||||
countPositive++;
|
||||
if (param.componentSubType === "bar") {
|
||||
if (y > 0) {
|
||||
sumPositive += y;
|
||||
countPositive++;
|
||||
} else {
|
||||
sumNegative += y;
|
||||
countNegative++;
|
||||
}
|
||||
}
|
||||
rows.push(
|
||||
html`<ha-chart-tooltip-marker
|
||||
@@ -301,6 +305,8 @@ function formatTooltip(
|
||||
(row, i) => html`${i > 0 ? html`<br />` : nothing}${row}`
|
||||
)}${sumPositive !== 0 && countPositive > 1 && formatTotal
|
||||
? html`<br /><b>${formatTotal(sumPositive)}</b>`
|
||||
: nothing}${sumNegative !== 0 && countNegative > 1 && formatTotal
|
||||
? html`<br /><b>${formatTotal(sumNegative)}</b>`
|
||||
: nothing}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import type { EnergyDevicesGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import "../../../../components/ha-card";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { measureTextWidth } from "../../../../util/text";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -190,11 +189,9 @@ export class HuiEnergyDevicesGraphCard
|
||||
)}
|
||||
.height=${`${Math.max(modes.includes("pie") ? 300 : 100, (this._legendData?.length || 0) * 28 + 50)}px`}
|
||||
.extraComponents=${[PieChart]}
|
||||
click-label-for-more-info
|
||||
@chart-click=${this._handleChartClick}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@legend-label-click=${this._handleLegendLabelClick}
|
||||
></ha-chart-base>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -546,20 +543,11 @@ export class HuiEnergyDevicesGraphCard
|
||||
chartData.splice(this._config.max_devices);
|
||||
}
|
||||
|
||||
this._legendData = chartData.map((d) => {
|
||||
const id = (d as any).id as string;
|
||||
return {
|
||||
...d,
|
||||
name: this._getDeviceName(d.name),
|
||||
value: `${formatNumber(d.value[0], this.hass.locale)} kWh`,
|
||||
// Untracked is synthetic and external statistics aren't real entities,
|
||||
// so their labels can't open more-info; fall back to toggling visibility.
|
||||
noLabelClick:
|
||||
id === "untracked" ||
|
||||
isExternalStatistic(id) ||
|
||||
!(id in this.hass.states),
|
||||
};
|
||||
});
|
||||
this._legendData = chartData.map((d) => ({
|
||||
...d,
|
||||
name: this._getDeviceName(d.name),
|
||||
value: `${formatNumber(d.value[0], this.hass.locale)} kWh`,
|
||||
}));
|
||||
// filter out hidden stats in place
|
||||
for (let i = chartData.length - 1; i >= 0; i--) {
|
||||
if (this._hiddenStats.includes((chartData[i] as any).id)) {
|
||||
@@ -568,11 +556,8 @@ export class HuiEnergyDevicesGraphCard
|
||||
}
|
||||
|
||||
if (compareData) {
|
||||
const compareById = new Map(
|
||||
chartDataCompare.map((d2) => [(d2 as any).id as string, d2] as const)
|
||||
);
|
||||
datasets[1].data = chartData.map((d) =>
|
||||
compareById.get(d.id)
|
||||
chartDataCompare.find((d2) => (d2 as any).id === d.id)
|
||||
) as typeof chartDataCompare;
|
||||
}
|
||||
|
||||
@@ -594,11 +579,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
e.detail.event?.target?.type === "tspan" // label
|
||||
) {
|
||||
const id = (e.detail.data as any).id as string;
|
||||
if (
|
||||
id !== "untracked" &&
|
||||
!isExternalStatistic(id) &&
|
||||
this.hass.states[id]
|
||||
) {
|
||||
if (id !== "untracked") {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: id,
|
||||
});
|
||||
@@ -606,16 +587,6 @@ export class HuiEnergyDevicesGraphCard
|
||||
}
|
||||
}
|
||||
|
||||
private _handleLegendLabelClick(
|
||||
ev: HASSDomEvent<HASSDomEvents["legend-label-click"]>
|
||||
) {
|
||||
const entityId = ev.detail.id;
|
||||
if (isExternalStatistic(entityId) || !this.hass.states[entityId]) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
|
||||
private _handleChartTypeChange(): void {
|
||||
if (!this._chartType) {
|
||||
return;
|
||||
|
||||
@@ -51,13 +51,6 @@ const colorPropertyMap = {
|
||||
used_battery: "--energy-battery-out-color",
|
||||
};
|
||||
|
||||
const stackOrder = {
|
||||
to_battery: 1,
|
||||
to_grid: 2,
|
||||
used_solar: 3,
|
||||
used_battery: 4,
|
||||
};
|
||||
|
||||
@customElement("hui-energy-usage-graph-card")
|
||||
export class HuiEnergyUsageGraphCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
@@ -181,10 +174,15 @@ export class HuiEnergyUsageGraphCard
|
||||
}
|
||||
|
||||
private _formatTotal = (total: number) =>
|
||||
this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_consumed",
|
||||
{ num: formatNumber(total, this.hass.locale) }
|
||||
);
|
||||
total > 0
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_consumed",
|
||||
{ num: formatNumber(total, this.hass.locale) }
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_returned",
|
||||
{ num: formatNumber(-total, this.hass.locale) }
|
||||
);
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
(
|
||||
@@ -561,7 +559,7 @@ export class HuiEnergyUsageGraphCard
|
||||
this._compareStart!
|
||||
);
|
||||
|
||||
Object.entries(combinedData).forEach(([type, sources]) => {
|
||||
Object.entries(combinedData).forEach(([type, sources], idx) => {
|
||||
Object.entries(sources).forEach(([statId, source]) => {
|
||||
const points: BarSeriesOption["data"] = [];
|
||||
// Process chart data.
|
||||
@@ -594,7 +592,12 @@ export class HuiEnergyUsageGraphCard
|
||||
statisticsMetaData[statId]
|
||||
),
|
||||
// @ts-expect-error
|
||||
order: stackOrder[type] ?? Object.keys(combinedData).length,
|
||||
order:
|
||||
type === "used_solar"
|
||||
? 1
|
||||
: type === "to_battery"
|
||||
? Object.keys(combinedData).length
|
||||
: idx + 2,
|
||||
barMaxWidth: 50,
|
||||
itemStyle: {
|
||||
borderColor: getEnergyColor(
|
||||
|
||||
@@ -236,17 +236,7 @@ export class HuiPowerSourcesGraphCard
|
||||
};
|
||||
|
||||
const now = Date.now();
|
||||
const seriesData: Record<
|
||||
string,
|
||||
{
|
||||
colorHex: string;
|
||||
rgb: [number, number, number];
|
||||
positive: [number, number][];
|
||||
negative: [number, number][];
|
||||
}
|
||||
> = {};
|
||||
|
||||
Object.keys(statIds).forEach((key) => {
|
||||
Object.keys(statIds).forEach((key, keyIndex) => {
|
||||
if (statIds[key].stats.length) {
|
||||
const colorHex = computedStyles.getPropertyValue(statIds[key].color);
|
||||
const rgb = hex2rgb(colorHex);
|
||||
@@ -271,32 +261,14 @@ export class HuiPowerSourcesGraphCard
|
||||
}),
|
||||
trackY
|
||||
);
|
||||
|
||||
seriesData[key] = { colorHex, rgb, positive, negative };
|
||||
}
|
||||
});
|
||||
|
||||
const pushSeries = (
|
||||
key: string,
|
||||
data: [number, number][],
|
||||
stack: "positive" | "negative",
|
||||
z: number
|
||||
) => {
|
||||
const { colorHex, rgb } = seriesData[key];
|
||||
|
||||
datasets.push({
|
||||
...commonSeriesOptions,
|
||||
id: stack === "positive" ? key : `${key}-negative`,
|
||||
name: statIds[key].name,
|
||||
color: colorHex,
|
||||
stack,
|
||||
areaStyle: {
|
||||
color: new LinearGradient(
|
||||
0,
|
||||
stack === "positive" ? 0 : 1,
|
||||
0,
|
||||
stack === "positive" ? 1 : 0,
|
||||
[
|
||||
datasets.push({
|
||||
...commonSeriesOptions,
|
||||
id: key,
|
||||
name: statIds[key].name,
|
||||
color: colorHex,
|
||||
stack: "positive",
|
||||
areaStyle: {
|
||||
color: new LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
||||
@@ -305,32 +277,34 @@ export class HuiPowerSourcesGraphCard
|
||||
offset: 1,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
|
||||
},
|
||||
]
|
||||
),
|
||||
},
|
||||
data,
|
||||
z,
|
||||
});
|
||||
};
|
||||
|
||||
// Draw in reverse order so 0 value lines are overwritten
|
||||
["solar", "battery", "grid"].forEach((key, i) => {
|
||||
if (seriesData[key]) {
|
||||
pushSeries(key, seriesData[key].positive, "positive", 3 - i);
|
||||
}
|
||||
});
|
||||
|
||||
// Draw in reverse order but above positive series
|
||||
["battery", "grid"].forEach((key, i) => {
|
||||
if (seriesData[key]) {
|
||||
pushSeries(key, seriesData[key].negative, "negative", 4 - i);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(statIds).forEach((key) => {
|
||||
if (seriesData[key]) {
|
||||
const { colorHex, rgb } = seriesData[key];
|
||||
|
||||
]),
|
||||
},
|
||||
data: positive,
|
||||
z: 3 - keyIndex, // draw in reverse order so 0 value lines are overwritten
|
||||
});
|
||||
if (key !== "solar") {
|
||||
datasets.push({
|
||||
...commonSeriesOptions,
|
||||
id: `${key}-negative`,
|
||||
name: statIds[key].name,
|
||||
color: colorHex,
|
||||
stack: "negative",
|
||||
areaStyle: {
|
||||
color: new LinearGradient(0, 1, 0, 0, [
|
||||
{
|
||||
offset: 0,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
|
||||
},
|
||||
]),
|
||||
},
|
||||
data: negative,
|
||||
z: 4 - keyIndex, // draw in reverse order but above positive series
|
||||
});
|
||||
}
|
||||
this._legendData!.push({
|
||||
id: key,
|
||||
secondaryIds: key !== "solar" ? [`${key}-negative`] : [],
|
||||
|
||||
@@ -1047,7 +1047,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
height: 24px;
|
||||
padding: 0 4px;
|
||||
padding: 16px 4px;
|
||||
}
|
||||
|
||||
.deleteItemButton {
|
||||
|
||||
@@ -49,9 +49,6 @@ const addEntityId = (entities: Set<string>, entity) => {
|
||||
};
|
||||
|
||||
const addEntities = (entities: Set<string>, obj) => {
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
if (obj.entity) {
|
||||
addEntityId(entities, obj.entity);
|
||||
}
|
||||
|
||||
+2
-13
@@ -12,7 +12,6 @@ import "../../../../../components/ha-dropdown";
|
||||
import "../../../../../components/ha-dropdown-item";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import type { LovelaceStrategyConfig } from "../../../../../data/lovelace/config/strategy";
|
||||
import { DirtyStateProviderMixin } from "../../../../../mixins/dirty-state-provider-mixin";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
@@ -28,9 +27,7 @@ import type { DashboardStrategyEditorDialogParams } from "./show-dialog-dashboar
|
||||
import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown";
|
||||
|
||||
@customElement("dialog-dashboard-strategy-editor")
|
||||
class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStrategyConfig>()(
|
||||
LitElement
|
||||
) {
|
||||
class DialogDashboardStrategyEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DashboardStrategyEditorDialogParams;
|
||||
@@ -52,7 +49,6 @@ class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStra
|
||||
this._params = params;
|
||||
this._strategyConfig = params.config.strategy;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._strategyConfig);
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
@@ -72,7 +68,6 @@ class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStra
|
||||
ev.stopPropagation();
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
this._strategyConfig = ev.detail.config as LovelaceStrategyConfig;
|
||||
this._updateDirtyState(this._strategyConfig);
|
||||
}
|
||||
|
||||
private _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void {
|
||||
@@ -87,7 +82,6 @@ class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStra
|
||||
strategy: this._strategyConfig!,
|
||||
});
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
@@ -143,7 +137,6 @@ class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStra
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
header-title=${title || "-"}
|
||||
header-subtitle=${ifDefined(this._params.title)}
|
||||
width="large"
|
||||
@@ -202,11 +195,7 @@ class DialogDashboardStrategyEditor extends DirtyStateProviderMixin<LovelaceStra
|
||||
>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
?disabled=${!this.isDirtyState}
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this._save}>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
} from "../../../../data/lovelace/config/view";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
@@ -54,7 +53,7 @@ const TABS = ["tab-settings", "tab-visibility"] as const;
|
||||
|
||||
@customElement("hui-dialog-edit-section")
|
||||
export class HuiDialogEditSection
|
||||
extends DirtyStateProviderMixin<LovelaceSectionRawConfig>()(LitElement)
|
||||
extends LitElement
|
||||
implements HassDialog<EditSectionDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -97,7 +96,6 @@ export class HuiDialogEditSection
|
||||
this._viewConfig = findLovelaceContainer(this._params.lovelaceConfig, [
|
||||
this._params.viewIndex,
|
||||
]);
|
||||
this._initDirtyTracking({ type: "deep" }, this._config);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -161,7 +159,7 @@ export class HuiDialogEditSection
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._dialogClosed}
|
||||
class=${classMap({
|
||||
@@ -233,11 +231,7 @@ export class HuiDialogEditSection
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
?disabled=${!this.isDirtyState}
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this._save}>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
@@ -248,7 +242,6 @@ export class HuiDialogEditSection
|
||||
private _configChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this._config = ev.detail.value;
|
||||
this._updateDirtyState(this._config!);
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent): void {
|
||||
@@ -406,7 +399,6 @@ export class HuiDialogEditSection
|
||||
return;
|
||||
}
|
||||
this._config = ev.detail.value;
|
||||
this._updateDirtyState(this._config!);
|
||||
}
|
||||
|
||||
private _ignoreKeydown(ev: KeyboardEvent) {
|
||||
@@ -431,7 +423,6 @@ export class HuiDialogEditSection
|
||||
);
|
||||
|
||||
this._params.saveConfig(newConfig);
|
||||
this._markDirtyStateClean();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user