diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
index 839dfc9e7ec8..8eed97d9521a 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
@@ -113,26 +113,26 @@ export default {
},
assignDomain: {
permissionDenied: 'Permission denied.',
- addNew: 'Add new domain',
- addCurrent: 'Add current domain',
+ addNew: 'Add new hostname',
+ addCurrent: 'Add current hostname',
remove: 'remove',
invalidNode: 'Invalid node.',
- invalidDomain: 'One or more domains have an invalid format.',
- duplicateDomain: 'Domain has already been assigned.',
- language: 'Language',
- domain: 'Domain',
- domainCreated: "New domain '%0%' has been created",
- domainDeleted: "Domain '%0%' is deleted",
- domainExists: "Domain '%0%' has already been assigned",
- domainUpdated: "Domain '%0%' has been updated",
- orEdit: 'Edit Current Domains',
+ invalidDomain: 'One or more hostnames have an invalid format.',
+ duplicateDomain: 'Hostname has already been assigned.',
+ language: 'Culture',
+ domain: 'Hostname',
+ domainCreated: "New hostname '%0%' has been created",
+ domainDeleted: "Hostname '%0%' is deleted",
+ domainExists: "Hostname '%0%' has already been assigned",
+ domainUpdated: "Hostname '%0%' has been updated",
+ orEdit: 'Edit Current Hostnames',
domainHelpWithVariants:
- 'Valid domain names are: "example.com", "www.example.com", "example.com:8080", or "https://www.example.com/". Furthermore also one-level paths in domains are supported, e.g. "example.com/en" or "/en".',
+ 'Valid hostnames are: "example.com", "www.example.com", "example.com:8080", or "https://www.example.com/". Furthermore also one-level paths in hostnames are supported, e.g. "example.com/en" or "/en".',
inherit: 'Inherit',
setLanguage: 'Culture',
setLanguageHelp:
- 'Set the culture for nodes below the current node,
or inherit culture from parent nodes. Will also apply
to the current node, unless a domain below applies too.',
- setDomains: 'Domains',
+ 'Set the culture for nodes below the current node, or inherit culture from parent nodes. Will also apply to the current node, unless a hostname below applies too.',
+ setDomains: 'Hostnames',
},
buttons: {
clearSelection: 'Clear selection',
@@ -191,7 +191,7 @@ export default {
save: 'Media saved',
},
auditTrails: {
- assigndomain: 'Domain assigned: %0%',
+ assigndomain: 'Hostname assigned: %0%',
atViewingFor: 'Viewing for',
delete: 'Content deleted',
unpublish: 'Content unpublished',
@@ -209,7 +209,7 @@ export default {
custom: '%0%',
contentversionpreventcleanup: 'Clean up disabled for version: %0%',
contentversionenablecleanup: 'Clean up enabled for version: %0%',
- smallAssignDomain: 'Assign Domain',
+ smallAssignDomain: 'Assign Hostname',
smallCopy: 'Copy',
smallPublish: 'Publish',
smallPublishVariant: 'Publish',
@@ -1562,9 +1562,9 @@ export default {
dictionaryItemExportedError: 'An error occurred while exporting the dictionary item(s)',
dictionaryItemImported: 'The following dictionary item(s) has been imported!',
publishWithNoDomains:
- 'Domains are not configured for multilingual site, please contact an administrator, see log for more information',
+ 'Hostnames are not configured for multilingual site, please contact an administrator, see log for more information',
publishWithMissingDomain:
- 'There is no domain configured for %0%, please contact an administrator, see log for more information',
+ 'There is no hostname configured for %0%, please contact an administrator, see log for more information',
copySuccessMessage: 'Your system information has successfully been copied to the clipboard',
cannotCopyInformation: 'Could not copy your system information to the clipboard',
webhookSaved: 'Webhook saved',
@@ -2772,7 +2772,7 @@ export default {
minimalLevelDescription: 'We will only send an anonymised site ID to let us know that the site exists.',
basicLevelDescription: 'We will send an anonymised site ID, Umbraco version, and packages installed',
detailedLevelDescription:
- 'We will send:
- Anonymised site ID, Umbraco version, and packages installed.
- Number of: Root nodes, Content nodes, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, and Property Editors in use.
- System information: Webserver, server OS, server framework, server OS language, and database provider.
- Configuration settings: ModelsBuilder mode, if custom Umbraco path exists, ASP environment, whether the delivery API is enabled, and allows public access, and if you are in debug mode.
We might change what we send on the Detailed level in the future. If so, it will be listed above.
By choosing "Detailed" you agree to current and future anonymised information being collected.',
+ 'We will send: - Anonymised site ID, Umbraco version, and packages installed.
- Number of: Root nodes, Content nodes, Media, Document Types, Templates, Languages, Hostnames, User Group, Users, Members, Backoffice external login providers, and Property Editors in use.
- System information: Webserver, server OS, server framework, server OS language, and database provider.
- Configuration settings: ModelsBuilder mode, if custom Umbraco path exists, ASP environment, whether the delivery API is enabled, and allows public access, and if you are in debug mode.
We might change what we send on the Detailed level in the future. If so, it will be listed above.
By choosing "Detailed" you agree to current and future anonymised information being collected.',
},
routing: {
routeNotFoundTitle: 'Not found',
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts
index b03235b7ccb2..104e74e046f3 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts
@@ -3,19 +3,49 @@ import type {
UmbCultureAndHostnamesModalData,
UmbCultureAndHostnamesModalValue,
} from './culture-and-hostnames-modal.token.js';
-import { css, customElement, html, query, repeat, state } from '@umbraco-cms/backoffice/external/lit';
+import {
+ css,
+ customElement,
+ html,
+ query,
+ repeat,
+ state,
+ type PropertyValues,
+} from '@umbraco-cms/backoffice/external/lit';
import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
-import type { DomainPresentationModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
import type { UUIInputEvent, UUIPopoverContainerElement, UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
+import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
+import { UmbId } from '@umbraco-cms/backoffice/id';
+interface UmbDomainPresentationModel {
+ unique: string;
+ domainName: string;
+ isoCode: string;
+}
@customElement('umb-culture-and-hostnames-modal')
export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
UmbCultureAndHostnamesModalData,
UmbCultureAndHostnamesModalValue
> {
+ #sorter = new UmbSorterController(this, {
+ getUniqueOfElement: (element) => {
+ return element.getAttribute('data-sort-entry-id');
+ },
+ getUniqueOfModel: (modelEntry: UmbDomainPresentationModel) => {
+ return modelEntry.unique;
+ },
+ itemSelector: '.hostname-item',
+ containerSelector: '#sorter-wrapper',
+ onChange: ({ model }) => {
+ const oldValue = this._domains;
+ this._domains = model;
+ this.requestUpdate('_domains', oldValue);
+ },
+ });
+
#documentRepository = new UmbDocumentCultureAndHostnamesRepository(this);
#languageCollectionRepository = new UmbLanguageCollectionRepository(this);
@@ -28,13 +58,20 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
private _defaultIsoCode?: string | null;
@state()
- private _domains: Array = [];
+ private _domains: Array = [];
@query('#more-options')
popoverContainerElement?: UUIPopoverContainerElement;
// Init
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('_domains')) {
+ // Update sorter whenever _domains changes
+ this.#sorter.setModel(this._domains);
+ }
+ }
+
override firstUpdated() {
this.#unique = this.data?.unique;
this.#requestLanguages();
@@ -47,7 +84,7 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
if (!data) return;
this._defaultIsoCode = data.defaultIsoCode;
- this._domains = data.domains;
+ this._domains = data.domains.map((domain) => ({ ...domain, unique: UmbId.new() }));
}
async #requestLanguages() {
@@ -57,7 +94,8 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
}
async #handleSave() {
- this.value = { defaultIsoCode: this._defaultIsoCode, domains: this._domains };
+ const cleanDomains = this._domains.map((domain) => ({ domainName: domain.domainName, isoCode: domain.isoCode }));
+ this.value = { defaultIsoCode: this._defaultIsoCode, domains: cleanDomains };
const { error } = await this.#documentRepository.updateCultureAndHostnames(this.#unique!, this.value);
if (!error) {
@@ -101,18 +139,61 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.popoverContainerElement?.hidePopover();
- this._domains = [...this._domains, { isoCode: defaultModel?.unique ?? '', domainName: window.location.host }];
+ this._domains = [
+ ...this._domains,
+ { isoCode: defaultModel?.unique ?? '', domainName: window.location.host, unique: UmbId.new() },
+ ];
+
+ this.#focusNewItem();
} else {
- this._domains = [...this._domains, { isoCode: defaultModel?.unique ?? '', domainName: '' }];
+ this._domains = [...this._domains, { isoCode: defaultModel?.unique ?? '', domainName: '', unique: UmbId.new() }];
+
+ this.#focusNewItem();
}
}
+ async #focusNewItem() {
+ await this.updateComplete;
+ const items = this.shadowRoot?.querySelectorAll('div.hostname-item') as NodeListOf;
+ const newItem = items[items.length - 1];
+ const firstInput = newItem?.querySelector('uui-input') as HTMLElement;
+ firstInput?.focus();
+ }
+
// Renders
override render() {
return html`
- ${this.#renderCultureSection()} ${this.#renderDomainSection()}
+
+
+
+
+
+ ${this.localize.term('assignDomain_inherit')}
+
+ ${this.#renderLanguageModelOptions()}
+
+
+
+
+
+
+ ${this.#renderDomains()} ${this.#renderAddNewDomainButton()}
+
- ${this.localize.term('assignDomain_language')}
-
-
-
- ${this.localize.term('assignDomain_inherit')}
-
- ${this.#renderLanguageModelOptions()}
-
-
-
- `;
- }
-
- #renderDomainSection() {
- return html`
-
-
- Valid domain names are: "example.com", "www.example.com", "example.com:8080", or
- "https://www.example.com/".
Furthermore also one-level paths in domains are supported, eg.
- "example.com/en" or "/en".
-
- ${this.#renderDomains()} ${this.#renderAddNewDomainButton()}
-
- `;
- }
-
#renderDomains() {
- if (!this._domains?.length) return;
return html`
-
+
${repeat(
this._domains,
- (domain) => domain.isoCode,
+ (domain) => domain.unique,
(domain, index) => html`
-
this.#onChangeDomainHostname(e, index)}>
-
this.#onChangeDomainLanguage(e, index)}>
- ${this.#renderLanguageModelOptions()}
-
-
this.#onRemoveDomain(index)}>
-
-
+
+
+
+ this.#onChangeDomainHostname(e, index)}>
+ this.#onChangeDomainLanguage(e, index)}>
+ ${this.#renderLanguageModelOptions()}
+
+ this.#onRemoveDomain(index)}>
+
+
+
+
`,
)}
@@ -229,6 +281,9 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
static override styles = [
UmbTextStyles,
css`
+ umb-property-layout[orientation='vertical'] {
+ padding: 0;
+ }
uui-button-group {
width: 100%;
}
@@ -241,12 +296,49 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
flex-grow: 0;
}
- #domains {
- margin-top: var(--uui-size-layout-1);
- margin-bottom: var(--uui-size-2);
+ .hostname-item {
+ position: relative;
+ display: flex;
+ gap: var(--uui-size-1);
+ align-items: center;
+ }
+
+ .hostname-wrapper {
+ position: relative;
+ flex: 1;
display: grid;
grid-template-columns: 1fr 1fr auto;
- grid-gap: var(--uui-size-1);
+ gap: var(--uui-size-1);
+ }
+
+ #sorter-wrapper {
+ margin-bottom: var(--uui-size-2);
+ display: flex;
+ flex-direction: column;
+ gap: var(--uui-size-1);
+ }
+
+ .handle {
+ cursor: grab;
+ }
+
+ .handle:active {
+ cursor: grabbing;
+ }
+ #action {
+ display: block;
+ }
+
+ .--umb-sorter-placeholder {
+ position: relative;
+ visibility: hidden;
+ }
+ .--umb-sorter-placeholder::after {
+ content: '';
+ position: absolute;
+ inset: 0px;
+ border-radius: var(--uui-border-radius);
+ border: 1px dashed var(--uui-color-divider-emphasis);
}
`,
];