Merge branch 'main' into Feature-Collection-Manifest

This commit is contained in:
Mads Rasmussen
2023-11-27 09:54:53 +01:00
20 changed files with 282 additions and 30 deletions

View File

@@ -6,3 +6,4 @@ schemas
temp-schema-generator
APP_PLUGINS
/src/external/router-slot
/examples

View File

@@ -0,0 +1,49 @@
import * as readline from 'readline';
import { execSync } from 'child_process';
import { readdir } from 'fs/promises';
const exampleDirectory = 'examples';
const getDirectories = async (source) =>
(await readdir(source, { withFileTypes: true }))
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name)
async function pickExampleUI(){
// Find sub folder:
const exampleFolderNames = await getDirectories(`${exampleDirectory}`);
// Create UI:
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// List examples:
console.log('Please select an example by entering the corresponding number:');
exampleFolderNames.forEach((folder, index) => {
console.log(`[${index + 1}] ${folder}`);
});
// Ask user to select an example:
rl.question('Enter your selection: ', (answer) => {
// User picked an example:
const selectedFolder = exampleFolderNames[parseInt(answer) - 1];
console.log(`You selected: ${selectedFolder}`);
process.env['VITE_EXAMPLE_PATH'] = `${exampleDirectory}/${selectedFolder}`;
// Start vite server:
try {
execSync('npm run dev', {stdio: 'inherit'});
} catch (error) {
// Nothing, cause this is most likely just the server begin stopped.
//console.log(error);
}
});
};
pickExampleUI();

View File

@@ -0,0 +1,7 @@
# Backoffice Examples
This folder contains example packages showcasing the usage of extensions in Backoffice.
The purpose of these projects includes serving as demonstration or example for
packages, as well as testing to make sure the extension points continue
to work in these situations and to assist in developing new integrations.

View File

@@ -0,0 +1,8 @@
# Workspace Context Counter Example
This example demonstrates the essence of the Workspace Context.
The Workspace Context is available for everything within the Workspace, giving any extension within the ability to communicate through this.
In this example, the Workspace Context houses a counter, which can be incremented by a Workspace Action and shown in the Workspace View.
To demonstrate this, the example comes with: A Workspace Context, A Workspace Action and a Workspace View.

View File

@@ -0,0 +1,29 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbBaseController, type UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbNumberState } from '@umbraco-cms/backoffice/observable-api';
// The Example Workspace Context Controller:
export class WorkspaceContextCounter extends UmbBaseController {
// We always keep our states private, and expose the values as observables:
#counter = new UmbNumberState(0);
readonly counter = this.#counter.asObservable();
constructor(host: UmbControllerHost) {
super(host);
this.provideContext(EXAMPLE_COUNTER_CONTEXT, this);
}
// Lets expose methods to update the state:
increment() {
this.#counter.next(this.#counter.value + 1);
}
}
// Declare a api export, so Extension Registry can initialize this class:
export const api = WorkspaceContextCounter;
// Declare a Context Token that other elements can use to request the WorkspaceContextCounter:
export const EXAMPLE_COUNTER_CONTEXT = new UmbContextToken<WorkspaceContextCounter>('example.workspaceContext.counter');

View File

@@ -0,0 +1,60 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
import { css, html, customElement, state, LitElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import { EXAMPLE_COUNTER_CONTEXT } from './counter-workspace-context';
@customElement('example-counter-workspace-view')
export class ExampleCounterWorkspaceView extends UmbElementMixin(LitElement) {
#counterContext?: typeof EXAMPLE_COUNTER_CONTEXT.TYPE;
@state()
private count = '';
constructor() {
super();
this.consumeContext(EXAMPLE_COUNTER_CONTEXT, (instance) => {
this.#counterContext = instance;
this.#observeCounter();
});
}
#observeCounter(): void {
if (!this.#counterContext) return;
this.observe(this.#counterContext.counter, (count) => {
this.count = count;
});
}
render() {
return html`
<uui-box class="uui-text">
<h1 class="uui-h2" style="margin-top: var(--uui-size-layout-1);">Counter Example</h1>
<p class="uui-lead">
Current count value: ${this.count}
</p>
<p>
This is a Workspace View, that consumes the Counter Context, and displays the current count.
</p>
</uui-box>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
`,
];
}
export default ExampleCounterWorkspaceView;
declare global {
interface HTMLElementTagNameMap {
'example-counter-workspace-view': ExampleCounterWorkspaceView;
}
}

View File

@@ -0,0 +1,17 @@
import { UmbBaseController } from '@umbraco-cms/backoffice/controller-api';
import type { UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace';
import { EXAMPLE_COUNTER_CONTEXT } from './counter-workspace-context';
// The Example Incrementor Workspace Action Controller:
export class ExampleIncrementorWorkspaceAction extends UmbBaseController implements UmbWorkspaceAction {
// This method is executed
async execute() {
await this.consumeContext(EXAMPLE_COUNTER_CONTEXT, (context) => {
context.increment();
}).asPromise();
}
}
// Declare a api export, so Extension Registry can initialize this class:
export const api = ExampleIncrementorWorkspaceAction;

View File

@@ -0,0 +1,52 @@
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [
{
type: 'workspaceContext',
name: 'Example Counter Workspace Context',
alias: 'example.workspaceCounter.counter',
js: () => import('./counter-workspace-context.js'),
conditions: [
{
alias: 'Umb.Condition.WorkspaceAlias',
match: 'Umb.Workspace.Document',
},
],
},
{
type: 'workspaceAction',
name: 'Example Count Incerementor Workspace Action',
alias: 'example.workspaceAction.incrementor',
weight: 1000,
api: () => import('./incrementor-workspace-action.js'),
meta: {
label: 'Increment',
look: 'primary',
color: 'danger',
},
conditions: [
{
alias: 'Umb.Condition.WorkspaceAlias',
match: 'Umb.Workspace.Document',
},
],
},
{
type: 'workspaceEditorView',
name: 'Example Counter Workspace View',
alias: 'example.workspaceView.counter',
element: () => import('./counter-workspace-view.js'),
weight: 900,
meta: {
label: 'Counter',
pathname: 'counter',
icon: 'icon-lab',
},
conditions: [
{
alias: 'Umb.Condition.WorkspaceAlias',
match: 'Umb.Workspace.Document',
},
],
},
]

View File

@@ -1,5 +1,6 @@
import { UmbAppElement } from './src/apps/app/app.element.js';
import { startMockServiceWorker } from './src/mocks/index.js';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
if (import.meta.env.VITE_UMBRACO_USE_MSW === 'on') {
startMockServiceWorker();
@@ -18,4 +19,24 @@ if (import.meta.env.DEV) {
appElement.bypassAuth = isMocking;
document.body.appendChild(appElement);
// Example injector:
if(import.meta.env.VITE_EXAMPLE_PATH) {
import(/* @vite-ignore */ './'+import.meta.env.VITE_EXAMPLE_PATH+'/index.ts').then((js) => {
if (js) {
Object.keys(js).forEach((key) => {
const value = js[key];
if (Array.isArray(value)) {
umbExtensionsRegistry.registerMany(value);
} else if (typeof value === 'object') {
umbExtensionsRegistry.register(value);
}
});
}
});
}

View File

@@ -71,10 +71,13 @@
"./user": "./dist-cms/packages/user/user/index.js",
"./user-permission": "./dist-cms/packages/user/user-permission/index.js",
"./code-editor": "./dist-cms/packages/templating/code-editor/index.js",
"./external/*": "./dist-cms/external/*/index.js"
"./external/*": "./dist-cms/external/*/index.js",
"./examples/*": "./examples/*/index.js",
"./examples": "./examples/index.js"
},
"files": [
"dist-cms",
"examples",
"README.md"
],
"repository": {
@@ -120,7 +123,8 @@
"new-extension": "plop --plopfile ./devops/plop/plop.js",
"compile": "tsc",
"check": "npm run lint:errors && npm run compile && npm run build-storybook && npm run generate:jsonschema:dist",
"prepublishOnly": "node ./devops/publish/cleanse-pkg.js"
"prepublishOnly": "node ./devops/publish/cleanse-pkg.js",
"example": "node ./devops/example-runner/index.js"
},
"engines": {
"node": ">=20.9 <21",

View File

@@ -17,7 +17,7 @@ import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
type UmbClassMixinConstructor = new (
host: UmbControllerHost,
controllerAlias: UmbControllerAlias
controllerAlias: UmbControllerAlias,
) => UmbClassMixinDeclaration;
declare class UmbClassMixinDeclaration implements UmbClassMixinInterface {
@@ -25,16 +25,19 @@ declare class UmbClassMixinDeclaration implements UmbClassMixinInterface {
observe<T>(
source: Observable<T>,
callback: (_value: T) => void,
controllerAlias?: UmbControllerAlias
controllerAlias?: UmbControllerAlias,
): UmbObserverController<T>;
provideContext<
BaseType = unknown,
ResultType extends BaseType = BaseType,
InstanceType extends ResultType = ResultType
>(alias: string | UmbContextToken<BaseType, ResultType>, instance: InstanceType): UmbContextProviderController<BaseType, ResultType, InstanceType>;
InstanceType extends ResultType = ResultType,
>(
alias: string | UmbContextToken<BaseType, ResultType>,
instance: InstanceType,
): UmbContextProviderController<BaseType, ResultType, InstanceType>;
consumeContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>
callback: UmbContextCallback<ResultType>,
): UmbContextConsumerController<BaseType, ResultType>;
hasController(controller: UmbController): boolean;
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[];
@@ -86,15 +89,13 @@ export const UmbClassMixin = <T extends ClassConstructor>(superClass: T) => {
* @return {UmbContextProviderController} Reference to a Context Provider Controller instance
* @memberof UmbElementMixin
*/
provideContext
<
provideContext<
BaseType = unknown,
ResultType extends BaseType = BaseType,
InstanceType extends ResultType = ResultType
>
(
InstanceType extends ResultType = ResultType,
>(
contextAlias: string | UmbContextToken<BaseType, ResultType>,
instance: InstanceType
instance: InstanceType,
): UmbContextProviderController {
return new UmbContextProviderController<BaseType, ResultType, InstanceType>(this, contextAlias, instance);
}
@@ -108,8 +109,8 @@ export const UmbClassMixin = <T extends ClassConstructor>(superClass: T) => {
*/
consumeContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
contextAlias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>
): UmbContextConsumerController<BaseType, ResultType> {
callback: UmbContextCallback<ResultType>,
): UmbContextConsumerController<BaseType, ResultType> {
return new UmbContextConsumerController(this, contextAlias, callback);
}
}

View File

@@ -195,6 +195,7 @@ export class UmbDataTypePickerFlowModalElement extends UmbLitElement {
private _renderFilter() {
return html` <uui-input
type="search"
id="filter"
@input="${this._handleFilterInput}"
placeholder="Type to filter..."

View File

@@ -111,6 +111,7 @@ export class UmbPropertyEditorUIPickerModalElement extends UmbLitElement {
private _renderFilter() {
return html` <uui-input
type="search"
id="filter"
@input="${this._handleFilterInput}"
placeholder="Type to filter..."

View File

@@ -106,10 +106,11 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
renderSearchbar() {
return html` <uui-input
@keyup="${this.#filterIcons}"
type="search"
placeholder="Type to filter..."
label="Type to filter icons"
id="searchbar">
id="searchbar"
@keyup="${this.#filterIcons}">
<uui-icon name="search" slot="prepend" id="searchbar_icon"></uui-icon>
</uui-input>`;
}

View File

@@ -1,23 +1,20 @@
import { UmbWorkspaceContextInterface, UMB_WORKSPACE_CONTEXT } from '../workspace-context/index.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import { UmbBaseController, type UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export interface UmbWorkspaceAction<WorkspaceType = unknown> extends UmbApi {
host: UmbControllerHost;
workspaceContext?: WorkspaceType;
export interface UmbWorkspaceAction extends UmbApi {
execute(): Promise<void>;
}
export abstract class UmbWorkspaceActionBase<WorkspaceContextType extends UmbWorkspaceContextInterface>
implements UmbWorkspaceAction<WorkspaceContextType>
export abstract class UmbWorkspaceActionBase<WorkspaceContextType extends UmbWorkspaceContextInterface> extends UmbBaseController
implements UmbWorkspaceAction
{
host: UmbControllerHost;
workspaceContext?: WorkspaceContextType;
constructor(host: UmbControllerHost) {
this.host = host;
super(host);
new UmbContextConsumerController(this.host, UMB_WORKSPACE_CONTEXT, (instance) => {
// TODO, we most likely should require a context token here in this type, and mane it specifically for workspace actions with context workspace request.
this.consumeContext(UMB_WORKSPACE_CONTEXT, (instance) => {
// TODO: Be aware we are casting here. We should consider a better solution for typing the contexts. (But notice we still want to capture the first workspace...)
this.workspaceContext = instance as unknown as WorkspaceContextType;
});

View File

@@ -129,10 +129,11 @@ export class UmbDashboardTranslationDictionaryElement extends UmbLitElement {
${this.localize.term('dictionary_createNew')}
</uui-button>
<uui-input
@keyup="${this.#filter}"
type="search"
id="searchbar"
placeholder=${this.localize.term('placeholders_filter')}
label=${this.localize.term('placeholders_filter')}
id="searchbar">
@keyup="${this.#filter}">
<div slot="prepend">
<uui-icon name="search" id="searchbar_icon"></uui-icon>
</div>

View File

@@ -54,6 +54,7 @@ export class UmbDocumentWorkspaceElement extends UmbLitElement {
},
];
// TODO: We need to recreate when ID changed?
new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'workspaceContext', [this, this.#workspaceContext]);
}

View File

@@ -112,6 +112,7 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
<p>Search the ${this.searcherName} and view the results</p>
<div class="flex">
<uui-input
type="search"
id="search-input"
placeholder="Type to filter..."
label="Type to filter"

View File

@@ -126,7 +126,7 @@
"@umbraco-cms/internal/test-utils": ["utils/test-utils.ts"]
}
},
"include": ["src/**/*.ts", "apps/**/*.ts", "e2e/**/*.ts", "index.ts", "storybook/stories/**/*.ts"],
"include": ["src/**/*.ts", "apps/**/*.ts", "e2e/**/*.ts", "index.ts", "storybook/stories/**/*.ts", "examples/**/*.ts", ],
"references": [
{
"path": "./tsconfig.node.json"