Add UI for new selectors (#7822)

This commit is contained in:
Bram Kragten 2020-11-26 18:38:01 +01:00 committed by GitHub
parent e3293837a8
commit e1add14453
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 480 additions and 169 deletions

View File

@ -76,7 +76,7 @@ class HaExpansionPanel extends LitElement {
.summary { .summary {
display: flex; display: flex;
padding: 0px 16px; padding: var(--expansion-panel-summary-padding, 0px 16px);
min-height: 48px; min-height: 48px;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;

View File

@ -0,0 +1,30 @@
import { customElement, html, LitElement, property } from "lit-element";
import { HomeAssistant } from "../../types";
import { AreaSelector } from "../../data/selector";
import "../ha-area-picker";
@customElement("ha-selector-area")
export class HaAreaSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: AreaSelector;
@property() public value?: any;
@property() public label?: string;
protected render() {
return html`<ha-area-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
no-add
></ha-area-picker>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-area": HaAreaSelector;
}
}

View File

@ -0,0 +1,54 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-formfield";
import "../ha-switch";
@customElement("ha-selector-boolean")
export class HaBooleanSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public value?: number;
@property() public label?: string;
protected render() {
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
></ha-switch>
</ha-formfield>`;
}
private _handleChange(ev) {
const value = ev.target.checked;
if (this.value === value) {
return;
}
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResult {
return css`
ha-formfield {
width: 100%;
margin: 16px 0;
--mdc-typography-body2-font-size: 1em;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-boolean": HaBooleanSelector;
}
}

View File

@ -38,6 +38,12 @@ export class HaDeviceSelector extends LitElement {
.value=${this.value} .value=${this.value}
.label=${this.label} .label=${this.label}
.deviceFilter=${(device) => this._filterDevices(device)} .deviceFilter=${(device) => this._filterDevices(device)}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
allow-custom-entity allow-custom-entity
></ha-device-picker>`; ></ha-device-picker>`;
} }

View File

@ -0,0 +1,104 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { NumberSelector } from "../../data/selector";
import "@polymer/paper-input/paper-input";
import "../ha-slider";
import { fireEvent } from "../../common/dom/fire_event";
import { classMap } from "lit-html/directives/class-map";
@customElement("ha-selector-number")
export class HaNumberSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: NumberSelector;
@property() public value?: number;
@property() public label?: string;
protected render() {
return html`${this.label}
${this.selector.number.mode === "slider"
? html`<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
>
</ha-slider>`
: ""}
<paper-input
pattern="[0-9]+([\\.][0-9]+)?"
.label=${this.selector.number.mode === "slider"
? undefined
: this.label}
.noLabelFloat=${this.selector.number.mode === "slider"}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step}
type="number"
auto-validate
@value-changed=${this._handleInputChange}
>
${this.selector.number.unit_of_measurement
? html`<div slot="suffix">
${this.selector.number.unit_of_measurement}
</div>`
: ""}
</paper-input>`;
}
private get _value() {
return this.value || 0;
}
private _handleInputChange(ev) {
const value = ev.detail.value;
if (this._value === value) {
return;
}
fireEvent(this, "value-changed", { value });
}
private _handleSliderChange(ev) {
const value = ev.target.value;
if (this._value === value) {
return;
}
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResult {
return css`
:host {
display: flex;
justify-content: space-between;
align-items: center;
}
ha-slider {
flex: 1;
}
.single {
flex: 1;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-number": HaNumberSelector;
}
}

View File

@ -0,0 +1,59 @@
import { customElement, html, LitElement, property } from "lit-element";
import { HomeAssistant } from "../../types";
import { TimeSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
import "../paper-time-input";
const test = new Date().toLocaleString();
const useAMPM = test.includes("AM") || test.includes("PM");
@customElement("ha-selector-time")
export class HaTimeSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: TimeSelector;
@property() public value?: string;
@property() public label?: string;
protected render() {
const parts = this.value?.split(":") || [];
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
return html`
<paper-time-input
.label=${this.label}
.hour=${useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours}
.min=${parts[1] ?? "00"}
.sec=${parts[2] ?? "00"}
.format=${useAMPM ? 12 : 24}
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
@change=${this._timeChanged}
@am-pm-changed=${this._timeChanged}
hide-label
enable-second
></paper-time-input>
`;
}
private _timeChanged(ev) {
let value = ev.target.value;
if (useAMPM) {
let hours = Number(ev.target.hour);
if (ev.target.amPm === "PM") {
hours += 12;
}
value = `${hours}:${ev.target.min}:${ev.target.sec}`;
}
fireEvent(this, "value-changed", {
value,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-time": HaTimeSelector;
}
}

View File

@ -4,6 +4,10 @@ import { HomeAssistant } from "../../types";
import "./ha-selector-entity"; import "./ha-selector-entity";
import "./ha-selector-device"; import "./ha-selector-device";
import "./ha-selector-area";
import "./ha-selector-number";
import "./ha-selector-boolean";
import "./ha-selector-time";
import { Selector } from "../../data/selector"; import { Selector } from "../../data/selector";
@customElement("ha-selector") @customElement("ha-selector")

View File

@ -97,6 +97,7 @@ export class PaperTimeInput extends PolymerElement {
.time-input-wrap { .time-input-wrap {
@apply --layout-horizontal; @apply --layout-horizontal;
@apply --layout-no-wrap; @apply --layout-no-wrap;
justify-content: var(--paper-time-input-justify-content, normal);
} }
[hidden] { [hidden] {

View File

@ -11,18 +11,19 @@ export interface Blueprint {
export interface BlueprintMetaData { export interface BlueprintMetaData {
domain: string; domain: string;
name: string; name: string;
input?: Record<string, BlueprintInput | null>;
description?: string; description?: string;
input: Record<string, BlueprintInput | null>; source_url?: string;
} }
export interface BlueprintInput { export interface BlueprintInput {
name?: string; name?: string;
description?: string; description?: string;
selector?: Selector; selector?: Selector;
default?: any;
} }
export interface BlueprintImportResult { export interface BlueprintImportResult {
url: string;
suggested_filename: string; suggested_filename: string;
raw_data: string; raw_data: string;
blueprint: Blueprint; blueprint: Blueprint;

View File

@ -5,10 +5,10 @@ export interface InputNumber {
name: string; name: string;
min: number; min: number;
max: number; max: number;
step: number;
mode: "box" | "slider";
icon?: string; icon?: string;
initial?: number; initial?: number;
step?: number;
mode?: "box" | "slider";
unit_of_measurement?: string; unit_of_measurement?: string;
} }

View File

@ -1,4 +1,10 @@
export type Selector = EntitySelector | DeviceSelector; export type Selector =
| EntitySelector
| DeviceSelector
| AreaSelector
| NumberSelector
| BooleanSelector
| TimeSelector;
export interface EntitySelector { export interface EntitySelector {
entity: { entity: {
@ -13,5 +19,31 @@ export interface DeviceSelector {
integration?: string; integration?: string;
manufacturer?: string; manufacturer?: string;
model?: string; model?: string;
entity?: EntitySelector["entity"];
}; };
} }
export interface AreaSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
area: {};
}
export interface NumberSelector {
number: {
min: number;
max: number;
step: number;
mode: "box" | "slider";
unit_of_measurement?: string;
};
}
export interface BooleanSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
boolean: {};
}
export interface TimeSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
time: {};
}

View File

@ -33,6 +33,7 @@ import {
import "../../../components/ha-blueprint-picker"; import "../../../components/ha-blueprint-picker";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-selector/ha-selector"; import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row";
@customElement("blueprint-automation-editor") @customElement("blueprint-automation-editor")
export class HaBlueprintAutomationEditor extends LitElement { export class HaBlueprintAutomationEditor extends LitElement {
@ -40,7 +41,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
@property() public isWide!: boolean; @property() public isWide!: boolean;
@property() public narrow!: boolean; @property({ reflect: true, type: Boolean }) public narrow!: boolean;
@property() public config!: BlueprintAutomationConfig; @property() public config!: BlueprintAutomationConfig;
@ -125,7 +126,6 @@ export class HaBlueprintAutomationEditor extends LitElement {
)}</span )}</span
> >
<ha-card> <ha-card>
<div class="card-content">
<div class="blueprint-picker-container"> <div class="blueprint-picker-container">
${this._blueprints ${this._blueprints
? Object.keys(this._blueprints).length ? Object.keys(this._blueprints).length
@ -153,7 +153,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
${this.config.use_blueprint.path ${this.config.use_blueprint.path
? blueprint && "error" in blueprint ? blueprint && "error" in blueprint
? html`<p class="warning"> ? html`<p class="warning padding">
There is an error in this Blueprint: ${blueprint.error} There is an error in this Blueprint: ${blueprint.error}
</p>` </p>`
: html`${blueprint?.metadata.description : html`${blueprint?.metadata.description
@ -168,32 +168,36 @@ export class HaBlueprintAutomationEditor extends LitElement {
</h3> </h3>
${Object.entries(blueprint.metadata.input).map( ${Object.entries(blueprint.metadata.input).map(
([key, value]) => ([key, value]) =>
html`<div> html`<ha-settings-row .narrow=${this.narrow}>
${value?.description} <span slot="heading">${value?.name || key}</span>
<span slot="description"
>${value?.description}</span
>
${value?.selector ${value?.selector
? html`<ha-selector ? html`<ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${value.selector} .selector=${value.selector}
.key=${key} .key=${key}
.label=${value?.name || key} .value=${(this.config.use_blueprint.input &&
.value=${this.config.use_blueprint.input && this.config.use_blueprint.input[key]) ||
this.config.use_blueprint.input[key]} value?.default}
@value-changed=${this._inputChanged} @value-changed=${this._inputChanged}
></ha-selector>` ></ha-selector>`
: html`<paper-input : html`<paper-input
.key=${key} .key=${key}
.label=${value?.name || key}
.value=${this.config.use_blueprint.input && .value=${this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]} this.config.use_blueprint.input[key]}
@value-changed=${this._inputChanged} @value-changed=${this._inputChanged}
no-label-float
></paper-input>`} ></paper-input>`}
</div>` </ha-settings-row>`
)}` )}`
: this.hass.localize( : html`<p class="padding">
${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_inputs" "ui.panel.config.automation.editor.blueprint.no_inputs"
)}` )}
</p>`}`
: ""} : ""}
</div>
</ha-card> </ha-card>
</ha-config-section>`; </ha-config-section>`;
} }
@ -279,16 +283,20 @@ export class HaBlueprintAutomationEditor extends LitElement {
font-weight: bold; font-weight: bold;
color: var(--error-color); color: var(--error-color);
} }
.padding {
padding: 16px;
}
.content { .content {
padding-bottom: 20px; padding-bottom: 20px;
} }
.blueprint-picker-container { .blueprint-picker-container {
padding: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
h3 { h3 {
margin-top: 16px; margin: 16px;
} }
span[slot="introduction"] a { span[slot="introduction"] a {
color: var(--primary-color); color: var(--primary-color);
@ -299,6 +307,16 @@ export class HaBlueprintAutomationEditor extends LitElement {
ha-entity-toggle { ha-entity-toggle {
margin-right: 8px; margin-right: 8px;
} }
ha-settings-row {
--paper-time-input-justify-content: flex-end;
border-top: 1px solid var(--divider-color);
}
:host(:not([narrow])) ha-settings-row paper-input {
width: 50%;
}
:host(:not([narrow])) ha-settings-row ha-selector {
width: 50%;
}
mwc-fab { mwc-fab {
position: relative; position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom)); bottom: calc(-80px - env(safe-area-inset-bottom));

View File

@ -205,12 +205,14 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
${"use_blueprint" in this._config ${"use_blueprint" in this._config
? html`<blueprint-automation-editor ? html`<blueprint-automation-editor
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow}
.stateObj=${stateObj} .stateObj=${stateObj}
.config=${this._config} .config=${this._config}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></blueprint-automation-editor>` ></blueprint-automation-editor>`
: html`<manual-automation-editor : html`<manual-automation-editor
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow}
.stateObj=${stateObj} .stateObj=${stateObj}
.config=${this._config} .config=${this._config}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}

View File

@ -23,6 +23,7 @@ import {
importBlueprint, importBlueprint,
saveBlueprint, saveBlueprint,
} from "../../../data/blueprint"; } from "../../../data/blueprint";
import "../../../components/ha-expansion-panel";
@customElement("ha-dialog-import-blueprint") @customElement("ha-dialog-import-blueprint")
class DialogImportBlueprint extends LitElement { class DialogImportBlueprint extends LitElement {
@ -71,7 +72,9 @@ class DialogImportBlueprint extends LitElement {
html`<b>${this._result.blueprint.metadata.name}</b>`, html`<b>${this._result.blueprint.metadata.name}</b>`,
"domain", "domain",
this._result.blueprint.metadata.domain this._result.blueprint.metadata.domain
)} <br /><br /> )}
<br />
${this._result.blueprint.metadata.description}
${this._result.validation_errors ${this._result.validation_errors
? html` ? html`
<p class="error"> <p class="error">
@ -94,7 +97,14 @@ class DialogImportBlueprint extends LitElement {
)} )}
></paper-input> ></paper-input>
`} `}
<pre>${this._result.raw_data}</pre>` <ha-expansion-panel>
<span slot="title"
>${this.hass.localize(
"ui.panel.config.blueprint.add.raw_blueprint"
)}</span
>
<pre>${this._result.raw_data}</pre>
</ha-expansion-panel>`
: html`${this.hass.localize( : html`${this.hass.localize(
"ui.panel.config.blueprint.add.import_introduction" "ui.panel.config.blueprint.add.import_introduction"
)}<paper-input )}<paper-input
@ -180,7 +190,7 @@ class DialogImportBlueprint extends LitElement {
this._result!.blueprint.metadata.domain, this._result!.blueprint.metadata.domain,
filename, filename,
this._result!.raw_data, this._result!.raw_data,
this._result!.url this._result!.blueprint.metadata.source_url
); );
this._params.importedCallback(); this._params.importedCallback();
this.closeDialog(); this.closeDialog();
@ -192,7 +202,14 @@ class DialogImportBlueprint extends LitElement {
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [haStyleDialog, css``]; return [
haStyleDialog,
css`
ha-expansion-panel {
--expansion-panel-summary-padding: 0;
}
`,
];
} }
} }

View File

@ -80,8 +80,7 @@ class HaBlueprintOverview extends LitElement {
}); });
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow, _language): DataTableColumnContainer => { (narrow, _language): DataTableColumnContainer => ({
const columns: DataTableColumnContainer = {
name: { name: {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.blueprint.overview.headers.name" "ui.panel.config.blueprint.overview.headers.name"
@ -90,25 +89,35 @@ class HaBlueprintOverview extends LitElement {
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
}, template: narrow
}; ? (name, entity: any) =>
html`
if (narrow) {
columns.name.template = (name, entity: any) => {
return html`
${name}<br /> ${name}<br />
<div class="secondary"> <div class="secondary">
${entity.path} ${entity.path}
</div> </div>
`; `
}; : undefined,
columns.create = { },
path: {
title: this.hass.localize(
"ui.panel.config.blueprint.overview.headers.file_name"
),
sortable: true,
filterable: true,
hidden: narrow,
direction: "asc",
width: "25%",
},
create: {
title: "", title: "",
type: "icon-button", type: narrow ? "icon-button" : undefined,
width: narrow ? undefined : "180px",
template: (_, blueprint: any) => template: (_, blueprint: any) =>
blueprint.error blueprint.error
? "" ? ""
: html` <mwc-icon-button : narrow
? html`<mwc-icon-button
.blueprint=${blueprint} .blueprint=${blueprint}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.blueprint.overview.use_blueprint" "ui.panel.config.blueprint.overview.use_blueprint"
@ -118,24 +127,7 @@ class HaBlueprintOverview extends LitElement {
)} )}
@click=${(ev) => this._createNew(ev)} @click=${(ev) => this._createNew(ev)}
><ha-svg-icon .path=${mdiRobot}></ha-svg-icon ><ha-svg-icon .path=${mdiRobot}></ha-svg-icon
></mwc-icon-button>`, ></mwc-icon-button>`
};
} else {
columns.path = {
title: this.hass.localize(
"ui.panel.config.blueprint.overview.headers.file_name"
),
sortable: true,
filterable: true,
direction: "asc",
width: "25%",
};
columns.create = {
title: "",
width: "180px",
template: (_, blueprint: any) =>
blueprint.error
? ""
: html`<mwc-button : html`<mwc-button
.blueprint=${blueprint} .blueprint=${blueprint}
@click=${(ev) => this._createNew(ev)} @click=${(ev) => this._createNew(ev)}
@ -144,10 +136,8 @@ class HaBlueprintOverview extends LitElement {
"ui.panel.config.blueprint.overview.use_blueprint" "ui.panel.config.blueprint.overview.use_blueprint"
)} )}
</mwc-button>`, </mwc-button>`,
}; },
} delete: {
columns.delete = {
title: "", title: "",
type: "icon-button", type: "icon-button",
template: (_, blueprint: any) => template: (_, blueprint: any) =>
@ -161,10 +151,8 @@ class HaBlueprintOverview extends LitElement {
@click=${(ev) => this._delete(ev)} @click=${(ev) => this._delete(ev)}
><ha-svg-icon .path=${mdiDelete}></ha-svg-icon ><ha-svg-icon .path=${mdiDelete}></ha-svg-icon
></mwc-icon-button>`, ></mwc-icon-button>`,
}; },
})
return columns;
}
); );
protected render(): TemplateResult { protected render(): TemplateResult {

View File

@ -6,6 +6,7 @@ import {
internalProperty, internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
query,
} from "lit-element"; } from "lit-element";
import "../../../components/ha-date-input"; import "../../../components/ha-date-input";
import type { HaDateInput } from "../../../components/ha-date-input"; import type { HaDateInput } from "../../../components/ha-date-input";
@ -25,6 +26,10 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
@internalProperty() private _config?: EntityConfig; @internalProperty() private _config?: EntityConfig;
@query("paper-time-input") private _timeInputEl?: PaperTimeInput;
@query("ha-date-input") private _dateInputEl?: HaDateInput;
public setConfig(config: EntityConfig): void { public setConfig(config: EntityConfig): void {
if (!config) { if (!config) {
throw new Error("Invalid configuration"); throw new Error("Invalid configuration");
@ -74,11 +79,10 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
.min=${stateObj.state === UNKNOWN .min=${stateObj.state === UNKNOWN
? "" ? ""
: ("0" + stateObj.attributes.minute).slice(-2)} : ("0" + stateObj.attributes.minute).slice(-2)}
.amPm=${false}
@change=${this._selectedValueChanged} @change=${this._selectedValueChanged}
@click=${this._stopEventPropagation} @click=${this._stopEventPropagation}
hide-label hide-label
format="24" .format=${24}
></paper-time-input> ></paper-time-input>
` `
: ``} : ``}
@ -90,24 +94,14 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
ev.stopPropagation(); ev.stopPropagation();
} }
private get _timeInputEl(): PaperTimeInput {
return this.shadowRoot!.querySelector("paper-time-input")!;
}
private get _dateInputEl(): HaDateInput {
return this.shadowRoot!.querySelector("ha-date-input")!;
}
private _selectedValueChanged(ev): void { private _selectedValueChanged(ev): void {
const stateObj = this.hass!.states[this._config!.entity]; const stateObj = this.hass!.states[this._config!.entity];
const time = const time = this._timeInputEl
this._timeInputEl !== null ? this._timeInputEl.value?.trim()
? this._timeInputEl.value.trim() + ":00"
: undefined; : undefined;
const date = const date = this._dateInputEl ? this._dateInputEl.value : undefined;
this._dateInputEl !== null ? this._dateInputEl.value : undefined;
if (time !== stateObj.state) { if (time !== stateObj.state) {
setInputDateTimeValue(this.hass!, stateObj.entity_id, time, date); setInputDateTimeValue(this.hass!, stateObj.entity_id, time, date);

View File

@ -1453,7 +1453,7 @@
}, },
"confirm_delete_header": "Delete this blueprint?", "confirm_delete_header": "Delete this blueprint?",
"confirm_delete_text": "Are you sure you want to delete this blueprint?", "confirm_delete_text": "Are you sure you want to delete this blueprint?",
"add_blueprint": "Add blueprint", "add_blueprint": "Import blueprint",
"use_blueprint": "Create automation", "use_blueprint": "Create automation",
"delete_blueprint": "Delete blueprint" "delete_blueprint": "Delete blueprint"
}, },
@ -1462,6 +1462,7 @@
"import_header": "Import \"{name}\" (type: {domain})", "import_header": "Import \"{name}\" (type: {domain})",
"import_introduction": "You can import blueprints of other users from Github and the community forums. Enter the URL of the blueprint below.", "import_introduction": "You can import blueprints of other users from Github and the community forums. Enter the URL of the blueprint below.",
"url": "URL of the blueprint", "url": "URL of the blueprint",
"raw_blueprint": "Blueprint content",
"importing": "Importing blueprint...", "importing": "Importing blueprint...",
"import_btn": "Import blueprint", "import_btn": "Import blueprint",
"saving": "Saving blueprint...", "saving": "Saving blueprint...",