Merge branch 'main' into bugfix/persisting-user-group-start-nodes

This commit is contained in:
Mads Rasmussen
2024-04-17 12:24:51 +02:00
committed by GitHub
19 changed files with 426 additions and 138 deletions

View File

@@ -1,12 +1,13 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { tryExecute } from '@umbraco-cms/backoffice/resources';
import { ServerService } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbBasicState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import {
type UmbExtensionManifestInitializer,
UmbExtensionsManifestInitializer,
} from '@umbraco-cms/backoffice/extension-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestSection } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { type ManifestSection, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbExtensionManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
export class UmbBackofficeContext extends UmbContextBase<UmbBackofficeContext> {
#activeSectionAlias = new UmbStringState(undefined);
@@ -16,11 +17,32 @@ export class UmbBackofficeContext extends UmbContextBase<UmbBackofficeContext> {
#allowedSections = new UmbBasicState<Array<UmbExtensionManifestInitializer<ManifestSection>>>([]);
public readonly allowedSections = this.#allowedSections.asObservable();
#verison = new UmbStringState(undefined);
public readonly version = this.#verison.asObservable();
constructor(host: UmbControllerHost) {
super(host, UMB_BACKOFFICE_CONTEXT);
new UmbExtensionsManifestInitializer(this, umbExtensionsRegistry, 'section', null, (sections) => {
this.#allowedSections.setValue([...sections]);
});
this.#getVersion();
}
async #getVersion() {
const { data } = await tryExecute(ServerService.getServerInformation());
if (!data) return;
// A quick semver parser (to remove the unwanted bits) [LK]
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [semVer, major, minor, patch, prerelease, buildmetadata] =
data.version.match(
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,
) ?? [];
const version = [major, minor, patch].join('.') + (prerelease ? `-${prerelease}` : '');
this.#verison.setValue(version);
}
public setActiveSectionAlias(alias: string) {

View File

@@ -0,0 +1,73 @@
import { UMB_BACKOFFICE_CONTEXT } from '../backoffice.context.js';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
@customElement('umb-backoffice-header-logo')
export class UmbBackofficeHeaderLogoElement extends UmbLitElement {
@state()
private _version?: string;
constructor() {
super();
this.consumeContext(UMB_BACKOFFICE_CONTEXT, (context) => {
this.observe(
context.version,
(version) => {
if (!version) return;
this._version = version;
},
'_observeVersion',
);
});
}
render() {
return html`
<uui-button id="logo" look="primary" label="Umbraco" compact popovertarget="logo-popover">
<img src="/umbraco/backoffice/assets/umbraco_logomark_white.svg" alt="Umbraco" />
</uui-button>
<uui-popover-container id="logo-popover" placement="bottom-start">
<umb-popover-layout>
<div id="modal">
<img src="/umbraco/backoffice/assets/umbraco_logo_blue.svg" alt="Umbraco" loading="lazy" />
<span>${this._version}</span>
<a href="https://umbraco.com" target="_blank" rel="noopener">Umbraco.com</a>
</div>
</umb-popover-layout>
</uui-popover-container>
`;
}
static styles = [
UmbTextStyles,
css`
#logo {
--uui-button-padding-top-factor: 1;
--uui-button-padding-bottom-factor: 0.5;
margin-right: var(--uui-size-space-2);
--uui-button-background-color: transparent;
}
#logo > img {
height: var(--uui-size-10);
width: var(--uui-size-10);
}
#modal {
padding: var(--uui-size-space-6);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--uui-size-space-3);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-backoffice-header-logo': UmbBackofficeHeaderLogoElement;
}
}

View File

@@ -1,4 +1,3 @@
import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -7,17 +6,14 @@ export class UmbBackofficeHeaderElement extends UmbLitElement {
render() {
return html`
<div id="appHeader">
<uui-button id="logo" look="primary" label="Umbraco" compact>
<img src="/umbraco/backoffice/assets/umbraco_logomark_white.svg" alt="Umbraco" />
</uui-button>
<umb-backoffice-header-logo></umb-backoffice-header-logo>
<umb-backoffice-header-sections id="sections"></umb-backoffice-header-sections>
<umb-backoffice-header-apps></umb-backoffice-header-apps>
</div>
`;
}
static styles: CSSResultGroup = [
static styles = [
css`
:host {
width: 100%;
@@ -31,18 +27,6 @@ export class UmbBackofficeHeaderElement extends UmbLitElement {
padding: 0 var(--uui-size-space-5);
}
#logo {
--uui-button-padding-top-factor: 1;
--uui-button-padding-bottom-factor: 0.5;
margin-right: var(--uui-size-space-2);
--uui-button-background-color: transparent;
}
#logo img {
height: var(--uui-size-10);
width: var(--uui-size-10);
}
#sections {
flex: 1 1 auto;
}

View File

@@ -1,4 +1,5 @@
export * from './backoffice-header-apps.element.js';
export * from './backoffice-header-logo.element.js';
export * from './backoffice-header-sections.element.js';
export * from './backoffice-header.element.js';
export * from './backoffice-main.element.js';

View File

@@ -543,7 +543,6 @@ export type DatatypeConfigurationResponseModel = {
canBeChanged: DataTypeChangeModeModel
documentListViewId: string
mediaListViewId: string
memberListViewId: string
};
export type DefaultReferenceResponseModel = {
@@ -689,6 +688,11 @@ documentType: DocumentTypeReferenceResponseModel
variants: Array<DocumentVariantItemResponseModel>
};
export type DocumentTypeBlueprintItemResponseModel = {
id: string
name: string
};
export type DocumentTypeCleanupModel = {
preventCleanup: boolean
keepAllVersionsNewerThanDays?: number | null
@@ -800,6 +804,11 @@ export type DocumentUrlInfoModel = {
url: string
};
export type DocumentUrlInfoResponseModel = {
id: string
urlInfos: Array<DocumentUrlInfoModel>
};
export type DocumentValueModel = {
culture?: string | null
segment?: string | null
@@ -1252,6 +1261,11 @@ export type MediaUrlInfoModel = {
url: string
};
export type MediaUrlInfoResponseModel = {
id: string
urlInfos: Array<MediaUrlInfoModel>
};
export type MediaValueModel = {
culture?: string | null
segment?: string | null
@@ -1579,6 +1593,11 @@ export type PagedDocumentTreeItemResponseModel = {
items: Array<DocumentTreeItemResponseModel>
};
export type PagedDocumentTypeBlueprintItemResponseModel = {
total: number
items: Array<DocumentTypeBlueprintItemResponseModel>
};
export type PagedDocumentTypeTreeItemResponseModel = {
total: number
items: Array<DocumentTypeTreeItemResponseModel>
@@ -2971,6 +2990,10 @@ PostDocumentBlueprintFromDocument: {
GetItemDocumentBlueprint: {
id?: Array<string>
};
GetTreeDocumentBlueprintAncestors: {
descendantId?: string
};
GetTreeDocumentBlueprintChildren: {
foldersOnly?: boolean
@@ -3000,6 +3023,7 @@ take?: number
,PutDocumentBlueprintFolderById: string
,PostDocumentBlueprintFromDocument: string
,GetItemDocumentBlueprint: Array<DocumentBlueprintItemResponseModel>
,GetTreeDocumentBlueprintAncestors: Array<DocumentBlueprintTreeItemResponseModel>
,GetTreeDocumentBlueprintChildren: PagedDocumentBlueprintTreeItemResponseModel
,GetTreeDocumentBlueprintRoot: PagedDocumentBlueprintTreeItemResponseModel
@@ -3030,6 +3054,12 @@ requestBody?: UpdateDocumentTypeRequestModel
GetDocumentTypeByIdAllowedChildren: {
id: string
skip?: number
take?: number
};
GetDocumentTypeByIdBlueprint: {
id: string
skip?: number
take?: number
};
@@ -3109,6 +3139,7 @@ take?: number
,DeleteDocumentTypeById: string
,PutDocumentTypeById: string
,GetDocumentTypeByIdAllowedChildren: PagedAllowedDocumentTypeModel
,GetDocumentTypeByIdBlueprint: PagedDocumentTypeBlueprintItemResponseModel
,GetDocumentTypeByIdCompositionReferences: Array<DocumentTypeCompositionResponseModel>
,PostDocumentTypeByIdCopy: string
,PutDocumentTypeByIdMove: string
@@ -3288,6 +3319,10 @@ take?: number
PutDocumentSort: {
requestBody?: SortingRequestModel
};
GetDocumentUrls: {
id?: Array<string>
};
PostDocumentValidate: {
requestBody?: CreateDocumentRequestModel
@@ -3373,6 +3408,7 @@ take?: number
,GetDocumentAreReferenced: PagedReferenceByIdModel
,GetDocumentConfiguration: DocumentConfigurationResponseModel
,PutDocumentSort: string
,GetDocumentUrls: Array<DocumentUrlInfoResponseModel>
,PostDocumentValidate: string
,GetItemDocument: Array<DocumentItemResponseModel>
,GetItemDocumentSearch: PagedModelDocumentItemResponseModel
@@ -3835,6 +3871,10 @@ take?: number
PutMediaSort: {
requestBody?: SortingRequestModel
};
GetMediaUrls: {
id?: Array<string>
};
PostMediaValidate: {
requestBody?: CreateMediaRequestModel
@@ -3900,6 +3940,7 @@ take?: number
,GetMediaAreReferenced: PagedReferenceByIdModel
,GetMediaConfiguration: MediaConfigurationResponseModel
,PutMediaSort: string
,GetMediaUrls: Array<MediaUrlInfoResponseModel>
,PostMediaValidate: string
,DeleteRecycleBinMedia: string
,DeleteRecycleBinMediaById: string

View File

@@ -1149,6 +1149,28 @@ requestBody
});
}
/**
* @returns unknown Success
* @throws ApiError
*/
public static getTreeDocumentBlueprintAncestors(data: DocumentBlueprintData['payloads']['GetTreeDocumentBlueprintAncestors'] = {}): CancelablePromise<DocumentBlueprintData['responses']['GetTreeDocumentBlueprintAncestors']> {
const {
descendantId
} = data;
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/tree/document-blueprint/ancestors',
query: {
descendantId
},
errors: {
401: `The resource is protected and requires an authentication token`,
403: `The authenticated user do not have access to this resource`,
},
});
}
/**
* @returns unknown Success
* @throws ApiError
@@ -1329,6 +1351,34 @@ take
});
}
/**
* @returns unknown Success
* @throws ApiError
*/
public static getDocumentTypeByIdBlueprint(data: DocumentTypeData['payloads']['GetDocumentTypeByIdBlueprint']): CancelablePromise<DocumentTypeData['responses']['GetDocumentTypeByIdBlueprint']> {
const {
id,
skip,
take
} = data;
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/document-type/{id}/blueprint',
path: {
id
},
query: {
skip, take
},
errors: {
401: `The resource is protected and requires an authentication token`,
403: `The authenticated user do not have access to this resource`,
404: `Not Found`,
},
});
}
/**
* @returns unknown Success
* @throws ApiError
@@ -2448,6 +2498,28 @@ take
});
}
/**
* @returns unknown Success
* @throws ApiError
*/
public static getDocumentUrls(data: DocumentData['payloads']['GetDocumentUrls'] = {}): CancelablePromise<DocumentData['responses']['GetDocumentUrls']> {
const {
id
} = data;
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/document/urls',
query: {
id
},
errors: {
401: `The resource is protected and requires an authentication token`,
403: `The authenticated user do not have access to this resource`,
},
});
}
/**
* @returns string Success
* @throws ApiError
@@ -4298,6 +4370,28 @@ take
});
}
/**
* @returns unknown Success
* @throws ApiError
*/
public static getMediaUrls(data: MediaData['payloads']['GetMediaUrls'] = {}): CancelablePromise<MediaData['responses']['GetMediaUrls']> {
const {
id
} = data;
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/media/urls',
query: {
id
},
errors: {
401: `The resource is protected and requires an authentication token`,
403: `The authenticated user do not have access to this resource`,
},
});
}
/**
* @returns string Success
* @throws ApiError
@@ -6587,6 +6681,7 @@ export class SecurityService {
url: '/umbraco/management/api/v1/security/configuration',
errors: {
401: `The resource is protected and requires an authentication token`,
403: `The authenticated user do not have access to this resource`,
},
});
}

View File

@@ -1,15 +1,12 @@
import { html, customElement, property, css, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository';
import {
UMB_DATATYPE_WORKSPACE_MODAL,
UMB_DATA_TYPE_ENTITY_TYPE,
UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS,
UMB_DATA_TYPE_PICKER_FLOW_DATA_TYPE_PICKER_MODAL,
} from '@umbraco-cms/backoffice/data-type';
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import type { UmbDataTypeItemModel } from '@umbraco-cms/backoffice/data-type';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
@customElement('umb-input-collection-configuration')
@@ -18,38 +15,24 @@ export class UmbInputCollectionConfigurationElement extends UmbFormControlMixin(
return undefined;
}
#itemManager = new UmbRepositoryItemsManager<UmbDataTypeItemModel>(
this,
UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS,
(x) => x.unique,
);
#createDataTypeModal: UmbModalRouteRegistrationController;
#dataTypeModal: UmbModalRouteRegistrationController;
#propertyEditorUiAlias = 'Umb.PropertyEditorUi.CollectionView';
@state()
private _dataTypePickerModalPath?: string;
@state()
private _item?: UmbDataTypeItemModel;
@property({ attribute: 'default-value' })
defaultValue?: string;
#setValue(value: string) {
this.value = value;
this.#itemManager.setUniques(value ? [value] : []);
this.dispatchEvent(new UmbChangeEvent());
}
constructor() {
super();
this.observe(this.#itemManager.items, (items) => {
this._item = items[0];
});
new UmbModalRouteRegistrationController(this, UMB_DATA_TYPE_PICKER_FLOW_DATA_TYPE_PICKER_MODAL)
.addAdditionalPath(':uiAlias')
.onSetup((routingInfo) => {
@@ -71,7 +54,7 @@ export class UmbInputCollectionConfigurationElement extends UmbFormControlMixin(
this._dataTypePickerModalPath = routeBuilder({ uiAlias: this.#propertyEditorUiAlias });
});
this.#createDataTypeModal = new UmbModalRouteRegistrationController(this, UMB_DATATYPE_WORKSPACE_MODAL)
this.#dataTypeModal = new UmbModalRouteRegistrationController(this, UMB_DATATYPE_WORKSPACE_MODAL)
.addAdditionalPath(':uiAlias')
.onSetup((params) => {
return { data: { entityType: UMB_DATA_TYPE_ENTITY_TYPE, preset: { editorUiAlias: params.uiAlias } } };
@@ -81,51 +64,49 @@ export class UmbInputCollectionConfigurationElement extends UmbFormControlMixin(
});
}
connectedCallback() {
super.connectedCallback();
if (this.value) {
this.#itemManager.setUniques([this.value as string]);
}
}
#clearDataType() {
this.#setValue('');
}
#createDataType() {
this.#createDataTypeModal.open(
this.#dataTypeModal.open(
{ uiAlias: this.#propertyEditorUiAlias },
`create/parent/${UMB_DATA_TYPE_ENTITY_TYPE}/null`,
);
}
#editDataType() {
this.#dataTypeModal?.open({}, `edit/${this.value}`);
}
render() {
return !this.value ? this.#renderCreate() : this.#renderConfigured();
}
#renderCreate() {
if (!this._dataTypePickerModalPath) return nothing;
return html`<uui-button
id="create-button"
color="default"
look="placeholder"
label="Configure as a collection"
href=${this._dataTypePickerModalPath}></uui-button>`;
return html`
<uui-button
id="create-button"
color="default"
look="placeholder"
label="Configure as a collection"
href=${this._dataTypePickerModalPath}></uui-button>
`;
}
#renderConfigured() {
if (!this._item || !this._dataTypePickerModalPath) return nothing;
if (!this.value || !this._dataTypePickerModalPath) return nothing;
return html`
<uui-ref-list>
<uui-ref-node-data-type standalone name=${this._item.name} detail=${this._item.unique}>
<umb-ref-data-type standalone data-type-id=${this.value as string} @open=${this.#editDataType}>
<uui-action-bar slot="actions">
<uui-button
label=${this.localize.term('general_choose')}
href=${this._dataTypePickerModalPath}></uui-button>
<uui-button @click=${this.#clearDataType} label=${this.localize.term('general_remove')}></uui-button>
</uui-action-bar>
</uui-ref-node-data-type>
</umb-ref-data-type>
</uui-ref-list>
`;
}

View File

@@ -65,6 +65,12 @@ export class UmbPropertyTypeSettingsModalElement extends UmbModalBaseElement<
this.consumeContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT, (instance) => {
if (!this.data?.contentTypeId) return;
if (instance.getUnique() !== this.data.contentTypeId) {
// We can currently only edit properties that are part of a content type workspace, which has to be present outside of the modal. [NL]
throw new Error(
'The content type workspace context does not match the content type id of the property type settings modal.',
);
}
this.observe(instance.variesByCulture, (variesByCulture) => (this._contentTypeVariesByCulture = variesByCulture));
this.observe(instance.variesBySegment, (variesBySegment) => (this._contentTypeVariesBySegment = variesBySegment));

View File

@@ -178,6 +178,13 @@ export class UmbContentTypePropertyStructureHelper<T extends UmbContentTypeModel
return this.#structure.ownerContentTypePart((x) => x?.properties.some((y) => y.id === propertyId));
}
async contentTypeOfProperty(propertyId: UmbPropertyTypeId) {
await this.#init;
if (!this.#structure) return;
return this.#structure.contentTypeOfProperty(propertyId);
}
// TODO: consider moving this to another class, to separate 'viewer' from 'manipulator':
/** Manipulate methods: */

View File

@@ -19,6 +19,8 @@ import {
import { incrementString } from '@umbraco-cms/backoffice/utils';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
type UmbPropertyTypeId = UmbPropertyTypeModel['id'];
/**
* Manages a structure of a Content Type and its properties and containers.
* This loads and merges the structures of the Content Type and its inherited and composed Content Types.
@@ -192,6 +194,10 @@ export class UmbContentTypeStructureManager<
return this.#contentTypes.getValue().find((y) => y.unique === this.#ownerContentTypeUnique);
}
getOwnerContentTypeUnique() {
return this.#ownerContentTypeUnique;
}
updateOwnerContentType(entry: Partial<T>) {
this.#contentTypes.updateOne(this.#ownerContentTypeUnique, entry);
}
@@ -670,6 +676,12 @@ export class UmbContentTypeStructureManager<
});
}
contentTypeOfProperty(propertyId: UmbPropertyTypeId) {
return this.#contentTypes.asObservablePart((contentTypes) =>
contentTypes.find((contentType) => contentType.properties.some((p) => p.id === propertyId)),
);
}
private _reset() {
this.#contentTypeObservers.forEach((observer) => observer.destroy());
this.#contentTypeObservers = [];

View File

@@ -208,10 +208,7 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement {
return html`
<umb-content-type-design-editor-property
data-umb-property-id=${property.id}
owner-content-type-id=${ifDefined(this._ownerContentType!.unique)}
owner-content-type-name=${ifDefined(this._ownerContentType!.name)}
.editContentTypePath=${this._editContentTypePath}
?inherited=${property.container?.id !== this.containerId}
?sort-mode-active=${this._sortModeActive}
.propertyStructureHelper=${this.#propertyStructureHelper}
.property=${property}>

View File

@@ -58,28 +58,21 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
}
private _property?: UmbPropertyTypeModel | UmbPropertyTypeScaffoldModel | undefined;
/**
* Inherited, Determines if the property is part of the main content type thats being edited.
* If true, then the property is inherited from another content type, not a part of the main content type.
* @type {boolean}
* @attr
* @default undefined
*/
@property({ type: Boolean })
public inherited?: boolean;
@property({ type: Boolean, reflect: true, attribute: 'sort-mode-active' })
public sortModeActive = false;
@property({ type: String, attribute: 'owner-content-type-id' })
public ownerContentTypeId?: string;
@property({ type: String, attribute: 'owner-content-type-name' })
public ownerContentTypeName?: string;
@property({ type: String, attribute: 'edit-content-type-path' })
public editContentTypePath?: string;
@state()
public _inherited?: boolean;
@state()
public _inheritedContentTypeId?: string;
@state()
public _inheritedContentTypeName?: string;
@state()
protected _modalRoute?: string;
@@ -96,7 +89,7 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
this.#settingsModal = new UmbModalRouteRegistrationController(this, UMB_PROPERTY_TYPE_SETTINGS_MODAL)
.addUniquePaths(['propertyId'])
.onSetup(() => {
const id = this.ownerContentTypeId;
const id = this._inheritedContentTypeId;
if (id === undefined) return false;
const propertyData = this.property;
if (propertyData === undefined) return false;
@@ -114,9 +107,12 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
if (this._propertyStructureHelper && this._property) {
// We can first match with something if we have a name [NL]
this.observe(
await this._propertyStructureHelper!.isOwnerProperty(this._property.id),
(isOwned) => {
this.inherited = !isOwned;
await this._propertyStructureHelper!.contentTypeOfProperty(this._property.id),
(contentType) => {
this._inherited =
this._propertyStructureHelper?.getStructureManager()?.getOwnerContentTypeUnique() !== contentType?.unique;
this._inheritedContentTypeId = contentType?.unique;
this._inheritedContentTypeName = contentType?.name;
},
'observeIsOwnerProperty',
);
@@ -196,7 +192,7 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
render() {
// TODO: Only show alias on label if user has access to DocumentType within settings: [NL]
return this.inherited ? this.renderInheritedProperty() : this.renderEditableProperty();
return this._inherited ? this.renderInheritedProperty() : this.renderEditableProperty();
}
renderInheritedProperty() {
@@ -217,8 +213,8 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
<uui-icon name="icon-merge"></uui-icon>
<span
>${this.localize.term('contentTypeEditor_inheritedFrom')}
<a href=${this.editContentTypePath + 'edit/' + this.ownerContentTypeId}>
${this.ownerContentTypeName ?? '??'}
<a href=${this.editContentTypePath + 'edit/' + this._inheritedContentTypeId}>
${this._inheritedContentTypeName ?? '??'}
</a>
</span>
</uui-tag>
@@ -275,12 +271,12 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement {
if (!this.property) return;
return html`
<div class="sortable">
<uui-icon name="${this.inherited ? 'icon-merge' : 'icon-navigation'}"></uui-icon>
<uui-icon name="${this._inherited ? 'icon-merge' : 'icon-navigation'}"></uui-icon>
${this.property.name} <span style="color: var(--uui-color-disabled-contrast)">(${this.property.alias})</span>
</div>
<uui-input
type="number"
?readonly=${this.inherited}
?readonly=${this._inherited}
label="sort order"
@change=${(e: UUIInputEvent) =>
this.#partialUpdate({ sortOrder: parseInt(e.target.value as string) ?? 0 } as UmbPropertyTypeModel)}

View File

@@ -41,7 +41,8 @@ export class UmbPropertyEditorUIColorPickerElement extends UmbLitElement impleme
#ensureHashPrefix(swatch: UmbSwatchDetails): UmbSwatchDetails {
return {
label: swatch.label,
value: swatch.value.startsWith('#') ? swatch.value : `#${swatch.value}`,
// hex color regex adapted from: https://stackoverflow.com/a/9682781/12787
value: swatch.value.match(/^(?:[0-9a-f]{3}){1,2}$/i) ? `#${swatch.value}` : swatch.value,
};
}

View File

@@ -4,4 +4,4 @@ export {
} from './detail/index.js';
export { UmbDocumentBlueprintItemRepository, UMB_DOCUMENT_BLUEPRINT_ITEM_REPOSITORY_ALIAS } from './item/index.js';
export type { UmbDocumentBlueprintItemModel } from './item/types.js';
export type { UmbDocumentBlueprintItemModel, UmbDocumentBlueprintItemBaseModel } from './item/types.js';

View File

@@ -5,7 +5,13 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbItemRepositoryBase } from '@umbraco-cms/backoffice/repository';
export class UmbDocumentBlueprintItemRepository extends UmbItemRepositoryBase<UmbDocumentBlueprintItemModel> {
#dataSource = new UmbDocumentBlueprintItemServerDataSource(this);
constructor(host: UmbControllerHost) {
super(host, UmbDocumentBlueprintItemServerDataSource, UMB_DOCUMENT_BLUEPRINT_ITEM_STORE_CONTEXT);
}
async requestItemsByDocumentType(unique: string) {
return this.#dataSource.getItemsByDocumentType(unique);
}
}

View File

@@ -1,9 +1,10 @@
import { UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE } from '../../entity.js';
import type { UmbDocumentBlueprintItemModel } from './types.js';
import { DocumentBlueprintService } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbDocumentBlueprintItemBaseModel, UmbDocumentBlueprintItemModel } from './types.js';
import { DocumentBlueprintService, DocumentTypeService } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository';
import type { DocumentBlueprintItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for Document Blueprint items that fetches data from the server
@@ -15,6 +16,7 @@ export class UmbDocumentBlueprintItemServerDataSource extends UmbItemServerDataS
DocumentBlueprintItemResponseModel,
UmbDocumentBlueprintItemModel
> {
#host: UmbControllerHost;
/**
* Creates an instance of UmbDocumentBlueprintItemServerDataSource.
* @param {UmbControllerHost} host
@@ -25,6 +27,26 @@ export class UmbDocumentBlueprintItemServerDataSource extends UmbItemServerDataS
getItems,
mapper,
});
this.#host = host;
}
async getItemsByDocumentType(unique: string) {
if (!unique) throw new Error('Unique is missing');
const { data, error } = await tryExecuteAndNotify(
this.#host,
DocumentTypeService.getDocumentTypeByIdBlueprint({ id: unique }),
);
if (data) {
const items: Array<UmbDocumentBlueprintItemBaseModel> = data.items.map((item) => ({
entityType: UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE,
unique: item.id,
name: item.name,
}));
return { data: items };
}
return { error };
}
}

View File

@@ -2,10 +2,7 @@ import type { UmbDocumentBlueprintEntityType } from '../../entity.js';
import type { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
export interface UmbDocumentBlueprintItemModel {
entityType: UmbDocumentBlueprintEntityType;
name: string;
unique: string;
export interface UmbDocumentBlueprintItemModel extends UmbDocumentBlueprintItemBaseModel {
documentType: {
unique: string;
icon: string;
@@ -13,6 +10,12 @@ export interface UmbDocumentBlueprintItemModel {
};
}
export interface UmbDocumentBlueprintItemBaseModel {
entityType: UmbDocumentBlueprintEntityType;
name: string;
unique: string;
}
export interface UmbDocumentBlueprintItemVariantModel {
name: string;
culture: string | null;

View File

@@ -11,8 +11,8 @@ import {
type UmbAllowedDocumentTypeModel,
} from '@umbraco-cms/backoffice/document-type';
import {
type UmbDocumentBlueprintItemModel,
UmbDocumentBlueprintItemRepository,
type UmbDocumentBlueprintItemBaseModel,
} from '@umbraco-cms/backoffice/document-blueprint';
@customElement('umb-document-create-options-modal')
@@ -35,7 +35,7 @@ export class UmbDocumentCreateOptionsModalElement extends UmbModalBaseElement<
`${this.localize.term('create_createUnder')} ${this.localize.term('actionCategories_content')}`;
@state()
private _availableBlueprints: Array<UmbDocumentBlueprintItemModel> = [];
private _availableBlueprints: Array<UmbDocumentBlueprintItemBaseModel> = [];
async firstUpdated() {
const parentUnique = this.data?.parent.unique;
@@ -88,10 +88,9 @@ export class UmbDocumentCreateOptionsModalElement extends UmbModalBaseElement<
this.#documentTypeUnique = documentTypeUnique;
this.#documentTypeIcon = this._allowedDocumentTypes.find((dt) => dt.unique === documentTypeUnique)?.icon ?? '';
/** TODO: Fix this to use the correct endpoint when it becomes available */
const { data } = await this.#documentBlueprintItemRepository.requestItems([]);
const { data } = await this.#documentBlueprintItemRepository.requestItemsByDocumentType(documentTypeUnique);
this._availableBlueprints = data?.filter((blueprint) => blueprint.documentType.unique === documentTypeUnique) ?? [];
this._availableBlueprints = data ?? [];
if (!this._availableBlueprints.length) {
this.#onNavigate(documentTypeUnique);

View File

@@ -1,46 +1,60 @@
/* eslint-disable lit/attribute-value-entities */
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-umbraco-news-dashboard')
export class UmbUmbracoNewsDashboardElement extends UmbLitElement {
#currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE;
@state()
private _name = '';
constructor() {
super();
this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => {
this.#currentUserContext = instance;
this.#observeCurrentUser();
});
}
#observeCurrentUser(): void {
if (!this.#currentUserContext) return;
this.observe(this.#currentUserContext.currentUser, (user) => {
this._name = user?.name ?? '';
});
}
#infoLinks = [
{
name: 'Documentation',
description: 'Find the answers to all your Umbraco questions',
href: 'https://docs.umbraco.com/?utm_source=core&utm_medium=dashboard&utm_campaign=docs',
},
{
name: 'Community',
description: 'Get support and inspiration from driven Umbraco experts',
href: 'https://our.umbraco.com/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=our_forum',
},
{
name: 'Resources',
description: 'Free video tutorials to jumpstart your journey with the CMS',
href: 'https://umbraco.com/resources/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=resources',
},
{
name: 'Training',
description: 'Real-life training and official Umbraco certifications',
href: 'https://umbraco.com/training/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=training',
},
];
render() {
return html`
<uui-box class="uui-text">
<h1 class="uui-h2" style="margin-top: var(--uui-size-layout-1);">
<umb-localize key="dashboard_welcome">Welcome</umb-localize>, ${this._name}
</h1>
<p class="uui-lead">
This is the beta version of Umbraco 14, where you can have a first-hand look at the new Backoffice.
</p>
<p>
Please refer to the
<a target="_blank" href="https://docs.umbraco.com/umbraco-cms/v/14.latest-beta/">documentation</a> to learn
more about what is possible. Here you will find excellent tutorials, guides, and references to help you get
started extending the Backoffice.
</p>
</uui-box>
<div id="info-links" class="uui-text">
<uui-box id="our-umbraco">
<div>
<h2 class="uui-h3">Our Umbraco - The Friendliest Community</h2>
<p>
Our Umbraco, the official community site, is your one-stop-shop for everything Umbraco. Whether you need a
question answered, cool plugins, or a guide of how to do something in Umbraco, the world's best and
friendliest community is just a click away.
</p>
<uui-button
look="primary"
target="_blank"
href="https://our.umbraco.com/?utm_source=core&utm_medium=dashboard&utm_content=image&utm_campaign=our"
label="Visit Our Umbraco"></uui-button>
</div>
</uui-box>
${this.#infoLinks.map(
(link) => html`
<a class="info-link" target="_blank" href=${link.href}>
<h3 class="uui-h5">${link.name}</h3>
<p>${link.description}</p>
</a>
`,
)}
</div>
`;
}
@@ -54,6 +68,34 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement {
p {
position: relative;
}
#our-umbraco {
grid-column-start: 1;
grid-column-end: -1;
margin-bottom: var(--uui-size-space-4);
}
#info-links {
display: grid;
max-width: 1000px;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-gap: var(--uui-size-space-4);
}
.info-link {
border: 1px solid var(--uui-color-border);
padding: var(--uui-size-space-4);
border-radius: calc(var(--uui-border-radius) * 2);
line-height: 1.5;
background-color: var(--uui-color-surface);
text-decoration: none;
}
.info-link h3 {
margin-top: 0;
margin-bottom: var(--uui-size-space-1);
}
.info-link p {
margin-top: 0;
margin-bottom: 0;
}
`,
];
}