Merge pull request #1131 from umbraco/feature/culture-and-hostnames

Culture and Host Names
This commit is contained in:
Jacob Overgaard
2024-02-02 09:57:24 +01:00
committed by GitHub
14 changed files with 436 additions and 28 deletions

View File

@@ -1,14 +0,0 @@
import type { UmbDocumentDetailRepository } from '../repository/index.js';
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbDocumentCultureAndHostnamesEntityAction extends UmbEntityActionBase<UmbDocumentDetailRepository> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {
console.log(`execute for: ${this.unique}`);
//await this.repository?.setCultureAndHostnames();
}
}

View File

@@ -0,0 +1,28 @@
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UMB_CULTURE_AND_HOSTNAMES_MODAL, type UmbDocumentDetailRepository } from '@umbraco-cms/backoffice/document';
export class UmbDocumentCultureAndHostnamesEntityAction extends UmbEntityActionBase<UmbDocumentDetailRepository> {
#modalContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;
});
}
async execute() {
if (!this.repository) return;
this._openModal(this.unique || null);
}
private async _openModal(unique: string | null) {
if (!this.#modalContext) return;
this.#modalContext.open(UMB_CULTURE_AND_HOSTNAMES_MODAL, {
data: { unique },
});
}
}

View File

@@ -0,0 +1,2 @@
export * from './modal/index.js';
export * from './repository/index.js';

View File

@@ -0,0 +1,31 @@
import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js';
import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS } from '../../repository/index.js';
import { UmbDocumentCultureAndHostnamesEntityAction } from './culture-and-hostnames.action.js';
import type { ManifestEntityAction, ManifestModal } from '@umbraco-cms/backoffice/extension-registry';
const entityActions: Array<ManifestEntityAction> = [
{
type: 'entityAction',
alias: 'Umb.EntityAction.Document.CultureAndHostnames',
name: 'Culture And Hostnames Document Entity Action',
weight: 400,
api: UmbDocumentCultureAndHostnamesEntityAction,
meta: {
icon: 'icon-home',
label: 'Culture and Hostnames',
repositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS,
entityTypes: [UMB_DOCUMENT_ENTITY_TYPE],
},
},
];
const manifestModals: Array<ManifestModal> = [
{
type: 'modal',
alias: 'Umb.Modal.CultureAndHostnames',
name: 'Culture And Hostnames Modal',
js: () => import('./modal/culture-and-hostnames-modal.element.js'),
},
];
export const manifests = [...entityActions, ...manifestModals];

View File

@@ -0,0 +1,247 @@
import { html, customElement, state, css, repeat, query } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbLanguageRepository } from '@umbraco-cms/backoffice/language';
import type { DomainPresentationModel, LanguageResponseModel } from '@umbraco-cms/backoffice/backend-api';
import {
UmbDocumentCultureAndHostnamesRepository,
type UmbCultureAndHostnamesModalData,
type UmbCultureAndHostnamesModalValue,
} from '@umbraco-cms/backoffice/document';
import type { UUIInputEvent, UUIPopoverContainerElement, UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-culture-and-hostnames-modal')
export class UmbCultureAndHostnamesModalElement extends UmbModalBaseElement<
UmbCultureAndHostnamesModalData,
UmbCultureAndHostnamesModalValue
> {
#documentRepository = new UmbDocumentCultureAndHostnamesRepository(this);
#languageRepository = new UmbLanguageRepository(this);
#unique?: string | null;
@state()
private _languageModel: Array<LanguageResponseModel> = [];
@state()
private _defaultIsoCode?: string | null;
@state()
private _domains: Array<DomainPresentationModel> = [];
@query('#more-options')
popoverContainerElement?: UUIPopoverContainerElement;
// Init
firstUpdated() {
this.#unique = this.data?.unique;
this.#requestLanguages();
this.#readDomains();
}
async #readDomains() {
if (!this.#unique) return;
const { data } = await this.#documentRepository.readCultureAndHostnames(this.#unique);
if (!data) return;
this._defaultIsoCode = data.defaultIsoCode;
this._domains = data.domains;
}
async #requestLanguages() {
const { data } = await this.#languageRepository.requestLanguages();
if (!data) return;
this._languageModel = data.items;
}
// Modal
async #handleSave() {
this.value = { defaultIsoCode: this._defaultIsoCode, domains: this._domains };
await this.#documentRepository.updateCultureAndHostnames(this.#unique!, this.value);
}
#handleClose() {
this.modalContext?.submit();
}
// Events
#onChangeLanguage(e: UUISelectEvent) {
const defaultIsoCode = e.target.value as string;
if (defaultIsoCode === 'inherit') {
this._defaultIsoCode = null;
} else {
this._defaultIsoCode = defaultIsoCode;
}
}
#onChangeDomainLanguage(e: UUISelectEvent, index: number) {
const isoCode = e.target.value as string;
this._domains = this._domains.map((domain, i) => (index === i ? { ...domain, isoCode } : domain));
}
#onChangeDomainHostname(e: UUIInputEvent, index: number) {
const domainName = e.target.value as string;
this._domains = this._domains.map((domain, i) => (index === i ? { ...domain, domainName } : domain));
}
async #onRemoveDomain(index: number) {
this._domains = this._domains.filter((d, i) => index !== i);
}
#onAddDomain(useCurrentDomain?: boolean) {
const defaultModel = this._languageModel.find((model) => model.isDefault);
if (useCurrentDomain) {
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.popoverContainerElement?.hidePopover();
this._domains = [...this._domains, { isoCode: defaultModel?.isoCode ?? '', domainName: window.location.host }];
} else {
this._domains = [...this._domains, { isoCode: defaultModel?.isoCode ?? '', domainName: '' }];
}
}
// Renders
render() {
return html`
<umb-body-layout headline=${this.localize.term('actions_assigndomain')}>
${this.#renderCultureSection()} ${this.#renderDomainSection()}
<uui-button
slot="actions"
id="close"
label=${this.localize.term('general_close')}
@click="${this.#handleClose}"></uui-button>
<uui-button
slot="actions"
id="save"
look="primary"
color="positive"
label=${this.localize.term('buttons_save')}
@click="${this.#handleSave}"></uui-button>
</umb-body-layout>
`;
}
#renderCultureSection() {
return html`<uui-box headline=${this.localize.term('assignDomain_setLanguage')}>
<uui-label for="select">${this.localize.term('assignDomain_language')}</uui-label>
<uui-combobox
id="select"
label=${this.localize.term('assignDomain_language')}
.value=${(this._defaultIsoCode as string) ?? 'inherit'}
@change=${this.#onChangeLanguage}>
<uui-combobox-list>
<uui-combobox-list-option .value=${'inherit'}>
${this.localize.term('assignDomain_inherit')}
</uui-combobox-list-option>
${this.#renderLanguageModelOptions()}
</uui-combobox-list>
</uui-combobox>
</uui-box>`;
}
#renderDomainSection() {
return html`<uui-box headline=${this.localize.term('assignDomain_setDomains')}>
<umb-localize key="assignDomain_domainHelpWithVariants">
Valid domain names are: "example.com", "www.example.com", "example.com:8080", or "https://www.example.com/".<br />Furthermore
also one-level paths in domains are supported, eg. "example.com/en" or "/en".
</umb-localize>
${this.#renderDomains()} ${this.#renderAddNewDomainButton()}
</uui-box>`;
}
#renderDomains() {
if (!this._domains?.length) return;
return html`<div id="domains">
${repeat(
this._domains,
(domain) => domain.isoCode,
(domain, index) => html`
<uui-input
label=${this.localize.term('assignDomain_domain')}
.value=${domain.domainName}
@change=${(e: UUIInputEvent) => this.#onChangeDomainHostname(e, index)}></uui-input>
<uui-combobox
.value=${domain.isoCode as string}
label=${this.localize.term('assignDomain_language')}
@change=${(e: UUISelectEvent) => this.#onChangeDomainLanguage(e, index)}>
<uui-combobox-list> ${this.#renderLanguageModelOptions()} </uui-combobox-list>
</uui-combobox>
<uui-button
look="outline"
color="danger"
label=${this.localize.term('assignDomain_remove')}
@click=${() => this.#onRemoveDomain(index)}>
<uui-icon name="icon-trash"></uui-icon>
</uui-button>
`,
)}
</div>`;
}
#renderLanguageModelOptions() {
return html`${repeat(
this._languageModel,
(model) => model.isoCode,
(model) => html`<uui-combobox-list-option .value=${model.isoCode}>${model.name}</uui-combobox-list-option>`,
)}`;
}
#renderAddNewDomainButton() {
return html`<uui-button-group>
<uui-button
label=${this.localize.term('assignDomain_addNew')}
look="placeholder"
@click=${() => this.#onAddDomain()}></uui-button>
<uui-button
id="dropdown"
label=${this.localize.term('buttons_select')}
look="placeholder"
popovertarget="more-options">
<uui-icon name="icon-navigation-down"></uui-icon>
</uui-button>
<uui-popover-container id="more-options" placement="bottom-end">
<umb-popover-layout>
<uui-button label=${this.localize.term('assignDomain_addCurrent')} @click=${() => this.#onAddDomain(true)}></uui-button>
</umb-popover-layout>
</uui-popover-container>
</uui-button-group> `;
}
static styles = [
UmbTextStyles,
css`
uui-button-group {
width: 100%;
}
uui-box:first-child {
margin-bottom: var(--uui-size-layout-1);
}
#dropdown {
flex-grow: 0;
}
#domains {
margin-top: var(--uui-size-layout-1);
margin-bottom: var(--uui-size-2);
display: grid;
grid-template-columns: 1fr 1fr auto;
grid-gap: var(--uui-size-1);
}
`,
];
}
export default UmbCultureAndHostnamesModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-culture-and-hostnames-modal': UmbCultureAndHostnamesModalElement;
}
}

View File

@@ -0,0 +1,21 @@
import type { DomainPresentationModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbCultureAndHostnamesModalData {
unique: string | null;
}
export interface UmbCultureAndHostnamesModalValue {
defaultIsoCode?: string | null;
domains: Array<DomainPresentationModel>;
}
export const UMB_CULTURE_AND_HOSTNAMES_MODAL = new UmbModalToken<
UmbCultureAndHostnamesModalData,
UmbCultureAndHostnamesModalValue
>('Umb.Modal.CultureAndHostnames', {
modal: {
type: 'sidebar',
size: 'medium',
},
});

View File

@@ -0,0 +1,2 @@
export * from './culture-and-hostnames-modal.token.js';
export * from './culture-and-hostnames-modal.element.js';

View File

@@ -0,0 +1,42 @@
import { UmbDocumentCultureAndHostnamesServerDataSource } from './culture-and-hostnames.server.data.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { DomainsPresentationModelBaseModel } from '@umbraco-cms/backoffice/backend-api';
export class UmbDocumentCultureAndHostnamesRepository extends UmbBaseController implements UmbApi {
#dataSource = new UmbDocumentCultureAndHostnamesServerDataSource(this);
#notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE;
constructor(host: UmbControllerHost) {
super(host);
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => {
this.#notificationContext = instance;
});
}
async readCultureAndHostnames(unique: string) {
if (!unique) throw new Error('Unique is missing');
const { data, error } = await this.#dataSource.read(unique);
if (!error) {
return { data };
}
return { error };
}
async updateCultureAndHostnames(unique: string, data: DomainsPresentationModelBaseModel) {
if (!unique) throw new Error('Unique is missing');
if (!data) throw new Error('Data is missing');
const { error } = await this.#dataSource.update(unique, data);
if (!error) {
const notification = { data: { message: `Cultures and hostnames saved` } };
this.#notificationContext?.peek('positive', notification);
}
return { error };
}
}

View File

@@ -0,0 +1,44 @@
import { DocumentResource } from '@umbraco-cms/backoffice/backend-api';
import type { DomainsPresentationModelBaseModel } from '@umbraco-cms/backoffice/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Document Culture and Hostnames that fetches data from the server
* @export
* @class UmbDocumentCultureAndHostnamesServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbDocumentCultureAndHostnamesServerDataSource {
#host: UmbControllerHost;
/**
* Creates an instance of UmbDocumentCultureAndHostnamesServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbDocumentCultureAndHostnamesServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Fetches the Culture and Hostnames for the given Document unique
* @param {string} unique
* @memberof UmbDocumentCultureAndHostnamesServerDataSource
*/
async read(unique: string) {
if (!unique) throw new Error('Unique is missing');
return tryExecuteAndNotify(this.#host, DocumentResource.getDocumentByIdDomains({ id: unique }));
}
/**
* Updates Culture and Hostnames for the given Document unique
* @param {string} unique
* @param {DomainsPresentationModelBaseModel} data
* @memberof UmbDocumentCultureAndHostnamesServerDataSource
*/
async update(unique: string, data: DomainsPresentationModelBaseModel) {
if (!unique) throw new Error('Unique is missing');
return tryExecuteAndNotify(this.#host, DocumentResource.putDocumentByIdDomains({ id: unique, requestBody: data }));
}
}

View File

@@ -0,0 +1,2 @@
export { UmbDocumentCultureAndHostnamesRepository } from './culture-and-hostnames.repository.js';
export { UMB_DOCUMENT_CULTURE_AND_HOSTNAMES_REPOSITORY_ALIAS } from './manifests.js';

View File

@@ -0,0 +1,13 @@
import { UmbDocumentCultureAndHostnamesRepository } from '../repository/index.js';
import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_DOCUMENT_CULTURE_AND_HOSTNAMES_REPOSITORY_ALIAS = 'Umb.Repository.Document.CultureAndHostnames';
const repository: ManifestRepository = {
type: 'repository',
alias: UMB_DOCUMENT_CULTURE_AND_HOSTNAMES_REPOSITORY_ALIAS,
name: 'Document Culture And Hostnames Repository',
api: UmbDocumentCultureAndHostnamesRepository,
};
export const manifests = [repository];

View File

@@ -0,0 +1 @@
export * from './culture-and-hostnames/index.js';

View File

@@ -1,13 +1,13 @@
import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS } from '../repository/index.js';
import { UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_ROOT_ENTITY_TYPE } from '../entity.js';
import { UmbPublishDocumentEntityAction } from './publish.action.js';
import { UmbDocumentCultureAndHostnamesEntityAction } from './culture-and-hostnames.action.js';
import { UmbCreateDocumentBlueprintEntityAction } from './create-blueprint.action.js';
import { UmbDocumentPublicAccessEntityAction } from './public-access.action.js';
import { UmbUnpublishDocumentEntityAction } from './unpublish.action.js';
import { UmbRollbackDocumentEntityAction } from './rollback.action.js';
import { manifests as createManifests } from './create/manifests.js';
import { manifests as permissionManifests } from './permissions/manifests.js';
import { manifests as cultureAndHostnamesManifests } from './culture-and-hostnames/manifests.js';
import {
UmbCopyEntityAction,
UmbMoveEntityAction,
@@ -18,6 +18,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
const entityActions: Array<ManifestTypes> = [
...createManifests,
...permissionManifests,
...cultureAndHostnamesManifests,
{
type: 'entityAction',
alias: 'Umb.EntityAction.Document.CreateBlueprint',
@@ -70,19 +71,6 @@ const entityActions: Array<ManifestTypes> = [
entityTypes: [UMB_DOCUMENT_ROOT_ENTITY_TYPE, UMB_DOCUMENT_ENTITY_TYPE],
},
},
{
type: 'entityAction',
alias: 'Umb.EntityAction.Document.CultureAndHostnames',
name: 'Culture And Hostnames Document Entity Action',
weight: 400,
api: UmbDocumentCultureAndHostnamesEntityAction,
meta: {
icon: 'icon-home',
label: 'Culture And Hostnames (TBD)',
repositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS,
entityTypes: [UMB_DOCUMENT_ENTITY_TYPE],
},
},
{
type: 'entityAction',
alias: 'Umb.EntityAction.Document.PublicAccess',

View File

@@ -7,6 +7,7 @@ export * from './recycle-bin/index.js';
export * from './user-permissions/index.js';
export * from './components/index.js';
export * from './entity.js';
export * from './entity-actions/index.js';
export { UMB_DOCUMENT_TREE_ALIAS } from './tree/index.js';
export { UMB_CONTENT_MENU_ALIAS } from './menu.manifests.js';