Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension Examples as part of this repo + Example Dev Mode #1016

Merged
merged 12 commits into from
Nov 27, 2023
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ schemas
temp-schema-generator
APP_PLUGINS
/src/external/router-slot
/examples
49 changes: 49 additions & 0 deletions devops/example-runner/index.js
Original file line number Diff line number Diff line change
@@ -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();
7 changes: 7 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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.
Empty file added examples/index.js
Empty file.
8 changes: 8 additions & 0 deletions examples/workspace-context-counter/README.md
Original file line number Diff line number Diff line change
@@ -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.
29 changes: 29 additions & 0 deletions examples/workspace-context-counter/counter-workspace-context.ts
Original file line number Diff line number Diff line change
@@ -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');
60 changes: 60 additions & 0 deletions examples/workspace-context-counter/counter-workspace-view.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
17 changes: 17 additions & 0 deletions examples/workspace-context-counter/incrementor-workspace-action.ts
Original file line number Diff line number Diff line change
@@ -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;
52 changes: 52 additions & 0 deletions examples/workspace-context-counter/index.ts
Original file line number Diff line number Diff line change
@@ -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',
},
],
},
]
21 changes: 21 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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);
}
});
}
});

}
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
27 changes: 14 additions & 13 deletions src/libs/class-api/class.mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,27 @@ import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';

type UmbClassMixinConstructor = new (
host: UmbControllerHost,
controllerAlias: UmbControllerAlias
controllerAlias: UmbControllerAlias,
) => UmbClassMixinDeclaration;

declare class UmbClassMixinDeclaration implements UmbClassMixinInterface {
_host: UmbControllerHost;
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[];
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}

Expand Down
Loading
Loading