Merge branch 'main' into feature/rich-text-editor

This commit is contained in:
Nathan Woulfe
2023-04-21 12:49:56 +10:00
229 changed files with 4987 additions and 1471 deletions

View File

@@ -44,6 +44,7 @@
"local-rules/enforce-element-suffix-on-element-class-name": "error",
"local-rules/prefer-umbraco-cms-imports": "error",
"local-rules/no-external-imports": "error",
"local-rules/umb-class-prefix": "error",
"@typescript-eslint/no-non-null-assertion": "off"
},
"settings": {

View File

@@ -13,7 +13,7 @@ import { UmbDocumentTypeStore } from '../src/backoffice/documents/document-types
import { UmbDocumentStore } from '../src/backoffice/documents/documents/repository/document.store.ts';
import { UmbDocumentTreeStore } from '../src/backoffice/documents/documents/repository/document.tree.store.ts';
import customElementManifests from '../custom-elements.json';
import customElementManifests from '../dist/libs/custom-elements.json';
import { UmbIconStore } from '../src/core/stores/icon/icon.store';
import { onUnhandledRequest } from '../src/core/mocks/browser';
import { handlers } from '../src/core/mocks/browser-handlers';

View File

@@ -205,4 +205,31 @@ module.exports = {
};
},
},
/** @type {import('eslint').Rule.RuleModule} */
'umb-class-prefix': {
meta: {
type: 'problem',
docs: {
description: 'Ensure that all class declarations are prefixed with "Umb"',
category: 'Best Practices',
recommended: true,
},
schema: [],
},
create: function (context) {
function checkClassName(node) {
if (node.id && node.id.name && !node.id.name.startsWith('Umb')) {
context.report({
node: node.id,
message: 'Class declaration should be prefixed with "Umb"',
});
}
}
return {
ClassDeclaration: checkClassName,
};
},
},
};

View File

@@ -6,15 +6,14 @@ import type { PropertyTypeAppearanceModel } from './PropertyTypeAppearanceModel'
import type { PropertyTypeValidationModel } from './PropertyTypeValidationModel';
export type PropertyTypeResponseModelBaseModel = {
id?: string;
containerId?: string | null;
alias?: string;
name?: string;
description?: string | null;
dataTypeId?: string;
variesByCulture?: boolean;
variesBySegment?: boolean;
validation?: PropertyTypeValidationModel;
appearance?: PropertyTypeAppearanceModel;
id?: string;
containerId?: string | null;
alias?: string;
name?: string;
description?: string | null;
dataTypeId?: string;
variesByCulture?: boolean;
variesBySegment?: boolean;
validation?: PropertyTypeValidationModel;
appearance?: PropertyTypeAppearanceModel;
};

View File

@@ -5,7 +5,7 @@ import { UmbContextRequestEventImplementation, umbContextRequestEventType } from
const testContextAlias = 'my-test-context';
class MyClass {
class UmbTestContextConsumerClass {
prop = 'value from provider';
}
@@ -39,16 +39,20 @@ describe('UmbContextConsumer', () => {
});
it('works with UmbContextProvider', (done) => {
const provider = new UmbContextProvider(document.body, testContextAlias, new MyClass());
const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass());
provider.hostConnected();
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(element, testContextAlias, (_instance: MyClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
});
const localConsumer = new UmbContextConsumer(
element,
testContextAlias,
(_instance: UmbTestContextConsumerClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
}
);
localConsumer.hostConnected();
provider.hostDisconnected();

View File

@@ -3,21 +3,21 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbContextConsumer } from '../consume/context-consumer';
import { UmbContextProviderController } from './context-provider.controller';
class MyClass {
class UmbTestContextProviderControllerClass {
prop = 'value from provider';
}
class ControllerHostElement extends UmbLitElement {}
const controllerHostElement = defineCE(ControllerHostElement);
class UmbTestControllerHostElement extends UmbLitElement {}
const controllerHostElement = defineCE(UmbTestControllerHostElement);
describe('UmbContextProviderController', () => {
let instance: MyClass;
let instance: UmbTestContextProviderControllerClass;
let provider: UmbContextProviderController;
let element: UmbLitElement;
beforeEach(async () => {
element = await fixture(`<${controllerHostElement}></${controllerHostElement}>`);
instance = new MyClass();
instance = new UmbTestContextProviderControllerClass();
provider = new UmbContextProviderController(element, 'my-test-context', instance);
});
@@ -39,11 +39,15 @@ describe('UmbContextProviderController', () => {
});
it('works with UmbContextConsumer', (done) => {
const localConsumer = new UmbContextConsumer(element, 'my-test-context', (_instance: MyClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
localConsumer.hostDisconnected();
});
const localConsumer = new UmbContextConsumer(
element,
'my-test-context',
(_instance: UmbTestContextProviderControllerClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
localConsumer.hostDisconnected();
}
);
localConsumer.hostConnected();
});

View File

@@ -3,16 +3,16 @@ import { UmbContextConsumer } from '../consume/context-consumer';
import { UmbContextRequestEventImplementation } from '../consume/context-request.event';
import { UmbContextProvider } from './context-provider';
class MyClass {
class UmbTestContextProviderClass {
prop = 'value from provider';
}
describe('UmbContextProvider', () => {
let instance: MyClass;
let instance: UmbTestContextProviderClass;
let provider: UmbContextProvider;
beforeEach(() => {
instance = new MyClass();
instance = new UmbTestContextProviderClass();
provider = new UmbContextProvider(document.body, 'my-test-context', instance);
provider.hostConnected();
});
@@ -40,10 +40,13 @@ describe('UmbContextProvider', () => {
});
it('handles context request events', (done) => {
const event = new UmbContextRequestEventImplementation('my-test-context', (_instance: MyClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
});
const event = new UmbContextRequestEventImplementation(
'my-test-context',
(_instance: UmbTestContextProviderClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
}
);
document.body.dispatchEvent(event);
});
@@ -52,11 +55,15 @@ describe('UmbContextProvider', () => {
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(element, 'my-test-context', (_instance: MyClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
localConsumer.hostDisconnected();
});
const localConsumer = new UmbContextConsumer(
element,
'my-test-context',
(_instance: UmbTestContextProviderClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
localConsumer.hostDisconnected();
}
);
localConsumer.hostConnected();
});
});

View File

@@ -5,13 +5,13 @@ import { UmbContextToken } from './context-token';
const testContextAlias = 'my-test-context';
class MyClass {
class UmbTestContextTokenClass {
prop = 'value from provider';
}
describe('ContextAlias', () => {
const contextAlias = new UmbContextToken<MyClass>(testContextAlias);
const typedProvider = new UmbContextProvider(document.body, contextAlias, new MyClass());
const contextAlias = new UmbContextToken<UmbTestContextTokenClass>(testContextAlias);
const typedProvider = new UmbContextProvider(document.body, contextAlias, new UmbTestContextTokenClass());
typedProvider.hostConnected();
after(() => {
@@ -27,7 +27,7 @@ describe('ContextAlias', () => {
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(element, contextAlias, (_instance) => {
expect(_instance).to.be.instanceOf(MyClass);
expect(_instance).to.be.instanceOf(UmbTestContextTokenClass);
expect(_instance.prop).to.eq('value from provider');
done();
});
@@ -39,8 +39,8 @@ describe('ContextAlias', () => {
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(element, testContextAlias, (_instance: MyClass) => {
expect(_instance).to.be.instanceOf(MyClass);
const localConsumer = new UmbContextConsumer(element, testContextAlias, (_instance: UmbTestContextTokenClass) => {
expect(_instance).to.be.instanceOf(UmbTestContextTokenClass);
expect(_instance.prop).to.eq('value from provider');
done();
});

View File

@@ -3,18 +3,18 @@ import { customElement } from 'lit/decorators.js';
import { UmbControllerHostElement, UmbControllerHostMixin } from './controller-host.mixin';
import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api';
class MyClass {
class UmbTestContext {
prop = 'value from provider';
}
@customElement('test-my-controller-host')
export class MyHostElement extends UmbControllerHostMixin(HTMLElement) {}
export class UmbTestControllerHostElement extends UmbControllerHostMixin(HTMLElement) {}
describe('UmbContextProvider', () => {
type NewType = UmbControllerHostElement;
let hostElement: NewType;
const contextInstance = new MyClass();
const contextInstance = new UmbTestContext();
beforeEach(() => {
hostElement = document.createElement('test-my-controller-host') as UmbControllerHostElement;
@@ -35,7 +35,7 @@ describe('UmbContextProvider', () => {
describe('Unique controllers replace each other', () => {
it('has a host property', () => {
const firstCtrl = new UmbContextProviderController(hostElement, 'my-test-context', contextInstance);
const secondCtrl = new UmbContextProviderController(hostElement, 'my-test-context', new MyClass());
const secondCtrl = new UmbContextProviderController(hostElement, 'my-test-context', new UmbTestContext());
expect(hostElement.hasController(firstCtrl)).to.be.false;
expect(hostElement.hasController(secondCtrl)).to.be.true;

View File

@@ -2,9 +2,10 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { UmbFolderRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository';
export class UmbDeleteFolderEntityAction<
T extends { deleteFolder(unique: string): Promise<void>; requestTreeItems(uniques: Array<string>): any }
T extends UmbItemRepository<any> & UmbFolderRepository
> extends UmbEntityActionBase<T> {
#modalContext?: UmbModalContext;
@@ -19,7 +20,7 @@ export class UmbDeleteFolderEntityAction<
async execute() {
if (!this.repository || !this.#modalContext) return;
const { data } = await this.repository.requestTreeItems([this.unique]);
const { data } = await this.repository.requestItems([this.unique]);
if (data) {
const item = data[0];

View File

@@ -2,9 +2,10 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { UmbDetailRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository';
export class UmbDeleteEntityAction<
T extends { delete(unique: string): Promise<void>; requestTreeItems(uniques: Array<string>): any }
T extends UmbDetailRepository & UmbItemRepository<any>
> extends UmbEntityActionBase<T> {
#modalContext?: UmbModalContext;
@@ -19,7 +20,7 @@ export class UmbDeleteEntityAction<
async execute() {
if (!this.repository || !this.#modalContext) return;
const { data } = await this.repository.requestTreeItems([this.unique]);
const { data } = await this.repository.requestItems([this.unique]);
if (data) {
const item = data[0];

View File

@@ -1,6 +1,8 @@
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
// TODO: investigate what we need to finish the generic move action. We would need to open a picker, which requires a modal token,
// maybe we can use kinds to make a specific manifest to the move action.
export class UmbMoveEntityAction<T extends { move(): Promise<void> }> extends UmbEntityActionBase<T> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);

View File

@@ -2,9 +2,10 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { UmbItemRepository } from '@umbraco-cms/backoffice/repository';
export class UmbTrashEntityAction<
T extends { trash(unique: Array<string>): Promise<void>; requestTreeItems(uniques: Array<string>): any }
T extends UmbItemRepository<any> & { trash(unique: Array<string>): Promise<void> }
> extends UmbEntityActionBase<T> {
#modalContext?: UmbModalContext;
@@ -19,7 +20,7 @@ export class UmbTrashEntityAction<
async execute() {
if (!this.repository) return;
const { data } = await this.repository.requestTreeItems([this.unique]);
const { data } = await this.repository.requestItems([this.unique]);
if (data) {
const item = data[0];

View File

@@ -6,8 +6,24 @@ export interface ManifestCollectionView extends ManifestElement, ManifestWithCon
}
export interface MetaCollectionView {
/**
* The friendly name of the collection view
*/
label: string;
/**
* An icon to represent the collection view
*
* @examples [
* "umb:box",
* "umb:grid"
* ]
*/
icon: string;
/**
* The URL pathname for this collection view that can be deep linked to by sharing the url
*/
pathName: string;
}

View File

@@ -6,10 +6,34 @@ export interface ManifestDashboard extends ManifestElement, ManifestWithConditio
}
export interface MetaDashboard {
/**
* This is the URL path for the dashboard which is used for navigating or deep linking directly to the dashboard
* https://yoursite.com/section/settings/dashboard/my-dashboard-path
*
* @example my-dashboard-path
* @examples [
* "my-dashboard-path"
* ]
*/
pathname: string;
/**
* The displayed name (label) for the tab of the dashboard
*/
label?: string;
}
export interface ConditionsDashboard {
/**
* An array of section aliases that the dashboard should be available in
*
* @uniqueItems true
* @minItems 1
* @items.examples [
* "Umb.Section.Content",
* "Umb.Section.Settings"
* ]
*
*/
sections: string[];
}

View File

@@ -1,5 +1,9 @@
import type { ManifestElement } from './models';
/**
* An action to perform on an entity
* For example for content you may wish to create a new document etc
*/
export interface ManifestEntityAction extends ManifestElement {
type: 'entityAction';
meta: MetaEntityAction;
@@ -7,9 +11,38 @@ export interface ManifestEntityAction extends ManifestElement {
}
export interface MetaEntityAction {
/**
* An icon to represent the action to be performed
*
* @examples [
* "umb:box",
* "umb:grid"
* ]
*/
icon?: string;
/**
* The friendly name of the action to perform
*
* @examples [
* "Create",
* "Create Content Template"
* ]
*/
label: string;
/**
* @TJS-ignore
*/
api: any; // create interface
/**
* The alias for the repsoitory of the entity type this action is for
* such as 'Umb.Repository.Documents'
* @examples [
* "Umb.Repository.Documents"
* ]
*/
repositoryAlias: string;
}

View File

@@ -1,13 +1,33 @@
import type { ManifestElement, ManifestWithConditions } from './models';
/**
* An action to perform on multiple entities
* For example for content you may wish to move one or more documents in bulk
*/
export interface ManifestEntityBulkAction extends ManifestElement, ManifestWithConditions<ConditionsEntityBulkAction> {
type: 'entityBulkAction';
meta: MetaEntityBulkAction;
}
export interface MetaEntityBulkAction {
/**
* A friendly label for the action
*/
label: string;
/**
* @TJS-ignore
*/
api: any; // create interface
/**
* The alias for the repsoitory of the entity type this action is for
* such as 'Umb.Repository.Documents'
*
* @examples [
* "Umb.Repository.Documents"
* ]
*/
repositoryAlias: string;
}

View File

@@ -1,10 +1,15 @@
import type { ManifestElement } from './models';
/**
* Header apps are displayed in the top right corner of the backoffice
* The two provided header apps are the search and the user menu
*/
export interface ManifestHeaderApp extends ManifestElement {
type: 'headerApp';
//meta: MetaHeaderApp;
}
// TODO: Warren these don't seem to be used anywhere
export interface MetaHeaderApp {
pathname: string;
label: string;

View File

@@ -25,7 +25,7 @@ import type { ManifestWorkspaceView } from './workspace-view.models';
import type { ManifestWorkspaceViewCollection } from './workspace-view-collection.models';
import type { ManifestRepository } from './repository.models';
import type { ManifestModal } from './modal.models';
import type { ManifestStore, ManifestTreeStore } from './store.models';
import type { ManifestStore, ManifestTreeStore, ManifestItemStore } from './store.models';
import type { ClassConstructor } from '@umbraco-cms/backoffice/models';
export * from './collection-view.models';
@@ -92,6 +92,7 @@ export type ManifestTypes =
| ManifestModal
| ManifestStore
| ManifestTreeStore
| ManifestItemStore
| ManifestBase;
export type ManifestStandardTypes = ManifestTypes['type'];
@@ -104,10 +105,31 @@ export type SpecificManifestTypeOrManifestBase<T extends keyof ManifestTypeMap |
T extends keyof ManifestTypeMap ? ManifestTypeMap[T] : ManifestBase;
export interface ManifestBase {
/**
* The type of extension such as dashboard etc...
*/
type: string;
/**
* The alias of the extension, ensure it is unique
*/
alias: string;
kind?: any; // I had to add the optional kind property set to undefined. To make the ManifestTypes recognize the Manifest Kind types. Notice that Kinds has to Omit the kind property when extending.
/**
* The kind of the extension, used to group extensions together
*
* @examples ["button"]
*/
kind?: unknown; // I had to add the optional kind property set to undefined. To make the ManifestTypes recognize the Manifest Kind types. Notice that Kinds has to Omit the kind property when extending.
/**
* The friendly name of the extension
*/
name: string;
/**
* Extensions such as dashboards are ordered by weight with lower numbers being first in the list
*/
weight?: number;
}
@@ -120,17 +142,39 @@ export interface ManifestKind {
}
export interface ManifestWithConditions<ConditionsType> {
/**
* Set the conditions for when the extension should be loaded
*/
conditions: ConditionsType;
}
export interface ManifestWithLoader<LoaderReturnType> extends ManifestBase {
/**
* @TJS-ignore
*/
loader?: () => Promise<LoaderReturnType>;
}
/**
* The type of extension such as dashboard etc...
*/
export interface ManifestClass<T = unknown> extends ManifestWithLoader<object> {
//type: ManifestStandardTypes;
/**
* The file location of the javascript file to load
* @TJS-required
*/
js?: string;
/**
* @TJS-ignore
*/
className?: string;
/**
* @TJS-ignore
*/
class?: ClassConstructor<T>;
//loader?: () => Promise<object | HTMLElement>;
}
@@ -141,10 +185,26 @@ export interface ManifestClassWithClassConstructor extends ManifestClass {
export interface ManifestElement extends ManifestWithLoader<object | HTMLElement> {
//type: ManifestStandardTypes;
/**
* The file location of the javascript file to load
*
* @TJS-require
*/
js?: string;
/**
* The HTML web component name to use such as 'my-dashboard'
* Note it is NOT <my-dashboard></my-dashboard> but just the name
*/
elementName?: string;
//loader?: () => Promise<object | HTMLElement>;
meta?: any;
/**
* This contains properties specific to the type of extension
*/
meta?: unknown;
}
export interface ManifestWithView extends ManifestElement {
@@ -158,22 +218,29 @@ export interface MetaManifestWithView {
}
export interface ManifestElementWithElementName extends ManifestElement {
/**
* The HTML web component name to use such as 'my-dashboard'
* Note it is NOT <my-dashboard></my-dashboard> but just the name
*/
elementName: string;
}
// TODO: Remove Custom as it has no purpose currently:
/*
export interface ManifestCustom extends ManifestBase {
type: 'custom';
meta?: unknown;
}
*/
export interface ManifestWithMeta extends ManifestBase {
/**
* This contains properties specific to the type of extension
*/
meta: unknown;
}
/**
* This type of extension gives full control and will simply load the specified JS file
* You could have custom logic to decide which extensions to load/register by using extensionRegistry
*/
export interface ManifestEntrypoint extends ManifestBase {
type: 'entrypoint';
/**
* The file location of the javascript file to load in the backoffice
*/
js: string;
}

View File

@@ -1,5 +1,5 @@
import type { ManifestClass } from './models';
import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store';
import { UmbItemStore, UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store';
export interface ManifestStore extends ManifestClass<UmbStoreBase> {
type: 'store';
@@ -8,3 +8,7 @@ export interface ManifestStore extends ManifestClass<UmbStoreBase> {
export interface ManifestTreeStore extends ManifestClass<UmbTreeStore> {
type: 'treeStore';
}
export interface ManifestItemStore extends ManifestClass<UmbItemStore> {
type: 'itemStore';
}

View File

@@ -1,7 +1,17 @@
import type { ManifestWithLoader } from './models';
// TODO: make or find type for JS Module with default export: Would be nice to support css file directly.
/**
* Theme manifest for styling the backoffice of Umbraco such as dark, high contrast etc
*/
export interface ManifestTheme extends ManifestWithLoader<string> {
type: 'theme';
/**
* File location of the CSS file of the theme
*
* @examples ["themes/dark.theme.css"]
*/
css?: string;
}

View File

@@ -0,0 +1,28 @@
import type { ManifestTypes } from './models';
/**
* Umbraco package manifest JSON
*/
export class UmbracoPackage {
/**
* @title The name of the Umbraco package
*/
name?: string;
/**
* @title The version of the Umbraco package in the style of semver
* @examples ["0.1.0"]
*/
version?: string;
/**
* @title Decides if the package sends telemetry data for collection
* @default true
*/
allowTelemetry?: boolean;
/**
* @title An array of Umbraco package manifest types that will be installed
*/
extensions?: ManifestTypes[];
}

View File

@@ -1,6 +1,5 @@
import type { InterfaceColor, InterfaceLook } from '@umbraco-ui/uui-base/lib/types/index';
import type { ManifestElement } from './models';
import { UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace';
import type { ClassConstructor } from '@umbraco-cms/backoffice/models';
export interface ManifestWorkspaceAction extends ManifestElement {
@@ -13,7 +12,7 @@ export interface MetaWorkspaceAction {
label?: string; //TODO: Use or implement additional label-key
look?: InterfaceLook;
color?: InterfaceColor;
api: ClassConstructor<UmbWorkspaceAction>;
api: ClassConstructor<any>;
}
export interface ConditionsWorkspaceAction {

View File

@@ -0,0 +1,18 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDataTypePickerModalData {
selection?: Array<string>;
multiple?: boolean;
}
export interface UmbDataTypePickerModalResult {
selection: Array<string>;
}
export const UMB_DATA_TYPE_PICKER_MODAL = new UmbModalToken<UmbDataTypePickerModalData, UmbDataTypePickerModalResult>(
'Umb.Modal.DataTypePicker',
{
type: 'sidebar',
size: 'small',
}
);

View File

@@ -1,8 +1,8 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbIconPickerModalData {
multiple: boolean;
selection: string[];
color: string | undefined;
icon: string | undefined;
}
export interface UmbIconPickerModalResult {

View File

@@ -27,3 +27,4 @@ export * from './user-group-picker-modal.token';
export * from './user-picker-modal.token';
export * from './code-editor-modal.token';
export * from './folder-modal.token';
export * from './data-type-picker-modal.token';

View File

@@ -5,7 +5,7 @@
* @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return.
* @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different.
* @description - Creates a RxJS Observable from RxJS Subject.
* @example <caption>Example append new entry for a ArrayState or a part of DeepState/ObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing.</caption>
* @example <caption>Example append new entry for a ArrayState or a part of UmbDeepState/UmbObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing.</caption>
* const entry = {id: 'myKey', value: 'myValue'};
* const newDataSet = appendToFrozenArray(mySubject.getValue(), entry, x => x.id === id);
* mySubject.next(newDataSet);

View File

@@ -1,11 +1,11 @@
import { expect } from '@open-wc/testing';
import { ArrayState } from './array-state';
import { UmbArrayState } from './array-state';
describe('ArrayState', () => {
type ObjectType = { key: string; another: string };
type ArrayType = ObjectType[];
let subject: ArrayState<ObjectType>;
let subject: UmbArrayState<ObjectType>;
let initialData: ArrayType;
beforeEach(() => {
@@ -14,7 +14,7 @@ describe('ArrayState', () => {
{ key: '2', another: 'myValue2' },
{ key: '3', another: 'myValue3' },
];
subject = new ArrayState(initialData, (x) => x.key);
subject = new UmbArrayState(initialData, (x) => x.key);
});
it('replays latests, no matter the amount of subscriptions.', (done) => {

View File

@@ -1,17 +1,17 @@
import { DeepState } from './deep-state';
import { UmbDeepState } from './deep-state';
import { partialUpdateFrozenArray } from './partial-update-frozen-array.function';
import { pushToUniqueArray } from './push-to-unique-array.function';
/**
* @export
* @class ArrayState
* @extends {DeepState<T>}
* @class UmbArrayState
* @extends {UmbDeepState<T>}
* @description - A RxJS BehaviorSubject which deepFreezes the object-data to ensure its not manipulated from any implementations.
* Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content.
*
* The ArrayState provides methods to append data when the data is an Object.
*/
export class ArrayState<T> extends DeepState<T[]> {
export class UmbArrayState<T> extends UmbDeepState<T[]> {
#getUnique?: (entry: T) => unknown;
#sortMethod?: (a: T, b: T) => number;
@@ -29,7 +29,7 @@ export class ArrayState<T> extends DeepState<T[]> {
* { key: 1, value: 'foo'},
* { key: 2, value: 'bar'}
* ];
* const myState = new ArrayState(data, (x) => x.key);
* const myState = new UmbArrayState(data, (x) => x.key);
* myState.sortBy((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
*/
sortBy(sortMethod?: (a: T, b: T) => number) {
@@ -48,14 +48,14 @@ export class ArrayState<T> extends DeepState<T[]> {
/**
* @method remove
* @param {unknown[]} uniques - The unique values to remove.
* @return {ArrayState<T>} Reference to it self.
* @return {UmbArrayState<T>} Reference to it self.
* @description - Remove some new data of this Subject.
* @example <caption>Example remove entry with id '1' and '2'</caption>
* const data = [
* { id: 1, value: 'foo'},
* { id: 2, value: 'bar'}
* ];
* const myState = new ArrayState(data, (x) => x.id);
* const myState = new UmbArrayState(data, (x) => x.id);
* myState.remove([1, 2]);
*/
remove(uniques: unknown[]) {
@@ -77,14 +77,14 @@ export class ArrayState<T> extends DeepState<T[]> {
/**
* @method removeOne
* @param {unknown} unique - The unique value to remove.
* @return {ArrayState<T>} Reference to it self.
* @return {UmbArrayState<T>} Reference to it self.
* @description - Remove some new data of this Subject.
* @example <caption>Example remove entry with id '1'</caption>
* const data = [
* { id: 1, value: 'foo'},
* { id: 2, value: 'bar'}
* ];
* const myState = new ArrayState(data, (x) => x.id);
* const myState = new UmbArrayState(data, (x) => x.id);
* myState.removeOne(1);
*/
removeOne(unique: unknown) {
@@ -104,7 +104,7 @@ export class ArrayState<T> extends DeepState<T[]> {
/**
* @method filter
* @param {unknown} filterMethod - The unique value to remove.
* @return {ArrayState<T>} Reference to it self.
* @return {UmbArrayState<T>} Reference to it self.
* @description - Remove some new data of this Subject.
* @example <caption>Example remove entry with key '1'</caption>
* const data = [
@@ -112,7 +112,7 @@ export class ArrayState<T> extends DeepState<T[]> {
* { key: 2, value: 'bar'},
* { key: 3, value: 'poo'}
* ];
* const myState = new ArrayState(data, (x) => x.key);
* const myState = new UmbArrayState(data, (x) => x.key);
* myState.filter((entry) => entry.key !== 1);
*
* Result:
@@ -130,14 +130,14 @@ export class ArrayState<T> extends DeepState<T[]> {
/**
* @method appendOne
* @param {T} entry - new data to be added in this Subject.
* @return {ArrayState<T>} Reference to it self.
* @return {UmbArrayState<T>} Reference to it self.
* @description - Append some new data to this Subject.
* @example <caption>Example append some data.</caption>
* const data = [
* { key: 1, value: 'foo'},
* { key: 2, value: 'bar'}
* ];
* const myState = new ArrayState(data);
* const myState = new UmbArrayState(data);
* myState.append({ key: 1, value: 'replaced-foo'});
*/
appendOne(entry: T) {
@@ -154,14 +154,14 @@ export class ArrayState<T> extends DeepState<T[]> {
/**
* @method append
* @param {T[]} entries - A array of new data to be added in this Subject.
* @return {ArrayState<T>} Reference to it self.
* @return {UmbArrayState<T>} Reference to it self.
* @description - Append some new data to this Subject, if it compares to existing data it will replace it.
* @example <caption>Example append some data.</caption>
* const data = [
* { key: 1, value: 'foo'},
* { key: 2, value: 'bar'}
* ];
* const myState = new ArrayState(data);
* const myState = new UmbArrayState(data);
* myState.append([
* { key: 1, value: 'replaced-foo'},
* { key: 3, value: 'another-bla'}
@@ -184,14 +184,14 @@ export class ArrayState<T> extends DeepState<T[]> {
* @method updateOne
* @param {unknown} unique - Unique value to find entry to update.
* @param {Partial<T>} entry - new data to be added in this Subject.
* @return {ArrayState<T>} Reference to it self.
* @return {UmbArrayState<T>} Reference to it self.
* @description - Update a item with some new data, requires the ArrayState to be constructed with a getUnique method.
* @example <caption>Example append some data.</caption>
* const data = [
* { key: 1, value: 'foo'},
* { key: 2, value: 'bar'}
* ];
* const myState = new ArrayState(data, (x) => x.key);
* const myState = new UmbArrayState(data, (x) => x.key);
* myState.updateOne(2, {value: 'updated-bar'});
*/
updateOne(unique: unknown, entry: Partial<T>) {

View File

@@ -2,11 +2,11 @@ import { BehaviorSubject } from 'rxjs';
/**
* @export
* @class BasicState
* @class UmbBasicState
* @extends {BehaviorSubject<T>}
* @description - A RxJS BehaviorSubject this Subject ensures the data is unique, not updating any Observes unless there is an actual change of the value.
*/
export class BasicState<T> extends BehaviorSubject<T> {
export class UmbBasicState<T> extends BehaviorSubject<T> {
constructor(initialData: T) {
super(initialData);
}

View File

@@ -1,12 +1,12 @@
import { BasicState } from './basic-state';
import { UmbBasicState } from './basic-state';
/**
* @export
* @class BooleanState
* @class UmbBooleanState
* @extends {BehaviorSubject<T>}
* @description - A RxJS BehaviorSubject this Subject ensures the data is unique, not updating any Observes unless there is an actual change of the value.
*/
export class BooleanState<T> extends BasicState<T | boolean> {
export class UmbBooleanState<T> extends UmbBasicState<T | boolean> {
constructor(initialData: T | boolean) {
super(initialData);
}

View File

@@ -1,16 +1,16 @@
import { BehaviorSubject } from 'rxjs';
interface ClassStateData {
equal(otherClass: ClassStateData): boolean;
interface UmbClassStateData {
equal(otherClass: UmbClassStateData): boolean;
}
/**
* @export
* @class ClassState
* @class UmbClassState
* @extends {BehaviorSubject<T>}
* @description - A RxJS BehaviorSubject which can hold class instance which has a equal method to compare in coming instances for changes.
*/
export class ClassState<T extends ClassStateData | undefined | null> extends BehaviorSubject<T> {
export class UmbClassState<T extends UmbClassStateData | undefined | null> extends BehaviorSubject<T> {
constructor(initialData: T) {
super(initialData);
}

View File

@@ -1,30 +1,27 @@
import { expect } from '@open-wc/testing';
import { DeepState } from './deep-state';
import { UmbDeepState } from './deep-state';
describe('DeepState', () => {
describe('UmbDeepState', () => {
type ObjectType = { key: string; another: string };
type ObjectType = {key: string, another: string};
let subject: DeepState<ObjectType>;
let subject: UmbDeepState<ObjectType>;
let initialData: ObjectType;
beforeEach(() => {
initialData = {key: 'some', another: 'myValue'};
subject = new DeepState(initialData);
initialData = { key: 'some', another: 'myValue' };
subject = new UmbDeepState(initialData);
});
it('getValue gives the initial data', () => {
expect(subject.value.another).to.be.equal(initialData.another);
});
it('update via next', () => {
subject.next({key: 'some', another: 'myNewValue'});
subject.next({ key: 'some', another: 'myNewValue' });
expect(subject.value.another).to.be.equal('myNewValue');
});
it('replays latests, no matter the amount of subscriptions.', (done) => {
const observer = subject.asObservable();
observer.subscribe((value) => {
expect(value).to.be.equal(initialData);
@@ -33,28 +30,24 @@ describe('DeepState', () => {
expect(value).to.be.equal(initialData);
done();
});
});
it('use gObservablePart, updates on its specific change.', (done) => {
let amountOfCallbacks = 0;
const subObserver = subject.getObservablePart(data => data.another);
const subObserver = subject.getObservablePart((data) => data.another);
subObserver.subscribe((value) => {
amountOfCallbacks++;
if(amountOfCallbacks === 1) {
if (amountOfCallbacks === 1) {
expect(value).to.be.equal('myValue');
}
if(amountOfCallbacks === 2) {
if (amountOfCallbacks === 2) {
expect(value).to.be.equal('myNewValue');
done();
}
});
subject.next({key: 'change_this_first_should_not_trigger_update', another: 'myValue'});
subject.next({key: 'some', another: 'myNewValue'});
subject.next({ key: 'change_this_first_should_not_trigger_update', another: 'myValue' });
subject.next({ key: 'some', another: 'myNewValue' });
});
});

View File

@@ -7,12 +7,12 @@ import { naiveObjectComparison } from './naive-object-comparison';
/**
* @export
* @class DeepState
* @class UmbDeepState
* @extends {BehaviorSubject<T>}
* @description - A RxJS BehaviorSubject which deepFreezes the data to ensure its not manipulated from any implementations.
* Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content.
*/
export class DeepState<T> extends BehaviorSubject<T> {
export class UmbDeepState<T> extends BehaviorSubject<T> {
constructor(initialData: T) {
super(deepFreeze(initialData));
}

View File

@@ -0,0 +1,13 @@
/**
* @export
* @method filterFrozenArray
* @param {Array<T>} data - RxJS Subject to use for this Observable.
* @param {(entry: T) => boolean} filterMethod - Method to filter the array.
* @description - Creates a RxJS Observable from RxJS Subject.
* @example <caption>Example remove an entry of a ArrayState or a part of DeepState/ObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing.</caption>
* const newDataSet = filterFrozenArray(mySubject.getValue(), x => x.id !== "myKey");
* mySubject.next(newDataSet);
*/
export function filterFrozenArray<T>(data: T[], filterMethod: (entry: T) => boolean): T[] {
return [...data].filter((x) => filterMethod(x));
}

View File

@@ -10,5 +10,6 @@ export * from './array-state';
export * from './object-state';
export * from './create-observable-part.function';
export * from './append-to-frozen-array.function';
export * from './filter-frozen-array.function';
export * from './partial-update-frozen-array.function';
export * from './mapping-function';

View File

@@ -1,12 +1,12 @@
import { BasicState } from './basic-state';
import { UmbBasicState } from './basic-state';
/**
* @export
* @class NumberState
* @class UmbNumberState
* @extends {BehaviorSubject<T>}
* @description - A RxJS BehaviorSubject this Subject ensures the data is unique, not updating any Observes unless there is an actual change of the value.
*/
export class NumberState<T> extends BasicState<T | number> {
export class UmbNumberState<T> extends UmbBasicState<T | number> {
constructor(initialData: T | number) {
super(initialData);
}

View File

@@ -1,21 +1,18 @@
import { expect } from '@open-wc/testing';
import { ObjectState } from './object-state';
import { UmbObjectState } from './object-state';
describe('ObjectState', () => {
describe('UmbObjectState', () => {
type ObjectType = { key: string; another: string };
type ObjectType = {key: string, another: string};
let subject: ObjectState<ObjectType>;
let subject: UmbObjectState<ObjectType>;
let initialData: ObjectType;
beforeEach(() => {
initialData = {key: 'some', another: 'myValue'};
subject = new ObjectState(initialData);
initialData = { key: 'some', another: 'myValue' };
subject = new UmbObjectState(initialData);
});
it('replays latests, no matter the amount of subscriptions.', (done) => {
const observer = subject.asObservable();
observer.subscribe((value) => {
expect(value).to.be.equal(initialData);
@@ -24,28 +21,24 @@ describe('ObjectState', () => {
expect(value).to.be.equal(initialData);
done();
});
});
it('use getObservablePart, updates on its specific change.', (done) => {
let amountOfCallbacks = 0;
const subObserver = subject.getObservablePart(data => data.another);
const subObserver = subject.getObservablePart((data) => data.another);
subObserver.subscribe((value) => {
amountOfCallbacks++;
if(amountOfCallbacks === 1) {
if (amountOfCallbacks === 1) {
expect(value).to.be.equal('myValue');
}
if(amountOfCallbacks === 2) {
if (amountOfCallbacks === 2) {
expect(value).to.be.equal('myNewValue');
done();
}
});
subject.update({key: 'change_this_first_should_not_trigger_update'});
subject.update({another: 'myNewValue'});
subject.update({ key: 'change_this_first_should_not_trigger_update' });
subject.update({ another: 'myNewValue' });
});
});

View File

@@ -1,23 +1,23 @@
import { DeepState } from './deep-state';
import { UmbDeepState } from './deep-state';
/**
* @export
* @class ObjectState
* @extends {DeepState<T>}
* @class UmbObjectState
* @extends {UmbDeepState<T>}
* @description - A RxJS BehaviorSubject which deepFreezes the object-data to ensure its not manipulated from any implementations.
* Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content.
*
* The ObjectState provides methods to append data when the data is an Object.
* The UmbObjectState provides methods to append data when the data is an Object.
*/
export class ObjectState<T> extends DeepState<T> {
export class UmbObjectState<T> extends UmbDeepState<T> {
/**
* @method update
* @param {Partial<T>} partialData - A object containing some of the data to update in this Subject.
* @description - Append some new data to this Object.
* @return {ObjectState<T>} Reference to it self.
* @return {UmbObjectState<T>} Reference to it self.
* @example <caption>Example append some data.</caption>
* const data = {key: 'myKey', value: 'myInitialValue'};
* const myState = new ObjectState(data);
* const myState = new UmbObjectState(data);
* myState.update({value: 'myNewValue'});
*/
update(partialData: Partial<T>) {

View File

@@ -5,7 +5,7 @@
* @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return.
* @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different.
* @description - Creates a RxJS Observable from RxJS Subject.
* @example <caption>Example append new entry for a ArrayState or a part of DeepState/ObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing.</caption>
* @example <caption>Example append new entry for a ArrayState or a part of UmbDeepState/UmbObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing.</caption>
* const partialEntry = {value: 'myValue'};
* const newDataSet = partialUpdateFrozenArray(mySubject.getValue(), partialEntry, x => x.key === 'myKey');
* mySubject.next(newDataSet);

View File

@@ -1,12 +1,12 @@
import { BasicState } from './basic-state';
import { UmbBasicState } from './basic-state';
/**
* @export
* @class StringState
* @extends {BasicState<T>}
* @class UmbStringState
* @extends {UmbBasicState<T>}
* @description - A RxJS BehaviorSubject this Subject ensures the data is unique, not updating any Observes unless there is an actual change of the value.
*/
export class StringState<T> extends BasicState<T | string> {
export class UmbStringState<T> extends UmbBasicState<T | string> {
constructor(initialData: T | string) {
super(initialData);
}

View File

@@ -0,0 +1 @@
export * from './picker-input.context';

View File

@@ -0,0 +1,136 @@
import { UmbItemRepository } from '@umbraco-cms/backoffice/repository';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
import { UmbArrayState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import { createExtensionClass, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extensions-api';
import {
UMB_CONFIRM_MODAL,
UMB_MODAL_CONTEXT_TOKEN,
UmbModalContext,
UmbModalToken,
} from '@umbraco-cms/backoffice/modal';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { ItemResponseModelBaseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/events';
export class UmbPickerInputContext<ItemType extends ItemResponseModelBaseModel> {
host: UmbControllerHostElement;
modalAlias: string | UmbModalToken;
repository?: UmbItemRepository<ItemType>;
#getUnique: (entry: ItemType) => string | undefined;
public modalContext?: UmbModalContext;
#selection = new UmbArrayState<string>([]);
selection = this.#selection.asObservable();
#selectedItems = new UmbArrayState<ItemType>([]);
selectedItems = this.#selectedItems.asObservable();
#selectedItemsObserver?: UmbObserverController<ItemType[]>;
max = Infinity;
min = 0;
/* TODO: find a better way to have a getUniqueMethod. If we want to support trees/items of different types,
then it need to be bound to the type and can't be a generic method we pass in. */
constructor(
host: UmbControllerHostElement,
repositoryAlias: string,
modalAlias: string | UmbModalToken,
getUniqueMethod?: (entry: ItemType) => string | undefined
) {
this.host = host;
this.modalAlias = modalAlias;
this.#getUnique = getUniqueMethod || ((entry) => entry.id || '');
// TODO: unsure a method can't be called before everything is initialized
new UmbObserverController(
this.host,
// TODO: this code is reused in multiple places, so it should be extracted to a function
umbExtensionsRegistry.getByTypeAndAlias('repository', repositoryAlias),
async (repositoryManifest) => {
if (!repositoryManifest) return;
try {
const result = await createExtensionClass<UmbItemRepository<ItemType>>(repositoryManifest, [this.host]);
this.repository = result;
} catch (error) {
throw new Error('Could not create repository with alias: ' + repositoryAlias + '');
}
}
);
new UmbContextConsumerController(this.host, UMB_MODAL_CONTEXT_TOKEN, (instance) => {
this.modalContext = instance;
});
}
getSelection() {
return this.#selection.value;
}
setSelection(selection: string[]) {
this.#selection.next(selection);
}
// TODO: revisit this method. How do we best pass picker data?
// If modalAlias is a ModalToken, then via TS, we should get the correct type for pickerData. Otherwise fallback to unknown.
openPicker(pickerData?: any) {
if (!this.modalContext) throw new Error('Modal context is not initialized');
const modalHandler = this.modalContext.open(this.modalAlias, {
multiple: this.max === 1 ? false : true,
selection: [...this.getSelection()],
...pickerData,
});
modalHandler?.onSubmit().then(({ selection }: any) => {
this.setSelection(selection);
this.host.dispatchEvent(new UmbChangeEvent());
// TODO: we only want to request items that are not already in the selectedItems array
this.#requestItems();
});
}
async requestRemoveItem(unique: string) {
if (!this.repository) throw new Error('Repository is not initialized');
// TODO: id won't always be available on the model, so we need to get the unique property from somewhere. Maybe the repository?
const item = this.#selectedItems.value.find((item) => this.#getUnique(item) === unique);
if (!item) throw new Error('Could not find item with unique: ' + unique);
const modalHandler = this.modalContext?.open(UMB_CONFIRM_MODAL, {
color: 'danger',
headline: `Remove ${item.name}?`,
content: 'Are you sure you want to remove this item',
confirmLabel: 'Remove',
});
await modalHandler?.onSubmit();
this.#removeItem(unique);
}
async #requestItems() {
if (!this.repository) throw new Error('Repository is not initialized');
if (this.#selectedItemsObserver) this.#selectedItemsObserver.destroy();
const { asObservable } = await this.repository.requestItems(this.getSelection());
if (asObservable) {
this.#selectedItemsObserver = new UmbObserverController(this.host, asObservable(), (data) =>
this.#selectedItems.next(data)
);
}
}
#removeItem(unique: string) {
const newSelection = this.getSelection().filter((value) => value !== unique);
this.setSelection(newSelection);
// remove items items from selectedItems array
// TODO: id won't always be available on the model, so we need to get the unique property from somewhere. Maybe the repository?
const newSelectedItems = this.#selectedItems.value.filter((item) => this.#getUnique(item) !== unique);
this.#selectedItems.next(newSelectedItems);
}
}

View File

@@ -0,0 +1,5 @@
import { UmbRepositoryResponse } from './detail-repository.interface';
export interface UmbCopyRepository {
copy(unique: string, targetUnique: string): Promise<UmbRepositoryResponse<string>>;
}

View File

@@ -0,0 +1,5 @@
import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface UmbCopyDataSource {
copy(unique: string, targetUnique: string): Promise<DataSourceResponse<string>>;
}

View File

@@ -1,6 +1,9 @@
import type { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
export interface DataSourceResponse<T = undefined> {
export interface DataSourceResponse<T = undefined> extends UmbDataSourceErrorResponse {
data?: T;
}
export interface UmbDataSourceErrorResponse {
error?: ProblemDetailsModel;
}

View File

@@ -2,3 +2,6 @@ export * from './data-source-response.interface';
export * from './data-source.interface';
export * from './folder-data-source.interface';
export * from './tree-data-source.interface';
export * from './item-data-source.interface';
export * from './move-data-source.interface';
export * from './copy-data-source.interface';

View File

@@ -0,0 +1,5 @@
import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface UmbItemDataSource<ItemType> {
getItems(unique: Array<string>): Promise<DataSourceResponse<Array<ItemType>>>;
}

View File

@@ -0,0 +1,5 @@
import type { UmbDataSourceErrorResponse } from '@umbraco-cms/backoffice/repository';
export interface UmbMoveDataSource {
move(unique: string, targetUnique: string): Promise<UmbDataSourceErrorResponse>;
}

View File

@@ -3,5 +3,7 @@ import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository';
export interface UmbTreeDataSource<PagedItemsType = any, ItemsType = any> {
getRootItems(): Promise<DataSourceResponse<PagedItemsType>>;
getChildrenOf(parentUnique: string): Promise<DataSourceResponse<PagedItemsType>>;
// TODO: remove this when all repositories are migrated to the new items interface
getItems(unique: Array<string>): Promise<DataSourceResponse<Array<ItemsType>>>;
}

View File

@@ -2,3 +2,6 @@ export * from './data-source';
export * from './detail-repository.interface';
export * from './tree-repository.interface';
export * from './folder-repository.interface';
export * from './item-repository.interface';
export * from './move-repository.interface';
export * from './copy-repository.interface';

View File

@@ -0,0 +1,11 @@
import type { Observable } from 'rxjs';
import { ItemResponseModelBaseModel, ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
export interface UmbItemRepository<ItemType extends ItemResponseModelBaseModel> {
requestItems: (uniques: string[]) => Promise<{
data?: Array<ItemType> | undefined;
error?: ProblemDetailsModel | undefined;
asObservable?: () => Observable<Array<ItemType>>;
}>;
items: (uniques: string[]) => Promise<Observable<Array<ItemType>>>;
}

View File

@@ -0,0 +1,5 @@
import { UmbRepositoryErrorResponse } from './detail-repository.interface';
export interface UmbMoveRepository {
move(unique: string, targetUnique: string): Promise<UmbRepositoryErrorResponse>;
}

View File

@@ -17,7 +17,9 @@ export interface UmbTreeRepository<ItemType = any, PagedItemType = UmbPagedData<
error: ProblemDetailsModel | undefined;
asObservable?: () => Observable<ItemType[]>;
}>;
requestTreeItems: (uniques: string[]) => Promise<{
// TODO: remove this when all repositories are migrated to the new interface items interface
requestItemsLegacy?: (uniques: string[]) => Promise<{
data: Array<ItemType> | undefined;
error: ProblemDetailsModel | undefined;
asObservable?: () => Observable<ItemType[]>;
@@ -25,5 +27,7 @@ export interface UmbTreeRepository<ItemType = any, PagedItemType = UmbPagedData<
rootTreeItems: () => Promise<Observable<ItemType[]>>;
treeItemsOf: (parentUnique: string | null) => Promise<Observable<ItemType[]>>;
treeItems: (uniques: string[]) => Promise<Observable<ItemType[]>>;
// TODO: remove this when all repositories are migrated to the new items interface
itemsLegacy?: (uniques: string[]) => Promise<Observable<ItemType[]>>;
}

View File

@@ -1,4 +1,4 @@
export type * from 'router-slot/model';
export * from 'router-slot/model';
export * from 'router-slot/util';
export * from './route-location.interface';
export * from './route.context';

View File

@@ -0,0 +1 @@
export * from './sorter.controller';

View File

@@ -0,0 +1,685 @@
(function () {
'use strict';
function isWithinRect(x, y, rect, modifier) {
return (
x > rect.left - modifier && x < rect.right + modifier && y > rect.top - modifier && y < rect.bottom + modifier
);
}
function getParentScrollElement(el, includeSelf) {
// skip to window
if (!el || !el.getBoundingClientRect) return null;
var elem = el;
var gotSelf = false;
while (elem) {
// we don't need to get elem css if it isn't even overflowing in the first place (performance)
if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) {
var elemCSS = getComputedStyle(elem);
if (
(elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll')) ||
(elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll'))
) {
if (!elem.getBoundingClientRect || elem === document.body) return null;
if (gotSelf || includeSelf) return elem;
gotSelf = true;
}
}
if (elem.parentNode === document) {
return null;
} else if (elem.parentNode instanceof DocumentFragment) {
elem = elem.parentNode.host;
} else {
elem = elem.parentNode;
}
}
return null;
}
const DefaultConfig = {
compareElementToModel: (el, modelEntry) => modelEntry.contentUdi === el.dataset.elementUdi,
querySelectModelToElement: (container, modelEntry) =>
container.querySelector(`[data-element-udi='${modelEntry.contentUdi}']`),
identifier: 'UmbBlockGridSorter',
containerSelector: 'ol', // To find container and to connect with others.
ignorerSelector: 'a, img, iframe',
itemSelector: 'li',
placeholderClass: 'umb-drag-placeholder',
};
function UmbBlockGridSorter() {
function link(scope, element) {
let observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addedNode) {
if (addedNode.matches && addedNode.matches(scope.config.itemSelector)) {
setupItem(addedNode);
}
});
mutation.removedNodes.forEach(function (removedNode) {
if (removedNode.matches && removedNode.matches(scope.config.itemSelector)) {
destroyItem(removedNode);
}
});
});
});
let vm = {};
const config = { ...DefaultConfig, ...scope.config };
vm.identifier = config.identifier;
vm.ownerVM = config.ownerVM || null;
let scrollElement = null;
let containerEl = config.containerSelector ? element[0].closest(config.containerSelector) : element[0];
if (!containerEl) {
console.error('Could not initialize umb block grid sorter.', element[0]);
return;
}
function init() {
containerEl['umbBlockGridSorter:vm'] = () => {
return vm;
};
containerEl.addEventListener('dragover', preventDragOver);
observer.observe(containerEl, { childList: true, subtree: false });
}
init();
function preventDragOver(e) {
e.preventDefault();
}
function setupItem(element) {
setupIgnorerElements(element);
element.draggable = true;
element.addEventListener('dragstart', handleDragStart);
}
function destroyItem(element) {
destroyIgnorerElements(element);
element.removeEventListener('dragstart', handleDragStart);
}
function setupIgnorerElements(element) {
config.ignorerSelector.split(',').forEach(function (criteria) {
element.querySelectorAll(criteria.trim()).forEach(setupPreventEvent);
});
}
function destroyIgnorerElements(element) {
config.ignorerSelector.split(',').forEach(function (criteria) {
element.querySelectorAll(criteria.trim()).forEach(destroyPreventEvent);
});
}
function setupPreventEvent(element) {
element.draggable = false;
}
function destroyPreventEvent(element) {
element.removeAttribute('draggable');
}
let currentContainerElement = containerEl;
let currentContainerVM = vm;
let rqaId = null;
let currentItem = null;
let currentElement = null;
let currentDragElement = null;
let currentDragRect = null;
let dragX = 0;
let dragY = 0;
function handleDragStart(event) {
if (currentElement) {
handleDragEnd();
}
event.stopPropagation();
event.dataTransfer.effectAllowed = 'move'; // copyMove when we enhance the drag with clipboard data.
event.dataTransfer.dropEffect = 'none'; // visual feedback when dropped.
if (!scrollElement) {
scrollElement = getParentScrollElement(containerEl, true);
}
const element = event.target.closest(config.itemSelector);
currentElement = element;
currentDragElement = config.draggableSelector
? currentElement.querySelector(config.draggableSelector)
: currentElement;
currentDragRect = currentDragElement.getBoundingClientRect();
currentItem = vm.getItemOfElement(currentElement);
if (!currentItem) {
console.error('Could not find item related to this element.');
return;
}
currentElement.style.transform = 'translateZ(0)'; // Solves problem with FireFox and ShadowDom in the drag-image.
if (config.dataTransferResolver) {
config.dataTransferResolver(event.dataTransfer, currentItem);
}
if (config.onStart) {
config.onStart({ item: currentItem, element: currentElement });
}
window.addEventListener('dragover', handleDragMove);
window.addEventListener('dragend', handleDragEnd);
// We must wait one frame before changing the look of the block.
rqaId = requestAnimationFrame(() => {
// It should be okay to use the same refId, as the move does not or is okay not to happen on first frame/drag-move.
rqaId = null;
currentElement.style.transform = '';
currentElement.classList.add(config.placeholderClass);
});
}
function handleDragEnd() {
if (!currentElement) {
return;
}
window.removeEventListener('dragover', handleDragMove);
window.removeEventListener('dragend', handleDragEnd);
currentElement.style.transform = '';
currentElement.classList.remove(config.placeholderClass);
stopAutoScroll();
removeAllowIndication();
if (currentContainerVM.sync(currentElement, vm) === false) {
// Sync could not succeed, might be because item is not allowed here.
currentContainerVM = vm;
if (config.onContainerChange) {
config.onContainerChange({
item: currentItem,
element: currentElement,
ownerVM: currentContainerVM.ownerVM,
});
}
// Lets move the Element back to where it came from:
const movingItemIndex = scope.model.indexOf(currentItem);
if (movingItemIndex < scope.model.length - 1) {
const afterItem = scope.model[movingItemIndex + 1];
const afterEl = config.querySelectModelToElement(containerEl, afterItem);
containerEl.insertBefore(currentElement, afterEl);
} else {
containerEl.appendChild(currentElement);
}
}
if (config.onEnd) {
config.onEnd({ item: currentItem, element: currentElement });
}
if (rqaId) {
cancelAnimationFrame(rqaId);
}
currentContainerElement = containerEl;
currentContainerVM = vm;
rqaId = null;
currentItem = null;
currentElement = null;
currentDragElement = null;
currentDragRect = null;
dragX = 0;
dragY = 0;
}
function handleDragMove(event) {
if (!currentElement) {
return;
}
const clientX = (event.touches ? event.touches[0] : event).clientX;
const clientY = (event.touches ? event.touches[1] : event).clientY;
if (clientX !== 0 && clientY !== 0) {
if (dragX === clientX && dragY === clientY) {
return;
}
dragX = clientX;
dragY = clientY;
handleAutoScroll(dragX, dragY);
currentDragRect = currentDragElement.getBoundingClientRect();
const insideCurrentRect = isWithinRect(dragX, dragY, currentDragRect, 0);
if (!insideCurrentRect) {
if (rqaId === null) {
rqaId = requestAnimationFrame(moveCurrentElement);
}
}
}
}
function moveCurrentElement() {
rqaId = null;
if (!currentElement) {
return;
}
const currentElementRect = currentElement.getBoundingClientRect();
const insideCurrentRect = isWithinRect(dragX, dragY, currentElementRect);
if (insideCurrentRect) {
return;
}
// If we have a boundarySelector, try it, if we didn't get anything fall back to currentContainerElement.
var currentBoundaryElement =
(config.boundarySelector
? currentContainerElement.closest(config.boundarySelector)
: currentContainerElement) || currentContainerElement;
var currentBoundaryRect = currentBoundaryElement.getBoundingClientRect();
const currentContainerHasItems = currentContainerVM.hasOtherItemsThan(currentItem);
// if empty we will be move likely to accept an item (add 20px to the bounding box)
// If we have items we must be 10 within the container to accept the move.
const offsetEdge = currentContainerHasItems ? -10 : 20;
if (!isWithinRect(dragX, dragY, currentBoundaryRect, offsetEdge)) {
// we are outside the current container boundary, so lets see if there is a parent we can move.
var parentContainer = currentContainerElement.parentNode.closest(config.containerSelector);
if (parentContainer) {
const parentContainerVM = parentContainer['umbBlockGridSorter:vm']();
if (parentContainerVM.identifier === vm.identifier) {
currentContainerElement = parentContainer;
currentContainerVM = parentContainerVM;
if (config.onContainerChange) {
config.onContainerChange({
item: currentItem,
element: currentElement,
ownerVM: currentContainerVM.ownerVM,
});
}
}
}
}
// We want to retrieve the children of the container, every time to ensure we got the right order and index
const orderedContainerElements = Array.from(currentContainerElement.children);
var currentContainerRect = currentContainerElement.getBoundingClientRect();
// gather elements on the same row.
let elementsInSameRow = [];
let placeholderIsInThisRow = false;
for (const el of orderedContainerElements) {
const elRect = el.getBoundingClientRect();
// gather elements on the same row.
if (dragY >= elRect.top && dragY <= elRect.bottom) {
const dragElement = config.draggableSelector ? el.querySelector(config.draggableSelector) : el;
const dragElementRect = dragElement.getBoundingClientRect();
if (el !== currentElement) {
elementsInSameRow.push({ el: el, dragRect: dragElementRect });
} else {
placeholderIsInThisRow = true;
}
}
}
let lastDistance = 99999;
let foundEl = null;
let foundElDragRect = null;
let placeAfter = false;
elementsInSameRow.forEach((sameRow) => {
const centerX = sameRow.dragRect.left + sameRow.dragRect.width * 0.5;
let distance = Math.abs(dragX - centerX);
if (distance < lastDistance) {
foundEl = sameRow.el;
foundElDragRect = sameRow.dragRect;
lastDistance = Math.abs(distance);
placeAfter = dragX > centerX;
}
});
// If we are on top or closest to our self, we should not do anything.
if (foundEl === currentElement) {
return;
}
if (foundEl) {
const isInsideFound = isWithinRect(dragX, dragY, foundElDragRect, 0);
// If we are inside the found element, lets look for sub containers.
// use the itemHasNestedContainersResolver, if not configured fallback to looking for the existence of a container via DOM.
if (
isInsideFound && config.itemHasNestedContainersResolver
? config.itemHasNestedContainersResolver(foundEl)
: foundEl.querySelector(config.containerSelector)
) {
// Find all sub containers:
const subLayouts = foundEl.querySelectorAll(config.containerSelector);
for (const subLayoutEl of subLayouts) {
// Use boundary element or fallback to container element.
var subBoundaryElement =
(config.boundarySelector ? subLayoutEl.closest(config.boundarySelector) : subLayoutEl) || subLayoutEl;
var subBoundaryRect = subBoundaryElement.getBoundingClientRect();
const subContainerHasItems = subLayoutEl.querySelector(
config.itemSelector + ':not(.' + config.placeholderClass + ')'
);
// gather elements on the same row.
const subOffsetEdge = subContainerHasItems ? -10 : 20;
if (isWithinRect(dragX, dragY, subBoundaryRect, subOffsetEdge)) {
var subVm = subLayoutEl['umbBlockGridSorter:vm']();
if (subVm.identifier === vm.identifier) {
currentContainerElement = subLayoutEl;
currentContainerVM = subVm;
if (config.onContainerChange) {
config.onContainerChange({
item: currentItem,
element: currentElement,
ownerVM: currentContainerVM.ownerVM,
});
}
moveCurrentElement();
return;
}
}
}
}
// Indication if drop is good:
if (updateAllowIndication(currentContainerVM, currentItem) === false) {
return;
}
let verticalDirection = scope.config.resolveVerticalDirection
? scope.config.resolveVerticalDirection({
containerElement: currentContainerElement,
containerRect: currentContainerRect,
item: currentItem,
element: currentElement,
elementRect: currentElementRect,
relatedElement: foundEl,
relatedRect: foundElDragRect,
placeholderIsInThisRow: placeholderIsInThisRow,
horizontalPlaceAfter: placeAfter,
})
: true;
if (verticalDirection) {
placeAfter = dragY > foundElDragRect.top + foundElDragRect.height * 0.5;
}
if (verticalDirection) {
let el;
if (placeAfter === false) {
let lastLeft = foundElDragRect.left;
elementsInSameRow.findIndex((x) => {
if (x.dragRect.left < lastLeft) {
lastLeft = x.dragRect.left;
el = x.el;
}
});
} else {
let lastRight = foundElDragRect.right;
elementsInSameRow.findIndex((x) => {
if (x.dragRect.right > lastRight) {
lastRight = x.dragRect.right;
el = x.el;
}
});
}
if (el) {
foundEl = el;
}
}
const foundElIndex = orderedContainerElements.indexOf(foundEl);
const placeAt = placeAfter ? foundElIndex + 1 : foundElIndex;
move(orderedContainerElements, placeAt);
return;
}
// We skipped the above part cause we are above or below container:
// Indication if drop is good:
if (updateAllowIndication(currentContainerVM, currentItem) === false) {
return;
}
if (dragY < currentContainerRect.top) {
move(orderedContainerElements, 0);
} else if (dragY > currentContainerRect.bottom) {
move(orderedContainerElements, -1);
}
}
function move(orderedContainerElements, newElIndex) {
newElIndex = newElIndex === -1 ? orderedContainerElements.length : newElIndex;
const placeBeforeElement = orderedContainerElements[newElIndex];
if (placeBeforeElement) {
// We do not need to move this, if the element to be placed before is it self.
if (placeBeforeElement !== currentElement) {
currentContainerElement.insertBefore(currentElement, placeBeforeElement);
}
} else {
currentContainerElement.appendChild(currentElement);
}
if (config.onChange) {
config.onChange({ element: currentElement, item: currentItem, ownerVM: currentContainerVM.ownerVM });
}
}
/** Removes an element from container and returns its items-data entry */
vm.getItemOfElement = function (element) {
if (!element) {
return null;
}
return scope.model.find((entry) => config.compareElementToModel(element, entry));
};
vm.removeItem = function (item) {
if (!item) {
return null;
}
const oldIndex = scope.model.indexOf(item);
if (oldIndex !== -1) {
return scope.model.splice(oldIndex, 1)[0];
}
return null;
};
vm.hasOtherItemsThan = function (item) {
return scope.model.filter((x) => x !== item).length > 0;
};
vm.sync = function (element, fromVm) {
const movingItem = fromVm.getItemOfElement(element);
if (!movingItem) {
console.error('Could not find item of sync item');
return false;
}
if (vm.notifyRequestDrop({ item: movingItem }) === false) {
return false;
}
if (fromVm.removeItem(movingItem) === null) {
console.error('Sync could not remove item');
return false;
}
/** Find next element, to then find the index of that element in items-data, to use as a safe reference to where the item will go in our items-data.
* This enables the container to contain various other elements and as well having these elements change while sorting is occurring.
*/
// find next valid element (This assumes the next element in DOM is presented in items-data, aka. only moving one item between each sync)
let nextEl;
let loopEl = element;
while ((loopEl = loopEl.nextElementSibling)) {
if (loopEl.matches && loopEl.matches(config.itemSelector)) {
nextEl = loopEl;
break;
}
}
let newIndex = scope.model.length;
if (nextEl) {
// We had a reference element, we want to get the index of it.
// This is problem if a item is being moved forward?
newIndex = scope.model.findIndex((entry) => config.compareElementToModel(nextEl, entry));
}
scope.model.splice(newIndex, 0, movingItem);
const eventData = { item: movingItem, fromController: fromVm, toController: vm };
if (fromVm !== vm) {
fromVm.notifySync(eventData);
}
vm.notifySync(eventData);
return true;
};
var _lastIndicationContainerVM = null;
function updateAllowIndication(contextVM, item) {
// Remove old indication:
if (_lastIndicationContainerVM !== null && _lastIndicationContainerVM !== contextVM) {
_lastIndicationContainerVM.notifyAllowed();
}
_lastIndicationContainerVM = contextVM;
if (contextVM.notifyRequestDrop({ item: item }) === true) {
contextVM.notifyAllowed();
return true;
}
contextVM.notifyDisallowed(); // This block is not accepted to we will indicate that its not allowed.
return false;
}
function removeAllowIndication() {
// Remove old indication:
if (_lastIndicationContainerVM !== null) {
_lastIndicationContainerVM.notifyAllowed();
}
_lastIndicationContainerVM = null;
}
let autoScrollRAF;
let autoScrollEl;
const autoScrollSensitivity = 50;
const autoScrollSpeed = 16;
let autoScrollX = 0;
let autoScrollY = 0;
function handleAutoScroll(clientX, clientY) {
let scrollRect = null;
if (scrollElement) {
autoScrollEl = scrollElement;
scrollRect = scrollElement.getBoundingClientRect();
} else {
autoScrollEl = document.scrollingElement || document.documentElement;
scrollRect = {
top: 0,
left: 0,
bottom: window.innerHeight,
right: window.innerWidth,
height: window.innerHeight,
width: window.innerWidth,
};
}
const scrollWidth = autoScrollEl.scrollWidth;
const scrollHeight = autoScrollEl.scrollHeight;
const canScrollX = scrollRect.width < scrollWidth;
const canScrollY = scrollRect.height < scrollHeight;
const scrollPosX = autoScrollEl.scrollLeft;
const scrollPosY = autoScrollEl.scrollTop;
cancelAnimationFrame(autoScrollRAF);
if (canScrollX || canScrollY) {
autoScrollX =
(Math.abs(scrollRect.right - clientX) <= autoScrollSensitivity &&
scrollPosX + scrollRect.width < scrollWidth) -
(Math.abs(scrollRect.left - clientX) <= autoScrollSensitivity && !!scrollPosX);
autoScrollY =
(Math.abs(scrollRect.bottom - clientY) <= autoScrollSensitivity &&
scrollPosY + scrollRect.height < scrollHeight) -
(Math.abs(scrollRect.top - clientY) <= autoScrollSensitivity && !!scrollPosY);
autoScrollRAF = requestAnimationFrame(performAutoScroll);
}
}
function performAutoScroll() {
autoScrollEl.scrollLeft += autoScrollX * autoScrollSpeed;
autoScrollEl.scrollTop += autoScrollY * autoScrollSpeed;
autoScrollRAF = requestAnimationFrame(performAutoScroll);
}
function stopAutoScroll() {
cancelAnimationFrame(autoScrollRAF);
autoScrollRAF = null;
}
vm.notifySync = function (data) {
if (config.onSync) {
config.onSync(data);
}
};
vm.notifyDisallowed = function () {
if (config.onDisallowed) {
config.onDisallowed();
}
};
vm.notifyAllowed = function () {
if (config.onAllowed) {
config.onAllowed();
}
};
vm.notifyRequestDrop = function (data) {
if (config.onRequestDrop) {
return config.onRequestDrop(data);
}
return true;
};
scope.$on('$destroy', () => {
if (currentElement) {
handleDragEnd();
}
_lastIndicationContainerVM = null;
containerEl['umbBlockGridSorter:vm'] = null;
containerEl.removeEventListener('dragover', preventDragOver);
observer.disconnect();
observer = null;
containerEl = null;
scrollElement = null;
vm = null;
});
}
var directive = {
restrict: 'A',
scope: {
config: '=umbBlockGridSorter',
model: '=umbBlockGridSorterModel',
},
link: link,
};
return directive;
}
angular.module('umbraco.directives').directive('umbBlockGridSorter', UmbBlockGridSorter);
})();

View File

@@ -0,0 +1,60 @@
import { expect, fixture, html } from '@open-wc/testing';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { customElement } from 'lit/decorators.js';
import { UmbSorterConfig, UmbSorterController } from './sorter.controller';
type SortEntryType = {
id: string;
value: string;
};
const sorterConfig: UmbSorterConfig<SortEntryType> = {
compareElementToModel: (element: HTMLElement, model: SortEntryType) => {
return element.getAttribute('id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: SortEntryType) => {
return container.querySelector('data-sort-entry-id[' + modelEntry.id + ']');
},
identifier: 'test-sorter',
itemSelector: 'li',
};
const model: Array<SortEntryType> = [
{
id: '0',
value: 'Entry 0',
},
{
id: '1',
value: 'Entry 1',
},
{
id: '2',
value: 'Entry 2',
},
];
@customElement('test-my-sorter-controller')
class UmbTestSorterControllerElement extends UmbLitElement {
public sorter;
constructor() {
super();
this.sorter = new UmbSorterController(this, sorterConfig);
this.sorter.setModel(model);
}
}
describe('UmbContextConsumer', () => {
let hostElement: UmbTestSorterControllerElement;
beforeEach(async () => {
hostElement = await fixture(html` <test-my-sorter-controller></test-my-sorter-controller> `);
});
// TODO: Testing ideas:
// - Test that the model is updated correctly?
// - Test that the DOM is updated correctly?
// - Use the controller to sort the DOM and test that the model is updated correctly...
});

View File

@@ -0,0 +1,838 @@
import { UmbControllerInterface, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
const autoScrollSensitivity = 50;
const autoScrollSpeed = 16;
function isWithinRect(x: number, y: number, rect: DOMRect, modifier = 0) {
return x > rect.left - modifier && x < rect.right + modifier && y > rect.top - modifier && y < rect.bottom + modifier;
}
function getParentScrollElement(el: Element, includeSelf: boolean) {
if (!el || !el.getBoundingClientRect) return null;
let elem = el;
let gotSelf = false;
while (elem) {
// we don't need to get elem css if it isn't even overflowing in the first place (performance)
if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) {
const elemCSS = getComputedStyle(elem);
if (
(elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll')) ||
(elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll'))
) {
if (!elem.getBoundingClientRect || elem === document.body) return null;
if (gotSelf || includeSelf) return elem;
gotSelf = true;
}
}
if (elem.parentNode === document) {
return null;
} else if (elem.parentNode instanceof ShadowRoot) {
elem = elem.parentNode.host;
} else {
elem = elem.parentNode as Element;
}
}
return null;
}
function preventDragOver(e: Event) {
e.preventDefault();
}
function setupIgnorerElements(element: HTMLElement, ignorerSelectors: string) {
ignorerSelectors.split(',').forEach(function (criteria) {
element.querySelectorAll(criteria.trim()).forEach(setupPreventEvent);
});
}
function destroyIgnorerElements(element: HTMLElement, ignorerSelectors: string) {
ignorerSelectors.split(',').forEach(function (criteria: string) {
element.querySelectorAll(criteria.trim()).forEach(destroyPreventEvent);
});
}
function setupPreventEvent(element: Element) {
(element as HTMLElement).draggable = false;
}
function destroyPreventEvent(element: Element) {
element.removeAttribute('draggable');
}
type INTERNAL_UmbSorterConfig<T> = {
compareElementToModel: (el: HTMLElement, modelEntry: T) => boolean;
querySelectModelToElement: (container: HTMLElement, modelEntry: T) => HTMLElement | null;
identifier: string;
itemSelector: string;
disabledItemSelector?: string;
containerSelector: string;
ignorerSelector: string;
placeholderClass: string;
draggableSelector?: string;
boundarySelector?: string;
dataTransferResolver?: (dataTransfer: DataTransfer | null, currentItem: T) => void;
onStart?: (argument: { item: T; element: HTMLElement }) => void;
onChange?: (argument: { item: T; element: HTMLElement }) => void;
onContainerChange?: (argument: { item: T; element: HTMLElement }) => void;
onEnd?: (argument: { item: T; element: HTMLElement }) => void;
onSync?: (argument: {
item: T;
fromController: UmbSorterController<T>;
toController: UmbSorterController<T>;
}) => void;
itemHasNestedContainersResolver?: (element: HTMLElement) => boolean;
onDisallowed?: () => void;
onAllowed?: () => void;
onRequestDrop?: (argument: { item: T }) => boolean | void;
resolveVerticalDirection?: (argument: {
containerElement: Element;
containerRect: DOMRect;
item: T;
element: HTMLElement;
elementRect: DOMRect;
relatedElement: HTMLElement;
relatedRect: DOMRect;
placeholderIsInThisRow: boolean;
horizontalPlaceAfter: boolean;
}) => void;
performItemInsert?: (argument: { item: T; newIndex: number }) => Promise<boolean> | boolean;
performItemRemove?: (argument: { item: T }) => Promise<boolean> | boolean;
};
// External type with some properties optional, as they have defaults:
export type UmbSorterConfig<T> = Omit<
INTERNAL_UmbSorterConfig<T>,
'placeholderClass' | 'ignorerSelector' | 'containerSelector'
> &
Partial<Pick<INTERNAL_UmbSorterConfig<T>, 'placeholderClass' | 'ignorerSelector' | 'containerSelector'>>;
/**
* @export
* @class UmbSorterController
* @implements {UmbControllerInterface}
* @description This controller can make user able to sort items.
*/
export class UmbSorterController<T> implements UmbControllerInterface {
#host;
#config: INTERNAL_UmbSorterConfig<T>;
#observer;
#model: Array<T> = [];
#rqaId?: number;
#containerElement!: HTMLElement;
#currentContainerVM = this;
#currentContainerElement: Element | null = null;
#scrollElement?: Element | null;
#currentElement?: HTMLElement;
#currentDragElement?: Element;
#currentDragRect?: DOMRect;
#currentItem?: T | null;
#dragX = 0;
#dragY = 0;
private _lastIndicationContainerVM: UmbSorterController<T> | null = null;
public get unique() {
return this.#config.identifier;
}
constructor(host: UmbControllerHostElement, config: UmbSorterConfig<T>) {
this.#host = host;
// Set defaults:
config.ignorerSelector ??= 'a, img, iframe';
config.placeholderClass ??= '--umb-sorter-placeholder';
this.#config = config as INTERNAL_UmbSorterConfig<T>;
host.addController(this);
//this.#currentContainerElement = host;
this.#observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((addedNode) => {
if ((addedNode as HTMLElement).matches && (addedNode as HTMLElement).matches(this.#config.itemSelector)) {
this.setupItem(addedNode as HTMLElement);
}
});
mutation.removedNodes.forEach((removedNode) => {
if ((removedNode as HTMLElement).matches && (removedNode as HTMLElement).matches(this.#config.itemSelector)) {
this.destroyItem(removedNode as HTMLElement);
}
});
});
});
}
setModel(model: Array<T>) {
if (this.#model) {
// TODO: Some updates might need to be done, as the modal is about to changed? Do make the changes after setting the model?..
}
this.#model = model;
}
hostConnected() {
requestAnimationFrame(this._onFirstRender);
}
private _onFirstRender = () => {
const containerEl =
(this.#config.containerSelector
? this.#host.shadowRoot!.querySelector(this.#config.containerSelector)
: this.#host) ?? this.#host;
if (this.#currentContainerElement === this.#containerElement) {
this.#currentContainerElement = containerEl;
}
this.#containerElement = containerEl as HTMLElement;
this.#containerElement.addEventListener('dragover', preventDragOver);
(this.#containerElement as any)['__umbBlockGridSorterController'] = () => {
return this;
};
console.log('containerEl', this.#containerElement.shadowRoot ?? this.#containerElement);
// TODO: Clean up??
this.#observer.disconnect();
const containerElement = this.#containerElement.shadowRoot ?? this.#containerElement;
containerElement.querySelectorAll(this.#config.itemSelector).forEach((child) => {
if (child.matches && child.matches(this.#config.itemSelector)) {
this.setupItem(child as HTMLElement);
}
});
this.#observer.observe(containerElement, {
childList: true,
subtree: false,
});
};
hostDisconnected() {
// TODO: Clean up??
this.#observer.disconnect();
(this.#containerElement as any)['__umbBlockGridSorterController'] = undefined;
this.#containerElement.removeEventListener('dragover', preventDragOver);
(this.#containerElement as any) = undefined;
}
setupItem(element: HTMLElement) {
if (this.#config.ignorerSelector) {
setupIgnorerElements(element, this.#config.ignorerSelector);
}
if (!this.#config.disabledItemSelector || !element.matches(this.#config.disabledItemSelector)) {
element.draggable = true;
element.addEventListener('dragstart', this.handleDragStart);
}
}
destroyItem(element: HTMLElement) {
if (this.#config.ignorerSelector) {
destroyIgnorerElements(element, this.#config.ignorerSelector);
}
element.removeEventListener('dragstart', this.handleDragStart);
}
handleDragStart = (event: DragEvent) => {
if (this.#currentElement) {
this.handleDragEnd();
}
event.stopPropagation();
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'; // copyMove when we enhance the drag with clipboard data.
event.dataTransfer.dropEffect = 'none'; // visual feedback when dropped.
}
if (!this.#scrollElement) {
this.#scrollElement = getParentScrollElement(this.#containerElement, true);
}
const element = (event.target as HTMLElement).closest(this.#config.itemSelector);
if (!element) return;
this.#currentDragElement = this.#config.draggableSelector
? element.querySelector(this.#config.draggableSelector) ?? undefined
: element;
if (!this.#currentDragElement) {
throw new Error(
'Could not find drag element, query was made with the `draggableSelector` of "' +
this.#config.draggableSelector +
'"'
);
return;
}
this.#currentElement = element as HTMLElement;
this.#currentDragRect = this.#currentDragElement.getBoundingClientRect();
this.#currentItem = this.getItemOfElement(this.#currentElement);
if (!this.#currentItem) {
console.error('Could not find item related to this element.');
return;
}
this.#currentElement.style.transform = 'translateZ(0)'; // Solves problem with FireFox and ShadowDom in the drag-image.
if (this.#config.dataTransferResolver) {
this.#config.dataTransferResolver(event.dataTransfer, this.#currentItem);
}
if (this.#config.onStart) {
this.#config.onStart({ item: this.#currentItem!, element: this.#currentElement });
}
window.addEventListener('dragover', this.handleDragMove);
window.addEventListener('dragend', this.handleDragEnd);
// We must wait one frame before changing the look of the block.
this.#rqaId = requestAnimationFrame(() => {
// It should be okay to use the same rqaId, as the move does not or is okay not to happen on first frame/drag-move.
this.#rqaId = undefined;
if (this.#currentElement) {
this.#currentElement.style.transform = '';
this.#currentElement.classList.add(this.#config.placeholderClass);
}
});
};
handleDragEnd = async () => {
if (!this.#currentElement || !this.#currentItem) {
return;
}
window.removeEventListener('dragover', this.handleDragMove);
window.removeEventListener('dragend', this.handleDragEnd);
this.#currentElement.style.transform = '';
this.#currentElement.classList.remove(this.#config.placeholderClass);
this.stopAutoScroll();
this.removeAllowIndication();
if ((await this.#currentContainerVM.sync(this.#currentElement, this)) === false) {
// Sync could not succeed, might be because item is not allowed here.
this.#currentContainerVM = this;
if (this.#config.onContainerChange) {
this.#config.onContainerChange({
item: this.#currentItem,
element: this.#currentElement,
//ownerVM: this.#currentContainerVM.ownerVM,
});
}
// Lets move the Element back to where it came from:
const movingItemIndex = this.#model.indexOf(this.#currentItem);
if (movingItemIndex < this.#model.length - 1) {
const afterItem = this.#model[movingItemIndex + 1];
const afterEl = this.#config.querySelectModelToElement(this.#containerElement, afterItem);
if (afterEl) {
this.#containerElement.insertBefore(this.#currentElement, afterEl);
} else {
this.#containerElement.appendChild(this.#currentElement);
}
} else {
this.#containerElement.appendChild(this.#currentElement);
}
}
if (this.#config.onEnd) {
this.#config.onEnd({ item: this.#currentItem, element: this.#currentElement });
}
if (this.#rqaId) {
cancelAnimationFrame(this.#rqaId);
}
this.#currentContainerElement = this.#containerElement;
this.#currentContainerVM = this;
this.#rqaId = undefined;
this.#currentItem = undefined;
this.#currentElement = undefined;
this.#currentDragElement = undefined;
this.#currentDragRect = undefined;
this.#dragX = 0;
this.#dragY = 0;
};
handleDragMove = (event: DragEvent) => {
if (!this.#currentElement) {
return;
}
const clientX = (event as unknown as TouchEvent).touches
? (event as unknown as TouchEvent).touches[0].clientX
: event.clientX;
const clientY = (event as unknown as TouchEvent).touches
? (event as unknown as TouchEvent).touches[0].clientY
: event.clientY;
if (clientX !== 0 && clientY !== 0) {
if (this.#dragX === clientX && this.#dragY === clientY) {
return;
}
this.#dragX = clientX;
this.#dragY = clientY;
this.handleAutoScroll(this.#dragX, this.#dragY);
this.#currentDragRect = this.#currentDragElement!.getBoundingClientRect();
const insideCurrentRect = isWithinRect(this.#dragX, this.#dragY, this.#currentDragRect);
if (!insideCurrentRect) {
if (this.#rqaId === undefined) {
this.#rqaId = requestAnimationFrame(this.moveCurrentElement);
}
}
}
};
moveCurrentElement = () => {
this.#rqaId = undefined;
if (!this.#currentElement || !this.#currentContainerElement || !this.#currentItem) {
return;
}
const currentElementRect = this.#currentElement.getBoundingClientRect();
const insideCurrentRect = isWithinRect(this.#dragX, this.#dragY, currentElementRect);
if (insideCurrentRect) {
return;
}
// If we have a boundarySelector, try it, if we didn't get anything fall back to currentContainerElement.
const currentBoundaryElement =
(this.#config.boundarySelector
? this.#currentContainerElement.closest(this.#config.boundarySelector)
: this.#currentContainerElement) ?? this.#currentContainerElement;
const currentBoundaryRect = currentBoundaryElement.getBoundingClientRect();
const currentContainerHasItems = this.#currentContainerVM.hasOtherItemsThan(this.#currentItem!);
// if empty we will be move likely to accept an item (add 20px to the bounding box)
// If we have items we must be 10 within the container to accept the move.
const offsetEdge = currentContainerHasItems ? -10 : 20;
if (!isWithinRect(this.#dragX, this.#dragY, currentBoundaryRect, offsetEdge)) {
// we are outside the current container boundary, so lets see if there is a parent we can move.
const parentNode = this.#currentContainerElement.parentNode;
if (parentNode) {
// TODO: support multiple parent shadowDOMs?
const parentContainer = this.#config.containerSelector
? (parentNode as HTMLElement).closest(this.#config.containerSelector)
: null;
if (parentContainer) {
const parentContainerVM = (parentContainer as any)['__umbBlockGridSorterController']();
if (parentContainerVM.unique === this.unique) {
this.#currentContainerElement = parentContainer as Element;
this.#currentContainerVM = parentContainerVM;
if (this.#config.onContainerChange) {
this.#config.onContainerChange({
item: this.#currentItem,
element: this.#currentElement,
//ownerVM: this.#currentContainerVM.ownerVM,
});
}
}
}
}
}
// We want to retrieve the children of the container, every time to ensure we got the right order and index
const orderedContainerElements = Array.from(
this.#currentContainerElement.shadowRoot
? this.#currentContainerElement.shadowRoot.querySelectorAll(this.#config.itemSelector)
: this.#currentContainerElement.querySelectorAll(this.#config.itemSelector)
);
const currentContainerRect = this.#currentContainerElement.getBoundingClientRect();
// gather elements on the same row.
const elementsInSameRow = [];
let placeholderIsInThisRow = false;
for (const el of orderedContainerElements) {
const elRect = el.getBoundingClientRect();
// gather elements on the same row.
if (this.#dragY >= elRect.top && this.#dragY <= elRect.bottom) {
const dragElement = this.#config.draggableSelector ? el.querySelector(this.#config.draggableSelector) : el;
if (dragElement) {
const dragElementRect = dragElement.getBoundingClientRect();
if (el !== this.#currentElement) {
elementsInSameRow.push({ el: el, dragRect: dragElementRect });
} else {
placeholderIsInThisRow = true;
}
}
}
}
let lastDistance = 99999;
let foundEl: Element | null = null;
let foundElDragRect!: DOMRect;
let placeAfter = false;
elementsInSameRow.forEach((sameRow) => {
const centerX = sameRow.dragRect.left + sameRow.dragRect.width * 0.5;
const distance = Math.abs(this.#dragX - centerX);
if (distance < lastDistance) {
foundEl = sameRow.el;
foundElDragRect = sameRow.dragRect;
lastDistance = distance;
placeAfter = this.#dragX > centerX;
}
});
if (foundEl) {
// If we are on top or closest to our self, we should not do anything.
if (foundEl === this.#currentElement) {
return;
}
const isInsideFound = isWithinRect(this.#dragX, this.#dragY, foundElDragRect, 0);
// If we are inside the found element, lets look for sub containers.
// use the itemHasNestedContainersResolver, if not configured fallback to looking for the existence of a container via DOM.
// TODO: Ability to look into shadowDOMs for sub containers?
if (
isInsideFound && this.#config.itemHasNestedContainersResolver
? this.#config.itemHasNestedContainersResolver(foundEl)
: (foundEl as HTMLElement).querySelector(this.#config.containerSelector)
) {
// Find all sub containers:
const subLayouts = (foundEl as HTMLElement).querySelectorAll(this.#config.containerSelector);
for (const subLayoutEl of subLayouts) {
// Use boundary element or fallback to container element.
const subBoundaryElement =
(this.#config.boundarySelector ? subLayoutEl.closest(this.#config.boundarySelector) : subLayoutEl) ||
subLayoutEl;
const subBoundaryRect = subBoundaryElement.getBoundingClientRect();
const subContainerHasItems = subLayoutEl.querySelector(
this.#config.itemSelector + ':not(.' + this.#config.placeholderClass + ')'
);
// gather elements on the same row.
const subOffsetEdge = subContainerHasItems ? -10 : 20;
if (isWithinRect(this.#dragX, this.#dragY, subBoundaryRect, subOffsetEdge)) {
const subVm = (subLayoutEl as any)['__umbBlockGridSorterController']();
if (subVm.unique === this.unique) {
this.#currentContainerElement = subLayoutEl as HTMLElement;
this.#currentContainerVM = subVm;
if (this.#config.onContainerChange) {
this.#config.onContainerChange({
item: this.#currentItem,
element: this.#currentElement,
//ownerVM: this.#currentContainerVM.ownerVM,
});
}
this.moveCurrentElement();
return;
}
}
}
}
// Indication if drop is good:
if (this.updateAllowIndication(this.#currentContainerVM, this.#currentItem) === false) {
return;
}
const verticalDirection = this.#config.resolveVerticalDirection
? this.#config.resolveVerticalDirection({
containerElement: this.#currentContainerElement,
containerRect: currentContainerRect,
item: this.#currentItem,
element: this.#currentElement,
elementRect: currentElementRect,
relatedElement: foundEl,
relatedRect: foundElDragRect,
placeholderIsInThisRow: placeholderIsInThisRow,
horizontalPlaceAfter: placeAfter,
})
: true;
if (verticalDirection) {
placeAfter = this.#dragY > foundElDragRect.top + foundElDragRect.height * 0.5;
}
if (verticalDirection) {
let el;
if (placeAfter === false) {
let lastLeft = foundElDragRect.left;
elementsInSameRow.findIndex((x) => {
if (x.dragRect.left < lastLeft) {
lastLeft = x.dragRect.left;
el = x.el;
}
});
} else {
let lastRight = foundElDragRect.right;
elementsInSameRow.findIndex((x) => {
if (x.dragRect.right > lastRight) {
lastRight = x.dragRect.right;
el = x.el;
}
});
}
if (el) {
foundEl = el;
}
}
const foundElIndex = orderedContainerElements.indexOf(foundEl);
const placeAt = placeAfter ? foundElIndex + 1 : foundElIndex;
this.move(orderedContainerElements, placeAt);
return;
}
// We skipped the above part cause we are above or below container:
// Indication if drop is good:
if (this.updateAllowIndication(this.#currentContainerVM, this.#currentItem) === false) {
return;
}
if (this.#dragY < currentContainerRect.top) {
this.move(orderedContainerElements, 0);
} else if (this.#dragY > currentContainerRect.bottom) {
this.move(orderedContainerElements, -1);
}
};
move(orderedContainerElements: Array<Element>, newElIndex: number) {
if (!this.#currentElement || !this.#currentItem || !this.#currentContainerElement) return;
newElIndex = newElIndex === -1 ? orderedContainerElements.length : newElIndex;
const containerElement = this.#currentContainerElement.shadowRoot ?? this.#currentContainerElement;
const placeBeforeElement = orderedContainerElements[newElIndex];
if (placeBeforeElement) {
// We do not need to move this, if the element to be placed before is it self.
if (placeBeforeElement !== this.#currentElement) {
containerElement.insertBefore(this.#currentElement, placeBeforeElement);
}
} else {
containerElement.appendChild(this.#currentElement);
}
if (this.#config.onChange) {
this.#config.onChange({
element: this.#currentElement,
item: this.#currentItem,
//ownerVM: this.#currentContainerVM.ownerVM
});
}
}
/** Management methods: */
public getItemOfElement(element: HTMLElement) {
if (!element) {
return null;
}
return this.#model.find((entry: T) => this.#config.compareElementToModel(element, entry));
}
public async removeItem(item: T) {
if (!item) {
return null;
}
if (this.#config.performItemRemove) {
return await this.#config.performItemRemove({ item });
} else {
const oldIndex = this.#model.indexOf(item);
if (oldIndex !== -1) {
return this.#model.splice(oldIndex, 1)[0];
}
}
return null;
}
hasOtherItemsThan(item: T) {
return this.#model.filter((x) => x !== item).length > 0;
}
public async sync(element: HTMLElement, fromVm: UmbSorterController<T>) {
const movingItem = fromVm.getItemOfElement(element);
if (!movingItem) {
console.error('Could not find item of sync item');
return false;
}
if (this.notifyRequestDrop({ item: movingItem }) === false) {
return false;
}
if (fromVm.removeItem(movingItem) === null) {
console.error('Sync could not remove item');
return false;
}
/** Find next element, to then find the index of that element in items-data, to use as a safe reference to where the item will go in our items-data.
* This enables the container to contain various other elements and as well having these elements change while sorting is occurring.
*/
// find next valid element (This assumes the next element in DOM is presented in items-data, aka. only moving one item between each sync)
let nextEl: Element | null = null;
let loopEl: Element | null = element;
while ((loopEl = loopEl?.nextElementSibling)) {
if (loopEl.matches && loopEl.matches(this.#config.itemSelector)) {
nextEl = loopEl;
break;
}
}
let newIndex = this.#model.length;
const movingItemIndex = this.#model.indexOf(movingItem);
if (movingItemIndex !== -1 && movingItemIndex <= movingItemIndex) {
newIndex--;
}
if (nextEl) {
// We had a reference element, we want to get the index of it.
// This is might a problem if a item is being moved forward? (was also like this in the AngularJS version...)
newIndex = this.#model.findIndex((entry) => this.#config.compareElementToModel(nextEl! as HTMLElement, entry));
}
if (this.#config.performItemInsert) {
const result = await this.#config.performItemInsert({ item: movingItem, newIndex });
if (result === false) {
return false;
}
} else {
this.#model.splice(newIndex, 0, movingItem);
}
const eventData = { item: movingItem, fromController: fromVm, toController: this };
if (fromVm !== this) {
fromVm.notifySync(eventData);
}
this.notifySync(eventData);
return true;
}
updateAllowIndication(contextVM: UmbSorterController<T>, item: T) {
// Remove old indication:
if (this._lastIndicationContainerVM !== null && this._lastIndicationContainerVM !== contextVM) {
this._lastIndicationContainerVM.notifyAllowed();
}
this._lastIndicationContainerVM = contextVM;
if (contextVM.notifyRequestDrop({ item: item }) === true) {
contextVM.notifyAllowed();
return true;
}
contextVM.notifyDisallowed(); // This block is not accepted to we will indicate that its not allowed.
return false;
}
removeAllowIndication() {
// Remove old indication:
if (this._lastIndicationContainerVM !== null) {
this._lastIndicationContainerVM.notifyAllowed();
}
this._lastIndicationContainerVM = null;
}
// TODO: Move auto scroll into its own class?
#autoScrollRAF: number | null = null;
#autoScrollEl?: Element;
private autoScrollX = 0;
private autoScrollY = 0;
private handleAutoScroll(clientX: number, clientY: number) {
let scrollRect: DOMRect | null = null;
if (this.#scrollElement) {
this.#autoScrollEl = this.#scrollElement;
scrollRect = this.#autoScrollEl.getBoundingClientRect();
} else {
this.#autoScrollEl = document.scrollingElement || document.documentElement;
scrollRect = {
top: 0,
left: 0,
bottom: window.innerHeight,
right: window.innerWidth,
height: window.innerHeight,
width: window.innerWidth,
} as DOMRect;
}
const scrollWidth = this.#autoScrollEl.scrollWidth;
const scrollHeight = this.#autoScrollEl.scrollHeight;
const canScrollX = scrollRect.width < scrollWidth;
const canScrollY = scrollRect.height < scrollHeight;
const scrollPosX = this.#autoScrollEl.scrollLeft;
const scrollPosY = this.#autoScrollEl.scrollTop;
cancelAnimationFrame(this.#autoScrollRAF!);
if (canScrollX || canScrollY) {
this.autoScrollX =
Math.abs(scrollRect.right - clientX) <= autoScrollSensitivity && scrollPosX + scrollRect.width < scrollWidth
? 1
: Math.abs(scrollRect.left - clientX) <= autoScrollSensitivity && !!scrollPosX
? -1
: 0;
this.autoScrollY =
Math.abs(scrollRect.bottom - clientY) <= autoScrollSensitivity && scrollPosY + scrollRect.height < scrollHeight
? 1
: Math.abs(scrollRect.top - clientY) <= autoScrollSensitivity && !!scrollPosY
? -1
: 0;
this.#autoScrollRAF = requestAnimationFrame(this._performAutoScroll);
}
}
private _performAutoScroll() {
this.#autoScrollEl!.scrollLeft += this.autoScrollX * autoScrollSpeed;
this.#autoScrollEl!.scrollTop += this.autoScrollY * autoScrollSpeed;
this.#autoScrollRAF = requestAnimationFrame(this._performAutoScroll);
}
private stopAutoScroll() {
cancelAnimationFrame(this.#autoScrollRAF!);
this.#autoScrollRAF = null;
}
public notifySync(data: any) {
if (this.#config.onSync) {
this.#config.onSync(data);
}
}
public notifyDisallowed() {
if (this.#config.onDisallowed) {
this.#config.onDisallowed();
}
}
public notifyAllowed() {
if (this.#config.onAllowed) {
this.#config.onAllowed();
}
}
public notifyRequestDrop(data: any) {
if (this.#config.onRequestDrop) {
return this.#config.onRequestDrop(data) || false;
}
return true;
}
destroy() {
// Do something when host element is destroyed.
if (this.#currentElement) {
this.handleDragEnd();
}
this._lastIndicationContainerVM = null;
// TODO: Clean up items??
this.#observer.disconnect();
// For auto scroller:
this.#scrollElement = null;
this.#autoScrollEl = undefined;
}
}

View File

@@ -1,49 +1,27 @@
import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { ArrayState, partialUpdateFrozenArray } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
/**
* @export
* @class UmbEntityTreeStore
* @extends {UmbStoreBase}
* @description - General Tree Data Store
* @description - Entity Tree Store
*/
export class UmbEntityTreeStore extends UmbStoreBase implements UmbTreeStore<EntityTreeItemResponseModel> {
#data = new ArrayState<EntityTreeItemResponseModel>([], (x) => x.id);
/**
* Appends items to the store
* @param {Array<EntityTreeItemResponseModel>} items
* @memberof UmbEntityTreeStore
*/
appendItems(items: Array<EntityTreeItemResponseModel>) {
this.#data.append(items);
}
/**
* Updates an item in the store
* @param {string} id
* @param {Partial<EntityTreeItemResponseModel>} data
* @memberof UmbEntityTreeStore
*/
updateItem(id: string, data: Partial<EntityTreeItemResponseModel>) {
this.#data.next(partialUpdateFrozenArray(this.#data.getValue(), data, (entry) => entry.id === id));
}
/**
* Removes an item from the store
* @param {string} id
* @memberof UmbEntityTreeStore
*/
removeItem(id: string) {
this.#data.removeOne(id);
export class UmbEntityTreeStore
extends UmbStoreBase<EntityTreeItemResponseModel>
implements UmbTreeStore<EntityTreeItemResponseModel>
{
constructor(host: UmbControllerHostElement, storeAlias: string) {
super(host, storeAlias, new UmbArrayState<EntityTreeItemResponseModel>([], (x) => x.id));
}
/**
* An observable to observe the root items
* @memberof UmbEntityTreeStore
*/
rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.parentId === null));
rootItems = this._data.getObservablePart((items) => items.filter((item) => item.parentId === null));
/**
* Returns an observable to observe the children of a given parent
@@ -52,7 +30,7 @@ export class UmbEntityTreeStore extends UmbStoreBase implements UmbTreeStore<Ent
* @memberof UmbEntityTreeStore
*/
childrenOf(parentId: string | null) {
return this.#data.getObservablePart((items) => items.filter((item) => item.parentId === parentId));
return this._data.getObservablePart((items) => items.filter((item) => item.parentId === parentId));
}
/**
@@ -62,6 +40,6 @@ export class UmbEntityTreeStore extends UmbStoreBase implements UmbTreeStore<Ent
* @memberof UmbEntityTreeStore
*/
items(ids: Array<string>) {
return this.#data.getObservablePart((items) => items.filter((item) => ids.includes(item.id ?? '')));
return this._data.getObservablePart((items) => items.filter((item) => ids.includes(item.id ?? '')));
}
}

View File

@@ -1,49 +1,27 @@
import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api';
import { ArrayState, partialUpdateFrozenArray } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbStoreBase, UmbTreeStore } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
/**
* @export
* @class UmbFileSystemTreeStore
* @extends {UmbStoreBase}
* @description - General Tree Data Store
* @description - File System Tree Store
*/
export class UmbFileSystemTreeStore extends UmbStoreBase implements UmbTreeStore<FileSystemTreeItemPresentationModel> {
#data = new ArrayState<FileSystemTreeItemPresentationModel>([], (x) => x.path);
/**
* Appends items to the store
* @param {Array<FileSystemTreeItemPresentationModel>} items
* @memberof UmbFileSystemTreeStore
*/
appendItems(items: Array<FileSystemTreeItemPresentationModel>) {
this.#data.append(items);
}
/**
* Updates an item in the store
* @param {string} path
* @param {Partial<FileSystemTreeItemPresentationModel>} data
* @memberof UmbFileSystemTreeStore
*/
updateItem(path: string, data: Partial<FileSystemTreeItemPresentationModel>) {
this.#data.appendOne(data);
}
/**
* Removes an item from the store
* @param {string} path
* @memberof UmbFileSystemTreeStore
*/
removeItem(path: string) {
this.#data.removeOne(path);
export class UmbFileSystemTreeStore
extends UmbStoreBase<FileSystemTreeItemPresentationModel>
implements UmbTreeStore<FileSystemTreeItemPresentationModel>
{
constructor(host: UmbControllerHostElement, storeAlias: string) {
super(host, storeAlias, new UmbArrayState<FileSystemTreeItemPresentationModel>([], (x) => x.path));
}
/**
* An observable to observe the root items
* @memberof UmbFileSystemTreeStore
*/
rootItems = this.#data.getObservablePart((items) => items.filter((item) => item.path?.includes('/') === false));
rootItems = this._data.getObservablePart((items) => items.filter((item) => item.path?.includes('/') === false));
/**
* Returns an observable to observe the children of a given parent
@@ -52,7 +30,7 @@ export class UmbFileSystemTreeStore extends UmbStoreBase implements UmbTreeStore
* @memberof UmbFileSystemTreeStore
*/
childrenOf(parentPath: string | null) {
return this.#data.getObservablePart((items) => items.filter((item) => item.path?.startsWith(parentPath + '/')));
return this._data.getObservablePart((items) => items.filter((item) => item.path?.startsWith(parentPath + '/')));
}
/**
@@ -62,6 +40,6 @@ export class UmbFileSystemTreeStore extends UmbStoreBase implements UmbTreeStore
* @memberof UmbFileSystemTreeStore
*/
items(paths: Array<string>) {
return this.#data.getObservablePart((items) => items.filter((item) => paths.includes(item.path ?? '')));
return this._data.getObservablePart((items) => items.filter((item) => paths.includes(item.path ?? '')));
}
}

View File

@@ -3,3 +3,4 @@ export * from './store-base';
export * from './entity-tree-store';
export * from './file-system-tree.store';
export * from './tree-store.interface';
export * from './item-store.interface';

View File

@@ -0,0 +1,7 @@
import type { Observable } from 'rxjs';
import { ItemResponseModelBaseModel } from '../backend-api';
import { UmbStore } from './store.interface';
export interface UmbItemStore<T extends ItemResponseModelBaseModel = any> extends UmbStore<T> {
items: (uniques: Array<string>) => Observable<Array<T>>;
}

View File

@@ -1,9 +1,48 @@
import { UmbStore } from './store.interface';
import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
// TODO: Make a Store interface?
export class UmbStoreBase {
constructor(protected _host: UmbControllerHostElement, public readonly storeAlias: string) {
export class UmbStoreBase<StoreItemType = any> implements UmbStore<StoreItemType> {
protected _host: UmbControllerHostElement;
protected _data: UmbArrayState<StoreItemType>;
public readonly storeAlias: string;
constructor(_host: UmbControllerHostElement, storeAlias: string, data: UmbArrayState<StoreItemType>) {
this._host = _host;
this.storeAlias = storeAlias;
this._data = data;
new UmbContextProviderController(_host, storeAlias, this);
}
/**
* Appends items to the store
* @param {Array<StoreItemType>} items
* @memberof UmbEntityTreeStore
*/
appendItems(items: Array<StoreItemType>) {
this._data.append(items);
}
/**
* Updates an item in the store
* @param {string} id
* @param {Partial<StoreItemType>} data
* @memberof UmbEntityTreeStore
*/
updateItem(unique: string, data: Partial<StoreItemType>) {
this._data.updateOne(unique, data);
}
/**
* Removes an item from the store
* @param {string} id
* @memberof UmbEntityTreeStore
*/
removeItem(unique: string) {
this._data.removeOne(unique);
}
}

View File

@@ -0,0 +1,5 @@
export interface UmbStore<T> {
appendItems: (items: Array<T>) => void;
updateItem: (unique: string, item: Partial<T>) => void;
removeItem: (unique: string) => void;
}

View File

@@ -1,3 +1,4 @@
// TODO: delete when the last usages are gone
import type { Observable } from 'rxjs';
export interface UmbDataStoreIdentifiers {

View File

@@ -1,12 +1,10 @@
import type { Observable } from 'rxjs';
import { TreeItemPresentationModel } from '../backend-api';
import { UmbStore } from './store.interface';
export interface UmbTreeStore<T extends TreeItemPresentationModel = any> {
appendItems: (items: Array<T>) => void;
updateItem: (unique: string, item: Partial<T>) => void;
removeItem: (unique: string) => void;
export interface UmbTreeStore<T extends TreeItemPresentationModel = any> extends UmbStore<T> {
rootItems: Observable<Array<T>>;
childrenOf: (parentUnique: string | null) => Observable<Array<T>>;
// TODO: remove this one when all repositories are using an item store
items: (uniques: Array<string>) => Observable<Array<T>>;
}

View File

@@ -72,6 +72,7 @@
"storybook": "^7.0.2",
"tiny-glob": "^0.2.9",
"typescript": "^5.0.3",
"typescript-json-schema": "^0.55.0",
"uglify-js": "^3.17.4",
"vite": "^4.2.1",
"vite-plugin-static-copy": "^0.13.0",
@@ -2004,6 +2005,28 @@
"node": ">=0.1.90"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
@@ -4737,6 +4760,30 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
"dev": true
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true
},
"node_modules/@tsconfig/node16": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
"node_modules/@types/accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz",
@@ -7053,6 +7100,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/address": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz",
@@ -7264,6 +7320,12 @@
"node": ">=10"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -8617,6 +8679,12 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"node_modules/cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
@@ -13555,6 +13623,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"node_modules/make-iterator": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
@@ -14691,6 +14765,12 @@
"tslib": "^2.0.3"
}
},
"node_modules/path-equal": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz",
"integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==",
"dev": true
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -16204,6 +16284,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-stable-stringify": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz",
"integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -17264,6 +17353,58 @@
"node": ">=6.10"
}
},
"node_modules/ts-node": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"dev": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/ts-node/node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/ts-simple-type": {
"version": "2.0.0-next.0",
"resolved": "https://registry.npmjs.org/ts-simple-type/-/ts-simple-type-2.0.0-next.0.tgz",
@@ -17419,6 +17560,64 @@
"node": ">=12.20"
}
},
"node_modules/typescript-json-schema": {
"version": "0.55.0",
"resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.55.0.tgz",
"integrity": "sha512-BXaivYecUdiXWWNiUqXgY6A9cMWerwmhtO+lQE7tDZGs7Mf38sORDeQZugfYOZOHPZ9ulsD+w0LWjFDOQoXcwg==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"@types/node": "^16.9.2",
"glob": "^7.1.7",
"path-equal": "^1.1.2",
"safe-stable-stringify": "^2.2.0",
"ts-node": "^10.9.1",
"typescript": "~4.8.2",
"yargs": "^17.1.1"
},
"bin": {
"typescript-json-schema": "bin/typescript-json-schema"
}
},
"node_modules/typescript-json-schema/node_modules/@types/node": {
"version": "16.18.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.16.tgz",
"integrity": "sha512-ZOzvDRWp8dCVBmgnkIqYCArgdFOO9YzocZp8Ra25N/RStKiWvMOXHMz+GjSeVNe5TstaTmTWPucGJkDw0XXJWA==",
"dev": true
},
"node_modules/typescript-json-schema/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/typescript-json-schema/node_modules/typescript": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/typical": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
@@ -17737,6 +17936,12 @@
"integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==",
"dev": true
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"node_modules/v8-to-istanbul": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz",
@@ -18380,6 +18585,15 @@
"node": ">= 4.0.0"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -19741,6 +19955,27 @@
"dev": true,
"optional": true
},
"@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"requires": {
"@jridgewell/trace-mapping": "0.3.9"
},
"dependencies": {
"@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
}
}
},
"@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
@@ -21623,6 +21858,30 @@
"magic-string": "^0.27.0"
}
},
"@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
"dev": true
},
"@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true
},
"@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true
},
"@tsconfig/node16": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
"@types/accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz",
@@ -23668,6 +23927,12 @@
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true
},
"acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true
},
"address": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz",
@@ -23833,6 +24098,12 @@
"readable-stream": "^3.6.0"
}
},
"arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -24848,6 +25119,12 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
},
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"cross-fetch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
@@ -28520,6 +28797,12 @@
"semver": "^6.0.0"
}
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"make-iterator": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
@@ -29380,6 +29663,12 @@
"tslib": "^2.0.3"
}
},
"path-equal": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz",
"integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==",
"dev": true
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -30465,6 +30754,12 @@
"is-regex": "^1.1.4"
}
},
"safe-stable-stringify": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz",
"integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==",
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -31327,6 +31622,35 @@
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
"dev": true
},
"ts-node": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"dev": true,
"requires": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"dependencies": {
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
}
}
},
"ts-simple-type": {
"version": "2.0.0-next.0",
"resolved": "https://registry.npmjs.org/ts-simple-type/-/ts-simple-type-2.0.0-next.0.tgz",
@@ -31438,6 +31762,50 @@
"integrity": "sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA==",
"dev": true
},
"typescript-json-schema": {
"version": "0.55.0",
"resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.55.0.tgz",
"integrity": "sha512-BXaivYecUdiXWWNiUqXgY6A9cMWerwmhtO+lQE7tDZGs7Mf38sORDeQZugfYOZOHPZ9ulsD+w0LWjFDOQoXcwg==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.9",
"@types/node": "^16.9.2",
"glob": "^7.1.7",
"path-equal": "^1.1.2",
"safe-stable-stringify": "^2.2.0",
"ts-node": "^10.9.1",
"typescript": "~4.8.2",
"yargs": "^17.1.1"
},
"dependencies": {
"@types/node": {
"version": "16.18.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.16.tgz",
"integrity": "sha512-ZOzvDRWp8dCVBmgnkIqYCArgdFOO9YzocZp8Ra25N/RStKiWvMOXHMz+GjSeVNe5TstaTmTWPucGJkDw0XXJWA==",
"dev": true
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"typescript": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
"dev": true
}
}
},
"typical": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
@@ -31666,6 +32034,12 @@
"integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg==",
"dev": true
},
"v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"v8-to-istanbul": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz",
@@ -32131,6 +32505,12 @@
"integrity": "sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA==",
"dev": true
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
},
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -28,7 +28,7 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build --mode staging",
"build:libs": "npm run wc-analyze && npm run wc-analyze:vscode && rollup -c rollup-libs.config.js && node utils/move-libs.js",
"build:libs": "npm run wc-analyze && npm run wc-analyze:vscode && npm run generate:jsonschema && rollup -c rollup-libs.config.js && node utils/move-libs.js",
"build:for:static": "tsc && vite build",
"build:for:cms": "tsc && npm run build:libs && vite build -c vite.cms.config.ts",
"build:for:cms:watch": "tsc && npm run build:libs && vite build -c vite.cms.config.ts --watch",
@@ -45,12 +45,13 @@
"format:fix": "npm run format -- --write",
"generate:api": "openapi --input https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v13/dev/src/Umbraco.Cms.Api.Management/OpenApi.json --output libs/backend-api/src --postfix Resource --useOptions",
"generate:api-dev": "openapi --input http://localhost:11000/umbraco/swagger/v1/swagger.json --output libs/backend-api/src --postfix Resource --useOptions",
"generate:jsonschema": "typescript-json-schema --required --include \"./libs/extensions-registry/*.ts\" --out dist/libs/umbraco-package-schema.json tsconfig.json UmbracoPackage",
"storybook": "npm run wc-analyze && storybook dev -p 6006",
"storybook:build": "npm run wc-analyze && storybook build",
"build-storybook": "npm run wc-analyze && storybook build",
"generate:icons": "node ./devops/icons/index.js",
"wc-analyze": "wca **/*.element.ts --outFile custom-elements.json",
"wc-analyze:vscode": "wca **/*.element.ts --format vscode --outFile vscode-html-custom-data.json",
"wc-analyze": "wca **/*.element.ts --outFile dist/libs/custom-elements.json",
"wc-analyze:vscode": "wca **/*.element.ts --format vscode --outFile dist/libs/vscode-html-custom-data.json",
"new-extension": "plop --plopfile ./devops/plop/plop.js",
"compile": "tsc",
"check": "npm run lint && npm run compile && npm run build-storybook",
@@ -124,6 +125,7 @@
"storybook": "^7.0.2",
"tiny-glob": "^0.2.9",
"typescript": "^5.0.3",
"typescript-json-schema": "^0.55.0",
"uglify-js": "^3.17.4",
"vite": "^4.2.1",
"vite-plugin-static-copy": "^0.13.0",

View File

@@ -71,7 +71,8 @@ export class UmbBackofficeElement extends UmbLitElement {
this.provideContext(UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, new UmbCurrentUserHistoryStore());
// Register All Stores
this.observe(umbExtensionsRegistry.extensionsOfTypes(['store', 'treeStore']), (stores) => {
// TODO: can we use kinds here so we don't have to hardcode the types?
this.observe(umbExtensionsRegistry.extensionsOfTypes(['store', 'treeStore', 'itemStore']), (stores) => {
stores.forEach((store) => createExtensionClass(store, [this]));
});
}

View File

@@ -1,6 +1,6 @@
import type { DocumentBlueprintDetails } from '@umbraco-cms/backoffice/models';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { ArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
@@ -11,11 +11,14 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
* @description - Data Store for Document Blueprints
*/
export class UmbDocumentBlueprintStore extends UmbStoreBase {
// TODO: use the right type:
#data = new ArrayState<DocumentBlueprintDetails>([], (x) => x.id);
constructor(host: UmbControllerHostElement) {
super(host, UMB_DOCUMENT_BLUEPRINT_STORE_CONTEXT_TOKEN.toString());
super(
host,
UMB_DOCUMENT_BLUEPRINT_STORE_CONTEXT_TOKEN.toString(),
// TODO: use the right type:
new UmbArrayState<DocumentBlueprintDetails>([], (x) => x.id)
);
}
/**
@@ -29,10 +32,10 @@ export class UmbDocumentBlueprintStore extends UmbStoreBase {
fetch(`/umbraco/management/api/v1/document-blueprint/details/${id}`)
.then((res) => res.json())
.then((data) => {
this.#data.append(data);
this._data.append(data);
});
return this.#data.getObservablePart((documents) => documents.find((document) => document.id === id));
return this._data.getObservablePart((documents) => documents.find((document) => document.id === id));
}
getScaffold(entityType: string, parentId: string | null) {
@@ -68,7 +71,7 @@ export class UmbDocumentBlueprintStore extends UmbStoreBase {
})
.then((res) => res.json())
.then((data: Array<DocumentBlueprintDetails>) => {
this.#data.append(data);
this._data.append(data);
});
}
@@ -89,7 +92,7 @@ export class UmbDocumentBlueprintStore extends UmbStoreBase {
},
});
this.#data.remove(ids);
this._data.remove(ids);
}
}

View File

@@ -1,11 +1,15 @@
import { DocumentTypeTreeServerDataSource } from './sources/document-type.tree.server.data';
import { UmbDocumentTypeTreeServerDataSource } from './sources/document-type.tree.server.data';
import { UmbDocumentTypeServerDataSource } from './sources/document-type.server.data';
import { UmbDocumentTypeTreeStore, UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT_TOKEN } from './document-type.tree.store';
import { UmbDocumentTypeStore, UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN } from './document-type.store';
import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { ProblemDetailsModel, DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import {
ProblemDetailsModel,
DocumentTypeResponseModel,
FolderTreeItemResponseModel,
} from '@umbraco-cms/backoffice/backend-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
type ItemType = DocumentTypeResponseModel;
@@ -27,7 +31,7 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository<ItemType>, U
this.#host = host;
// TODO: figure out how spin up get the correct data source
this.#treeSource = new DocumentTypeTreeServerDataSource(this.#host);
this.#treeSource = new UmbDocumentTypeTreeServerDataSource(this.#host);
this.#detailDataSource = new UmbDocumentTypeServerDataSource(this.#host);
this.#init = Promise.all([
@@ -77,7 +81,7 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository<ItemType>, U
return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) };
}
async requestTreeItems(ids: Array<string>) {
async requestItemsLegacy(ids: Array<string>) {
await this.#init;
if (!ids) {
@@ -100,7 +104,7 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository<ItemType>, U
return this.#treeStore!.childrenOf(parentId);
}
async treeItems(ids: Array<string>) {
async itemsLegacy(ids: Array<string>) {
await this.#init;
return this.#treeStore!.items(ids);
}
@@ -141,19 +145,22 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository<ItemType>, U
// Could potentially be general methods:
async create(template: ItemType) {
if (!template || !template.id) throw new Error('Template is missing');
async create(documentType: ItemType) {
if (!documentType || !documentType.id) throw new Error('Template is missing');
await this.#init;
const { error } = await this.#detailDataSource.insert(template);
const { error } = await this.#detailDataSource.insert(documentType);
if (!error) {
const treeItem = createTreeItem(documentType);
this.#treeStore?.appendItems([treeItem]);
const notification = { data: { message: `Document created` } };
this.#notificationContext?.peek('positive', notification);
// TODO: we currently don't use the detail store for anything.
// Consider to look up the data before fetching from the server
this.#detailStore?.append(template);
this.#detailStore?.append(documentType);
// TODO: Update tree store with the new item? or ask tree to request the new item?
}
@@ -206,3 +213,20 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository<ItemType>, U
return { error };
}
}
export const createTreeItem = (item: ItemType): FolderTreeItemResponseModel => {
if (!item) throw new Error('item is null or undefined');
if (!item.id) throw new Error('item.id is null or undefined');
// TODO: needs parentID, this is missing in the current model. Should be good when updated to a createModel.
return {
$type: 'FolderTreeItemResponseModel',
type: 'data-type',
parentId: null,
name: item.name,
id: item.id,
isFolder: false,
isContainer: false,
hasChildren: false,
};
};

View File

@@ -1,6 +1,6 @@
import { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { ArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
@@ -11,15 +11,17 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
* @description - Data Store for Document Types
*/
export class UmbDocumentTypeStore extends UmbStoreBase {
#data = new ArrayState<DocumentTypeResponseModel>([], (x) => x.id);
/**
* Creates an instance of UmbDocumentTypeStore.
* @param {UmbControllerHostElement} host
* @memberof UmbDocumentTypeStore
*/
constructor(host: UmbControllerHostElement) {
super(host, UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN.toString());
super(
host,
UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN.toString(),
new UmbArrayState<DocumentTypeResponseModel>([], (x) => x.id)
);
}
/**
@@ -28,7 +30,7 @@ export class UmbDocumentTypeStore extends UmbStoreBase {
* @memberof UmbDocumentTypeStore
*/
append(document: DocumentTypeResponseModel) {
this.#data.append([document]);
this._data.append([document]);
}
/**
@@ -37,7 +39,7 @@ export class UmbDocumentTypeStore extends UmbStoreBase {
* @memberof UmbDocumentTypeStore
*/
byId(id: DocumentTypeResponseModel['id']) {
return this.#data.getObservablePart((x) => x.find((y) => y.id === id));
return this._data.getObservablePart((x) => x.find((y) => y.id === id));
}
/**
@@ -46,7 +48,7 @@ export class UmbDocumentTypeStore extends UmbStoreBase {
* @memberof UmbDocumentTypeStore
*/
remove(uniques: Array<DocumentTypeResponseModel['id']>) {
this.#data.remove(uniques);
this._data.remove(uniques);
}
}

View File

@@ -66,11 +66,7 @@ export class UmbDocumentTypeServerDataSource implements UmbDataSource<any, any,
* @memberof UmbDocumentTypeServerDataSource
*/
async insert(document: DocumentTypeResponseModel) {
if (!document.id) {
//const error: ProblemDetails = { title: 'Document id is missing' };
return Promise.reject();
}
//const payload = { id: document.id, requestBody: document };
if (!document.id) throw new Error('ID is missing');
let body: string;

View File

@@ -6,10 +6,10 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Document tree that fetches data from the server
* @export
* @class DocumentTreeServerDataSource
* @implements {DocumentTreeDataSource}
* @class UmbDocumentTypeTreeServerDataSource
* @implements {UmbTreeDataSource}
*/
export class DocumentTypeTreeServerDataSource implements UmbTreeDataSource {
export class UmbDocumentTypeTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHostElement;
// TODO: how do we handle trashed items?
@@ -42,9 +42,9 @@ export class DocumentTypeTreeServerDataSource implements UmbTreeDataSource {
}
/**
* Creates an instance of DocumentTreeServerDataSource.
* Creates an instance of UmbDocumentTypeTreeServerDataSource.
* @param {UmbControllerHostElement} host
* @memberof DocumentTreeServerDataSource
* @memberof UmbDocumentTypeTreeServerDataSource
*/
constructor(host: UmbControllerHostElement) {
this.#host = host;
@@ -53,7 +53,7 @@ export class DocumentTypeTreeServerDataSource implements UmbTreeDataSource {
/**
* Fetches the root items for the tree from the server
* @return {*}
* @memberof DocumentTreeServerDataSource
* @memberof UmbDocumentTypeTreeServerDataSource
*/
async getRootItems() {
return tryExecuteAndNotify(this.#host, DocumentTypeResource.getTreeDocumentTypeRoot({}));
@@ -63,7 +63,7 @@ export class DocumentTypeTreeServerDataSource implements UmbTreeDataSource {
* Fetches the children of a given parent id from the server
* @param {(string | null)} parentId
* @return {*}
* @memberof DocumentTreeServerDataSource
* @memberof UmbDocumentTypeTreeServerDataSource
*/
async getChildrenOf(parentId: string | null) {
if (!parentId) {
@@ -83,7 +83,7 @@ export class DocumentTypeTreeServerDataSource implements UmbTreeDataSource {
* Fetches the items for the given ids from the server
* @param {Array<string>} ids
* @return {*}
* @memberof DocumentTreeServerDataSource
* @memberof UmbDocumentTypeTreeServerDataSource
*/
async getItems(ids: Array<string>) {
if (ids) {

View File

@@ -3,13 +3,115 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbDocumentTypeWorkspaceContext } from './document-type-workspace.context';
import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN, UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/modal';
import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api';
@customElement('umb-document-type-workspace-editor')
export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement {
@state()
private _icon?: string;
@state()
private _iconColorAlias?: string;
// TODO: Color should be using an alias, and look up in some dictionary/key/value) of project-colors.
#workspaceContext?: UmbDocumentTypeWorkspaceContext;
@state()
private _name?: string;
@state()
private _alias?: string;
private _modalContext?: UmbModalContext;
constructor() {
super();
this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (instance) => {
this.#workspaceContext = instance as UmbDocumentTypeWorkspaceContext;
this.#observeDocumentType();
});
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
this._modalContext = instance;
});
}
#observeDocumentType() {
if (!this.#workspaceContext) return;
this.observe(this.#workspaceContext.name, (name) => (this._name = name));
this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias));
this.observe(this.#workspaceContext.icon, (icon) => (this._icon = icon));
}
// TODO. find a way where we don't have to do this for all workspaces.
private _handleNameInput(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
this.#workspaceContext?.setName(target.value);
}
}
}
// TODO. find a way where we don't have to do this for all workspaces.
private _handleAliasInput(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
this.#workspaceContext?.setAlias(target.value);
}
}
event.stopPropagation();
}
private async _handleIconClick() {
const modalHandler = this._modalContext?.open(UMB_ICON_PICKER_MODAL, {
icon: this._icon,
color: this._iconColorAlias,
});
modalHandler?.onSubmit().then((saved) => {
if (saved.icon) this.#workspaceContext?.setIcon(saved.icon);
// TODO: save color ALIAS as well
});
}
render() {
return html`
<umb-workspace-layout alias="Umb.Workspace.DocumentType">
<div id="header" slot="header">
<uui-button id="icon" @click=${this._handleIconClick} compact>
<uui-icon name="${this._icon}" style="color: ${this._iconColorAlias}"></uui-icon>
</uui-button>
<uui-input id="name" .value=${this._name} @input="${this._handleNameInput}">
<uui-input-lock id="alias" slot="append" .value=${this._alias} @input="${this._handleAliasInput}"></uui-input
></uui-input-lock>
</uui-input>
</div>
<div slot="footer">
<!-- TODO: Shortcuts Modal? -->
<uui-button label="Show keyboard shortcuts">
Keyboard Shortcuts
<uui-keyboard-shortcut>
<uui-key>ALT</uui-key>
+
<uui-key>shift</uui-key>
+
<uui-key>k</uui-key>
</uui-keyboard-shortcut>
</uui-button>
</div>
</umb-workspace-layout>
`;
}
static styles = [
UUITextStyles,
css`
@@ -42,107 +144,6 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement {
}
`,
];
// TODO: notice this format is not acceptable:
@state()
private _icon = {
color: '#000000',
name: 'umb:document-dashed-line',
};
#workspaceContext?: UmbDocumentTypeWorkspaceContext;
//@state()
//private _documentType?: DocumentTypeResponseModel;
@state()
private _name?: string;
@state()
private _alias?: string;
private _modalContext?: UmbModalContext;
constructor() {
super();
this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (instance) => {
this.#workspaceContext = instance as UmbDocumentTypeWorkspaceContext;
this.#observeDocumentType();
});
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
this._modalContext = instance;
});
}
#observeDocumentType() {
if (!this.#workspaceContext) return;
//this.observe(this.#workspaceContext.data, (data) => (this._documentType = data));
this.observe(this.#workspaceContext.name, (name) => (this._name = name));
this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias));
}
// TODO. find a way where we don't have to do this for all workspaces.
private _handleNameInput(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
this.#workspaceContext?.setName(target.value);
}
}
}
// TODO. find a way where we don't have to do this for all workspaces.
private _handleAliasInput(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
this.#workspaceContext?.setAlias(target.value);
}
}
event.stopPropagation();
}
private async _handleIconClick() {
const modalHandler = this._modalContext?.open(UMB_ICON_PICKER_MODAL);
modalHandler?.onSubmit().then((saved) => {
if (saved.icon) this.#workspaceContext?.setIcon(saved.icon);
// TODO save color ALIAS as well
});
}
render() {
return html`
<umb-workspace-layout alias="Umb.Workspace.DocumentType">
<div id="header" slot="header">
<uui-button id="icon" @click=${this._handleIconClick} compact>
<uui-icon name="${this._icon.name}" style="color: ${this._icon.color}"></uui-icon>
</uui-button>
<uui-input id="name" .value=${this._name} @input="${this._handleNameInput}">
<uui-input-lock id="alias" slot="append" .value=${this._alias} @input="${this._handleAliasInput}"></uui-input
></uui-input-lock>
</uui-input>
</div>
<div slot="footer">
<uui-button label="Show keyboard shortcuts">
Keyboard Shortcuts
<uui-keyboard-shortcut>
<uui-key>ALT</uui-key>
+
<uui-key>shift</uui-key>
+
<uui-key>k</uui-key>
</uui-keyboard-shortcut>
</uui-button>
</div>
</umb-workspace-layout>
`;
}
}
export default UmbDocumentTypeWorkspaceEditorElement;

View File

@@ -2,7 +2,11 @@ import { UmbWorkspaceContext } from '../../../shared/components/workspace/worksp
import { UmbDocumentTypeRepository } from '../repository/document-type.repository';
import { UmbWorkspacePropertyStructureManager } from '../../../shared/components/workspace/workspace-context/workspace-structure-manager.class';
import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace';
import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import type {
ContentTypeCompositionModel,
ContentTypeSortModel,
DocumentTypeResponseModel,
} from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
type EntityType = DocumentTypeResponseModel;
@@ -80,12 +84,43 @@ export class UmbDocumentTypeWorkspaceContext
setAlias(alias: string) {
this.structure.updateRootDocumentType({ alias });
}
setDescription(description: string) {
this.structure.updateRootDocumentType({ description });
}
// TODO => manage setting icon color
// TODO: manage setting icon color alias?
setIcon(icon: string) {
this.structure.updateRootDocumentType({ icon });
}
setAllowedAsRoot(allowedAsRoot: boolean) {
this.structure.updateRootDocumentType({ allowedAsRoot });
}
setVariesByCulture(variesByCulture: boolean) {
this.structure.updateRootDocumentType({ variesByCulture });
}
setVariesBySegment(variesBySegment: boolean) {
this.structure.updateRootDocumentType({ variesBySegment });
}
setIsElement(isElement: boolean) {
this.structure.updateRootDocumentType({ isElement });
}
setAllowedContentTypes(allowedContentTypes: Array<ContentTypeSortModel>) {
this.structure.updateRootDocumentType({ allowedContentTypes });
}
setCompositions(compositions: Array<ContentTypeCompositionModel>) {
this.structure.updateRootDocumentType({ compositions });
}
// Document type specific:
setAllowedTemplateIds(allowedTemplateIds: Array<string>) {
console.log('setAllowedTemplateIds', allowedTemplateIds);
this.structure.updateRootDocumentType({ allowedTemplateIds });
}
setDefaultTemplateId(defaultTemplateId: string) {
this.structure.updateRootDocumentType({ defaultTemplateId });
}
async createScaffold(parentId: string) {
const { data } = await this.structure.createScaffold(parentId);
if (!data) return undefined;

View File

@@ -2,17 +2,53 @@ import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { UmbWorkspacePropertyStructureHelper } from '../../../../../shared/components/workspace/workspace-context/workspace-property-structure-helper.class';
import { PropertyContainerTypes } from '../../../../../shared/components/workspace/workspace-context/workspace-structure-manager.class';
import { UmbSorterController, UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { DocumentTypePropertyTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UMB_MODAL_CONTEXT_TOKEN, UMB_PROPERTY_SETTINGS_MODAL } from '@umbraco-cms/backoffice/modal';
import './document-type-workspace-view-edit-property.element';
const SORTER_CONFIG: UmbSorterConfig<DocumentTypePropertyTypeResponseModel> = {
compareElementToModel: (element: HTMLElement, model: DocumentTypePropertyTypeResponseModel) => {
return element.getAttribute('data-umb-property-id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: DocumentTypePropertyTypeResponseModel) => {
return container.querySelector('data-umb-property-id[' + modelEntry.id + ']');
},
identifier: 'content-type-property-sorter',
itemSelector: '[data-umb-property-id]',
disabledItemSelector: '[inherited]',
containerSelector: '#property-list',
};
@customElement('umb-document-type-workspace-view-edit-properties')
export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitElement {
#propertySorter = new UmbSorterController(this, {
...SORTER_CONFIG,
performItemInsert: (args) => {
let sortOrder = 0;
if (this._propertyStructure.length > 0) {
if (args.newIndex === 0) {
// TODO: Remove 'as any' when sortOrder is added to the model:
sortOrder = ((this._propertyStructure[0] as any).sortOrder ?? 0) - 1;
} else {
sortOrder =
((this._propertyStructure[Math.min(args.newIndex, this._propertyStructure.length - 1)] as any).sortOrder ??
0) + 1;
}
}
return this._propertyStructureHelper.insertProperty(args.item, sortOrder);
},
performItemRemove: (args) => {
return this._propertyStructureHelper.removeProperty(args.item.id!);
},
});
private _containerId: string | undefined;
@property({ type: String, attribute: 'container-id', reflect: false })
public get containerId(): string | undefined {
return this._containerId;
}
@@ -52,6 +88,7 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => (this.#modalContext = instance));
this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => {
this._propertyStructure = propertyStructure;
this.#propertySorter.setModel(this._propertyStructure);
});
}
@@ -70,50 +107,29 @@ export class UmbDocumentTypeWorkspaceViewEditPropertiesElement extends UmbLitEle
}
render() {
return html`${repeat(
this._propertyStructure,
(property) => property.alias,
(property) =>
html`<document-type-workspace-view-edit-property
.property=${property}
@partial-property-update=${(event: CustomEvent) => {
this._propertyStructureHelper.partialUpdateProperty(property.id, event.detail);
}}></document-type-workspace-view-edit-property>`
)}<uui-button id="add" look="placeholder" @click=${this.#onAddProperty}> Add property </uui-button>`;
return html`<div id="property-list">
${repeat(
this._propertyStructure,
(property) => property.alias ?? '' + property.containerId ?? '' + (property as any).sortOrder ?? '',
(property) =>
html`<document-type-workspace-view-edit-property
class="property"
data-umb-property-id=${property.id}
data-property-container-is=${property.containerId}
data-container-id=${this.containerId}
?inherited=${ifDefined(property.containerId !== this.containerId)}
.property=${property}
@partial-property-update=${(event: CustomEvent) => {
this._propertyStructureHelper.partialUpdateProperty(property.id, event.detail);
}}></document-type-workspace-view-edit-property>`
)}
</div>
<uui-button id="add" look="placeholder" @click=${this.#onAddProperty}> Add property </uui-button> `;
}
static styles = [
UUITextStyles,
css`
.property:first-of-type {
padding-top: 0;
}
.property {
border-bottom: 1px solid var(--uui-color-divider);
}
.property:last-child {
border-bottom: 0;
}
.property {
display: grid;
grid-template-columns: 200px auto;
column-gap: var(--uui-size-layout-2);
border-bottom: 1px solid var(--uui-color-divider);
padding: var(--uui-size-layout-1) 0;
container-type: inline-size;
}
.property > div {
grid-column: span 2;
}
@container (width > 600px) {
.property:not([orientation='vertical']) > div {
grid-column: span 1;
}
}
#add {
width: 100%;
}

View File

@@ -12,13 +12,23 @@ import { PropertyTypeResponseModelBaseModel } from '@umbraco-cms/backoffice/back
export class UmbDocumentTypeWorkspacePropertyElement extends LitElement {
/**
* Property, the data object for the property.
* @type {string}
* @type {PropertyTypeResponseModelBaseModel}
* @attr
* @default ''
* @default undefined
*/
@property({ type: Object })
public property?: PropertyTypeResponseModelBaseModel;
/**
* Inherited, Determines if the property is part of the main document type thats being edited.
* If true, then the property is inherited from another document type, not a part of the main document type.
* @type {boolean}
* @attr
* @default undefined
*/
@property({ type: Boolean })
public inherited?: boolean;
_firePartialUpdate(propertyName: string, value: string | number | boolean | null | undefined) {
const partialObject = {} as any;
partialObject[propertyName] = value;
@@ -26,8 +36,20 @@ export class UmbDocumentTypeWorkspacePropertyElement extends LitElement {
this.dispatchEvent(new CustomEvent('partial-property-update', { detail: partialObject }));
}
render() {
// TODO: Only show alias on label if user has access to DocumentType within settings:
renderInheritedProperty() {
return this.property
? html`
<div id="header">
<b>${this.property.name}</b>
<i>${this.property.alias}</i>
<p>${this.property.description}</p>
</div>
<div id="editor"></div>
`
: '';
}
renderEditableProperty() {
return this.property
? html`
<div id="header">
@@ -50,11 +72,16 @@ export class UmbDocumentTypeWorkspacePropertyElement extends LitElement {
}}></uui-textarea>
</p>
</div>
<div></div>
<div id="editor"></div>
`
: '';
}
render() {
// TODO: Only show alias on label if user has access to DocumentType within settings:
return this.inherited ? this.renderInheritedProperty() : this.renderEditableProperty();
}
static styles = [
UUITextStyles,
css`
@@ -81,9 +108,28 @@ export class UmbDocumentTypeWorkspacePropertyElement extends LitElement {
border-bottom: none;
}
:host-context(umb-variantable-property:first-of-type) {
:host(:first-of-type) {
padding-top: 0;
}
:host([draggable='true']) {
cursor: grab;
}
/* Placeholder style, used when property is being dragged.*/
:host(.--umb-sorter-placeholder) {
height: 2px;
}
:host(.--umb-sorter-placeholder) > div {
display: none;
}
:host(.--umb-sorter-placeholder)::after {
content: '';
grid-column: span 2;
width: 100%;
border-top: 2px solid blue;
border-radius: 1px;
/* TODO: Make use of same highlight color as UUI and the same Animation. Consider making this a component/(available style) in UUI? */
}
p {
margin-bottom: 0;
@@ -95,6 +141,10 @@ export class UmbDocumentTypeWorkspacePropertyElement extends LitElement {
height: min-content;
z-index: 2;
}
#editor {
background-color: var(--uui-color-background);
}
`,
];
}

View File

@@ -78,7 +78,7 @@ export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement {
#onAddGroup = () => {
// Idea, maybe we can gather the sortOrder from the last group rendered and add 1 to it?
this._groupStructureHelper.addGroup(this._ownerTabId);
this._groupStructureHelper.addContainer(this._ownerTabId);
};
render() {
@@ -87,6 +87,7 @@ export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement {
? html`
<uui-box>
<umb-document-type-workspace-view-edit-properties
container-id=${this.ownerTabId}
container-type="Tab"
container-name=${this.tabName || ''}></umb-document-type-workspace-view-edit-properties>
</uui-box>
@@ -97,6 +98,7 @@ export class UmbDocumentTypeWorkspaceViewEditTabElement extends UmbLitElement {
(group) => group.name,
(group) => html`<uui-box .headline=${group.name || ''}>
<umb-document-type-workspace-view-edit-properties
container-id=${group.id}
container-type="Group"
container-name=${group.name || ''}></umb-document-type-workspace-view-edit-properties>
</uui-box>`

View File

@@ -120,6 +120,11 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement {
this._routes = routes;
}
#requestRemoveTab(tabId: string | undefined) {
// TODO: If this tab is composed of other tabs, then notify that it will only delete the local tab.
// TODO: Update URL when removing tab.
this.#remove(tabId);
}
#remove(tabId: string | undefined) {
if (!tabId) return;
this._workspaceContext?.structure.removeContainer(null, tabId);
@@ -142,19 +147,31 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement {
: ''}
${repeat(
this._tabs,
(tab) => tab.id,
(tab) => tab.id! + tab.name,
(tab) => {
// TODO: make better url folder name:
const path = this._routerPath + '/tab/' + encodeURI(tab.name || '');
return html`<uui-tab label=${tab.name!} .active=${path === this._activePath} href=${path}>
${path === this._activePath
? html` <uui-input label="Tab name" look="placeholder" value=${tab.name!} placeholder="Enter a name">
<!-- todo only if its part of root: -->
${path === this._activePath && this._tabsStructureHelper.isOwnerContainer(tab.id!)
? html` <uui-input
label="Tab name"
look="placeholder"
value=${tab.name!}
placeholder="Enter a name"
@change=${(e: InputEvent) => {
const newName = (e.target as HTMLInputElement).value;
this._tabsStructureHelper.partialUpdateContainer(tab.id, {
name: newName,
});
// Update the current URL, so we are still on this specific tab:
window.history.replaceState(null, '', this._routerPath + '/tab/' + encodeURI(newName));
}}>
<uui-button
label="Remove tab"
class="trash"
slot="append"
@click=${() => this.#remove(tab.id)}
@click=${() => this.#requestRemoveTab(tab.id)}
compact>
<uui-icon name="umb:trash"></uui-icon>
</uui-button>

View File

@@ -1,13 +1,102 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import type { UUIToggleElement } from '@umbraco-ui/uui';
import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api';
@customElement('umb-document-type-workspace-view-details')
export class UmbDocumentTypeWorkspaceViewDetailsElement extends UmbLitElement {
#workspaceContext?: UmbDocumentTypeWorkspaceContext;
@state()
private _variesByCulture?: boolean;
@state()
private _variesBySegment?: boolean;
@state()
private _isElement?: boolean;
constructor() {
super();
// TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken
this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => {
this.#workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext;
this._observeDocumentType();
});
}
private _observeDocumentType() {
if (!this.#workspaceContext) return;
this.observe(
this.#workspaceContext.variesByCulture,
(variesByCulture) => (this._variesByCulture = variesByCulture)
);
this.observe(
this.#workspaceContext.variesBySegment,
(variesBySegment) => (this._variesBySegment = variesBySegment)
);
this.observe(this.#workspaceContext.isElement, (isElement) => (this._isElement = isElement));
}
render() {
return html`
<uui-box headline="Data configuration">
<umb-workspace-property-layout alias="VaryByCulture" label="Allow vary by culture">
<div slot="description">Allow editors to create content of different languages.</div>
<div slot="editor">
<uui-toggle
.checked=${this._variesByCulture}
@change=${(e: CustomEvent) => {
this.#workspaceContext?.setVariesByCulture((e.target as UUIToggleElement).checked);
}}
label="Vary by culture"></uui-toggle>
</div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="VaryBySegments" label="Allow segmentation">
<div slot="description">Allow editors to segment their content.</div>
<div slot="editor">
<uui-toggle
.checked=${this._variesBySegment}
@change=${(e: CustomEvent) => {
this.#workspaceContext?.setVariesBySegment((e.target as UUIToggleElement).checked);
}}
label="Vary by segments"></uui-toggle>
</div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="ElementType" label="Is an Element Type">
<div slot="description">
An Element Type is used for content instances in Property Editors, like the Block Editors.
</div>
<div slot="editor">
<uui-toggle
.checked=${this._isElement}
@change=${(e: CustomEvent) => {
this.#workspaceContext?.setIsElement((e.target as UUIToggleElement).checked);
}}
label="Element type"></uui-toggle>
</div>
</umb-workspace-property-layout>
</uui-box>
<uui-box headline="History cleanup">
<umb-workspace-property-layout alias="HistoryCleanup" label="History cleanup">
<div slot="description">
Allow overriding the global history cleanup settings. (TODO: this ui is not working.. )
</div>
<div slot="editor">
<!-- TODO: Bind this with context/data -->
<uui-toggle .checked="${true}" label="Auto cleanup"></uui-toggle>
<uui-label for="versions-newer-than-days">Keep all versions newer than X days</uui-label>
<umb-property-editor-ui-number id="versions-newer-than-days"></umb-property-editor-ui-number>
<uui-label for="latest-version-per-day-days">Keep latest version per day for X days</uui-label>
<umb-property-editor-ui-number id="latest-version-per-day-days"></umb-property-editor-ui-number>
</div>
</umb-workspace-property-layout>
</uui-box>
`;
}
static styles = [
UUITextStyles,
css`
@@ -26,57 +115,6 @@ export class UmbDocumentTypeWorkspaceViewDetailsElement extends UmbLitElement {
}
`,
];
private _workspaceContext?: UmbDocumentTypeWorkspaceContext;
constructor() {
super();
// TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken
this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => {
this._workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext;
this._observeDocumentType();
});
}
private _observeDocumentType() {
if (!this._workspaceContext) return;
}
render() {
return html`
<uui-box headline="Data configuration">
<umb-workspace-property-layout alias="VaryByCulture" label="Allow vary by culture">
<div slot="description">Allow editors to create content of different languages.</div>
<div slot="editor"><uui-toggle label="Vary by culture"></uui-toggle></div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="VaryBySegments" label="Allow segmentation">
<div slot="description">Allow editors to segment their content.</div>
<div slot="editor"><uui-toggle label="Vary by segments"></uui-toggle></div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="ElementType" label="Is an Element Type">
<div slot="description">
An Element Type is used for content instances in Property Editors, like the Block Editors.
</div>
<div slot="editor"><uui-toggle label="Element type"></uui-toggle></div>
</umb-workspace-property-layout>
</uui-box>
<uui-box headline="History cleanup">
<umb-workspace-property-layout alias="HistoryCleanup" label="History cleanup">
<div slot="description">
Allow overriding the global history cleanup settings. (TODO: this ui is not working.. )
</div>
<div slot="editor">
<uui-toggle .checked="${true}" label="Auto cleanup"></uui-toggle>
<uui-label for="versions-newer-than-days">Keep all versions newer than X days</uui-label>
<umb-property-editor-ui-number id="versions-newer-than-days"></umb-property-editor-ui-number>
<uui-label for="latest-version-per-day-days">Keep latest version per day for X days</uui-label>
<umb-property-editor-ui-number id="latest-version-per-day-days"></umb-property-editor-ui-number>
</div>
</umb-workspace-property-layout>
</uui-box>
`;
}
}
export default UmbDocumentTypeWorkspaceViewDetailsElement;

View File

@@ -1,13 +1,90 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import type { UUIToggleElement } from '@umbraco-ui/uui';
import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context';
import type { UmbInputDocumentTypePickerElement } from '../../../../../../backoffice/shared/components/input-document-type-picker/input-document-type-picker.element';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { DocumentTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api';
@customElement('umb-document-type-workspace-view-structure')
export class UmbDocumentTypeWorkspaceViewStructureElement extends UmbLitElement {
#workspaceContext?: UmbDocumentTypeWorkspaceContext;
@state()
private _allowedAsRoot?: boolean;
@state()
private _allowedContentTypeIDs?: Array<string>;
constructor() {
super();
// TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken
this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => {
this.#workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext;
this._observeDocumentType();
});
}
private _observeDocumentType() {
if (!this.#workspaceContext) return;
this.observe(this.#workspaceContext.allowedAsRoot, (allowedAsRoot) => (this._allowedAsRoot = allowedAsRoot));
this.observe(this.#workspaceContext.allowedContentTypes, (allowedContentTypes) => {
this._allowedContentTypeIDs = allowedContentTypes
?.map((x) => x.id)
.filter((x) => x !== undefined) as Array<string>;
console.log('this._allowedContentTypeIDs', this._allowedContentTypeIDs);
});
}
render() {
return html`
<uui-box headline="Structure">
<umb-workspace-property-layout alias="Root" label="Allow as Root">
<div slot="description">Allow editors to create content of this type in the root of the content tree.</div>
<div slot="editor">
<uui-toggle
label="Allow as root"
.checked=${this._allowedAsRoot}
@change=${(e: CustomEvent) => {
this.#workspaceContext?.setAllowedAsRoot((e.target as UUIToggleElement).checked);
}}></uui-toggle>
</div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="ChildNodeType" label="Allowed child node types">
<div slot="description">
Allow content of the specified types to be created underneath content of this type.
</div>
<div slot="editor">
<!-- TODO: maybe we want to somehow display the hierarchy, but not necessary in the same way as old backoffice? -->
<umb-input-document-type-picker
.selectedIds=${this._allowedContentTypeIDs}
@change="${(e: CustomEvent) => {
const sortedContentTypesList = (e.target as UmbInputDocumentTypePickerElement).selectedIds.map(
(id, index) => ({
id: id,
sortOrder: index,
})
);
this.#workspaceContext?.setAllowedContentTypes(sortedContentTypesList);
}}">
</umb-input-document-type-picker>
</div>
</umb-workspace-property-layout>
</uui-box>
<uui-box headline="Presentation">
<umb-workspace-property-layout alias="Root" label="Collection">
<div slot="description">
Use this document as a collection, displaying its children in a Collection View. This could be a list or a
table.
</div>
<div slot="editor"><uui-toggle label="Present as a Collection"></uui-toggle></div>
</umb-workspace-property-layout>
</uui-box>
`;
}
static styles = [
UUITextStyles,
css`
@@ -26,51 +103,6 @@ export class UmbDocumentTypeWorkspaceViewStructureElement extends UmbLitElement
}
`,
];
private _workspaceContext?: UmbDocumentTypeWorkspaceContext;
constructor() {
super();
// TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken
this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => {
this._workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext;
this._observeDocumentType();
});
}
private _observeDocumentType() {
if (!this._workspaceContext) return;
}
render() {
return html`
<uui-box headline="Structure">
<umb-workspace-property-layout alias="Root" label="Allow as Root">
<div slot="description">Allow editors to create content of this type in the root of the content tree.</div>
<div slot="editor"><uui-toggle label="Allow as root"></uui-toggle></div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="ChildNodeType" label="Allowed child node types">
<div slot="description">
Allow content of the specified types to be created underneath content of this type.
</div>
<div slot="editor">
<!-- TODO: maybe we want to somehow display the hierarchy, but not necessary in the same way as old backoffice? -->
<umb-input-document-type-picker></umb-input-document-type-picker>
</div>
</umb-workspace-property-layout>
</uui-box>
<uui-box headline="Presentation">
<umb-workspace-property-layout alias="Root" label="Collection">
<div slot="description">
Use this document as a collection, displaying its children in a Collection View. This could be a list or a
table.
</div>
<div slot="editor"><uui-toggle label="Present as a Collection"></uui-toggle></div>
</umb-workspace-property-layout>
</uui-box>
`;
}
}
export default UmbDocumentTypeWorkspaceViewStructureElement;

View File

@@ -1,12 +1,63 @@
import { css, html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement } from 'lit/decorators.js';
import { customElement, state } from 'lit/decorators.js';
import { UmbDocumentTypeWorkspaceContext } from '../../document-type-workspace.context';
import type { UmbInputTemplateElement } from '../../../../../../backoffice/shared/components/input-template/input-template.element';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-api';
@customElement('umb-document-type-workspace-view-templates')
export class UmbDocumentTypeWorkspaceViewTemplatesElement extends UmbLitElement {
#workspaceContext?: UmbDocumentTypeWorkspaceContext;
@state()
private _defaultTemplateId?: string | null;
@state()
private _allowedTemplateIds?: Array<string>;
constructor() {
super();
this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => {
this.#workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext;
this._observeDocumentType();
});
}
private _observeDocumentType() {
if (!this.#workspaceContext) return;
this.observe(
this.#workspaceContext.defaultTemplateId,
(defaultTemplateId) => (this._defaultTemplateId = defaultTemplateId)
);
this.observe(this.#workspaceContext.allowedTemplateIds, (allowedTemplateIds) => {
console.log('allowedTemplateIds', allowedTemplateIds);
this._allowedTemplateIds = allowedTemplateIds;
});
}
#templateInputChange(e: CustomEvent) {
console.log('change', e);
// save new allowed ids
const input = e.target as UmbInputTemplateElement;
this.#workspaceContext?.setAllowedTemplateIds(input.selectedIds);
this.#workspaceContext?.setDefaultTemplateId(input.defaultId);
}
render() {
return html`<uui-box headline="Templates">
<umb-workspace-property-layout alias="Templates" label="Allowed Templates">
<div slot="description">Choose which templates editors are allowed to use on content of this type</div>
<div id="templates" slot="editor">
<umb-input-template
.defaultId=${this._defaultTemplateId}
.selectedIds=${this._allowedTemplateIds}
@change=${this.#templateInputChange}></umb-input-template>
</div>
</umb-workspace-property-layout>
</uui-box>`;
}
static styles = [
UUITextStyles,
css`
@@ -34,45 +85,6 @@ export class UmbDocumentTypeWorkspaceViewTemplatesElement extends UmbLitElement
}
`,
];
private _workspaceContext?: UmbDocumentTypeWorkspaceContext;
constructor() {
super();
this.consumeContext(UMB_ENTITY_WORKSPACE_CONTEXT, (documentTypeContext) => {
this._workspaceContext = documentTypeContext as UmbDocumentTypeWorkspaceContext;
this._observeDocumentType();
});
}
private _observeDocumentType() {
if (!this._workspaceContext) return;
}
async #changeDefaultId(e: CustomEvent) {
// save new default id
console.log('workspace: default template id', e);
}
#changeAllowedKeys(e: CustomEvent) {
// save new allowed ids
console.log('workspace: allowed templates changed', e);
}
render() {
return html`<uui-box headline="Templates">
<umb-workspace-property-layout alias="Templates" label="Allowed Templates">
<div slot="description">Choose which templates editors are allowed to use on content of this type</div>
<div id="templates" slot="editor">
<umb-input-template-picker
.defaultKey="${/*this._documentType?.defaultTemplateId ??*/ ''}"
.allowedKeys="${/*this._documentType?.allowedTemplateIds ??*/ []}"
@change-default="${this.#changeDefaultId}"
@change-allowed="${this.#changeAllowedKeys}"></umb-input-template-picker>
</div>
</umb-workspace-property-layout>
</uui-box>`;
}
}
export default UmbDocumentTypeWorkspaceViewTemplatesElement;

View File

@@ -1,7 +1,7 @@
import { UmbDocumentServerDataSource } from './sources/document.server.data';
import { UmbDocumentStore, UMB_DOCUMENT_STORE_CONTEXT_TOKEN } from './document.store';
import { UmbDocumentTreeStore, UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from './document.tree.store';
import { DocumentTreeServerDataSource } from './sources/document.tree.server.data';
import { UmbDocumentTreeServerDataSource } from './sources/document.tree.server.data';
import type { UmbTreeDataSource, UmbTreeRepository, UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
@@ -32,7 +32,7 @@ export class UmbDocumentRepository implements UmbTreeRepository<ItemType>, UmbDe
this.#host = host;
// TODO: figure out how spin up get the correct data source
this.#treeSource = new DocumentTreeServerDataSource(this.#host);
this.#treeSource = new UmbDocumentTreeServerDataSource(this.#host);
this.#detailDataSource = new UmbDocumentServerDataSource(this.#host);
this.#init = Promise.all([
@@ -82,7 +82,7 @@ export class UmbDocumentRepository implements UmbTreeRepository<ItemType>, UmbDe
return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) };
}
async requestTreeItems(ids: Array<string>) {
async requestItemsLegacy(ids: Array<string>) {
await this.#init;
if (!ids) {
@@ -105,7 +105,7 @@ export class UmbDocumentRepository implements UmbTreeRepository<ItemType>, UmbDe
return this.#treeStore!.childrenOf(parentId);
}
async treeItems(ids: Array<string>) {
async itemsLegacy(ids: Array<string>) {
await this.#init;
return this.#treeStore!.items(ids);
}

View File

@@ -1,6 +1,6 @@
import { DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { ArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
@@ -11,15 +11,13 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
* @description - Data Store for Template Details
*/
export class UmbDocumentStore extends UmbStoreBase {
#data = new ArrayState<DocumentResponseModel>([], (x) => x.id);
/**
* Creates an instance of UmbDocumentDetailStore.
* @param {UmbControllerHostElement} host
* @memberof UmbDocumentDetailStore
*/
constructor(host: UmbControllerHostElement) {
super(host, UMB_DOCUMENT_STORE_CONTEXT_TOKEN.toString());
super(host, UMB_DOCUMENT_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState<DocumentResponseModel>([], (x) => x.id));
}
/**
@@ -28,7 +26,7 @@ export class UmbDocumentStore extends UmbStoreBase {
* @memberof UmbDocumentDetailStore
*/
append(document: DocumentResponseModel) {
this.#data.append([document]);
this._data.append([document]);
}
/**
@@ -37,7 +35,7 @@ export class UmbDocumentStore extends UmbStoreBase {
* @memberof UmbDocumentStore
*/
byKey(id: DocumentResponseModel['id']) {
return this.#data.getObservablePart((x) => x.find((y) => y.id === id));
return this._data.getObservablePart((x) => x.find((y) => y.id === id));
}
/**
@@ -46,7 +44,7 @@ export class UmbDocumentStore extends UmbStoreBase {
* @memberof UmbDocumentDetailStore
*/
remove(uniques: Array<DocumentResponseModel['id']>) {
this.#data.remove(uniques);
this._data.remove(uniques);
}
}

View File

@@ -90,26 +90,7 @@ export class UmbDocumentServerDataSource
async insert(document: CreateDocumentRequestModel & { id: string }) {
if (!document.id) throw new Error('Id is missing');
let body: string;
try {
body = JSON.stringify(document);
} catch (error) {
console.error(error);
return Promise.reject();
}
//return tryExecuteAndNotify(this.#host, DocumentResource.postDocument(payload));
// TODO: use resources when end point is ready:
return tryExecuteAndNotify<string>(
this.#host,
fetch('/umbraco/management/api/v1/document/save', {
method: 'POST',
body: body,
headers: {
'Content-Type': 'application/json',
},
}) as any
);
return tryExecuteAndNotify(this.#host, DocumentResource.postDocument({ requestBody: document }));
}
/**
@@ -118,33 +99,10 @@ export class UmbDocumentServerDataSource
* @return {*}
* @memberof UmbDocumentServerDataSource
*/
async update(id: string, document: DocumentResponseModel) {
if (!document.id) {
const error: ProblemDetailsModel = { title: 'Document id is missing' };
return { error };
}
//const payload = { id: document.id, requestBody: document };
async update(id: string, document: UpdateDocumentRequestModel) {
if (!id) throw new Error('Id is missing');
let body: string;
try {
body = JSON.stringify(document);
} catch (error) {
const myError: ProblemDetailsModel = { title: 'JSON could not parse' };
return { error: myError };
}
// TODO: use resources when end point is ready:
return tryExecuteAndNotify<DocumentResponseModel>(
this.#host,
fetch('/umbraco/management/api/v1/document/save', {
method: 'POST',
body: body,
headers: {
'Content-Type': 'application/json',
},
}) as any
);
return tryExecuteAndNotify(this.#host, DocumentResource.putDocumentById({ id, requestBody: document }));
}
/**

View File

@@ -6,10 +6,10 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the Document tree that fetches data from the server
* @export
* @class DocumentTreeServerDataSource
* @implements {DocumentTreeDataSource}
* @class UmbDocumentTreeServerDataSource
* @implements {UmbTreeDataSource}
*/
export class DocumentTreeServerDataSource implements UmbTreeDataSource {
export class UmbDocumentTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHostElement;
// TODO: how do we handle trashed items?
@@ -42,9 +42,9 @@ export class DocumentTreeServerDataSource implements UmbTreeDataSource {
}
/**
* Creates an instance of DocumentTreeServerDataSource.
* Creates an instance of UmbDocumentTreeServerDataSource.
* @param {UmbControllerHostElement} host
* @memberof DocumentTreeServerDataSource
* @memberof UmbDocumentTreeServerDataSource
*/
constructor(host: UmbControllerHostElement) {
this.#host = host;
@@ -53,7 +53,7 @@ export class DocumentTreeServerDataSource implements UmbTreeDataSource {
/**
* Fetches the root items for the tree from the server
* @return {*}
* @memberof DocumentTreeServerDataSource
* @memberof UmbDocumentTreeServerDataSource
*/
async getRootItems() {
return tryExecuteAndNotify(this.#host, DocumentResource.getTreeDocumentRoot({}));
@@ -63,7 +63,7 @@ export class DocumentTreeServerDataSource implements UmbTreeDataSource {
* Fetches the children of a given parent id from the server
* @param {(string | null)} parentId
* @return {*}
* @memberof DocumentTreeServerDataSource
* @memberof UmbDocumentTreeServerDataSource
*/
async getChildrenOf(parentId: string | null) {
if (!parentId) {
@@ -83,7 +83,7 @@ export class DocumentTreeServerDataSource implements UmbTreeDataSource {
* Fetches the items for the given ids from the server
* @param {Array<string>} ids
* @return {*}
* @memberof DocumentTreeServerDataSource
* @memberof UmbDocumentTreeServerDataSource
*/
async getItems(ids: Array<string>) {
if (!ids) {

View File

@@ -14,17 +14,6 @@ import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-ap
@customElement('umb-document-workspace-editor')
export class UmbDocumentWorkspaceEditorElement extends UmbLitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
`,
];
//private _defaultVariant?: VariantViewModelBaseModel;
private splitViewElement = new UmbDocumentWorkspaceSplitViewElement();
@@ -132,6 +121,17 @@ export class UmbDocumentWorkspaceEditorElement extends UmbLitElement {
>`
: '';
}
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
`,
];
}
export default UmbDocumentWorkspaceEditorElement;

View File

@@ -10,30 +10,6 @@ import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/context-ap
@customElement('umb-document-workspace-split-view')
export class UmbDocumentWorkspaceSplitViewElement extends UmbLitElement {
static styles = [
UUITextStyles,
css`
:host {
width: 100%;
height: 100%;
display: flex;
flex: 1;
flex-direction: column;
}
#splitViews {
display: flex;
width: 100%;
height: calc(100% - var(--umb-footer-layout-height));
}
#breadcrumbs {
margin: 0 var(--uui-size-layout-1);
}
`,
];
private _workspaceContext?: UmbDocumentWorkspaceContext;
@state()
@@ -82,6 +58,30 @@ export class UmbDocumentWorkspaceSplitViewElement extends UmbLitElement {
</umb-workspace-footer-layout>`
: nothing;
}
static styles = [
UUITextStyles,
css`
:host {
width: 100%;
height: 100%;
display: flex;
flex: 1;
flex-direction: column;
}
#splitViews {
display: flex;
width: 100%;
height: calc(100% - var(--umb-footer-layout-height));
}
#breadcrumbs {
margin: 0 var(--uui-size-layout-1);
}
`,
];
}
export default UmbDocumentWorkspaceSplitViewElement;

View File

@@ -6,7 +6,11 @@ import { UmbVariantId } from '../../../shared/variants/variant-id.class';
import { UmbWorkspacePropertyStructureManager } from '../../../shared/components/workspace/workspace-context/workspace-structure-manager.class';
import { UmbWorkspaceSplitViewManager } from '../../../shared/components/workspace/workspace-context/workspace-split-view-manager.class';
import type { CreateDocumentRequestModel, DocumentResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { partialUpdateFrozenArray, ObjectState, UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import {
partialUpdateFrozenArray,
UmbObjectState,
UmbObserverController,
} from '@umbraco-cms/backoffice/observable-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
// TODO: should this context be called DocumentDraft instead of workspace? or should the draft be part of this?
@@ -22,12 +26,12 @@ export class UmbDocumentWorkspaceContext
* For now lets not share this publicly as it can become confusing.
* TODO: Use this to compare, for variants with changes.
*/
#document = new ObjectState<EntityType | undefined>(undefined);
#document = new UmbObjectState<EntityType | undefined>(undefined);
/**
* The document is the current state/draft version of the document.
*/
#draft = new ObjectState<EntityType | undefined>(undefined);
#draft = new UmbObjectState<EntityType | undefined>(undefined);
readonly unique = this.#draft.getObservablePart((data) => data?.id);
readonly documentTypeKey = this.#draft.getObservablePart((data) => data?.contentTypeId);

View File

@@ -9,8 +9,6 @@ import './document-workspace-editor.element';
@customElement('umb-document-workspace')
export class UmbDocumentWorkspaceElement extends UmbLitElement {
static styles = [UUITextStyles];
#workspaceContext = new UmbDocumentWorkspaceContext(this);
#element = document.createElement('umb-document-workspace-editor');
@@ -40,6 +38,8 @@ export class UmbDocumentWorkspaceElement extends UmbLitElement {
render() {
return html`<umb-router-slot .routes="${this._routes}"></umb-router-slot>`;
}
static styles = [UUITextStyles];
}
export default UmbDocumentWorkspaceElement;

View File

@@ -1,7 +1,7 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
import { ArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import type { MediaTypeDetails } from '@umbraco-cms/backoffice/models';
/**
@@ -11,18 +11,16 @@ import type { MediaTypeDetails } from '@umbraco-cms/backoffice/models';
* @description - Details Data Store for Media Types
*/
export class UmbMediaTypeStore extends UmbStoreBase {
#data = new ArrayState<MediaTypeDetails>([], (x) => x.id);
constructor(host: UmbControllerHostElement) {
super(host, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN.toString());
super(host, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState<MediaTypeDetails>([], (x) => x.id));
}
append(mediaType: MediaTypeDetails) {
this.#data.append([mediaType]);
this._data.append([mediaType]);
}
remove(uniques: string[]) {
this.#data.remove(uniques);
this._data.remove(uniques);
}
}

View File

@@ -1,7 +1,7 @@
import { UmbMediaTypeTreeStore, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './media-type.tree.store';
import { UmbMediaTypeDetailServerDataSource } from './sources/media-type.detail.server.data';
import { UmbMediaTypeStore, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN } from './media-type.detail.store';
import { MediaTypeTreeServerDataSource } from './sources/media-type.tree.server.data';
import { UmbMediaTypeTreeServerDataSource } from './sources/media-type.tree.server.data';
import { ProblemDetailsModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
@@ -26,7 +26,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository {
this.#host = host;
// TODO: figure out how spin up get the correct data source
this.#treeSource = new MediaTypeTreeServerDataSource(this.#host);
this.#treeSource = new UmbMediaTypeTreeServerDataSource(this.#host);
this.#detailSource = new UmbMediaTypeDetailServerDataSource(this.#host);
this.#init = Promise.all([
@@ -73,7 +73,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository {
return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) };
}
async requestTreeItems(ids: Array<string>) {
async requestItemsLegacy(ids: Array<string>) {
await this.#init;
if (!ids) {
@@ -96,7 +96,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository {
return this.#treeStore!.childrenOf(parentId);
}
async treeItems(ids: Array<string>) {
async itemsLegacy(ids: Array<string>) {
await this.#init;
return this.#treeStore!.items(ids);
}

View File

@@ -6,10 +6,10 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the MediaType tree that fetches data from the server
* @export
* @class MediaTypeTreeServerDataSource
* @implements {MediaTypeTreeDataSource}
* @class UmbMediaTypeTreeServerDataSource
* @implements {UmbTreeDataSource}
*/
export class MediaTypeTreeServerDataSource implements UmbTreeDataSource {
export class UmbMediaTypeTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHostElement;
/**
@@ -24,7 +24,7 @@ export class MediaTypeTreeServerDataSource implements UmbTreeDataSource {
/**
* Fetches the root items for the tree from the server
* @return {*}
* @memberof MediaTypeTreeServerDataSource
* @memberof UmbMediaTypeTreeServerDataSource
*/
async getRootItems() {
return tryExecuteAndNotify(this.#host, MediaTypeResource.getTreeMediaTypeRoot({}));
@@ -34,7 +34,7 @@ export class MediaTypeTreeServerDataSource implements UmbTreeDataSource {
* Fetches the children of a given parent id from the server
* @param {(string | null)} parentId
* @return {*}
* @memberof MediaTypeTreeServerDataSource
* @memberof UmbMediaTypeTreeServerDataSource
*/
async getChildrenOf(parentId: string | null) {
if (!parentId) {
@@ -54,7 +54,7 @@ export class MediaTypeTreeServerDataSource implements UmbTreeDataSource {
* Fetches the items for the given ids from the server
* @param {Array<string>} ids
* @return {*}
* @memberof MediaTypeTreeServerDataSource
* @memberof UmbMediaTypeTreeServerDataSource
*/
async getItems(ids: Array<string>) {
if (!ids || ids.length === 0) {

View File

@@ -2,7 +2,7 @@ import { UmbWorkspaceContext } from '../../../shared/components/workspace/worksp
import { UmbMediaTypeRepository } from '../repository/media-type.repository';
import { UmbEntityWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
import { ObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import type { MediaTypeDetails } from '@umbraco-cms/backoffice/models';
type EntityType = MediaTypeDetails;
@@ -10,7 +10,7 @@ export class UmbWorkspaceMediaTypeContext
extends UmbWorkspaceContext<UmbMediaTypeRepository, EntityType>
implements UmbEntityWorkspaceContextInterface<EntityType | undefined>
{
#data = new ObjectState<MediaTypeDetails | undefined>(undefined);
#data = new UmbObjectState<MediaTypeDetails | undefined>(undefined);
data = this.#data.asObservable();
name = this.#data.getObservablePart((data) => data?.name);

View File

@@ -21,7 +21,7 @@ export class UmbMediaTrashEntityBulkAction extends UmbEntityBulkActionBase<UmbMe
if (!this.#modalContext || !this.repository) return;
// TODO: should we subscribe in cases like this?
const { data } = await this.repository.requestTreeItems(this.selection);
const { data } = await this.repository.requestItemsLegacy(this.selection);
if (data) {
// TODO: use correct markup

View File

@@ -1,5 +1,5 @@
import type { MediaDetails } from '../';
import { MediaTreeServerDataSource } from './sources/media.tree.server.data';
import { UmbMediaTreeServerDataSource } from './sources/media.tree.server.data';
import { UmbMediaTreeStore, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN } from './media.tree.store';
import { UmbMediaStore, UMB_MEDIA_STORE_CONTEXT_TOKEN } from './media.store';
import { UmbMediaDetailServerDataSource } from './sources/media.detail.server.data';
@@ -36,7 +36,7 @@ export class UmbMediaRepository
this.#host = host;
// TODO: figure out how spin up get the correct data source
this.#treeSource = new MediaTreeServerDataSource(this.#host);
this.#treeSource = new UmbMediaTreeServerDataSource(this.#host);
this.#detailDataSource = new UmbMediaDetailServerDataSource(this.#host);
new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => {
@@ -96,7 +96,7 @@ export class UmbMediaRepository
return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) };
}
async requestTreeItems(ids: Array<string>) {
async requestItemsLegacy(ids: Array<string>) {
await this.#init;
if (!ids) {
@@ -119,7 +119,7 @@ export class UmbMediaRepository
return this.#treeStore!.childrenOf(parentId);
}
async treeItems(ids: Array<string>) {
async itemsLegacy(ids: Array<string>) {
await this.#init;
return this.#treeStore!.items(ids);
}

View File

@@ -1,6 +1,6 @@
import type { MediaDetails } from '../';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { ArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
@@ -11,15 +11,13 @@ import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
* @description - Data Store for Template Details
*/
export class UmbMediaStore extends UmbStoreBase {
#data = new ArrayState<MediaDetails>([], (x) => x.id);
/**
* Creates an instance of UmbMediaStore.
* @param {UmbControllerHostElement} host
* @memberof UmbMediaStore
*/
constructor(host: UmbControllerHostElement) {
super(host, UMB_MEDIA_STORE_CONTEXT_TOKEN.toString());
super(host, UMB_MEDIA_STORE_CONTEXT_TOKEN.toString(), new UmbArrayState<MediaDetails>([], (x) => x.id));
}
/**
@@ -28,7 +26,7 @@ export class UmbMediaStore extends UmbStoreBase {
* @memberof UmbMediaStore
*/
append(media: MediaDetails) {
this.#data.append([media]);
this._data.append([media]);
}
/**
@@ -37,7 +35,7 @@ export class UmbMediaStore extends UmbStoreBase {
* @memberof UmbMediaStore
*/
remove(uniques: string[]) {
this.#data.remove(uniques);
this._data.remove(uniques);
}
}

View File

@@ -1,6 +1,6 @@
import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { ArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller';
@@ -13,7 +13,7 @@ export const UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMediaTr
* @description - Tree Data Store for Media
*/
export class UmbMediaTreeStore extends UmbEntityTreeStore {
#data = new ArrayState<EntityTreeItemResponseModel>([], (x) => x.id);
#data = new UmbArrayState<EntityTreeItemResponseModel>([], (x) => x.id);
/**
* Creates an instance of UmbMediaTreeStore.

Some files were not shown because too many files have changed in this diff Show More