Merge branch 'main' into feature/rich-text-editor
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
1
src/Umbraco.Web.UI.Client/libs/picker-input/index.ts
Normal file
1
src/Umbraco.Web.UI.Client/libs/picker-input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './picker-input.context';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UmbRepositoryResponse } from './detail-repository.interface';
|
||||
|
||||
export interface UmbCopyRepository {
|
||||
copy(unique: string, targetUnique: string): Promise<UmbRepositoryResponse<string>>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository';
|
||||
|
||||
export interface UmbCopyDataSource {
|
||||
copy(unique: string, targetUnique: string): Promise<DataSourceResponse<string>>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { DataSourceResponse } from '@umbraco-cms/backoffice/repository';
|
||||
|
||||
export interface UmbItemDataSource<ItemType> {
|
||||
getItems(unique: Array<string>): Promise<DataSourceResponse<Array<ItemType>>>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { UmbDataSourceErrorResponse } from '@umbraco-cms/backoffice/repository';
|
||||
|
||||
export interface UmbMoveDataSource {
|
||||
move(unique: string, targetUnique: string): Promise<UmbDataSourceErrorResponse>;
|
||||
}
|
||||
@@ -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>>>;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>>>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UmbRepositoryErrorResponse } from './detail-repository.interface';
|
||||
|
||||
export interface UmbMoveRepository {
|
||||
move(unique: string, targetUnique: string): Promise<UmbRepositoryErrorResponse>;
|
||||
}
|
||||
@@ -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[]>>;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
1
src/Umbraco.Web.UI.Client/libs/sorter/index.ts
Normal file
1
src/Umbraco.Web.UI.Client/libs/sorter/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sorter.controller';
|
||||
685
src/Umbraco.Web.UI.Client/libs/sorter/sorter.angular.txt
Normal file
685
src/Umbraco.Web.UI.Client/libs/sorter/sorter.angular.txt
Normal 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);
|
||||
})();
|
||||
@@ -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...
|
||||
});
|
||||
838
src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.ts
Normal file
838
src/Umbraco.Web.UI.Client/libs/sorter/sorter.controller.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 ?? '')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? '')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
5
src/Umbraco.Web.UI.Client/libs/store/store.interface.ts
Normal file
5
src/Umbraco.Web.UI.Client/libs/store/store.interface.ts
Normal 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;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: delete when the last usages are gone
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
export interface UmbDataStoreIdentifiers {
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
|
||||
380
src/Umbraco.Web.UI.Client/package-lock.json
generated
380
src/Umbraco.Web.UI.Client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user