Merge branch 'main' into feature/template-query-logic-
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
.eslintrc.js
|
||||
types
|
||||
/types
|
||||
dist
|
||||
dist-cms
|
||||
schemas
|
||||
|
||||
@@ -9,7 +9,7 @@ Here is the LIT documentation and playground: [https://lit.dev](https://lit.dev)
|
||||
### How best to find what needs converting from the old backoffice?
|
||||
|
||||
1. Navigate to [https://github.com/umbraco/Umbraco-CMS](https://github.com/umbraco/Umbraco-CMS)
|
||||
2. Make sure you are on the `v13/dev` branch
|
||||
2. Make sure you are on the `v14/dev` branch
|
||||
|
||||
### What is the process of contribution?
|
||||
|
||||
@@ -35,12 +35,11 @@ The frontend has an API formatter that takes the OpenAPI schema file and convert
|
||||
### Caveats
|
||||
|
||||
1. There is currently no way to add translations. All texts in the UI are expected to be written in Umbraco’s default language of English.
|
||||
2. The backoffice can be run and tested against a real Umbraco instance by cloning down the `v13/dev` branch, but there are no guarantees about how well it works yet.
|
||||
3. Authentication has not been built, so the login page is never shown - HQ is working actively on this.
|
||||
2. The backoffice can be run and tested against a real Umbraco instance by cloning down the `v14/dev` branch, but there are no guarantees about how well it works yet.
|
||||
|
||||
**Current schema for API:**
|
||||
|
||||
[https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v13/dev/src/Umbraco.Cms.Api.Management/OpenApi.json](https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v13/dev/src/Umbraco.Cms.Api.Management/OpenApi.json)
|
||||
[https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v13/dev/src/Umbraco.Cms.Api.Management/OpenApi.json](https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v14/dev/src/Umbraco.Cms.Api.Management/OpenApi.json)
|
||||
|
||||
**How to convert it:**
|
||||
|
||||
@@ -73,8 +72,8 @@ The old AngularJS controllers will have to be converted into modern TypeScript a
|
||||
To make the first button work, which simply just requests a new status from the server, we must make a call to `PublishedCacheResource.getPublishedCacheStatus()`. An additional thing here is to wrap that in a friendly function called `tryExecuteAndNotify`, which is something we make available to developers to automatically handle the responses coming from the server and additionally use the Notifications to notify of any errors:
|
||||
|
||||
```typescript
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/resources';
|
||||
import { PublishedCacheResource } from '@umbraco-cms/backend-api';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
import { PublishedCacheResource } from '@umbraco-cms/backoffice/backend-api';
|
||||
|
||||
private _getStatus() {
|
||||
const { data: status } = await tryExecuteAndNotify(this, PublishedCacheResource.getPublishedCacheStatus());
|
||||
@@ -113,17 +112,20 @@ To declare the Published Cache Status Dashboard as a new manifest, we need to ad
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: 'dashboard',
|
||||
alias: 'Umb.Dashboard.PublishedStatus',
|
||||
name: 'Published Status',
|
||||
elementName: 'umb-dashboard-published-status',
|
||||
loader: () => import('./backoffice/dashboards/published-status/dashboard-published-status.element'),
|
||||
meta: {
|
||||
sections: ['Umb.Section.Settings'],
|
||||
pathname: 'published-status',
|
||||
weight: 9,
|
||||
},
|
||||
}
|
||||
type: 'dashboard',
|
||||
alias: 'Umb.Dashboard.PublishedStatus',
|
||||
name: 'Published Status Dashboard',
|
||||
elementName: 'umb-dashboard-published-status',
|
||||
loader: () => import('./published-status/dashboard-published-status.element.js'),
|
||||
weight: 200,
|
||||
meta: {
|
||||
label: 'Published Status',
|
||||
pathname: 'published-status',
|
||||
},
|
||||
conditions: {
|
||||
sections: ['Umb.Section.Settings'],
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
Let’s go through each of these properties…
|
||||
@@ -150,7 +152,11 @@ Let’s go through each of these properties…
|
||||
|
||||
- Loader: references a function call to import the file that the element is declared within
|
||||
|
||||
- Meta: allows us to reference additional data - in our case we can specify the section that our dashboard will sit within, the pathname that will be displayed in the url and the weight of the section
|
||||
- Weight: allows us to specify the order in which the dashboard will be displayed within the tabs bar
|
||||
|
||||
- Meta: allows us to reference additional data - in our case we can specify the label that is shown in the tabs bar and the pathname that will be displayed in the url
|
||||
|
||||
- Conditions: allows us to specify the conditions that must be met in order for the dashboard to be displayed. In our case we are specifying that the dashboard will only be displayed within the Settings section
|
||||
|
||||
## API mock handlers
|
||||
|
||||
@@ -164,7 +170,7 @@ So to define this, we must first add a handler for the Published Status called `
|
||||
|
||||
```typescript
|
||||
const { rest } = window.MockServiceWorker;
|
||||
import { umbracoPath } from '@umbraco-cms/utils';
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export const handlers = [
|
||||
rest.get(umbracoPath('/published-cache/status'), (_req, res, ctx) => {
|
||||
@@ -226,10 +232,6 @@ We are using a tool called Web Test Runner which spins up a bunch of browsers us
|
||||
|
||||
Working with playwright: [https://playwright.dev/docs/intro](https://playwright.dev/docs/intro)
|
||||
|
||||
### End-to-end testing
|
||||
|
||||
This test is being performed by Playwright as well but is running in a mode where Playwright clicks through the browser in different scenarios and reports on those. There are no requirements to add these tests to new components yet, but it is encouraged. The tests are located in a separate app called “backoffice-e2e”.
|
||||
|
||||
## Putting it all together
|
||||
|
||||
When we are finished with the dashboard we will hopefully have something akin to this [real-world example of the actual dashboard that was migrated](https://github.com/umbraco/Umbraco.CMS.Backoffice/tree/main/src/backoffice/settings/dashboards/published-status).
|
||||
|
||||
32
src/Umbraco.Web.UI.Client/.github/ISSUE_TEMPLATE/01_bug_report.md
vendored
Normal file
32
src/Umbraco.Web.UI.Client/.github/ISSUE_TEMPLATE/01_bug_report.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "\U0001F41B Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]: "
|
||||
labels: type/bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Feature request
|
||||
name: "✨ Feature request"
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: type/feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
2
src/Umbraco.Web.UI.Client/.gitignore
vendored
2
src/Umbraco.Web.UI.Client/.gitignore
vendored
@@ -11,7 +11,7 @@ node_modules
|
||||
dist
|
||||
dist-cms
|
||||
dist-ssr
|
||||
types
|
||||
/types
|
||||
*.tsbuildinfo
|
||||
*.local
|
||||
*.tgz
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"Niels",
|
||||
"pickable",
|
||||
"templating",
|
||||
"tinymce",
|
||||
"umbraco",
|
||||
"Uncategorized",
|
||||
"variantable"
|
||||
|
||||
130
src/Umbraco.Web.UI.Client/package-lock.json
generated
130
src/Umbraco.Web.UI.Client/package-lock.json
generated
@@ -41,23 +41,23 @@
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.9",
|
||||
"@typescript-eslint/parser": "^5.59.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.60.1",
|
||||
"@web/dev-server-esbuild": "^0.3.3",
|
||||
"@web/dev-server-import-maps": "^0.1.1",
|
||||
"@web/dev-server-rollup": "^0.3.21",
|
||||
"@web/test-runner": "^0.16.1",
|
||||
"@web/test-runner-playwright": "^0.10.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-plugin-import": "^2.27.4",
|
||||
"eslint-plugin-lit": "^1.8.2",
|
||||
"eslint-plugin-lit-a11y": "^2.3.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-lit": "^1.8.3",
|
||||
"eslint-plugin-lit-a11y": "^3.0.0",
|
||||
"eslint-plugin-local-rules": "^1.3.2",
|
||||
"eslint-plugin-storybook": "^0.6.12",
|
||||
"eslint-plugin-wc": "^1.4.0",
|
||||
"eslint-plugin-wc": "^1.5.0",
|
||||
"msw": "^1.2.1",
|
||||
"openapi-typescript-codegen": "^0.24.0",
|
||||
"playwright-msw": "^2.1.1",
|
||||
@@ -3063,9 +3063,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz",
|
||||
"integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz",
|
||||
"integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
@@ -3087,9 +3087,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||
"integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
|
||||
"integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@humanwhocodes/object-schema": "^1.2.1",
|
||||
@@ -6394,15 +6394,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "5.59.9",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz",
|
||||
"integrity": "sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA==",
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz",
|
||||
"integrity": "sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "5.59.9",
|
||||
"@typescript-eslint/type-utils": "5.59.9",
|
||||
"@typescript-eslint/utils": "5.59.9",
|
||||
"@typescript-eslint/scope-manager": "5.60.1",
|
||||
"@typescript-eslint/type-utils": "5.60.1",
|
||||
"@typescript-eslint/utils": "5.60.1",
|
||||
"debug": "^4.3.4",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"ignore": "^5.2.0",
|
||||
@@ -6461,14 +6461,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "5.59.9",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.9.tgz",
|
||||
"integrity": "sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ==",
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz",
|
||||
"integrity": "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "5.59.9",
|
||||
"@typescript-eslint/types": "5.59.9",
|
||||
"@typescript-eslint/typescript-estree": "5.59.9",
|
||||
"@typescript-eslint/scope-manager": "5.60.1",
|
||||
"@typescript-eslint/types": "5.60.1",
|
||||
"@typescript-eslint/typescript-estree": "5.60.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6488,13 +6488,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "5.59.9",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.9.tgz",
|
||||
"integrity": "sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ==",
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz",
|
||||
"integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.59.9",
|
||||
"@typescript-eslint/visitor-keys": "5.59.9"
|
||||
"@typescript-eslint/types": "5.60.1",
|
||||
"@typescript-eslint/visitor-keys": "5.60.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
@@ -6505,13 +6505,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "5.59.9",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.9.tgz",
|
||||
"integrity": "sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q==",
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz",
|
||||
"integrity": "sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "5.59.9",
|
||||
"@typescript-eslint/utils": "5.59.9",
|
||||
"@typescript-eslint/typescript-estree": "5.60.1",
|
||||
"@typescript-eslint/utils": "5.60.1",
|
||||
"debug": "^4.3.4",
|
||||
"tsutils": "^3.21.0"
|
||||
},
|
||||
@@ -6532,9 +6532,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "5.59.9",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.9.tgz",
|
||||
"integrity": "sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw==",
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz",
|
||||
"integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
@@ -6545,13 +6545,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "5.59.9",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.9.tgz",
|
||||
"integrity": "sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA==",
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz",
|
||||
"integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.59.9",
|
||||
"@typescript-eslint/visitor-keys": "5.59.9",
|
||||
"@typescript-eslint/types": "5.60.1",
|
||||
"@typescript-eslint/visitor-keys": "5.60.1",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -6584,9 +6584,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
|
||||
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
|
||||
"version": "7.5.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
|
||||
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -6605,17 +6605,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "5.59.9",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.9.tgz",
|
||||
"integrity": "sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg==",
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz",
|
||||
"integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@typescript-eslint/scope-manager": "5.59.9",
|
||||
"@typescript-eslint/types": "5.59.9",
|
||||
"@typescript-eslint/typescript-estree": "5.59.9",
|
||||
"@typescript-eslint/scope-manager": "5.60.1",
|
||||
"@typescript-eslint/types": "5.60.1",
|
||||
"@typescript-eslint/typescript-estree": "5.60.1",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"semver": "^7.3.7"
|
||||
},
|
||||
@@ -6664,12 +6664,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "5.59.9",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.9.tgz",
|
||||
"integrity": "sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q==",
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz",
|
||||
"integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.59.9",
|
||||
"@typescript-eslint/types": "5.60.1",
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -11050,16 +11050,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.41.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz",
|
||||
"integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==",
|
||||
"version": "8.43.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz",
|
||||
"integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.4.0",
|
||||
"@eslint/eslintrc": "^2.0.3",
|
||||
"@eslint/js": "8.41.0",
|
||||
"@humanwhocodes/config-array": "^0.11.8",
|
||||
"@eslint/js": "8.43.0",
|
||||
"@humanwhocodes/config-array": "^0.11.10",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"ajv": "^6.10.0",
|
||||
@@ -11288,9 +11288,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-lit-a11y": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-lit-a11y/-/eslint-plugin-lit-a11y-2.4.1.tgz",
|
||||
"integrity": "sha512-UljRja/2cVrNtgnCDj5sCT3Larxda4mGqbsPhlksvECo0+KCD8EuUori/P6wFeFqk+pHlkIC3W200E5q85E3VQ==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-lit-a11y/-/eslint-plugin-lit-a11y-3.0.0.tgz",
|
||||
"integrity": "sha512-+HMLB0XVDh6hX9rJZqmfkSfOuTSXzjab/gmDSbhIr/i60aUV+xLm1WdLAPAqLE5YsyxWd1CcycON4OO37ro0xA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"aria-query": "^5.1.3",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"exports": {
|
||||
".": null,
|
||||
"./backend-api": "./dist-cms/external/backend-api/index.js",
|
||||
"./class-api": "./dist-cms/libs/class-api/index.js",
|
||||
"./context-api": "./dist-cms/libs/context-api/index.js",
|
||||
"./controller-api": "./dist-cms/libs/controller-api/index.js",
|
||||
"./element-api": "./dist-cms/libs/element-api/index.js",
|
||||
@@ -34,13 +35,14 @@
|
||||
"./modal": "./dist-cms/packages/core/modal/index.js",
|
||||
"./notification": "./dist-cms/packages/core/notification/index.js",
|
||||
"./picker-input": "./dist-cms/packages/core/picker-input/index.js",
|
||||
"./property-editor": "./dist-cms/packages/core/property-editor/index.js",
|
||||
"./section": "./dist-cms/packages/core/section/index.js",
|
||||
"./sorter": "./dist-cms/packages/core/sorter/index.js",
|
||||
"./store": "./dist-cms/packages/core/store/index.js",
|
||||
"./themes": "./dist-cms/packages/core/themes/index.ts",
|
||||
"./tree": "./dist-cms/packages/core/tree/index.js",
|
||||
"./variant": "./dist-cms/packages/core/variant/index.js",
|
||||
"./workspace": "./dist-cms/packages/core/workspace/index.js",
|
||||
"./property-editor": "./dist-cms/packages/core/property-editor/index.js",
|
||||
"./document": "./dist-cms/packages/documents/documents/index.ts",
|
||||
"./document-blueprint": "./dist-cms/packages/documents/document-blueprints/index.ts",
|
||||
"./document-type": "./dist-cms/packages/documents/document-types/index.ts",
|
||||
@@ -54,7 +56,6 @@
|
||||
"./language": "./dist-cms/packages/settings/languages/index.ts",
|
||||
"./logviewer": "./dist-cms/packages/settings/logviewer/index.js",
|
||||
"./relation-type": "./dist-cms/packages/settings/relation-types/index.ts",
|
||||
"./themes": "./dist-cms/packages/settings/themes/index.ts",
|
||||
"./tags": "./dist-cms/packages/tags/index.ts",
|
||||
"./partial-view": "./dist-cms/packages/templating/partial-views/index.ts",
|
||||
"./stylesheet": "./dist-cms/packages/templating/stylesheets/index.ts",
|
||||
@@ -152,23 +153,23 @@
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.9",
|
||||
"@typescript-eslint/parser": "^5.59.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.60.1",
|
||||
"@web/dev-server-esbuild": "^0.3.3",
|
||||
"@web/dev-server-import-maps": "^0.1.1",
|
||||
"@web/dev-server-rollup": "^0.3.21",
|
||||
"@web/test-runner": "^0.16.1",
|
||||
"@web/test-runner-playwright": "^0.10.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-plugin-import": "^2.27.4",
|
||||
"eslint-plugin-lit": "^1.8.2",
|
||||
"eslint-plugin-lit-a11y": "^2.3.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-lit": "^1.8.3",
|
||||
"eslint-plugin-lit-a11y": "^3.0.0",
|
||||
"eslint-plugin-local-rules": "^1.3.2",
|
||||
"eslint-plugin-storybook": "^0.6.12",
|
||||
"eslint-plugin-wc": "^1.4.0",
|
||||
"eslint-plugin-wc": "^1.5.0",
|
||||
"msw": "^1.2.1",
|
||||
"openapi-typescript-codegen": "^0.24.0",
|
||||
"playwright-msw": "^2.1.1",
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { Subject } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { PackageResource, OpenAPI } from '@umbraco-cms/backoffice/backend-api';
|
||||
import { UmbController, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbBaseController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
import { ManifestBase, isManifestJSType } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export class UmbExtensionInitializer extends UmbController {
|
||||
host: UmbControllerHostElement;
|
||||
export class UmbExtensionInitializer extends UmbBaseController {
|
||||
#extensionRegistry: UmbBackofficeExtensionRegistry;
|
||||
#unobserve = new Subject<void>();
|
||||
#localPackages: Array<Promise<any>> = [];
|
||||
#apiBaseUrl = OpenAPI.BASE;
|
||||
|
||||
constructor(host: UmbControllerHostElement, extensionRegistry: UmbBackofficeExtensionRegistry) {
|
||||
constructor(host: UmbControllerHost, extensionRegistry: UmbBackofficeExtensionRegistry) {
|
||||
super(host, UmbExtensionInitializer.name);
|
||||
this.host = host;
|
||||
this.#extensionRegistry = extensionRegistry;
|
||||
}
|
||||
|
||||
@@ -41,13 +39,13 @@ export class UmbExtensionInitializer extends UmbController {
|
||||
|
||||
async #loadServerPackages() {
|
||||
/* TODO: we need a new endpoint here, to remove the dependency on the package repository, to get the modules available for the backoffice scope
|
||||
/ we will need a similar endpoint for the login, installer etc at some point.
|
||||
/ we will need a similar endpoint for the login, installer etc at some point.
|
||||
We should expose more information about the packages when not authorized so the end point should only return a list of modules from the manifest with
|
||||
with the correct scope.
|
||||
|
||||
This code is copy pasted from the package repository. We probably don't need this is the package repository anymore.
|
||||
*/
|
||||
const { data: packages } = await tryExecuteAndNotify(this.host, PackageResource.getPackageManifest());
|
||||
const { data: packages } = await tryExecuteAndNotify(this._host, PackageResource.getPackageManifest());
|
||||
|
||||
if (packages) {
|
||||
// Append packages to the store but only if they have a name
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { UmbControllerHost } from '../controller-api/controller-host.interface.js';
|
||||
import type { UmbObserverController } from '../observable-api/index.js';
|
||||
import type {
|
||||
UmbContextCallback,
|
||||
UmbContextConsumerController,
|
||||
UmbContextProviderController,
|
||||
UmbContextToken,
|
||||
} from '../context-api/index.js';
|
||||
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { UmbController } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export interface UmbClassMixinInterface extends UmbControllerHost, UmbController {
|
||||
observe<T>(
|
||||
source: Observable<T> | { asObservable: () => Observable<T> },
|
||||
callback: (_value: T) => void,
|
||||
unique?: string
|
||||
): UmbObserverController<T>;
|
||||
provideContext<R = unknown>(alias: string | UmbContextToken<R>, instance: R): UmbContextProviderController<R>;
|
||||
consumeContext<R = unknown>(
|
||||
alias: string | UmbContextToken<R>,
|
||||
callback: UmbContextCallback<R>
|
||||
): UmbContextConsumerController<R>;
|
||||
}
|
||||
117
src/Umbraco.Web.UI.Client/src/libs/class-api/class.mixin.ts
Normal file
117
src/Umbraco.Web.UI.Client/src/libs/class-api/class.mixin.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { UmbClassMixinInterface } from './class.interface.js';
|
||||
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api';
|
||||
import {
|
||||
type UmbControllerHost,
|
||||
UmbControllerHostBaseMixin,
|
||||
UmbController,
|
||||
UmbControllerAlias,
|
||||
} from '@umbraco-cms/backoffice/controller-api';
|
||||
import {
|
||||
UmbContextToken,
|
||||
UmbContextCallback,
|
||||
UmbContextConsumerController,
|
||||
UmbContextProviderController,
|
||||
} from '@umbraco-cms/backoffice/context-api';
|
||||
import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
type UmbClassMixinConstructor = new (
|
||||
host: UmbControllerHost,
|
||||
controllerAlias: UmbControllerAlias
|
||||
) => UmbClassMixinDeclaration;
|
||||
|
||||
declare class UmbClassMixinDeclaration implements UmbClassMixinInterface {
|
||||
_host: UmbControllerHost;
|
||||
observe<T>(
|
||||
source: Observable<T> | { asObservable: () => Observable<T> },
|
||||
callback: (_value: T) => void,
|
||||
controllerAlias?: UmbControllerAlias
|
||||
): UmbObserverController<T>;
|
||||
provideContext<R = unknown>(alias: string | UmbContextToken<R>, instance: R): UmbContextProviderController<R>;
|
||||
consumeContext<R = unknown>(
|
||||
alias: string | UmbContextToken<R>,
|
||||
callback: UmbContextCallback<R>
|
||||
): UmbContextConsumerController<R>;
|
||||
hasController(controller: UmbController): boolean;
|
||||
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[];
|
||||
addController(controller: UmbController): void;
|
||||
removeControllerByAlias(controllerAlias: UmbControllerAlias): void;
|
||||
removeController(controller: UmbController): void;
|
||||
getHostElement(): EventTarget;
|
||||
|
||||
get controllerAlias(): UmbControllerAlias;
|
||||
hostConnected(): void;
|
||||
hostDisconnected(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export const UmbClassMixin = <T extends ClassConstructor>(superClass: T) => {
|
||||
class UmbClassMixinClass extends UmbControllerHostBaseMixin(superClass) implements UmbControllerHost {
|
||||
protected _host: UmbControllerHost;
|
||||
protected _controllerAlias: UmbControllerAlias;
|
||||
|
||||
constructor(host: UmbControllerHost, controllerAlias: UmbControllerAlias) {
|
||||
super();
|
||||
this._host = host;
|
||||
this._controllerAlias = controllerAlias ?? undefined; // ?? Symbol();
|
||||
}
|
||||
|
||||
getHostElement(): EventTarget {
|
||||
return this._host.getHostElement();
|
||||
}
|
||||
|
||||
get controllerAlias(): UmbControllerAlias {
|
||||
return this._controllerAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Observe a RxJS source of choice.
|
||||
* @param {Observable<T>} source RxJS source
|
||||
* @param {method} callback Callback method called when data is changed.
|
||||
* @return {UmbObserverController} Reference to a Observer Controller instance
|
||||
* @memberof UmbElementMixin
|
||||
*/
|
||||
observe<T>(
|
||||
source: Observable<T> | { asObservable: () => Observable<T> },
|
||||
callback: (_value: T) => void,
|
||||
controllerAlias?: UmbControllerAlias
|
||||
) {
|
||||
return new UmbObserverController<T>(
|
||||
this,
|
||||
(source as any).asObservable ? (source as any).asObservable() : source,
|
||||
callback,
|
||||
controllerAlias
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Provide a context API for this or child elements.
|
||||
* @param {string} contextAlias
|
||||
* @param {instance} instance The API instance to be exposed.
|
||||
* @return {UmbContextProviderController} Reference to a Context Provider Controller instance
|
||||
* @memberof UmbElementMixin
|
||||
*/
|
||||
provideContext<R = unknown>(
|
||||
contextAlias: string | UmbContextToken<R>,
|
||||
instance: R
|
||||
): UmbContextProviderController<R> {
|
||||
return new UmbContextProviderController(this, contextAlias, instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Setup a subscription for a context. The callback is called when the context is resolved.
|
||||
* @param {string} contextAlias
|
||||
* @param {method} callback Callback method called when context is resolved.
|
||||
* @return {UmbContextConsumerController} Reference to a Context Consumer Controller instance
|
||||
* @memberof UmbElementMixin
|
||||
*/
|
||||
consumeContext<R = unknown>(
|
||||
contextAlias: string | UmbContextToken<R>,
|
||||
callback: UmbContextCallback<R>
|
||||
): UmbContextConsumerController<R> {
|
||||
return new UmbContextConsumerController(this, contextAlias, callback);
|
||||
}
|
||||
}
|
||||
|
||||
return UmbClassMixinClass as unknown as UmbClassMixinConstructor & UmbClassMixinDeclaration;
|
||||
};
|
||||
2
src/Umbraco.Web.UI.Client/src/libs/class-api/index.ts
Normal file
2
src/Umbraco.Web.UI.Client/src/libs/class-api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './class.interface.js';
|
||||
export * from './class.mixin.js';
|
||||
@@ -1,29 +1,26 @@
|
||||
import { UmbContextToken } from '../token/context-token.js';
|
||||
import { UmbContextConsumer } from './context-consumer.js';
|
||||
import { UmbContextCallback } from './context-request.event.js';
|
||||
import type { UmbControllerHostElement, UmbControllerInterface } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbContextConsumerController<T = unknown>
|
||||
extends UmbContextConsumer<UmbControllerHostElement, T>
|
||||
implements UmbControllerInterface
|
||||
{
|
||||
public get unique() {
|
||||
return undefined;
|
||||
export class UmbContextConsumerController<T = unknown> extends UmbContextConsumer<T> implements UmbController {
|
||||
#controllerAlias = Symbol();
|
||||
#host: UmbControllerHost;
|
||||
|
||||
public get controllerAlias() {
|
||||
return this.#controllerAlias;
|
||||
}
|
||||
|
||||
constructor(
|
||||
host: UmbControllerHostElement,
|
||||
contextAlias: string | UmbContextToken<T>,
|
||||
callback: UmbContextCallback<T>
|
||||
) {
|
||||
super(host, contextAlias, callback);
|
||||
constructor(host: UmbControllerHost, contextAlias: string | UmbContextToken<T>, callback: UmbContextCallback<T>) {
|
||||
super(host.getHostElement(), contextAlias, callback);
|
||||
this.#host = host;
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
if (this.host) {
|
||||
this.host.removeController(this);
|
||||
if (this.#host) {
|
||||
this.#host.removeController(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,50 +6,48 @@ import { UmbContextRequestEventImplementation, UmbContextCallback } from './cont
|
||||
* @export
|
||||
* @class UmbContextConsumer
|
||||
*/
|
||||
export class UmbContextConsumer<HostType extends EventTarget = EventTarget, T = unknown> {
|
||||
_promise?: Promise<T>;
|
||||
_promiseResolver?: (instance: T) => void;
|
||||
export class UmbContextConsumer<T = unknown> {
|
||||
#callback?: UmbContextCallback<T>;
|
||||
#promise?: Promise<T>;
|
||||
#promiseResolver?: (instance: T) => void;
|
||||
|
||||
private _instance?: T;
|
||||
#instance?: T;
|
||||
get instance() {
|
||||
return this._instance;
|
||||
return this.#instance;
|
||||
}
|
||||
|
||||
private _contextAlias: string;
|
||||
get consumerAlias(): string {
|
||||
return this._contextAlias;
|
||||
}
|
||||
#contextAlias: string;
|
||||
|
||||
/**
|
||||
* Creates an instance of UmbContextConsumer.
|
||||
* @param {EventTarget} host
|
||||
* @param {string} _contextAlias
|
||||
* @param {EventTarget} hostElement
|
||||
* @param {string} contextAlias
|
||||
* @param {UmbContextCallback} _callback
|
||||
* @memberof UmbContextConsumer
|
||||
*/
|
||||
constructor(
|
||||
protected host: HostType,
|
||||
_contextAlias: string | UmbContextToken<T>,
|
||||
private _callback?: UmbContextCallback<T>
|
||||
protected hostElement: EventTarget,
|
||||
contextAlias: string | UmbContextToken<T>,
|
||||
callback?: UmbContextCallback<T>
|
||||
) {
|
||||
this._contextAlias = _contextAlias.toString();
|
||||
this.#contextAlias = contextAlias.toString();
|
||||
this.#callback = callback;
|
||||
}
|
||||
|
||||
protected _onResponse = (instance: T) => {
|
||||
// TODO: check that this check is not giving us any problems:
|
||||
if (this._instance === instance) {
|
||||
if (this.#instance === instance) {
|
||||
return;
|
||||
}
|
||||
this._instance = instance;
|
||||
this._callback?.(instance);
|
||||
this._promiseResolver?.(instance);
|
||||
this.#instance = instance;
|
||||
this.#callback?.(instance);
|
||||
this.#promiseResolver?.(instance);
|
||||
};
|
||||
|
||||
public asPromise() {
|
||||
return (
|
||||
this._promise ||
|
||||
(this._promise = new Promise<T>((resolve) => {
|
||||
this._instance ? resolve(this._instance) : (this._promiseResolver = resolve);
|
||||
this.#promise ||
|
||||
(this.#promise = new Promise<T>((resolve) => {
|
||||
this.#instance ? resolve(this.#instance) : (this.#promiseResolver = resolve);
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -58,8 +56,8 @@ export class UmbContextConsumer<HostType extends EventTarget = EventTarget, T =
|
||||
* @memberof UmbContextConsumer
|
||||
*/
|
||||
public request() {
|
||||
const event = new UmbContextRequestEventImplementation(this._contextAlias, this._onResponse);
|
||||
this.host.dispatchEvent(event);
|
||||
const event = new UmbContextRequestEventImplementation(this.#contextAlias, this._onResponse);
|
||||
this.hostElement.dispatchEvent(event);
|
||||
}
|
||||
|
||||
public hostConnected() {
|
||||
@@ -76,16 +74,17 @@ export class UmbContextConsumer<HostType extends EventTarget = EventTarget, T =
|
||||
private _handleNewProvider = (event: Event) => {
|
||||
if (!isUmbContextProvideEventType(event)) return;
|
||||
|
||||
if (this._contextAlias === event.contextAlias) {
|
||||
if (this.#contextAlias === event.contextAlias) {
|
||||
this.request();
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Test destroy scenarios:
|
||||
public destroy() {
|
||||
delete this._instance;
|
||||
delete this._callback;
|
||||
delete this._promise;
|
||||
delete this._promiseResolver;
|
||||
this.hostDisconnected();
|
||||
this.#callback = undefined;
|
||||
this.#promise = undefined;
|
||||
this.#promiseResolver = undefined;
|
||||
this.#instance = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { UmbContextConsumer } from './context-consumer.js';
|
||||
|
||||
export function isContextConsumerType(instance: unknown): instance is UmbContextConsumer {
|
||||
return (
|
||||
typeof instance === 'object' && instance !== null && (instance as UmbContextConsumer).consumerAlias !== undefined
|
||||
);
|
||||
}
|
||||
@@ -23,11 +23,11 @@ describe('UmbContextProviderController', () => {
|
||||
|
||||
describe('Public API', () => {
|
||||
describe('properties', () => {
|
||||
it('has a unique property', () => {
|
||||
expect(provider).to.have.property('unique');
|
||||
it('has a controllerAlias property', () => {
|
||||
expect(provider).to.have.property('controllerAlias');
|
||||
});
|
||||
it('has a unique property, is equal to the unique', () => {
|
||||
expect(provider.unique).to.eq('my-test-context');
|
||||
it('has a controllerAlias property, is equal to the controllerAlias', () => {
|
||||
expect(provider.controllerAlias).to.eq('my-test-context');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { UmbContextToken } from '../token/context-token.js';
|
||||
import { UmbContextProvider } from './context-provider.js';
|
||||
import type { UmbControllerHostElement, UmbControllerInterface } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbContextProviderController<T = unknown>
|
||||
extends UmbContextProvider<UmbControllerHostElement>
|
||||
implements UmbControllerInterface
|
||||
{
|
||||
public get unique() {
|
||||
export class UmbContextProviderController<T = unknown> extends UmbContextProvider implements UmbController {
|
||||
#host: UmbControllerHost;
|
||||
|
||||
public get controllerAlias() {
|
||||
return this._contextAlias.toString();
|
||||
}
|
||||
|
||||
constructor(host: UmbControllerHostElement, contextAlias: string | UmbContextToken<T>, instance: T) {
|
||||
super(host, contextAlias, instance);
|
||||
constructor(host: UmbControllerHost, contextAlias: string | UmbContextToken<T>, instance: T) {
|
||||
super(host.getHostElement(), contextAlias, instance);
|
||||
this.#host = host;
|
||||
|
||||
// If this API is already provided with this alias? Then we do not want to register this controller:
|
||||
const existingControllers = host.getControllers((x) => x.unique === this.unique);
|
||||
const existingControllers = host.getControllers((x) => x.controllerAlias === this.controllerAlias);
|
||||
if (
|
||||
existingControllers.length > 0 &&
|
||||
(existingControllers[0] as UmbContextProviderController).providerInstance?.() === instance
|
||||
) {
|
||||
// Back out, this instance is already provided, by another controller.
|
||||
throw new Error(
|
||||
`Context API: The context of '${this.unique}' is already provided with the same API by another Context Provider Controller.`
|
||||
`Context API: The context of '${this.controllerAlias}' is already provided with the same API by another Context Provider Controller.`
|
||||
);
|
||||
} else {
|
||||
host.addController(this);
|
||||
@@ -30,8 +30,8 @@ export class UmbContextProviderController<T = unknown>
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
if (this.host) {
|
||||
this.host.removeController(this);
|
||||
if (this.#host) {
|
||||
this.#host.removeController(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { UmbContextConsumerController } from '../consume/context-consumer.controller.js';
|
||||
import { UmbContextProviderElement } from './context-provider.element.js';
|
||||
import { customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbControllerHostMixin } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
@customElement('umb-test-context')
|
||||
export class UmbTestContextElement extends UmbControllerHostMixin(HTMLElement) {
|
||||
export class UmbTestContextElement extends UmbControllerHostElementMixin(HTMLElement) {
|
||||
public value: string | null = null;
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UmbControllerHostElement, UmbControllerHostMixin } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbControllerHostElement, UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbContextProviderController, UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
/**
|
||||
@@ -9,7 +9,7 @@ import { UmbContextProviderController, UmbContextToken } from '@umbraco-cms/back
|
||||
* @throws {Error} If the key property is not set.
|
||||
* @throws {Error} If the value property is not set.
|
||||
*/
|
||||
export class UmbContextProviderElement extends UmbControllerHostMixin(HTMLElement) {
|
||||
export class UmbContextProviderElement extends UmbControllerHostElementMixin(HTMLElement) {
|
||||
/**
|
||||
* The value to provide to the context.
|
||||
* @optional
|
||||
|
||||
@@ -23,8 +23,8 @@ describe('UmbContextProvider', () => {
|
||||
|
||||
describe('Public API', () => {
|
||||
describe('properties', () => {
|
||||
it('has a host property', () => {
|
||||
expect(provider).to.have.property('host');
|
||||
it('has a hostElement property', () => {
|
||||
expect(provider).to.have.property('hostElement');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import { UmbContextProvideEventImplementation } from './context-provide.event.js
|
||||
* @export
|
||||
* @class UmbContextProvider
|
||||
*/
|
||||
export class UmbContextProvider<HostType extends EventTarget = EventTarget> {
|
||||
protected host: HostType;
|
||||
export class UmbContextProvider {
|
||||
protected hostElement: EventTarget;
|
||||
|
||||
protected _contextAlias: string;
|
||||
#instance: unknown;
|
||||
@@ -32,8 +32,8 @@ export class UmbContextProvider<HostType extends EventTarget = EventTarget> {
|
||||
* @param {*} instance
|
||||
* @memberof UmbContextProvider
|
||||
*/
|
||||
constructor(host: HostType, contextAlias: string | UmbContextToken, instance: unknown) {
|
||||
this.host = host;
|
||||
constructor(hostElement: EventTarget, contextAlias: string | UmbContextToken, instance: unknown) {
|
||||
this.hostElement = hostElement;
|
||||
this._contextAlias = contextAlias.toString();
|
||||
this.#instance = instance;
|
||||
}
|
||||
@@ -42,18 +42,18 @@ export class UmbContextProvider<HostType extends EventTarget = EventTarget> {
|
||||
* @memberof UmbContextProvider
|
||||
*/
|
||||
public hostConnected() {
|
||||
this.host.addEventListener(umbContextRequestEventType, this._handleContextRequest);
|
||||
this.host.dispatchEvent(new UmbContextProvideEventImplementation(this._contextAlias));
|
||||
this.hostElement.addEventListener(umbContextRequestEventType, this._handleContextRequest);
|
||||
this.hostElement.dispatchEvent(new UmbContextProvideEventImplementation(this._contextAlias));
|
||||
|
||||
// Listen to our debug event 'umb:debug-contexts'
|
||||
this.host.addEventListener(umbDebugContextEventType, this._handleDebugContextRequest);
|
||||
this.hostElement.addEventListener(umbDebugContextEventType, this._handleDebugContextRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof UmbContextProvider
|
||||
*/
|
||||
public hostDisconnected() {
|
||||
this.host.removeEventListener(umbContextRequestEventType, this._handleContextRequest);
|
||||
this.hostElement.removeEventListener(umbContextRequestEventType, this._handleContextRequest);
|
||||
// TODO: fire unprovided event.
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type UmbControllerAlias = string | symbol | undefined;
|
||||
@@ -0,0 +1,114 @@
|
||||
import { ClassConstructor } from '../extension-api/types.js';
|
||||
import { UmbControllerHost } from './controller-host.interface.js';
|
||||
import type { UmbController } from './controller.interface.js';
|
||||
|
||||
declare class UmbControllerHostBaseDeclaration implements Omit<UmbControllerHost, 'getHostElement'> {
|
||||
hasController(controller: UmbController): boolean;
|
||||
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[];
|
||||
addController(controller: UmbController): void;
|
||||
removeControllerByAlias(unique: UmbController['controllerAlias']): void;
|
||||
removeController(controller: UmbController): void;
|
||||
|
||||
hostConnected(): void;
|
||||
hostDisconnected(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin enables a class to host controllers.
|
||||
* This enables controllers to be added to the life cycle of this element.
|
||||
*
|
||||
* @param {Object} superClass - superclass to be extended.
|
||||
* @mixin
|
||||
*/
|
||||
export const UmbControllerHostBaseMixin = <T extends ClassConstructor<any>>(superClass: T) => {
|
||||
class UmbControllerHostBaseClass extends superClass {
|
||||
#controllers: UmbController[] = [];
|
||||
|
||||
#attached = false;
|
||||
|
||||
/**
|
||||
* Tests if a controller is assigned to this element.
|
||||
* @param {UmbController} ctrl
|
||||
*/
|
||||
hasController(ctrl: UmbController): boolean {
|
||||
return this.#controllers.indexOf(ctrl) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve controllers matching a filter of this element.
|
||||
* @param {method} filterMethod
|
||||
*/
|
||||
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[] {
|
||||
return this.#controllers.filter(filterMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a controller to this element.
|
||||
* @param {UmbController} ctrl
|
||||
*/
|
||||
addController(ctrl: UmbController): void {
|
||||
// If this specific class is already added, then skip out.
|
||||
if (this.#controllers.indexOf(ctrl) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there is one already with same unique
|
||||
this.removeControllerByAlias(ctrl.controllerAlias);
|
||||
|
||||
this.#controllers.push(ctrl);
|
||||
if (this.#attached) {
|
||||
// If a controller is created on a already attached element, then it will be added directly. This might not be optimal. As the controller it self has not finished its constructor method jet. therefor i postpone the call:
|
||||
Promise.resolve().then(() => ctrl.hostConnected());
|
||||
//ctrl.hostConnected();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a controller from this element.
|
||||
* Notice this will also destroy the controller.
|
||||
* @param {UmbController} ctrl
|
||||
*/
|
||||
removeController(ctrl: UmbController): void {
|
||||
const index = this.#controllers.indexOf(ctrl);
|
||||
if (index !== -1) {
|
||||
this.#controllers.splice(index, 1);
|
||||
if (this.#attached) {
|
||||
ctrl.hostDisconnected();
|
||||
}
|
||||
ctrl.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a controller from this element by its alias.
|
||||
* Notice this will also destroy the controller.
|
||||
* @param {string | symbol} controllerAlias
|
||||
*/
|
||||
removeControllerByAlias(controllerAlias: UmbController['controllerAlias']): void {
|
||||
if (controllerAlias) {
|
||||
this.#controllers.forEach((x) => {
|
||||
if (x.controllerAlias === controllerAlias) {
|
||||
this.removeController(x);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.#attached = true;
|
||||
this.#controllers.forEach((ctrl: UmbController) => ctrl.hostConnected());
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this.#attached = false;
|
||||
this.#controllers.forEach((ctrl: UmbController) => ctrl.hostDisconnected());
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#controllers.forEach((ctrl: UmbController) => ctrl.destroy());
|
||||
}
|
||||
}
|
||||
|
||||
return UmbControllerHostBaseClass as unknown as ClassConstructor<UmbControllerHostBaseDeclaration> & T;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { HTMLElementConstructor } from '../extension-api/types.js';
|
||||
import { UmbControllerAlias } from './controller-alias.type.js';
|
||||
import { UmbControllerHostBaseMixin } from './controller-host-base.mixin.js';
|
||||
import { UmbControllerHost } from './controller-host.interface.js';
|
||||
import type { UmbController } from './controller.interface.js';
|
||||
|
||||
export declare class UmbControllerHostElement extends HTMLElement implements UmbControllerHost {
|
||||
hasController(controller: UmbController): boolean;
|
||||
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[];
|
||||
addController(controller: UmbController): void;
|
||||
removeControllerByAlias(alias: UmbControllerAlias): void;
|
||||
removeController(controller: UmbController): void;
|
||||
getHostElement(): EventTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin enables a web-component to host controllers.
|
||||
* This enables controllers to be added to the life cycle of this element.
|
||||
*
|
||||
* @param {Object} superClass - superclass to be extended.
|
||||
* @mixin
|
||||
*/
|
||||
export const UmbControllerHostElementMixin = <T extends HTMLElementConstructor>(superClass: T) => {
|
||||
class UmbControllerHostElementClass extends UmbControllerHostBaseMixin(superClass) implements UmbControllerHost {
|
||||
getHostElement(): EventTarget {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback?.();
|
||||
this.hostConnected();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback?.();
|
||||
this.hostDisconnected();
|
||||
}
|
||||
}
|
||||
|
||||
return UmbControllerHostElementClass as unknown as HTMLElementConstructor<UmbControllerHostElement> & T;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElement {
|
||||
connectedCallback(): void;
|
||||
disconnectedCallback(): void;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { UmbControllerHostElement, UmbControllerHostMixin } from './controller-host.mixin.js';
|
||||
import { UmbControllerHostElement, UmbControllerHostElementMixin } from './controller-host-element.mixin.js';
|
||||
|
||||
export class UmbControllerHostInitializerElement
|
||||
extends UmbControllerHostMixin(HTMLElement)
|
||||
extends UmbControllerHostElementMixin(HTMLElement)
|
||||
implements UmbControllerHostElement
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { UmbControllerHostInitializerElement } from './controller-host-initializer.element.js';
|
||||
import { UmbControllerHostElement, UmbControllerHostMixin } from './controller-host.mixin.js';
|
||||
import { UmbControllerHostElement, UmbControllerHostElementMixin } from './controller-host-element.mixin.js';
|
||||
import { customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbContextConsumerController, UmbContextProviderController } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-test-controller-host-initializer-consumer')
|
||||
export class UmbTestControllerHostInitializerConsumerElement extends UmbControllerHostMixin(HTMLElement) {
|
||||
export class UmbTestControllerHostInitializerConsumerElement extends UmbControllerHostElementMixin(HTMLElement) {
|
||||
public value: string | null = null;
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { UmbController } from './controller.interface.js';
|
||||
|
||||
export declare class UmbControllerHost {
|
||||
hasController(controller: UmbController): boolean;
|
||||
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[];
|
||||
addController(controller: UmbController): void;
|
||||
removeControllerByAlias(unique: UmbController['controllerAlias']): void;
|
||||
removeController(controller: UmbController): void;
|
||||
getHostElement(): EventTarget;
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import type { UmbControllerInterface } from './controller.interface.js';
|
||||
|
||||
type HTMLElementConstructor<T = HTMLElement> = new (...args: any[]) => T;
|
||||
|
||||
export declare class UmbControllerHostElement extends HTMLElement {
|
||||
hasController(controller: UmbControllerInterface): boolean;
|
||||
getControllers(filterMethod: (ctrl: UmbControllerInterface) => boolean): UmbControllerInterface[];
|
||||
addController(controller: UmbControllerInterface): void;
|
||||
removeControllerByUnique(unique: UmbControllerInterface['unique']): void;
|
||||
removeController(controller: UmbControllerInterface): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This mixin enables the component to host controllers.
|
||||
* This is done by calling the `consumeContext` method.
|
||||
*
|
||||
* @param {Object} superClass - superclass to be extended.
|
||||
* @mixin
|
||||
*/
|
||||
export const UmbControllerHostMixin = <T extends HTMLElementConstructor>(superClass: T) => {
|
||||
class UmbContextConsumerClass extends superClass {
|
||||
#controllers: UmbControllerInterface[] = [];
|
||||
|
||||
#attached = false;
|
||||
|
||||
/**
|
||||
* Tests if a controller is assigned to this element.
|
||||
* @param {UmbControllerInterface} ctrl
|
||||
*/
|
||||
hasController(ctrl: UmbControllerInterface): boolean {
|
||||
return this.#controllers.indexOf(ctrl) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve controllers matching a filter of this element.
|
||||
* @param {method} filterMethod
|
||||
*/
|
||||
getControllers(filterMethod: (ctrl: UmbControllerInterface) => boolean): UmbControllerInterface[] {
|
||||
return this.#controllers.filter(filterMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a controller to this element.
|
||||
* @param {UmbControllerInterface} ctrl
|
||||
*/
|
||||
addController(ctrl: UmbControllerInterface): void {
|
||||
// Check if there is one already with same unique
|
||||
this.removeControllerByUnique(ctrl.unique);
|
||||
|
||||
this.#controllers.push(ctrl);
|
||||
if (this.#attached) {
|
||||
// If a controller is created on a already attached element, then it will be added directly. This might not be optimal. As the controller it self has not finished its constructor method jet. therefor i postpone the call:
|
||||
Promise.resolve().then(() => ctrl.hostConnected());
|
||||
//ctrl.hostConnected();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a controller from this element, by its unique/alias.
|
||||
* @param {unknown} unique/alias
|
||||
*/
|
||||
removeControllerByUnique(unique: UmbControllerInterface['unique']): void {
|
||||
if (unique) {
|
||||
this.#controllers.forEach((x) => {
|
||||
if (x.unique === unique) {
|
||||
this.removeController(x);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a controller from this element.
|
||||
* Notice this will also destroy the controller.
|
||||
* @param {UmbControllerInterface} ctrl
|
||||
*/
|
||||
removeController(ctrl: UmbControllerInterface): void {
|
||||
const index = this.#controllers.indexOf(ctrl);
|
||||
if (index !== -1) {
|
||||
this.#controllers.splice(index, 1);
|
||||
if (this.#attached) {
|
||||
ctrl.hostDisconnected();
|
||||
}
|
||||
ctrl.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a controller from this element by its alias.
|
||||
* Notice this will also destroy the controller.
|
||||
* @param {string} unique
|
||||
*/
|
||||
removeControllerByAlias(unique: string): void {
|
||||
this.#controllers.forEach((x) => {
|
||||
if (x.unique === unique) {
|
||||
this.removeController(x);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback?.();
|
||||
this.#attached = true;
|
||||
this.#controllers.forEach((ctrl: UmbControllerInterface) => ctrl.hostConnected());
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback?.();
|
||||
this.#attached = false;
|
||||
this.#controllers.forEach((ctrl: UmbControllerInterface) => ctrl.hostDisconnected());
|
||||
}
|
||||
}
|
||||
|
||||
return UmbContextConsumerClass as unknown as HTMLElementConstructor<UmbControllerHostElement> & T;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElement {
|
||||
connectedCallback(): void;
|
||||
disconnectedCallback(): void;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,23 @@
|
||||
import { UmbControllerHostElement } from './controller-host.mixin.js';
|
||||
import type { UmbControllerInterface } from './controller.interface.js';
|
||||
import { UmbClassMixin } from '../class-api/index.js';
|
||||
import { UmbControllerHost } from './controller-host.interface.js';
|
||||
import { UmbController } from './controller.interface.js';
|
||||
|
||||
export abstract class UmbController implements UmbControllerInterface {
|
||||
protected host?: UmbControllerHostElement;
|
||||
|
||||
private _alias?: string;
|
||||
public get unique() {
|
||||
return this._alias;
|
||||
/**
|
||||
* This mixin enables a web-component to host controllers.
|
||||
* This enables controllers to be added to the life cycle of this element.
|
||||
*
|
||||
*/
|
||||
export abstract class UmbBaseController extends UmbClassMixin(class {}) implements UmbController {
|
||||
constructor(host: UmbControllerHost, controllerAlias?: UmbController['controllerAlias']) {
|
||||
super(host, controllerAlias);
|
||||
this._host.addController(this);
|
||||
}
|
||||
|
||||
constructor(host: UmbControllerHostElement, alias?: string) {
|
||||
this.host = host;
|
||||
this._alias = alias;
|
||||
this.host.addController(this);
|
||||
}
|
||||
|
||||
abstract hostConnected(): void;
|
||||
abstract hostDisconnected(): void;
|
||||
|
||||
public destroy() {
|
||||
if (this.host) {
|
||||
this.host.removeController(this);
|
||||
if (this._host) {
|
||||
this._host.removeController(this);
|
||||
}
|
||||
delete this.host;
|
||||
//delete this.host;
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface UmbControllerInterface {
|
||||
get unique(): string | undefined;
|
||||
import type { UmbControllerAlias } from './controller-alias.type.js';
|
||||
export interface UmbController {
|
||||
get controllerAlias(): UmbControllerAlias;
|
||||
hostConnected(): void;
|
||||
hostDisconnected(): void;
|
||||
destroy(): void;
|
||||
|
||||
@@ -1,44 +1,198 @@
|
||||
import { expect } from '@open-wc/testing';
|
||||
import { UmbControllerHostElement, UmbControllerHostMixin } from './controller-host.mixin.js';
|
||||
import { UmbControllerHostElement, UmbControllerHostElementMixin } from './controller-host-element.mixin.js';
|
||||
import { UmbBaseController } from './controller.class.js';
|
||||
import { customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
class UmbTestContext {
|
||||
prop = 'value from provider';
|
||||
}
|
||||
|
||||
@customElement('test-my-controller-host')
|
||||
export class UmbTestControllerHostElement extends UmbControllerHostMixin(HTMLElement) {}
|
||||
export class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
|
||||
|
||||
describe('UmbContextProvider', () => {
|
||||
export class UmbTestControllerImplementationElement extends UmbBaseController {
|
||||
testIsConnected = false;
|
||||
testIsDestroyed = false;
|
||||
|
||||
hostConnected(): void {
|
||||
super.hostConnected();
|
||||
this.testIsConnected = true;
|
||||
}
|
||||
hostDisconnected(): void {
|
||||
super.hostDisconnected();
|
||||
this.testIsConnected = false;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
super.destroy();
|
||||
this.testIsDestroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
describe('UmbController', () => {
|
||||
type NewType = UmbControllerHostElement;
|
||||
|
||||
let hostElement: NewType;
|
||||
const contextInstance = new UmbTestContext();
|
||||
|
||||
beforeEach(() => {
|
||||
hostElement = document.createElement('test-my-controller-host') as UmbControllerHostElement;
|
||||
});
|
||||
|
||||
describe('Destroyed controllers is gone from host', () => {
|
||||
it('has a host property', () => {
|
||||
const ctrl = new UmbContextProviderController(hostElement, 'my-test-context', contextInstance);
|
||||
describe('Controller Host Public API', () => {
|
||||
describe('methods', () => {
|
||||
it('has an hasController method', () => {
|
||||
expect(hostElement).to.have.property('hasController').that.is.a('function');
|
||||
});
|
||||
it('has an getControllers method', () => {
|
||||
expect(hostElement).to.have.property('getControllers').that.is.a('function');
|
||||
});
|
||||
it('has an addController method', () => {
|
||||
expect(hostElement).to.have.property('addController').that.is.a('function');
|
||||
});
|
||||
it('has an removeControllerByAlias method', () => {
|
||||
expect(hostElement).to.have.property('removeControllerByAlias').that.is.a('function');
|
||||
});
|
||||
it('has an removeController method', () => {
|
||||
expect(hostElement).to.have.property('removeController').that.is.a('function');
|
||||
});
|
||||
it('has an hostConnected method', () => {
|
||||
expect(hostElement).to.have.property('hostConnected').that.is.a('function');
|
||||
});
|
||||
it('has an hostDisconnected method', () => {
|
||||
expect(hostElement).to.have.property('hostDisconnected').that.is.a('function');
|
||||
});
|
||||
it('has an destroy method', () => {
|
||||
expect(hostElement).to.have.property('destroy').that.is.a('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Controller Public API', () => {
|
||||
let controller: UmbTestControllerImplementationElement;
|
||||
beforeEach(() => {
|
||||
controller = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
it('has an controllerAlias property', () => {
|
||||
expect(controller).to.have.property('controllerAlias').that.is.a('string');
|
||||
});
|
||||
it('has an hasController method', () => {
|
||||
expect(controller).to.have.property('hasController').that.is.a('function');
|
||||
});
|
||||
it('has an getControllers method', () => {
|
||||
expect(controller).to.have.property('getControllers').that.is.a('function');
|
||||
});
|
||||
it('has an addController method', () => {
|
||||
expect(controller).to.have.property('addController').that.is.a('function');
|
||||
});
|
||||
it('has an removeControllerByAlias method', () => {
|
||||
expect(controller).to.have.property('removeControllerByAlias').that.is.a('function');
|
||||
});
|
||||
it('has an removeController method', () => {
|
||||
expect(controller).to.have.property('removeController').that.is.a('function');
|
||||
});
|
||||
it('has an hostConnected method', () => {
|
||||
expect(controller).to.have.property('hostConnected').that.is.a('function');
|
||||
});
|
||||
it('has an hostDisconnected method', () => {
|
||||
expect(controller).to.have.property('hostDisconnected').that.is.a('function');
|
||||
});
|
||||
it('has an destroy method', () => {
|
||||
expect(controller).to.have.property('destroy').that.is.a('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Controllers lifecycle', () => {
|
||||
it('controller is removed from host when destroyed', () => {
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl, 'my-test-context');
|
||||
|
||||
expect(hostElement.hasController(ctrl)).to.be.true;
|
||||
expect(ctrl.hasController(subCtrl)).to.be.true;
|
||||
|
||||
ctrl.destroy();
|
||||
|
||||
expect(hostElement.hasController(ctrl)).to.be.false;
|
||||
expect(ctrl.testIsConnected).to.be.false;
|
||||
expect(ctrl.testIsDestroyed).to.be.true;
|
||||
expect(ctrl.hasController(subCtrl)).to.be.false;
|
||||
expect(subCtrl.testIsConnected).to.be.false;
|
||||
expect(subCtrl.testIsDestroyed).to.be.true;
|
||||
});
|
||||
|
||||
it('controller is destroyed when removed from host', () => {
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl, 'my-test-context');
|
||||
|
||||
expect(ctrl.testIsDestroyed).to.be.false;
|
||||
expect(subCtrl.testIsDestroyed).to.be.false;
|
||||
expect(hostElement.hasController(ctrl)).to.be.true;
|
||||
expect(ctrl.hasController(subCtrl)).to.be.true;
|
||||
|
||||
hostElement.removeController(ctrl);
|
||||
|
||||
expect(ctrl.testIsDestroyed).to.be.true;
|
||||
expect(hostElement.hasController(ctrl)).to.be.false;
|
||||
expect(subCtrl.testIsDestroyed).to.be.true;
|
||||
expect(ctrl.hasController(subCtrl)).to.be.false;
|
||||
});
|
||||
|
||||
it('hostConnected & hostDisconnected is triggered accordingly to the state of the controller host.', () => {
|
||||
const ctrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
const subCtrl = new UmbTestControllerImplementationElement(ctrl, 'my-test-context');
|
||||
|
||||
expect(hostElement.hasController(ctrl)).to.be.true;
|
||||
expect(ctrl.hasController(subCtrl)).to.be.true;
|
||||
expect(ctrl.testIsConnected).to.be.false;
|
||||
expect(subCtrl.testIsConnected).to.be.false;
|
||||
|
||||
document.body.appendChild(hostElement);
|
||||
|
||||
expect(ctrl.testIsConnected).to.be.true;
|
||||
expect(subCtrl.testIsConnected).to.be.true;
|
||||
|
||||
document.body.removeChild(hostElement);
|
||||
|
||||
expect(ctrl.testIsConnected).to.be.false;
|
||||
expect(subCtrl.testIsConnected).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unique controllers replace each other', () => {
|
||||
it('has a host property', () => {
|
||||
const firstCtrl = new UmbContextProviderController(hostElement, 'my-test-context', contextInstance);
|
||||
const secondCtrl = new UmbContextProviderController(hostElement, 'my-test-context', new UmbTestContext());
|
||||
describe('Controllers against other Controller', () => {
|
||||
it('controller is replaced by another controller using the same string as controller-alias', () => {
|
||||
const firstCtrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
const secondCtrl = new UmbTestControllerImplementationElement(hostElement, 'my-test-context');
|
||||
|
||||
expect(hostElement.hasController(firstCtrl)).to.be.false;
|
||||
expect(hostElement.hasController(secondCtrl)).to.be.true;
|
||||
});
|
||||
|
||||
it('controller is replaced by another controller using the the same symbol as controller-alias', () => {
|
||||
const mySymbol = Symbol();
|
||||
const firstCtrl = new UmbTestControllerImplementationElement(hostElement, mySymbol);
|
||||
const secondCtrl = new UmbTestControllerImplementationElement(hostElement, mySymbol);
|
||||
|
||||
expect(hostElement.hasController(firstCtrl)).to.be.false;
|
||||
expect(hostElement.hasController(secondCtrl)).to.be.true;
|
||||
});
|
||||
|
||||
it('controller is not replacing another controller when using the undefined as controller-alias', () => {
|
||||
const firstCtrl = new UmbTestControllerImplementationElement(hostElement, undefined);
|
||||
const secondCtrl = new UmbTestControllerImplementationElement(hostElement, undefined);
|
||||
|
||||
expect(hostElement.hasController(firstCtrl)).to.be.true;
|
||||
expect(hostElement.hasController(secondCtrl)).to.be.true;
|
||||
});
|
||||
|
||||
it('sub controllers is not replacing another sub controllers when using the same controller-alias', () => {
|
||||
const mySymbol = Symbol();
|
||||
|
||||
const firstCtrl = new UmbTestControllerImplementationElement(hostElement, undefined);
|
||||
const secondCtrl = new UmbTestControllerImplementationElement(hostElement, undefined);
|
||||
|
||||
const firstSubCtrl = new UmbTestControllerImplementationElement(firstCtrl, mySymbol);
|
||||
const secondSubCtrl = new UmbTestControllerImplementationElement(secondCtrl, mySymbol);
|
||||
|
||||
expect(firstCtrl.hasController(firstSubCtrl)).to.be.true;
|
||||
expect(secondCtrl.hasController(secondSubCtrl)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
export * from './controller-host.mixin.js';
|
||||
export * from './controller-host-base.mixin.js';
|
||||
export * from './controller-host.interface.js';
|
||||
export * from './controller-host-element.mixin.js';
|
||||
export * from './controller.class.js';
|
||||
export * from './controller.interface.js';
|
||||
export * from './controller-alias.type.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import type { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbControllerHostMixin } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import {
|
||||
UmbContextToken,
|
||||
@@ -10,12 +10,7 @@ import {
|
||||
} from '@umbraco-cms/backoffice/context-api';
|
||||
import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
// TODO: can we use this aliases to generate the key of this type
|
||||
interface ResolvedContexts {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export declare class UmbElementMixinInterface extends UmbControllerHostElement {
|
||||
export declare class UmbElement extends UmbControllerHostElement {
|
||||
observe<T>(
|
||||
source: Observable<T> | { asObservable: () => Observable<T> },
|
||||
callback: (_value: T) => void,
|
||||
@@ -26,11 +21,10 @@ export declare class UmbElementMixinInterface extends UmbControllerHostElement {
|
||||
alias: string | UmbContextToken<R>,
|
||||
callback: UmbContextCallback<R>
|
||||
): UmbContextConsumerController<R>;
|
||||
consumeAllContexts(contextAliases: string[], callback: (_instances: ResolvedContexts) => void): void;
|
||||
}
|
||||
|
||||
export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T) => {
|
||||
class UmbElementMixinClass extends UmbControllerHostMixin(superClass) implements UmbElementMixinInterface {
|
||||
class UmbElementMixinClass extends UmbControllerHostElementMixin(superClass) implements UmbElement {
|
||||
/**
|
||||
* @description Observe a RxJS source of choice.
|
||||
* @param {Observable<T>} source RxJS source
|
||||
@@ -75,34 +69,7 @@ export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T)
|
||||
): UmbContextConsumerController<R> {
|
||||
return new UmbContextConsumerController(this, alias, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Setup a subscription for multiple contexts. The callback is called when all contexts are resolved.
|
||||
* @param {string} aliases
|
||||
* @param {method} callback Callback method called when all contexts are resolved.
|
||||
* @memberof UmbElementMixin
|
||||
* @deprecated it should not be necessary to consume multiple contexts at once, use consumeContext instead with an UmbContextToken
|
||||
*/
|
||||
consumeAllContexts(_contextAliases: Array<string>, callback: (_instances: ResolvedContexts) => void) {
|
||||
let resolvedAmount = 0;
|
||||
const controllers = _contextAliases.map(
|
||||
(alias) =>
|
||||
new UmbContextConsumerController(this, alias, () => {
|
||||
resolvedAmount++;
|
||||
|
||||
if (resolvedAmount === _contextAliases.length) {
|
||||
const result: ResolvedContexts = {};
|
||||
|
||||
controllers.forEach((contextCtrl: UmbContextConsumerController) => {
|
||||
result[contextCtrl.consumerAlias?.toString()] = contextCtrl.instance;
|
||||
});
|
||||
|
||||
callback(result);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return UmbElementMixinClass as unknown as HTMLElementConstructor<UmbElementMixinInterface> & T;
|
||||
return UmbElementMixinClass as unknown as HTMLElementConstructor<UmbElement> & T;
|
||||
};
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import type { ManifestBundle } from './types.js';
|
||||
import type { ManifestBase, ManifestBundle } from './types.js';
|
||||
import { loadExtension } from './load-extension.function.js';
|
||||
import { UmbExtensionRegistry } from './registry/extension.registry.js';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbBundleExtensionInitializer {
|
||||
#host;
|
||||
#extensionRegistry;
|
||||
#bundleMap = new Map();
|
||||
|
||||
constructor(host: UmbControllerHostElement, extensionRegistry: UmbExtensionRegistry<ManifestBundle>) {
|
||||
this.#host = host;
|
||||
this.#extensionRegistry = extensionRegistry;
|
||||
extensionRegistry.extensionsOfType('bundle').subscribe((bundles) => {
|
||||
// Unregister removed bundles:
|
||||
this.#bundleMap.forEach((existingBundle) => {
|
||||
if (!bundles.find((b) => b.alias === existingBundle.alias)) {
|
||||
this.unregisterBundle(existingBundle);
|
||||
this.#bundleMap.delete(existingBundle.alias);
|
||||
}
|
||||
});
|
||||
|
||||
// Register new bundles:
|
||||
bundles.forEach((bundle) => {
|
||||
if (this.#bundleMap.has(bundle.alias)) return;
|
||||
this.#bundleMap.set(bundle.alias, bundle);
|
||||
// TODO: Should we unInit a entry point if is removed?
|
||||
this.instantiateBundle(bundle);
|
||||
});
|
||||
});
|
||||
@@ -36,4 +42,20 @@ export class UmbBundleExtensionInitializer {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async unregisterBundle(manifest: ManifestBundle) {
|
||||
const js = await loadExtension(manifest);
|
||||
|
||||
if (js) {
|
||||
Object.keys(js).forEach((key) => {
|
||||
const value = js[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
this.#extensionRegistry.unregisterMany(value.map((v) => v.alias));
|
||||
} else if (typeof value === 'object') {
|
||||
this.#extensionRegistry.unregister((value as ManifestBase).alias);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import type { ManifestEntryPoint } from './types.js';
|
||||
import { hasInitExport } from './has-init-export.function.js';
|
||||
import { loadExtension } from './load-extension.function.js';
|
||||
import { UmbExtensionRegistry } from './registry/extension.registry.js';
|
||||
import { UmbElementMixinInterface } from '@umbraco-cms/backoffice/element-api';
|
||||
import { UmbElement } from '@umbraco-cms/backoffice/element-api';
|
||||
|
||||
export class UmbEntryPointExtensionInitializer {
|
||||
#host;
|
||||
#extensionRegistry;
|
||||
#entryPointMap = new Map();
|
||||
|
||||
constructor(host: UmbElementMixinInterface, extensionRegistry: UmbExtensionRegistry<ManifestEntryPoint>) {
|
||||
constructor(host: UmbElement, extensionRegistry: UmbExtensionRegistry<ManifestEntryPoint>) {
|
||||
this.#host = host;
|
||||
this.#extensionRegistry = extensionRegistry;
|
||||
extensionRegistry.extensionsOfType('entryPoint').subscribe((entryPoints) => {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { UmbExtensionRegistry } from './registry/extension.registry.js';
|
||||
import { ManifestBase } from './types.js';
|
||||
import type { UmbElementMixinInterface } from '@umbraco-cms/backoffice/element-api';
|
||||
import type { UmbElement } from '@umbraco-cms/backoffice/element-api';
|
||||
|
||||
export type UmbEntryPointOnInit = (
|
||||
host: UmbElementMixinInterface,
|
||||
extensionRegistry: UmbExtensionRegistry<ManifestBase>
|
||||
) => void;
|
||||
export type UmbEntryPointOnInit = (host: UmbElement, extensionRegistry: UmbExtensionRegistry<ManifestBase>) => void;
|
||||
|
||||
/**
|
||||
* Interface containing supported life-cycle functions for ESModule entry points
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { UmbEntryPointModule } from './umb-lifecycle.interface.js';
|
||||
import type { UmbEntryPointModule } from './entry-point.interface.js';
|
||||
|
||||
/**
|
||||
* Validate if an ESModule exports a known init function called 'onInit'
|
||||
|
||||
@@ -9,4 +9,4 @@ export * from './load-extension.function.js';
|
||||
export * from './registry/extension.registry.js';
|
||||
export * from './type-guards/index.js';
|
||||
export * from './types.js';
|
||||
export * from './umb-lifecycle.interface.js';
|
||||
export * from './entry-point.interface.js';
|
||||
|
||||
@@ -55,7 +55,11 @@ describe('UmbExtensionRegistry', () => {
|
||||
it('should register an extension', () => {
|
||||
const registeredExtensions = extensionRegistry['_extensions'].getValue();
|
||||
expect(registeredExtensions).to.have.lengthOf(4);
|
||||
expect(registeredExtensions?.[0]?.name).to.eq('test-section-1');
|
||||
expect(registeredExtensions?.[0]?.alias).to.eq('Umb.Test.Section.1');
|
||||
});
|
||||
|
||||
it('should say that an extension is registered', () => {
|
||||
expect(extensionRegistry.isRegistered('Umb.Test.Section.1')).to.be.true;
|
||||
});
|
||||
|
||||
it('should get an extension by alias', (done) => {
|
||||
@@ -96,7 +100,7 @@ describe('UmbExtensionRegistry', () => {
|
||||
.unsubscribe();
|
||||
});
|
||||
|
||||
it('Observable only trigged when changes made to the scope of it', (done) => {
|
||||
it('should only trigger observable when changes made to the scope of it', (done) => {
|
||||
let amountOfTimesTriggered = -1;
|
||||
let lastAmount = 0;
|
||||
|
||||
@@ -218,6 +222,10 @@ describe('UmbExtensionRegistry with kinds', () => {
|
||||
manifests.forEach((manifest) => extensionRegistry.register(manifest));
|
||||
});
|
||||
|
||||
it('should say that an extension kind is registered', () => {
|
||||
expect(extensionRegistry.isRegistered('Umb.Test.Kind')).to.be.true;
|
||||
});
|
||||
|
||||
it('should merge with kinds', (done) => {
|
||||
extensionRegistry
|
||||
.extensionsOfType('section')
|
||||
@@ -225,6 +233,7 @@ describe('UmbExtensionRegistry with kinds', () => {
|
||||
expect(extensions).to.have.lengthOf(3);
|
||||
expect(extensions?.[0]?.elementName).to.not.eq('my-kind-element');
|
||||
expect(extensions?.[1]?.alias).to.eq('Umb.Test.Section.3');
|
||||
expect(extensions?.[1]?.meta.label).to.eq('Test Section 3');
|
||||
expect(extensions?.[1]?.elementName).to.eq('my-kind-element');
|
||||
expect(extensions?.[2]?.alias).to.eq('Umb.Test.Section.1');
|
||||
expect(extensions?.[2]?.elementName).to.eq('my-kind-element');
|
||||
@@ -233,4 +242,49 @@ describe('UmbExtensionRegistry with kinds', () => {
|
||||
})
|
||||
.unsubscribe();
|
||||
});
|
||||
|
||||
it('should update extensions using kinds, when a kind appears', (done) => {
|
||||
let amountOfTimesTriggered = -1;
|
||||
|
||||
extensionRegistry.unregister('Umb.Test.Kind');
|
||||
|
||||
extensionRegistry
|
||||
.extensionsOfType('section')
|
||||
.subscribe((extensions) => {
|
||||
amountOfTimesTriggered++;
|
||||
expect(extensions).to.have.lengthOf(3);
|
||||
|
||||
if (amountOfTimesTriggered === 0) {
|
||||
expect(extensions?.[2]?.meta.label).to.be.undefined;
|
||||
expect(extensionRegistry.isRegistered('Umb.Test.Kind')).to.be.false;
|
||||
extensionRegistry.register(manifests[0]); // Registration of the kind again.
|
||||
expect(extensionRegistry.isRegistered('Umb.Test.Kind')).to.be.true;
|
||||
} else if (amountOfTimesTriggered === 1) {
|
||||
expect(extensions?.[2]?.meta.label).to.eq('my-kind-meta-label');
|
||||
done();
|
||||
}
|
||||
})
|
||||
.unsubscribe();
|
||||
});
|
||||
|
||||
it('should update extensions using kinds, when a kind is removed', (done) => {
|
||||
let amountOfTimesTriggered = -1;
|
||||
|
||||
extensionRegistry
|
||||
.extensionsOfType('section')
|
||||
.subscribe((extensions) => {
|
||||
amountOfTimesTriggered++;
|
||||
expect(extensions).to.have.lengthOf(3);
|
||||
|
||||
if (amountOfTimesTriggered === 0) {
|
||||
expect(extensions?.[2]?.meta.label).to.not.be.undefined;
|
||||
extensionRegistry.unregister('Umb.Test.Kind');
|
||||
expect(extensionRegistry.isRegistered('Umb.Test.Kind')).to.be.false;
|
||||
} else if (amountOfTimesTriggered === 1) {
|
||||
expect(extensions?.[2]?.meta.label).to.be.undefined;
|
||||
done();
|
||||
}
|
||||
})
|
||||
.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { ManifestTypeMap, ManifestBase, SpecificManifestTypeOrManifestBase, ManifestKind } from '../types.js';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
map,
|
||||
Observable,
|
||||
distinctUntilChanged,
|
||||
combineLatest,
|
||||
} from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { map, Observable, distinctUntilChanged, combineLatest } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
|
||||
function extensionArrayMemoization<T extends { alias: string }>(
|
||||
function extensionArrayMemoization<T extends Pick<ManifestBase, 'alias'>>(
|
||||
previousValue: Array<T>,
|
||||
currentValue: Array<T>
|
||||
): boolean {
|
||||
@@ -22,6 +17,34 @@ function extensionArrayMemoization<T extends { alias: string }>(
|
||||
return true;
|
||||
}
|
||||
|
||||
// Note: Keeping the memoization in two separate function, for performance concern.
|
||||
function extensionAndKindMatchArrayMemoization<T extends Pick<ManifestBase, 'alias'> & { isMatchedWithKind?: boolean }>(
|
||||
previousValue: Array<T>,
|
||||
currentValue: Array<T>
|
||||
): boolean {
|
||||
// If length is different, data is different:
|
||||
if (previousValue.length !== currentValue.length) {
|
||||
return false;
|
||||
}
|
||||
// previousValue has an alias that is not present in currentValue:
|
||||
/* !! This is covered by the test below:
|
||||
if (previousValue.find((p) => !currentValue.find((c) => c.alias === p.alias))) {
|
||||
return false;
|
||||
}*/
|
||||
// if previousValue has different meta values:
|
||||
if (
|
||||
currentValue.find((newValue: T) => {
|
||||
const oldValue = previousValue.find((c) => c.alias === newValue.alias);
|
||||
// First check if we found a previous value, matching this alias.
|
||||
// Then checking isMatchedWithKind, as this is much more performant than checking the whole object. (I assume the only change happening to an extension is the match with a kind, we do not want to watch for other changes)
|
||||
return oldValue === undefined || newValue.isMatchedWithKind !== oldValue.isMatchedWithKind;
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function extensionSingleMemoization<T extends { alias: string }>(
|
||||
previousValue: T | undefined,
|
||||
currentValue: T | undefined
|
||||
@@ -40,11 +63,10 @@ export class UmbExtensionRegistry<
|
||||
> {
|
||||
readonly MANIFEST_TYPES: ManifestTypes = undefined as never;
|
||||
|
||||
// TODO: Use UniqueBehaviorSubject, as we don't want someone to edit data of extensions.
|
||||
private _extensions = new BehaviorSubject<Array<ManifestTypes>>([]);
|
||||
private _extensions = new UmbBasicState<Array<ManifestTypes>>([]);
|
||||
public readonly extensions = this._extensions.asObservable();
|
||||
|
||||
private _kinds = new BehaviorSubject<Array<ManifestKind<ManifestTypes>>>([]);
|
||||
private _kinds = new UmbBasicState<Array<ManifestKind<ManifestTypes>>>([]);
|
||||
public readonly kinds = this._kinds.asObservable();
|
||||
|
||||
defineKind(kind: ManifestKind<ManifestTypes>) {
|
||||
@@ -84,22 +106,28 @@ export class UmbExtensionRegistry<
|
||||
manifests.forEach((manifest) => this.register(manifest));
|
||||
}
|
||||
|
||||
unregisterMany(aliases: Array<string>): void {
|
||||
aliases.forEach((alias) => this.unregister(alias));
|
||||
}
|
||||
|
||||
unregister(alias: string): void {
|
||||
const oldExtensionsValues = this._extensions.getValue();
|
||||
const newExtensionsValues = oldExtensionsValues.filter((extension) => extension.alias !== alias);
|
||||
|
||||
// TODO: Maybe its not needed to fire an console.error. as you might want to call this method without needing to check the existence first.
|
||||
if (oldExtensionsValues.length === newExtensionsValues.length) {
|
||||
console.error(`Unable to unregister extension with alias ${alias}`);
|
||||
return;
|
||||
}
|
||||
const newKindsValues = this._kinds.getValue().filter((kind) => kind.alias !== alias);
|
||||
const newExtensionsValues = this._extensions.getValue().filter((extension) => extension.alias !== alias);
|
||||
|
||||
this._kinds.next(newKindsValues);
|
||||
this._extensions.next(newExtensionsValues);
|
||||
}
|
||||
|
||||
isRegistered(alias: string): boolean {
|
||||
const values = this._extensions.getValue();
|
||||
return values.some((ext) => ext.alias === alias);
|
||||
if (this._extensions.getValue().find((ext) => ext.alias === alias)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._kinds.getValue().find((ext) => ext.alias === alias)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -155,7 +183,7 @@ export class UmbExtensionRegistry<
|
||||
if (ext) {
|
||||
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
|
||||
if (baseManifest) {
|
||||
const merged = { ...baseManifest, ...ext } as any;
|
||||
const merged = { isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
if ((baseManifest as any).meta) {
|
||||
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
|
||||
}
|
||||
@@ -179,7 +207,7 @@ export class UmbExtensionRegistry<
|
||||
// Specific Extension Meta merge (does not merge conditions)
|
||||
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
|
||||
if (baseManifest) {
|
||||
const merged = { ...baseManifest, ...ext } as any;
|
||||
const merged = { isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
if ((baseManifest as any).meta) {
|
||||
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
|
||||
}
|
||||
@@ -189,7 +217,7 @@ export class UmbExtensionRegistry<
|
||||
})
|
||||
.sort(sortExtensions)
|
||||
),
|
||||
distinctUntilChanged(extensionArrayMemoization)
|
||||
distinctUntilChanged(extensionAndKindMatchArrayMemoization)
|
||||
) as Observable<Array<T>>;
|
||||
}
|
||||
|
||||
@@ -204,7 +232,7 @@ export class UmbExtensionRegistry<
|
||||
if (ext) {
|
||||
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
|
||||
if (baseManifest) {
|
||||
const merged = { ...baseManifest, ...ext } as any;
|
||||
const merged = { isMatchedWithKind: true, ...baseManifest, ...ext } as any;
|
||||
if ((baseManifest as any).meta) {
|
||||
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
|
||||
}
|
||||
@@ -215,7 +243,7 @@ export class UmbExtensionRegistry<
|
||||
})
|
||||
.sort(sortExtensions)
|
||||
),
|
||||
distinctUntilChanged(extensionArrayMemoization)
|
||||
distinctUntilChanged(extensionAndKindMatchArrayMemoization)
|
||||
) as Observable<Array<ExtensionTypes>>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { UmbExtensionRegistry } from './registry/extension.registry.js';
|
||||
import { UmbEntryPointModule } from './entry-point.interface.js';
|
||||
import { UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type HTMLElementConstructor<T = HTMLElement> = new (...args: any[]) => T;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ClassConstructor<T> = new (...args: any[]) => T;
|
||||
export type ClassConstructor<T = object> = new (...args: any[]) => T;
|
||||
|
||||
export type ManifestTypeMap<ManifestTypes extends ManifestBase> = {
|
||||
[Manifest in ManifestTypes as Manifest['type']]: Manifest;
|
||||
@@ -156,10 +155,7 @@ export interface ManifestWithMeta extends ManifestBase {
|
||||
* This type of extension gives full control and will simply load the specified JS file
|
||||
* You could have custom logic to decide which extensions to load/register by using extensionRegistry
|
||||
*/
|
||||
export interface ManifestEntryPoint
|
||||
extends ManifestWithLoader<{
|
||||
onInit: (host: UmbControllerHostElement, extensionApi: UmbBackofficeExtensionRegistry) => void;
|
||||
}> {
|
||||
export interface ManifestEntryPoint extends ManifestWithLoader<UmbEntryPointModule> {
|
||||
type: 'entryPoint';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BehaviorSubject } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
|
||||
interface UmbClassStateData {
|
||||
equal(otherClass: UmbClassStateData): boolean;
|
||||
equal(otherClass: UmbClassStateData | undefined): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10,7 +10,7 @@ interface UmbClassStateData {
|
||||
* @extends {BehaviorSubject<T>}
|
||||
* @description - A RxJS BehaviorSubject which can hold class instance which has a equal method to compare in coming instances for changes.
|
||||
*/
|
||||
export class UmbClassState<T extends UmbClassStateData | undefined | null> extends BehaviorSubject<T> {
|
||||
export class UmbClassState<T extends UmbClassStateData | undefined> extends BehaviorSubject<T> {
|
||||
constructor(initialData: T) {
|
||||
super(initialData);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { UmbObserver } from './observer.js';
|
||||
import { Observable } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { UmbControllerInterface, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbController, UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbObserverController<T = unknown> extends UmbObserver<T> implements UmbControllerInterface {
|
||||
_alias?: string;
|
||||
public get unique() {
|
||||
export class UmbObserverController<T = unknown> extends UmbObserver<T> implements UmbController {
|
||||
_alias?: UmbControllerAlias;
|
||||
public get controllerAlias() {
|
||||
return this._alias;
|
||||
}
|
||||
|
||||
constructor(host: UmbControllerHostElement, source: Observable<T>, callback: (_value: T) => void, alias?: string) {
|
||||
constructor(
|
||||
host: UmbControllerHost,
|
||||
source: Observable<T>,
|
||||
callback: (_value: T) => void,
|
||||
alias?: UmbControllerAlias
|
||||
) {
|
||||
super(source, callback);
|
||||
this._alias = alias;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
|
||||
import { UmbData } from './data.js';
|
||||
import { UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
|
||||
import { PagedUserResponseModel, UserResponseModel, UserStateModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
|
||||
// Temp mocked database
|
||||
|
||||
@@ -11,4 +11,11 @@ export const handlers = [
|
||||
ctx.json<ProfilingStatusResponseModel>({ enabled: true })
|
||||
);
|
||||
}),
|
||||
|
||||
rest.put(umbracoPath('/profiling/status'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200)
|
||||
);
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { type UmbDataTypeConfigProperty, type UmbDataTypeConfig } from '../../property-editors/index.js';
|
||||
import { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
|
||||
/**
|
||||
* Extends Array to add utility functions for accessing data type properties
|
||||
* by alias, returning either the value or the complete DataTypePropertyPresentationModel object
|
||||
*/
|
||||
export class UmbDataTypePropertyCollection extends Array<DataTypePropertyPresentationModel> {
|
||||
constructor(args: Array<DataTypePropertyPresentationModel> = []) {
|
||||
export class UmbDataTypeConfigCollection extends Array<UmbDataTypeConfigProperty> {
|
||||
constructor(args: UmbDataTypeConfig) {
|
||||
super(...args);
|
||||
}
|
||||
static get [Symbol.species](): ArrayConstructor {
|
||||
@@ -43,4 +44,15 @@ export class UmbDataTypePropertyCollection extends Array<DataTypePropertyPresent
|
||||
toObject(): Record<string, unknown> {
|
||||
return Object.fromEntries(this.map((x) => [x.alias, x.value]));
|
||||
}
|
||||
|
||||
equal(other: UmbDataTypeConfigCollection | undefined): boolean {
|
||||
if (this.length !== other?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.every((x) => {
|
||||
const otherProperty = x.alias ? other.getByAlias(x.alias!) : undefined;
|
||||
return otherProperty && x.value === otherProperty.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { renderEditor, type tinymce } from '@umbraco-cms/backoffice/external/tin
|
||||
import { UMB_AUTH, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth';
|
||||
import {
|
||||
TinyMcePluginArguments,
|
||||
UmbDataTypePropertyCollection,
|
||||
UmbDataTypeConfigCollection,
|
||||
UmbTinyMcePluginBase,
|
||||
} from '@umbraco-cms/backoffice/components';
|
||||
import { ClassConstructor, hasDefaultExport, loadExtension } from '@umbraco-cms/backoffice/extension-api';
|
||||
@@ -29,8 +29,8 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
// TODO => integrate macro picker, update stylesheet fetch when backend CLI exists (ref tinymce.service.js in existing backoffice)
|
||||
@customElement('umb-input-tiny-mce')
|
||||
export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
@property({ type: Object })
|
||||
configuration?: UmbDataTypePropertyCollection;
|
||||
@property({ attribute: false })
|
||||
configuration?: UmbDataTypeConfigCollection;
|
||||
|
||||
@state()
|
||||
private _tinyConfig: tinymce.RawEditorOptions = {};
|
||||
@@ -43,7 +43,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
#editorRef?: tinymce.Editor | null = null;
|
||||
|
||||
protected getFormElement() {
|
||||
return undefined;
|
||||
return this._editorElement?.querySelector('iframe') ?? undefined;
|
||||
}
|
||||
|
||||
@query('#editor', true)
|
||||
@@ -80,6 +80,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this.#editorRef) {
|
||||
// TODO: Test if there is any problems with destroying the RTE here, but not initializing on connectedCallback. (firstUpdated is only called first time the element is rendered, not when it is reconnected)
|
||||
this.#editorRef.destroy();
|
||||
}
|
||||
}
|
||||
@@ -104,6 +105,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
|
||||
async #setTinyConfig() {
|
||||
// create an object by merging the configuration onto the fallback config
|
||||
// TODO: Seems like a too tight coupling between DataTypeConfigCollection and TinyMceConfig, I would love it begin more explicit what we take from DataTypeConfigCollection and parse on, but I understand that this gives some flexibility. Is this flexibility on purpose?
|
||||
const configurationOptions: Record<string, any> = {
|
||||
...defaultFallbackConfig,
|
||||
...(this.configuration ? this.configuration?.toObject() : {}),
|
||||
@@ -203,7 +205,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
// Plugins require a reference to the current editor as a param, so can not
|
||||
// be instantiated until we have an editor
|
||||
for (const plugin of this.#plugins) {
|
||||
new plugin({ host: this, editor });
|
||||
new plugin({ host: this, editor });
|
||||
}
|
||||
|
||||
// define keyboard shortcuts
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { UmbDataTypePropertyCollection, UmbInputTinyMceElement } from '@umbraco-cms/backoffice/components';
|
||||
import type { UmbDataTypeConfigCollection, UmbInputTinyMceElement } from '@umbraco-cms/backoffice/components';
|
||||
import type { tinymce } from '@umbraco-cms/backoffice/external/tinymce';
|
||||
|
||||
export class UmbTinyMcePluginBase {
|
||||
host: UmbInputTinyMceElement;
|
||||
editor: tinymce.Editor;
|
||||
configuration?: UmbDataTypePropertyCollection;
|
||||
configuration?: UmbDataTypeConfigCollection;
|
||||
|
||||
constructor(arg: TinyMcePluginArguments) {
|
||||
this.host = arg.host;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { UmbDataTypeConfig, type UmbDataTypeConfigProperty } from '../../property-editors/index.js';
|
||||
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { css, html, ifDefined, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbDataTypeRepository } from '@umbraco-cms/backoffice/data-type';
|
||||
import { UmbDocumentWorkspaceContext } from '@umbraco-cms/backoffice/document';
|
||||
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
|
||||
import type {
|
||||
DataTypeResponseModel,
|
||||
DataTypePropertyPresentationModel,
|
||||
PropertyTypeModelBaseModel,
|
||||
} from '@umbraco-cms/backoffice/backend-api';
|
||||
import type { DataTypeResponseModel, PropertyTypeModelBaseModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
|
||||
@@ -32,7 +29,7 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
|
||||
private _propertyEditorUiAlias?: string;
|
||||
|
||||
@state()
|
||||
private _dataTypeData: DataTypePropertyPresentationModel[] = [];
|
||||
private _dataTypeData?: UmbDataTypeConfig;
|
||||
|
||||
private _dataTypeRepository: UmbDataTypeRepository = new UmbDataTypeRepository(this);
|
||||
private _dataTypeObserver?: UmbObserverController<DataTypeResponseModel | undefined>;
|
||||
@@ -92,7 +89,7 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
|
||||
this._dataTypeObserver = this.observe(
|
||||
await this._dataTypeRepository.byId(dataTypeId),
|
||||
(dataType) => {
|
||||
this._dataTypeData = dataType?.values || [];
|
||||
this._dataTypeData = dataType?.values;
|
||||
this._propertyEditorUiAlias = dataType?.propertyEditorUiAlias || undefined;
|
||||
// If there is no UI, we will look up the Property editor model to find the default UI alias:
|
||||
if (!this._propertyEditorUiAlias && dataType?.propertyEditorAlias) {
|
||||
@@ -102,7 +99,7 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
|
||||
(extension) => {
|
||||
if (!extension) return;
|
||||
this._propertyEditorUiAlias = extension?.meta.defaultPropertyEditorUiAlias;
|
||||
this.removeControllerByUnique('_observePropertyEditorSchema');
|
||||
this.removeControllerByAlias('_observePropertyEditorSchema');
|
||||
},
|
||||
'_observePropertyEditorSchema'
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
PropertyTypeModelBaseModel,
|
||||
DocumentTypeResponseModel,
|
||||
} from '@umbraco-cms/backoffice/backend-api';
|
||||
import { UmbControllerHostElement, UmbControllerInterface } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbControllerHostElement, UmbController } from '@umbraco-cms/backoffice/controller-api';
|
||||
import {
|
||||
UmbArrayState,
|
||||
UmbObserverController,
|
||||
@@ -29,7 +29,7 @@ export class UmbContentTypePropertyStructureManager<R extends UmbDetailRepositor
|
||||
#contentTypeRepository: R;
|
||||
|
||||
#ownerDocumentTypeId?: string;
|
||||
#documentTypeObservers = new Array<UmbControllerInterface>();
|
||||
#documentTypeObservers = new Array<UmbController>();
|
||||
#documentTypes = new UmbArrayState<T>([], (x) => x.id);
|
||||
readonly documentTypes = this.#documentTypes.asObservable();
|
||||
private readonly _documentTypeContainers = this.#documentTypes.getObservablePart((x) =>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createExtensionClass } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UmbBaseController, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
/**
|
||||
* Initializes extension classes for a host element.
|
||||
* Extension class will be given one argument, the host element.
|
||||
*
|
||||
* @param host The host element to initialize extension classes for.
|
||||
* @param extensionTypes The extension types(strings) to initialize.
|
||||
*
|
||||
*/
|
||||
export class UmbClassExtensionsInitializer extends UmbBaseController {
|
||||
#extensionMap = new Map();
|
||||
|
||||
constructor(host: UmbControllerHostElement, extensionTypes: Array<string>) {
|
||||
super(host);
|
||||
|
||||
this.observe(umbExtensionsRegistry.extensionsOfTypes(extensionTypes), (extensions) => {
|
||||
if (!extensions) return;
|
||||
|
||||
extensions.forEach((extension) => {
|
||||
if (this.#extensionMap.has(extension.alias)) return;
|
||||
|
||||
// Instantiate and provide extension JS class. For Context API the classes provide them selfs when the class instantiates.
|
||||
this.#extensionMap.set(extension.alias, createExtensionClass(extension, [this._host]));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.#extensionMap.forEach((extension) => {
|
||||
extension.destroy();
|
||||
});
|
||||
this.#extensionMap.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
import type { ManifestTypes } from './models/index.js';
|
||||
import { umbExtensionsRegistry } from './registry.js';
|
||||
import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
|
||||
import {
|
||||
createExtensionClass,
|
||||
ManifestBase,
|
||||
ManifestClass,
|
||||
SpecificManifestTypeOrManifestBase,
|
||||
} from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbBaseController, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
export class UmbExtensionClassInitializer<
|
||||
ExtensionType extends string = string,
|
||||
ExtensionManifest extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, ExtensionType>,
|
||||
ExtensionClassInterface = ExtensionManifest extends ManifestClass ? ExtensionManifest['CLASS_TYPE'] : unknown
|
||||
> {
|
||||
#observable;
|
||||
> extends UmbBaseController {
|
||||
#currentPromise?: Promise<ExtensionClassInterface | undefined>;
|
||||
#currentPromiseResolver?: (value: ExtensionClassInterface | undefined) => void;
|
||||
#currentClass?: ExtensionClassInterface;
|
||||
|
||||
constructor(
|
||||
host: UmbControllerHostElement,
|
||||
@@ -24,16 +23,20 @@ export class UmbExtensionClassInitializer<
|
||||
extensionAlias: string,
|
||||
callback: (extensionClass: ExtensionClassInterface | undefined) => void
|
||||
) {
|
||||
super(host);
|
||||
const source = umbExtensionsRegistry.getByTypeAndAlias(extensionType, extensionAlias);
|
||||
//TODO: The promise can probably be done in a cleaner way.
|
||||
this.#observable = new UmbObserverController(host, source, async (manifest) => {
|
||||
this.observe(source, async (manifest) => {
|
||||
if (!manifest) return;
|
||||
|
||||
try {
|
||||
const initializedClass = await createExtensionClass<ExtensionClassInterface>(manifest, [host]);
|
||||
callback(initializedClass);
|
||||
// Destroy the previous class if it exists, and if destroy method is an method on the class.
|
||||
(this.#currentClass as any)?.destroy?.();
|
||||
|
||||
this.#currentClass = await createExtensionClass<ExtensionClassInterface>(manifest, [host]);
|
||||
callback(this.#currentClass);
|
||||
if (this.#currentPromiseResolver) {
|
||||
this.#currentPromiseResolver(initializedClass);
|
||||
this.#currentPromiseResolver(this.#currentClass);
|
||||
this.#currentPromise = undefined;
|
||||
this.#currentPromiseResolver = undefined;
|
||||
}
|
||||
@@ -49,4 +52,10 @@ export class UmbExtensionClassInitializer<
|
||||
});
|
||||
return this.#currentPromise;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
super.destroy();
|
||||
// Destroy the current class if it exists, and if destroy method is an method on the class.
|
||||
(this.#currentClass as any)?.destroy?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './interfaces/index.js';
|
||||
export * from './models/index.js';
|
||||
export * from './registry.js';
|
||||
export * from './extension-class-initializer.js';
|
||||
export * from './class-extensions-initializer.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { UmbDataTypePropertyCollection } from '@umbraco-cms/backoffice/components';
|
||||
import type { UmbDataTypeConfigCollection } from '@umbraco-cms/backoffice/components';
|
||||
|
||||
export interface UmbPropertyEditorExtensionElement extends HTMLElement {
|
||||
value: unknown;
|
||||
config?: UmbDataTypePropertyCollection;
|
||||
config?: UmbDataTypeConfigCollection;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { ManifestClass } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export interface ManifestGlobalContext extends ManifestClass {
|
||||
type: 'globalContext';
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { ManifestDashboardCollection } from './dashboard-collection.model.j
|
||||
import type { ManifestEntityAction } from './entity-action.model.js';
|
||||
import type { ManifestEntityBulkAction } from './entity-bulk-action.model.js';
|
||||
import type { ManifestExternalLoginProvider } from './external-login-provider.model.js';
|
||||
import type { ManifestGlobalContext } from './global-context.model.js';
|
||||
import type { ManifestHeaderApp, ManifestHeaderAppButtonKind } from './header-app.model.js';
|
||||
import type { ManifestHealthCheck } from './health-check.model.js';
|
||||
import type { ManifestMenu } from './menu.model.js';
|
||||
@@ -34,6 +35,7 @@ export * from './dashboard.model.js';
|
||||
export * from './entity-action.model.js';
|
||||
export * from './entity-bulk-action.model.js';
|
||||
export * from './external-login-provider.model.js';
|
||||
export * from './global-context.model.js';
|
||||
export * from './header-app.model.js';
|
||||
export * from './health-check.model.js';
|
||||
export * from './menu-item.model.js';
|
||||
@@ -66,6 +68,7 @@ export type ManifestTypes =
|
||||
| ManifestEntityBulkAction
|
||||
| ManifestEntryPoint
|
||||
| ManifestExternalLoginProvider
|
||||
| ManifestGlobalContext
|
||||
| ManifestHeaderApp
|
||||
| ManifestHeaderAppButtonKind
|
||||
| ManifestHealthCheck
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UmbPropertyEditorExtensionElement } from '../interfaces/index.js';
|
||||
import { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
import { type UmbDataTypeConfig } from '../../property-editors/index.js';
|
||||
import type { ManifestElement, ManifestBase } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
export interface ManifestPropertyEditorUi extends ManifestElement<UmbPropertyEditorExtensionElement> {
|
||||
@@ -39,7 +39,7 @@ export interface PropertyEditorConfigProperty {
|
||||
description?: string;
|
||||
alias: string;
|
||||
propertyEditorUiAlias: string;
|
||||
config?: Array<DataTypePropertyPresentationModel>;
|
||||
config?: UmbDataTypeConfig;
|
||||
}
|
||||
|
||||
export interface PropertyEditorConfigDefaultData {
|
||||
|
||||
@@ -2,16 +2,20 @@ import { UmbBackofficeNotificationContainerElement, UmbBackofficeModalContainerE
|
||||
import { manifests as debugManifests } from './debug/manifests.js';
|
||||
import { manifests as propertyActionManifests } from './property-actions/manifests.js';
|
||||
import { manifests as propertyEditorManifests } from './property-editors/manifests.js';
|
||||
import { manifests as tinyMcePluginManifests } from './property-editors/uis/tiny-mce/plugins/manifests.js';
|
||||
import { manifests as tinyMcePluginManifests } from './property-editors/uis/tiny-mce/plugins/manifests.js';
|
||||
import { manifests as workspaceManifests } from './workspace/manifests.js';
|
||||
import { manifests as modalManifests } from './modal/common/manifests.js';
|
||||
import { UmbStoreExtensionInitializer } from './store/index.js';
|
||||
import { manifests as themeManifests } from './themes/manifests.js';
|
||||
|
||||
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
|
||||
import { UmbModalManagerContext, UMB_MODAL_MANAGER_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal';
|
||||
import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api';
|
||||
import type { UmbEntryPointOnInit } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import {
|
||||
ManifestTypes,
|
||||
UmbBackofficeManifestKind,
|
||||
UmbClassExtensionsInitializer,
|
||||
} from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
export * from './action/index.js';
|
||||
export * from './collection/index.js';
|
||||
@@ -43,10 +47,11 @@ const manifests: Array<ManifestTypes | UmbBackofficeManifestKind> = [
|
||||
...tinyMcePluginManifests,
|
||||
...workspaceManifests,
|
||||
...modalManifests,
|
||||
...themeManifests,
|
||||
];
|
||||
|
||||
export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => {
|
||||
new UmbStoreExtensionInitializer(host);
|
||||
new UmbClassExtensionsInitializer(host, ['globalContext', 'store', 'treeStore', 'itemStore']);
|
||||
|
||||
extensionRegistry.registerMany(manifests);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { BehaviorSubject } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { ManifestModal, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import type { UmbRouterSlotElement } from '@umbraco-cms/backoffice/router';
|
||||
import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
|
||||
import type { UmbControllerHostElement, UmbControllerInterface } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbControllerHostElement, UmbController } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbContextProvider, UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
@@ -41,9 +41,7 @@ type OptionalSubmitArgumentIfUndefined<T> = T extends undefined
|
||||
};
|
||||
|
||||
// TODO: consider splitting this into two separate handlers
|
||||
export class UmbModalContextClass<ModalData extends object = object, ModalResult = unknown>
|
||||
implements UmbControllerInterface
|
||||
{
|
||||
export class UmbModalContextClass<ModalData extends object = object, ModalResult = unknown> implements UmbController {
|
||||
#host: UmbControllerHostElement;
|
||||
|
||||
#submitPromise: Promise<ModalResult>;
|
||||
@@ -63,7 +61,7 @@ export class UmbModalContextClass<ModalData extends object = object, ModalResult
|
||||
public readonly type: UmbModalType = 'dialog';
|
||||
public readonly size: UUIModalSidebarSize = 'small';
|
||||
|
||||
public get unique() {
|
||||
public get controllerAlias() {
|
||||
return 'umbModalContext:' + this.key;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import { UmbModalRouteRegistration } from './modal-route-registration.js';
|
||||
import { UmbModalToken } from './token/index.js';
|
||||
import { UmbModalConfig } from './modal-manager.context.js';
|
||||
import { UMB_ROUTE_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/router';
|
||||
import type { UmbControllerHostElement, UmbControllerInterface } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type { UmbControllerHostElement, UmbController } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
export class UmbModalRouteRegistrationController<D extends object = object, R = any>
|
||||
extends UmbModalRouteRegistration<D, R>
|
||||
implements UmbControllerInterface
|
||||
implements UmbController
|
||||
{
|
||||
//#host: UmbControllerHostInterface;
|
||||
#init;
|
||||
@@ -19,7 +19,7 @@ export class UmbModalRouteRegistrationController<D extends object = object, R =
|
||||
#routeContext?: typeof UMB_ROUTE_CONTEXT_TOKEN.TYPE;
|
||||
#modalRegistration?: UmbModalRouteRegistration;
|
||||
|
||||
public get unique() {
|
||||
public get controllerAlias() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api';
|
||||
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbDeepState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbBaseController, type UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
export class UmbPropertyActionMenuContext {
|
||||
#isOpen = new UmbDeepState(false);
|
||||
export class UmbPropertyActionMenuContext extends UmbBaseController {
|
||||
#isOpen = new UmbBooleanState(false);
|
||||
public readonly isOpen = this.#isOpen.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHostElement) {
|
||||
new UmbContextProviderController(host, 'umbPropertyActionMenu', this);
|
||||
super(host);
|
||||
this.provideContext('umbPropertyActionMenu', this);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
|
||||
@@ -10,9 +10,8 @@ import '../property-action/property-action.element.js';
|
||||
|
||||
@customElement('umb-property-action-menu')
|
||||
export class UmbPropertyActionMenuElement extends UmbLitElement {
|
||||
// TODO: we need to investigate context api vs values props and events
|
||||
@property()
|
||||
public value?: string;
|
||||
@property({ attribute: false })
|
||||
public value?: unknown;
|
||||
|
||||
@property()
|
||||
set propertyEditorUiAlias(alias: string) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
|
||||
|
||||
import type { ManifestPropertyAction } from '@umbraco-cms/backoffice/extension-registry';
|
||||
|
||||
// TODO: Here is a problem. The UmbPropertyActionElement is used for the type of the Extension Element. But is also a component that renders the Extension Element...
|
||||
@customElement('umb-property-action')
|
||||
export class UmbPropertyActionElement extends LitElement implements UmbPropertyAction {
|
||||
private _propertyAction?: ManifestPropertyAction;
|
||||
@@ -18,8 +19,8 @@ export class UmbPropertyActionElement extends LitElement implements UmbPropertyA
|
||||
}
|
||||
|
||||
// TODO: we need to investigate context api vs values props and events
|
||||
@property()
|
||||
public value?: string;
|
||||
@property({ attribute: false })
|
||||
public value?: unknown;
|
||||
|
||||
@state()
|
||||
private _element?: UmbPropertyActionElement;
|
||||
@@ -28,6 +29,7 @@ export class UmbPropertyActionElement extends LitElement implements UmbPropertyA
|
||||
if (!this.propertyAction) return;
|
||||
|
||||
try {
|
||||
// TODO: Here is a problem. The UmbPropertyActionElement is used for the type of the Extension Element. But is also a component that renders the Extension Element...
|
||||
this._element = (await createExtensionElement(this.propertyAction)) as UmbPropertyActionElement | undefined;
|
||||
if (!this._element) return;
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface UmbPropertyAction extends HTMLElement {
|
||||
value?: string;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './property-value-change.event.js';
|
||||
@@ -0,0 +1,6 @@
|
||||
export class UmbPropertyValueChangeEvent extends Event {
|
||||
public constructor() {
|
||||
// mimics the native change event
|
||||
super('property-value-change', { bubbles: true, composed: false, cancelable: false });
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,2 @@
|
||||
export class UmbPropertyValueChangeEvent extends Event {
|
||||
public constructor() {
|
||||
// mimics the native change event
|
||||
super('property-value-change', { bubbles: true, composed: false, cancelable: false });
|
||||
}
|
||||
}
|
||||
export * from './types/index.js';
|
||||
export * from './events/index.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { manifests as propertyEditorSchemaManifests } from './models/manifests.js';
|
||||
import { manifests as propertyEditorSchemaManifests } from './schemas/manifests.js';
|
||||
import { manifests as propertyEditorUIManifests } from './uis/manifests.js';
|
||||
|
||||
export const manifests = [...propertyEditorSchemaManifests, ...propertyEditorUIManifests];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user