rmove stories regarding extensions. This has been transfered to Docs now.
This commit is contained in:
@@ -1,126 +0,0 @@
|
||||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
<Meta title="Guides/Context API" />
|
||||
|
||||
# Context API
|
||||
|
||||
The Context API enables connections between Elements and APIs.
|
||||
DOM structure defines the context of which an API is exposed for. APIs are provided via an element and can then be consumed by any decending element.
|
||||
|
||||
### Consume a Context API.
|
||||
|
||||
From a Umbraco Element or Umbraco Controller:
|
||||
|
||||
```ts
|
||||
this.consumeContext('requestThisContextAlias', (context) => {
|
||||
// Notice this is a subscription, as context might change or a new one appears.
|
||||
console.log("I've got the context", context);
|
||||
});
|
||||
```
|
||||
|
||||
Or with a Controller using a 'host' reference to Controller Host(Thats either a Umbraco Element or just another Controller):
|
||||
|
||||
```ts
|
||||
new UmbContextConsumerController(host, 'requestThisContextAlias', (context) => {
|
||||
// Notice this is a subscription, as context might change or a new one appears.
|
||||
console.log("I've got the context", context);
|
||||
});
|
||||
```
|
||||
|
||||
#### Context Token
|
||||
|
||||
Using a Context Token gives you a typed context:
|
||||
|
||||
```ts
|
||||
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
|
||||
|
||||
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => {
|
||||
// Notice this is a subscription, as context might change or a new one appears, but the value is strongly typed
|
||||
console.log("I've got the context of the right type", context);
|
||||
});
|
||||
```
|
||||
|
||||
#### Write your own Context Token
|
||||
|
||||
A Context Token is generally just a string matched with a type. In this way users of the token can be sure to get the right type of context.
|
||||
|
||||
```ts
|
||||
import { ContextToken } from '@umbraco-cms/backoffice/context';
|
||||
|
||||
type MyContext = {
|
||||
foo: string;
|
||||
bar: number;
|
||||
};
|
||||
|
||||
const MY_CONTEXT = new ContextToken<MyContext>('My.Context.Token');
|
||||
```
|
||||
|
||||
#### Context Token with discriminator.
|
||||
|
||||
Notice this is only relevant if you are going to make multiple context API for the same context.
|
||||
|
||||
In some cases we need to have different APIs for the same context. Our Workspace Contexts is a good example of this.
|
||||
|
||||
If someone wants the workspace name, they might not care about the specific API of the Workspace Context. These implementations can use a standard Context Token with a type of a generic Workspace Context.
|
||||
|
||||
Our Document Workspace Context, has features around Publishing. We do not want a new Context for these features, as we want to make sure when we are in a Workspace, we do not accidentally retrieve workspace context of a parent workspace. So we need to provide a workspace context in each workspace, the one we retrieve is the one we will be using.
|
||||
But since publishing is not part of the generic Workspace Context, we need to identify if the context is a Document Workspace Context and then recast it.
|
||||
|
||||
To avoid each implementation taking care of this, Context Tokens can be extended with a type discriminator.
|
||||
This will dicard the given api if it does not live up to the needs, and when it is the decired type, it will cast the api to the desired type.
|
||||
|
||||
This example, shows how to create a discriminator Context Token, that will discard the api if it is not a Publishable Context:
|
||||
|
||||
Context token example:
|
||||
|
||||
```ts
|
||||
import { ContextToken } from '@umbraco-cms/backoffice/context';
|
||||
|
||||
interface MyBaseContext {
|
||||
foo: string;
|
||||
bar: number;
|
||||
}
|
||||
|
||||
interface MyPublishableContext extends MyBaseContext {
|
||||
publish();
|
||||
}
|
||||
|
||||
const MY_PUBLISHABLE_CONTEXT = new ContextToken<MyContext, MyPublishableContext>(
|
||||
'My.Context.Token',
|
||||
(context): context is MyPublishableContext => {
|
||||
return 'publish' in context;
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
Implementation of context token example:
|
||||
|
||||
```ts
|
||||
const contextElement = new UmbLitElement();
|
||||
contextElement.provideContext(MY_PUBLISHABLE_CONTEXT, new MyPublishableContext());
|
||||
|
||||
const consumerElement = new UmbLitElement();
|
||||
contextElement.appendChild(contextElement);
|
||||
consumerElement.consumeContext(MY_PUBLISHABLE_CONTEXT, (context) => {
|
||||
// context is of type 'MyPublishableContext'
|
||||
console.log("I've got the context of the right type", context);
|
||||
});
|
||||
```
|
||||
|
||||
This enables implementors to request a publishable context, without the knowledge about how do identify such, neither they need to know about the Type.
|
||||
|
||||
In details, the Context API will find the first API matching alias 'My.Context.Token', and never look furhter. If that API does live up to the type discriminator, it will be returned. If not the consumer will never reply.
|
||||
|
||||
### Provide a Context API.
|
||||
|
||||
From a Umbraco Element or Umbraco Controller:
|
||||
|
||||
```ts
|
||||
this.provideContext('myContextAlias', new MyContextApi());
|
||||
```
|
||||
|
||||
Or with a Controller using a 'host' reference to Controller Host(Umbraco Element/Controller):
|
||||
|
||||
```ts
|
||||
new UmbContextProviderController(host, 'myContextAlias', new MyContextApi());
|
||||
```
|
||||
@@ -1,216 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Entity Actions"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Entity Actions
|
||||
|
||||
TODO: introduction to actions
|
||||
|
||||
- **Entity Action:**
|
||||
Relates to an entity type: (document, media, etc.). Performs the action on a specific item.
|
||||
|
||||
- **Entity Bulk Action:**
|
||||
Relates to an entity type: document, media, etc. Performs the action on a selection of items.
|
||||
|
||||
## Entity Actions in the UI
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
<strong>Sidebar Context Menu</strong>
|
||||
</div>
|
||||
<img src="docs/entity-action-sidebar-context.svg" width="400" />
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<strong>Workspace Entity Action Menu</strong>
|
||||
</div>
|
||||
<img src="docs/entity-action-workspace-menu.svg" width="400" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
<strong>Collection</strong>
|
||||
</div>
|
||||
<img src="docs/entity-action-collection-menu.svg" width="400" />
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<strong>Pickers</strong>
|
||||
</div>
|
||||
<img src="docs/entity-action-picker-context-menu.svg" width="400" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Registering an Entity Action
|
||||
|
||||
TODO: can we show the typescript interface for the manifest?
|
||||
|
||||
```javascript
|
||||
import { extensionRegistry } from '@umbraco-cms/extension-registry';
|
||||
import { MyEntityAction } from './entity-action';
|
||||
|
||||
const manifest = {
|
||||
type: 'entityAction',
|
||||
alias: 'My.EntityAction',
|
||||
name: 'My Entity Action',
|
||||
weight: 10,
|
||||
api: MyEntityAction,
|
||||
forEntityTypes: ['my-entity'],
|
||||
meta: {
|
||||
icon: 'icon-add',
|
||||
label: 'My Entity Action',
|
||||
repositoryAlias: 'My.Repository',
|
||||
},
|
||||
};
|
||||
|
||||
extensionRegistry.register(manifest);
|
||||
```
|
||||
|
||||
**Default Element**
|
||||
|
||||
```ts
|
||||
// TODO: get interface
|
||||
interface UmbEntityActionElement {}
|
||||
```
|
||||
|
||||
### The Entity Action Class
|
||||
|
||||
As part of the Extension Manifest you can attach a class that will be instanciated as part of the action.
|
||||
It will have access to the host element, a repository with the given alias and the unique (key etc) of the entity.
|
||||
|
||||
The class either provides a getHref method, or an execute method. If the getHref method is provided, the action will use the link. Otherwise the `execute` method will be used.
|
||||
When the action is clicked the `execute` method on the api class will be run. When the action is completed, an event on the host element will be dispatched to notify any surrounding elements.
|
||||
|
||||
Example of providing a `getHref` method:
|
||||
|
||||
```ts
|
||||
import { UmbEntityActionBase } from '@umbraco-cms/entity-action';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/controller';
|
||||
import type { MyRepository } from './my-repository';
|
||||
|
||||
export class MyEntityAction extends UmbEntityActionBase<MyRepository> {
|
||||
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
|
||||
super(host, repositoryAlias, unique);
|
||||
}
|
||||
|
||||
async getHref() {
|
||||
return 'my-link/path-to-something';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example of providing a `execute` method:
|
||||
|
||||
```ts
|
||||
import { UmbEntityActionBase } from '@umbraco-cms/entity-action';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/controller';
|
||||
import type { MyRepository } from './my-repository';
|
||||
|
||||
export class MyEntityAction extends UmbEntityActionBase<MyRepository> {
|
||||
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
|
||||
super(host, repositoryAlias, unique);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
await this.repository.myAction(this.unique);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If any additional contexts are needed, these can be consumed from the host element:
|
||||
|
||||
```ts
|
||||
import { UmbEntityActionBase } from '@umbraco-cms/entity-action';
|
||||
import { UmbContextConsumerController } from '@umbraco-cms/controller';
|
||||
import { UMB_MODAL_SERVICE_CONTEXT } from '@umbraco-cms/modal';
|
||||
import { MyRepository } from './my-repository';
|
||||
|
||||
export class MyEntityAction extends UmbEntityActionBase<MyRepository> {
|
||||
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
|
||||
super(host, repositoryAlias, unique);
|
||||
|
||||
new UmbContextConsumerController(this.host, UMB_MODAL_SERVICE_CONTEXT, (instance) => {
|
||||
this.#modalService = instance;
|
||||
});
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
We currently have a couple of generic actions that can be used across silos, so we don't have to write the same logic again: copy, move, trash, delete, etc. We can add more as we discover the needs.
|
||||
|
||||
TODO: List generic actions + List what alias' they are registered under.
|
||||
|
||||
## Entity Bulk Actions in the UI
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
<strong>Collection</strong>
|
||||
</div>
|
||||
<img src="docs/entity-bulk-action-collection-menu.svg" width="400" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Registering an Entity Bulk Action
|
||||
|
||||
TODO: can we show the typescript interface for the manifest?
|
||||
|
||||
```javascript
|
||||
import { extensionRegistry } from '@umbraco-cms/extension-registry';
|
||||
import { MyEntityBulkAction } from './entity-bulk-action';
|
||||
|
||||
const manifest = {
|
||||
type: 'entityBulkAction',
|
||||
alias: 'My.EntityBulkAction',
|
||||
name: 'My Entity Bulk Action',
|
||||
weight: 10,
|
||||
api: MyEntityBulkAction,
|
||||
meta: {
|
||||
icon: 'icon-add',
|
||||
label: 'My Entity Bulk Action',
|
||||
repositoryAlias: 'My.Repository',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.CollectionAlias',
|
||||
match: 'my-collection-alias',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
extensionRegistry.register(manifest);
|
||||
```
|
||||
|
||||
### The Entity Bulk Action Class
|
||||
|
||||
As part of the Extension Manifest you can attach a class that will be instanciated as part of the action. It will have access to the host element, a repository with the given alias and the unique (key etc) of the entity. When the action is clicked the `execute` method on the api class will be run. When the action is completed, an event on the host element will be dispatched to notify any surrounding elements.
|
||||
|
||||
```ts
|
||||
import { UmbEntityBulkActionBase } from '@umbraco-cms/entity-action';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/controller';
|
||||
import { MyRepository } from './my-repository';
|
||||
|
||||
export class MyEntityBulkAction extends UmbEntityBulkActionBase<MyRepository> {
|
||||
constructor(host: UmbControllerHostElement, repositoryAlias: string, selection: Array<string>) {
|
||||
super(host, repositoryAlias, selection);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
await this.repository?.myBulkAction(this.selection);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Guides/Extending the Backoffice/Header Apps" parameters={{ previewTabs: { canvas: { hidden: true } } }} />
|
||||
|
||||
# Header Apps
|
||||
|
||||
TODO: Describe header apps
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="docs/header-apps.svg" width="400" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
**Manifest**
|
||||
|
||||
```json
|
||||
// TODO: get interface
|
||||
{
|
||||
"type": "headerApp",
|
||||
"alias": "My.HeaderApp",
|
||||
"name": "My Header App",
|
||||
"elementName": "my-header-app",
|
||||
"js": "./my-header-app.js",
|
||||
"weight": 10,
|
||||
"meta": {
|
||||
"label": "My Header App",
|
||||
"icon": "icon-name",
|
||||
"pathname": "search",
|
||||
},
|
||||
},
|
||||
|
||||
```
|
||||
|
||||
**Default Element**
|
||||
|
||||
```ts
|
||||
// TODO: get interface
|
||||
interface UmbHeaderAppElement {}
|
||||
```
|
||||
|
||||
#### Button Header App
|
||||
|
||||
**Manifest**
|
||||
|
||||
```json
|
||||
// TODO: get interface
|
||||
{}
|
||||
```
|
||||
|
||||
**Default Element**
|
||||
|
||||
```ts
|
||||
// TODO: get interface
|
||||
interface UmbButtonHeaderAppElement {}
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Guides/Extending the Backoffice/Intro" parameters={{ previewTabs: { canvas: { hidden: true } } }} />
|
||||
|
||||
# UI Extensions
|
||||
|
||||
TODO: introduction to the extendable UI
|
||||
|
||||
The Umbraco Backoffice currently support the following extension types:
|
||||
|
||||
- [Registration](?path=/docs/guides-extending-the-backoffice-registration-intro--docs)
|
||||
- [Header App](?path=/docs/guides-extending-the-backoffice-header-apps--docs)
|
||||
- [Section](?path=/docs/guides-extending-the-backoffice-sections--docs)
|
||||
- [Entity Action](?path=/docs/guides-extending-the-backoffice-entity-actions--docs)
|
||||
- [Workspace](?path=/docs/guides-extending-the-backoffice-workspaces-intro--docs)
|
||||
- [Property Editor](?path=/docs/guides-extending-the-backoffice-property-editors--docs)
|
||||
- [Repository](?path=/docs/guides-extending-the-backoffice-property-editors--docs)
|
||||
- [Menu](?path=/docs/guides-extending-the-backoffice-menu--docs)
|
||||
- [Tree](?path=/docs/guides-extending-the-backoffice-trees--docs)
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Guides/Extending the Backoffice/Menu" parameters={{ previewTabs: { canvas: { hidden: true } } }} />
|
||||
|
||||
# Menu
|
||||
|
||||
TODO: introduction to the menu
|
||||
The menu is still work in progress
|
||||
|
||||
<img src="docs/menu.svg" width="150" />
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "menu",
|
||||
"alias": "My.Menu",
|
||||
"name": "My Menu"
|
||||
}
|
||||
```
|
||||
|
||||
### Menu Item
|
||||
|
||||
TODO: introduction to the menu item
|
||||
|
||||
<img src="docs/menu-item.svg" width="150" />
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "menuItem",
|
||||
"alias": "My.MenuItem",
|
||||
"name": "My Menu Item",
|
||||
"meta": {
|
||||
"label": "My Menu Item",
|
||||
"menus": ["My.Menu"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Tree Menu Item
|
||||
|
||||
// TODO adds docs when we have extension kinds
|
||||
|
||||
**Manifest**
|
||||
|
||||
```json
|
||||
// it will be something like this
|
||||
{
|
||||
"type": "menuItem",
|
||||
"kind": "tree",
|
||||
"alias": "My.TreeMenuItem",
|
||||
"name": "My Tree Menu Item",
|
||||
"meta": {
|
||||
"label": "My Tree Menu Item",
|
||||
"menus": ["My.Menu"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Default Element**
|
||||
|
||||
```ts
|
||||
// get interface
|
||||
interface UmbTreeMenuItemElement {}
|
||||
```
|
||||
|
||||
#### Adding menu items to an existing menu
|
||||
|
||||
The backoffice comes with a couple of menus.
|
||||
|
||||
- Content, Media, Settings, Templating, Dictionary, etc.
|
||||
|
||||
To add a menu item to an existing menu, you can use the `meta.menus` property.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "menuItem",
|
||||
"alias": "My.MenuItem",
|
||||
"name": "My Menu Item",
|
||||
"meta": {
|
||||
"label": "My Menu Item",
|
||||
"menus": ["Umb.Menu.Content"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Guides/Extending the Backoffice/Modals/Intro" parameters={{ previewTabs: { canvas: { hidden: true } } }} />
|
||||
|
||||
# Modals
|
||||
|
||||
// TODO: add description about what modals is
|
||||
|
||||
- Sidebar
|
||||
- Infinite Editors
|
||||
- Dialogs
|
||||
|
||||
## Define a Manifest for a Modal Type
|
||||
|
||||
### Manifest
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "modal",
|
||||
"alias": "Our.Modal.SomethingPicker",
|
||||
"name": "My Something Picker Modal",
|
||||
"js": "./my-something-picker-modal-element.js",
|
||||
},
|
||||
```
|
||||
|
||||
## Implement a Modal
|
||||
|
||||
### Modal Token
|
||||
|
||||
For type safety, we recommend that you make a Modal Token, It's possible to go without.
|
||||
The Modal Token binds the Modal Type to the Modal Data Type and Modal Value Type.
|
||||
|
||||
``
|
||||
|
||||
```ts
|
||||
import { ModalToken } from '@umbraco-cms/element';
|
||||
|
||||
export type OurSomethingPickerModalData = {
|
||||
key: string | null;
|
||||
};
|
||||
|
||||
export type OurSomethingPickerModalValue = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
export const MY_SOMETHING_PICKER_MODAL = new UmbModalToken<UmbLinkPickerModalData, UmbLinkPickerModalValue>(
|
||||
'Our.Modal.SomethingPicker',
|
||||
{
|
||||
config: {
|
||||
type: 'sidebar',
|
||||
size: 'small',
|
||||
},
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Make a modal registration
|
||||
|
||||
# TODO Link to modal documentation here.
|
||||
@@ -1,137 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Property Editors"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Property Editors
|
||||
|
||||
// TODO: add description
|
||||
(rough notes. We have a lot on docs.umbraco.com we can use)
|
||||
|
||||
- This section describes how to work with and create Property Editors. A property editor is the editor used to insert content into Umbraco.
|
||||
- A whole Property Editor is defined by two parts.
|
||||
- The Property Editor and the Property Editor UI.
|
||||
- The Property Editor UI is the UI that is used to edit the data in the backoffice.
|
||||
- Each Property Editor can have multiple Property Editor UIs.
|
||||
|
||||
- Both a Property Editor Schema and Property Editor UI can define the Settings used for their configuration.
|
||||
- The Property Editor Schema settings is used for configuration that the server needs to know about.
|
||||
- The Property Editor UI settings is used for configuration that is related to rendering the UI in the backoffice.
|
||||
- Both will be available for the Property Editor UI to use.
|
||||
|
||||
## Property Editor Schema
|
||||
|
||||
**Manifest**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "propertyEditorSchema",
|
||||
"name": "Text Box",
|
||||
"alias": "Umbraco.TextBox",
|
||||
};
|
||||
```
|
||||
|
||||
## Property Editor UI
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "propertyEditorUi",
|
||||
"alias": "Umb.PropertyEditorUi.TextBox",
|
||||
"name": "Text Box Property Editor UI",
|
||||
"elementName": "my-text-box",
|
||||
"js": "./my-text-box.element.js",
|
||||
"meta": {
|
||||
"label": "My Text Box",
|
||||
"propertyEditorSchema": "Umbraco.TextBox",
|
||||
"icon": "icon-autofill",
|
||||
"group": "common"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no Property Editor Schema is specified in the manifest, the Propety Editor UI will use a generic JSON Property Editor Model
|
||||
|
||||
## Configuration
|
||||
|
||||
// TODO: add description
|
||||
|
||||
- Data Type Settings for a Property Editor or Property Editor UI is defined in the manifests.
|
||||
- They both use the same format for their settings.
|
||||
|
||||
**Manifest**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "propertyEditorUi",
|
||||
"alias": "My.PropertyEditorUI.TextArea",
|
||||
//... more
|
||||
"meta": {
|
||||
//... more
|
||||
"settings": {
|
||||
"properties": [
|
||||
{
|
||||
"alias": "rows",
|
||||
"label": "Number of rows",
|
||||
"description": "If empty - 10 rows would be set as the default value",
|
||||
"propertyEditorUi": "Umb.PropertyEditorUi.Number",
|
||||
},
|
||||
],
|
||||
"defaultData": [
|
||||
{
|
||||
"alias": "rows",
|
||||
"value": 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## The Property Editor UI Element
|
||||
|
||||
```ts
|
||||
// TODO: get interface
|
||||
interface UmbPropertyEditorUIElement {}
|
||||
```
|
||||
|
||||
**Example with LitElement**
|
||||
|
||||
```ts
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UmbElementMixin } from '@umbraco-cms/element';
|
||||
|
||||
// TODO: should we make examples with LitElement or just vanilla JS? or should we have for more libraries?
|
||||
@customElement('my-text-box')
|
||||
export class UmbPropertyEditorUITextBoxElement
|
||||
extends UmbElementMixin(LitElement)
|
||||
implements UmbPropertyEditorUIElement
|
||||
{
|
||||
static styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
uui-input {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@property()
|
||||
value = '';
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
public config?: UmbPropertyEditorConfigCollection;
|
||||
|
||||
private onInput(e: InputEvent) {
|
||||
this.value = (e.target as HTMLInputElement).value;
|
||||
this.dispatchEvent(new CustomEvent('property-value-change'));
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<uui-input .value=${this.value} type="text" @input=${this.onInput}></uui-input>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Registration/Conditions"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
## UI Extension Conditions
|
||||
|
||||
Extension conditions are used to determine if an extension should be used or not.
|
||||
Many of the Extension Types supports conditions, but not all of them.
|
||||
|
||||
All given conditions for an extension must be valid for an extension to be utilized.
|
||||
|
||||
### Using conditions
|
||||
|
||||
In this following example we define the manifest for a Workspace Action, this action will only be available in the workspace with the alias `My.Example.Workspace`.
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: 'workspaceAction',
|
||||
name: 'example-workspace-action',
|
||||
alias: 'My.Example.WorkspaceAction',
|
||||
elementName: 'my-workspace-action-element',
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.SectionAlias',
|
||||
match: 'My.Example.Workspace'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The conditions are defined as an array of conditions.
|
||||
Each condition is an object with the following properties:
|
||||
|
||||
- `alias`- The alias of the condition to utilize.
|
||||
- `...` - The rest of the properties of the object are specific to the condition.
|
||||
|
||||
In the above example the `Umb.Condition.SectionAlias` condition is used, this condition takes a property `match` which must be set to the alias of the section to match.
|
||||
|
||||
### Core conditions types
|
||||
|
||||
The following conditions are available out of the box, for all extension types that support conditions.
|
||||
|
||||
- `Umb.Condition.SectionAlias` - Checks if the current section alias matches the one specified.
|
||||
- `Umb.Condition.WorkspaceAlias` - Checks if the current workspace alias matches the one specified.
|
||||
|
||||
### Make your own conditions
|
||||
|
||||
You can make your own conditions by creating a class that implements the `UmbExtensionCondition` interface.
|
||||
|
||||
```typescript
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import {
|
||||
ManifestCondition,
|
||||
UmbConditionConfigBase,
|
||||
UmbConditionControllerArguments,
|
||||
UmbExtensionCondition,
|
||||
} from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section';
|
||||
|
||||
type MyConditionConfig = UmbConditionConfigBase & {
|
||||
match: string;
|
||||
};
|
||||
|
||||
export class MyExtensionCondition extends UmbControllerBase implements UmbExtensionCondition {
|
||||
config: MyConditionConfig;
|
||||
permitted = false;
|
||||
|
||||
constructor(args: UmbConditionControllerArguments<MyConditionConfig>) {
|
||||
super(args.host);
|
||||
// This condition aproves after 10 seconds
|
||||
setTimeout(() => {
|
||||
this.permitted = strue;
|
||||
args.onChange();
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This has to be registered in the extension registry, like this:
|
||||
|
||||
TODO: Make an example that will work from JSON (not a direct reference to the class).
|
||||
|
||||
```typescript
|
||||
export const manifest: ManifestCondition = {
|
||||
type: 'condition',
|
||||
name: 'My Condition',
|
||||
alias: 'My.Condition.TenSecondDelay',
|
||||
api: MyExtensionCondition,
|
||||
};
|
||||
```
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Registration/Intro"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Extension Registry
|
||||
|
||||
TODO: describe the registration process and the extension registry.
|
||||
TODO: add typescript interface
|
||||
|
||||
Most of BackOffice is based on Extensions making it crucial to understand how to register your own extensions.
|
||||
This introduction will give you an outline of the abilities with the extension registry.
|
||||
|
||||
## Registration
|
||||
|
||||
The extension registry is a global registry that can be accessed and changed at anytime while Backoffice is running.
|
||||
|
||||
To provide new UI to backoffice, you need to register them via a extension manifest.
|
||||
This has to initially hapen on the server, via a JSON Package Manifest.
|
||||
This will enable you to registere one or more extensions.
|
||||
|
||||
[Read more about registration here](?path=/docs/guides-extending-the-backoffice-registration-registration--docs)
|
||||
|
||||
## Extension types
|
||||
|
||||
The abilities of the extensions relies of the speicfic extension type.
|
||||
The Type sets the scene for what the extension can do and what it needs to be utilized.
|
||||
|
||||
Some extension types rely on a reference to other extensions.
|
||||
|
||||
[Read about the different core extension types here](?path=/docs/guides-extending-the-backoffice-registration-types-index--docs)
|
||||
[Learn how to develop your own extension types here (TBD)](?path=/docs/guides-extending-the-backoffice-registration-registration--docs)
|
||||
|
||||
## Conditions
|
||||
|
||||
Most extension types supports conditions.
|
||||
Defining conditions enables you to control when and where the extension is available.
|
||||
|
||||
[Read more about the core conditions here](?path=/docs/guides-extending-the-backoffice-registration-conditions--docs)
|
||||
|
||||
## Kinds
|
||||
|
||||
The kinds feature enables you to base your extension registration on a preset.
|
||||
A kind provides the base manifest which you like to extend.
|
||||
|
||||
[Read more about kinds here](?path=/docs/guides-extending-the-backoffice-registration-types-kind--docs)
|
||||
|
||||
```ts
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
```
|
||||
|
||||
## Package Manifest
|
||||
|
||||
TODO: Describe the Package Manifest
|
||||
|
||||
```json
|
||||
// show example of package-manifest.json
|
||||
```
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Registration/Registration"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
## UI Extension Registration
|
||||
|
||||
Registering UI extensions happens through the global extension registry.
|
||||
|
||||
There are two ways to register UI extensions:
|
||||
|
||||
1. Via a manifest JSON file on the server.
|
||||
2. In JavaScript code.
|
||||
|
||||
These two options can be combined by defining a small extension in a JSON Manifest, which uses the extension type `entryPoint` or `bundle` to registrer a JS file, which then registers the rest of the extensions.
|
||||
|
||||
### Extension Manifest
|
||||
|
||||
.. Describe the extension manifest format.
|
||||
|
||||
insert example here.
|
||||
|
||||
- `type` - The type define the type of extension. The type is used to determine where extension is will be used, this defines the data needed for this manifest.
|
||||
- `alias`- The alias is used to identify the extension. This has to be unique for each extension.
|
||||
- `name` - The name of the extension. This is used to identify the extension in the UI.
|
||||
|
||||
### Registration via a Package Manifest
|
||||
|
||||
...Describe and show the pakcage manifest format.
|
||||
|
||||
... Describe the configuration etc.
|
||||
|
||||
### Registering via a Manifest in JS.
|
||||
|
||||
... Describe the JS API for registering extensions. (How to import and method to call)
|
||||
|
||||
Example of manifest registration.
|
||||
|
||||
### Using the Entry Point Extension Type via Pakcage Manifest
|
||||
|
||||
I want us to bring this example as it ties above nicely together.
|
||||
|
||||
### Using the Bundle Extension Type via Pakcage Manifest
|
||||
|
||||
Describe and bring and example of how to make a Package using the bundle extension type.
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Registration/Types/EntryPoint"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Entry Point Manifest Type
|
||||
|
||||
The Entry Point manifest type is used to register an entry point for the backoffice. An entry point is a single javascript file that is loaded when the backoffice is initialized. This file can be used to do anything, this enables more complex logic to take place on startup.
|
||||
|
||||
**_Register an entry point in a JSON manifest_**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "entryPoint",
|
||||
"alias": "My.EntryPoint",
|
||||
"js": "./index.js"
|
||||
}
|
||||
```
|
||||
|
||||
**_Register additional UI extensions in the entry point file_**
|
||||
|
||||
```ts
|
||||
import { extensionRegistry } from "@umbraco-cms/extension-registry"
|
||||
|
||||
const manifest = {
|
||||
{
|
||||
type: '', // type of extension
|
||||
alias: '', // unique alias for the extension
|
||||
elementName: '', // unique name of the custom element
|
||||
js: '', // path to the javascript resource
|
||||
meta: {
|
||||
// additional props for the extension type
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extensionRegistry.register(extension);
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Registration/Types/Index"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Extension Manifest types
|
||||
|
||||
TO BE DONE.
|
||||
@@ -1,95 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Registration/Types/Kind"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Kind Manifest Type
|
||||
|
||||
A kind extension provides the preset for other extensions to use.
|
||||
|
||||
A kind is matched with a specific type, when another extension using that type and kind it will inherit the perset manifest of the kind.
|
||||
|
||||
The registration of Kinds, is done in the same maner as the registration of other extensions.
|
||||
But the format of it is quite different, lets look at the Kind registration of the Header App Button Kind (The kind used in the above example):
|
||||
|
||||
## Kind example
|
||||
|
||||
In the following example a kind is registred. This kind provides a default element for extensions utilizing this kind.
|
||||
|
||||
```ts
|
||||
import { extensionRegistry } from '@umbraco-cms/extension-registry';
|
||||
|
||||
const manifest: ManifestKind = {
|
||||
type: 'kind',
|
||||
alias: 'Umb.Kind.MyButtonKind',
|
||||
matchType: 'headerApp',
|
||||
matchKind: 'button',
|
||||
manifest: {
|
||||
elementName: 'umb-header-app-button',
|
||||
},
|
||||
};
|
||||
|
||||
umbExtensionsRegistry.register(manifest);
|
||||
```
|
||||
|
||||
This eanbles other extensions to use this kind, and inherit the manifest properties defined in the kind.
|
||||
|
||||
In this example a Header App is registered without defining a element, this is posible because the resgistration inherits the elementName from the kind.
|
||||
|
||||
```ts
|
||||
import { extensionRegistry } from '@umbraco-cms/extension-registry';
|
||||
|
||||
const manifest = {
|
||||
type: 'headerApp',
|
||||
kind: 'button',
|
||||
name: 'My Header App Example',
|
||||
alias: 'My.HeaderApp.Example',
|
||||
meta: {
|
||||
label: 'My Example',
|
||||
icon: 'icon-home',
|
||||
href: '/some/path/to/open/when/clicked',
|
||||
},
|
||||
};
|
||||
|
||||
extensionRegistry.register(extension);
|
||||
```
|
||||
|
||||
## Understanding the Kind extension
|
||||
|
||||
The root properties of this object, defines the Kind registration.
|
||||
And then the manifest property holds the preset for the extension using this kind to be based upon.
|
||||
This object can hold the property values that makes sense for the Kind.
|
||||
|
||||
```ts
|
||||
...
|
||||
|
||||
const manifest: ManifestKind = {
|
||||
type: 'kind',
|
||||
alias: 'Umb.Kind.MyButtonKind',
|
||||
matchType: 'headerApp',
|
||||
matchKind: 'button',
|
||||
manifest: {
|
||||
...
|
||||
},
|
||||
};
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
For the kind to be used, it needs to match up with the registration of the extension using it.
|
||||
This happons when the extension uses a type, which matches the value of `matchType` of the Kind.
|
||||
As well the extension has to utilize that kind, by setting the value of `kind` to the value of `matchKind` of the Kind.
|
||||
|
||||
```ts
|
||||
...
|
||||
|
||||
const manifest = {
|
||||
type: 'headerApp',
|
||||
kind: 'button',
|
||||
...
|
||||
};
|
||||
|
||||
...
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Guides/Extending the Backoffice/Repositories" parameters={{ previewTabs: { canvas: { hidden: true } } }} />
|
||||
|
||||
# Repositories
|
||||
|
||||
TODO: make this understandable for others
|
||||
|
||||
A repository is the Backoffices entry point to request data and get notified about updates. Each domain should register their own repository in the Backoffice.
|
||||
|
||||
## Register a repository:
|
||||
|
||||
```js
|
||||
import { umbExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { MyRepository } from './MyRepository';
|
||||
|
||||
const repositoryManifest = {
|
||||
type: 'repository',
|
||||
alias: 'My.Repository',
|
||||
name: 'My Repository',
|
||||
api: MyRepository,
|
||||
};
|
||||
```
|
||||
|
||||
With a repository we can have different data sources depending on the state of the app. It can be from a server, an offline database, a store, a Signal-R connection, etc. That means that the consumer will not have to be concerned how to access the data, add or remove items from a collection of items, etc. This means we get a loose connection between the consumer and the data storing procedures hiding all complex implementation.
|
||||
|
||||
## Data flow with a repository
|
||||
|
||||
<img src="docs/data-flow.svg" width="400" />
|
||||
|
||||
A repository has to be instanced in the context where it is used. It should take a host element as part of the constructor, so any contexts consumed in the repository (notifications, modals, etc.) get rendered in the correct DOM context.
|
||||
|
||||
A repository can be called directly from an element, but will often be instantiated in a context, like the Workspace Context.
|
||||
|
||||
## The Repository Class
|
||||
|
||||
TODO: get typescript interface
|
||||
TODO: show repository example:
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Sections/Intro"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Sections
|
||||
|
||||
TODO: Introduction to sections
|
||||
|
||||
<img src="docs/section.svg" width="400" />
|
||||
|
||||
**Manifest**
|
||||
|
||||
```json
|
||||
// TODO: get interface
|
||||
{
|
||||
"type": "section",
|
||||
"alias": "My.Section",
|
||||
"name": "My Section",
|
||||
"meta": {
|
||||
"label": "My Section",
|
||||
"pathname": "my-section"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Default Element**
|
||||
|
||||
```ts
|
||||
// TODO: get interface
|
||||
interface UmbSectionElement {}
|
||||
```
|
||||
|
||||
## The Section Context
|
||||
|
||||
**Interface**
|
||||
|
||||
```ts
|
||||
// TODO: get interface
|
||||
interface UmbSectionContext {}
|
||||
```
|
||||
|
||||
## Examples of sections:
|
||||
|
||||
TODO: link to all sections in storybook. Can we somehow auto-generate this list?
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Sections/Sidebar"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Section Sidebar
|
||||
|
||||
TODO: Introduction to section sidebar
|
||||
|
||||
<img src="docs/section-sidebar.svg" width="400" />
|
||||
|
||||
### Section Sidebar Apps
|
||||
|
||||
TODO: Introduction to Section Sidebar Apps
|
||||
|
||||
<img src="docs/section-sidebar-apps.svg" width="400" />
|
||||
|
||||
**Manifest**
|
||||
|
||||
```json
|
||||
// TODO: add interface
|
||||
{
|
||||
"type": "sectionSidebarApp",
|
||||
"alias": "My.SectionSidebarApp",
|
||||
"name": "My Section Sidebar App",
|
||||
"meta": {
|
||||
"sections": ["My.Section"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Default Element**
|
||||
|
||||
```ts
|
||||
// TODO: get interface
|
||||
interface UmbSectionSidebarAppElement {}
|
||||
```
|
||||
|
||||
#### Menu Sidebar App
|
||||
|
||||
TODO: Introduction to the sidebar menu
|
||||
|
||||
(rough notes)
|
||||
|
||||
- The Backoffice comes with a menu sidebar app that can be used to create a menu in the sidebar.
|
||||
- To register a new menu sidebar app, add the following to your manifest
|
||||
- The menu sidebar app will reference a menu that you have registered in the menu with a menu manifest
|
||||
|
||||
<img src="docs/section-menu-sidebar-app.svg" width="400" />
|
||||
|
||||
**Manifest**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "menuSectionSidebarApp",
|
||||
"alias": "My.SectionSidebarApp.MyMenu",
|
||||
"name": "My Menu Section Sidebar App",
|
||||
"meta": {
|
||||
"label": "My Sidebar Menu",
|
||||
"sections": ["My.Section"],
|
||||
"menu": "My.Menu"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Default Element**
|
||||
|
||||
```ts
|
||||
// TODO: get interface
|
||||
interface UmbMenuSectionSidebarAppElement {}
|
||||
```
|
||||
|
||||
[See Menu Docs](?path=/docs/guides-extending-the-backoffice-menu--docs)
|
||||
|
||||
This will make it possible to compose a sidebar menu from multiple Apps:
|
||||
|
||||
<img src="docs/section-sidebar-composed-apps.svg" width="400" />
|
||||
|
||||
#### Adding Items to an existing menu
|
||||
|
||||
[See Menu Docs](?path=/docs/guides-extending-the-backoffice-menu--docs)
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Sections/Views"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Section Views
|
||||
|
||||
TODO: add description of section views
|
||||
|
||||
<img src="docs/section-views.svg" width="400" />
|
||||
|
||||
**Manifest**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "sectionView",
|
||||
"alias": "My.SectionView",
|
||||
"name": "My Section View",
|
||||
"meta": {
|
||||
"sections": ["My.Section"],
|
||||
"label": "My View",
|
||||
"pathname": "/my-view"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,136 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Guides/Extending the Backoffice/Trees" parameters={{ previewTabs: { canvas: { hidden: true } } }} />
|
||||
|
||||
# Trees
|
||||
|
||||
// TODO: add description of trees.
|
||||
Rough notes:
|
||||
The tree is a hierarchical structure of nodes. The tree is registered in the Backoffice extension registry. A tree can be rendered anywhere in the Backoffice with the help of the umb-tree element.
|
||||
|
||||
## Registering a tree
|
||||
|
||||
Tree Manifest
|
||||
|
||||
```json
|
||||
// TODO: add interface
|
||||
{
|
||||
"type": "tree",
|
||||
"alias": "My.Tree.Alias",
|
||||
"name": "My Tree",
|
||||
"meta": {
|
||||
"repositoryAlias": "My.Repository.Alias"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "treeItem",
|
||||
"kind": "entity",
|
||||
"alias": "My.TreeItem.Alias",
|
||||
"name": "My Tree Item",
|
||||
"conditions": {
|
||||
"entityType": "my-entity-type",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The backoffice comes with two different tree item kinds out of the box:
|
||||
entity and fileSystem
|
||||
|
||||
## Rendering a tree
|
||||
|
||||
```html
|
||||
<umb-tree alias="My.Tree.Alias"></umb-tree>
|
||||
```
|
||||
|
||||
## Render a Custom Tree Item
|
||||
|
||||
The Tree Item Manifest
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "treeItem",
|
||||
"alias": "Umb.TreeItem.Alias",
|
||||
"name": "My Tree Item",
|
||||
"js": "./my-tree-item.element.js",
|
||||
"conditions": {
|
||||
"entityType": "my-entity-type",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### The Tree Item Element
|
||||
|
||||
```typescript
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
|
||||
import { UmbMyTreeItemContext, MyTreeItemDataModel } from './my-tree-item.context';
|
||||
|
||||
@customElement('my-tree-item')
|
||||
export class MyTreeItemElement extends UmbElementMixin(LitElement) {
|
||||
private _item?: MyTreeItemDataModel;
|
||||
@property({ type: Object, attribute: false })
|
||||
public get item() {
|
||||
return this._item;
|
||||
}
|
||||
public set item(value: MyTreeItemDataModel | undefined) {
|
||||
this._item = value;
|
||||
this.#context.setTreeItem(value);
|
||||
}
|
||||
|
||||
#context = new UmbMyTreeItemContext(this);
|
||||
|
||||
render() {
|
||||
if (!this.item) return nothing;
|
||||
return html` <umb-tree-item-base> Some custom markup </umb-tree-item-base>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default MyTreeItemElement;
|
||||
```
|
||||
|
||||
### The Tree Item Context
|
||||
|
||||
```typescript
|
||||
// TODO: auto generate this from the interface
|
||||
export interface UmbTreeItemContext<T> {
|
||||
host: UmbControllerHostElement;
|
||||
unique?: string;
|
||||
type?: string;
|
||||
|
||||
treeItem: Observable<T | undefined>;
|
||||
hasChildren: Observable<boolean>;
|
||||
isLoading: Observable<boolean>;
|
||||
isSelectable: Observable<boolean>;
|
||||
isSelected: Observable<boolean>;
|
||||
isActive: Observable<boolean>;
|
||||
hasActions: Observable<boolean>;
|
||||
path: Observable<string>;
|
||||
|
||||
setTreeItem(treeItem: T | undefined): void;
|
||||
|
||||
requestChildren(): Promise<{
|
||||
data: PagedResponse<T> | undefined;
|
||||
error: ProblemDetails | undefined;
|
||||
asObservable?: () => Observable<T[]>;
|
||||
}>;
|
||||
toggleContextMenu(): void;
|
||||
select(): void;
|
||||
deselect(): void;
|
||||
constructPath(pathname: string, entityType: string, unique: string): string;
|
||||
}
|
||||
```
|
||||
|
||||
### Extending the Tree Item Context base
|
||||
|
||||
We provide a base class for the tree item context. This class provides some default implementations for the context. You can extend this class to overwrite any of the default implementations.
|
||||
|
||||
```typescript
|
||||
export class UmbMyTreeItemContext extends UmbTreeItemContextBase<MyTreeItemDataModel> {
|
||||
constructor(host: UmbControllerHostElement) {
|
||||
super(host, (x: MyTreeItemDataModel) => x.unique);
|
||||
}
|
||||
|
||||
// overwrite any methods or properties here if needed
|
||||
}
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Dataset Context"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Variant Context
|
||||
|
||||
The Variant Context is a context that holds the data for a set of properties.
|
||||
Property Editors UIs require the Variant Context to be present to work. This enables Property Editor UIs to have a generic relation with its ownership.
|
||||
|
||||
The Variant context holds a name and a set of properties. What makes a property can vary but we require an alias and a value.
|
||||
|
||||
## Variant Context concerning Property Editors and Workspaces.
|
||||
|
||||
A Variant Context is the connection point between a Property Editor and a Workspace.
|
||||
|
||||
The hierarchy is as follows:
|
||||
- Workspace Context
|
||||
- Variant Context
|
||||
- Property Editor UIs
|
||||
|
||||
A variant context covers a set of properties, in some cases a workspace then needs to have multiple variants. An example of such is Document Workspace. Each variant has its own set of properties and a name.
|
||||
|
||||
## Setup a Variant Context
|
||||
|
||||
It would be good to have examples for developers to see how to set up a Variant Context, in code. (This might need to be a tutorial demonstrating implementing a simple workspace with a variant with Property Editor UIs)
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Workspaces/Actions"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Workspace Actions
|
||||
|
||||
// TODO: intro to workspace actions
|
||||
|
||||
- Relates to a workspace alias (Umb.Workspace.Document).
|
||||
- Performs the action on the workspace draft state.
|
||||
- Has Access to the workspace context.
|
||||
|
||||
<img src="docs/workspace-actions.svg" width="400" />
|
||||
|
||||
**Manifest**
|
||||
|
||||
```javascript
|
||||
import { extensionRegistry } from '@umbraco-cms/extension-registry';
|
||||
import { MyWorkspaceAction } from './my-workspace-action';
|
||||
|
||||
const manifest = {
|
||||
type: 'workspaceAction',
|
||||
alias: 'My.WorkspaceAction',
|
||||
name: 'My Workspace Action',
|
||||
api: MyWorkspaceAction,
|
||||
meta: {
|
||||
label: 'My Action',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.WorkspaceAlias',
|
||||
match: 'My.Workspace',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
extensionRegistry.register(manifest);
|
||||
```
|
||||
|
||||
### The Workspace Action Class
|
||||
|
||||
As part of the Extension Manifest you can attach a class that will be instanciated as part of the action. It will have access to the host element and the Workspace Context.
|
||||
When the action is clicked the `execute` method on the api class will be run.
|
||||
When the action is completed, an event on the host element will be dispatched to notify any surrounding elements.
|
||||
|
||||
```ts
|
||||
import { UmbEntityBulkActionBase } from '@umbraco-cms/entity-action';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/controller';
|
||||
import { MyRepository } from './my-repository';
|
||||
|
||||
export class MyWorkspaceAction extends UmbWorkspaceActionBase {
|
||||
constructor(host: UmbControllerHostElement) {
|
||||
super(host);
|
||||
}
|
||||
|
||||
async execute() {
|
||||
await this.workspaceContext.repository?.myAction(this.selection);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Default Element**
|
||||
|
||||
```ts
|
||||
// TODO: get interface
|
||||
interface UmbWorkspaceActionElement {}
|
||||
```
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Workspaces/Context"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Workspace Context
|
||||
|
||||
A Workspace context is a container for the data of a workspace. It is a wrapper around the data of the entity that the workspace is working on. It is also responsible for loading and saving the data to the server.
|
||||
TODO: Extend the description of a workspace
|
||||
|
||||
(rough notes)
|
||||
|
||||
- A workspace context knows about its entity type (e.g. content, media, member, etc.) and holds its unique string (e.g.: key).
|
||||
- Most workspace contexts hold a draft state of its entity data. It is a copy of the entity data that can be modified at runtime and sent to the server to be saved.
|
||||
|
||||
If a workspace wants to utilize Property Editor UIs, then it must provide a variant context for the property editors. The property-dataset context is the generic interface between workspace and property editors. See variant contexts for more info.
|
||||
|
||||
TODO: More points and examples:
|
||||
|
||||
```ts
|
||||
// TODO: get typescript interface
|
||||
interface UmbWorkspaceContext {}
|
||||
```
|
||||
|
||||
## Examples of workspaces:
|
||||
|
||||
TODO: link to all workspaces in storybook. Can we somehow auto-generate this list?
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Workspaces/Intro"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Workspaces
|
||||
|
||||
A Workspace is the editor for a specific entity type. It can either be a simple view of data or a complex editor with multiple views.
|
||||
TODO: extend the description of a workspace
|
||||
|
||||
(rough notes)
|
||||
|
||||
- A workspace is based on a entity type (e.g. content, media, member, etc.) and a unique string (ex: key).
|
||||
- Most workspaces holds a draft state of an entity. It is a copy of the entity data that can be modified at runtime and send to the server to be saved.
|
||||
- A workspace can be a single view or consist of multiple views.
|
||||
- A workspace should host a workspace context, of which anything within can communicate with.
|
||||
|
||||
<img src="docs/workspace.svg" width="400" />
|
||||
|
||||
```ts
|
||||
// TODO: get typescript interface
|
||||
interface UmbWorkspaceElement {}
|
||||
```
|
||||
|
||||
## The Workspace Context
|
||||
|
||||
[Read more about Workspace Context](?path=/docs/guides-extending-the-backoffice-workspaces-context--docs)
|
||||
|
||||
## Examples of workspaces:
|
||||
|
||||
TODO: link to all workspaces in storybook. Can we somehow auto-generate this list?
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="Guides/Extending the Backoffice/Workspaces/Views"
|
||||
parameters={{ previewTabs: { canvas: { hidden: true } } }}
|
||||
/>
|
||||
|
||||
# Workspace Views
|
||||
|
||||
TODO: add description of a workspace views
|
||||
|
||||
<img src="docs/workspace-views.svg" width="400" />
|
||||
|
||||
**Manifest**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "workspaceView",
|
||||
"alias": "My.WorkspaceView",
|
||||
"name": "My Workspace View",
|
||||
"meta": {
|
||||
"workspaces": ["My.Workspace"],
|
||||
"label": "My View",
|
||||
"pathname": "/my-view"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
<Meta title="Guides/Getting Started" />
|
||||
|
||||
# Getting started
|
||||
|
||||
This section contains a set of guide which will ease the learning of the Umbraco CMS (Backoffice).
|
||||
In this document you will get a overview of the articles — Enabling you to get started with the parts that makes sense for you.
|
||||
|
||||
## Terminology
|
||||
|
||||
There is a few words that covers certain concepts, which is good to learn to easilier decode the purpose of code.
|
||||
|
||||
- **Resource** A API enabling communication with a server. [Go to Resource Guide](/?path=/story/guides-resource--page)
|
||||
- **Store** A API representing data, generally coming from the server. Most stores would talk with one or more resources. [Go to Store Guide](?path=/docs/guides-store--docs)
|
||||
|
||||
- **State** A reactive container holding data, when data is changed all its Observables will be notified.
|
||||
- **Observable** An observable is the hook for others to subscribe to the data of a State.
|
||||
- **Observe** Observe is the term of what we do when subscriping to a Observable, We observe and observable.
|
||||
|
||||
- **Context-API** The name of the system used to serve APIs(instances/classes) that for a certain context in the DOM. An API that is served via the Context-API is called a Context [Go to Context API Guide](?path=/docs/guides-context-api--docs)
|
||||
- **Context Provider** One that provides a class instance as a Context API.
|
||||
- **Context Consumer** One that consumer/subscripes to a class instance as a Context API.
|
||||
|
||||
- **Controller** An abstract term for a things that hooks into the lifecycle of a element. Many things in our system is Controllers. [Go to Controller Guide](?path=/docs/guides-controller--docs)
|
||||
- **Umbraco Controller** Enables hosting controllers. Additionally it provides few shortcut methods for initializing core Umbraco Controllers.
|
||||
|
||||
- **Controller Host** A class which can host controllers.
|
||||
- **Controller Host Element** The element that can host controllers.
|
||||
- **Umbraco Element** The `UmbLitElement` or `UmbElemenMixin` enables hosting controllers. Additionally it provides few shortcut methods for initializing core Umbraco Controllers. [Go to Umbraco Element Guide](?path=/docs/guides-umbraco-element--docs)
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
export default {
|
||||
title: 'API/Modals',
|
||||
id: 'umb-modal-context',
|
||||
argTypes: {
|
||||
modalLayout: {
|
||||
control: 'select',
|
||||
//options: ['Confirm', 'Content Picker', 'Property Editor UI Picker', 'Icon Picker'],
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (props) => {
|
||||
return html`
|
||||
Under construction
|
||||
<umb-story-modal-context-example .modalLayout=${props.modalLayout}></umb-story-modal-context-example>
|
||||
`;
|
||||
};
|
||||
|
||||
export const Overview = Template.bind({});
|
||||
@@ -1,52 +0,0 @@
|
||||
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT, UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
@customElement('umb-story-modal-context-example')
|
||||
export class UmbStoryModalContextExampleElement extends UmbLitElement {
|
||||
@property()
|
||||
modalLayout = 'confirm';
|
||||
|
||||
@state()
|
||||
value = '';
|
||||
|
||||
private _modalContext?: UmbModalManagerContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
|
||||
this._modalContext = instance;
|
||||
});
|
||||
}
|
||||
|
||||
private _open() {
|
||||
// TODO: use the extension registry to get all modals
|
||||
/*
|
||||
switch (this.modalLayout) {
|
||||
case 'Content Picker':
|
||||
this._modalContext?.documentPicker();
|
||||
break;
|
||||
case 'Property Editor UI Picker':
|
||||
this._modalContext?.propertyEditorUIPicker();
|
||||
break;
|
||||
case 'Icon Picker':
|
||||
this._modalContext?.iconPicker();
|
||||
break;
|
||||
default:
|
||||
this._modalContext?.confirm({
|
||||
headline: 'Headline',
|
||||
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
|
||||
});
|
||||
break;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-button label="open-dialog" look="primary" @click=${() => this._open()} style="margin-right: 9px;"
|
||||
>Open modal</uui-button
|
||||
>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Guides/Store" parameters={{ previewTabs: { canvas: { hidden: true } } }} />
|
||||
|
||||
# Store
|
||||
|
||||
A store is the link between a Resource and a Repository. A store is mainly taken form as a Context, In other words we will have to Consume the Context(Store) to get the Store.
|
||||
Generally a Store will be holding one or more State Objects, each Subject is made available for Observation via a Observables.
|
||||
|
||||
### A Simple Store:
|
||||
|
||||
```typescript
|
||||
class MyProductStore {
|
||||
#products = new UmbArrayState(<Array<MyProductType>>[], (product) => product.id);
|
||||
|
||||
public readonly products = this.#products.asObservable();
|
||||
|
||||
protected host: UmbControllerHostElement;
|
||||
constructor(host: UmbControllerHostElement) {
|
||||
this.host = host;
|
||||
|
||||
// The Store provides it self as a Context-API.
|
||||
new UmbContextProviderController(_host, 'MyStoreContextAlias', this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example we created a ArrayState, A State which is specific to Arrays.
|
||||
This holds the data for one or more Observables to convey for outsiders.
|
||||
|
||||
This example shows how to use the store:
|
||||
|
||||
```typescript
|
||||
class MyImplementation extends UmbLitElement {
|
||||
private _myProductStore?: MyProductStore;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Notice the consume callback is triggered initially and everytime the Context is changed.
|
||||
this.consume('MyStoreContextAlias', (context) => {
|
||||
this._myProductStore = context;
|
||||
|
||||
this._observeAllProducts();
|
||||
});
|
||||
}
|
||||
|
||||
private _observeAllProducts() {
|
||||
if (!this._myProductStore) return;
|
||||
|
||||
// Notice this callback will be triggered initially and each time the products change:
|
||||
this.observe(this._myProductStore.products, (products) => {
|
||||
console.log('The data of all products is:', products);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### A bit more meaningful Store:
|
||||
|
||||
Here we added a method that returns an Observable that is specific to the requested product.
|
||||
|
||||
```typescript
|
||||
class MyProductStore {
|
||||
|
||||
...
|
||||
|
||||
getByKey(id: string) {
|
||||
|
||||
// Request data via a Resource to then take part of this state when recieved.
|
||||
tryExecuteAndNotify(this.host, ProductResource.getByKey({id})).then(({ data }) => {
|
||||
if (data) {
|
||||
this.#products.append(data.items);
|
||||
}
|
||||
});
|
||||
|
||||
// Return a Observable part, to listen for this specific product and the future changes of it.
|
||||
return this.#data.asObservablePart((documents) =>
|
||||
documents.find((document) => document.id === id)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
An example using this method:
|
||||
|
||||
```typescript
|
||||
class MyImplementation extends UmbLitElement {
|
||||
private _myProductStore?: MyProductStore;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Notice the consume callback is triggered initially and everytime the Context is changed.
|
||||
this.consume('MyStoreContextAlias', (context) => {
|
||||
this._myProductStore = context;
|
||||
|
||||
this._observeASpecificProduct();
|
||||
});
|
||||
}
|
||||
|
||||
private _observeASpecificProduct() {
|
||||
if (!this._myProductStore) return;
|
||||
|
||||
// Notice this callback will be triggered initially and each time the specific product change:
|
||||
this.observe(this._myProductStore.getByKey('1234'), (product) => {
|
||||
console.log('The data of product `1234`` is:', product);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Create many Observables:
|
||||
|
||||
A Store must hold different Observables some very general and others specific. All in perspective of what types of observes we like to accommodate.
|
||||
|
||||
This example give some inspiration to how fine grained this can become:
|
||||
|
||||
```typescript
|
||||
class MyProductStore {
|
||||
#products = new UmbArrayState(<Array<MyProductType>>[]);
|
||||
|
||||
public readonly products = this.#products.asObservable();
|
||||
public readonly amountOfProducts = this.#products.asObservablePart((products) => products.length);
|
||||
public readonly topTenRatedProducts = this.#products.asObservablePart((products) => products.sort((a, b) => b.rating - a.rating).slice(0, 10));
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
An observer of an Observable will only be triggered if the specific part of that data has changed.
|
||||
With this we can make a high performant application, only triggering the parts that needs to update when data is changed.
|
||||
|
||||
### Ensure unique data:
|
||||
|
||||
For incoming data to replace existing data, we need to clarify what makes a entry of the array unique.
|
||||
In the examples of this guide each product has a id, and we have clarified this to the State by giving it the little method `(product) => product.id` as part of the its creation:
|
||||
|
||||
```typescript
|
||||
class MyProductStore {
|
||||
#products = new UmbArrayState(<Array<MyProductType>>[], (product) => product.id);
|
||||
...
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user