This commit is contained in:
Niels Lyngsø
2024-05-16 10:35:55 +02:00
41 changed files with 426 additions and 112 deletions

View File

@@ -41,6 +41,7 @@
"./extension-registry": "./dist-cms/packages/core/extension-registry/index.js",
"./icon": "./dist-cms/packages/core/icon-registry/index.js",
"./id": "./dist-cms/packages/core/id/index.js",
"./imaging": "./dist-cms/packages/core/imaging/index.js",
"./language": "./dist-cms/packages/language/index.js",
"./lit-element": "./dist-cms/packages/core/lit-element/index.js",
"./localization": "./dist-cms/packages/core/localization/index.js",

View File

@@ -0,0 +1,35 @@
/** Example of how a grid layout stylehseet could be done with Flex box: */
.umb-block-grid__layout-container {
position: relative;
display: flex;
flex-wrap: wrap;
gap: var(--umb-block-grid--row-gap, 0) var(--umb-block-grid--column-gap, 0);
}
.umb-block-grid__layout-item {
position: relative;
--umb-block-grid__layout-item-calc: calc(var(--umb-block-grid--item-column-span) / var(--umb-block-grid--grid-columns));
width: calc(var(--umb-block-grid__layout-item-calc) * 100% - (1 - var(--umb-block-grid__layout-item-calc)) * var(--umb-block-grid--column-gap, 0px));
}
.umb-block-grid__area-container, .umb-block-grid__block--view::part(area-container) {
position: relative;
display: flex;
flex-wrap: wrap;
width: 100%;
gap: var(--umb-block-grid--areas-row-gap, 0) var(--umb-block-grid--areas-column-gap, 0);
}
.umb-block-grid__area {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
--umb-block-grid__area-calc: calc(var(--umb-block-grid--area-column-span) / var(--umb-block-grid--area-grid-columns, 1));
width: calc(var(--umb-block-grid__area-calc) * 100% - (1 - var(--umb-block-grid__area-calc)) * var(--umb-block-grid--areas-column-gap, 0px));
}
.umb-block-grid__actions {
clear: both;
}

View File

@@ -0,0 +1,46 @@
.umb-block-grid__layout-container {
position: relative;
display: grid;
grid-template-columns: repeat(var(--umb-block-grid--grid-columns, 1), minmax(0, 1fr));
grid-auto-flow: row;
grid-auto-rows: minmax(50px, min-content);
column-gap: var(--umb-block-grid--column-gap, 0);
row-gap: var(--umb-block-grid--row-gap, 0);
}
.umb-block-grid__layout-item {
position: relative;
/* For small devices we scale columnSpan by three, to make everything bigger than 1/3 take full width: */
grid-column-end: span min(calc(var(--umb-block-grid--item-column-span, 1) * 3), var(--umb-block-grid--grid-columns));
grid-row: span var(--umb-block-grid--item-row-span, 1);
}
.umb-block-grid__area-container, .umb-block-grid__block--view::part(area-container) {
position: relative;
display: grid;
grid-template-columns: repeat(var(--umb-block-grid--area-grid-columns, var(--umb-block-grid--grid-columns, 1)), minmax(0, 1fr));
grid-auto-flow: row;
grid-auto-rows: minmax(50px, min-content);
column-gap: var(--umb-block-grid--areas-column-gap, 0);
row-gap: var(--umb-block-grid--areas-row-gap, 0);
}
.umb-block-grid__area {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
/* For small devices we scale columnSpan by three, to make everything bigger than 1/3 take full width: */
grid-column-end: span min(calc(var(--umb-block-grid--area-column-span, 1) * 3), var(--umb-block-grid--area-grid-columns));
grid-row: span var(--umb-block-grid--area-row-span, 1);
}
@media (min-width:1024px) {
.umb-block-grid__layout-item {
grid-column-end: span min(var(--umb-block-grid--item-column-span, 1), var(--umb-block-grid--grid-columns));
}
.umb-block-grid__area {
grid-column-end: span min(var(--umb-block-grid--area-column-span, 1), var(--umb-block-grid--area-grid-columns));
}
}

View File

@@ -2583,6 +2583,12 @@ value: string
key: string
};
export type UserExternalLoginProviderModel = {
providerSchemeName: string
isLinkedOnUser: boolean
hasManualLinkingEnabled: boolean
};
export type UserGroupItemResponseModel = {
id: string
name: string
@@ -5234,6 +5240,7 @@ PostUserUnlock: {
,PostUserCurrentAvatar: string
,PostUserCurrentChangePassword: string
,GetUserCurrentConfiguration: CurrenUserConfigurationResponseModel
,GetUserCurrentLoginProviders: Array<UserExternalLoginProviderModel>
,GetUserCurrentLogins: LinkedLoginsRequestModel
,GetUserCurrentPermissions: UserPermissionsResponseModel
,GetUserCurrentPermissionsDocument: Array<UserPermissionsResponseModel>

View File

@@ -8686,6 +8686,21 @@ requestBody
});
}
/**
* @returns unknown Success
* @throws ApiError
*/
public static getUserCurrentLoginProviders(): CancelablePromise<UserData['responses']['GetUserCurrentLoginProviders']> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/user/current/login-providers',
errors: {
401: `The resource is protected and requires an authentication token`,
},
});
}
/**
* @returns unknown Success
* @throws ApiError

View File

@@ -19,6 +19,7 @@ export const manifest: ManifestPropertyEditorSchema = {
label: 'Amount',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.NumberRange',
config: [{ alias: 'validationRange', value: { min: 0, max: Infinity } }],
weight: 100,
},
],
defaultData: [

View File

@@ -20,6 +20,7 @@ export const manifests: Array<ManifestTypes> = [
alias: 'blockGroups',
label: '',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.BlockTypeGroupConfiguration',
weight: 1,
},
{
alias: 'useLiveEditing',
@@ -42,9 +43,12 @@ export const manifests: Array<ManifestTypes> = [
{
alias: 'gridColumns',
label: 'Grid Columns',
description: 'Set the number of columns for the layout. (defaults to 12)',
description: 'Set the number of columns for the layout.',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Integer',
config: [{ alias: 'min', value: 0 }],
config: [
{ alias: 'min', value: 0 },
{ alias: 'placeholder', value: '12' },
],
},
{
alias: 'layoutStylesheet',

View File

@@ -44,7 +44,7 @@ export class UmbPropertyEditorUIBlockGridLayoutStylesheetElement
.min=${0}
.max=${1}></umb-input-static-file>
<br />
<a href="#Missinhg_link_to_default_layout_stylesheet">Link to default layout stylesheet</a>
<a href="/umbraco/backoffice/assets/css/umbraco-blockgridlayout.css">Link to default layout stylesheet</a>
`;
}

View File

@@ -1,5 +1,5 @@
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbPropertyEditorConfig } from '@umbraco-cms/backoffice/property-editor';
@@ -8,6 +8,9 @@ import { UMB_DATA_TYPE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/data-ty
@customElement('umb-block-grid-type-workspace-view-areas')
export class UmbBlockGridTypeWorkspaceViewAreasElement extends UmbLitElement implements UmbWorkspaceViewElement {
//
@state()
_areaColumnsConfigurationObject?: UmbPropertyEditorConfig;
@state()
_areaConfigConfigurationObject?: UmbPropertyEditorConfig;
@@ -18,7 +21,8 @@ export class UmbBlockGridTypeWorkspaceViewAreasElement extends UmbLitElement imp
this.observe(
await context.propertyValueByAlias<undefined | string>('gridColumns'),
(value) => {
const dataTypeGridColumns = value ? parseInt(value, 10) : undefined;
const dataTypeGridColumns = value ? parseInt(value, 10) : 12;
this._areaColumnsConfigurationObject = [{ alias: 'placeholder', value: dataTypeGridColumns }];
this._areaConfigConfigurationObject = [{ alias: 'defaultAreaGridColumns', value: dataTypeGridColumns }];
},
'observeGridColumns',
@@ -27,21 +31,24 @@ export class UmbBlockGridTypeWorkspaceViewAreasElement extends UmbLitElement imp
}
render() {
return html`
<uui-box headline="Areas">
<umb-property
label=${this.localize.term('blockEditor_areasLayoutColumns')}
alias="areaGridColumns"
property-editor-ui-alias="Umb.PropertyEditorUi.Integer"></umb-property>
<umb-property
label=${this.localize.term('blockEditor_areasConfigurations')}
alias="areas"
property-editor-ui-alias="Umb.PropertyEditorUi.BlockGridAreasConfig"
.config=${this._areaConfigConfigurationObject}
>></umb-property
>
</uui-box>
`;
return this._areaConfigConfigurationObject
? html`
<uui-box headline="Areas">
<umb-property
label=${this.localize.term('blockEditor_areasLayoutColumns')}
alias="areaGridColumns"
property-editor-ui-alias="Umb.PropertyEditorUi.Integer"
.config=${this._areaColumnsConfigurationObject}></umb-property>
<umb-property
label=${this.localize.term('blockEditor_areasConfigurations')}
alias="areas"
property-editor-ui-alias="Umb.PropertyEditorUi.BlockGridAreasConfig"
.config=${this._areaConfigConfigurationObject}
>></umb-property
>
</uui-box>
`
: nothing;
}
static styles = [

View File

@@ -82,20 +82,23 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement
const blocks = config.getValueByAlias<Array<UmbBlockTypeBaseModel>>('blocks') ?? [];
this.#managerContext.setBlockTypes(blocks);
const useInlineEditingAsDefault = config.getValueByAlias<boolean>('useInlineEditingAsDefault');
this.#managerContext.setInlineEditingMode(useInlineEditingAsDefault);
this.style.maxWidth = config.getValueByAlias<string>('maxPropertyWidth') ?? '';
// TODO:
//config.useSingleBlockMode, not done jet
this.#managerContext.setEditorConfiguration(config);
const customCreateButtonLabel = config.getValueByAlias<string>('createLabel');
if (customCreateButtonLabel) {
this._createButtonLabel = customCreateButtonLabel;
} else if (blocks.length === 1) {
this._createButtonLabel = `${this.localize.term('general_add')} ${blocks[0].label}`;
this.#managerContext.contentTypesLoaded.then(() => {
const firstContentTypeName = this.#managerContext.getContentTypeNameOf(blocks[0].contentElementTypeKey);
this._createButtonLabel = `${this.localize.term('general_add')} ${firstContentTypeName}`;
});
}
const useInlineEditingAsDefault = config.getValueByAlias<boolean>('useInlineEditingAsDefault');
this.#managerContext.setInlineEditingMode(useInlineEditingAsDefault);
// TODO:
//config.useSingleBlockMode, not done jet
this.style.maxWidth = config.getValueByAlias<string>('maxPropertyWidth') ?? '';
this.#managerContext.setEditorConfiguration(config);
}
@state()

View File

@@ -18,6 +18,9 @@ export class UmbBlockTypeCardElement extends UmbLitElement {
@property({ type: String, attribute: false })
href?: string;
@property({ type: String, attribute: false })
iconFile?: string;
@property({ type: String, attribute: false })
iconColor?: string;
@@ -41,7 +44,7 @@ export class UmbBlockTypeCardElement extends UmbLitElement {
private _elementTypeKey?: string | undefined;
@state()
_fallbackName?: string;
_name?: string;
@state()
_fallbackIcon?: string | null;
@@ -53,7 +56,7 @@ export class UmbBlockTypeCardElement extends UmbLitElement {
const item = items[0];
if (item) {
this._fallbackIcon = item.icon;
this._fallbackName = item.name;
this._name = item.name;
}
});
}
@@ -63,9 +66,11 @@ export class UmbBlockTypeCardElement extends UmbLitElement {
return html`
<uui-card-block-type
href=${ifDefined(this.href)}
.name=${this._fallbackName ?? 'Unknown'}
.name=${this._name ?? 'Unknown'}
.background=${this.backgroundColor}>
<umb-icon name=${this._fallbackIcon ?? ''} style="color:${this.iconColor}"></umb-icon>
${this.iconFile
? html`<img src=${this.iconFile} alt="" />`
: html`<umb-icon name=${this._fallbackIcon ?? ''} style="color:${this.iconColor}"></umb-icon>`}
<slot name="actions" slot="actions"> </slot>
</uui-card-block-type>
`;

View File

@@ -7,7 +7,10 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import { UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import { UMB_DOCUMENT_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/document-type';
import {
UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT,
UMB_DOCUMENT_TYPE_PICKER_MODAL,
} from '@umbraco-cms/backoffice/document-type';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
/** TODO: Look into sending a "change" event when there is a change, rather than create, delete, and change event. Make sure it doesn't break move for RTE/List/Grid. [LI] */
@@ -123,10 +126,13 @@ export class UmbInputBlockTypeElement<
}
async #onRequestDelete(item: BlockType) {
const store = await this.getContext(UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT);
const contentType = store.getItems([item.contentElementTypeKey]);
await umbConfirmModal(this, {
color: 'danger',
headline: `Remove [TODO: Get name]?`,
content: 'Are you sure you want to remove this block type?',
headline: `Remove ${contentType[0]?.name}?`,
// TODO: Translations: [NL]
content: 'Are you sure you want to remove this Block Type Configuration?',
confirmLabel: 'Remove',
});
this.deleteItem(item.contentElementTypeKey);
@@ -143,6 +149,7 @@ export class UmbInputBlockTypeElement<
<umb-block-type-card
.data-umb-content-element-key=${block.contentElementTypeKey}
.name=${block.label}
.iconFile=${block.thumbnail}
.iconColor=${block.iconColor}
.backgroundColor=${block.backgroundColor}
.href="${this.workspacePath}edit/${block.contentElementTypeKey}"

View File

@@ -6,6 +6,7 @@ export interface UmbBlockTypeBaseModel {
label?: string;
//view?: string; // TODO: remove/replace with custom element manifest type for block list.
//stylesheet?: string; // TODO: remove/replace with custom element manifest type for block list.
thumbnail?: string;
iconColor?: string;
backgroundColor?: string;
editorSize?: UUIModalSidebarSize;

View File

@@ -26,6 +26,10 @@ export abstract class UmbBlockManagerContext<
BlockLayoutType extends UmbBlockLayoutBaseModel = UmbBlockLayoutBaseModel,
> extends UmbContextBase<UmbBlockManagerContext> {
//
get contentTypesLoaded() {
return Promise.all(this.#contentTypeRequests);
}
#contentTypeRequests: Array<Promise<unknown>> = [];
#contentTypeRepository = new UmbDocumentTypeDetailRepository(this);
#propertyAlias = new UmbStringState(undefined);
@@ -115,7 +119,9 @@ export abstract class UmbBlockManagerContext<
async #ensureContentType(unique: string) {
if (this.#contentTypes.getValue().find((x) => x.unique === unique)) return;
const { data } = await this.#contentTypeRepository.requestByUnique(unique);
const contentTypeRequest = this.#contentTypeRepository.requestByUnique(unique);
this.#contentTypeRequests.push(contentTypeRequest);
const { data } = await contentTypeRequest;
if (!data) {
this.#contentTypes.removeOne(unique);
return;
@@ -132,6 +138,9 @@ export abstract class UmbBlockManagerContext<
contentTypeNameOf(contentTypeKey: string) {
return this.#contentTypes.asObservablePart((source) => source.find((x) => x.unique === contentTypeKey)?.name);
}
getContentTypeNameOf(contentTypeKey: string) {
return this.#contentTypes.getValue().find((x) => x.unique === contentTypeKey)?.name;
}
blockTypeOf(contentTypeKey: string) {
return this.#blockTypes.asObservablePart((source) =>
source.find((x) => x.contentElementTypeKey === contentTypeKey),

View File

@@ -51,6 +51,7 @@ export interface PropertyEditorSettingsProperty {
alias: string;
propertyEditorUiAlias: string;
config?: UmbPropertyEditorConfig;
weight?: number;
}
export interface PropertyEditorSettingsDefaultData {

View File

@@ -0,0 +1,28 @@
import type { UmbImagingModel } from './types.js';
import { UmbImagingServerDataSource } from './imaging.server.data.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbImagingRepository extends UmbControllerBase implements UmbApi {
#itemSource: UmbImagingServerDataSource;
constructor(host: UmbControllerHost) {
super(host);
this.#itemSource = new UmbImagingServerDataSource(host);
}
/**
* Requests the items for the given uniques
* @param {Array<string>} uniques
* @return {*}
* @memberof UmbImagingRepository
*/
async requestResizedItems(uniques: Array<string>, imagingModel?: UmbImagingModel) {
if (!uniques.length) throw new Error('Uniques are missing');
const { data, error: _error } = await this.#itemSource.getItems(uniques, imagingModel);
const error: any = _error;
return { data, error };
}
}

View File

@@ -0,0 +1,53 @@
import type { UmbImagingModel } from './types.js';
import { ImagingService, type MediaUrlInfoResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbMediaUrlModel } from '@umbraco-cms/backoffice/media';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Imaging Service that resizes a media item from the server
* @export
* @class UmbImagingServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbImagingServerDataSource {
#host: UmbControllerHost;
/**
* Creates an instance of UmbImagingServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbImagingServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Fetches the URL for the given media items as resized images
* @param {string} unique
* @memberof UmbImagingServerDataSource
*/
async getItems(uniques: Array<string>, imagingModel?: UmbImagingModel) {
if (!uniques.length) throw new Error('Uniques are missing');
const { data, error } = await tryExecuteAndNotify(
this.#host,
ImagingService.getImagingResizeUrls({ id: uniques, ...imagingModel }),
);
if (data) {
const items = data.map((item) => this.#mapper(item));
return { data: items };
}
return { error };
}
#mapper(item: MediaUrlInfoResponseModel): UmbMediaUrlModel {
const url = item.urlInfos[0]?.url;
return {
unique: item.id,
url: url,
};
}
}

View File

@@ -0,0 +1,2 @@
export { UmbImagingRepository } from './imaging.repository.js';
export { UMB_IMAGING_REPOSITORY_ALIAS } from './manifests.js';

View File

@@ -0,0 +1,13 @@
import { UmbImagingRepository } from './imaging.repository.js';
import type { ManifestRepository, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_IMAGING_REPOSITORY_ALIAS = 'Umb.Repository.Imaging';
const repository: ManifestRepository = {
type: 'repository',
alias: UMB_IMAGING_REPOSITORY_ALIAS,
name: 'Imaging Repository',
api: UmbImagingRepository,
};
export const manifests: Array<ManifestTypes> = [repository];

View File

@@ -0,0 +1,7 @@
import type { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api';
export interface UmbImagingModel {
height?: number;
width?: number;
mode?: ImageCropModeModel;
}

View File

@@ -7,6 +7,7 @@ import { manifests as debugManifests } from './debug/manifests.js';
import { manifests as entityActionManifests } from './entity-action/manifests.js';
import { manifests as extensionManifests } from './extension-registry/manifests.js';
import { manifests as iconRegistryManifests } from './icon-registry/manifests.js';
import { manifests as imagingManifests } from './imaging/manifests.js';
import { manifests as localizationManifests } from './localization/manifests.js';
import { manifests as modalManifests } from './modal/common/manifests.js';
import { manifests as propertyActionManifests } from './property-action/manifests.js';
@@ -24,6 +25,7 @@ export const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
...authManifests,
...extensionManifests,
...iconRegistryManifests,
...imagingManifests,
...cultureManifests,
...localizationManifests,
...themeManifests,

View File

@@ -74,7 +74,7 @@ export class UmbStoreBase<StoreItemType = any> extends UmbContextBase<any> imple
* @returns {Array<StoreItemType>}
* @memberof UmbStoreBase
*/
getItems(uniques: Array<string>) {
getItems(uniques: Array<string>): Array<StoreItemType> {
return this._data.getValue().filter((item) => uniques.includes(this._data.getUniqueMethod(item) as string));
}

View File

@@ -58,7 +58,9 @@ export class UmbDataTypeWorkspaceContext
readonly propertyEditorUiAlias = this.#currentData.asObservablePart((data) => data?.editorUiAlias);
readonly propertyEditorSchemaAlias = this.#currentData.asObservablePart((data) => data?.editorAlias);
#properties = new UmbArrayState<PropertyEditorSettingsProperty>([], (x) => x.alias);
#properties = new UmbArrayState<PropertyEditorSettingsProperty>([], (x) => x.alias).sortBy(
(a, b) => (a.weight || 0) - (b.weight || 0),
);
readonly properties = this.#properties.asObservable();
#defaults = new UmbArrayState<PropertyEditorSettingsDefaultData>([], (entry) => entry.alias);
@@ -164,7 +166,11 @@ export class UmbDataTypeWorkspaceContext
return this.observe(
umbExtensionsRegistry.byTypeAndAlias('propertyEditorSchema', propertyEditorSchemaAlias),
(manifest) => {
this.#propertyEditorSchemaSettingsProperties = manifest?.meta.settings?.properties || [];
// Maps properties to have a weight, so they can be sorted
this.#propertyEditorSchemaSettingsProperties = (manifest?.meta.settings?.properties ?? []).map((x, i) => ({
...x,
weight: x.weight ?? i,
}));
this.#propertyEditorSchemaSettingsDefaultData = manifest?.meta.settings?.defaultData || [];
this.#propertyEditorSchemaConfigDefaultUIAlias = manifest?.meta.defaultPropertyEditorUiAlias || null;
},
@@ -180,7 +186,11 @@ export class UmbDataTypeWorkspaceContext
this.#propertyEditorUiName.setValue(manifest?.name || null);
this.#propertyEditorUISettingsSchemaAlias = manifest?.meta.propertyEditorSchemaAlias;
this.#propertyEditorUISettingsProperties = manifest?.meta.settings?.properties || [];
// Maps properties to have a weight, so they can be sorted, notice UI properties have a +1000 weight compared to schema properties.
this.#propertyEditorUISettingsProperties = (manifest?.meta.settings?.properties ?? []).map((x, i) => ({
...x,
weight: x.weight ?? 1000 + i,
}));
this.#propertyEditorUISettingsDefaultData = manifest?.meta.settings?.defaultData || [];
},
'editorUi',

View File

@@ -0,0 +1,6 @@
import type { UmbDocumentTypeItemStore } from './document-type-item.store.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT = new UmbContextToken<UmbDocumentTypeItemStore>(
'UmbDocumentTypeItemStore',
);

View File

@@ -1,6 +1,6 @@
import type { UmbDocumentTypeItemModel } from './types.js';
import { UmbDocumentTypeItemServerDataSource } from './document-type-item.server.data-source.js';
import { UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT } from './document-type-item.store.js';
import { UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT } from './document-type-item-store.context-token.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbItemRepositoryBase } from '@umbraco-cms/backoffice/repository';

View File

@@ -1,5 +1,5 @@
import type { UmbDocumentTypeItemModel } from './types.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT } from './document-type-item-store.context-token.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbItemStoreBase } from '@umbraco-cms/backoffice/store';
@@ -20,7 +20,3 @@ export class UmbDocumentTypeItemStore extends UmbItemStoreBase<UmbDocumentTypeIt
super(host, UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT.toString());
}
}
export const UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT = new UmbContextToken<UmbDocumentTypeItemStore>(
'UmbDocumentTypeItemStore',
);

View File

@@ -1,3 +1,4 @@
export { UmbDocumentTypeItemRepository } from './document-type-item.repository.js';
export { UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS, UMB_DOCUMENT_TYPE_ITEM_STORE_ALIAS } from './manifests.js';
export { UmbDocumentTypeItemRepository } from './document-type-item.repository.js';
export * from './document-type-item-store.context-token.js';
export * from './types.js';

View File

@@ -1,5 +1,7 @@
import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from './types.js';
import { UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS } from './views/index.js';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -7,7 +9,26 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext<
UmbMediaCollectionItemModel,
UmbMediaCollectionFilterModel
> {
#imagingRepository: UmbImagingRepository;
#thumbnailItems = new UmbArrayState<UmbMediaCollectionItemModel>([], (x) => x);
public readonly thumbnailItems = this.#thumbnailItems.asObservable();
constructor(host: UmbControllerHost) {
super(host, UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS);
this.#imagingRepository = new UmbImagingRepository(host);
this.observe(this.items, async (items) => {
if (!items?.length) return;
const { data } = await this.#imagingRepository.requestResizedItems(items.map((m) => m.unique));
this.#thumbnailItems.setValue(
items.map((item) => {
const thumbnail = data?.find((m) => m.unique === item.unique)?.url;
return { ...item, url: thumbnail };
}),
);
});
}
}

View File

@@ -20,10 +20,10 @@ export interface UmbMediaCollectionItemModel {
updateDate: Date;
updater?: string | null;
values: Array<{ alias: string; value: string }>;
url?: string;
}
export interface UmbEditableMediaCollectionItemModel {
item: UmbMediaCollectionItemModel;
editPath: string;
}

View File

@@ -26,7 +26,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
constructor() {
super();
this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => {
this.#collectionContext = collectionContext;
this.#collectionContext = collectionContext as UmbMediaCollectionContext;
this.#observeCollectionContext();
});
@@ -51,7 +51,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
this.observe(this.#collectionContext.loading, (loading) => (this._loading = loading), '_observeLoading');
this.observe(this.#collectionContext.items, (items) => (this._items = items), '_observeItems');
this.observe(this.#collectionContext.thumbnailItems, (items) => (this._items = items), '_observeItems');
this.observe(
this.#collectionContext.selection.selection,
@@ -128,6 +128,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
@deselected=${() => this.#onDeselect(item)}
class="media-item"
file-ext="${item.icon}">
${item.url ? html`<img src=${item.url} alt=${item.name} />` : html`<umb-icon name=${item.icon}></umb-icon>`}
<!-- TODO: [LK] I'd like to indicate a busy state when bulk actions are triggered. -->
<!-- <div class="container"><uui-loader></uui-loader></div> -->
</uui-card-media>
@@ -151,9 +152,12 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
#media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-template-rows: repeat(auto-fill, 200px);
grid-auto-rows: 200px;
gap: var(--uui-size-space-5);
}
umb-icon {
font-size: var(--uui-size-24);
}
`,
];
}

View File

@@ -253,7 +253,6 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement {
box-sizing: border-box;
height: auto;
width: 100%;
padding: var(--uui-size-space-3) var(--uui-size-space-6);
}
.container {

View File

@@ -2,24 +2,23 @@ import type { UmbMediaPathModel } from '../types.js';
import type { UmbMediaDetailModel } from '../../../types.js';
import { UmbMediaDetailRepository } from '../../../repository/index.js';
import { UmbMediaTreeRepository } from '../../../tree/index.js';
import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../../entity.js';
import { UMB_MEDIA_ENTITY_TYPE, UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../../entity.js';
import { css, html, customElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit';
import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbId } from '@umbraco-cms/backoffice/id';
import { getUmbracoFolderUnique } from '@umbraco-cms/backoffice/media-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
// TODO: get root from tree repository
const root = { name: 'Media', unique: null, entityType: UMB_MEDIA_ROOT_ENTITY_TYPE };
const root: UmbMediaPathModel = { name: 'Media', unique: null, entityType: UMB_MEDIA_ROOT_ENTITY_TYPE };
@customElement('umb-media-picker-folder-path')
export class UmbMediaPickerFolderPathElement extends UmbLitElement {
#mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure
#mediaDetailRepository = new UmbMediaDetailRepository(this); // used to create folders
@property({ type: Object, attribute: false })
public set currentMedia(value: UmbEntityModel | undefined) {
@property({ attribute: false })
public set currentMedia(value: UmbMediaPathModel) {
if (value !== this._currentMedia) {
this._currentMedia = value;
this.#loadPath();
@@ -31,7 +30,7 @@ export class UmbMediaPickerFolderPathElement extends UmbLitElement {
}
@state()
private _currentMedia: UmbEntityModel | undefined;
private _currentMedia: UmbMediaPathModel = root;
@state()
private _paths: Array<UmbMediaPathModel> = [root];
@@ -45,32 +44,30 @@ export class UmbMediaPickerFolderPathElement extends UmbLitElement {
}
async #loadPath() {
const unique = this._currentMedia?.unique;
const entityType = this._currentMedia?.entityType;
const unique = this._currentMedia.unique;
if (unique && entityType) {
const { data } = await this.#mediaTreeRepository.requestTreeItemAncestors({
treeItem: {
unique,
entityType,
},
});
const items = unique
? (
await this.#mediaTreeRepository.requestTreeItemAncestors({
treeItem: { unique, entityType: UMB_MEDIA_ENTITY_TYPE },
})
).data
: undefined;
if (data) {
this._paths = [
root,
...data.map((item) => ({ name: item.name, unique: item.unique, entityType: item.entityType })),
];
return;
}
if (items) {
this._paths = [
root,
...items.map((item) => ({ name: item.name, unique: item.unique, entityType: item.entityType })),
];
return;
}
this._paths = [root];
}
#goToFolder(entity: UmbEntityModel) {
#goToFolder(entity: UmbMediaPathModel) {
this._paths = [...this._paths].slice(0, this._paths.findIndex((path) => path.unique === entity.unique) + 1);
this.currentMedia = entity;
this.dispatchEvent(new UmbChangeEvent());
}
#focusFolderInput() {
@@ -84,6 +81,7 @@ export class UmbMediaPickerFolderPathElement extends UmbLitElement {
async #addFolder(e: UUIInputEvent) {
e.stopPropagation();
const newName = e.target.value as string;
this._typingNewFolder = false;
if (!newName) return;
@@ -118,7 +116,8 @@ export class UmbMediaPickerFolderPathElement extends UmbLitElement {
const entityType = data.entityType;
this._paths = [...this._paths, { name, unique, entityType }];
this.currentMedia = { unique, entityType };
this.currentMedia = { name, unique, entityType };
this.dispatchEvent(new UmbChangeEvent());
}
render() {
@@ -130,8 +129,8 @@ export class UmbMediaPickerFolderPathElement extends UmbLitElement {
html`<uui-button
compact
.label=${path.name}
?disabled=${this.currentMedia?.unique === path.unique}
@click=${() => this.#goToFolder({ unique: path.unique, entityType: path.entityType })}></uui-button
?disabled=${this.currentMedia.unique === path.unique}
@click=${() => this.#goToFolder(path)}></uui-button
>/`,
)}${this._typingNewFolder
? html`<uui-input

View File

@@ -1,19 +1,22 @@
import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';
import { type UmbMediaItemModel, UmbMediaItemRepository, UmbMediaUrlRepository } from '../../repository/index.js';
import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js';
import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../entity.js';
import type { UmbMediaCardItemModel } from './types.js';
import type { UmbMediaCardItemModel, UmbMediaPathModel } from './types.js';
import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js';
import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
const root: UmbMediaPathModel = { name: 'Media', unique: null, entityType: UMB_MEDIA_ROOT_ENTITY_TYPE };
@customElement('umb-media-picker-modal')
export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPickerModalData, UmbMediaPickerModalValue> {
#mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure
#mediaUrlRepository = new UmbMediaUrlRepository(this); // used to get urls
#mediaItemRepository = new UmbMediaItemRepository(this); // used to search & get media type of current path
#mediaItemRepository = new UmbMediaItemRepository(this); // used to search
#imagingRepository = new UmbImagingRepository(this); // used to get image renditions
#mediaItemsCurrentFolder: Array<UmbMediaCardItemModel> = [];
@@ -27,7 +30,8 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
private _searchQuery = '';
@state()
private _currentMediaEntity: UmbEntityModel = { unique: null, entityType: UMB_MEDIA_ROOT_ENTITY_TYPE };
private _currentMediaEntity: UmbMediaPathModel = root;
async connectedCallback(): Promise<void> {
super.connectedCallback();
@@ -35,7 +39,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
const { data } = await this.#mediaItemRepository.requestItems([this.data.startNode]);
if (data?.length) {
this._currentMediaEntity = { unique: data[0].unique, entityType: data[0].entityType };
this._currentMediaEntity = { name: data[0].name, unique: data[0].unique, entityType: data[0].entityType };
}
}
@@ -59,18 +63,17 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
async #mapMediaUrls(items: Array<UmbMediaItemModel>): Promise<Array<UmbMediaCardItemModel>> {
if (!items.length) return [];
const { data } = await this.#mediaUrlRepository.requestItems(items.map((item) => item.unique));
const { data } = await this.#imagingRepository.requestResizedItems(items.map((item) => item.unique));
return items.map((item): UmbMediaCardItemModel => {
const url = data?.find((media) => media.unique === item.unique)?.url;
const extension = url?.split('.').pop();
//TODO Eventually we will get a renderable img from the server. Use this for the url. [LI]
return { name: item.name, unique: item.unique, url, extension, entityType: item.entityType };
return { name: item.name, unique: item.unique, url, icon: item.mediaType.icon, entityType: item.entityType };
});
}
#onOpen(item: UmbMediaCardItemModel) {
this._currentMediaEntity = {
name: item.name,
unique: item.unique,
entityType: UMB_MEDIA_ROOT_ENTITY_TYPE,
};
@@ -189,9 +192,10 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
@selected=${() => this.#onSelected(item)}
@deselected=${() => this.#onDeselected(item)}
?selected=${this.value?.selection?.find((value) => value === item.unique)}
selectable
file-ext=${ifDefined(item.extension)}>
${item.url ? html`<img src=${item.url} alt=${ifDefined(item.name)} />` : ''}
selectable>
${item.url
? html`<img src=${item.url} alt=${ifDefined(item.name)} />`
: html`<umb-icon .name=${item.icon}></umb-icon>`}
</uui-card-media>
`;
}
@@ -199,7 +203,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
#renderPath() {
return html`<umb-media-picker-folder-path
slot="footer-info"
.currentPath=${this._currentMediaEntity.unique}
.currentMedia=${this._currentMediaEntity}
@change=${this.#onPathChange}></umb-media-picker-folder-path>`;
}
@@ -227,10 +231,23 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement<UmbMediaPick
#media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-template-rows: repeat(auto-fill, 200px);
grid-auto-rows: 200px;
gap: var(--uui-size-space-5);
padding-bottom: 5px; /** The modal is a bit jumpy due to the img card focus/hover border. This fixes the issue. */
}
umb-icon {
font-size: var(--uui-size-24);
}
img {
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill-opacity=".1"><path d="M50 0h50v50H50zM0 50h50v50H0z"/></svg>');
background-size: 10px 10px;
background-repeat: repeat;
}
#actions {
max-width: 100%;
}
`,
];
}

View File

@@ -6,7 +6,7 @@ export interface UmbMediaCardItemModel {
unique: string;
entityType: UmbMediaEntityType;
url?: string;
extension?: string;
icon?: string;
}
export interface UmbMediaPathModel extends UmbEntityModel {

View File

@@ -2,4 +2,5 @@ export { UmbMediaDetailRepository, UMB_MEDIA_DETAIL_REPOSITORY_ALIAS } from './d
export { UmbMediaItemRepository, UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from './item/index.js';
export { UmbMediaUrlRepository, UMB_MEDIA_URL_REPOSITORY_ALIAS } from './url/index.js';
export type { UmbMediaUrlModel } from './url/types.js';
export type { UmbMediaItemModel } from './item/types.js';

View File

@@ -14,7 +14,14 @@ export const manifests: Array<ManifestTypes> = [
icon: 'icon-autofill',
group: 'common',
settings: {
properties: [],
properties: [
{
alias: 'placeholder',
label: 'Placeholder text',
description: 'Enter the text to be displayed when the value is empty',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TextBox',
},
],
defaultData: [
{
alias: 'step',

View File

@@ -18,11 +18,15 @@ export class UmbPropertyEditorUINumberElement extends UmbLitElement implements U
@state()
private _step?: number;
@state()
private _placeholder?: string;
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
this._min = this.#parseInt(config.getValueByAlias('min'));
this._max = this.#parseInt(config.getValueByAlias('max'));
this._step = this.#parseInt(config.getValueByAlias('step'));
this._placeholder = config.getValueByAlias('placeholder');
}
#parseInt(input: unknown): number | undefined {
@@ -42,7 +46,8 @@ export class UmbPropertyEditorUINumberElement extends UmbLitElement implements U
min=${ifDefined(this._min)}
max=${ifDefined(this._max)}
step=${ifDefined(this._step)}
.value=${this.value ?? 0}
placeholder=${ifDefined(this._placeholder)}
.value=${this.value ?? (this._placeholder ? undefined : 0)}
@input=${this.#onInput}>
</uui-input>
`;

View File

@@ -40,12 +40,15 @@ export class UmbUserWorkspaceAccessElement extends UmbLitElement {
#renderDocumentStartNodes() {
return html` <b><umb-localize key="sections_content">Content</umb-localize></b>
<umb-user-document-start-node
.uniques=${this._user?.documentStartNodeUniques || []}></umb-user-document-start-node>`;
.uniques=${this._user?.documentStartNodeUniques.map((reference) => reference.unique) ||
[]}></umb-user-document-start-node>`;
}
#renderMediaStartNodes() {
return html` <b><umb-localize key="sections_media">Media</umb-localize></b>
<umb-user-media-start-node .uniques=${this._user?.mediaStartNodeUniques || []}></umb-user-media-start-node>`;
<umb-user-media-start-node
.uniques=${this._user?.mediaStartNodeUniques.map((reference) => reference.unique) ||
[]}></umb-user-media-start-node>`;
}
static styles = [

View File

@@ -130,7 +130,7 @@ export class UmbUserWorkspaceAssignAccessElement extends UmbLitElement {
description="${this.localize.term('user_groupsHelp')}">
<umb-user-group-input
slot="editor"
.selection=${this._userGroupUniques}
.selection=${this._userGroupUniques.map((reference) => reference.unique)}
@change=${this.#onUserGroupsChange}></umb-user-group-input>
</umb-property-layout>`;
}
@@ -152,7 +152,7 @@ export class UmbUserWorkspaceAssignAccessElement extends UmbLitElement {
? html`
<umb-input-document
slot="editor"
.selection=${this._documentStartNodeUniques}
.selection=${this._documentStartNodeUniques.map((reference) => reference.unique)}
@change=${this.#onDocumentStartNodeChange}></umb-input-document>
`
: nothing}
@@ -177,7 +177,7 @@ export class UmbUserWorkspaceAssignAccessElement extends UmbLitElement {
? html`
<umb-input-media
slot="editor"
.selection=${this._mediaStartNodeUniques}
.selection=${this._mediaStartNodeUniques.map((reference) => reference.unique)}
@change=${this.#onMediaStartNodeChange}></umb-input-media>
`
: nothing}

View File

@@ -1,10 +1,7 @@
{
"navigationFallback": {
"rewrite": "/index.html",
"exclude": [
"*.{jpg,jpeg,gif,png,svg}",
"/assets/*"
]
},
"trailingSlash": "never"
}
"navigationFallback": {
"rewrite": "/index.html",
"exclude": ["*.{jpg,jpeg,gif,png,svg,css}", "/assets/*"]
},
"trailingSlash": "never"
}

View File

@@ -66,6 +66,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
"@umbraco-cms/backoffice/extension-registry": ["./src/packages/core/extension-registry/index.ts"],
"@umbraco-cms/backoffice/icon": ["./src/packages/core/icon-registry/index.ts"],
"@umbraco-cms/backoffice/id": ["./src/packages/core/id/index.ts"],
"@umbraco-cms/backoffice/imaging": ["./src/packages/core/imaging/index.ts"],
"@umbraco-cms/backoffice/language": ["./src/packages/language/index.ts"],
"@umbraco-cms/backoffice/lit-element": ["./src/packages/core/lit-element/index.ts"],
"@umbraco-cms/backoffice/localization": ["./src/packages/core/localization/index.ts"],