Skip to content

Commit f99e939

Browse files
MrHutmatCopilotnielslyngsoeandr317c
authored
Culture and Hostnames: Add ability to sort hostnames (closes #20691) (#20826)
* Adding the sorter controller, and fixing some ui elements so you are able to drag the hostname elements around to sort them * Fixed sorting * Changed the html structure and tweaked around with the css to make it look better. Added a description for the Culture section. Alligned the rendered text to allign better with the name "Culture and Hostnames" * Update src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts Forgot to remove this after I was done testing Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts Changing grid-gap to just gap Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Removed the disabled and readonly props I added since they are not needed. Removed the conditional rendering that was attached to the readonly and disabled properties * Removed the item id from the element and changed css and sorter logic to target the hostname-item class instead * Updated test * Bumped helpers --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Niels Lyngsø <nsl@umbraco.dk> Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Co-authored-by: Andreas Zerbst <andr317c@live.dk>
1 parent 9c038bc commit f99e939

File tree

5 files changed

+182
-89
lines changed

5 files changed

+182
-89
lines changed

src/Umbraco.Web.UI.Client/src/assets/lang/en.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -113,26 +113,26 @@ export default {
113113
},
114114
assignDomain: {
115115
permissionDenied: 'Permission denied.',
116-
addNew: 'Add new domain',
117-
addCurrent: 'Add current domain',
116+
addNew: 'Add new hostname',
117+
addCurrent: 'Add current hostname',
118118
remove: 'remove',
119119
invalidNode: 'Invalid node.',
120-
invalidDomain: 'One or more domains have an invalid format.',
121-
duplicateDomain: 'Domain has already been assigned.',
122-
language: 'Language',
123-
domain: 'Domain',
124-
domainCreated: "New domain '%0%' has been created",
125-
domainDeleted: "Domain '%0%' is deleted",
126-
domainExists: "Domain '%0%' has already been assigned",
127-
domainUpdated: "Domain '%0%' has been updated",
128-
orEdit: 'Edit Current Domains',
120+
invalidDomain: 'One or more hostnames have an invalid format.',
121+
duplicateDomain: 'Hostname has already been assigned.',
122+
language: 'Culture',
123+
domain: 'Hostname',
124+
domainCreated: "New hostname '%0%' has been created",
125+
domainDeleted: "Hostname '%0%' is deleted",
126+
domainExists: "Hostname '%0%' has already been assigned",
127+
domainUpdated: "Hostname '%0%' has been updated",
128+
orEdit: 'Edit Current Hostnames',
129129
domainHelpWithVariants:
130-
'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".',
130+
'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".',
131131
inherit: 'Inherit',
132132
setLanguage: 'Culture',
133133
setLanguageHelp:
134-
'Set the culture for nodes below the current node,<br /> or inherit culture from parent nodes. Will also apply<br /> to the current node, unless a domain below applies too.',
135-
setDomains: 'Domains',
134+
'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.',
135+
setDomains: 'Hostnames',
136136
},
137137
buttons: {
138138
clearSelection: 'Clear selection',
@@ -191,7 +191,7 @@ export default {
191191
save: 'Media saved',
192192
},
193193
auditTrails: {
194-
assigndomain: 'Domain assigned: %0%',
194+
assigndomain: 'Hostname assigned: %0%',
195195
atViewingFor: 'Viewing for',
196196
delete: 'Content deleted',
197197
unpublish: 'Content unpublished',
@@ -209,7 +209,7 @@ export default {
209209
custom: '%0%',
210210
contentversionpreventcleanup: 'Clean up disabled for version: %0%',
211211
contentversionenablecleanup: 'Clean up enabled for version: %0%',
212-
smallAssignDomain: 'Assign Domain',
212+
smallAssignDomain: 'Assign Hostname',
213213
smallCopy: 'Copy',
214214
smallPublish: 'Publish',
215215
smallPublishVariant: 'Publish',
@@ -1562,9 +1562,9 @@ export default {
15621562
dictionaryItemExportedError: 'An error occurred while exporting the dictionary item(s)',
15631563
dictionaryItemImported: 'The following dictionary item(s) has been imported!',
15641564
publishWithNoDomains:
1565-
'Domains are not configured for multilingual site, please contact an administrator, see log for more information',
1565+
'Hostnames are not configured for multilingual site, please contact an administrator, see log for more information',
15661566
publishWithMissingDomain:
1567-
'There is no domain configured for %0%, please contact an administrator, see log for more information',
1567+
'There is no hostname configured for %0%, please contact an administrator, see log for more information',
15681568
copySuccessMessage: 'Your system information has successfully been copied to the clipboard',
15691569
cannotCopyInformation: 'Could not copy your system information to the clipboard',
15701570
webhookSaved: 'Webhook saved',
@@ -2786,7 +2786,7 @@ export default {
27862786
minimalLevelDescription: 'We will only send an anonymised site ID to let us know that the site exists.',
27872787
basicLevelDescription: 'We will send an anonymised site ID, Umbraco version, and packages installed',
27882788
detailedLevelDescription:
2789-
'We will send: <ul><li>Anonymised site ID, Umbraco version, and packages installed.</li><li>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.</li><li>System information: Webserver, server OS, server framework, server OS language, and database provider.</li><li>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.</li></ul> <em>We might change what we send on the Detailed level in the future. If so, it will be listed above.<br>By choosing "Detailed" you agree to current and future anonymised information being collected.</em>',
2789+
'We will send: <ul><li>Anonymised site ID, Umbraco version, and packages installed.</li><li>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.</li><li>System information: Webserver, server OS, server framework, server OS language, and database provider.</li><li>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.</li></ul> <em>We might change what we send on the Detailed level in the future. If so, it will be listed above.<br>By choosing "Detailed" you agree to current and future anonymised information being collected.</em>',
27902790
},
27912791
routing: {
27922792
routeNotFoundTitle: 'Not found',

src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/modal/culture-and-hostnames-modal.element.ts

Lines changed: 157 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,49 @@ import type {
33
UmbCultureAndHostnamesModalData,
44
UmbCultureAndHostnamesModalValue,
55
} from './culture-and-hostnames-modal.token.js';
6-
import { css, customElement, html, query, repeat, state } from '@umbraco-cms/backoffice/external/lit';
6+
import {
7+
css,
8+
customElement,
9+
html,
10+
query,
11+
repeat,
12+
state,
13+
type PropertyValues,
14+
} from '@umbraco-cms/backoffice/external/lit';
715
import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
816
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
917
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
10-
import type { DomainPresentationModel } from '@umbraco-cms/backoffice/external/backend-api';
1118
import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
1219
import type { UUIInputEvent, UUIPopoverContainerElement, UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
20+
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
21+
import { UmbId } from '@umbraco-cms/backoffice/id';
1322

23+
interface UmbDomainPresentationModel {
24+
unique: string;
25+
domainName: string;
26+
isoCode: string;
27+
}
1428
@customElement('umb-culture-and-hostnames-modal')
1529
export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
1630
UmbCultureAndHostnamesModalData,
1731
UmbCultureAndHostnamesModalValue
1832
> {
33+
#sorter = new UmbSorterController(this, {
34+
getUniqueOfElement: (element) => {
35+
return element.getAttribute('data-sort-entry-id');
36+
},
37+
getUniqueOfModel: (modelEntry: UmbDomainPresentationModel) => {
38+
return modelEntry.unique;
39+
},
40+
itemSelector: '.hostname-item',
41+
containerSelector: '#sorter-wrapper',
42+
onChange: ({ model }) => {
43+
const oldValue = this._domains;
44+
this._domains = model;
45+
this.requestUpdate('_domains', oldValue);
46+
},
47+
});
48+
1949
#documentRepository = new UmbDocumentCultureAndHostnamesRepository(this);
2050
#languageCollectionRepository = new UmbLanguageCollectionRepository(this);
2151

@@ -28,13 +58,20 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
2858
private _defaultIsoCode?: string | null;
2959

3060
@state()
31-
private _domains: Array<DomainPresentationModel> = [];
61+
private _domains: Array<UmbDomainPresentationModel> = [];
3262

3363
@query('#more-options')
3464
popoverContainerElement?: UUIPopoverContainerElement;
3565

3666
// Init
3767

68+
override willUpdate(changedProperties: PropertyValues) {
69+
if (changedProperties.has('_domains')) {
70+
// Update sorter whenever _domains changes
71+
this.#sorter.setModel(this._domains);
72+
}
73+
}
74+
3875
override firstUpdated() {
3976
this.#unique = this.data?.unique;
4077
this.#requestLanguages();
@@ -47,7 +84,7 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
4784

4885
if (!data) return;
4986
this._defaultIsoCode = data.defaultIsoCode;
50-
this._domains = data.domains;
87+
this._domains = data.domains.map((domain) => ({ ...domain, unique: UmbId.new() }));
5188
}
5289

5390
async #requestLanguages() {
@@ -57,7 +94,8 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
5794
}
5895

5996
async #handleSave() {
60-
this.value = { defaultIsoCode: this._defaultIsoCode, domains: this._domains };
97+
const cleanDomains = this._domains.map((domain) => ({ domainName: domain.domainName, isoCode: domain.isoCode }));
98+
this.value = { defaultIsoCode: this._defaultIsoCode, domains: cleanDomains };
6199
const { error } = await this.#documentRepository.updateCultureAndHostnames(this.#unique!, this.value);
62100

63101
if (!error) {
@@ -101,18 +139,61 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
101139
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
102140
// @ts-ignore
103141
this.popoverContainerElement?.hidePopover();
104-
this._domains = [...this._domains, { isoCode: defaultModel?.unique ?? '', domainName: window.location.host }];
142+
this._domains = [
143+
...this._domains,
144+
{ isoCode: defaultModel?.unique ?? '', domainName: window.location.host, unique: UmbId.new() },
145+
];
146+
147+
this.#focusNewItem();
105148
} else {
106-
this._domains = [...this._domains, { isoCode: defaultModel?.unique ?? '', domainName: '' }];
149+
this._domains = [...this._domains, { isoCode: defaultModel?.unique ?? '', domainName: '', unique: UmbId.new() }];
150+
151+
this.#focusNewItem();
107152
}
108153
}
109154

155+
async #focusNewItem() {
156+
await this.updateComplete;
157+
const items = this.shadowRoot?.querySelectorAll('div.hostname-item') as NodeListOf<HTMLElement>;
158+
const newItem = items[items.length - 1];
159+
const firstInput = newItem?.querySelector('uui-input') as HTMLElement;
160+
firstInput?.focus();
161+
}
162+
110163
// Renders
111164

112165
override render() {
113166
return html`
114167
<umb-body-layout headline=${this.localize.term('actions_assigndomain')}>
115-
${this.#renderCultureSection()} ${this.#renderDomainSection()}
168+
<uui-box>
169+
<umb-property-layout
170+
label=${this.localize.term('assignDomain_language')}
171+
description=${this.localize.term('assignDomain_setLanguageHelp')}
172+
orientation="vertical"
173+
><div slot="editor">
174+
<uui-combobox
175+
id="select"
176+
label=${this.localize.term('assignDomain_language')}
177+
.value=${(this._defaultIsoCode as string) ?? 'inherit'}
178+
@change=${this.#onChangeLanguage}>
179+
<uui-combobox-list>
180+
<uui-combobox-list-option .value=${'inherit'}>
181+
${this.localize.term('assignDomain_inherit')}
182+
</uui-combobox-list-option>
183+
${this.#renderLanguageModelOptions()}
184+
</uui-combobox-list>
185+
</uui-combobox>
186+
</div>
187+
</umb-property-layout>
188+
</uui-box>
189+
<uui-box>
190+
<umb-property-layout
191+
label=${this.localize.term('assignDomain_setDomains')}
192+
description=${this.localize.term('assignDomain_domainHelpWithVariants')}
193+
orientation="vertical"
194+
><div slot="editor">${this.#renderDomains()} ${this.#renderAddNewDomainButton()}</div></umb-property-layout
195+
>
196+
</uui-box>
116197
<uui-button
117198
slot="actions"
118199
id="close"
@@ -129,64 +210,35 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
129210
`;
130211
}
131212

132-
#renderCultureSection() {
133-
return html`
134-
<uui-box headline=${this.localize.term('assignDomain_setLanguage')}>
135-
<uui-label for="select">${this.localize.term('assignDomain_language')}</uui-label>
136-
<uui-combobox
137-
id="select"
138-
label=${this.localize.term('assignDomain_language')}
139-
.value=${(this._defaultIsoCode as string) ?? 'inherit'}
140-
@change=${this.#onChangeLanguage}>
141-
<uui-combobox-list>
142-
<uui-combobox-list-option .value=${'inherit'}>
143-
${this.localize.term('assignDomain_inherit')}
144-
</uui-combobox-list-option>
145-
${this.#renderLanguageModelOptions()}
146-
</uui-combobox-list>
147-
</uui-combobox>
148-
</uui-box>
149-
`;
150-
}
151-
152-
#renderDomainSection() {
153-
return html`
154-
<uui-box headline=${this.localize.term('assignDomain_setDomains')}>
155-
<umb-localize key="assignDomain_domainHelpWithVariants">
156-
Valid domain names are: "example.com", "www.example.com", "example.com:8080", or
157-
"https://www.example.com/".<br />Furthermore also one-level paths in domains are supported, eg.
158-
"example.com/en" or "/en".
159-
</umb-localize>
160-
${this.#renderDomains()} ${this.#renderAddNewDomainButton()}
161-
</uui-box>
162-
`;
163-
}
164-
165213
#renderDomains() {
166-
if (!this._domains?.length) return;
167214
return html`
168-
<div id="domains">
215+
<div id="sorter-wrapper">
169216
${repeat(
170217
this._domains,
171-
(domain) => domain.isoCode,
218+
(domain) => domain.unique,
172219
(domain, index) => html`
173-
<uui-input
174-
label=${this.localize.term('assignDomain_domain')}
175-
.value=${domain.domainName}
176-
@change=${(e: UUIInputEvent) => this.#onChangeDomainHostname(e, index)}></uui-input>
177-
<uui-combobox
178-
.value=${domain.isoCode as string}
179-
label=${this.localize.term('assignDomain_language')}
180-
@change=${(e: UUISelectEvent) => this.#onChangeDomainLanguage(e, index)}>
181-
<uui-combobox-list> ${this.#renderLanguageModelOptions()} </uui-combobox-list>
182-
</uui-combobox>
183-
<uui-button
184-
look="outline"
185-
color="danger"
186-
label=${this.localize.term('assignDomain_remove')}
187-
@click=${() => this.#onRemoveDomain(index)}>
188-
<uui-icon name="icon-trash"></uui-icon>
189-
</uui-button>
220+
<div class="hostname-item" data-sort-entry-id=${domain.unique}>
221+
<uui-icon name="icon-grip" class="handle"></uui-icon>
222+
<div class="hostname-wrapper">
223+
<uui-input
224+
label=${this.localize.term('assignDomain_domain')}
225+
.value=${domain.domainName}
226+
@change=${(e: UUIInputEvent) => this.#onChangeDomainHostname(e, index)}></uui-input>
227+
<uui-combobox
228+
.value=${domain.isoCode as string}
229+
label=${this.localize.term('assignDomain_language')}
230+
@change=${(e: UUISelectEvent) => this.#onChangeDomainLanguage(e, index)}>
231+
<uui-combobox-list> ${this.#renderLanguageModelOptions()} </uui-combobox-list>
232+
</uui-combobox>
233+
<uui-button
234+
look="outline"
235+
color="danger"
236+
label=${this.localize.term('assignDomain_remove')}
237+
@click=${() => this.#onRemoveDomain(index)}>
238+
<uui-icon name="icon-trash"></uui-icon>
239+
</uui-button>
240+
</div>
241+
</div>
190242
`,
191243
)}
192244
</div>
@@ -229,6 +281,9 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
229281
static override styles = [
230282
UmbTextStyles,
231283
css`
284+
umb-property-layout[orientation='vertical'] {
285+
padding: 0;
286+
}
232287
uui-button-group {
233288
width: 100%;
234289
}
@@ -241,12 +296,49 @@ export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
241296
flex-grow: 0;
242297
}
243298
244-
#domains {
245-
margin-top: var(--uui-size-layout-1);
246-
margin-bottom: var(--uui-size-2);
299+
.hostname-item {
300+
position: relative;
301+
display: flex;
302+
gap: var(--uui-size-1);
303+
align-items: center;
304+
}
305+
306+
.hostname-wrapper {
307+
position: relative;
308+
flex: 1;
247309
display: grid;
248310
grid-template-columns: 1fr 1fr auto;
249-
grid-gap: var(--uui-size-1);
311+
gap: var(--uui-size-1);
312+
}
313+
314+
#sorter-wrapper {
315+
margin-bottom: var(--uui-size-2);
316+
display: flex;
317+
flex-direction: column;
318+
gap: var(--uui-size-1);
319+
}
320+
321+
.handle {
322+
cursor: grab;
323+
}
324+
325+
.handle:active {
326+
cursor: grabbing;
327+
}
328+
#action {
329+
display: block;
330+
}
331+
332+
.--umb-sorter-placeholder {
333+
position: relative;
334+
visibility: hidden;
335+
}
336+
.--umb-sorter-placeholder::after {
337+
content: '';
338+
position: absolute;
339+
inset: 0px;
340+
border-radius: var(--uui-border-radius);
341+
border: 1px dashed var(--uui-color-divider-emphasis);
250342
}
251343
`,
252344
];

0 commit comments

Comments
 (0)