diff --git a/.eslintrc.json b/.eslintrc.json index acde9551ae..ca3b358d25 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -39,7 +39,7 @@ "import/no-unresolved": "off", "import/order": "warn", "import/no-self-import": "error", - "import/no-cycle": ["error", { "maxDepth": 2, "allowUnsafeDynamicCyclicDependency": true }], + "import/no-cycle": ["error", { "maxDepth": 6, "allowUnsafeDynamicCyclicDependency": true }], "local-rules/bad-type-import": "error", "local-rules/enforce-element-suffix-on-element-class-name": "error", "local-rules/enforce-umb-prefix-on-element-name": "error", diff --git a/.github/workflows/azure-static-web-apps-ambitious-stone-0033b3603.yml b/.github/workflows/azure-static-web-apps-ambitious-stone-0033b3603.yml index 3bbb454348..cce252608c 100644 --- a/.github/workflows/azure-static-web-apps-ambitious-stone-0033b3603.yml +++ b/.github/workflows/azure-static-web-apps-ambitious-stone-0033b3603.yml @@ -4,10 +4,12 @@ on: push: branches: - main + - release/* pull_request: types: [opened, synchronize, reopened, closed] branches: - main + - release/* workflow_dispatch: inputs: issue_number: diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index bd973a46e3..6bccda7c83 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -5,9 +5,13 @@ name: Build and test on: push: - branches: [main] + branches: + - main + - release/* pull_request: - branches: [main] + branches: + - main + - release/* # Allows GitHub to use this workflow to validate the merge queue merge_group: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 013b41e58f..ac5f4990ff 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,14 +9,17 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "CodeQL" +name: 'CodeQL' on: push: - branches: [ "main" ] + branches: + - main + - release/* pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: + - main + - release/* schedule: - cron: '33 2 * * 1' @@ -32,25 +35,25 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript' ] + language: ['javascript'] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml index de22a81deb..fbb9d054cd 100644 --- a/.github/workflows/devskim.yml +++ b/.github/workflows/devskim.yml @@ -7,7 +7,9 @@ name: DevSkim on: push: - branches: [ "main" ] + branches: + - main + - release/* schedule: - cron: '19 14 * * 5' @@ -28,7 +30,7 @@ jobs: with: directory-to-scan: src should-scan-archives: false - ignore-globs: "**/.git/**,*.md,*.mdx,*.stories.ts,*.js" + ignore-globs: '**/.git/**,*.md,*.mdx,*.stories.ts,*.js' - name: Upload DevSkim scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v3 diff --git a/examples/workspace-context-counter/counter-workspace-context.ts b/examples/workspace-context-counter/counter-workspace-context.ts index cfcfb13a33..5a94378782 100644 --- a/examples/workspace-context-counter/counter-workspace-context.ts +++ b/examples/workspace-context-counter/counter-workspace-context.ts @@ -24,4 +24,7 @@ export class WorkspaceContextCounter extends UmbControllerBase { export const api = WorkspaceContextCounter; // Declare a Context Token that other elements can use to request the WorkspaceContextCounter: -export const EXAMPLE_COUNTER_CONTEXT = new UmbContextToken('example.workspaceContext.counter'); +export const EXAMPLE_COUNTER_CONTEXT = new UmbContextToken( + 'UmbWorkspaceContext', + 'example.workspaceContext.counter', +); diff --git a/index.html b/index.html index 94d127f213..94d6a2ed4d 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + @@ -7,6 +7,7 @@ Umbraco + diff --git a/package-lock.json b/package-lock.json index 40529614de..0c40cdbf37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,29 +1,29 @@ { "name": "@umbraco-cms/backoffice", - "version": "14.0.0-rc2", + "version": "14.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@umbraco-cms/backoffice", - "version": "14.0.0-rc2", + "version": "14.1.0", "license": "MIT", "dependencies": { - "@types/diff": "^5.0.9", + "@types/diff": "^5.2.1", "@types/dompurify": "^3.0.5", "@types/uuid": "^9.0.8", - "@umbraco-ui/uui": "1.8.0-rc.3", - "@umbraco-ui/uui-css": "1.8.0-rc.0", + "@umbraco-ui/uui": "1.8.1", + "@umbraco-ui/uui-css": "1.8.0", "base64-js": "^1.5.1", "diff": "^5.2.0", - "dompurify": "^3.0.9", - "element-internals-polyfill": "^1.3.10", - "lit": "^3.1.2", - "marked": "^12.0.0", - "monaco-editor": "^0.46.0", + "dompurify": "^3.1.4", + "element-internals-polyfill": "^1.3.11", + "lit": "^3.1.3", + "marked": "^12.0.2", + "monaco-editor": "^0.48.0", "rxjs": "^7.8.1", "tinymce": "^6.8.3", - "tinymce-i18n": "^24.1.29", + "tinymce-i18n": "^24.5.8", "uuid": "^9.0.1" }, "devDependencies": { @@ -45,7 +45,7 @@ "@storybook/web-components-vite": "^7.6.17", "@types/chai": "^4.3.5", "@types/mocha": "^10.0.1", - "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", "@web/dev-server-esbuild": "^1.0.2", "@web/dev-server-import-maps": "^0.2.0", @@ -63,7 +63,7 @@ "eslint-plugin-storybook": "^0.6.15", "eslint-plugin-wc": "^2.0.4", "glob": "^10.3.10", - "lucide-static": "^0.367.0", + "lucide-static": "^0.379.0", "msw": "^1.3.2", "playwright-msw": "^3.0.1", "prettier": "3.2.5", @@ -74,12 +74,12 @@ "rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-import-css": "^3.5.0", "rollup-plugin-web-worker-loader": "^1.6.1", - "simple-icons": "^11.11.0", + "simple-icons": "^11.15.0", "storybook": "^7.6.17", "tiny-glob": "^0.2.9", "tsc-alias": "^1.8.8", "typedoc": "^0.25.10", - "typescript": "^5.3.3", + "typescript": "^5.4.5", "typescript-json-schema": "^0.63.0", "vite": "^5.2.9", "vite-plugin-static-copy": "^1.0.2", @@ -6087,9 +6087,9 @@ "dev": true }, "node_modules/@types/diff": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.0.9.tgz", - "integrity": "sha512-RWVEhh/zGXpAVF/ZChwNnv7r4rvqzJ7lYNSmZSVTxjV0PBLf6Qu7RNg+SUtkpzxmiNkjCx0Xn2tPp7FIkshJwQ==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.1.tgz", + "integrity": "sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g==" }, "node_modules/@types/doctrine": { "version": "0.0.3", @@ -6482,33 +6482,31 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.10.0.tgz", + "integrity": "sha512-PzCr+a/KAef5ZawX7nbyNwBDtM1HdLIT53aSA2DDlxmxMngZ43O8SIePOeX8H5S+FHXeI6t97mTt/dDdzY4Fyw==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/type-utils": "7.10.0", + "@typescript-eslint/utils": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -6516,39 +6514,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/parser": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz", @@ -6686,16 +6651,16 @@ "dev": true }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", + "integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -6703,25 +6668,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.10.0.tgz", + "integrity": "sha512-D7tS4WDkJWrVkuzgm90qYw9RdgBcrWmbbRkrLA4d7Pg3w0ttVGDsvYGV19SH8gPR5L7OtcN5J1hTtyenO9xE9g==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/typescript-estree": "7.10.0", + "@typescript-eslint/utils": "7.10.0", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -6730,12 +6695,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", + "integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -6743,22 +6708,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", + "integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -6770,26 +6735,26 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -6797,81 +6762,39 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", + "integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.10.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -6879,814 +6802,814 @@ } }, "node_modules/@umbraco-ui/uui": { - "version": "1.8.0-rc.3", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.8.0-rc.3.tgz", - "integrity": "sha512-35kEb4xjQY+90cYCC28+wIbk6XPXB504TIr6qDi0Ekl11zN3PvMIyhQooohjKD1FxpwfDNeagiBW56e2LnZJkA==", - "dependencies": { - "@umbraco-ui/uui-action-bar": "1.8.0-rc.0", - "@umbraco-ui/uui-avatar": "1.8.0-rc.0", - "@umbraco-ui/uui-avatar-group": "1.8.0-rc.0", - "@umbraco-ui/uui-badge": "1.8.0-rc.0", - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-boolean-input": "1.8.0-rc.0", - "@umbraco-ui/uui-box": "1.8.0-rc.1", - "@umbraco-ui/uui-breadcrumbs": "1.8.0-rc.0", - "@umbraco-ui/uui-button": "1.8.0-rc.2", - "@umbraco-ui/uui-button-group": "1.8.0-rc.0", - "@umbraco-ui/uui-button-inline-create": "1.8.0-rc.0", - "@umbraco-ui/uui-card": "1.8.0-rc.0", - "@umbraco-ui/uui-card-block-type": "1.8.0-rc.0", - "@umbraco-ui/uui-card-content-node": "1.8.0-rc.0", - "@umbraco-ui/uui-card-media": "1.8.0-rc.0", - "@umbraco-ui/uui-card-user": "1.8.0-rc.0", - "@umbraco-ui/uui-caret": "1.8.0-rc.0", - "@umbraco-ui/uui-checkbox": "1.8.0-rc.0", - "@umbraco-ui/uui-color-area": "1.8.0-rc.0", - "@umbraco-ui/uui-color-picker": "1.8.0-rc.0", - "@umbraco-ui/uui-color-slider": "1.8.0-rc.0", - "@umbraco-ui/uui-color-swatch": "1.8.0-rc.0", - "@umbraco-ui/uui-color-swatches": "1.8.0-rc.0", - "@umbraco-ui/uui-combobox": "1.8.0-rc.2", - "@umbraco-ui/uui-combobox-list": "1.8.0-rc.0", - "@umbraco-ui/uui-css": "1.8.0-rc.0", - "@umbraco-ui/uui-dialog": "1.8.0-rc.0", - "@umbraco-ui/uui-dialog-layout": "1.8.0-rc.0", - "@umbraco-ui/uui-file-dropzone": "1.8.0-rc.0", - "@umbraco-ui/uui-file-preview": "1.8.0-rc.0", - "@umbraco-ui/uui-form": "1.8.0-rc.0", - "@umbraco-ui/uui-form-layout-item": "1.8.0-rc.0", - "@umbraco-ui/uui-form-validation-message": "1.8.0-rc.0", - "@umbraco-ui/uui-icon": "1.8.0-rc.0", - "@umbraco-ui/uui-icon-registry": "1.8.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0", - "@umbraco-ui/uui-input": "1.8.0-rc.0", - "@umbraco-ui/uui-input-file": "1.8.0-rc.2", - "@umbraco-ui/uui-input-lock": "1.8.0-rc.2", - "@umbraco-ui/uui-input-password": "1.8.0-rc.0", - "@umbraco-ui/uui-keyboard-shortcut": "1.8.0-rc.0", - "@umbraco-ui/uui-label": "1.8.0-rc.0", - "@umbraco-ui/uui-loader": "1.8.0-rc.0", - "@umbraco-ui/uui-loader-bar": "1.8.0-rc.0", - "@umbraco-ui/uui-loader-circle": "1.8.0-rc.0", - "@umbraco-ui/uui-menu-item": "1.8.0-rc.1", - "@umbraco-ui/uui-modal": "1.8.0-rc.3", - "@umbraco-ui/uui-pagination": "1.8.0-rc.2", - "@umbraco-ui/uui-popover": "1.8.0-rc.0", - "@umbraco-ui/uui-popover-container": "1.8.0-rc.0", - "@umbraco-ui/uui-progress-bar": "1.8.0-rc.0", - "@umbraco-ui/uui-radio": "1.8.0-rc.0", - "@umbraco-ui/uui-range-slider": "1.8.0-rc.0", - "@umbraco-ui/uui-ref": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-list": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node-data-type": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node-document-type": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node-form": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node-member": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node-package": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node-user": "1.8.0-rc.0", - "@umbraco-ui/uui-scroll-container": "1.8.0-rc.0", - "@umbraco-ui/uui-select": "1.8.0-rc.0", - "@umbraco-ui/uui-slider": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-expand": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-file": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-file-dropzone": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-file-thumbnail": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-folder": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-lock": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-more": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-sort": "1.8.0-rc.0", - "@umbraco-ui/uui-table": "1.8.0-rc.0", - "@umbraco-ui/uui-tabs": "1.8.0-rc.2", - "@umbraco-ui/uui-tag": "1.8.0-rc.0", - "@umbraco-ui/uui-textarea": "1.8.0-rc.0", - "@umbraco-ui/uui-toast-notification": "1.8.0-rc.2", - "@umbraco-ui/uui-toast-notification-container": "1.8.0-rc.2", - "@umbraco-ui/uui-toast-notification-layout": "1.8.0-rc.0", - "@umbraco-ui/uui-toggle": "1.8.0-rc.0", - "@umbraco-ui/uui-visually-hidden": "1.8.0-rc.0" + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.8.1.tgz", + "integrity": "sha512-KWKtuSQdxeCbGH2gezumuFysf+33q2TZy4h85zCABFvHgOeeR1EB7/S2jUNGoe2IOqYLktYl0GdQszfmyFN1dg==", + "dependencies": { + "@umbraco-ui/uui-action-bar": "1.8.0", + "@umbraco-ui/uui-avatar": "1.8.0", + "@umbraco-ui/uui-avatar-group": "1.8.0", + "@umbraco-ui/uui-badge": "1.8.0", + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-boolean-input": "1.8.0", + "@umbraco-ui/uui-box": "1.8.0", + "@umbraco-ui/uui-breadcrumbs": "1.8.0", + "@umbraco-ui/uui-button": "1.8.0", + "@umbraco-ui/uui-button-group": "1.8.0", + "@umbraco-ui/uui-button-inline-create": "1.8.0", + "@umbraco-ui/uui-card": "1.8.0", + "@umbraco-ui/uui-card-block-type": "1.8.0", + "@umbraco-ui/uui-card-content-node": "1.8.0", + "@umbraco-ui/uui-card-media": "1.8.0", + "@umbraco-ui/uui-card-user": "1.8.0", + "@umbraco-ui/uui-caret": "1.8.0", + "@umbraco-ui/uui-checkbox": "1.8.0", + "@umbraco-ui/uui-color-area": "1.8.0", + "@umbraco-ui/uui-color-picker": "1.8.0", + "@umbraco-ui/uui-color-slider": "1.8.0", + "@umbraco-ui/uui-color-swatch": "1.8.0", + "@umbraco-ui/uui-color-swatches": "1.8.0", + "@umbraco-ui/uui-combobox": "1.8.0", + "@umbraco-ui/uui-combobox-list": "1.8.0", + "@umbraco-ui/uui-css": "1.8.0", + "@umbraco-ui/uui-dialog": "1.8.0", + "@umbraco-ui/uui-dialog-layout": "1.8.0", + "@umbraco-ui/uui-file-dropzone": "1.8.0", + "@umbraco-ui/uui-file-preview": "1.8.0", + "@umbraco-ui/uui-form": "1.8.0", + "@umbraco-ui/uui-form-layout-item": "1.8.1", + "@umbraco-ui/uui-form-validation-message": "1.8.1", + "@umbraco-ui/uui-icon": "1.8.0", + "@umbraco-ui/uui-icon-registry": "1.8.0", + "@umbraco-ui/uui-icon-registry-essential": "1.8.0", + "@umbraco-ui/uui-input": "1.8.0", + "@umbraco-ui/uui-input-file": "1.8.0", + "@umbraco-ui/uui-input-lock": "1.8.0", + "@umbraco-ui/uui-input-password": "1.8.0", + "@umbraco-ui/uui-keyboard-shortcut": "1.8.0", + "@umbraco-ui/uui-label": "1.8.0", + "@umbraco-ui/uui-loader": "1.8.0", + "@umbraco-ui/uui-loader-bar": "1.8.0", + "@umbraco-ui/uui-loader-circle": "1.8.0", + "@umbraco-ui/uui-menu-item": "1.8.0", + "@umbraco-ui/uui-modal": "1.8.0", + "@umbraco-ui/uui-pagination": "1.8.0", + "@umbraco-ui/uui-popover": "1.8.0", + "@umbraco-ui/uui-popover-container": "1.8.0", + "@umbraco-ui/uui-progress-bar": "1.8.0", + "@umbraco-ui/uui-radio": "1.8.0", + "@umbraco-ui/uui-range-slider": "1.8.0", + "@umbraco-ui/uui-ref": "1.8.0", + "@umbraco-ui/uui-ref-list": "1.8.0", + "@umbraco-ui/uui-ref-node": "1.8.0", + "@umbraco-ui/uui-ref-node-data-type": "1.8.0", + "@umbraco-ui/uui-ref-node-document-type": "1.8.0", + "@umbraco-ui/uui-ref-node-form": "1.8.0", + "@umbraco-ui/uui-ref-node-member": "1.8.0", + "@umbraco-ui/uui-ref-node-package": "1.8.0", + "@umbraco-ui/uui-ref-node-user": "1.8.0", + "@umbraco-ui/uui-scroll-container": "1.8.0", + "@umbraco-ui/uui-select": "1.8.0", + "@umbraco-ui/uui-slider": "1.8.0", + "@umbraco-ui/uui-symbol-expand": "1.8.0", + "@umbraco-ui/uui-symbol-file": "1.8.0", + "@umbraco-ui/uui-symbol-file-dropzone": "1.8.0", + "@umbraco-ui/uui-symbol-file-thumbnail": "1.8.0", + "@umbraco-ui/uui-symbol-folder": "1.8.0", + "@umbraco-ui/uui-symbol-lock": "1.8.0", + "@umbraco-ui/uui-symbol-more": "1.8.0", + "@umbraco-ui/uui-symbol-sort": "1.8.0", + "@umbraco-ui/uui-table": "1.8.0", + "@umbraco-ui/uui-tabs": "1.8.0", + "@umbraco-ui/uui-tag": "1.8.0", + "@umbraco-ui/uui-textarea": "1.8.0", + "@umbraco-ui/uui-toast-notification": "1.8.0", + "@umbraco-ui/uui-toast-notification-container": "1.8.0", + "@umbraco-ui/uui-toast-notification-layout": "1.8.0", + "@umbraco-ui/uui-toggle": "1.8.0", + "@umbraco-ui/uui-visually-hidden": "1.8.0" } }, "node_modules/@umbraco-ui/uui-action-bar": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-action-bar/-/uui-action-bar-1.8.0-rc.0.tgz", - "integrity": "sha512-zG53nm7DMLsyu79gehxtuMj/2bEMgKA7lI6qudi7de74/lFvrGhxZ7JqT5RV6pDiriCxsqBd9OlLoynoWMOJow==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-action-bar/-/uui-action-bar-1.8.0.tgz", + "integrity": "sha512-IRs42chstgXFo5b3i0j80Emt+uZSt/WmDDv7gTtB768FL1C+k0BR5sYVleEmUdkfCOv+WIVo1FAqd+9CPFkDDw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-button-group": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-button-group": "1.8.0" } }, "node_modules/@umbraco-ui/uui-avatar": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar/-/uui-avatar-1.8.0-rc.0.tgz", - "integrity": "sha512-UbwoRBRXgtrebNZ1PTvQWqmgMuTSBD4dZ4xEF6mf7s0BRImI+6AoJevx3GETClnRB9UuD1lOR1GyxA7D76PCDA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar/-/uui-avatar-1.8.0.tgz", + "integrity": "sha512-ek6SFYEvEbu1Jf1FVrqBDHuWqCnekkU1hm4XDHEpEyhPE5OOC70SyYLB6brT0kvgBE0QKB2txYu7u8ZbWzy+OQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-avatar-group": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar-group/-/uui-avatar-group-1.8.0-rc.0.tgz", - "integrity": "sha512-s+odJUrwkMnDBqqP7ms1AGJznLQOw1XdVTtaJLx926U1932JBdlkDFtlKWPNrvGHRv6nILTVQWiyaz0TP02Lcg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar-group/-/uui-avatar-group-1.8.0.tgz", + "integrity": "sha512-AS6+GzeoAOS6vuZ6okP30iik8cvYPjBvoWtSYcnV0gScw52FIg9ak+j5L+rQHzE8LCqT8c6RE63HsAsJe7f6oA==", "dependencies": { - "@umbraco-ui/uui-avatar": "1.8.0-rc.0", - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-avatar": "1.8.0", + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-badge": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-badge/-/uui-badge-1.8.0-rc.0.tgz", - "integrity": "sha512-gG6Lr9dc/6GuHcGVTnb1uJFea95GKqxZHPyp0ljuc1TWUfrX9tKm+QXsHjMwbhpM0GWJUH5iPt0dB7xCsrkTFA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-badge/-/uui-badge-1.8.0.tgz", + "integrity": "sha512-mKHkkXIwN7oUybeQo5J5TOgqghinJH5gE9lJwOemNCy/oiV/TeYHOr7MqHxIJ+13Nwl9O6JbSRWbPbOD9TSkVw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-base": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-base/-/uui-base-1.8.0-rc.0.tgz", - "integrity": "sha512-C7JhJD0Z5/wGZ/f3E68gdtqIspGK5LaEXN0A9pJtS2ZkdMbUFYo85y8aY9NWGGXDkEXHttMvUHv/ePiFsr/7Gw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-base/-/uui-base-1.8.0.tgz", + "integrity": "sha512-LIPS3sfgOr/cgpDueTqpX+t6Bw0BpNISQSrAeyC+c6X0WiahKLuwob6UXSnefh9j5xIYa5+GY1gEUDgI4wlRhg==", "peerDependencies": { "lit": ">=2.8.0" } }, "node_modules/@umbraco-ui/uui-boolean-input": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-boolean-input/-/uui-boolean-input-1.8.0-rc.0.tgz", - "integrity": "sha512-EFK9GPRa59E94Hp3K32uJGKAlYu2P9nENK9WQ8swCthBtw9a8GL9HxaQxGebgbf1kwkjyoihCYAF229czDe2Xw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-boolean-input/-/uui-boolean-input-1.8.0.tgz", + "integrity": "sha512-6GqzuALrzrJIWIAdsYAau9t3sxYL0P+OKSKpcvj+4/DkbhbWjk54CtVFyWBAzYa9LhZHauGl2VYzxSvmGWARSA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-box": { - "version": "1.8.0-rc.1", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-box/-/uui-box-1.8.0-rc.1.tgz", - "integrity": "sha512-5FcG55k0dDYili4JfdOjsNkhAyseyHbqcEKOGmtV110ugvTZpjB/N/Zo49aHNkEIw4FduTLGhP6/2eFjSpLmYA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-box/-/uui-box-1.8.0.tgz", + "integrity": "sha512-/j/69od/uWd1Utb2IzU5pq5cvKpsq0cV4F+pS9e6HejzpcVUCz1AtdKNQvgpyOzd/oS9r8Z6pYL/V/gEydyqwg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-css": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-css": "1.8.0" } }, "node_modules/@umbraco-ui/uui-breadcrumbs": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-breadcrumbs/-/uui-breadcrumbs-1.8.0-rc.0.tgz", - "integrity": "sha512-eubEanIW9LyNSxj53LNb5zCO9Hj23TJEqpYnoci4lSaeaqNOUfpQnn2FYoS8rmvZwnGpBWOq40XSCd1VbNbgbQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-breadcrumbs/-/uui-breadcrumbs-1.8.0.tgz", + "integrity": "sha512-Hv5NAfRzfaXcDYcuribjpaooZk1LVcHNTaLwoxVAUN64sufYS8kfw0sN8+jsacumUP3rpy0XgR9Ic37JUoIkBw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-button": { - "version": "1.8.0-rc.2", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.8.0-rc.2.tgz", - "integrity": "sha512-5JAS247c0NdjsOdzdXOqjOEsfb1HxvPWvBc2KUMOi2hjh/TQbp765BXB0lvc5RqePwuJbwogeAhbesLuRvCCwQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.8.0.tgz", + "integrity": "sha512-M0pLzpGt2CuzQAHqiHQwVdzFOyb9EznCT7b+YMQOlJCMfeVmN2KBF2xUlfb/3M2LVDukTHyGHzqaWj8Lj6YUbA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-icon-registry-essential": "1.8.0" } }, "node_modules/@umbraco-ui/uui-button-group": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-group/-/uui-button-group-1.8.0-rc.0.tgz", - "integrity": "sha512-m4+OE/5CLuPikIoPIne2Rvi9cSUcTvz9qne1ZuTHtu/owuYLKA1lp1wCPVGJTkA3zITeLXkMbpuRaCpaKjB6Zw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-group/-/uui-button-group-1.8.0.tgz", + "integrity": "sha512-5K/cvrOWvRmoXByuI1illF2e9sCzzegmlEpS4XbVk1XW/6quzRJpXSCrY+awj01kFrxB+UC8mB1DIECHKNyeVg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-button-inline-create": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-inline-create/-/uui-button-inline-create-1.8.0-rc.0.tgz", - "integrity": "sha512-Xz7LGdotdyApJxeEmEU8OiHImS3/4yLEIhD0yjAO1F2i33/9pu6KUmloCq0cYO+3RhbQcdcsmrd+zjQutwBHHg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-inline-create/-/uui-button-inline-create-1.8.0.tgz", + "integrity": "sha512-smSZKMG0cAp+BTZe0wRaYotcQElja8gqznGaIyuGuWvvpvWEiuWMC9xjMytFNvaawCN1+uLc5fdcCArPmFjH+w==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-card": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card/-/uui-card-1.8.0-rc.0.tgz", - "integrity": "sha512-EIS73DSOpbYmpkI1IibbO/cuhZPQulweXy2wfXq1gmeux8rr+frXZtZ+6dsvgpdBIx9K/+WoGPEzpyFg08EYOw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card/-/uui-card-1.8.0.tgz", + "integrity": "sha512-wwHaDbwDmragvIhhAtv/D2Ys47kXFPBKvE+/ahofXUygjTGbmjbKJ57Qfo6x8sD1BM3bSTDU6gUWaf4s0/D3WQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-card-block-type": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-block-type/-/uui-card-block-type-1.8.0-rc.0.tgz", - "integrity": "sha512-KGODyaGfbJiK8YQczU6yvpW/PFKzu1iKCnURogMsb0qOZHyKUJyddRm1fgFpX+i7RpyR4Cjbmw/Vmnt/oeNenQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-block-type/-/uui-card-block-type-1.8.0.tgz", + "integrity": "sha512-8Ydat2K4LipsQaCEhDTN4DeVHiqOCdEOY4Z43XCf6bTU91lNPGdagtC0ZE9b4M28E03ou4E19Ms7o2m59g0OWw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-card": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-card": "1.8.0" } }, "node_modules/@umbraco-ui/uui-card-content-node": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-content-node/-/uui-card-content-node-1.8.0-rc.0.tgz", - "integrity": "sha512-uHo3WY/cAsi87sa0QIR2sUFzUggtM31xKmoLCMm3bUMiDW0Y2lDfwb6mZU6yhykjczhcHuPSxwK1SgsI8bJsPg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-content-node/-/uui-card-content-node-1.8.0.tgz", + "integrity": "sha512-R0SYtKk5Z+on0xQ0cD1z2z43mSTgYi7sKtQDrhwoP/sWbp9xIS2wPOO+PoMiUonwXPo4JuSZ9ghgT4HzsQce1A==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-card": "1.8.0-rc.0", - "@umbraco-ui/uui-icon": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-card": "1.8.0", + "@umbraco-ui/uui-icon": "1.8.0" } }, "node_modules/@umbraco-ui/uui-card-media": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-media/-/uui-card-media-1.8.0-rc.0.tgz", - "integrity": "sha512-ISc540E2PZCBkJQLfRL9d2tMjP6GdLsP9+VihVgAKLSkG9+T/dMkbtTWlGX5ehmfaDzDmxXS9XUh8MCkplrRPw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-media/-/uui-card-media-1.8.0.tgz", + "integrity": "sha512-C1xsdO9mWJ/gzE8nLfF2k5NfpFzJ2yMQYzJVtov3s9C33iy6NVq7OM67o+QugCqDuwwYSkonjgNJLHTav78KVg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-card": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-file": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-folder": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-card": "1.8.0", + "@umbraco-ui/uui-symbol-file": "1.8.0", + "@umbraco-ui/uui-symbol-folder": "1.8.0" } }, "node_modules/@umbraco-ui/uui-card-user": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-user/-/uui-card-user-1.8.0-rc.0.tgz", - "integrity": "sha512-VIOGZnhvDO38e4RmQAxWVZFmiuT1FVIdA/DPNmrOiTJte2sWhwYb6mZWihiP1M09ChkhqviwZSkmt+xo2A80ng==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-user/-/uui-card-user-1.8.0.tgz", + "integrity": "sha512-b6LiMCl/oFaW6MnPXBMCPqlVDHF/TT3LByQOTJ8rE+WHzY/zE9toVLy/LccxB62Ur/Hz7XakhU8xHaugH+zs3w==", "dependencies": { - "@umbraco-ui/uui-avatar": "1.8.0-rc.0", - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-card": "1.8.0-rc.0" + "@umbraco-ui/uui-avatar": "1.8.0", + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-card": "1.8.0" } }, "node_modules/@umbraco-ui/uui-caret": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-caret/-/uui-caret-1.8.0-rc.0.tgz", - "integrity": "sha512-9Ko0NUorE4k3Iv49gJjVnSxiHJKUE3ImrgkPxI3GhNw5eUagpOecyl3MKlI/twpkHfY1CCeq7ngUV9+3E2uIiw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-caret/-/uui-caret-1.8.0.tgz", + "integrity": "sha512-LxS0vLKZYNKsef/FgNXFh/CBauf+6Dgac+n5VyCu6FElh8UTP+NOeAA/4KEVSJh8gThJ0Fmb8qoMSFop+41wLg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-checkbox": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-checkbox/-/uui-checkbox-1.8.0-rc.0.tgz", - "integrity": "sha512-zOc35YNojxC1iqezOPnf3vL/WBMhdVPkXUUvVGrWCNMBsTy42JZUt8EDOgiRJu0j1yIrPuQy4NbQJ5NPQCRuLQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-checkbox/-/uui-checkbox-1.8.0.tgz", + "integrity": "sha512-Gf/kZ4q5SuLNEEfcL1/YRzcOI5CZTsMyo2+9LuksCZqRzWtxoo1meB42GZRjE4GTCFZfQOr4Ayz7ZNRaizuM+Q==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-boolean-input": "1.8.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-boolean-input": "1.8.0", + "@umbraco-ui/uui-icon-registry-essential": "1.8.0" } }, "node_modules/@umbraco-ui/uui-color-area": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-area/-/uui-color-area-1.8.0-rc.0.tgz", - "integrity": "sha512-xj9uAsh1SPdxmsGeS+k/hM5sFwXA9XuBwgZwvXSGluiOEranTxmGW2Gq9p+dng1CQEyvvRbQ+R12R40wghpPUA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-area/-/uui-color-area-1.8.0.tgz", + "integrity": "sha512-V3+iph2Vq58T9f4FoJvxsjq0LpH5VJhC2P2VkAWvMO1m528QOULDP+b5csYWH1pF78RxSZ5Lm042Dnw9XOqN2w==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", + "@umbraco-ui/uui-base": "1.8.0", "colord": "^2.9.3" } }, "node_modules/@umbraco-ui/uui-color-picker": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-picker/-/uui-color-picker-1.8.0-rc.0.tgz", - "integrity": "sha512-l5d8Ar9+z4K3GWrD3w2qn5ChnAhFmBcbJglgaKbJ78BFFccB+HYjYB2znMYPyi57+UVxcQPvr6jAz5i8BSisZg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-picker/-/uui-color-picker-1.8.0.tgz", + "integrity": "sha512-DQtKN4ivIRx54SYUhxdlbFf5ibmnp5/mscOiC98HObYVTeM9ZJUrKfFGTU9Qfekz3q+rPzzv7eec8E0Zb6qfwg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-popover-container": "1.8.0-rc.0", + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-popover-container": "1.8.0", "colord": "^2.9.3" } }, "node_modules/@umbraco-ui/uui-color-slider": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-slider/-/uui-color-slider-1.8.0-rc.0.tgz", - "integrity": "sha512-eh8XtPthXxl0z0FbyXA4epjiB18ZDnCMJJQ8w5br6d4tQicBPgJhWZQKBrZ1bNRpPaiHTxaA7kJ+mBz1l/DiUA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-slider/-/uui-color-slider-1.8.0.tgz", + "integrity": "sha512-DHCFX0JZXqI6wp2fRe+lOd9TAJVzTC9ghDNP+qMGattsxRnTf/pxoYucW7F5ee/ggiBIsS8i47kFa2wDmausiA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-color-swatch": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatch/-/uui-color-swatch-1.8.0-rc.0.tgz", - "integrity": "sha512-BdP1GYu8imhrO1KRpputsItIEGzSH44mWG3dRZakrOCyJt6V/nLGpHRymEuh2B0qcd7nDmyfN3mhtRRQ3N328w==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatch/-/uui-color-swatch-1.8.0.tgz", + "integrity": "sha512-z9RQ0R5E9SErqiY1/kU6Rnp+LQBM119OKqAexHo32cM/9iyzLIEUY4CwzCYE9/ogeXDgljXLTGX21jddCNCv9A==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0", + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-icon-registry-essential": "1.8.0", "colord": "^2.9.3" } }, "node_modules/@umbraco-ui/uui-color-swatches": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatches/-/uui-color-swatches-1.8.0-rc.0.tgz", - "integrity": "sha512-HrRS9QZKEinHoiFUfmYNs430OrkkB55r3gDgsVn+LWuiKiYDR3pXO6wYrtvq4+RiviGPMLPEzuInd3GvDI+vlA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatches/-/uui-color-swatches-1.8.0.tgz", + "integrity": "sha512-qCHYtHPhPsubrVn/cydmigbAde0fc+kJC7ZBxCcuJjyP+wiUhq2/d6dSfYumCcVw1N3Hyj7BRJ/8ZedUIZQ5/w==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-color-swatch": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-color-swatch": "1.8.0" } }, "node_modules/@umbraco-ui/uui-combobox": { - "version": "1.8.0-rc.2", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.8.0-rc.2.tgz", - "integrity": "sha512-71AbVcHweB36g3jUCur/PKIKbpSHMvJq2iQou84NgVtO+hBM0PxH2JOsLRCMFG76D8fjIUd03tNp6szBHH1RMQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.8.0.tgz", + "integrity": "sha512-FVc3PlBYU8S48Zr75pig+5YXh05R3wRKdLl41l7vFBDGGWsgjIsM+vJ6LaM+VoshnTzUTOVvKLE/N0FNTVUvrg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-button": "1.8.0-rc.2", - "@umbraco-ui/uui-combobox-list": "1.8.0-rc.0", - "@umbraco-ui/uui-icon": "1.8.0-rc.0", - "@umbraco-ui/uui-popover-container": "1.8.0-rc.0", - "@umbraco-ui/uui-scroll-container": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-expand": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-button": "1.8.0", + "@umbraco-ui/uui-combobox-list": "1.8.0", + "@umbraco-ui/uui-icon": "1.8.0", + "@umbraco-ui/uui-popover-container": "1.8.0", + "@umbraco-ui/uui-scroll-container": "1.8.0", + "@umbraco-ui/uui-symbol-expand": "1.8.0" } }, "node_modules/@umbraco-ui/uui-combobox-list": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox-list/-/uui-combobox-list-1.8.0-rc.0.tgz", - "integrity": "sha512-UTxJvlhS4zf4XXh4j9FeTEzdLs0U9cuiBFDlNQ5F0gF2LvA9LK/bCszA1iQhKx6umyWv0U+04hXgLTY/MT1Ymg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox-list/-/uui-combobox-list-1.8.0.tgz", + "integrity": "sha512-3w353u7FdYNLCz6YV+Pk+lxlTeHd2H6rmxzwb+0BHCjbwaw9MLojbhE4JGTCpf/qtk54Xc8Dzg++aznKAYpbVw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-css": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.8.0-rc.0.tgz", - "integrity": "sha512-trwLCgJtT91iP2b20QlHWjuj44AF4lWCg4CqBZoT2Z8a5IedqflnQstXCZRYm/F5Re32YGTwlR9lF1rAXqq4gg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.8.0.tgz", + "integrity": "sha512-9o9OGUXQK8D9i/VSmH3gUTvH7se+4Ey22dSfbn4Jzcbe6AyGmxKocr/8eZXdkIYwNvK2aUIz/b7GIEbQc4utbA==", "peerDependencies": { "lit": ">=2.8.0" } }, "node_modules/@umbraco-ui/uui-dialog": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog/-/uui-dialog-1.8.0-rc.0.tgz", - "integrity": "sha512-B6HW+2wj17wyt4LGI1Zfs9OmMwK5qzDYmrAtx3uo3L+aieQ3QNpmYLJHP7c7xeU4kO0u+GJgBFt/qFRb/iPM1g==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog/-/uui-dialog-1.8.0.tgz", + "integrity": "sha512-QI/Oa7BJ7eEdHcy7hSuFhMPbbPitxnIb2BHVmQzy+YpBShrR3C1GW0FGcYFgJlI/S+jodf3A59VDnVL69gyliQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-css": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-css": "1.8.0" } }, "node_modules/@umbraco-ui/uui-dialog-layout": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog-layout/-/uui-dialog-layout-1.8.0-rc.0.tgz", - "integrity": "sha512-uMuLWLQczUWPo5QokKiYJ5CuhVly3T8kYdFaKB+OkVRPIG2GHF1VKvM/JJX6Tx3po1EsehR3wb6JaxY+/BsX0Q==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog-layout/-/uui-dialog-layout-1.8.0.tgz", + "integrity": "sha512-iv4wEfb6QhCOm8lxg/zH7yrJuVSEEBGgAN67qdvZ9Tu2SFzUCGTzrUcBXlL+U10cbIlq7j6KKvSQvE/IHX40Tg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-file-dropzone": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-dropzone/-/uui-file-dropzone-1.8.0-rc.0.tgz", - "integrity": "sha512-8JN8nzoIysub+pxvj3gNhvHRrZmui9SflvF6mMTKpk1uwZxE7LI4HZDhjmczKKDX9MChbzn1bSd8JYaxoA4zzQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-dropzone/-/uui-file-dropzone-1.8.0.tgz", + "integrity": "sha512-Fpl/aGprUbcdPngB6xpR8/i7o8HKAWNCUze+N1pXiIVBRXmthEWO6fSm4+LkkkRoZwvYqQSaeu6Mw+Sj+MzHGA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-file-dropzone": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-symbol-file-dropzone": "1.8.0" } }, "node_modules/@umbraco-ui/uui-file-preview": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-preview/-/uui-file-preview-1.8.0-rc.0.tgz", - "integrity": "sha512-1xQ6R6ZoabFpAkkLWMyndR17j2xbgt0tBk59xFipnBeYsPyWkZd94MV7kl6T98YlxgmPY83+uhMI6qFuTPogiw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-preview/-/uui-file-preview-1.8.0.tgz", + "integrity": "sha512-WhE/9klwIUjch6PxF+DuFlu+ql0h9YbefMxj/xU4rGUTE/dK4CgA7eVQ/YfAp+WrdtwUFHyp3fThyJvfrodSgA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-file": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-file-thumbnail": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-folder": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-symbol-file": "1.8.0", + "@umbraco-ui/uui-symbol-file-thumbnail": "1.8.0", + "@umbraco-ui/uui-symbol-folder": "1.8.0" } }, "node_modules/@umbraco-ui/uui-form": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form/-/uui-form-1.8.0-rc.0.tgz", - "integrity": "sha512-5NtMr9qfcRNhL+q9UmXjR57/qGxin4x86OFVANb1Y12rx+V2l1MlyTHhGhGyukZX0re3f7DaY8kwg1CvUy9U3w==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form/-/uui-form-1.8.0.tgz", + "integrity": "sha512-9NtavsRoh9X39ezzLtwwbK77mUecavcjxP58Jdba/2/6wXRx+vx7qsWV5Rn3OC9XG4tZi6VLFFKahbV8N/jgjw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-form-layout-item": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-layout-item/-/uui-form-layout-item-1.8.0-rc.0.tgz", - "integrity": "sha512-5q8b0DVhbTUUYUaPWQDdjU7+kzWmns1EESA3dOL/2I4DzejfBgU7eGufdVRjlrvQyhPuHHp0wVs4aAL3jF0CCg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-layout-item/-/uui-form-layout-item-1.8.1.tgz", + "integrity": "sha512-QHBZayR4T49MAyS9N2jy1rgQ5Yk1JpwoWvWBpbI/WDE718zTjwUJxbLfukq+NnJx7Q1DplZ+IHTnHZkInMC2GQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-form-validation-message": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-form-validation-message": "1.8.1" } }, "node_modules/@umbraco-ui/uui-form-validation-message": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-validation-message/-/uui-form-validation-message-1.8.0-rc.0.tgz", - "integrity": "sha512-ePlg9BD4PkPvcjD+rCXrkJjAmoctXoVsDnBmhbn0cWbUiSpSX21OVTDiS9XW9Iyb+HOh5Pm0coGGTpF18+yM1A==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-validation-message/-/uui-form-validation-message-1.8.1.tgz", + "integrity": "sha512-o4WfGHRtV+DrN064DtzyljRkUGYMYEkLHxxbawLowpdmdwyvLByaGOkxfuEJvHgPnncR02//gM04EjulEbGitw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-icon": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon/-/uui-icon-1.8.0-rc.0.tgz", - "integrity": "sha512-qEXgNGhN+hdRRune048zN76EkEpsHrQR3kLW7uYtF0/NSslKQxGLVWhNMK7X2Rw8t9btRmfp9Ib2mwZFNNayQw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon/-/uui-icon-1.8.0.tgz", + "integrity": "sha512-hub+KNcwiy+SKxEpI/ei3w1Ficr1SWxcLfwL67MOKS5YyB6RDwSl2FOXx+MkttTAO7PvGBbAgkiiXEkI/rxivg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-icon-registry": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry/-/uui-icon-registry-1.8.0-rc.0.tgz", - "integrity": "sha512-amoA60rpXAciPeepYM4G+pwHDG9llqPuQmTOMv7GchFbjghENDOT3dW9kOQZbSEncUEgHWpsf/Nu8zN2aJmiVw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry/-/uui-icon-registry-1.8.0.tgz", + "integrity": "sha512-+55qGgxOBlHmQerxIhKkKJYJgPwnXwsLOBVwF8oYIM1sV58bu7BrGQRUw/K0ytFavrFOb+Easkn62wqzisXsRQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-icon": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-icon": "1.8.0" } }, "node_modules/@umbraco-ui/uui-icon-registry-essential": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry-essential/-/uui-icon-registry-essential-1.8.0-rc.0.tgz", - "integrity": "sha512-0X42o/uet3bjL3v/5i2Q5hxgbkWYK9eTUrmDwBq01PIyw7XKW7Z/5C5Ryi292a3cwEGr0cAwawCLAkiTeIQiwg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry-essential/-/uui-icon-registry-essential-1.8.0.tgz", + "integrity": "sha512-6+bM30t3+YpknxIjcCHi07eS+MXMkDRP+GBfrUgULqH/EqnJN/OuwMzSdbwFuwzqxmC7thx74Zfl6+JBuIs9lw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-icon-registry": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-icon-registry": "1.8.0" } }, "node_modules/@umbraco-ui/uui-input": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input/-/uui-input-1.8.0-rc.0.tgz", - "integrity": "sha512-SuiTzZP0N1qTpivGKhRzVRKumTe6v6F34KGux303tJws7Nng4AW/KEq+QFpzd1nl0NQGQRQ3JaL+StSmgQFYJA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input/-/uui-input-1.8.0.tgz", + "integrity": "sha512-abtQbWWDT+3uo4KVaU+ZbDVKSNtB8r0C/3L7Ql/7xJ2BNI2oSUSAcWoSV6V8Vx0DYh1XWlEQpfSAbk5Fdr7u4w==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-input-file": { - "version": "1.8.0-rc.2", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.8.0-rc.2.tgz", - "integrity": "sha512-WO4boW7+K4cFF+wo+qunBtiWyfn2XOdd3tl+8M/+lBwmCDIxuLbhrDosZEiUKvhyG4BjZxK1+C5JFqROZSQrkg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.8.0.tgz", + "integrity": "sha512-20fXbIwjyLZWIVsqFt06ldQqA8sHEPm8Y0hmPwbb0nagYfjmIblIE1thT76JA95do7qcU50xGBN9mHt8KvPpQA==", "dependencies": { - "@umbraco-ui/uui-action-bar": "1.8.0-rc.0", - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-button": "1.8.0-rc.2", - "@umbraco-ui/uui-file-dropzone": "1.8.0-rc.0", - "@umbraco-ui/uui-icon": "1.8.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0" + "@umbraco-ui/uui-action-bar": "1.8.0", + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-button": "1.8.0", + "@umbraco-ui/uui-file-dropzone": "1.8.0", + "@umbraco-ui/uui-icon": "1.8.0", + "@umbraco-ui/uui-icon-registry-essential": "1.8.0" } }, "node_modules/@umbraco-ui/uui-input-lock": { - "version": "1.8.0-rc.2", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.8.0-rc.2.tgz", - "integrity": "sha512-k8Dv83zUuEQvQBOFE+oD6tBNXB+UBd6pnQmoqLvohDobWVmjWo8o0vL2AszroLR9XFPGbPE+UpCNODM7OAEw9A==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.8.0.tgz", + "integrity": "sha512-ZFBssrhCPrCiSfoS6Y9yA9u2dktzNDCRFQ95Z9nQEthhVm2okyqMS3daGz57Vdm4+LVB0HrqIjpEHC6dOqNTBg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-button": "1.8.0-rc.2", - "@umbraco-ui/uui-icon": "1.8.0-rc.0", - "@umbraco-ui/uui-input": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-button": "1.8.0", + "@umbraco-ui/uui-icon": "1.8.0", + "@umbraco-ui/uui-input": "1.8.0" } }, "node_modules/@umbraco-ui/uui-input-password": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-password/-/uui-input-password-1.8.0-rc.0.tgz", - "integrity": "sha512-B867GMgEr9/40tobOaO4rGI8fX7n+OZfizJrkxPNYgq9IHXMzbuHWZNiM/vW+uZNDDHDStZYxMa1L2NEfYOypg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-password/-/uui-input-password-1.8.0.tgz", + "integrity": "sha512-Ra7bR40GzjX11qobHsog2FPGd3FsPzgxPpGLFyMTHzDg2gchYU+KQKkmt6CTzGPgSFuN7DsEW0BMnFnWJ+2moQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0", - "@umbraco-ui/uui-input": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-icon-registry-essential": "1.8.0", + "@umbraco-ui/uui-input": "1.8.0" } }, "node_modules/@umbraco-ui/uui-keyboard-shortcut": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-keyboard-shortcut/-/uui-keyboard-shortcut-1.8.0-rc.0.tgz", - "integrity": "sha512-EwGbFA3TGdc9JydR4wmWCwGgMTksH7bjdBUyCR6Ix2nsekBsxNsfc/vcbHkfzw1u12WEqSZU4CzeNb8vYIldbg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-keyboard-shortcut/-/uui-keyboard-shortcut-1.8.0.tgz", + "integrity": "sha512-XBO06bZbo7H39k33pm8EoWxm9MP/NXh7W430dLqB5E3q1EOO24PQw2voLsOyegVM3IqgKyMBTO7xHR8NmeZkyg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-label": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-label/-/uui-label-1.8.0-rc.0.tgz", - "integrity": "sha512-lUYd9vmGulWyF4qeWtX4ZwFaSUwP20KsGoXfJmNJ0EF7gSaYrFPJ0Sax2VSeo8BVFzhd1VtOHmLqL4DcQcOn1w==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-label/-/uui-label-1.8.0.tgz", + "integrity": "sha512-Qj3CtfVIv3rVIanTbMvg6UAB2faWGu2mMtvVfmr9kkEP9MGfmA/xgQN94lKlDI7ic54FVkCV4N+1kA5yNkeDSQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-loader": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader/-/uui-loader-1.8.0-rc.0.tgz", - "integrity": "sha512-S3LZCFrnI/F75I6OUVjXJ3E4jTH9o4b4JzE6Xe0V8u+gUfCU4NaAdoOl0x+u01Qh7NlZc318a+HiURP4W2Zr1g==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader/-/uui-loader-1.8.0.tgz", + "integrity": "sha512-CL+n3Icp4ugfn7SBbhbYC4WTBQ+kT27WwJIesOcjw2lmt2l20RQUyBBbZAABx2ayyDuvIzEWnFEzWW8VyVworw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-loader-bar": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-bar/-/uui-loader-bar-1.8.0-rc.0.tgz", - "integrity": "sha512-oJdr5vXP+tny6ZaM7VTV0E50MPuHeO7dbbX+hai+A55KOsSF/ZZrQMB1WZtdcyY3pzWA6AKgvTtRFEbU6nHf7w==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-bar/-/uui-loader-bar-1.8.0.tgz", + "integrity": "sha512-KuUtQ4r/wkbVl4IJVgUb9DCXvV1yvupDw6AAnWuOu7zexuzPfrl32cKBLlnNmhGqnEGcQonX6jf24Lf8T7W6Nw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-loader-circle": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-circle/-/uui-loader-circle-1.8.0-rc.0.tgz", - "integrity": "sha512-6g0bxqkTnNBAkSdm6EqlMQ0Gm5PDSEBGvGJ1qfyvgr9Rfa1rHR4GpOrz04A+Fobz9dXoCN2xfqNayUDBo4dy2g==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-circle/-/uui-loader-circle-1.8.0.tgz", + "integrity": "sha512-L5RyH10Es/stG7CzNzS0bKOzCY+zLSwmqyh0dc8HwzZCvc6XtFHV0KgcxMjmtWWulLsQPgzIOIigf3wefr2VNQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-menu-item": { - "version": "1.8.0-rc.1", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-menu-item/-/uui-menu-item-1.8.0-rc.1.tgz", - "integrity": "sha512-yccczOwXnODykEgILX/wrg+D3i06L2iAEg8uetFp+NQgMPgLz0SIVyJioNHiGkWnNlvwG/wfNOPT5PTY4LrCQA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-menu-item/-/uui-menu-item-1.8.0.tgz", + "integrity": "sha512-0zEMhcw35Evw7cEq1W0GYNVCzIorTVMzFquU7Sg2QiZmk3eiF6HvkF/gHr4d0WR7HvztM1k/eVm+RTs6zp+96g==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-loader-bar": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-expand": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-loader-bar": "1.8.0", + "@umbraco-ui/uui-symbol-expand": "1.8.0" } }, "node_modules/@umbraco-ui/uui-modal": { - "version": "1.8.0-rc.3", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.8.0-rc.3.tgz", - "integrity": "sha512-QFkGXLweUXzR7KveCNJybaqNIMGbOmb8oP0QnhK0g73yccVBozxjMqdv1Mzl/h17VpCNtNkP+P2pNXNEe8TPoQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.8.0.tgz", + "integrity": "sha512-Cqd4pabMLnpnMEa35MKOwXCKQ+5okHYWdvdFRA8x1HqI3AEcz4FNx48nXVH94vhbELv8+fWFAPfrr1v0rvjK6w==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-pagination": { - "version": "1.8.0-rc.2", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.8.0-rc.2.tgz", - "integrity": "sha512-T4vw2M5EJliqwy8YI03eA7pg+gcym9fyeO95eGQvriUV6/OB60CrzjEQ2tXREkVQq1oW3RIBEUEikUgRktMpwA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.8.0.tgz", + "integrity": "sha512-SvnWMzbQcTDEN+XQJ0/lVCJ1wL+2L7LA9pfSxJgXIj/pB55Pf3Tt3zMnW8B7RT7i/74atMufaqSSKElZrcPfHQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-button": "1.8.0-rc.2", - "@umbraco-ui/uui-button-group": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-button": "1.8.0", + "@umbraco-ui/uui-button-group": "1.8.0" } }, "node_modules/@umbraco-ui/uui-popover": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover/-/uui-popover-1.8.0-rc.0.tgz", - "integrity": "sha512-cCeDHzwz67ugMJ4tg56dksHvsG1Zye3KlLTROHFt4qeOpnNcGuA8WDQTTxC7KyzGlxEkVWUkpHT1HxLP60FCFg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover/-/uui-popover-1.8.0.tgz", + "integrity": "sha512-bHERyYItLAvtWbpOdPgmuPFgYs2TuKImm3h04DtQRatYP4dq8wg0tftVyZK3k9TVfLMvStOy2EyzybTD+1ZmBg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-popover-container": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover-container/-/uui-popover-container-1.8.0-rc.0.tgz", - "integrity": "sha512-ErPEkvBvg3AhJnFXn8U32SZG+Tyfq3EyZxEhCwPgv4S7MajOrjz08SLN/2khSqLnqWw5dihRYcBTxcLtw4549g==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover-container/-/uui-popover-container-1.8.0.tgz", + "integrity": "sha512-FHaX4sIa7q4HTVzMr9w3Z6zuunPuDROnHjVS6QkovqHLEgQjeTQB8hGAxN6cd3QsFbgbtC9fzBo8zTn9yxJLmA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-progress-bar": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-progress-bar/-/uui-progress-bar-1.8.0-rc.0.tgz", - "integrity": "sha512-OJI7g8jWB2gT++faIjoyGDLGjfjzTXGg4lfvTt5zNGI8JgEUf1CY0j4C3ANoh+cXMwvxiURRGOX/UmmwZzN/uQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-progress-bar/-/uui-progress-bar-1.8.0.tgz", + "integrity": "sha512-WX+YjiH0HCljtzTImR6Q8bf06wm+jcCgEOE10pMRri0r4nyBCAN7Zms23sHj6rR+S/oxoYcuREqdWXWP4eRfFA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-radio": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-radio/-/uui-radio-1.8.0-rc.0.tgz", - "integrity": "sha512-lIjFN9ykLOaxcE0aHsO4HJ42X6uqX3L7S8juk8BVHKY2WiocYdW5EXKQ3qMjEkjx1rbhTrr/Ok2Pu7L8YeohJA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-radio/-/uui-radio-1.8.0.tgz", + "integrity": "sha512-3kPWyc0ZbtAIeOHxAKudSslomg1zKy4RMgrfDeNumQhuCei0VWhVn76Xyfu/Gl2n4XnLIzZY3zv4XCaxwWvJZw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-range-slider": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-range-slider/-/uui-range-slider-1.8.0-rc.0.tgz", - "integrity": "sha512-NuIOyT4F6pWweZj7VSRFrGnxijGayAIa3BhO5+stsw+Cz1EmESgTgV175buVRRhoqMGOAhky1EGnKkD9YYQ0hg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-range-slider/-/uui-range-slider-1.8.0.tgz", + "integrity": "sha512-ThDfLvxRMfP93HQobgLaglR860vQiwCM63PXp2igXWZl69ikDDa7HuraupEpmdL13VLKAv5L1BMue1ItC1x2MA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-ref": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref/-/uui-ref-1.8.0-rc.0.tgz", - "integrity": "sha512-dxf4h5lfpZYpqANRvVD1eR4LrLOqut2oI8O0gLoUG/crObaU0Yta53T4xTVl2aAL7Wjm4cYkj9iQMd/9MJdo1g==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref/-/uui-ref-1.8.0.tgz", + "integrity": "sha512-mcw7faAXeG/2QfLeuYsB950seU0FQH5Pd+aSfi6zwgWkCasxKWLrlslVTP1U5zD5WFkNt4ls6RnMGZhN6bq7mQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-ref-list": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-list/-/uui-ref-list-1.8.0-rc.0.tgz", - "integrity": "sha512-vdS3OMNX+r1akhUtuFfW2WfWsDflR7kA9QbVy2R0twvxE2JD5xttXlBybtqGljguks0nBuvUrEgcU3s014mPKQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-list/-/uui-ref-list-1.8.0.tgz", + "integrity": "sha512-9iiTYVyw+dpO1gEB2oMJSe2DOVvA3a2uY5FLwkRgpAjvzbhD9hhyGLcVgtvM1rxUYc9SvJbGJXk2tPY4Nit3pA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-ref-node": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node/-/uui-ref-node-1.8.0-rc.0.tgz", - "integrity": "sha512-bYoL7u8AixbcA1JRfRJ/l5SfEj0sq9jF8pJzILP5wLpphwthZU8PqUC9jqgvo6RZSOVmtDcVqDsLKpG1t/vT7g==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node/-/uui-ref-node-1.8.0.tgz", + "integrity": "sha512-KOGy10QhPUQFNFNPkmqro1YqFg5Y5b2N/DwkcYP/xyu4Kc1f5FuE+Uo5L41i2KdbDP6O+Tz5gsU6HpPvpEAc7Q==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-icon": "1.8.0-rc.0", - "@umbraco-ui/uui-ref": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-icon": "1.8.0", + "@umbraco-ui/uui-ref": "1.8.0" } }, "node_modules/@umbraco-ui/uui-ref-node-data-type": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-data-type/-/uui-ref-node-data-type-1.8.0-rc.0.tgz", - "integrity": "sha512-zZk1y8aJBcI+jx6JvqxQc9cuWU2cSSIRcfQeM2HGVriyUBNX5oz2dgfkV6hWJK5Ndc/o6ZrXkWL0CT9X16OKxQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-data-type/-/uui-ref-node-data-type-1.8.0.tgz", + "integrity": "sha512-x7PicpHE7PPZrVDojKO5ornG7HZoRB9/pjCZWV+8wxPznyGmFvcJpbRQLdIVvvXVkL1a0c4uLY2CsUO+K52Rbg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-ref-node": "1.8.0" } }, "node_modules/@umbraco-ui/uui-ref-node-document-type": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-document-type/-/uui-ref-node-document-type-1.8.0-rc.0.tgz", - "integrity": "sha512-tEBYvuS7XkuRoEMqgJLnMo2BIifivHOrQlyZOyoA4EQsbkQsLPNIYkn+tWDcW/qRK5GSDQGHjRPza0o3E281zA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-document-type/-/uui-ref-node-document-type-1.8.0.tgz", + "integrity": "sha512-QjeC8DvgA/WFb3wOUXMOALK5SoKeDAvYqNp1wNvU5AMxJY+5CTfi6JNWKZsI4K+Mh3lfzPP+HaWx5MTwt5epNQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-ref-node": "1.8.0" } }, "node_modules/@umbraco-ui/uui-ref-node-form": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-form/-/uui-ref-node-form-1.8.0-rc.0.tgz", - "integrity": "sha512-Ua25ZReynmdPqEvF0Sx4JClvWn0i2Y1bTo7KE45zm7M30jzhBjVt1LpAayy7mNK84VJY9lEt1vSt5B3rkug7oA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-form/-/uui-ref-node-form-1.8.0.tgz", + "integrity": "sha512-sm0xSru6pv8yK9qepzF5Wzzd1jarMBZFtzgqw5H65pGTP6pNhNG4GIkeXLfd2TH9g3e6biJNKAOFJ5w8Tz4O9A==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-ref-node": "1.8.0" } }, "node_modules/@umbraco-ui/uui-ref-node-member": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-member/-/uui-ref-node-member-1.8.0-rc.0.tgz", - "integrity": "sha512-6gW4G+kvGenCohf0vzd1heUAX2hI6ZmlSfLttdDGggETChUcYQ91sFDe/8r0pOIPGgE6r46B1WgGmFXNMFxMEA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-member/-/uui-ref-node-member-1.8.0.tgz", + "integrity": "sha512-z9l44zCER4KmNMSnCy6BFxXZ6awr/L405pJJJzqb3GAXsBip47pCzhYgxCbekB+xSY+E0hS1EuG1JJ5wn05yiQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-ref-node": "1.8.0" } }, "node_modules/@umbraco-ui/uui-ref-node-package": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-package/-/uui-ref-node-package-1.8.0-rc.0.tgz", - "integrity": "sha512-aSY1MMoNhZI+firF/UrvXrbR3JSNiOtHtrTDMreZTr85wzC82k6gRXmlz+1xvCKooduab3/e/gM+1yPoha85Ng==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-package/-/uui-ref-node-package-1.8.0.tgz", + "integrity": "sha512-bpWVBOm39tl5jY3Y+qn5dnikmnaAOrHEg2SBf/11d7aHOauEgDiJZQmM9BzjAeionaZrj4beYJw5szazaVkpDQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-ref-node": "1.8.0" } }, "node_modules/@umbraco-ui/uui-ref-node-user": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-user/-/uui-ref-node-user-1.8.0-rc.0.tgz", - "integrity": "sha512-0o6eyHFq2WHiEUaE1FgtVxavHLEwuAByYYesnzx4b2zwO1IHVZEjuaSp5nOJ6vyUdq1zK5TZZJa2LvWIT2uMNQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-user/-/uui-ref-node-user-1.8.0.tgz", + "integrity": "sha512-Z91Me56phGFyt/hJaRDnXrorlYVjrL6WaUWRDZAi+mqqThFriG1DVeaFEewZ1yeD3Hy6haHanDa7dtwm2FLsBQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-ref-node": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-ref-node": "1.8.0" } }, "node_modules/@umbraco-ui/uui-scroll-container": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-scroll-container/-/uui-scroll-container-1.8.0-rc.0.tgz", - "integrity": "sha512-zrq/m7x/4gU8eIZYaIe6ED755sQJTsZGCi3IVUjfjFTnqQs5C4vtXczYcefALgYOXHIzatLxyOfF4nilOvpu1Q==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-scroll-container/-/uui-scroll-container-1.8.0.tgz", + "integrity": "sha512-PHxOZhsM53J3SQaXhH0euUwshr6Ka06iFEmtNKaqVe+nPwTjYHWHDuulvp7/qm/btjzSIrJqKgs02ft8wGqXwA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-select": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-select/-/uui-select-1.8.0-rc.0.tgz", - "integrity": "sha512-Lh4RTjBll7eU/ptCllBWd6XiCEgLNw+rqXwCL4bzEdHLaiZDVaMs9SUWJW8L1eAQcna7aPNewntIrmIv+Z5r0w==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-select/-/uui-select-1.8.0.tgz", + "integrity": "sha512-xFXo7IlAtofZCWIqnhyThIjP8ORRwo6786fv5yarayhqlKeAwJRD5t6QtX4z5z/1b/zW11oF2GkJzrzp4KN4vw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-slider": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-slider/-/uui-slider-1.8.0-rc.0.tgz", - "integrity": "sha512-y1tiqp4pGTUReatXkXJcC5TQLQFEiaG9sSxTAbMlKoAgkNuZjM1xhOcJ3mDHqiqaWWznjCFRDlXN62Os8dpdcw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-slider/-/uui-slider-1.8.0.tgz", + "integrity": "sha512-qKgWyVzMKF8RVwwgpdMvKfCS3TEyMbZj/ZKClgTJJfs+3r8Q002F7irx7Lgh+mDww+jLsuMtG/cu0xSXU4HC4w==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-symbol-expand": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-expand/-/uui-symbol-expand-1.8.0-rc.0.tgz", - "integrity": "sha512-rGvYaWeSArxubTf6m1S0Xgt4nLDPT0NOJzWgfI2+LqwpSoanH35Mb9+qXror2BLvog1jXfxwTZz1krqwSM3FwQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-expand/-/uui-symbol-expand-1.8.0.tgz", + "integrity": "sha512-AfpoR4dTOL4gEfP9lnEVymU3mlNfjFSuk8TGbqy0jVMTMbYeol5Bcl6jJFqqPd1npfgT7FPZH9zrMkcFogfSSw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-symbol-file": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file/-/uui-symbol-file-1.8.0-rc.0.tgz", - "integrity": "sha512-urGF7kWtxXxMqbFJkACDs7dPk3o9KfYgLrRSCqUYE8qrz98n0R31BZ9KzrtCma+a2uI17zvFyIuRYd3xvwCilg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file/-/uui-symbol-file-1.8.0.tgz", + "integrity": "sha512-/2O0TNl+Sx/cCy2kQlSCOujvRwz+Rxg9JxyMX5Vc14ZqrVJ4FsD2S/jJWLtE2YJ+EtLoc15Zzw2GogZO7aBcLQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-symbol-file-dropzone": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-dropzone/-/uui-symbol-file-dropzone-1.8.0-rc.0.tgz", - "integrity": "sha512-zWwx1jTQ3Ka4HoZ24EaHyJH3I0ez/qALf8qo5r5lLy4vn3UplunGKcNVFfOUsx6H19GlR4rDuPILIqdXJ4C/BQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-dropzone/-/uui-symbol-file-dropzone-1.8.0.tgz", + "integrity": "sha512-n2QRKTEnvQQgiyTQ7uVbz7XsGL0HRwaEtLqEMbaON6oYCsGWFFsbp8QqyHdB8iBQSrlV9I1J4sS0e5Ry+W25YQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-symbol-file-thumbnail": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-thumbnail/-/uui-symbol-file-thumbnail-1.8.0-rc.0.tgz", - "integrity": "sha512-KV0gVKXFkYtJaOgwcma1jNBHMXT/xwDSuio4uamwV9GkdvjS1kFQyAMtXqBArWFisuifcfMcnPyZYxxeKx/CfQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-thumbnail/-/uui-symbol-file-thumbnail-1.8.0.tgz", + "integrity": "sha512-KdFOrfVIwtjavoa+S5ro1gi2Er8IPqXnY6gpTRpAgfO/f+/ZRg6AwPKn4SCc7QqJ8ThHpsg8wki8WGiK04rfbA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-symbol-folder": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-folder/-/uui-symbol-folder-1.8.0-rc.0.tgz", - "integrity": "sha512-orMaoQIfbhF+gXS5FjW9mujP71GiuePQr/rCNLGWeSJntqudxRp8bjNrcop2kAUoqzK8fAAvaBM7uZTDxVxU5g==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-folder/-/uui-symbol-folder-1.8.0.tgz", + "integrity": "sha512-g7FIonq/5wHH2+e/+DhB+t3E4wu7dM4MrKxLsP6b8JmYz7Y0t9OlTBT5J+i3P8YnJKYY6i5V5Eip4QNWJqC+sQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-symbol-lock": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-lock/-/uui-symbol-lock-1.8.0-rc.0.tgz", - "integrity": "sha512-IJsTvHRPmG0OHnCgRQD69O7eUTDF8s/sY41uMiO9Mhhwf+CpE5R/LUJkMHvMZMCRkwMrSPCsq9cjCMNpalGepQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-lock/-/uui-symbol-lock-1.8.0.tgz", + "integrity": "sha512-+/K59hTkBJr6bYOrTgPtvZEVsr59MPNwvIHhGm685WZPZrNA9dGPZrO9vnw1eyNq70XYuHkiNkmKUmna9mQmIA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-symbol-more": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-more/-/uui-symbol-more-1.8.0-rc.0.tgz", - "integrity": "sha512-ELJpdd5XpzYYMh+ZxlESQhZkvH711UoCgOgP4e5ftU8suWUs1Jb34F/7GzppKkMbSKZlfGw7nCx3pz5n3C5ANg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-more/-/uui-symbol-more-1.8.0.tgz", + "integrity": "sha512-BSWQ05XYJSS6WKpA6//QnSnMehV5py5j8bxl7bZzmrthr2SwyejwS+pGYq7rTqWw7BBk1mA8I7Zkl+kVph/7+g==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-symbol-sort": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-sort/-/uui-symbol-sort-1.8.0-rc.0.tgz", - "integrity": "sha512-wH6Qyck706HK36mM0d+B1EWs17bM2Qv/V+DybKu/8DD+QNptOnuSzGkcCbRAV6lo36A3t7/NEru2xxvavBjUcw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-sort/-/uui-symbol-sort-1.8.0.tgz", + "integrity": "sha512-+IFJYlPsUvJYNDc8sWB4zan/dTCCj4vkqwXALW3xLSxpsKSvtSvXPzXK/i4YwaT4Azx4hfrWIW2cz6/h5JDonA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-table": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-table/-/uui-table-1.8.0-rc.0.tgz", - "integrity": "sha512-TbDlh3qdZDsksjFw18e3emfHeGw8tAApPcPXK78WIN7LNDlKiHDHClan0J/Ul+QLZD4rUDteHJ5C1okYb/jboA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-table/-/uui-table-1.8.0.tgz", + "integrity": "sha512-nbRoValRn17SadfpGKKT1RfyoRlCLhvij2BRMw1KyldztWlWGozPQSDjqhcEPSzJZCrNV76nIMbg5FLfsTl4iA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-tabs": { - "version": "1.8.0-rc.2", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.8.0-rc.2.tgz", - "integrity": "sha512-MLtDabiXsOEqOxfgEuqU3ji1XTgY9ABbhqOHC23cFaaGBwlqAbUyi9hAMJhfso406vkQa/9t9A7yK8qpMqKdrA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.8.0.tgz", + "integrity": "sha512-xnZvjjmRHJbyC9bd6LMYfHA8Jjb51GTJuFAd4j4S9NVZYomQDBFl7IKVWtUFzQvVzk93zJHVSWO8vmtNLQZreg==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-button": "1.8.0-rc.2", - "@umbraco-ui/uui-popover-container": "1.8.0-rc.0", - "@umbraco-ui/uui-symbol-more": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-button": "1.8.0", + "@umbraco-ui/uui-popover-container": "1.8.0", + "@umbraco-ui/uui-symbol-more": "1.8.0" } }, "node_modules/@umbraco-ui/uui-tag": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tag/-/uui-tag-1.8.0-rc.0.tgz", - "integrity": "sha512-3opqus5jf4Ou8JNnFOZTl64rRD6n6qffKrPn+mEHTMkUhGQ5/wTKvdfWg4G0lD60aEMzKd7L6rojZydPDLEwFA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tag/-/uui-tag-1.8.0.tgz", + "integrity": "sha512-5Dt7EsvACfs75bsncIDvqitXYub2Rfntbrc3gzXLHjqOQy2YBL5s/HNGz3xsf5msKuDAR0dmTyxhItaDAi7EkQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-textarea": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-textarea/-/uui-textarea-1.8.0-rc.0.tgz", - "integrity": "sha512-EYOzu5E6BEqacLNoMxYPhFwbjK5xRExI47drDV+LzfyUe3hgFLUCZkIA/Z35vSFN8+HyguT7aaCfpzHhoSbFwA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-textarea/-/uui-textarea-1.8.0.tgz", + "integrity": "sha512-WYWiD3x1DXbsAALmTT2txoeeqiZ9J/FlkTGL1Wasu89jfm8bAgxUG5wuoa8SL4r79rAF+RUDrJPygeUqDm0N8A==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@umbraco-ui/uui-toast-notification": { - "version": "1.8.0-rc.2", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.8.0-rc.2.tgz", - "integrity": "sha512-ICvxWZVuDO1X/f1udYgtY1prHYbj26g3ZecKq2V2FVs9Ej5kYNIWU1nVGj6tWkdyKGnVPjoLfYmq/W8i9BJb9g==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.8.0.tgz", + "integrity": "sha512-62q36/jggXp+GdRSzseQ7d63hUIXtHIXe/5bnSSHXcxxIcnbf9Sy3pkBkOYM7CXgBUUwt9T9IHLZ6eGw/6K9Xw==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-button": "1.8.0-rc.2", - "@umbraco-ui/uui-css": "1.8.0-rc.0", - "@umbraco-ui/uui-icon": "1.8.0-rc.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-button": "1.8.0", + "@umbraco-ui/uui-css": "1.8.0", + "@umbraco-ui/uui-icon": "1.8.0", + "@umbraco-ui/uui-icon-registry-essential": "1.8.0" } }, "node_modules/@umbraco-ui/uui-toast-notification-container": { - "version": "1.8.0-rc.2", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.8.0-rc.2.tgz", - "integrity": "sha512-iQ1xDQBgKrvTtCAUsT/3DJayCNVPWb+T9B5V+MyfuHnV9qOnmPtchs7l9r8cFwabOO5ZpxMke/tltsgMawwajQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.8.0.tgz", + "integrity": "sha512-NdILHgGvKFF+JZGejTJnXt1LdJIl/MxmPUj6+rvEGCO2SDhZmIOHjnIohT8HFytxmJqyWJpryPQjo6KJezRVbQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-toast-notification": "1.8.0-rc.2" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-toast-notification": "1.8.0" } }, "node_modules/@umbraco-ui/uui-toast-notification-layout": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-layout/-/uui-toast-notification-layout-1.8.0-rc.0.tgz", - "integrity": "sha512-ouQ1DlBlf0nkZZEsVIe+8Nz6mca1fjyyi0iypzDvwJq+K6tWm64FuxeqPpwp7M4jkNkcgDPOJqaONtPHQJdLKA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-layout/-/uui-toast-notification-layout-1.8.0.tgz", + "integrity": "sha512-NyGFv3kAcU8XMxLAyDhy3dt1oIHOwbAYO5+Utm4CFAAvcJzVTNn948Sp0dTdcfSjXjZG+3Ufv/Qb/OpcrybJ/w==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-css": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-css": "1.8.0" } }, "node_modules/@umbraco-ui/uui-toggle": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toggle/-/uui-toggle-1.8.0-rc.0.tgz", - "integrity": "sha512-OytTeUPk9X3H4w3vWo1JLG2WK2npoXOUBBKft4WwtN+hUoX9/KquxBrz0qkxdO/jQ4LpSRJopS9SqjNg5mqVug==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toggle/-/uui-toggle-1.8.0.tgz", + "integrity": "sha512-PNk2qeaL7bJXnSkG0T1J5pheNy7yTeaVbdyXuL/9fkoIdb9IkD/h6XLE9niiyWYF1xrdjpgAKZ32u0Oc4qXVyA==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0", - "@umbraco-ui/uui-boolean-input": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-boolean-input": "1.8.0" } }, "node_modules/@umbraco-ui/uui-visually-hidden": { - "version": "1.8.0-rc.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-visually-hidden/-/uui-visually-hidden-1.8.0-rc.0.tgz", - "integrity": "sha512-ZawyaL4DJsv0e7pyxiJWB5ogCxkn0mrzMimbajp6ZDDQMvAJDiA/I14hcaBVOudzZyTAzNK5xJlbB3ft2TwFdg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-visually-hidden/-/uui-visually-hidden-1.8.0.tgz", + "integrity": "sha512-3a4/B2uM3YfXjI9dyfSL2Z47ziwW7HuYoozNderKO/I7l0CgxgoHIOwF1sRb3QgOlsFhhfeKdndvU7SbD6tazQ==", "dependencies": { - "@umbraco-ui/uui-base": "1.8.0-rc.0" + "@umbraco-ui/uui-base": "1.8.0" } }, "node_modules/@ungap/structured-clone": { @@ -10680,9 +10603,9 @@ "dev": true }, "node_modules/dompurify": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz", - "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==" + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.4.tgz", + "integrity": "sha512-2gnshi6OshmuKil8rMZuQCGiUF3cUxHY3NGDzUAdUx/NPEe5DVnO8BDoAQouvgwnx0R/+a6jUn36Z0FSdq8vww==" }, "node_modules/dotenv": { "version": "16.4.5", @@ -10781,9 +10704,9 @@ "dev": true }, "node_modules/element-internals-polyfill": { - "version": "1.3.10", - "resolved": "https://registry.npmjs.org/element-internals-polyfill/-/element-internals-polyfill-1.3.10.tgz", - "integrity": "sha512-hflkht5sNZ2LF2sP9+OHfqGDcr8R9NIiDCuDfXep8uptqqt0OjZDaWJ/7ip+OdoIZBFJL+fFJ3+aLku1cTIEGA==" + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/element-internals-polyfill/-/element-internals-polyfill-1.3.11.tgz", + "integrity": "sha512-SQLQNVY4wMdpnP/F/HtalJbpEenQd46Avtjm5hvUdeTs3QU0zHFNX5/AmtQIPPcfzePb0ipCkQGY4GwYJIhLJA==" }, "node_modules/emoji-regex": { "version": "10.3.0", @@ -14700,9 +14623,9 @@ "dev": true }, "node_modules/lit": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.2.tgz", - "integrity": "sha512-VZx5iAyMtX7CV4K8iTLdCkMaYZ7ipjJZ0JcSdJ0zIdGxxyurjIn7yuuSxNBD7QmjvcNJwr0JS4cAdAtsy7gZ6w==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.3.tgz", + "integrity": "sha512-l4slfspEsnCcHVRTvaP7YnkTZEZggNFywLEIhQaGhYDczG+tu/vlgm/KaWIEjIp+ZyV20r2JnZctMb8LeLCG7Q==", "dependencies": { "@lit/reactive-element": "^2.0.4", "lit-element": "^4.0.4", @@ -14890,9 +14813,9 @@ } }, "node_modules/lucide-static": { - "version": "0.367.0", - "resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-0.367.0.tgz", - "integrity": "sha512-68o8sSfBE6+4RKtMkwqTqZIrYzORlLgnT1s0nBspUWeD96SLpAcb/tbralI/YvvO7yxFe6Qd0hH3iboa2cKKXg==", + "version": "0.379.0", + "resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-0.379.0.tgz", + "integrity": "sha512-oe0EPcRHVb0ua/XHs7B63fgtGI/8B/MchpXr6LZcXEQlPmqJeOua207Ju2f08g/yc8+LelngiFM0mD/B7O6mZA==", "dev": true }, "node_modules/lunr": { @@ -14972,9 +14895,9 @@ } }, "node_modules/marked": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.0.tgz", - "integrity": "sha512-Vkwtq9rLqXryZnWaQc86+FHLC6tr/fycMfYAhiOIXkrNmeGAyhSxjqu0Rs1i0bBqw5u0S7+lV9fdH2ZSVaoa0w==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", "bin": { "marked": "bin/marked.js" }, @@ -15964,9 +15887,9 @@ } }, "node_modules/monaco-editor": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.46.0.tgz", - "integrity": "sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==" + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.48.0.tgz", + "integrity": "sha512-goSDElNqFfw7iDHMg8WDATkfcyeLTNpBHQpO8incK6p5qZt5G/1j41X0xdGzpIkGojGXM+QiRQyLjnfDVvrpwA==" }, "node_modules/mri": { "version": "1.2.0", @@ -18545,9 +18468,9 @@ "dev": true }, "node_modules/simple-icons": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-11.11.0.tgz", - "integrity": "sha512-jF4FvxqJ5LGRgScMy6IWHYBuKG7SImwzsTDKHhmAB2RQVKEqvGWI8qmAHWYPKEGM8/oRA/VvKTVIbWdg7YPRsg==", + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-11.15.0.tgz", + "integrity": "sha512-uDAdtIGc56YJiGpdzImENY4E+5qtHEorW11KoXiwDj4u4YSY74G+q/a9idlY8iEqrjghHGkZ/ras0jRT7JpDTQ==", "dev": true, "engines": { "node": ">=0.12.18" @@ -19334,9 +19257,9 @@ "integrity": "sha512-3fCHKAeqT+xNwBVESf6iDbDV0VNwZNmfrkx9c/6Gz5iB8piMfaO6s7FvoiTrj1hf1gVbfyLTnz1DooI6DhgINQ==" }, "node_modules/tinymce-i18n": { - "version": "24.1.29", - "resolved": "https://registry.npmjs.org/tinymce-i18n/-/tinymce-i18n-24.1.29.tgz", - "integrity": "sha512-njcQUZ4UXl+IiZMpeXoIr6UEUkKCqHNQFjDgmwjxUbr3uMNff6k71hdBWGcIzIPOTvJoPSBr6wQPkWqjoMRkEg==" + "version": "24.5.8", + "resolved": "https://registry.npmjs.org/tinymce-i18n/-/tinymce-i18n-24.5.8.tgz", + "integrity": "sha512-wNWJl6c7GRdUizQVux34jRSc0rEvcClj0sNO9MsY9pNXem+N7cSLaarFTPIryNpeXcLKJIxYi/IMCI6E+rFbGA==" }, "node_modules/tmp": { "version": "0.0.33", @@ -19415,12 +19338,12 @@ } }, "node_modules/ts-api-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.0.tgz", - "integrity": "sha512-d+3WxW4r8WQy2cZWpNRPPGExX8ffOLGcIhheUANKbL5Sqjbhkneki76fRAWeXkaslV2etTb4tSJBSxOsH5+CJw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { - "node": ">=18" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" @@ -19740,9 +19663,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 8eb55c55d8..bf71749021 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "14.0.0-rc2", + "version": "14.1.0", "type": "module", "exports": { ".": null, @@ -11,6 +11,7 @@ "./controller-api": "./dist-cms/libs/controller-api/index.js", "./element-api": "./dist-cms/libs/element-api/index.js", "./extension-api": "./dist-cms/libs/extension-api/index.js", + "./formatting-api": "./dist-cms/libs/formatting-api/index.js", "./localization-api": "./dist-cms/libs/localization-api/index.js", "./observable-api": "./dist-cms/libs/observable-api/index.js", "./action": "./dist-cms/packages/core/action/index.js", @@ -41,6 +42,7 @@ "./extension-registry": "./dist-cms/packages/core/extension-registry/index.js", "./icon": "./dist-cms/packages/core/icon-registry/index.js", "./id": "./dist-cms/packages/core/id/index.js", + "./imaging": "./dist-cms/packages/media/imaging/index.js", "./language": "./dist-cms/packages/language/index.js", "./lit-element": "./dist-cms/packages/core/lit-element/index.js", "./localization": "./dist-cms/packages/core/localization/index.js", @@ -169,21 +171,21 @@ "npm": ">=10.1 < 11" }, "dependencies": { - "@types/diff": "^5.0.9", + "@types/diff": "^5.2.1", "@types/dompurify": "^3.0.5", "@types/uuid": "^9.0.8", - "@umbraco-ui/uui": "1.8.0-rc.3", - "@umbraco-ui/uui-css": "1.8.0-rc.0", + "@umbraco-ui/uui": "1.8.1", + "@umbraco-ui/uui-css": "1.8.0", "base64-js": "^1.5.1", "diff": "^5.2.0", - "dompurify": "^3.0.9", - "element-internals-polyfill": "^1.3.10", - "lit": "^3.1.2", - "marked": "^12.0.0", - "monaco-editor": "^0.46.0", + "dompurify": "^3.1.4", + "element-internals-polyfill": "^1.3.11", + "lit": "^3.1.3", + "marked": "^12.0.2", + "monaco-editor": "^0.48.0", "rxjs": "^7.8.1", "tinymce": "^6.8.3", - "tinymce-i18n": "^24.1.29", + "tinymce-i18n": "^24.5.8", "uuid": "^9.0.1" }, "devDependencies": { @@ -205,7 +207,7 @@ "@storybook/web-components-vite": "^7.6.17", "@types/chai": "^4.3.5", "@types/mocha": "^10.0.1", - "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", "@web/dev-server-esbuild": "^1.0.2", "@web/dev-server-import-maps": "^0.2.0", @@ -223,7 +225,7 @@ "eslint-plugin-storybook": "^0.6.15", "eslint-plugin-wc": "^2.0.4", "glob": "^10.3.10", - "lucide-static": "^0.367.0", + "lucide-static": "^0.379.0", "msw": "^1.3.2", "playwright-msw": "^3.0.1", "prettier": "3.2.5", @@ -234,12 +236,12 @@ "rollup-plugin-esbuild": "^6.1.1", "rollup-plugin-import-css": "^3.5.0", "rollup-plugin-web-worker-loader": "^1.6.1", - "simple-icons": "^11.11.0", + "simple-icons": "^11.15.0", "storybook": "^7.6.17", "tiny-glob": "^0.2.9", "tsc-alias": "^1.8.8", "typedoc": "^0.25.10", - "typescript": "^5.3.3", + "typescript": "^5.4.5", "typescript-json-schema": "^0.63.0", "vite": "^5.2.9", "vite-plugin-static-copy": "^1.0.2", diff --git a/src/apps/app/app-auth.controller.ts b/src/apps/app/app-auth.controller.ts index a0b7abb0d2..b893575d90 100644 --- a/src/apps/app/app-auth.controller.ts +++ b/src/apps/app/app-auth.controller.ts @@ -4,6 +4,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { setStoredPath } from '@umbraco-cms/backoffice/utils'; export class UmbAppAuthController extends UmbControllerBase { #authContext?: typeof UMB_AUTH_CONTEXT.TYPE; @@ -65,6 +66,14 @@ export class UmbAppAuthController extends UmbControllerBase { throw new Error('[Fatal] Auth context is not available'); } + // Save the current state + let currentUrl = window.location.href; + const searchParams = new URLSearchParams(window.location.search); + if (searchParams.has('returnPath')) { + currentUrl = decodeURIComponent(searchParams.get('returnPath') || currentUrl); + } + setStoredPath(currentUrl); + // Figure out which providers are available const availableProviders = await firstValueFrom(this.#authContext.getAuthProviders(umbExtensionsRegistry)); diff --git a/src/apps/app/app.element.ts b/src/apps/app/app.element.ts index a9f33d80b1..85e352156d 100644 --- a/src/apps/app/app.element.ts +++ b/src/apps/app/app.element.ts @@ -18,6 +18,7 @@ import { umbExtensionsRegistry, } from '@umbraco-cms/backoffice/extension-registry'; import { filter, first, firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; +import { hasOwnOpener, retrieveStoredPath } from '@umbraco-cms/backoffice/utils'; @customElement('umb-app') export class UmbAppElement extends UmbLitElement { @@ -76,7 +77,7 @@ export class UmbAppElement extends UmbLitElement { // The authorization request will be completed in the active window (main or popup) and the authorization signal will be sent. // If we are in a popup window, the storage event in UmbAuthContext will catch the signal and close the window. // If we are in the main window, the signal will be caught right here and the user will be redirected to the root. - if (window.opener) { + if (hasOwnOpener(this.backofficePath)) { (component as UmbAppErrorElement).errorMessage = hasCode ? this.localize.term('errors_externalLoginSuccess') : this.localize.term('errors_externalLoginFailed'); @@ -86,7 +87,15 @@ export class UmbAppElement extends UmbLitElement { : this.localize.term('errors_externalLoginFailed'); this.observe(this.#authContext.authorizationSignal, () => { - history.replaceState(null, '', ''); + // Redirect to the saved state or root + const url = retrieveStoredPath(); + const isBackofficePath = url?.pathname.startsWith(this.backofficePath) ?? true; + + if (isBackofficePath) { + history.replaceState(null, '', url?.toString() ?? ''); + } else { + window.location.href = url?.toString() ?? this.backofficePath; + } }); } @@ -99,6 +108,11 @@ export class UmbAppElement extends UmbLitElement { component: () => import('../upgrader/upgrader.element.js'), guards: [this.#isAuthorizedGuard()], }, + { + path: 'preview', + component: () => import('../preview/preview.element.js'), + guards: [this.#isAuthorizedGuard()], + }, { path: 'logout', resolve: () => { @@ -161,9 +175,13 @@ export class UmbAppElement extends UmbLitElement { // Try to initialise the auth flow and get the runtime status try { - // If the runtime level is "install" we should clear any cached tokens + // If the runtime level is "install" or ?status=false is set, we should clear any cached tokens // else we should try and set the auth status - if (this.#serverConnection.getStatus() === RuntimeLevelModel.INSTALL) { + const searchParams = new URLSearchParams(window.location.search); + if ( + (searchParams.has('status') && searchParams.get('status') === 'false') || + this.#serverConnection.getStatus() === RuntimeLevelModel.INSTALL + ) { await this.#authContext.clearTokenStorage(); } else { await this.#setAuthStatus(); @@ -207,6 +225,7 @@ export class UmbAppElement extends UmbLitElement { // Instruct all requests to use the auth flow to get and use the access_token for all subsequent requests OpenAPI.TOKEN = () => this.#authContext!.getLatestToken(); OpenAPI.WITH_CREDENTIALS = true; + OpenAPI.ENCODE_PATH = (path: string) => path; } #redirect() { diff --git a/src/apps/backoffice/backoffice.context.ts b/src/apps/backoffice/backoffice.context.ts index f460ac4776..adcdeeda39 100644 --- a/src/apps/backoffice/backoffice.context.ts +++ b/src/apps/backoffice/backoffice.context.ts @@ -9,6 +9,7 @@ import type { ManifestSection } from '@umbraco-cms/backoffice/extension-registry import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbExtensionManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; export class UmbBackofficeContext extends UmbContextBase { #activeSectionAlias = new UmbStringState(undefined); @@ -23,9 +24,6 @@ export class UmbBackofficeContext extends UmbContextBase { constructor(host: UmbControllerHost) { super(host, UMB_BACKOFFICE_CONTEXT); - new UmbExtensionsManifestInitializer(this, umbExtensionsRegistry, 'section', null, (sections) => { - this.#allowedSections.setValue([...sections]); - }); // TODO: We need to ensure this request is called every time the user logs in, but this should be done somewhere across the app and not here [JOV] this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => { @@ -34,6 +32,29 @@ export class UmbBackofficeContext extends UmbContextBase { this.#getVersion(); }); }); + + this.#init(); + } + + async #init() { + const userContext = await this.getContext(UMB_CURRENT_USER_CONTEXT); + this.observe( + userContext.allowedSections, + (allowedSections) => { + if (!allowedSections) return; + new UmbExtensionsManifestInitializer( + this, + umbExtensionsRegistry, + 'section', + (manifest) => allowedSections.includes(manifest.alias), + async (sections) => { + this.#allowedSections.setValue([...sections]); + }, + 'umbAllowedSectionsManifestInitializer', + ); + }, + 'umbAllowedSectionsObserver', + ); } async #getVersion() { diff --git a/src/apps/backoffice/backoffice.element.ts b/src/apps/backoffice/backoffice.element.ts index d292fc9be4..1dfb38b1c7 100644 --- a/src/apps/backoffice/backoffice.element.ts +++ b/src/apps/backoffice/backoffice.element.ts @@ -23,6 +23,7 @@ const CORE_PACKAGES = [ import('../../packages/media/umbraco-package.js'), import('../../packages/members/umbraco-package.js'), import('../../packages/models-builder/umbraco-package.js'), + import('../../packages/multi-url-picker/umbraco-package.js'), import('../../packages/packages/umbraco-package.js'), import('../../packages/property-editors/umbraco-package.js'), import('../../packages/relations/umbraco-package.js'), diff --git a/src/apps/backoffice/components/backoffice-header-logo.element.ts b/src/apps/backoffice/components/backoffice-header-logo.element.ts index faf844ff9f..1a2c453d63 100644 --- a/src/apps/backoffice/components/backoffice-header-logo.element.ts +++ b/src/apps/backoffice/components/backoffice-header-logo.element.ts @@ -44,6 +44,7 @@ export class UmbBackofficeHeaderLogoElement extends UmbLitElement { UmbTextStyles, css` #logo { + display: var(--umb-header-logo-display, inline); --uui-button-padding-top-factor: 1; --uui-button-padding-bottom-factor: 0.5; margin-right: var(--uui-size-space-2); @@ -61,6 +62,7 @@ export class UmbBackofficeHeaderLogoElement extends UmbLitElement { flex-direction: column; align-items: center; gap: var(--uui-size-space-3); + min-width: var(--uui-size-100); } `, ]; diff --git a/src/apps/backoffice/components/backoffice-header.element.ts b/src/apps/backoffice/components/backoffice-header.element.ts index feb5ef7681..3270a1bb74 100644 --- a/src/apps/backoffice/components/backoffice-header.element.ts +++ b/src/apps/backoffice/components/backoffice-header.element.ts @@ -20,7 +20,7 @@ export class UmbBackofficeHeaderElement extends UmbLitElement { } #appHeader { - background-color: var(--uui-color-header-surface); + background-color: var(--umb-header-background-color, var(--uui-color-header-surface)); display: flex; align-items: center; justify-content: space-between; diff --git a/src/apps/backoffice/components/backoffice-main.element.ts b/src/apps/backoffice/components/backoffice-main.element.ts index d94257088e..d2c9ce601f 100644 --- a/src/apps/backoffice/components/backoffice-main.element.ts +++ b/src/apps/backoffice/components/backoffice-main.element.ts @@ -64,22 +64,6 @@ export class UmbBackofficeMainElement extends UmbLitElement { } }); - if (this._sections.length > 0) { - const fallbackSectionPath = UMB_SECTION_PATH_PATTERN.generateLocal({ - sectionName: this._sections[0].manifest!.meta.pathname, - }); - this._routes.push({ - alias: '__redirect', - path: '/', - redirectTo: fallbackSectionPath, - }); - this._routes.push({ - alias: '__redirect', - path: '/section/', - redirectTo: fallbackSectionPath, - }); - } - this.requestUpdate('_routes', oldValue); } diff --git a/src/apps/preview/apps/manifests.ts b/src/apps/preview/apps/manifests.ts new file mode 100644 index 0000000000..70b6c62c48 --- /dev/null +++ b/src/apps/preview/apps/manifests.ts @@ -0,0 +1,32 @@ +import type { ManifestPreviewAppProvider } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'previewApp', + alias: 'Umb.PreviewApps.Device', + name: 'Preview: Device Switcher', + element: () => import('./preview-device.element.js'), + weight: 400, + }, + { + type: 'previewApp', + alias: 'Umb.PreviewApps.Culture', + name: 'Preview: Culture Switcher', + element: () => import('./preview-culture.element.js'), + weight: 300, + }, + { + type: 'previewApp', + alias: 'Umb.PreviewApps.OpenWebsite', + name: 'Preview: Open Website Button', + element: () => import('./preview-open-website.element.js'), + weight: 200, + }, + { + type: 'previewApp', + alias: 'Umb.PreviewApps.Exit', + name: 'Preview: Exit Button', + element: () => import('./preview-exit.element.js'), + weight: 100, + }, +]; diff --git a/src/apps/preview/apps/preview-culture.element.ts b/src/apps/preview/apps/preview-culture.element.ts new file mode 100644 index 0000000000..bfa6d14c39 --- /dev/null +++ b/src/apps/preview/apps/preview-culture.element.ts @@ -0,0 +1,103 @@ +import { UMB_PREVIEW_CONTEXT } from '../preview.context.js'; +import { css, customElement, html, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language'; +import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; + +const elementName = 'umb-preview-culture'; + +@customElement(elementName) +export class UmbPreviewCultureElement extends UmbLitElement { + #languageRepository = new UmbLanguageCollectionRepository(this); + + @state() + private _culture?: UmbLanguageDetailModel; + + @state() + private _cultures: Array = []; + + connectedCallback() { + super.connectedCallback(); + this.#getCultures(); + } + + async #getCultures() { + const { data: langauges } = await this.#languageRepository.requestCollection({ skip: 0, take: 100 }); + this._cultures = langauges?.items ?? []; + + const searchParams = new URLSearchParams(window.location.search); + const culture = searchParams.get('culture'); + + if (culture && culture !== this._culture?.unique) { + this._culture = this._cultures.find((c) => c.unique === culture); + } + } + + async #onClick(culture: UmbLanguageDetailModel) { + if (this._culture === culture) return; + this._culture = culture; + + const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT); + previewContext.updateIFrame({ culture: culture.unique }); + } + + render() { + if (this._cultures.length <= 1) return nothing; + return html` + +
+ + ${this._culture?.name ?? this.localize.term('treeHeaders_languages')} +
+
+ + + ${repeat( + this._cultures, + (item) => item.unique, + (item) => html` + this.#onClick(item)}> + + + `, + )} + + + `; + } + + static styles = [ + css` + :host { + display: flex; + border-left: 1px solid var(--uui-color-header-contrast); + --uui-button-font-weight: 400; + --uui-button-padding-left-factor: 3; + --uui-button-padding-right-factor: 3; + } + + uui-button > div { + display: flex; + align-items: center; + gap: 5px; + } + + umb-popover-layout { + --uui-color-surface: var(--uui-color-header-surface); + --uui-color-border: var(--uui-color-header-surface); + color: var(--uui-color-header-contrast); + } + `, + ]; +} + +export { UmbPreviewCultureElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbPreviewCultureElement; + } +} diff --git a/src/apps/preview/apps/preview-device.element.ts b/src/apps/preview/apps/preview-device.element.ts new file mode 100644 index 0000000000..73ceccf8d7 --- /dev/null +++ b/src/apps/preview/apps/preview-device.element.ts @@ -0,0 +1,149 @@ +import { UMB_PREVIEW_CONTEXT } from '../preview.context.js'; +import { css, customElement, html, property, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +export interface UmbPreviewDevice { + alias: string; + label: string; + css: string; + icon: string; + dimensions: { height: string; width: string }; +} + +const elementName = 'umb-preview-device'; + +@customElement(elementName) +export class UmbPreviewDeviceElement extends UmbLitElement { + #devices: Array = [ + { + alias: 'fullsize', + label: 'Fit browser', + css: 'fullsize', + icon: 'icon-application-window-alt', + dimensions: { height: '100%', width: '100%' }, + }, + { + alias: 'desktop', + label: 'Desktop', + css: 'desktop shadow', + icon: 'icon-display', + dimensions: { height: '1080px', width: '1920px' }, + }, + { + alias: 'laptop', + label: 'Laptop', + css: 'laptop shadow', + icon: 'icon-laptop', + dimensions: { height: '768px', width: '1366px' }, + }, + { + alias: 'ipad-portrait', + label: 'Tablet portrait', + css: 'ipad-portrait shadow', + icon: 'icon-ipad', + dimensions: { height: '929px', width: '769px' }, + }, + { + alias: 'ipad-landscape', + label: 'Tablet landscape', + css: 'ipad-landscape shadow flip', + icon: 'icon-ipad', + dimensions: { height: '675px', width: '1024px' }, + }, + { + alias: 'smartphone-portrait', + label: 'Smartphone portrait', + css: 'smartphone-portrait shadow', + icon: 'icon-iphone', + dimensions: { height: '640px', width: '360px' }, + }, + { + alias: 'smartphone-landscape', + label: 'Smartphone landscape', + css: 'smartphone-landscape shadow flip', + icon: 'icon-iphone', + dimensions: { height: '360px', width: '640px' }, + }, + ]; + + @property({ attribute: false, type: Object }) + device = this.#devices[0]; + + connectedCallback() { + super.connectedCallback(); + this.#changeDevice(this.device); + } + + async #changeDevice(device: UmbPreviewDevice) { + if (device === this.device) return; + + this.device = device; + + const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT); + + previewContext?.updateIFrame({ + className: device.css, + height: device.dimensions.height, + width: device.dimensions.width, + }); + } + + render() { + return html` + +
+ + ${this.device.label} +
+
+ + + ${repeat( + this.#devices, + (item) => item.alias, + (item) => html` + this.#changeDevice(item)}> + + + `, + )} + + + `; + } + + static styles = [ + css` + :host { + display: flex; + border-left: 1px solid var(--uui-color-header-contrast); + --uui-button-font-weight: 400; + --uui-button-padding-left-factor: 3; + --uui-button-padding-right-factor: 3; + } + + uui-button > div { + display: flex; + align-items: center; + gap: 5px; + } + + umb-popover-layout { + --uui-color-surface: var(--uui-color-header-surface); + --uui-color-border: var(--uui-color-header-surface); + color: var(--uui-color-header-contrast); + } + `, + ]; +} + +export { UmbPreviewDeviceElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbPreviewDeviceElement; + } +} diff --git a/src/apps/preview/apps/preview-exit.element.ts b/src/apps/preview/apps/preview-exit.element.ts new file mode 100644 index 0000000000..001f86bbf8 --- /dev/null +++ b/src/apps/preview/apps/preview-exit.element.ts @@ -0,0 +1,49 @@ +import { UMB_PREVIEW_CONTEXT } from '../preview.context.js'; +import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +const elementName = 'umb-preview-exit'; +@customElement(elementName) +export class UmbPreviewExitElement extends UmbLitElement { + async #onClick() { + const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT); + previewContext.exitPreview(0); + } + + render() { + return html` + +
+ + ${this.localize.term('preview_endLabel')} +
+
+ `; + } + + static styles = [ + css` + :host { + display: flex; + border-left: 1px solid var(--uui-color-header-contrast); + --uui-button-font-weight: 400; + --uui-button-padding-left-factor: 3; + --uui-button-padding-right-factor: 3; + } + + uui-button > div { + display: flex; + align-items: center; + gap: 5px; + } + `, + ]; +} + +export { UmbPreviewExitElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbPreviewExitElement; + } +} diff --git a/src/apps/preview/apps/preview-open-website.element.ts b/src/apps/preview/apps/preview-open-website.element.ts new file mode 100644 index 0000000000..e1d9bc285f --- /dev/null +++ b/src/apps/preview/apps/preview-open-website.element.ts @@ -0,0 +1,49 @@ +import { UMB_PREVIEW_CONTEXT } from '../preview.context.js'; +import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +const elementName = 'umb-preview-open-website'; +@customElement(elementName) +export class UmbPreviewOpenWebsiteElement extends UmbLitElement { + async #onClick() { + const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT); + previewContext.openWebsite(); + } + + render() { + return html` + +
+ + ${this.localize.term('preview_openWebsiteLabel')} +
+
+ `; + } + + static styles = [ + css` + :host { + display: flex; + border-left: 1px solid var(--uui-color-header-contrast); + --uui-button-font-weight: 400; + --uui-button-padding-left-factor: 3; + --uui-button-padding-right-factor: 3; + } + + uui-button > div { + display: flex; + align-items: center; + gap: 5px; + } + `, + ]; +} + +export { UmbPreviewOpenWebsiteElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbPreviewOpenWebsiteElement; + } +} diff --git a/src/apps/preview/preview.context.ts b/src/apps/preview/preview.context.ts new file mode 100644 index 0000000000..eba05b4198 --- /dev/null +++ b/src/apps/preview/preview.context.ts @@ -0,0 +1,208 @@ +import { UMB_APP_CONTEXT } from '../app/app.context.js'; +import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbDocumentPreviewRepository } from '@umbraco-cms/backoffice/document'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +const UMB_LOCALSTORAGE_SESSION_KEY = 'umb:previewSessions'; + +export class UmbPreviewContext extends UmbContextBase { + #culture?: string | null; + #serverUrl: string = ''; + #webSocket?: WebSocket; + #unique?: string | null; + + #iframeReady = new UmbBooleanState(false); + public readonly iframeReady = this.#iframeReady.asObservable(); + + #previewUrl = new UmbStringState(undefined); + public readonly previewUrl = this.#previewUrl.asObservable(); + + #documentPreviewRepository = new UmbDocumentPreviewRepository(this); + + constructor(host: UmbControllerHost) { + super(host, UMB_PREVIEW_CONTEXT); + this.#init(); + } + + async #init() { + const appContext = await this.getContext(UMB_APP_CONTEXT); + this.#serverUrl = appContext.getServerUrl(); + + const params = new URLSearchParams(window.location.search); + + this.#culture = params.get('culture'); + this.#unique = params.get('id'); + + if (!this.#unique) { + console.error('No unique ID found in query string.'); + return; + } + + this.#setPreviewUrl(); + } + + #configureWebSocket() { + if (this.#webSocket && this.#webSocket.readyState < 2) return; + + const url = `${this.#serverUrl.replace('https://', 'wss://')}/umbraco/PreviewHub`; + + this.#webSocket = new WebSocket(url); + + this.#webSocket.addEventListener('open', () => { + // NOTE: SignalR protocol handshake; it requires a terminating control character. + const endChar = String.fromCharCode(30); + this.#webSocket?.send(`{"protocol":"json","version":1}${endChar}`); + }); + + this.#webSocket.addEventListener('message', (event: MessageEvent) => { + if (!event?.data) return; + + // NOTE: Strip the terminating control character, (from SignalR). + const data = event.data.substring(0, event.data.length - 1); + const json = JSON.parse(data) as { type: number; target: string; arguments: Array }; + + if (json.type === 1 && json.target === 'refreshed') { + const pageId = json.arguments?.[0]; + if (pageId === this.#unique) { + this.#setPreviewUrl({ rnd: Math.random() }); + } + } + }); + } + + #getSessionCount(): number { + return Math.max(Number(localStorage.getItem(UMB_LOCALSTORAGE_SESSION_KEY)), 0) || 0; + } + + #setPreviewUrl(args?: { serverUrl?: string; unique?: string | null; culture?: string | null; rnd?: number }) { + const host = args?.serverUrl || this.#serverUrl; + const path = args?.unique || this.#unique; + const params = new URLSearchParams(); + const culture = args?.culture || this.#culture; + + if (culture) params.set('culture', culture); + if (args?.rnd) params.set('rnd', args.rnd.toString()); + + this.#previewUrl.setValue(`${host}/${path}?${params}`); + } + + #setSessionCount(sessions: number) { + localStorage.setItem(UMB_LOCALSTORAGE_SESSION_KEY, sessions.toString()); + } + + checkSession() { + const sessions = this.#getSessionCount(); + if (sessions > 0) return; + + umbConfirmModal(this._host, { + headline: `Preview website?`, + content: `You have ended preview mode, do you want to enable it again to view the latest saved version of your website?`, + cancelLabel: 'View published version', + confirmLabel: 'Preview latest version', + }) + .then(() => { + this.restartSession(); + }) + .catch(() => { + this.exitSession(); + }); + } + + async exitPreview(sessions: number = 0) { + this.#setSessionCount(sessions); + + // We are good to end preview mode. + if (sessions <= 0) { + await this.#documentPreviewRepository.exit(); + } + + if (this.#webSocket) { + this.#webSocket.close(); + this.#webSocket = undefined; + } + + const url = this.#previewUrl.getValue() as string; + window.location.replace(url); + } + + async exitSession() { + let sessions = this.#getSessionCount(); + sessions--; + this.exitPreview(sessions); + } + + iframeLoaded(iframe: HTMLIFrameElement) { + if (!iframe) return; + this.#configureWebSocket(); + this.#iframeReady.setValue(true); + } + + getIFrameWrapper(): HTMLElement | undefined { + return this.getHostElement().shadowRoot?.querySelector('#wrapper') as HTMLElement; + } + + openWebsite() { + const url = this.#previewUrl.getValue() as string; + window.open(url, '_blank'); + } + + // TODO: [LK] Figure out how to make `iframe.contentDocument` works, as it's not from SameOrigin. + reloadIFrame(iframe: HTMLIFrameElement) { + const document = iframe.contentDocument; + if (!document) return; + + document.location.reload(); + } + + async restartSession() { + await this.#documentPreviewRepository.enter(); + this.startSession(); + } + + startSession() { + let sessions = this.#getSessionCount(); + sessions++; + this.#setSessionCount(sessions); + } + + async updateIFrame(args?: { culture?: string; className?: string; height?: string; width?: string }) { + if (!args) return; + + const wrapper = this.getIFrameWrapper(); + if (!wrapper) return; + + const scaleIFrame = () => { + if (wrapper.className === 'fullsize') { + wrapper.style.transform = ''; + } else { + const wScale = document.body.offsetWidth / (wrapper.offsetWidth + 30); + const hScale = document.body.offsetHeight / (wrapper.offsetHeight + 30); + const scale = Math.min(wScale, hScale, 1); // get the lowest ratio, but not higher than 1 + wrapper.style.transform = `scale(${scale})`; + } + }; + + window.addEventListener('resize', scaleIFrame); + wrapper.addEventListener('transitionend', scaleIFrame); + + if (args.culture) { + this.#iframeReady.setValue(false); + + const params = new URLSearchParams(window.location.search); + params.set('culture', args.culture); + const newRelativePathQuery = window.location.pathname + '?' + params.toString(); + history.pushState(null, '', newRelativePathQuery); + + this.#setPreviewUrl({ culture: args.culture }); + } + + if (args.className) wrapper.className = args.className; + if (args.height) wrapper.style.height = args.height; + if (args.width) wrapper.style.width = args.width; + } +} + +export const UMB_PREVIEW_CONTEXT = new UmbContextToken('UmbPreviewContext'); diff --git a/src/apps/preview/preview.element.ts b/src/apps/preview/preview.element.ts new file mode 100644 index 0000000000..35a8619227 --- /dev/null +++ b/src/apps/preview/preview.element.ts @@ -0,0 +1,204 @@ +import { manifests as previewApps } from './apps/manifests.js'; +import { UmbPreviewContext } from './preview.context.js'; +import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +const elementName = 'umb-preview'; + +/** + * @element umb-preview + */ +@customElement(elementName) +export class UmbPreviewElement extends UmbLitElement { + #context = new UmbPreviewContext(this); + + constructor() { + super(); + + if (previewApps?.length) { + umbExtensionsRegistry.registerMany(previewApps); + } + + this.observe(this.#context.iframeReady, (iframeReady) => (this._iframeReady = iframeReady)); + this.observe(this.#context.previewUrl, (previewUrl) => (this._previewUrl = previewUrl)); + } + + connectedCallback() { + super.connectedCallback(); + this.addEventListener('visibilitychange', this.#onVisibilityChange); + window.addEventListener('beforeunload', () => this.#context.exitSession()); + this.#context.startSession(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.removeEventListener('visibilitychange', this.#onVisibilityChange); + // NOTE: Unsure how we remove an anonymous function from 'beforeunload' event listener. + // The reason for the anonymous function is that if we used a named function, + // `this` would be the `window` and would not have context to the class instance. [LK] + //window.removeEventListener('beforeunload', () => this.#context.exitSession()); + this.#context.exitSession(); + } + + @state() + private _iframeReady?: boolean; + + @state() + private _previewUrl?: string; + + #onIFrameLoad(event: Event & { target: HTMLIFrameElement }) { + this.#context.iframeLoaded(event.target); + } + + #onVisibilityChange() { + this.#context.checkSession(); + } + + render() { + if (!this._previewUrl) return nothing; + return html` + ${when(!this._iframeReady, () => html`
`)} +
+
+ +
+
+ + `; + } + + static styles = [ + css` + :host { + display: flex; + justify-content: center; + align-items: center; + + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + padding-bottom: 40px; + } + + #loading { + display: flex; + align-items: center; + justify-content: center; + + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + font-size: 6rem; + backdrop-filter: blur(5px); + } + + #wrapper { + transition: all 240ms cubic-bezier(0.165, 0.84, 0.44, 1); + flex-shrink: 0; + height: 100%; + width: 100%; + } + + #wrapper.fullsize { + margin: 0 auto; + overflow: hidden; + } + + #wrapper.shadow { + margin: 10px auto; + background-color: white; + border-radius: 3px; + overflow: hidden; + opacity: 1; + box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.26); + } + + #container { + width: 100%; + height: 100%; + margin: 0 auto; + overflow: hidden; + } + + #menu { + display: flex; + justify-content: space-between; + align-items: center; + + position: absolute; + bottom: 0; + left: 0; + right: 0; + + background-color: var(--uui-color-header-surface); + height: 40px; + + animation: menu-bar-animation 1.2s; + animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); + } + + #menu > h4 { + color: var(--uui-color-header-contrast-emphasis); + margin: 0; + padding: 0 15px; + } + + #menu > uui-button-group { + height: 100%; + } + + uui-icon.flip { + rotate: 90deg; + } + + iframe { + border: 0; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + overflow-x: hidden; + overflow-y: hidden; + } + + @keyframes menu-bar-animation { + 0% { + bottom: -50px; + } + 40% { + bottom: -50px; + } + 80% { + bottom: 0px; + } + } + `, + ]; +} + +export default UmbPreviewElement; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbPreviewElement; + } +} diff --git a/src/apps/preview/preview.stories.ts b/src/apps/preview/preview.stories.ts new file mode 100644 index 0000000000..fb6732e13d --- /dev/null +++ b/src/apps/preview/preview.stories.ts @@ -0,0 +1,10 @@ +import type { Meta } from '@storybook/web-components'; +import { html } from '@umbraco-cms/backoffice/external/lit'; + +export default { + title: 'Apps/Preview', + component: 'umb-preview', + id: 'umb-preview', +} satisfies Meta; + +export const Preview = () => html``; diff --git a/src/apps/preview/preview.test.ts b/src/apps/preview/preview.test.ts new file mode 100644 index 0000000000..3037fbf6da --- /dev/null +++ b/src/apps/preview/preview.test.ts @@ -0,0 +1,14 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { UmbPreviewElement } from './preview.element.js'; + +describe('UmbPreview', () => { + let element: UmbPreviewElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbPreviewElement); + }); +}); diff --git a/src/assets/css/umbraco-blockgridlayout-flexbox.css b/src/assets/css/umbraco-blockgridlayout-flexbox.css new file mode 100644 index 0000000000..3f921c0470 --- /dev/null +++ b/src/assets/css/umbraco-blockgridlayout-flexbox.css @@ -0,0 +1,35 @@ +/** Example of how a grid layout stylehseet could be done with Flex box: */ + +.umb-block-grid__layout-container { + position: relative; + display: flex; + flex-wrap: wrap; + gap: var(--umb-block-grid--row-gap, 0) var(--umb-block-grid--column-gap, 0); +} +.umb-block-grid__layout-item { + position: relative; + --umb-block-grid__layout-item-calc: calc(var(--umb-block-grid--item-column-span) / var(--umb-block-grid--grid-columns)); + width: calc(var(--umb-block-grid__layout-item-calc) * 100% - (1 - var(--umb-block-grid__layout-item-calc)) * var(--umb-block-grid--column-gap, 0px)); +} + + +.umb-block-grid__area-container, .umb-block-grid__block--view::part(area-container) { + position: relative; + display: flex; + flex-wrap: wrap; + width: 100%; + gap: var(--umb-block-grid--areas-row-gap, 0) var(--umb-block-grid--areas-column-gap, 0); +} +.umb-block-grid__area { + position: relative; + height: 100%; + display: flex; + flex-direction: column; + --umb-block-grid__area-calc: calc(var(--umb-block-grid--area-column-span) / var(--umb-block-grid--area-grid-columns, 1)); + width: calc(var(--umb-block-grid__area-calc) * 100% - (1 - var(--umb-block-grid__area-calc)) * var(--umb-block-grid--areas-column-gap, 0px)); +} + + +.umb-block-grid__actions { + clear: both; +} diff --git a/src/assets/css/umbraco-blockgridlayout.css b/src/assets/css/umbraco-blockgridlayout.css new file mode 100644 index 0000000000..8a4f567cac --- /dev/null +++ b/src/assets/css/umbraco-blockgridlayout.css @@ -0,0 +1,46 @@ +.umb-block-grid__layout-container { + position: relative; + display: grid; + grid-template-columns: repeat(var(--umb-block-grid--grid-columns, 1), minmax(0, 1fr)); + grid-auto-flow: row; + grid-auto-rows: minmax(50px, min-content); + + column-gap: var(--umb-block-grid--column-gap, 0); + row-gap: var(--umb-block-grid--row-gap, 0); +} +.umb-block-grid__layout-item { + position: relative; + /* For small devices we scale columnSpan by three, to make everything bigger than 1/3 take full width: */ + grid-column-end: span min(calc(var(--umb-block-grid--item-column-span, 1) * 3), var(--umb-block-grid--grid-columns)); + grid-row: span var(--umb-block-grid--item-row-span, 1); +} + + +.umb-block-grid__area-container, .umb-block-grid__block--view::part(area-container) { + position: relative; + display: grid; + grid-template-columns: repeat(var(--umb-block-grid--area-grid-columns, var(--umb-block-grid--grid-columns, 1)), minmax(0, 1fr)); + grid-auto-flow: row; + grid-auto-rows: minmax(50px, min-content); + + column-gap: var(--umb-block-grid--areas-column-gap, 0); + row-gap: var(--umb-block-grid--areas-row-gap, 0); +} +.umb-block-grid__area { + position: relative; + height: 100%; + display: flex; + flex-direction: column; + /* For small devices we scale columnSpan by three, to make everything bigger than 1/3 take full width: */ + grid-column-end: span min(calc(var(--umb-block-grid--area-column-span, 1) * 3), var(--umb-block-grid--area-grid-columns)); + grid-row: span var(--umb-block-grid--area-row-span, 1); +} + +@media (min-width:1024px) { + .umb-block-grid__layout-item { + grid-column-end: span min(var(--umb-block-grid--item-column-span, 1), var(--umb-block-grid--grid-columns)); + } + .umb-block-grid__area { + grid-column-end: span min(var(--umb-block-grid--area-column-span, 1), var(--umb-block-grid--area-grid-columns)); + } +} \ No newline at end of file diff --git a/src/assets/favicon.svg b/src/assets/favicon.svg index 7f139d6a6b..3e84ef7cb3 100644 --- a/src/assets/favicon.svg +++ b/src/assets/favicon.svg @@ -1,17 +1 @@ - - - - - - + diff --git a/src/assets/installer-illustration.svg b/src/assets/installer-illustration.svg index b7d84eab18..67942fe98e 100644 --- a/src/assets/installer-illustration.svg +++ b/src/assets/installer-illustration.svg @@ -1,722 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/assets/lang/da-dk.ts b/src/assets/lang/da-dk.ts index fa0a3caba6..b38bcea472 100644 --- a/src/assets/lang/da-dk.ts +++ b/src/assets/lang/da-dk.ts @@ -548,13 +548,15 @@ export default { noIconsFound: 'Ingen ikoner blev fundet', noMacroParams: 'Der er ingen parametre for denne makro', noMacros: 'Der er ikke tilføjet nogen makroer', - externalLoginProviders: 'Eksterne login-udbydere', + externalLoginProviders: 'Eksternt login', exceptionDetail: 'Undtagelsesdetaljer', stacktrace: 'Stacktrace', innerException: 'Indre undtagelse', - linkYour: 'Link dit', - unLinkYour: 'Fjern link fra dit', - account: 'konto', + linkYour: 'Link din {0} konto', + linkYourConfirm: 'For at linke dine Umbraco og {0} konti, vil du blive sendt til {0} for at bekræfte.', + unLinkYour: 'Fjern link fra din {0} konto', + unLinkYourConfirm: 'Du er ved at fjerne linket mellem dine Umbraco og {0} konti og du vil blive logget ud.', + linkedToService: 'Din konto er linket til denne service', selectEditor: 'Vælg editor', selectEditorConfiguration: 'Vælg konfiguration', selectSnippet: 'Vælg snippet', @@ -856,6 +858,7 @@ export default { upload: 'Upload', url: 'URL', user: 'Bruger', + users: 'Brugere', username: 'Brugernavn', value: 'Værdi', view: 'Vis', @@ -874,6 +877,10 @@ export default { saving: 'Gemmer...', current: 'nuværende', embed: 'Indlejring', + addEditLink: 'Tilføj/Rediger Link', + removeLink: 'Fjern Link', + mediaPicker: 'Mediebibliotek', + viewSourceCode: 'Vis kildekode', selected: 'valgt', other: 'Andet', articles: 'Artikler', @@ -929,6 +936,10 @@ export default { font: 'Skrifttype', text: 'Tekst', }, + globalSearch: { + navigateSearchProviders: 'Naviger søgeudbydere', + navigateSearchResults: 'Naviger søgeresultater', + }, headers: { page: 'Side', }, @@ -1787,6 +1798,7 @@ export default { partialViews: 'Partial Views', partialViewMacros: 'Partial View makrofiler', repositories: 'Installer fra "repository"', + relations: 'Relationer', runway: 'Installer Runway', runwayModules: 'Runway-moduler', scripting: 'Scripting filer', @@ -1837,6 +1849,7 @@ export default { failedPasswordAttempts: 'Fejlede loginforsøg', goToProfile: 'Gå til brugerprofil', groupsHelp: 'Tilføj grupper for at tildele adgang og tilladelser', + invite: 'Invitér', inviteAnotherUser: 'Invitér anden bruger', inviteUserHelp: 'Invitér nye brugere til at give dem adgang til Umbraco. En invitation vil blive sendt\n via e-mail til brugeren med oplysninger om, hvordan man logger ind i Umbraco.\n ', @@ -1879,6 +1892,9 @@ export default { searchAllChildren: "Søg alle 'børn'", languagesHelp: 'Tilføj sprog for at give brugerne adgang til at redigere', sectionsHelp: 'Tilføj sektioner for at give brugerne adgang', + allowAccessToAllLanguages: 'Tillad adgang til alle sprog', + allowAccessToAllDocuments: 'Tillad adgang til alle dokumenter', + allowAccessToAllMedia: 'Tillad adgang til alle medier', selectUserGroup: (multiple: boolean) => { return multiple ? 'Vælg brugergrupper' : 'Vælg brugergruppe'; }, @@ -1896,6 +1912,7 @@ export default { username: 'Navn', userPermissions: 'Brugertilladelser', usergroup: 'Brugergruppe', + usergroups: 'Brugergrupper', userInvited: 'er blevet inviteret', userInvitedSuccessHelp: 'En invitation er blevet sendt til den nye bruger med oplysninger om, hvordan man\n logger ind i Umbraco.\n ', @@ -1953,7 +1970,6 @@ export default { passwordRequiresUniqueChars: 'The password must use at least %0% different characters', passwordRequiresUpper: "The password must have at least one uppercase ('A'-'Z')", passwordTooShort: 'The password must be at least %0% characters long', - allowAccessToAllLanguages: 'Allow access to all languages', userHasPassword: 'The user already has a password set', userHasGroup: "The user is already in group '%0%'", userLockoutNotEnabled: 'Lockout is not enabled for this user', @@ -2457,6 +2473,8 @@ export default { getStarted: 'To get you started', }, settingsDashboard: { + documentationHeader: 'Dokumentation', + documentationDescription: 'Læs mere om at arbejde med elementerne i Indstillinger i vores Dokumentation.', communityHeader: 'Community', communityDescription: 'Stil et spørgsmål i community forummet eller i vores Discord community', trainingHeader: 'Træning', @@ -2477,6 +2495,20 @@ export default { fallbackDescription: "Thank you for choosing Umbraco - we think this could be the beginning of something\n beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast\n as possible.\n ", }, + welcomeDashboard: { + ourUmbracoHeadline: 'Our Umbraco - Det Venligste Fællesskab', + ourUmbracoDescription: + 'Our Umbraco, den officielle fællesskabsplatform, er dit komplette sted for alt, hvad der vedrører Umbraco. Uanset om du har brug for svar på spørgsmål, spændende tilføjelser eller vejledninger til at udføre noget i Umbraco, er verdens bedste og venligste fællesskab kun et klik væk.', + ourUmbracoButton: 'Besøg Our Umbraco', + documentationHeadline: 'Dokumentation', + documentationDescription: 'Find svarene på alle dine Umbraco-spørgsmål', + communityHeadline: 'Fællesskab', + communityDescription: 'Få support og inspiration fra engagerede Umbraco-eksperter', + resourcesHeadline: 'Ressourcer', + resourcesDescription: "Gratis video tutorials til at kickstarte din rejse med CMS'et", + trainingHeadline: 'Træning', + trainingDescription: 'Praktisk træning og officielle certificeringer fra Umbraco', + }, analytics: { consentForAnalytics: 'Consent for telemetry data', analyticsLevelSavedSuccess: 'Telemetry level saved!', diff --git a/src/assets/lang/en-us.ts b/src/assets/lang/en-us.ts index a1f3c17630..3da222ba60 100644 --- a/src/assets/lang/en-us.ts +++ b/src/assets/lang/en-us.ts @@ -559,13 +559,16 @@ export default { noIconsFound: 'No icons were found', noMacroParams: 'There are no parameters for this macro', noMacros: 'There are no macros available to insert', - externalLoginProviders: 'External login providers', + externalLoginProviders: 'External logins', exceptionDetail: 'Exception Details', stacktrace: 'Stacktrace', innerException: 'Inner Exception', - linkYour: 'Link your', - unLinkYour: 'Un-link your', - account: 'account', + linkYour: 'Link your {0} account', + linkYourConfirm: + 'You are about to link your Umbraco and {0} accounts and you will be redirected to {0} to confirm.', + unLinkYour: 'Un-link your {0} account', + unLinkYourConfirm: 'You are about to un-link your Umbraco and {0} accounts and you will be logged out.', + linkedToService: 'Your account is linked to this service', selectEditor: 'Select editor', selectEditorConfiguration: 'Select configuration', selectSnippet: 'Select snippet', @@ -875,6 +878,7 @@ export default { upload: 'Upload', url: 'URL', user: 'User', + users: 'Users', username: 'Username', validate: 'Validate', value: 'Value', @@ -893,6 +897,10 @@ export default { saving: 'Saving...', current: 'current', embed: 'Embed', + addEditLink: 'Add/Edit Link', + removeLink: 'Remove Link', + mediaPicker: 'Media Picker', + viewSourceCode: 'View Source Code', selected: 'selected', other: 'Other', articles: 'Articles', @@ -1800,6 +1808,7 @@ export default { partialViews: 'Partial Views', partialViewMacros: 'Partial View Macro Files', repositories: 'Install from repository', + relations: 'Relations', runway: 'Install Runway', runwayModules: 'Runway modules', scripting: 'Scripting Files', @@ -1851,6 +1860,7 @@ export default { failedPasswordAttempts: 'Failed login attempts', goToProfile: 'Go to user profile', groupsHelp: 'Add groups to assign access and permissions', + invite: 'Invite', inviteAnotherUser: 'Invite another user', inviteUserHelp: 'Invite new users to give them access to Umbraco. An invite email will be sent to the\n user with information on how to log in to Umbraco. Invites last for 72 hours.\n ', @@ -1925,6 +1935,7 @@ export default { userNotInGroup: "The user is not in group '%0%'", userPermissions: 'User permissions', usergroup: 'User group', + usergroups: 'User groups', userInvited: 'has been invited', userInvitedSuccessHelp: 'An invitation has been sent to the new user with details about how to log in to\n Umbraco.\n ', @@ -2327,6 +2338,20 @@ export default { fallbackDescription: "Thank you for choosing Umbraco - we think this could be the beginning of something\n beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast\n as possible.\n ", }, + welcomeDashboard: { + ourUmbracoHeadline: 'Our Umbraco - The Friendliest Community', + ourUmbracoDescription: + "Our Umbraco, the official community site, is your one-stop-shop for everything Umbraco. Whether you need a question answered, cool plugins, or a guide of how to do something in Umbraco, the world's best and friendliest community is just a click away.", + ourUmbracoButton: 'Visit Our Umbraco', + documentationHeadline: 'Documentation', + documentationDescription: 'Find the answers to all your Umbraco questions', + communityHeadline: 'Community', + communityDescription: 'Get support and inspiration from driven Umbraco experts', + resourcesHeadline: 'Resources', + resourcesDescription: 'Free video tutorials to jumpstart your journey with the CMS', + trainingHeadline: 'Training', + trainingDescription: 'Real-life training and official Umbraco certifications', + }, blockEditor: { headlineCreateBlock: 'Pick Element Type', headlineAddSettingsElementType: 'Attach a settings Element Type', diff --git a/src/assets/lang/en.ts b/src/assets/lang/en.ts index af418baa1b..7ac38c0e4e 100644 --- a/src/assets/lang/en.ts +++ b/src/assets/lang/en.ts @@ -177,6 +177,7 @@ export default { save: 'Media saved', }, auditTrails: { + assigndomain: 'Domain assigned: %0%', atViewingFor: 'Viewing for', delete: 'Content deleted', unpublish: 'Content unpublished', @@ -193,6 +194,7 @@ export default { custom: '%0%', contentversionpreventcleanup: 'Cleanup disabled for version: %0%', contentversionenablecleanup: 'Cleanup enabled for version: %0%', + smallAssignDomain: 'Assign Domain', smallCopy: 'Copy', smallPublish: 'Publish', smallPublishVariant: 'Publish', @@ -568,13 +570,16 @@ export default { noIconsFound: 'No icons were found', noMacroParams: 'There are no parameters for this macro', noMacros: 'There are no macros available to insert', - externalLoginProviders: 'External login providers', + externalLoginProviders: 'External logins', exceptionDetail: 'Exception Details', stacktrace: 'Stacktrace', innerException: 'Inner Exception', - linkYour: 'Link your', - unLinkYour: 'Un-link your', - account: 'account', + linkYour: 'Link your {0} account', + linkYourConfirm: + 'You are about to link your Umbraco and {0} accounts and you will be redirected to {0} to confirm.', + unLinkYour: 'Un-link your {0} account', + unLinkYourConfirm: 'You are about to un-link your Umbraco and {0} accounts and you will be logged out.', + linkedToService: 'Your account is linked to this service', selectEditor: 'Select editor', selectSnippet: 'Select snippet', variantdeletewarning: @@ -882,6 +887,7 @@ export default { upload: 'Upload', url: 'URL', user: 'User', + users: 'Users', username: 'Username', value: 'Value', view: 'View', @@ -899,6 +905,10 @@ export default { saving: 'Saving...', current: 'current', embed: 'Embed', + addEditLink: 'Add/Edit Link', + removeLink: 'Remove Link', + mediaPicker: 'Media Picker', + viewSourceCode: 'View Source Code', selected: 'selected', other: 'Other', articles: 'Articles', @@ -952,6 +962,10 @@ export default { font: 'Font', text: 'Text', }, + globalSearch: { + navigateSearchProviders: 'Navigate search providers', + navigateSearchResults: 'Navigate search results', + }, headers: { page: 'Page', }, @@ -1674,6 +1688,7 @@ export default { elementDoesNotSupport: 'This is not applicable for an Element Type', propertyHasChanges: 'You have made changes to this property. Are you sure you want to discard them?', displaySettingsHeadline: 'Appearance', + displaySettingsLabelOnLeft: 'Label to the left', displaySettingsLabelOnTop: 'Label above (full-width)', confirmDeleteTabMessage: 'Are you sure you want to delete the tab %0%?', confirmDeleteGroupMessage: 'Are you sure you want to delete the group %0%?', @@ -1844,6 +1859,7 @@ export default { partialViews: 'Partial Views', partialViewMacros: 'Partial View Macro Files', repositories: 'Install from repository', + relations: 'Relations', runway: 'Install Runway', runwayModules: 'Runway modules', scripting: 'Scripting Files', @@ -1897,6 +1913,7 @@ export default { failedPasswordAttempts: 'Failed login attempts', goToProfile: 'Go to user profile', groupsHelp: 'Add groups to assign access and permissions', + invite: 'Invite', inviteAnotherUser: 'Invite another user', inviteUserHelp: 'Invite new users to give them access to Umbraco. An invite email will be sent to the\n user with information on how to log in to Umbraco. Invites last for 72 hours.\n ', @@ -1971,6 +1988,7 @@ export default { userNotInGroup: "The user is not in group '%0%'", userPermissions: 'User permissions', usergroup: 'User group', + usergroups: 'User groups', userInvited: 'has been invited', userInvitedSuccessHelp: 'An invitation has been sent to the new user with details about how to log in to\n Umbraco.\n ', @@ -2023,6 +2041,7 @@ export default { }, validation: { validation: 'Validation', + validateNothing: 'No validation', validateAsEmail: 'Validate as an email address', validateAsNumber: 'Validate as a number', validateAsUrl: 'Validate as a URL', @@ -2382,6 +2401,20 @@ export default { fallbackDescription: "Thank you for choosing Umbraco - we think this could be the beginning of something\n beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast\n as possible.\n ", }, + welcomeDashboard: { + ourUmbracoHeadline: 'Our Umbraco - The Friendliest Community', + ourUmbracoDescription: + "Our Umbraco, the official community site, is your one-stop-shop for everything Umbraco. Whether you need a question answered, cool plugins, or a guide of how to do something in Umbraco, the world's best and friendliest community is just a click away.", + ourUmbracoButton: 'Visit Our Umbraco', + documentationHeadline: 'Documentation', + documentationDescription: 'Find the answers to all your Umbraco questions', + communityHeadline: 'Community', + communityDescription: 'Get support and inspiration from driven Umbraco experts', + resourcesHeadline: 'Resources', + resourcesDescription: 'Free video tutorials to jumpstart your journey with the CMS', + trainingHeadline: 'Training', + trainingDescription: 'Real-life training and official Umbraco certifications', + }, blockEditor: { headlineCreateBlock: 'Pick Element Type', headlineAddSettingsElementType: 'Attach a settings Element Type', diff --git a/src/assets/login.jpg b/src/assets/login.jpg index 311585a7be..4be79488eb 100644 Binary files a/src/assets/login.jpg and b/src/assets/login.jpg differ diff --git a/src/assets/umbraco_logo_blue.svg b/src/assets/umbraco_logo_blue.svg index 578bf592f6..171f598b1f 100644 --- a/src/assets/umbraco_logo_blue.svg +++ b/src/assets/umbraco_logo_blue.svg @@ -1,51 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/assets/umbraco_logo_white.svg b/src/assets/umbraco_logo_white.svg index 01f7260cd3..46f77cbfc3 100644 --- a/src/assets/umbraco_logo_white.svg +++ b/src/assets/umbraco_logo_white.svg @@ -1,51 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/css/user-defined.css b/src/css/user-defined.css new file mode 100644 index 0000000000..35c48a145b --- /dev/null +++ b/src/css/user-defined.css @@ -0,0 +1 @@ +/* This file can be overridden by placing a file with the same name in the /wwwroot/umbraco/backoffice/css folder of the website */ diff --git a/src/external/backend-api/src/models.ts b/src/external/backend-api/src/models.ts index da4e84c5d8..89eb0afc11 100644 --- a/src/external/backend-api/src/models.ts +++ b/src/external/backend-api/src/models.ts @@ -403,7 +403,7 @@ export type CreateUserRequestModel = { email: string userName: string name: string -userGroupIds: Array +userGroupIds: Array id?: string | null }; @@ -438,9 +438,9 @@ email: string userName: string name: string languageIsoCode?: string | null -documentStartNodeIds: Array +documentStartNodeIds: Array hasDocumentRootAccess: boolean -mediaStartNodeIds: Array +mediaStartNodeIds: Array hasMediaRootAccess: boolean avatarUrls: Array languages: Array @@ -458,6 +458,13 @@ export enum DataTypeChangeModeModel { FALSE_WITH_HELP_TEXT = 'FalseWithHelpText' } +export type DataTypeContentTypeReferenceModel = { + id: string +type: string | null +name: string | null +icon: string | null + }; + export type DataTypeItemResponseModel = { id: string name: string @@ -476,8 +483,7 @@ alias: string }; export type DataTypeReferenceResponseModel = { - id: string -type: string + contentType: DataTypeContentTypeReferenceModel properties: Array }; @@ -541,11 +547,11 @@ icon?: string | null }; export type DeleteUserGroupsRequestModel = { - userGroupIds: Array + userGroupIds: Array }; export type DeleteUsersRequestModel = { - userIds: Array + userIds: Array }; export type DictionaryItemItemResponseModel = { @@ -577,7 +583,7 @@ export enum DirectionModel { } export type DisableUserRequestModel = { - userIds: Array + userIds: Array }; export type DocumentBlueprintItemResponseModel = { @@ -898,7 +904,13 @@ secret: string }; export type EnableUserRequestModel = { - userIds: Array + userIds: Array + }; + +export type EntityImportAnalysisResponseModel = { + entityType: string +alias?: string | null +key?: string | null }; export enum EventMessageTypeModel { @@ -991,11 +1003,28 @@ url?: string | null type?: string | null }; +export enum ImageCropModeModel { + CROP = 'Crop', + MAX = 'Max', + STRETCH = 'Stretch', + PAD = 'Pad', + BOX_PAD = 'BoxPad', + MIN = 'Min' +} + export type ImportDictionaryRequestModel = { temporaryFile: ReferenceByIdModel parent?: ReferenceByIdModel | null }; +export type ImportDocumentTypeRequestModel = { + file: ReferenceByIdModel + }; + +export type ImportMediaTypeRequestModel = { + file: ReferenceByIdModel + }; + export type IndexResponseModel = { name: string healthStatus: HealthStatusResponseModel @@ -1021,7 +1050,7 @@ export type InviteUserRequestModel = { email: string userName: string name: string -userGroupIds: Array +userGroupIds: Array id?: string | null message?: string | null }; @@ -1048,15 +1077,6 @@ fallbackIsoCode?: string | null isoCode: string }; -export type LinkedLoginModel = { - providerName: string -providerKey: string - }; - -export type LinkedLoginsRequestModel = { - linkedLogins: Array - }; - export type LogLevelCountsReponseModel = { information: number debug: number @@ -1231,6 +1251,8 @@ containers: Array id: string allowedMediaTypes: Array compositions: Array +isDeletable: boolean +aliasCanBeChanged: boolean }; export type MediaTypeSortModel = { @@ -1245,6 +1267,7 @@ parent?: ReferenceByIdModel | null name: string isFolder: boolean icon: string +isDeletable: boolean }; export type MediaUrlInfoModel = { @@ -2268,7 +2291,7 @@ context: string }; export type UnlockUsersRequestModel = { - userIds: Array + userIds: Array }; export type UnpublishDocumentRequestModel = { @@ -2517,19 +2540,19 @@ permissions: Array -userGroupIds: Array + userIds: Array +userGroupIds: Array }; export type UpdateUserRequestModel = { email: string userName: string name: string -userGroupIds: Array +userGroupIds: Array languageIsoCode: string -documentStartNodeIds: Array +documentStartNodeIds: Array hasDocumentRootAccess: boolean -mediaStartNodeIds: Array +mediaStartNodeIds: Array hasMediaRootAccess: boolean }; @@ -2574,6 +2597,13 @@ value: string key: string }; +export type UserExternalLoginProviderModel = { + providerSchemeName: string +providerKey?: string | null +isLinkedOnUser: boolean +hasManualLinkingEnabled: boolean + }; + export type UserGroupItemResponseModel = { id: string name: string @@ -2638,12 +2668,12 @@ export type UserResponseModel = { email: string userName: string name: string -userGroupIds: Array +userGroupIds: Array id: string languageIsoCode?: string | null -documentStartNodeIds: Array +documentStartNodeIds: Array hasDocumentRootAccess: boolean -mediaStartNodeIds: Array +mediaStartNodeIds: Array hasMediaRootAccess: boolean avatarUrls: Array state: UserStateModel @@ -3074,6 +3104,15 @@ PostDocumentTypeByIdCopy: { id: string requestBody?: CopyDocumentTypeRequestModel + }; +GetDocumentTypeByIdExport: { + id: string + + }; +PutDocumentTypeByIdImport: { + id: string +requestBody?: ImportDocumentTypeRequestModel + }; PutDocumentTypeByIdMove: { id: string @@ -3105,6 +3144,10 @@ PutDocumentTypeFolderById: { id: string requestBody?: UpdateFolderResponseModel + }; +PostDocumentTypeImport: { + requestBody?: ImportDocumentTypeRequestModel + }; GetItemDocumentType: { id?: Array @@ -3145,6 +3188,8 @@ take?: number ,GetDocumentTypeByIdBlueprint: PagedDocumentTypeBlueprintItemResponseModel ,GetDocumentTypeByIdCompositionReferences: Array ,PostDocumentTypeByIdCopy: string + ,GetDocumentTypeByIdExport: Blob | File + ,PutDocumentTypeByIdImport: string ,PutDocumentTypeByIdMove: string ,GetDocumentTypeAllowedAtRoot: PagedAllowedDocumentTypeModel ,PostDocumentTypeAvailableCompositions: Array @@ -3153,6 +3198,7 @@ take?: number ,GetDocumentTypeFolderById: FolderResponseModel ,DeleteDocumentTypeFolderById: string ,PutDocumentTypeFolderById: string + ,PostDocumentTypeImport: string ,GetItemDocumentType: Array ,GetItemDocumentTypeSearch: PagedModelDocumentTypeItemResponseModel ,GetTreeDocumentTypeAncestors: Array @@ -3510,6 +3556,43 @@ tree?: string } +export type ImagingData = { + + payloads: { + GetImagingResizeUrls: { + height?: number +id?: Array +mode?: ImageCropModeModel +width?: number + + }; + } + + + responses: { + GetImagingResizeUrls: Array + + } + + } + +export type ImportData = { + + payloads: { + GetImportAnalyze: { + temporaryFileId?: string + + }; + } + + + responses: { + GetImportAnalyze: EntityImportAnalysisResponseModel + + } + + } + export type IndexerData = { payloads: { @@ -3700,6 +3783,11 @@ GetItemMediaTypeAllowed: { skip?: number take?: number + }; +GetItemMediaTypeFolders: { + skip?: number +take?: number + }; GetItemMediaTypeSearch: { query?: string @@ -3738,6 +3826,15 @@ PostMediaTypeByIdCopy: { id: string requestBody?: CopyMediaTypeRequestModel + }; +GetMediaTypeByIdExport: { + id: string + + }; +PutMediaTypeByIdImport: { + id: string +requestBody?: ImportMediaTypeRequestModel + }; PutMediaTypeByIdMove: { id: string @@ -3769,6 +3866,10 @@ PutMediaTypeFolderById: { id: string requestBody?: UpdateFolderResponseModel + }; +PostMediaTypeImport: { + requestBody?: ImportMediaTypeRequestModel + }; GetTreeMediaTypeAncestors: { descendantId?: string @@ -3793,6 +3894,7 @@ take?: number responses: { GetItemMediaType: Array ,GetItemMediaTypeAllowed: PagedModelMediaTypeItemResponseModel + ,GetItemMediaTypeFolders: PagedModelMediaTypeItemResponseModel ,GetItemMediaTypeSearch: PagedModelMediaTypeItemResponseModel ,PostMediaType: string ,GetMediaTypeById: MediaTypeResponseModel @@ -3801,6 +3903,8 @@ take?: number ,GetMediaTypeByIdAllowedChildren: PagedAllowedMediaTypeModel ,GetMediaTypeByIdCompositionReferences: Array ,PostMediaTypeByIdCopy: string + ,GetMediaTypeByIdExport: Blob | File + ,PutMediaTypeByIdImport: string ,PutMediaTypeByIdMove: string ,GetMediaTypeAllowedAtRoot: PagedAllowedMediaTypeModel ,PostMediaTypeAvailableCompositions: Array @@ -3808,6 +3912,7 @@ take?: number ,GetMediaTypeFolderById: FolderResponseModel ,DeleteMediaTypeFolderById: string ,PutMediaTypeFolderById: string + ,PostMediaTypeImport: string ,GetTreeMediaTypeAncestors: Array ,GetTreeMediaTypeChildren: PagedMediaTypeTreeItemResponseModel ,GetTreeMediaTypeRoot: PagedMediaTypeTreeItemResponseModel @@ -5015,12 +5120,12 @@ requestBody?: UpdateUserGroupRequestModel }; DeleteUserGroupByIdUsers: { id: string -requestBody?: Array +requestBody?: Array }; PostUserGroupByIdUsers: { id: string -requestBody?: Array +requestBody?: Array }; } @@ -5205,7 +5310,7 @@ PostUserUnlock: { ,PostUserCurrentAvatar: string ,PostUserCurrentChangePassword: string ,GetUserCurrentConfiguration: CurrenUserConfigurationResponseModel - ,GetUserCurrentLogins: LinkedLoginsRequestModel + ,GetUserCurrentLoginProviders: Array ,GetUserCurrentPermissions: UserPermissionsResponseModel ,GetUserCurrentPermissionsDocument: Array ,GetUserCurrentPermissionsMedia: UserPermissionsResponseModel diff --git a/src/external/backend-api/src/services.ts b/src/external/backend-api/src/services.ts index 84d88c42c7..2b3fe82994 100644 --- a/src/external/backend-api/src/services.ts +++ b/src/external/backend-api/src/services.ts @@ -1,7 +1,7 @@ import type { CancelablePromise } from './core/CancelablePromise'; import { OpenAPI } from './core/OpenAPI'; import { request as __request } from './core/request'; -import type { CultureData, DataTypeData, DictionaryData, DocumentBlueprintData, DocumentTypeData, DocumentVersionData, DocumentData, DynamicRootData, HealthCheckData, HelpData, IndexerData, InstallData, LanguageData, LogViewerData, ManifestData, MediaTypeData, MediaData, MemberGroupData, MemberTypeData, MemberData, ModelsBuilderData, ObjectTypesData, OembedData, PackageData, PartialViewData, PreviewData, ProfilingData, PropertyTypeData, PublishedCacheData, RedirectManagementData, RelationTypeData, RelationData, ScriptData, SearcherData, SecurityData, SegmentData, ServerData, StaticFileData, StylesheetData, TagData, TelemetryData, TemplateData, TemporaryFileData, UpgradeData, UserDataData, UserGroupData, UserData, WebhookData } from './models'; +import type { CultureData, DataTypeData, DictionaryData, DocumentBlueprintData, DocumentTypeData, DocumentVersionData, DocumentData, DynamicRootData, HealthCheckData, HelpData, ImagingData, ImportData, IndexerData, InstallData, LanguageData, LogViewerData, ManifestData, MediaTypeData, MediaData, MemberGroupData, MemberTypeData, MemberData, ModelsBuilderData, ObjectTypesData, OembedData, PackageData, PartialViewData, PreviewData, ProfilingData, PropertyTypeData, PublishedCacheData, RedirectManagementData, RelationTypeData, RelationData, ScriptData, SearcherData, SecurityData, SegmentData, ServerData, StaticFileData, StylesheetData, TagData, TelemetryData, TemplateData, TemporaryFileData, UpgradeData, UserDataData, UserGroupData, UserData, WebhookData } from './models'; export class CultureService { @@ -1347,6 +1347,57 @@ requestBody }); } + /** + * @returns unknown Success + * @throws ApiError + */ + public static getDocumentTypeByIdExport(data: DocumentTypeData['payloads']['GetDocumentTypeByIdExport']): CancelablePromise { + const { + + id + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/document-type/{id}/export', + path: { + id + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + 404: `Not Found`, + }, + }); + } + + /** + * @returns string Success + * @throws ApiError + */ + public static putDocumentTypeByIdImport(data: DocumentTypeData['payloads']['PutDocumentTypeByIdImport']): CancelablePromise { + const { + + id, +requestBody + } = data; + return __request(OpenAPI, { + method: 'PUT', + url: '/umbraco/management/api/v1/document-type/{id}/import', + path: { + id + }, + body: requestBody, + mediaType: 'application/json', + responseHeader: 'Umb-Notifications', + errors: { + 400: `Bad Request`, + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + 404: `Not Found`, + }, + }); + } + /** * @returns string Success * @throws ApiError @@ -1535,6 +1586,30 @@ requestBody }); } + /** + * @returns string Created + * @throws ApiError + */ + public static postDocumentTypeImport(data: DocumentTypeData['payloads']['PostDocumentTypeImport'] = {}): CancelablePromise { + const { + + requestBody + } = data; + return __request(OpenAPI, { + method: 'POST', + url: '/umbraco/management/api/v1/document-type/import', + body: requestBody, + mediaType: 'application/json', + responseHeader: 'Umb-Generated-Resource', + errors: { + 400: `Bad Request`, + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + 404: `Not Found`, + }, + }); + } + /** * @returns unknown Success * @throws ApiError @@ -2917,6 +2992,62 @@ baseUrl } +export class ImagingService { + + /** + * @returns unknown Success + * @throws ApiError + */ + public static getImagingResizeUrls(data: ImagingData['payloads']['GetImagingResizeUrls'] = {}): CancelablePromise { + const { + + id, +height, +width, +mode + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/imaging/resize/urls', + query: { + id, height, width, mode + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + }, + }); + } + +} + +export class ImportService { + + /** + * @returns unknown Success + * @throws ApiError + */ + public static getImportAnalyze(data: ImportData['payloads']['GetImportAnalyze'] = {}): CancelablePromise { + const { + + temporaryFileId + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/import/analyze', + query: { + temporaryFileId + }, + errors: { + 400: `Bad Request`, + 401: `The resource is protected and requires an authentication token`, + 404: `Not Found`, + }, + }); + } + +} + export class IndexerService { /** @@ -3532,6 +3663,29 @@ take }); } + /** + * @returns unknown Success + * @throws ApiError + */ + public static getItemMediaTypeFolders(data: MediaTypeData['payloads']['GetItemMediaTypeFolders'] = {}): CancelablePromise { + const { + + skip, +take + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/item/media-type/folders', + query: { + skip, take + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + }, + }); + } + /** * @returns unknown Success * @throws ApiError @@ -3735,6 +3889,57 @@ requestBody }); } + /** + * @returns unknown Success + * @throws ApiError + */ + public static getMediaTypeByIdExport(data: MediaTypeData['payloads']['GetMediaTypeByIdExport']): CancelablePromise { + const { + + id + } = data; + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/media-type/{id}/export', + path: { + id + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + 404: `Not Found`, + }, + }); + } + + /** + * @returns string Success + * @throws ApiError + */ + public static putMediaTypeByIdImport(data: MediaTypeData['payloads']['PutMediaTypeByIdImport']): CancelablePromise { + const { + + id, +requestBody + } = data; + return __request(OpenAPI, { + method: 'PUT', + url: '/umbraco/management/api/v1/media-type/{id}/import', + path: { + id + }, + body: requestBody, + mediaType: 'application/json', + responseHeader: 'Umb-Notifications', + errors: { + 400: `Bad Request`, + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + 404: `Not Found`, + }, + }); + } + /** * @returns string Success * @throws ApiError @@ -3907,6 +4112,30 @@ requestBody }); } + /** + * @returns string Created + * @throws ApiError + */ + public static postMediaTypeImport(data: MediaTypeData['payloads']['PostMediaTypeImport'] = {}): CancelablePromise { + const { + + requestBody + } = data; + return __request(OpenAPI, { + method: 'POST', + url: '/umbraco/management/api/v1/media-type/import', + body: requestBody, + mediaType: 'application/json', + responseHeader: 'Umb-Generated-Resource', + errors: { + 400: `Bad Request`, + 401: `The resource is protected and requires an authentication token`, + 403: `The authenticated user do not have access to this resource`, + 404: `Not Found`, + }, + }); + } + /** * @returns unknown Success * @throws ApiError @@ -5981,9 +6210,6 @@ export class PreviewService { method: 'DELETE', url: '/umbraco/management/api/v1/preview', responseHeader: 'Umb-Notifications', - errors: { - 401: `The resource is protected and requires an authentication token`, - }, }); } @@ -8661,11 +8887,11 @@ requestBody * @returns unknown Success * @throws ApiError */ - public static getUserCurrentLogins(): CancelablePromise { + public static getUserCurrentLoginProviders(): CancelablePromise { return __request(OpenAPI, { method: 'GET', - url: '/umbraco/management/api/v1/user/current/logins', + url: '/umbraco/management/api/v1/user/current/login-providers', errors: { 401: `The resource is protected and requires an authentication token`, }, diff --git a/src/external/diff/index.ts b/src/external/diff/index.ts index 498d9d0e23..3870b67b72 100644 --- a/src/external/diff/index.ts +++ b/src/external/diff/index.ts @@ -1 +1 @@ -export { diffWords } from 'diff'; +export { diffWords, type Change } from 'diff'; diff --git a/src/external/dompurify/index.ts b/src/external/dompurify/index.ts index 2a816a45c3..ce91fb5916 100644 --- a/src/external/dompurify/index.ts +++ b/src/external/dompurify/index.ts @@ -1,4 +1,3 @@ -/* eslint local-rules/enforce-umbraco-external-imports: 0 */ import DOMPurify from 'dompurify'; export { DOMPurify }; diff --git a/src/external/router-slot/model.ts b/src/external/router-slot/model.ts index f32ea71598..6872b18149 100644 --- a/src/external/router-slot/model.ts +++ b/src/external/router-slot/model.ts @@ -25,7 +25,7 @@ export type Guard = (info: IRoutingInfo) => boolean | Pr export type Cancel = () => boolean; export type PageComponent = HTMLElement | undefined; -export type ModuleResolver = Promise<{ default: any /*PageComponent*/ }>; +export type ModuleResolver = Promise<{ default?: any /*PageComponent*/; element?: any /*PageComponent*/ }>; export type Class = { new (...args: any[]): T }; export type Component = | Class diff --git a/src/external/router-slot/util/router.ts b/src/external/router-slot/util/router.ts index 0b3303ab05..5b084e4f71 100644 --- a/src/external/router-slot/util/router.ts +++ b/src/external/router-slot/util/router.ts @@ -74,7 +74,7 @@ export function matchRoute(route: IRoute, path: string | PathFragmen default: return new RegExp(`^[\/]?${routePath}(?:\/|$)`); } - })(); + })(); // Check if there's a match const match = path.match(regex); @@ -147,8 +147,13 @@ export async function resolvePageComponent(route: IComponentRoute, info: IRoutin // Instantiate the component let component!: PageComponent; + if (!(moduleClassOrPage instanceof HTMLElement)) { - component = new (moduleClassOrPage.default ? moduleClassOrPage.default : moduleClassOrPage)() as PageComponent; + component = new (moduleClassOrPage.default + ? moduleClassOrPage.default + : moduleClassOrPage.element + ? moduleClassOrPage.element + : moduleClassOrPage)() as PageComponent; } else { component = moduleClassOrPage as PageComponent; } diff --git a/src/libs/element-api/element.test.ts b/src/libs/element-api/element.test.ts index 20618e4dec..c9a29170d9 100644 --- a/src/libs/element-api/element.test.ts +++ b/src/libs/element-api/element.test.ts @@ -290,7 +290,7 @@ describe('UmbElementMixin', () => { const check2: CheckType = value as string; const check3: CheckType = value as null; const check4: CheckType = value as undefined; - expect(check).to.be.equal('hello'); + expect(check).to.be.equal(null); expect(check2 === check3 && check2 === check4).to.be.true; // Just to use the const for something. }); // Because the source is potentially undefined, the controller could be undefined and the value of the callback method could be undefined [NL] diff --git a/src/libs/extension-api/functions/create-extension-element.function.ts b/src/libs/extension-api/functions/create-extension-element.function.ts index 754b8ea7aa..fe503df335 100644 --- a/src/libs/extension-api/functions/create-extension-element.function.ts +++ b/src/libs/extension-api/functions/create-extension-element.function.ts @@ -11,7 +11,7 @@ export async function createExtensionElement( const elementConstructor = await loadManifestElement(elementPropValue); if (elementConstructor) { return new elementConstructor(); - } else { + } else if (!manifest.elementName) { console.error( `-- Extension of alias "${manifest.alias}" did not succeed creating an element class instance via the extension manifest property '${elementPropValue}'. The imported Element JS file did not export a 'element' or 'default'. Alternatively define the 'elementName' in the manifest.`, manifest, diff --git a/src/libs/formatting-api/formatting.controller.ts b/src/libs/formatting-api/formatting.controller.ts new file mode 100644 index 0000000000..1465cd3bbd --- /dev/null +++ b/src/libs/formatting-api/formatting.controller.ts @@ -0,0 +1,33 @@ +import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify'; +import { Marked } from '@umbraco-cms/backoffice/external/marked'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; + +const UmbMarked = new Marked({ gfm: true, breaks: true }); +const UmbDomPurify = DOMPurify(window); +const UmbDomPurifyConfig: DOMPurify.Config = { USE_PROFILES: { html: true } }; + +UmbDomPurify.addHook('afterSanitizeAttributes', function (node) { + // set all elements owning target to target=_blank + if ('target' in node) { + node.setAttribute('target', '_blank'); + } +}); + +/** + * @description - Controller for formatting text. + */ +export class UmbFormattingController extends UmbControllerBase { + #localize = new UmbLocalizationController(this._host); + + /** + * A method to localize the string input then transform any markdown to santized HTML. + */ + public transform(input?: string): string { + if (!input) return ''; + const translated = this.#localize.string(input); + const markup = UmbMarked.parse(translated) as string; + const sanitized = UmbDomPurify.sanitize(markup, UmbDomPurifyConfig) as string; + return sanitized; + } +} diff --git a/src/libs/formatting-api/index.ts b/src/libs/formatting-api/index.ts new file mode 100644 index 0000000000..a11870de1f --- /dev/null +++ b/src/libs/formatting-api/index.ts @@ -0,0 +1,2 @@ +export * from './formatting.controller.js'; +export * from './localizeAndTransform.function.js'; diff --git a/src/libs/formatting-api/localizeAndTransform.function.ts b/src/libs/formatting-api/localizeAndTransform.function.ts new file mode 100644 index 0000000000..21ff4e07cd --- /dev/null +++ b/src/libs/formatting-api/localizeAndTransform.function.ts @@ -0,0 +1,6 @@ +import { UmbFormattingController } from './formatting.controller.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export function localizeAndTransform(host: UmbControllerHost, input: string): string { + return new UmbFormattingController(host).transform(input); +} diff --git a/src/libs/observable-api/states/array-state.ts b/src/libs/observable-api/states/array-state.ts index a96ff31ce5..9741bbcebb 100644 --- a/src/libs/observable-api/states/array-state.ts +++ b/src/libs/observable-api/states/array-state.ts @@ -35,6 +35,7 @@ export class UmbArrayState extends UmbDeepState { */ sortBy(sortMethod?: (a: T, b: T) => number) { this.#sortMethod = sortMethod; + super.setValue(this.getValue().sort(this.#sortMethod)); return this; } diff --git a/src/libs/observable-api/states/basic-state.ts b/src/libs/observable-api/states/basic-state.ts index 57aaaa84b6..4c6394849f 100644 --- a/src/libs/observable-api/states/basic-state.ts +++ b/src/libs/observable-api/states/basic-state.ts @@ -33,7 +33,7 @@ export class UmbBasicState { * console.log("Value is: ", myState.value); */ public get value(): BehaviorSubject['value'] { - return this._subject.value; + return this.getValue(); } /** diff --git a/src/libs/observable-api/states/deep-state.ts b/src/libs/observable-api/states/deep-state.ts index a9a8b7f33a..f443fe02e1 100644 --- a/src/libs/observable-api/states/deep-state.ts +++ b/src/libs/observable-api/states/deep-state.ts @@ -13,15 +13,19 @@ import { UmbBasicState } from './basic-state.js'; * Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content. */ export class UmbDeepState extends UmbBasicState { + #mute?: boolean; + #value: T; + constructor(initialData: T) { super(deepFreeze(initialData)); + this.#value = this._subject.getValue(); } /** - * @export * @method createObservablePart * @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return. * @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different. + * @returns {Observable} * @description - Creates an Observable from this State. */ asObservablePart( @@ -39,9 +43,63 @@ export class UmbDeepState extends UmbBasicState { setValue(data: T): void { if (!this._subject) return; const frozenData = deepFreeze(data); - // Only update data if its different than current data. - if (!jsonStringComparison(frozenData, this._subject.getValue())) { + this.#value = frozenData; + // Only update data if its not muted and is different than current data. [NL] + if (!this.#mute && !jsonStringComparison(frozenData, this._subject.getValue())) { this._subject.next(frozenData); } } + + getValue(): T { + return this.#value; + } + + /** + * @method mute + * @description - Set mute for this state. + */ + mute() { + if (this.#mute) return; + this.#mute = true; + } + + /** + * @method unmute + * @description - Unset the mute of this state. + */ + unmute() { + if (!this.#mute) return; + this.#mute = false; + // Only update data if it is different than current data. [NL] + if (!jsonStringComparison(this.#value, this._subject.getValue())) { + this._subject?.next(this.#value); + } + } + + /** + * @method isMuted + * @description - Check if the state is muted. + * @returns {boolean} - Returns true if the state is muted. + */ + isMuted() { + return this.#mute; + } + + /** + * @method getMutePromise + * @description - Get a promise which resolves when the mute is unset. + * @returns {Promise} + */ + getMutePromise() { + return new Promise((resolve) => { + if (!this.#mute) { + resolve(); + return; + } + const subscription = this._subject.subscribe(() => { + subscription.unsubscribe(); + resolve(); + }); + }); + } } diff --git a/src/libs/observable-api/states/object-state.test.ts b/src/libs/observable-api/states/object-state.test.ts index 76dc7705dd..010716e036 100644 --- a/src/libs/observable-api/states/object-state.test.ts +++ b/src/libs/observable-api/states/object-state.test.ts @@ -41,4 +41,50 @@ describe('UmbObjectState', () => { subject.update({ key: 'change_this_first_should_not_trigger_update' }); subject.update({ another: 'myNewValue' }); }); + + it('replays the latests value when unmuted.', (done) => { + let amountOfCallbacks = 0; + + const observer = subject.asObservable(); + observer.subscribe((value) => { + amountOfCallbacks++; + if (amountOfCallbacks === 1) { + // First callback gives us the initialized value. + expect(value.key).to.be.equal('some'); + } + if (amountOfCallbacks === 2) { + // Second callback gives us the first change. + expect(value.key).to.be.equal('firstChange'); + } + if (amountOfCallbacks === 3) { + // Third callback gives us the last change before unmuted. + expect(value.key).to.be.equal('thirdChange'); + done(); + } + }); + + subject.update({ key: 'firstChange' }); + subject.mute(); + subject.update({ key: 'secondChange' }); + subject.update({ key: 'thirdChange' }); + subject.unmute(); + }); + + /* + it('replays latests unmuted value when muted.', (done) => { + const observer = subject.asObservable(); + observer.subscribe((value) => { + expect(value).to.be.equal(initialData); + }); + + subject.mute(); + subject.update({ key: 'firstChange' }); + + observer.subscribe((value) => { + expect(value).to.be.equal(initialData); + done(); + }); + + }); + */ }); diff --git a/src/libs/observable-api/states/object-state.ts b/src/libs/observable-api/states/object-state.ts index 815bee0865..b325d50b8a 100644 --- a/src/libs/observable-api/states/object-state.ts +++ b/src/libs/observable-api/states/object-state.ts @@ -21,7 +21,7 @@ export class UmbObjectState extends UmbDeepState { * myState.update({value: 'myNewValue'}); */ update(partialData: Partial) { - this.setValue({ ...this._subject.getValue(), ...partialData }); + this.setValue({ ...this.getValue(), ...partialData }); return this; } } diff --git a/src/libs/observable-api/utils/index.ts b/src/libs/observable-api/utils/index.ts index f207030636..ff6dfc7e0d 100644 --- a/src/libs/observable-api/utils/index.ts +++ b/src/libs/observable-api/utils/index.ts @@ -8,6 +8,7 @@ export * from './json-string-comparison.function.js'; export * from './merge-observables.function.js'; export * from './observe-multiple.function.js'; export * from './partial-update-frozen-array.function.js'; +export * from './push-at-to-unique-array.function.js'; export * from './push-to-unique-array.function.js'; export * from './simple-hash-code.function.js'; export * from './strict-equality-memoization.function.js'; diff --git a/src/mocks/data/data-type/data-type.data.ts b/src/mocks/data/data-type/data-type.data.ts index 509838185f..0eaa8a1855 100644 --- a/src/mocks/data/data-type/data-type.data.ts +++ b/src/mocks/data/data-type/data-type.data.ts @@ -831,7 +831,6 @@ export const data: Array = [ { alias: 'icon', value: 'icon-layers' }, { alias: 'tabName', value: 'Children' }, { alias: 'showContentFirst', value: true }, - { alias: 'useInfiniteEditor', value: true }, ], }, { @@ -876,7 +875,6 @@ export const data: Array = [ { alias: 'icon', value: 'icon-layers' }, { alias: 'tabName', value: 'Items' }, { alias: 'showContentFirst', value: false }, - { alias: 'useInfiniteEditor', value: true }, ], }, { @@ -988,7 +986,7 @@ export const data: Array = [ id: 'dt-integer', parent: null, editorAlias: 'Umbraco.Integer', - editorUiAlias: 'Umb.PropertyEditorUi.Number', + editorUiAlias: 'Umb.PropertyEditorUi.Integer', hasChildren: false, isFolder: false, isDeletable: true, diff --git a/src/mocks/data/media-type/media-type.data.ts b/src/mocks/data/media-type/media-type.data.ts index aeba10c756..9378249bf4 100644 --- a/src/mocks/data/media-type/media-type.data.ts +++ b/src/mocks/data/media-type/media-type.data.ts @@ -96,5 +96,7 @@ export const data: Array = [ isFolder: false, hasChildren: false, collection: { id: 'dt-collectionView' }, + isDeletable: false, + aliasCanBeChanged: false, }, ]; diff --git a/src/mocks/data/media-type/media-type.db.ts b/src/mocks/data/media-type/media-type.db.ts index 874a4e4374..de5e5765eb 100644 --- a/src/mocks/data/media-type/media-type.db.ts +++ b/src/mocks/data/media-type/media-type.db.ts @@ -66,6 +66,8 @@ const createMockMediaTypeFolderMapper = (request: CreateFolderRequestModel): Umb isFolder: true, hasChildren: false, collection: null, + isDeletable: false, + aliasCanBeChanged: false, }; }; @@ -88,6 +90,8 @@ const createMockMediaTypeMapper = (request: CreateMediaTypeRequestModel): UmbMoc isFolder: false, hasChildren: false, collection: null, + isDeletable: false, + aliasCanBeChanged: false, }; }; @@ -107,6 +111,8 @@ const mediaTypeDetailMapper = (item: UmbMockMediaTypeModel): MediaTypeResponseMo allowedMediaTypes: item.allowedMediaTypes, compositions: item.compositions, collection: item.collection, + isDeletable: item.isDeletable, + aliasCanBeChanged: item.aliasCanBeChanged, }; }; @@ -118,6 +124,7 @@ const mediaTypeTreeItemMapper = (item: UmbMockMediaTypeModel): MediaTypeTreeItem parent: item.parent, isFolder: item.isFolder, icon: item.icon, + isDeletable: item.isDeletable, }; }; diff --git a/src/mocks/data/user-group/user-group.db.ts b/src/mocks/data/user-group/user-group.db.ts index c6c9fc9bf5..60d8f4fbf2 100644 --- a/src/mocks/data/user-group/user-group.db.ts +++ b/src/mocks/data/user-group/user-group.db.ts @@ -1,3 +1,4 @@ +import { queryFilter } from '../utils.js'; import { UmbEntityMockDbBase } from '../utils/entity/entity-base.js'; import { UmbMockEntityDetailManager } from '../utils/entity/entity-detail.manager.js'; import { UmbMockEntityItemManager } from '../utils/entity/entity-item.manager.js'; @@ -6,12 +7,22 @@ import { data } from './user-group.data.js'; import type { CreateUserGroupRequestModel, DocumentPermissionPresentationModel, + PagedUserGroupResponseModel, UnknownTypePermissionPresentationModel, UserGroupItemResponseModel, UserGroupResponseModel, } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbId } from '@umbraco-cms/backoffice/id'; +interface UserGroupFilterOptions { + skip: number; + take: number; + filter: string; +} + +const userGroupQueryFilter = (filterOptions: UserGroupFilterOptions, item: UmbMockUserGroupModel) => + queryFilter(filterOptions.filter, item.name); + export class UmbUserGroupMockDB extends UmbEntityMockDbBase { item = new UmbMockEntityItemManager(this, itemMapper); detail = new UmbMockEntityDetailManager(this, createMockMapper, detailResponseMapper); @@ -27,10 +38,10 @@ export class UmbUserGroupMockDB extends UmbEntityMockDbBase, ): Array { const permissions = this.data - .filter((userGroup) => userGroupIds.includes(userGroup.id)) + .filter((userGroup) => userGroupIds.map((reference) => reference.id).includes(userGroup.id)) .map((userGroup) => (userGroup.permissions?.length ? userGroup.permissions : [])) .flat(); @@ -39,15 +50,32 @@ export class UmbUserGroupMockDB extends UmbEntityMockDbBase): string[] { const sections = this.data - .filter((userGroup) => userGroupIds.includes(userGroup.id)) + .filter((userGroup) => userGroupIds.map((reference) => reference.id).includes(userGroup.id)) .map((userGroup) => (userGroup.sections?.length ? userGroup.sections : [])) .flat(); // Remove duplicates return Array.from(new Set(sections)); } + + filter(options: UserGroupFilterOptions): PagedUserGroupResponseModel { + const allItems = this.getAll(); + + const filterOptions: UserGroupFilterOptions = { + skip: options.skip || 0, + take: options.take || 25, + filter: options.filter, + }; + + const filteredItems = allItems.filter((item) => userGroupQueryFilter(filterOptions, item)); + const totalItems = filteredItems.length; + + const paginatedItems = filteredItems.slice(filterOptions.skip, filterOptions.skip + filterOptions.take); + + return { total: totalItems, items: paginatedItems }; + } } const itemMapper = (item: UmbMockUserGroupModel): UserGroupItemResponseModel => { diff --git a/src/mocks/data/user/user.data.ts b/src/mocks/data/user/user.data.ts index 27b5ce7669..a1657a2531 100644 --- a/src/mocks/data/user/user.data.ts +++ b/src/mocks/data/user/user.data.ts @@ -24,16 +24,16 @@ export const data: Array = [ updateDate: '2/10/2022', createDate: '3/13/2022', failedLoginAttempts: 946, - userGroupIds: ['user-group-administrators-id', 'user-group-editors-id'], + userGroupIds: [{ id: 'user-group-administrators-id' }, { id: 'user-group-editors-id' }], userName: '', avatarUrls: [], isAdmin: true, }, { id: '82e11d3d-b91d-43c9-9071-34d28e62e81d', - documentStartNodeIds: ['simple-document-id'], + documentStartNodeIds: [{ id: 'simple-document-id' }], hasDocumentRootAccess: true, - mediaStartNodeIds: ['f2f81a40-c989-4b6b-84e2-057cecd3adc1'], + mediaStartNodeIds: [{ id: 'f2f81a40-c989-4b6b-84e2-057cecd3adc1' }], hasMediaRootAccess: true, name: 'Amelie Walker', email: 'awalker1@domain.com', @@ -45,7 +45,7 @@ export const data: Array = [ updateDate: '2023-10-12T18:30:32.879Z', createDate: '2023-10-12T18:30:32.879Z', failedLoginAttempts: 0, - userGroupIds: ['user-group-administrators-id'], + userGroupIds: [{ id: 'user-group-administrators-id' }], userName: '', avatarUrls: [], isAdmin: true, @@ -66,7 +66,7 @@ export const data: Array = [ updateDate: '2023-10-12T18:30:32.879Z', createDate: '2023-10-12T18:30:32.879Z', failedLoginAttempts: 0, - userGroupIds: ['user-group-editors-id'], + userGroupIds: [{ id: 'user-group-editors-id' }], userName: '', avatarUrls: [], isAdmin: false, @@ -87,7 +87,7 @@ export const data: Array = [ updateDate: '2023-10-12T18:30:32.879Z', createDate: '2023-10-12T18:30:32.879Z', failedLoginAttempts: 0, - userGroupIds: ['user-group-editors-id'], + userGroupIds: [{ id: 'user-group-editors-id' }], userName: '', avatarUrls: [], isAdmin: false, @@ -108,7 +108,7 @@ export const data: Array = [ updateDate: '2023-10-12T18:30:32.879Z', createDate: '2023-10-12T18:30:32.879Z', failedLoginAttempts: 25, - userGroupIds: ['user-group-editors-id', 'user-group-sensitive-data-id'], + userGroupIds: [{ id: 'user-group-editors-id' }, { id: 'user-group-sensitive-data-id' }], userName: '', avatarUrls: [], isAdmin: false, @@ -128,8 +128,4 @@ export const mfaLoginProviders: Array = [ isEnabledOnUser: false, providerName: 'sms', }, - { - isEnabledOnUser: true, - providerName: 'Email', - }, ]; diff --git a/src/mocks/data/user/user.db.ts b/src/mocks/data/user/user.db.ts index f414e6bc65..2333106580 100644 --- a/src/mocks/data/user/user.db.ts +++ b/src/mocks/data/user/user.db.ts @@ -1,5 +1,5 @@ import { umbUserGroupMockDb } from '../user-group/user-group.db.js'; -import { arrayFilter, stringFilter, queryFilter } from '../utils.js'; +import { arrayFilter, stringFilter, queryFilter, objectArrayFilter } from '../utils.js'; import { UmbEntityMockDbBase } from '../utils/entity/entity-base.js'; import { UmbMockEntityItemManager } from '../utils/entity/entity-item.manager.js'; import { UmbMockEntityDetailManager } from '../utils/entity/entity-detail.manager.js'; @@ -17,11 +17,22 @@ import type { } from '@umbraco-cms/backoffice/external/backend-api'; import { UserStateModel } from '@umbraco-cms/backoffice/external/backend-api'; -const userGroupFilter = (filterOptions: any, item: UmbMockUserModel) => - arrayFilter(filterOptions.userGroupIds, item.userGroupIds); -const userStateFilter = (filterOptions: any, item: UmbMockUserModel) => +interface UserFilterOptions { + skip: number; + take: number; + orderBy: string; + orderDirection: string; + userGroupIds: Array<{ id: string }>; + userStates: Array; + filter: string; +} + +const userGroupFilter = (filterOptions: UserFilterOptions, item: UmbMockUserModel) => + objectArrayFilter(filterOptions.userGroupIds, item.userGroupIds, 'id'); +const userStateFilter = (filterOptions: UserFilterOptions, item: UmbMockUserModel) => stringFilter(filterOptions.userStates, item.state); -const userQueryFilter = (filterOptions: any, item: UmbMockUserModel) => queryFilter(filterOptions.filter, item.name); +const userQueryFilter = (filterOptions: UserFilterOptions, item: UmbMockUserModel) => + queryFilter(filterOptions.filter, item.name); // Temp mocked database class UmbUserMockDB extends UmbEntityMockDbBase { @@ -38,7 +49,8 @@ class UmbUserMockDB extends UmbEntityMockDbBase { * @memberof UmbUserData */ setUserGroups(data: UpdateUserGroupsOnUserRequestModel): void { - const users = this.data.filter((user) => data.userIds?.includes(user.id ?? '')); + const users = this.data.filter((user) => data.userIds?.map((reference) => reference.id).includes(user.id)); + users.forEach((user) => { user.userGroupIds = data.userGroupIds; }); @@ -154,10 +166,10 @@ class UmbUserMockDB extends UmbEntityMockDbBase { return { userId: newUserId }; } - filter(options: any): PagedUserResponseModel { + filter(options: UserFilterOptions): PagedUserResponseModel { const allItems = this.getAll(); - const filterOptions = { + const filterOptions: UserFilterOptions = { skip: options.skip || 0, take: options.take || 25, orderBy: options.orderBy || 'name', @@ -209,7 +221,7 @@ const createMockMapper = (item: CreateUserRequestModel): UmbMockUserModel => { lastLoginDate: null, lastLockoutDate: null, lastPasswordChangeDate: null, - isAdmin: item.userGroupIds.includes(umbUserGroupMockDb.getAll()[0].id), + isAdmin: item.userGroupIds.map((reference) => reference.id).includes(umbUserGroupMockDb.getAll()[0].id), }; }; diff --git a/src/mocks/data/utils.ts b/src/mocks/data/utils.ts index 1635df1b75..7b2536ef50 100644 --- a/src/mocks/data/utils.ts +++ b/src/mocks/data/utils.ts @@ -7,11 +7,20 @@ export const arrayFilter = (filterBy: Array, value?: Array): boo return filterBy.some((filterValue: string) => value?.includes(filterValue)); }; +export const objectArrayFilter = (filterBy: Array, value: Array, key: string): boolean => { + if (!filterBy || !value) { + return true; + } + + return value.map((value) => value[key]).some((value: any) => filterBy.includes(value)); +}; + export const stringFilter = (filterBy: Array, value?: string): boolean => { // if a filter is not set, return all items if (!filterBy || !value) { return true; } + return filterBy.includes(value); }; diff --git a/src/mocks/handlers/manifests.handlers.ts b/src/mocks/handlers/manifests.handlers.ts index 4806994c0d..b8a500f4bb 100644 --- a/src/mocks/handlers/manifests.handlers.ts +++ b/src/mocks/handlers/manifests.handlers.ts @@ -76,6 +76,15 @@ const privateManifests: PackageManifestResponse = [ label: 'Setup SMS Verification', }, }, + { + type: 'mfaLoginProvider', + alias: 'My.MfaLoginProvider.Custom.Email', + name: 'My Custom Email MFA Provider', + forProviderName: 'email', + meta: { + label: 'Setup Email Verification', + }, + }, ], }, { @@ -92,20 +101,6 @@ const privateManifests: PackageManifestResponse = [ }, ], }, - { - name: 'My MFA Package', - extensions: [ - { - type: 'mfaLoginProvider', - alias: 'My.MfaLoginProvider.Custom', - name: 'My Custom MFA Provider', - forProviderName: 'sms', - meta: { - label: 'Setup SMS Verification', - }, - }, - ], - }, ]; const publicManifests: PackageManifestResponse = [ diff --git a/src/mocks/handlers/user-group/filter.handlers.ts b/src/mocks/handlers/user-group/filter.handlers.ts new file mode 100644 index 0000000000..931b80e094 --- /dev/null +++ b/src/mocks/handlers/user-group/filter.handlers.ts @@ -0,0 +1,21 @@ +const { rest } = window.MockServiceWorker; +import { umbUserGroupMockDb } from '../../data/user-group/user-group.db.js'; +import { UMB_SLUG } from './slug.js'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export const handlers = [ + rest.get(umbracoPath(`/filter${UMB_SLUG}`), (req, res, ctx) => { + const skip = Number(req.url.searchParams.get('skip')); + const take = Number(req.url.searchParams.get('take')); + const filter = req.url.searchParams.get('filter'); + + const options: any = { + skip: skip || undefined, + take: take || undefined, + filter: filter || undefined, + }; + + const response = umbUserGroupMockDb.filter(options); + return res(ctx.status(200), ctx.json(response)); + }), +]; diff --git a/src/mocks/handlers/user-group/index.ts b/src/mocks/handlers/user-group/index.ts index 8432caafdd..aa9f26c37e 100644 --- a/src/mocks/handlers/user-group/index.ts +++ b/src/mocks/handlers/user-group/index.ts @@ -1,4 +1,5 @@ import { detailHandlers } from './detail.handlers.js'; import { itemHandlers } from './item.handlers.js'; +import { handlers as filterHandlers } from './filter.handlers.js'; -export const handlers = [...itemHandlers, ...detailHandlers]; +export const handlers = [...itemHandlers, ...filterHandlers, ...detailHandlers]; diff --git a/src/mocks/handlers/user/current.handlers.ts b/src/mocks/handlers/user/current.handlers.ts index 9454d23c56..cff60866fd 100644 --- a/src/mocks/handlers/user/current.handlers.ts +++ b/src/mocks/handlers/user/current.handlers.ts @@ -1,7 +1,7 @@ const { rest } = window.MockServiceWorker; import { umbUserMockDb } from '../../data/user/user.db.js'; import { UMB_SLUG } from './slug.js'; -import type { LinkedLoginsRequestModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UserData } from '@umbraco-cms/backoffice/external/backend-api'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; export const handlers = [ @@ -9,19 +9,22 @@ export const handlers = [ const loggedInUser = umbUserMockDb.getCurrentUser(); return res(ctx.status(200), ctx.json(loggedInUser)); }), - rest.get(umbracoPath(`${UMB_SLUG}/current/logins`), (_req, res, ctx) => { - return res( - ctx.status(200), - ctx.json({ - linkedLogins: [ + rest.get( + umbracoPath(`${UMB_SLUG}/current/login-providers`), + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.json([ { + hasManualLinkingEnabled: true, + isLinkedOnUser: true, providerKey: 'google', - providerName: 'Umbraco.Google', + providerSchemeName: 'Umbraco.Google', }, - ], - }), - ); - }), + ]), + ); + }, + ), rest.get(umbracoPath(`${UMB_SLUG}/current/2fa`), (_req, res, ctx) => { const mfaLoginProviders = umbUserMockDb.getMfaLoginProviders(); return res(ctx.status(200), ctx.json(mfaLoginProviders)); diff --git a/src/mocks/handlers/user/filter.handlers.ts b/src/mocks/handlers/user/filter.handlers.ts index 8d078cfb57..f01d2922a3 100644 --- a/src/mocks/handlers/user/filter.handlers.ts +++ b/src/mocks/handlers/user/filter.handlers.ts @@ -13,7 +13,7 @@ export const handlers = [ const userStates = req.url.searchParams.getAll('userStates'); const filter = req.url.searchParams.get('filter'); - const options = { + const options: any = { skip: skip || undefined, take: take || undefined, orderBy: orderBy || undefined, diff --git a/src/packages/block/block-grid/components/block-grid-areas-container/block-grid-areas-container.element.ts b/src/packages/block/block-grid/components/block-grid-areas-container/block-grid-areas-container.element.ts index 4e6ba3e312..d4c1ee9496 100644 --- a/src/packages/block/block-grid/components/block-grid-areas-container/block-grid-areas-container.element.ts +++ b/src/packages/block/block-grid/components/block-grid-areas-container/block-grid-areas-container.element.ts @@ -1,4 +1,4 @@ -import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../../context/block-grid-manager.context.js'; +import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../../context/block-grid-manager.context-token.js'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_BLOCK_GRID_ENTRY_CONTEXT, type UmbBlockGridTypeAreaType } from '@umbraco-cms/backoffice/block-grid'; import { css, customElement, html, repeat, state } from '@umbraco-cms/backoffice/external/lit'; diff --git a/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts b/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts index fece6fb327..a0b17d486d 100644 --- a/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts +++ b/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts @@ -11,13 +11,19 @@ import { html, customElement, state, repeat, css, property, nothing } from '@umb import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import '../block-grid-entry/index.js'; import { UmbSorterController, type UmbSorterConfig, type resolvePlacementArgs } from '@umbraco-cms/backoffice/sorter'; +import { + UmbFormControlMixin, + UmbFormControlValidator, + type UmbFormControlValidatorConfig, +} from '@umbraco-cms/backoffice/validation'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; /** * Notice this utility method is not really shareable with others as it also takes areas into account. [NL] */ function resolvePlacementAsGrid(args: resolvePlacementArgs) { // If this has areas, we do not want to move, unless we are at the edge - if (args.relatedModel.areas.length > 0 && isWithinRect(args.pointerX, args.pointerY, args.relatedRect, -10)) { + if (args.relatedModel.areas?.length > 0 && isWithinRect(args.pointerX, args.pointerY, args.relatedRect, -10)) { return null; } @@ -98,7 +104,7 @@ const SORTER_CONFIG: UmbSorterConfig { - const oldValue = this._layoutEntries; - this.#sorter.setModel(layoutEntries); - this._layoutEntries = layoutEntries; - this.requestUpdate('layoutEntries', oldValue); - }); - - this.observe(this.#context.amountOfAllowedBlockTypes, (length) => { - this._canCreate = length > 0; - if (length === 1) { - this.observe( - this.#context.firstAllowedBlockTypeName(), - (firstAllowedName) => { - this._singleBlockTypeName = firstAllowedName; - }, - 'observeSingleBlockTypeName', - ); - } else { - this.removeUmbControllerByAlias('observeSingleBlockTypeName'); - } - }); + this.observe( + this.#context.layoutEntries, + (layoutEntries) => { + //const oldValue = this._layoutEntries; + this.#sorter.setModel(layoutEntries); + this._layoutEntries = layoutEntries; + //this.requestUpdate('layoutEntries', oldValue); + }, + null, + ); + + this.observe( + this.#context.amountOfAllowedBlockTypes, + (length) => { + this._canCreate = length > 0; + if (length === 1) { + this.observe( + this.#context.firstAllowedBlockTypeName(), + (firstAllowedName) => { + this._singleBlockTypeName = firstAllowedName; + }, + 'observeSingleBlockTypeName', + ); + } else { + this.removeUmbControllerByAlias('observeSingleBlockTypeName'); + } + }, + null, + ); + + this.observe( + this.#context.rangeLimits, + (rangeLimits) => { + this.#setupRangeValidation(rangeLimits); + }, + null, + ); this.#context.getManager().then((manager) => { this.observe( @@ -195,9 +218,55 @@ export class UmbBlockGridEntriesElement extends UmbLitElement { 'observeStylesheet', ); }); + + this.#controlValidator = new UmbFormControlValidator(this, this /*, this.#dataPath*/); + } + + #rangeUnderflowValidator?: UmbFormControlValidatorConfig; + #rangeOverflowValidator?: UmbFormControlValidatorConfig; + async #setupRangeValidation(rangeLimit: UmbNumberRangeValueType | undefined) { + if (this.#rangeUnderflowValidator) { + this.removeValidator(this.#rangeUnderflowValidator); + this.#rangeUnderflowValidator = undefined; + } + if (rangeLimit?.min !== 0) { + this.#rangeUnderflowValidator = this.addValidator( + 'rangeUnderflow', + () => { + return this.localize.term( + 'validation_entriesShort', + rangeLimit!.min, + (rangeLimit!.min ?? 0) - this._layoutEntries.length, + ); + }, + () => { + return this._layoutEntries.length < (rangeLimit?.min ?? 0); + }, + ); + } + + if (this.#rangeOverflowValidator) { + this.removeValidator(this.#rangeOverflowValidator); + this.#rangeOverflowValidator = undefined; + } + if (rangeLimit?.max !== Infinity) { + this.#rangeOverflowValidator = this.addValidator( + 'rangeOverflow', + () => { + return this.localize.term( + 'validation_entriesExceed', + rangeLimit!.max, + this._layoutEntries.length - (rangeLimit!.max ?? this._layoutEntries.length), + ); + }, + () => { + return (this._layoutEntries.length ?? 0) > (rangeLimit?.max ?? Infinity); + }, + ); + } } - // TODO: Missing ability to jump directly to creating a Block, when there is only one Block Type. + // TODO: Missing ability to jump directly to creating a Block, when there is only one Block Type. [NL] render() { return html` ${this._styleElement} @@ -208,12 +277,13 @@ export class UmbBlockGridEntriesElement extends UmbLitElement { (layoutEntry, index) => html` `, )} + ${this._canCreate ? this.#renderCreateButton() : nothing} `; } diff --git a/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts b/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts index c1b6eeb54c..dd7b87b52f 100644 --- a/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts +++ b/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts @@ -1,6 +1,7 @@ import { UmbBlockGridEntryContext } from '../../context/block-grid-entry.context.js'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { html, css, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbBlockViewPropsType } from '@umbraco-cms/backoffice/block'; import type { UmbBlockGridLayoutModel } from '@umbraco-cms/backoffice/block-grid'; @@ -14,7 +15,7 @@ import '../block-scale-handler/index.js'; @customElement('umb-block-grid-entry') export class UmbBlockGridEntryElement extends UmbLitElement implements UmbPropertyEditorUiElement { // - @property({ type: Number }) + @property({ type: Number, reflect: true }) public get index(): number | undefined { return this.#context.getIndex(); } @@ -37,6 +38,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper // #context = new UmbBlockGridEntryContext(this); + #renderTimeout: number | undefined; @state() _columnSpan?: number; @@ -51,11 +53,16 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper // If _createPath is undefined, its because no blocks are allowed to be created here[NL] @state() - _createPath?: string; + _createBeforePath?: string; + @state() + _createAfterPath?: string; @state() _label = ''; + @state() + _icon?: string; + @state() _workspaceEditContentPath?: string; @@ -68,6 +75,13 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper @state() _canScale?: boolean; + @state() + _showInlineCreateBefore?: boolean; + @state() + _showInlineCreateAfter?: boolean; + @state() + _inlineCreateAboveWidth?: string; + // TODO: use this type on the Element Interface for the Manifest. @state() _blockViewProps: UmbBlockViewPropsType = { contentUdi: undefined!, urls: {} }; // Set to undefined cause it will be set before we render. @@ -94,6 +108,10 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper this.#updateBlockViewProps({ label }); this._label = label; }); + this.observe(this.#context.contentElementTypeIcon, (icon) => { + this.#updateBlockViewProps({ icon }); + this._icon = icon; + }); this.observe(this.#context.inlineEditingMode, (mode) => { this._inlineEditingMode = mode; }); @@ -110,10 +128,15 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper }); // Paths: - this.observe(this.#context.createPath, (createPath) => { - const oldValue = this._createPath; - this._createPath = createPath; - this.requestUpdate('_createPath', oldValue); + this.observe(this.#context.createBeforePath, (createPath) => { + //const oldValue = this._createBeforePath; + this._createBeforePath = createPath; + //this.requestUpdate('_createPath', oldValue); + }); + this.observe(this.#context.createAfterPath, (createPath) => { + //const oldValue = this._createAfterPath; + this._createAfterPath = createPath; + //this.requestUpdate('_createPath', oldValue); }); this.observe(this.#context.workspaceEditContentPath, (path) => { this._workspaceEditContentPath = path; @@ -156,8 +179,52 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper this.setAttribute('data-content-element-type-alias', contentElementTypeAlias); } }); + + this.#callUpdateInlineCreateButtons(); + } + + protected updated(_changedProperties: PropertyValueMap | Map): void { + super.updated(_changedProperties); + if (_changedProperties.has('_blockViewProps') || _changedProperties.has('_columnSpan')) { + this.#callUpdateInlineCreateButtons(); + } + } + + #callUpdateInlineCreateButtons() { + clearTimeout(this.#renderTimeout); + this.#renderTimeout = setTimeout(this.#updateInlineCreateButtons, 100) as unknown as number; } + #updateInlineCreateButtons = () => { + // TODO: Could we optimize this, so it wont break?, cause currently we trust blindly that parentElement is '.umb-block-grid__layout-container' [NL] + const layoutContainer = this.parentElement; + if (!layoutContainer) return; + const layoutContainerRect = layoutContainer.getBoundingClientRect(); + + if (layoutContainerRect.width === 0) { + this._showInlineCreateBefore = false; + this._showInlineCreateAfter = false; + this._inlineCreateAboveWidth = undefined; + this.#renderTimeout = setTimeout(this.#updateInlineCreateButtons, 100) as unknown as number; + return; + } + + const layoutItemRect = this.getBoundingClientRect(); + if (layoutItemRect.right > layoutContainerRect.right - 5) { + this._showInlineCreateAfter = false; + } else { + this._showInlineCreateAfter = true; + } + + if (layoutItemRect.left > layoutContainerRect.left + 5) { + this._showInlineCreateBefore = false; + this._inlineCreateAboveWidth = undefined; + } else { + this._inlineCreateAboveWidth = getComputedStyle(layoutContainer).width; + this._showInlineCreateBefore = true; + } + }; + #renderInlineEditBlock() { return html`` + ${this._createBeforePath && this._showInlineCreateBefore + ? html`` : nothing}
` : nothing}
+ ${this._createAfterPath && this._showInlineCreateAfter + ? html`` + : nothing} ` : nothing; } @@ -223,6 +301,24 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper top: var(--uui-size-2); right: var(--uui-size-2); } + uui-button-inline-create { + top: 0px; + position: absolute; + + // Avoid showing inline-create in dragging-mode + --umb-block-grid__block--inline-create-button-display--condition: var(--umb-block-grid--dragging-mode) none; + display: var(--umb-block-grid__block--inline-create-button-display--condition); + } + uui-button-inline-create:not([vertical]) { + left: 0; + width: var(--umb-block-grid-editor--inline-create-width, 100%); + } + :host(:not([index='0'])) uui-button-inline-create:not([vertical]) { + top: calc(var(--umb-block-grid--row-gap, 0px) * -0.5); + } + uui-button-inline-create[vertical] { + right: calc(1px - (var(--umb-block-grid--column-gap, 0px) * 0.5)); + } :host([drag-placeholder]) { opacity: 0.2; diff --git a/src/packages/block/block-grid/context/block-grid-entries.context.ts b/src/packages/block/block-grid/context/block-grid-entries.context.ts index 96a700d236..7b5ad26bfc 100644 --- a/src/packages/block/block-grid/context/block-grid-entries.context.ts +++ b/src/packages/block/block-grid/context/block-grid-entries.context.ts @@ -1,13 +1,18 @@ import type { UmbBlockDataType } from '../../block/index.js'; import { UMB_BLOCK_CATALOGUE_MODAL, UmbBlockEntriesContext } from '../../block/index.js'; -import { UMB_BLOCK_GRID_ENTRY_CONTEXT, type UmbBlockGridWorkspaceData } from '../index.js'; +import { + UMB_BLOCK_GRID_ENTRY_CONTEXT, + UMB_BLOCK_GRID_WORKSPACE_MODAL, + type UmbBlockGridWorkspaceData, +} from '../index.js'; import type { UmbBlockGridLayoutModel, UmbBlockGridTypeAreaType, UmbBlockGridTypeModel } from '../types.js'; -import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context.js'; +import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js'; import type { UmbBlockGridScalableContainerContext } from './block-grid-scale-manager/block-grid-scale-manager.controller.js'; -import { UmbArrayState, UmbNumberState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbNumberState, UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { pathFolderName } from '@umbraco-cms/backoffice/utils'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; export class UmbBlockGridEntriesContext extends UmbBlockEntriesContext< @@ -20,6 +25,7 @@ export class UmbBlockGridEntriesContext { // #catalogueModal: UmbModalRouteRegistrationController; + #workspaceModal: UmbModalRouteRegistrationController; #parentEntry?: typeof UMB_BLOCK_GRID_ENTRY_CONTEXT.TYPE; @@ -31,6 +37,9 @@ export class UmbBlockGridEntriesContext #parentUnique?: string | null; #areaKey?: string | null; + #rangeLimits = new UmbObjectState(undefined); + readonly rangeLimits = this.#rangeLimits.asObservable(); + #allowedBlockTypes = new UmbArrayState([], (x) => x.contentElementTypeKey); public readonly allowedBlockTypes = this.#allowedBlockTypes.asObservable(); public readonly amountOfAllowedBlockTypes = this.#allowedBlockTypes.asObservablePart((x) => x.length); @@ -62,13 +71,13 @@ export class UmbBlockGridEntriesContext setParentUnique(contentUdi: string | null) { this.#parentUnique = contentUdi; // Notice pathFolderName can be removed when we have switched to use a proper GUID/ID/KEY. [NL] - this._workspaceModal.setUniquePathValue('parentUnique', pathFolderName(contentUdi ?? 'null')); + this.#workspaceModal.setUniquePathValue('parentUnique', pathFolderName(contentUdi ?? 'null')); this.#catalogueModal.setUniquePathValue('parentUnique', pathFolderName(contentUdi ?? 'null')); } setAreaKey(areaKey: string | null) { this.#areaKey = areaKey; - this._workspaceModal.setUniquePathValue('areaKey', areaKey ?? 'null'); + this.#workspaceModal.setUniquePathValue('areaKey', areaKey ?? 'null'); this.#catalogueModal.setUniquePathValue('areaKey', areaKey ?? 'null'); this.#gotAreaKey(); } @@ -89,8 +98,6 @@ export class UmbBlockGridEntriesContext constructor(host: UmbControllerHost) { super(host, UMB_BLOCK_GRID_MANAGER_CONTEXT); - this._workspaceModal.addUniquePaths(['parentUnique', 'areaKey']); - this.consumeContext(UMB_BLOCK_GRID_ENTRY_CONTEXT, (blockGridEntry) => { this.#parentEntry = blockGridEntry; this.#gotBlockParentEntry(); // is not used at this point. [NL] @@ -115,17 +122,37 @@ export class UmbBlockGridEntriesContext // TODO: Does it make any sense that this is a state? Check usage and confirm. [NL] this._catalogueRouteBuilderState.setValue(routeBuilder); }); + + this.#workspaceModal = new UmbModalRouteRegistrationController(this, UMB_BLOCK_GRID_WORKSPACE_MODAL) + .addUniquePaths(['propertyAlias', 'variantId', 'parentUnique', 'areaKey']) + .addAdditionalPath('block') + .onSetup(() => { + return { + data: { + entityType: 'block', + preset: {}, + originData: { areaKey: this.#areaKey, parentUnique: this.#parentUnique }, + }, + modal: { size: 'medium' }, + }; + }) + .observeRouteBuilder((routeBuilder) => { + const newPath = routeBuilder({}); + this._workspacePath.setValue(newPath); + }); } protected _gotBlockManager() { if (!this._manager) return; - this.#allowedBlockTypes.setValue(this.#retrieveAllowedElementTypes()); + this.#getAllowedBlockTypes(); + this.#getRangeLimits(); this.observe( this._manager.propertyAlias, (alias) => { this.#catalogueModal.setUniquePathValue('propertyAlias', alias ?? 'null'); + this.#workspaceModal.setUniquePathValue('propertyAlias', alias ?? 'null'); }, 'observePropertyAlias', ); @@ -133,9 +160,9 @@ export class UmbBlockGridEntriesContext this.observe( this._manager.variantId, (variantId) => { - if (variantId) { - this.#catalogueModal.setUniquePathValue('variantId', variantId.toString()); - } + // TODO: This might not be the property variant ID, but the content variant ID. Check up on what makes most sense? + this.#catalogueModal.setUniquePathValue('variantId', variantId?.toString()); + this.#workspaceModal.setUniquePathValue('variantId', variantId?.toString()); }, 'observePropertyAlias', ); @@ -154,6 +181,7 @@ export class UmbBlockGridEntriesContext await this._retrieveManager; if (!this._manager) return; + this.removeUmbControllerByAlias('observeParentUnique'); this.setParentUnique(null); this.observe( this._manager.layouts, @@ -181,6 +209,10 @@ export class UmbBlockGridEntriesContext hostEl.style.removeProperty('--umb-block-grid--area-column-span'); hostEl.style.removeProperty('--umb-block-grid--area-row-span'); } + + this.removeUmbControllerByAlias('observeAreaType'); + this.#getAllowedBlockTypes(); + this.#getRangeLimits(); } else { if (!this.#parentEntry) return; @@ -194,7 +226,9 @@ export class UmbBlockGridEntriesContext this.observe( this.#parentEntry.layoutsOfArea(this.#areaKey), (layouts) => { - this._layoutEntries.setValue(layouts); + if (layouts) { + this._layoutEntries.setValue(layouts); + } }, 'observeParentLayouts', ); @@ -221,12 +255,24 @@ export class UmbBlockGridEntriesContext hostEl.style.setProperty('--umb-block-grid--grid-columns', areaType?.columnSpan?.toString() ?? ''); hostEl.style.setProperty('--umb-block-grid--area-column-span', areaType?.columnSpan?.toString() ?? ''); hostEl.style.setProperty('--umb-block-grid--area-row-span', areaType?.rowSpan?.toString() ?? ''); + this.#getAllowedBlockTypes(); + this.#getRangeLimits(); }, 'observeAreaType', ); } } + #getAllowedBlockTypes() { + if (!this._manager) return; + this.#allowedBlockTypes.setValue(this.#retrieveAllowedElementTypes()); + } + #getRangeLimits() { + if (!this._manager) return; + const range = this.#retrieveRangeLimits(); + this.#rangeLimits.setValue(range); + } + getPathForCreateBlock(index: number) { return this._catalogueRouteBuilderState.getValue()?.({ view: 'create', index: index }); } @@ -235,6 +281,20 @@ export class UmbBlockGridEntriesContext return this._catalogueRouteBuilderState.getValue()?.({ view: 'clipboard', index: index }); } + /* + async setLayouts(layouts: Array) { + await this._retrieveManager; + if (this.#areaKey === null) { + this._manager?.setLayouts(layouts); + } else { + if (!this.#parentUnique || !this.#areaKey) { + throw new Error('ParentUnique or AreaKey not set'); + } + this._manager?.setLayoutsOfArea(this.#parentUnique, this.#areaKey, layouts); + } + } + */ + async create( contentElementTypeKey: string, partialLayoutEntry?: Omit, @@ -297,10 +357,34 @@ export class UmbBlockGridEntriesContext // No specific permissions setup, so we will fallback to items allowed in areas: return this._manager.getBlockTypes().filter((x) => x.allowInAreas); + } else if (this.#areaKey === null) { + // If AreaKey is null, then we are in the root, looking for items allowed as root: + return this._manager.getBlockTypes().filter((x) => x.allowAtRoot); + } + + return []; + } + + /** + * @internal + * @returns an NumberRange of the min and max allowed items in the current area. Or undefined if not ready jet. + */ + #retrieveRangeLimits(): UmbNumberRangeValueType | undefined { + if (this.#areaKey != null) { + // Area entries: + if (!this.#areaType) return undefined; + + return { min: this.#areaType.minAllowed ?? 0, max: this.#areaType.maxAllowed ?? Infinity }; + } else if (this.#areaKey === null) { + if (!this._manager) return undefined; + + const config = this._manager.getEditorConfiguration(); + const min = config?.getValueByAlias('validationLimit')?.min ?? 0; + const max = config?.getValueByAlias('validationLimit')?.max ?? Infinity; + return { min, max }; } - // If no AreaKey, then we are in the root, looking for items allowed as root: - return this._manager.getBlockTypes().filter((x) => x.allowAtRoot); + return undefined; } /** diff --git a/src/packages/block/block-grid/context/block-grid-entry.context.ts b/src/packages/block/block-grid/context/block-grid-entry.context.ts index 819e70f08a..2b026e2e92 100644 --- a/src/packages/block/block-grid/context/block-grid-entry.context.ts +++ b/src/packages/block/block-grid/context/block-grid-entry.context.ts @@ -1,5 +1,5 @@ import { closestColumnSpanOption } from '../utils/index.js'; -import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context.js'; +import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js'; import { UMB_BLOCK_GRID_ENTRIES_CONTEXT } from './block-grid-entries.context-token.js'; import { type UmbBlockGridScalableContext, @@ -31,14 +31,18 @@ export class UmbBlockGridEntryContext { // readonly columnSpan = this._layout.asObservablePart((x) => x?.columnSpan); - readonly rowSpan = this._layout.asObservablePart((x) => x?.rowSpan ?? 1); + readonly rowSpan = this._layout.asObservablePart((x) => x?.rowSpan); + readonly layoutAreas = this._layout.asObservablePart((x) => x?.areas); readonly columnSpanOptions = this._blockType.asObservablePart((x) => x?.columnSpanOptions ?? []); readonly areaTypeGridColumns = this._blockType.asObservablePart((x) => x?.areaGridColumns); readonly areas = this._blockType.asObservablePart((x) => x?.areas ?? []); - readonly minMaxRowSpan = this._blockType.asObservablePart((x) => [x?.rowMinSpan ?? 1, x?.rowMaxSpan ?? 1]); - public getMinMaxRowSpan(): [number, number] { + readonly minMaxRowSpan = this._blockType.asObservablePart((x) => + x ? [x.rowMinSpan ?? 1, x.rowMaxSpan ?? 1] : undefined, + ); + public getMinMaxRowSpan(): [number, number] | undefined { const x = this._blockType.getValue(); - return [x?.rowMinSpan ?? 1, x?.rowMaxSpan ?? 1]; + if (!x) return undefined; + return [x.rowMinSpan ?? 1, x.rowMaxSpan ?? 1]; } readonly inlineEditingMode = this._blockType.asObservablePart((x) => x?.inlineEditing === true); @@ -65,18 +69,8 @@ export class UmbBlockGridEntryContext super(host, UMB_BLOCK_GRID_MANAGER_CONTEXT, UMB_BLOCK_GRID_ENTRIES_CONTEXT); } - protected _gotLayout(layout: UmbBlockGridLayoutModel | undefined) { - if (layout) { - // TODO: Implement size correction to fit with configurations. both for columnSpan and rowSpan. - layout = { ...layout }; - layout.columnSpan ??= 999; - layout.areas ??= []; - } - return layout; - } - layoutsOfArea(areaKey: string) { - return this._layout.asObservablePart((x) => x?.areas.find((x) => x.key === areaKey)?.items ?? []); + return this._layout.asObservablePart((x) => x?.areas?.find((x) => x.key === areaKey)?.items); } areaType(areaKey: string) { @@ -87,7 +81,7 @@ export class UmbBlockGridEntryContext const frozenValue = this._layout.value; if (!frozenValue) return; const areas = appendToFrozenArray( - frozenValue?.areas, + frozenValue?.areas ?? [], { key: areaKey, items: layouts, @@ -97,40 +91,71 @@ export class UmbBlockGridEntryContext this._layout.update({ areas }); } + /** + * Set the column span of this entry. + * @param columnSpan {number} The new column span. + */ setColumnSpan(columnSpan: number) { if (!this._entries) return; const layoutColumns = this._entries.getLayoutColumns(); if (!layoutColumns) return; - columnSpan = Math.max(1, Math.min(columnSpan, layoutColumns)); - this._layout.update({ columnSpan }); + columnSpan = this.#calcColumnSpan(columnSpan, this.getRelevantColumnSpanOptions(), layoutColumns); + if (columnSpan === this.getColumnSpan()) return; + const layoutValue = this._layout.getValue(); + if (!layoutValue) return; + this._layout.setValue({ + ...layoutValue, + columnSpan, + }); } + /** + * Get the column span of this entry. + * @returns {number} The column span. + */ getColumnSpan() { return this._layout.getValue()?.columnSpan; } + /** + * Set the row span of this entry. + * @param rowSpan {number} The new row span. + */ setRowSpan(rowSpan: number) { - const blockType = this._blockType.getValue(); - if (!blockType) return; - rowSpan = Math.max(blockType.rowMinSpan, Math.min(rowSpan, blockType.rowMaxSpan)); - this._layout.update({ rowSpan }); + const minMax = this.getMinMaxRowSpan(); + if (!minMax) return; + rowSpan = Math.max(minMax[0], Math.min(rowSpan, minMax[1])); + if (rowSpan === this.getRowSpan()) return; + const layoutValue = this._layout.getValue(); + if (!layoutValue) return; + this._layout.setValue({ + ...layoutValue, + rowSpan, + }); } - + /** + * Get the row span of this entry. + * @returns {number} The row span. + */ getRowSpan() { return this._layout.getValue()?.rowSpan; } - _gotManager() {} + _gotManager() { + this.#gotEntriesAndManager(); + } _gotEntries() { this.scaleManager.setEntriesContext(this._entries); - if (!this._entries) return; + this.#gotEntriesAndManager(); + + // Retrieve scale options: this.observe( observeMultiple([this.minMaxRowSpan, this.columnSpanOptions, this._entries.layoutColumns]), ([minMaxRowSpan, columnSpanOptions, layoutColumns]) => { - if (!layoutColumns) return; + if (!layoutColumns || !minMaxRowSpan) return; const relevantColumnSpanOptions = columnSpanOptions ? columnSpanOptions .filter((x) => x.columnSpan <= layoutColumns) @@ -147,6 +172,7 @@ export class UmbBlockGridEntryContext 'observeScaleOptions', ); + // Retrieve The Grid Columns for the Areas: this.observe( observeMultiple([this.areaTypeGridColumns, this._entries.layoutColumns]), ([areaTypeGridColumns, layoutColumns]) => { @@ -154,32 +180,90 @@ export class UmbBlockGridEntryContext }, 'observeAreaGridColumns', ); + } + + #gotEntriesAndManager() { + if (!this._entries || !this._manager) return; + // Secure areas fits options: this.observe( - observeMultiple([this.columnSpan, this.relevantColumnSpanOptions, this._entries.layoutColumns]), - ([columnSpan, relevantColumnSpanOptions, layoutColumns]) => { - if (!columnSpan || !layoutColumns) return; - if (relevantColumnSpanOptions.length > 0) { - // Correct columnSpan so it fits. - const newColumnSpan = - closestColumnSpanOption(columnSpan, relevantColumnSpanOptions, layoutColumns) ?? layoutColumns; - if (newColumnSpan !== columnSpan) { - //this.setColumnSpan(newColumnSpan); - this._layout.update({ columnSpan: newColumnSpan }); - } - } else { - // Reset to the layoutColumns. - if (layoutColumns !== columnSpan) { - //this.setColumnSpan(layoutColumns); - this._layout.update({ columnSpan: layoutColumns }); - } + observeMultiple([this.areas, this.layoutAreas]), + ([areas, layoutAreas]) => { + if (!areas || !layoutAreas) return; + const areasAreIdentical = + areas.length === layoutAreas.length && areas.every((area) => layoutAreas.some((y) => y.key === area.key)); + if (areasAreIdentical === false) { + const layoutValue = this._layout.getValue(); + if (!layoutValue) return; + this._layout.setValue({ + ...layoutValue, + areas: layoutAreas.map((x) => (areas.find((y) => y.key === x.key) ? x : { key: x.key, items: [] })), + }); + } + }, + 'observeAreaValidation', + ); + + // Secure columnSpan fits options: + this.observe( + observeMultiple([this.layout, this.columnSpan, this.relevantColumnSpanOptions, this._entries.layoutColumns]), + ([layout, columnSpan, relevantColumnSpanOptions, layoutColumns]) => { + if (!layout || !layoutColumns) return; + const newColumnSpan = this.#calcColumnSpan( + columnSpan ?? layoutColumns, + relevantColumnSpanOptions, + layoutColumns, + ); + if (newColumnSpan !== columnSpan) { + const layoutValue = this._layout.getValue(); + if (!layoutValue) return; + this._layout.setValue({ + ...layoutValue, + columnSpan: newColumnSpan, + }); } }, 'observeColumnSpanValidation', ); + + // Secure rowSpan fits options: + this.observe( + observeMultiple([this.minMaxRowSpan, this.rowSpan]), + ([minMax, rowSpan]) => { + if (minMax) { + const newRowSpan = Math.max(minMax[0], Math.min(rowSpan ?? 1, minMax[1])); + if (newRowSpan !== rowSpan) { + const layoutValue = this._layout.getValue(); + if (!layoutValue) return; + this._layout.setValue({ + ...layoutValue, + rowSpan: newRowSpan, + }); + } + } + }, + 'observeRowSpanValidation', + ); } _gotContentType(contentType: UmbContentTypeModel | undefined) { this.#firstPropertyType.setValue(contentType?.properties[0]); } + + #calcColumnSpan(columnSpan: number, relevantColumnSpanOptions: number[], layoutColumns: number) { + if (relevantColumnSpanOptions.length > 0) { + // Correct to a columnSpan option. + const newColumnSpan = + closestColumnSpanOption(columnSpan, relevantColumnSpanOptions, layoutColumns) ?? layoutColumns; + if (newColumnSpan !== columnSpan) { + return newColumnSpan; + } + } else { + // Reset to the layoutColumns. + if (layoutColumns !== columnSpan) { + return layoutColumns; + } + } + return columnSpan; + } } diff --git a/src/packages/block/block-grid/context/block-grid-manager.context-token.ts b/src/packages/block/block-grid/context/block-grid-manager.context-token.ts new file mode 100644 index 0000000000..a75bae703e --- /dev/null +++ b/src/packages/block/block-grid/context/block-grid-manager.context-token.ts @@ -0,0 +1,8 @@ +import type { UmbBlockGridManagerContext } from './block-grid-manager.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +// TODO: Make discriminator method for this: +export const UMB_BLOCK_GRID_MANAGER_CONTEXT = new UmbContextToken< + UmbBlockGridManagerContext, + UmbBlockGridManagerContext +>('UmbBlockManagerContext'); diff --git a/src/packages/block/block-grid/context/block-grid-manager.context.ts b/src/packages/block/block-grid/context/block-grid-manager.context.ts index becca70930..7535e4b126 100644 --- a/src/packages/block/block-grid/context/block-grid-manager.context.ts +++ b/src/packages/block/block-grid/context/block-grid-manager.context.ts @@ -1,7 +1,6 @@ import type { UmbBlockGridLayoutModel, UmbBlockGridTypeModel } from '../types.js'; import type { UmbBlockGridWorkspaceData } from '../index.js'; -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbArrayState, appendToFrozenArray } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, appendToFrozenArray, pushAtToUniqueArray } from '@umbraco-cms/backoffice/observable-api'; import { type UmbBlockDataType, UmbBlockManagerContext } from '@umbraco-cms/backoffice/block'; import type { UmbBlockTypeGroup } from '@umbraco-cms/backoffice/block-type'; @@ -43,9 +42,10 @@ export class UmbBlockGridManagerContext< /** * Inserts a layout entry into an area of a layout entry. * @param layoutEntry The layout entry to insert. - * @param content The content data to insert. - * @param settings The settings data to insert. - * @param modalData The modal data. + * @param entries The layout entries to search within. + * @param parentUnique The parentUnique to search for. + * @param areaKey The areaKey to insert the layout entry into. + * @param index The index to insert the layout entry at. * @returns a updated layout entries array if the insert was successful. * * @remarks @@ -63,62 +63,77 @@ export class UmbBlockGridManagerContext< ): Array | undefined { // I'm sorry, this code is not easy to read or maintain [NL] let i: number = entries.length; - while (--i) { - const layoutEntry = entries[i]; - if (layoutEntry.contentUdi === parentId) { + while (i--) { + const currentEntry = entries[i]; + // Lets check if we found the right parent layout entry: + if (currentEntry.contentUdi === parentId) { // Append the layout entry to be inserted and unfreeze the rest of the data: + const areas = currentEntry.areas.map((x) => + x.key === areaKey + ? { + ...x, + items: pushAtToUniqueArray([...x.items], insert, (x) => x.contentUdi === insert.contentUdi, index), + } + : x, + ); return appendToFrozenArray( entries, { - ...layoutEntry, - areas: layoutEntry.areas.map((x) => - x.key === areaKey ? { ...x, items: appendToFrozenArray(x.items, insert) } : x, - ), + ...currentEntry, + areas, }, - (x) => x.contentUdi === layoutEntry.contentUdi, + (x) => x.contentUdi === currentEntry.contentUdi, ); } - let y: number = layoutEntry.areas?.length; - while (--y) { + // Otherwise check if any items of the areas are the parent layout entry we are looking for. We do so based on parentId, recursively: + let y: number = currentEntry.areas?.length; + while (y--) { // Recursively ask the items of this area to insert the layout entry, if something returns there was a match in this branch. [NL] const correctedAreaItems = this.#appendLayoutEntryToArea( insert, - layoutEntry.areas[y].items, + currentEntry.areas[y].items, parentId, areaKey, index, ); if (correctedAreaItems) { // This area got a corrected set of items, lets append those to the area and unfreeze the surrounding data: - const area = layoutEntry.areas[y]; + const area = currentEntry.areas[y]; return appendToFrozenArray( entries, { - ...layoutEntry, + ...currentEntry, areas: appendToFrozenArray( - layoutEntry.areas, + currentEntry.areas, { ...area, items: correctedAreaItems }, (z) => z.key === area.key, ), }, - (x) => x.contentUdi === layoutEntry.contentUdi, + (x) => x.contentUdi === currentEntry.contentUdi, ); } } } - // Find layout entry based on parentId, recursively, as it needs to check layout of areas as well: return undefined; } + // TODO: Remove dependency on modalData object here. [NL] Maybe change it into requiring the originData object instead. insert( layoutEntry: BlockLayoutType, content: UmbBlockDataType, settings: UmbBlockDataType | undefined, modalData: UmbBlockGridWorkspaceData, ) { - const index = modalData.originData.index ?? -1; + this.setOneLayout(layoutEntry, modalData); + this.insertBlockData(layoutEntry, content, settings, modalData); + + return true; + } + + setOneLayout(layoutEntry: BlockLayoutType, modalData?: UmbBlockGridWorkspaceData) { + const index = modalData?.originData.index ?? -1; - if (modalData.originData.parentUnique && modalData.originData.areaKey) { + if (modalData?.originData.parentUnique && modalData?.originData.areaKey) { // Find layout entry based on parentUnique, recursively, as it needs to check layout of areas as well: const layoutEntries = this.#appendLayoutEntryToArea( layoutEntry, @@ -135,10 +150,6 @@ export class UmbBlockGridManagerContext< } else { this._layouts.appendOneAt(layoutEntry, index); } - - this.insertBlockData(layoutEntry, content, settings, modalData); - - return true; } onDragStart() { @@ -149,9 +160,3 @@ export class UmbBlockGridManagerContext< (this.getHostElement() as HTMLElement).style.removeProperty('--umb-block-grid--is-dragging'); } } - -// TODO: Make discriminator method for this: -export const UMB_BLOCK_GRID_MANAGER_CONTEXT = new UmbContextToken< - UmbBlockGridManagerContext, - UmbBlockGridManagerContext ->('UmbBlockManagerContext'); diff --git a/src/packages/block/block-grid/context/block-grid-scale-manager/block-grid-scale-manager.controller.ts b/src/packages/block/block-grid/context/block-grid-scale-manager/block-grid-scale-manager.controller.ts index a542df1664..6dc5a7ca07 100644 --- a/src/packages/block/block-grid/context/block-grid-scale-manager/block-grid-scale-manager.controller.ts +++ b/src/packages/block/block-grid/context/block-grid-scale-manager/block-grid-scale-manager.controller.ts @@ -9,7 +9,7 @@ export interface UmbBlockGridScalableContext extends UmbControllerHost { setRowSpan: (rowSpan: number) => void; getColumnSpan: () => number | undefined; getRowSpan: () => number | undefined; - getMinMaxRowSpan: () => [number, number]; + getMinMaxRowSpan: () => [number, number] | undefined; getRelevantColumnSpanOptions: () => Array | undefined; } @@ -128,14 +128,17 @@ export class UmbBlockGridScaleManager extends UmbControllerBase { let newColumnSpan = Math.max(blockEndCol - blockStartCol, 1); - const spanOptions = this._host.getRelevantColumnSpanOptions(); - if (!spanOptions) return; + const columnOptions = this._host.getRelevantColumnSpanOptions(); + if (!columnOptions) return; // Find nearest allowed Column: - const bestColumnSpanOption = closestColumnSpanOption(newColumnSpan, spanOptions, layoutColumns - blockStartCol); + const bestColumnSpanOption = closestColumnSpanOption(newColumnSpan, columnOptions, layoutColumns - blockStartCol); newColumnSpan = bestColumnSpanOption ?? layoutColumns; - const [rowMinSpan, rowMaxSpan] = this._host.getMinMaxRowSpan(); + // Find allowed row spans: + const minMaxRowSpan = this._host.getMinMaxRowSpan(); + if (!minMaxRowSpan) return; + const [rowMinSpan, rowMaxSpan] = minMaxRowSpan; let newRowSpan = Math.round(Math.max(blockEndRow - blockStartRow, rowMinSpan)); if (rowMaxSpan != null) { newRowSpan = Math.min(newRowSpan, rowMaxSpan); diff --git a/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts b/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts index 1077f48264..17d0814fd2 100644 --- a/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts +++ b/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts @@ -11,7 +11,7 @@ import { type UmbPropertyEditorConfigCollection, } from '@umbraco-cms/backoffice/property-editor'; import { UmbId } from '@umbraco-cms/backoffice/id'; -import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { incrementString } from '@umbraco-cms/backoffice/utils'; @customElement('umb-property-editor-ui-block-grid-areas-config') diff --git a/src/packages/block/block-grid/property-editors/block-grid-column-span/property-editor-ui-block-grid-column-span.element.ts b/src/packages/block/block-grid/property-editors/block-grid-column-span/property-editor-ui-block-grid-column-span.element.ts index e635d38d9a..4df56123d2 100644 --- a/src/packages/block/block-grid/property-editors/block-grid-column-span/property-editor-ui-block-grid-column-span.element.ts +++ b/src/packages/block/block-grid/property-editors/block-grid-column-span/property-editor-ui-block-grid-column-span.element.ts @@ -41,18 +41,18 @@ export class UmbPropertyEditorUIBlockGridColumnSpanElement extends UmbLitElement this._columnsArray, (index) => index, (index) => { - const number = index + 1; + const colNumber = index + 1; let classes = 'default'; if (this.value && this.value.length > 0) { - const applied = this.value.find((column) => column.columnSpan >= index); - const picked = this.value.find((column) => column.columnSpan === index); + const applied = this.value.find((column) => column.columnSpan >= colNumber); + const picked = this.value.find((column) => column.columnSpan === colNumber); classes = picked ? 'picked applied' : applied ? 'applied' : 'default'; } return html`
- ${number} - + ${colNumber} +
`; }, ); diff --git a/src/packages/block/block-grid/property-editors/block-grid-editor/Umbraco.BlockGrid.ts b/src/packages/block/block-grid/property-editors/block-grid-editor/Umbraco.BlockGrid.ts index b6d7ac4518..4c9e5eb5df 100644 --- a/src/packages/block/block-grid/property-editors/block-grid-editor/Umbraco.BlockGrid.ts +++ b/src/packages/block/block-grid/property-editors/block-grid-editor/Umbraco.BlockGrid.ts @@ -19,6 +19,7 @@ export const manifest: ManifestPropertyEditorSchema = { label: 'Amount', propertyEditorUiAlias: 'Umb.PropertyEditorUi.NumberRange', config: [{ alias: 'validationRange', value: { min: 0, max: Infinity } }], + weight: 100, }, ], defaultData: [ diff --git a/src/packages/block/block-grid/property-editors/block-grid-editor/manifests.ts b/src/packages/block/block-grid/property-editors/block-grid-editor/manifests.ts index b35ca6977e..7ce77f5c5f 100644 --- a/src/packages/block/block-grid/property-editors/block-grid-editor/manifests.ts +++ b/src/packages/block/block-grid/property-editors/block-grid-editor/manifests.ts @@ -20,6 +20,7 @@ export const manifests: Array = [ alias: 'blockGroups', label: '', propertyEditorUiAlias: 'Umb.PropertyEditorUi.BlockTypeGroupConfiguration', + weight: 1, }, { alias: 'useLiveEditing', @@ -42,9 +43,12 @@ export const manifests: Array = [ { alias: 'gridColumns', label: 'Grid Columns', - description: 'Set the number of columns for the layout. (defaults to 12)', - propertyEditorUiAlias: 'Umb.PropertyEditorUi.Number', - config: [{ alias: 'min', value: 0 }], + description: 'Set the number of columns for the layout.', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.Integer', + config: [ + { alias: 'min', value: 0 }, + { alias: 'placeholder', value: '12' }, + ], }, { alias: 'layoutStylesheet', diff --git a/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts b/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts index ba9f5b71b0..0c5bbe1676 100644 --- a/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts +++ b/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts @@ -4,20 +4,22 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { html, customElement, property, state, css, type PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; -import { - UmbPropertyValueChangeEvent, - type UmbPropertyEditorConfigCollection, -} from '@umbraco-cms/backoffice/property-editor'; +import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbBlockTypeGroup } from '@umbraco-cms/backoffice/block-type'; import type { UmbBlockGridTypeModel, UmbBlockGridValueModel } from '@umbraco-cms/backoffice/block-grid'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; import '../../components/block-grid-entries/index.js'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; /** * @element umb-property-editor-ui-block-grid */ @customElement('umb-property-editor-ui-block-grid') -export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implements UmbPropertyEditorUiElement { +export class UmbPropertyEditorUIBlockGridElement + extends UmbFormControlMixin(UmbLitElement) + implements UmbPropertyEditorUiElement +{ #context = new UmbBlockGridManagerContext(this); // private _value: UmbBlockGridValueModel = { @@ -29,10 +31,10 @@ export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implement public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - const validationLimit = config.getValueByAlias('validationLimit'); + /*const validationLimit = config.getValueByAlias('validationLimit'); - this._limitMin = validationLimit?.min; - this._limitMax = validationLimit?.max; + this.#limitMin = validationLimit?.min; + this.#limitMax = validationLimit?.max;*/ const blocks = config.getValueByAlias>('blocks') ?? []; this.#context.setBlockTypes(blocks); @@ -46,11 +48,6 @@ export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implement this.#context.setEditorConfiguration(config); } - // - @state() - private _limitMin?: number; - @state() - private _limitMax?: number; @state() private _layoutColumns?: number; @@ -63,8 +60,8 @@ export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implement this._value = buildUpValue as UmbBlockGridValueModel; this.#context.setLayouts(this._value.layout[UMB_BLOCK_GRID_PROPERTY_EDITOR_ALIAS] ?? []); - this.#context.setContents(buildUpValue.contentData); - this.#context.setSettings(buildUpValue.settingsData); + this.#context.setContents(this._value.contentData); + this.#context.setSettings(this._value.settingsData); } public get value(): UmbBlockGridValueModel { return this._value; @@ -73,30 +70,24 @@ export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implement constructor() { super(); - // TODO: Prevent initial notification from these observes: - this.observe(this.#context.layouts, (layouts) => { - this._value = { ...this._value, layout: { [UMB_BLOCK_GRID_PROPERTY_EDITOR_ALIAS]: layouts } }; - this.#fireChangeEvent(); - }); - this.observe(this.#context.contents, (contents) => { - this._value = { ...this._value, contentData: contents }; - this.#fireChangeEvent(); - }); - this.observe(this.#context.settings, (settings) => { - this._value = { ...this._value, settingsData: settings }; - this.#fireChangeEvent(); + // TODO: Prevent initial notification from these observes + this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => { + this.observe( + observeMultiple([this.#context.layouts, this.#context.contents, this.#context.settings]), + ([layouts, contents, settings]) => { + this._value = { + ...this._value, + layout: { [UMB_BLOCK_GRID_PROPERTY_EDITOR_ALIAS]: layouts }, + contentData: contents, + settingsData: settings, + }; + propertyContext.setValue(this._value); + }, + 'motherObserver', + ); }); } - #debounceChangeEvent?: boolean; - #fireChangeEvent = async () => { - if (this.#debounceChangeEvent) return; - this.#debounceChangeEvent = true; - await Promise.resolve(); - this.dispatchEvent(new UmbPropertyValueChangeEvent()); - this.#debounceChangeEvent = false; - }; - protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { super.firstUpdated(_changedProperties); @@ -109,7 +100,7 @@ export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implement } render() { - return html``; } diff --git a/src/packages/block/block-grid/property-editors/block-grid-layout-stylesheet/property-editor-ui-block-grid-layout-stylesheet.element.ts b/src/packages/block/block-grid/property-editors/block-grid-layout-stylesheet/property-editor-ui-block-grid-layout-stylesheet.element.ts index 9cd3218e1c..d93dcc568c 100644 --- a/src/packages/block/block-grid/property-editors/block-grid-layout-stylesheet/property-editor-ui-block-grid-layout-stylesheet.element.ts +++ b/src/packages/block/block-grid/property-editors/block-grid-layout-stylesheet/property-editor-ui-block-grid-layout-stylesheet.element.ts @@ -27,6 +27,8 @@ export class UmbPropertyEditorUIBlockGridLayoutStylesheetElement return this._value; } + private _pickableFilter = (item: any) => item.unique.endsWith('css'); + @property({ attribute: false }) public config?: UmbPropertyEditorConfigCollection; @@ -40,11 +42,12 @@ export class UmbPropertyEditorUIBlockGridLayoutStylesheetElement return html`
- Link to default layout stylesheet + Link to default layout stylesheet `; } diff --git a/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts b/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts index b309c9ceae..d0e022fb83 100644 --- a/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts +++ b/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts @@ -24,7 +24,8 @@ import { UMB_PROPERTY_DATASET_CONTEXT, type UmbPropertyDatasetContext, } from '@umbraco-cms/backoffice/property'; -import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; interface MappedGroupWithBlockTypes extends UmbBlockGridTypeGroupType { diff --git a/src/packages/block/block-grid/workspace/block-grid-workspace.modal-token.ts b/src/packages/block/block-grid/workspace/block-grid-workspace.modal-token.ts index bbe0f6b456..25bf6cb533 100644 --- a/src/packages/block/block-grid/workspace/block-grid-workspace.modal-token.ts +++ b/src/packages/block/block-grid/workspace/block-grid-workspace.modal-token.ts @@ -2,12 +2,13 @@ import type { UmbBlockWorkspaceData } from '@umbraco-cms/backoffice/block'; import type { UmbWorkspaceModalData, UmbWorkspaceModalValue } from '@umbraco-cms/backoffice/modal'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -export interface UmbBlockGridWorkspaceData - extends UmbBlockWorkspaceData<{ - index: number; - parentUnique: string | null; - areaKey?: string; - }> {} +export interface UmbBlockGridWorkspaceOriginData { + index: number; + parentUnique: string | null; + areaKey?: string; +} + +export interface UmbBlockGridWorkspaceData extends UmbBlockWorkspaceData {} export const UMB_BLOCK_GRID_WORKSPACE_MODAL = new UmbModalToken( 'Umb.Modal.Workspace', diff --git a/src/packages/block/block-grid/workspace/views/block-grid-type-workspace-view-areas.element.ts b/src/packages/block/block-grid/workspace/views/block-grid-type-workspace-view-areas.element.ts index e45dc873bc..025fd3e564 100644 --- a/src/packages/block/block-grid/workspace/views/block-grid-type-workspace-view-areas.element.ts +++ b/src/packages/block/block-grid/workspace/views/block-grid-type-workspace-view-areas.element.ts @@ -1,5 +1,5 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbPropertyEditorConfig } from '@umbraco-cms/backoffice/property-editor'; @@ -8,6 +8,9 @@ import { UMB_DATA_TYPE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/data-ty @customElement('umb-block-grid-type-workspace-view-areas') export class UmbBlockGridTypeWorkspaceViewAreasElement extends UmbLitElement implements UmbWorkspaceViewElement { // + @state() + _areaColumnsConfigurationObject?: UmbPropertyEditorConfig; + @state() _areaConfigConfigurationObject?: UmbPropertyEditorConfig; @@ -18,7 +21,8 @@ export class UmbBlockGridTypeWorkspaceViewAreasElement extends UmbLitElement imp this.observe( await context.propertyValueByAlias('gridColumns'), (value) => { - const dataTypeGridColumns = value ? parseInt(value, 10) : undefined; + const dataTypeGridColumns = value ? parseInt(value, 10) : 12; + this._areaColumnsConfigurationObject = [{ alias: 'placeholder', value: dataTypeGridColumns }]; this._areaConfigConfigurationObject = [{ alias: 'defaultAreaGridColumns', value: dataTypeGridColumns }]; }, 'observeGridColumns', @@ -27,21 +31,24 @@ export class UmbBlockGridTypeWorkspaceViewAreasElement extends UmbLitElement imp } render() { - return html` - - - > - - `; + return this._areaConfigConfigurationObject + ? html` + + + > + + ` + : nothing; } static styles = [ diff --git a/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts b/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts index bca25ac505..8ecc6faf3b 100644 --- a/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts +++ b/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts @@ -42,6 +42,9 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper @state() _label = ''; + @state() + _icon?: string; + @state() _workspaceEditContentPath?: string; @@ -65,10 +68,14 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper this._hasSettings = !!settingsElementTypeKey; }); this.observe(this.#context.label, (label) => { - const oldValue = this._label; - this._blockViewProps.label = label; this._label = label; - this.requestUpdate('label', oldValue); + this._blockViewProps.label = label; + this.requestUpdate('_blockViewProps'); + }); + this.observe(this.#context.contentElementTypeIcon, (icon) => { + this._icon = icon; + this._blockViewProps.icon = icon; + this.requestUpdate('_blockViewProps'); }); this.observe(this.#context.inlineEditingMode, (inlineEditingMode) => { this._inlineEditingMode = inlineEditingMode; @@ -115,12 +122,12 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper ${this._showContentEdit && this._workspaceEditContentPath ? html` - ` + ` : ''} ${this._hasSettings && this._workspaceEditSettingsPath ? html` - ` + ` : ''} this.#context.requestDelete()}> diff --git a/src/packages/block/block-list/context/block-list-entries.context.ts b/src/packages/block/block-list/context/block-list-entries.context.ts index b34f6285cf..f668e69560 100644 --- a/src/packages/block/block-list/context/block-list-entries.context.ts +++ b/src/packages/block/block-list/context/block-list-entries.context.ts @@ -1,11 +1,11 @@ import type { UmbBlockDataType } from '../../block/index.js'; import { UMB_BLOCK_CATALOGUE_MODAL, UmbBlockEntriesContext } from '../../block/index.js'; -import type { UmbBlockListWorkspaceData } from '../index.js'; +import { UMB_BLOCK_LIST_WORKSPACE_MODAL, type UmbBlockListWorkspaceData } from '../index.js'; import type { UmbBlockListLayoutModel, UmbBlockListTypeModel } from '../types.js'; -import { UMB_BLOCK_LIST_MANAGER_CONTEXT } from './block-list-manager.context.js'; +import { UMB_BLOCK_LIST_MANAGER_CONTEXT } from './block-list-manager.context-token.js'; import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< typeof UMB_BLOCK_LIST_MANAGER_CONTEXT, @@ -15,6 +15,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< > { // #catalogueModal: UmbModalRouteRegistrationController; + #workspaceModal: UmbModalRouteRegistrationController; // We will just say its always allowed for list for now: [NL] public readonly canCreate = new UmbBooleanState(true).asObservable(); @@ -40,6 +41,17 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< .observeRouteBuilder((routeBuilder) => { this._catalogueRouteBuilderState.setValue(routeBuilder); }); + + this.#workspaceModal = new UmbModalRouteRegistrationController(this, UMB_BLOCK_LIST_WORKSPACE_MODAL) + .addUniquePaths(['propertyAlias', 'variantId']) + .addAdditionalPath('block') + .onSetup(() => { + return { data: { entityType: 'block', preset: {} }, modal: { size: 'medium' } }; + }) + .observeRouteBuilder((routeBuilder) => { + const newPath = routeBuilder({}); + this._workspacePath.setValue(newPath); + }); } protected _gotBlockManager() { @@ -64,6 +76,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< this._manager.propertyAlias, (alias) => { this.#catalogueModal.setUniquePathValue('propertyAlias', alias ?? 'null'); + this.#workspaceModal.setUniquePathValue('propertyAlias', alias ?? 'null'); }, 'observePropertyAlias', ); @@ -71,9 +84,9 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< this.observe( this._manager.variantId, (variantId) => { - if (variantId) { - this.#catalogueModal.setUniquePathValue('variantId', variantId.toString()); - } + // TODO: This might not be the property variant ID, but the content variant ID. Check up on what makes most sense? + this.#catalogueModal.setUniquePathValue('variantId', variantId?.toString()); + this.#workspaceModal.setUniquePathValue('variantId', variantId?.toString()); }, 'observePropertyAlias', ); @@ -87,6 +100,11 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< return this._catalogueRouteBuilderState.getValue()?.({ view: 'clipboard', index: index }); } + async setLayouts(layouts: Array) { + await this._retrieveManager; + this._manager?.setLayouts(layouts); + } + async create( contentElementTypeKey: string, partialLayoutEntry?: Omit, diff --git a/src/packages/block/block-list/context/block-list-entry.context.ts b/src/packages/block/block-list/context/block-list-entry.context.ts index 8756dde673..c040544713 100644 --- a/src/packages/block/block-list/context/block-list-entry.context.ts +++ b/src/packages/block/block-list/context/block-list-entry.context.ts @@ -1,4 +1,4 @@ -import { UMB_BLOCK_LIST_MANAGER_CONTEXT } from './block-list-manager.context.js'; +import { UMB_BLOCK_LIST_MANAGER_CONTEXT } from './block-list-manager.context-token.js'; import { UMB_BLOCK_LIST_ENTRIES_CONTEXT } from './block-list-entries.context-token.js'; import { UmbBlockEntryContext } from '@umbraco-cms/backoffice/block'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; diff --git a/src/packages/block/block-list/context/block-list-manager.context-token.ts b/src/packages/block/block-list/context/block-list-manager.context-token.ts new file mode 100644 index 0000000000..bde5ce5b25 --- /dev/null +++ b/src/packages/block/block-list/context/block-list-manager.context-token.ts @@ -0,0 +1,8 @@ +import type { UmbBlockListManagerContext } from './block-list-manager.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +// TODO: Make discriminator method for this: +export const UMB_BLOCK_LIST_MANAGER_CONTEXT = new UmbContextToken< + UmbBlockListManagerContext, + UmbBlockListManagerContext +>('UmbBlockManagerContext'); diff --git a/src/packages/block/block-list/context/block-list-manager.context.ts b/src/packages/block/block-list/context/block-list-manager.context.ts index cf7b2a3130..5f78892ef2 100644 --- a/src/packages/block/block-list/context/block-list-manager.context.ts +++ b/src/packages/block/block-list/context/block-list-manager.context.ts @@ -1,7 +1,6 @@ import type { UmbBlockListLayoutModel, UmbBlockListTypeModel } from '../types.js'; import type { UmbBlockListWorkspaceData } from '../index.js'; import type { UmbBlockDataType } from '../../block/types.js'; -import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block'; @@ -40,9 +39,3 @@ export class UmbBlockListManagerContext< return true; } } - -// TODO: Make discriminator method for this: -export const UMB_BLOCK_LIST_MANAGER_CONTEXT = new UmbContextToken< - UmbBlockListManagerContext, - UmbBlockListManagerContext ->('UmbBlockManagerContext'); diff --git a/src/packages/block/block-list/context/index.ts b/src/packages/block/block-list/context/index.ts index 235016411e..0471c6a1a7 100644 --- a/src/packages/block/block-list/context/index.ts +++ b/src/packages/block/block-list/context/index.ts @@ -1 +1,2 @@ +export * from './block-list-entries.context-token.js'; export * from './block-list-entry.context-token.js'; diff --git a/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts b/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts index 7c8a9a9297..48befd952c 100644 --- a/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts +++ b/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts @@ -1,5 +1,4 @@ import { UmbBlockListManagerContext } from '../../context/block-list-manager.context.js'; -import '../../components/block-list-entry/index.js'; import type { UmbBlockListEntryElement } from '../../components/block-list-entry/index.js'; import type { UmbBlockListLayoutModel, UmbBlockListValueModel } from '../../types.js'; import { UmbBlockListEntriesContext } from '../../context/block-list-entries.context.js'; @@ -14,11 +13,13 @@ import { } from '@umbraco-cms/backoffice/property-editor'; import type { UmbBlockLayoutBaseModel } from '@umbraco-cms/backoffice/block'; import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; -import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; -import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/modal'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router'; import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; +import '../../components/block-list-entry/index.js'; + const SORTER_CONFIG: UmbSorterConfig = { getUniqueOfElement: (element) => { return element.contentUdi!; @@ -74,7 +75,7 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - const validationLimit = config.getValueByAlias('validationLimit'); + const validationLimit = config.getValueByAlias('validationLimit'); this._limitMin = validationLimit?.min; this._limitMax = validationLimit?.max; @@ -82,20 +83,23 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement const blocks = config.getValueByAlias>('blocks') ?? []; this.#managerContext.setBlockTypes(blocks); - const customCreateButtonLabel = config.getValueByAlias('createLabel'); - if (customCreateButtonLabel) { - this._createButtonLabel = customCreateButtonLabel; - } else if (blocks.length === 1) { - this._createButtonLabel = `${this.localize.term('general_add')} ${blocks[0].label}`; - } - const useInlineEditingAsDefault = config.getValueByAlias('useInlineEditingAsDefault'); this.#managerContext.setInlineEditingMode(useInlineEditingAsDefault); + this.style.maxWidth = config.getValueByAlias('maxPropertyWidth') ?? ''; // TODO: //config.useSingleBlockMode, not done jet - this.style.maxWidth = config.getValueByAlias('maxPropertyWidth') ?? ''; this.#managerContext.setEditorConfiguration(config); + + const customCreateButtonLabel = config.getValueByAlias('createLabel'); + if (customCreateButtonLabel) { + this._createButtonLabel = customCreateButtonLabel; + } else if (blocks.length === 1) { + this.#managerContext.contentTypesLoaded.then(() => { + const firstContentTypeName = this.#managerContext.getContentTypeNameOf(blocks[0].contentElementTypeKey); + this._createButtonLabel = `${this.localize.term('general_add')} ${firstContentTypeName}`; + }); + } } @state() @@ -118,17 +122,6 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement constructor() { super(); - /* - this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => { - this.observe( - propertyContext?.alias, - (alias) => { - this.#catalogueModal.setUniquePathValue('propertyAlias', alias); - }, - 'observePropertyAlias', - ); - }); - */ this.observe(this.#entriesContext.layoutEntries, (layouts) => { this._layouts = layouts; // Update sorter. @@ -140,21 +133,14 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement // TODO: Prevent initial notification from these observes: this.observe(this.#managerContext.layouts, (layouts) => { this._value = { ...this._value, layout: { [UMB_BLOCK_LIST_PROPERTY_EDITOR_ALIAS]: layouts } }; - // Notify that the value has changed. - // TODO: idea: consider inserting an await here, so other changes could appear first? Maybe some mechanism to only fire change event onces? - //this.#entriesContext.setLayoutEntries(layouts); this.#fireChangeEvent(); }); this.observe(this.#managerContext.contents, (contents) => { this._value = { ...this._value, contentData: contents }; - // Notify that the value has changed. - //console.log('content changed', this._value); this.#fireChangeEvent(); }); this.observe(this.#managerContext.settings, (settings) => { this._value = { ...this._value, settingsData: settings }; - // Notify that the value has changed. - //console.log('settings changed', this._value); this.#fireChangeEvent(); }); this.observe(this.#managerContext.blockTypes, (blockTypes) => { @@ -164,34 +150,10 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement this.observe(this.#entriesContext.catalogueRouteBuilder, (routeBuilder) => { this._catalogueRouteBuilder = routeBuilder; }); - - /* - this.#catalogueModal = new UmbModalRouteRegistrationController(this, UMB_BLOCK_CATALOGUE_MODAL) - .addUniquePaths(['propertyAlias']) - .addAdditionalPath(':view/:index') - .onSetup((routingInfo) => { - const index = routingInfo.index ? parseInt(routingInfo.index) : -1; - return { - data: { - blocks: this._blocks ?? [], - openClipboard: routingInfo.view === 'clipboard', - blockOriginData: { index: index }, - }, - }; - }) - .observeRouteBuilder((routeBuilder) => { - this._catalogueRouteBuilder = routeBuilder; - }); - */ } - #debounceChangeEvent?: boolean; - #fireChangeEvent = async () => { - if (this.#debounceChangeEvent) return; - this.#debounceChangeEvent = true; - await Promise.resolve(); + #fireChangeEvent = () => { this.dispatchEvent(new UmbPropertyValueChangeEvent()); - this.#debounceChangeEvent = false; }; render() { @@ -208,6 +170,7 @@ export class UmbPropertyEditorUIBlockListElement extends UmbLitElement implement (x) => x.contentUdi, (layoutEntry, index) => html` `, diff --git a/src/packages/block/block-list/property-editors/block-list-type-configuration/property-editor-ui-block-list-type-configuration.element.ts b/src/packages/block/block-list/property-editors/block-list-type-configuration/property-editor-ui-block-list-type-configuration.element.ts index b49deaecdb..2ba4b23f77 100644 --- a/src/packages/block/block-list/property-editors/block-list-type-configuration/property-editor-ui-block-list-type-configuration.element.ts +++ b/src/packages/block/block-list/property-editors/block-list-type-configuration/property-editor-ui-block-list-type-configuration.element.ts @@ -8,7 +8,8 @@ import { type UmbPropertyEditorConfigCollection, } from '@umbraco-cms/backoffice/property-editor'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; /** * @element umb-property-editor-ui-block-list-type-configuration @@ -23,6 +24,12 @@ export class UmbPropertyEditorUIBlockListBlockConfigurationElement typeof UMB_WORKSPACE_MODAL.VALUE >; + @property({ attribute: false }) + value: UmbBlockTypeBaseModel[] = []; + + @property({ type: Object, attribute: false }) + public config?: UmbPropertyEditorConfigCollection; + @state() private _workspacePath?: string; @@ -41,12 +48,6 @@ export class UmbPropertyEditorUIBlockListBlockConfigurationElement }); } - @property({ attribute: false }) - value: UmbBlockTypeBaseModel[] = []; - - @property({ type: Object, attribute: false }) - public config?: UmbPropertyEditorConfigCollection; - #onCreate(e: CustomEvent) { const selectedElementType = e.detail.contentElementTypeKey; if (selectedElementType) { diff --git a/src/packages/block/block-list/workspace/block-list-workspace.modal-token.ts b/src/packages/block/block-list/workspace/block-list-workspace.modal-token.ts index e86a26f97d..d6ccd489c1 100644 --- a/src/packages/block/block-list/workspace/block-list-workspace.modal-token.ts +++ b/src/packages/block/block-list/workspace/block-list-workspace.modal-token.ts @@ -2,10 +2,11 @@ import type { UmbBlockWorkspaceData } from '@umbraco-cms/backoffice/block'; import type { UmbWorkspaceModalData, UmbWorkspaceModalValue } from '@umbraco-cms/backoffice/modal'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -export interface UmbBlockListWorkspaceData - extends UmbBlockWorkspaceData<{ - index: number; - }> {} +export interface UmbBlockListWorkspaceOriginData { + index: number; +} + +export interface UmbBlockListWorkspaceData extends UmbBlockWorkspaceData {} export const UMB_BLOCK_LIST_WORKSPACE_MODAL = new UmbModalToken( 'Umb.Modal.Workspace', diff --git a/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry-inline.element.ts b/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry-inline.element.ts new file mode 100644 index 0000000000..f2e59b38d8 --- /dev/null +++ b/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry-inline.element.ts @@ -0,0 +1,26 @@ +import { UmbBlockRteEntryElement } from './block-rte-entry.element.js'; +import { css, customElement } from '@umbraco-cms/backoffice/external/lit'; + +/** + * Implementation for the inline component, inherited code from the umb-rte-block element. + * @element umb-rte-block-inline + */ +@customElement('umb-rte-block-inline') +export class UmbBlockRteEntryInlineElement extends UmbBlockRteEntryElement { + static styles = [ + ...UmbBlockRteEntryElement.styles, + css` + :host { + display: inline-block; + } + `, + ]; +} + +export default UmbBlockRteEntryInlineElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-rte-block-inline': UmbBlockRteEntryInlineElement; + } +} diff --git a/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts b/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts new file mode 100644 index 0000000000..0eef218b1c --- /dev/null +++ b/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts @@ -0,0 +1,166 @@ +import { UmbBlockRteEntryContext } from '../../context/block-rte-entry.context.js'; +import type { UmbBlockRteLayoutModel } from '../../types.js'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { html, css, property, state, customElement } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; +import '../ref-rte-block/index.js'; +import type { UmbBlockViewPropsType } from '@umbraco-cms/backoffice/block'; + +/** + * @element umb-rte-block + * @element umb-rte-block-inline + */ +@customElement('umb-rte-block') +export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropertyEditorUiElement { + // + @property({ type: String, attribute: 'data-content-udi', reflect: true }) + public get contentUdi(): string | undefined { + return this._contentUdi; + } + public set contentUdi(value: string | undefined) { + if (!value) return; + this._contentUdi = value; + this.#context.setContentUdi(value); + } + private _contentUdi?: string | undefined; + + #context = new UmbBlockRteEntryContext(this); + + @state() + _showContentEdit = false; + @state() + _hasSettings = false; + + @state() + _label = ''; + + @state() + _icon?: string; + + @state() + _workspaceEditContentPath?: string; + + @state() + _workspaceEditSettingsPath?: string; + + // TODO: use this type on the Element Interface for the Manifest. + @state() + _blockViewProps: UmbBlockViewPropsType = { contentUdi: undefined!, urls: {} }; // Set to undefined cause it will be set before we render. + + constructor() { + super(); + + // We do not have index for RTE Blocks at the moment. + this.#context.setIndex(0); + + this.observe(this.#context.showContentEdit, (showContentEdit) => { + this._showContentEdit = showContentEdit; + }); + this.observe(this.#context.settingsElementTypeKey, (settingsElementTypeKey) => { + this._hasSettings = !!settingsElementTypeKey; + }); + this.observe(this.#context.label, (label) => { + this._label = label; + this._blockViewProps.label = label; + this.requestUpdate('_blockViewProps'); + }); + this.observe(this.#context.contentElementTypeIcon, (icon) => { + this._icon = icon; + this._blockViewProps.icon = icon; + this.requestUpdate('_blockViewProps'); + }); + // Data props: + this.observe(this.#context.layout, (layout) => { + this._blockViewProps.layout = layout; + }); + this.observe(this.#context.content, (content) => { + this._blockViewProps.content = content; + }); + this.observe(this.#context.settings, (settings) => { + this._blockViewProps.settings = settings; + }); + this.observe(this.#context.workspaceEditContentPath, (path) => { + this._workspaceEditContentPath = path; + this._blockViewProps.urls.editContent = path; + this.requestUpdate('_blockViewProps'); + }); + this.observe(this.#context.workspaceEditSettingsPath, (path) => { + this._workspaceEditSettingsPath = path; + this._blockViewProps.urls.editSettings = path; + this.requestUpdate('_blockViewProps'); + }); + } + + connectedCallback() { + super.connectedCallback(); + + // eslint-disable-next-line wc/no-self-class + this.classList.add('uui-font'); + // eslint-disable-next-line wc/no-self-class + this.classList.add('uui-text'); + + this.setAttribute('contenteditable', 'false'); + } + + #renderRefBlock() { + return html``; + } + + #renderBlock() { + return html` + ${this.#renderRefBlock()} + + ${this._showContentEdit && this._workspaceEditContentPath + ? html` + + ` + : ''} + ${this._hasSettings && this._workspaceEditSettingsPath + ? html` + + ` + : ''} + this.#context.requestDelete()}> + + + + `; + } + + render() { + return this.#renderBlock(); + } + + static styles = [ + css` + :host { + position: relative; + display: block; + user-select: none; + user-drag: auto; + } + uui-action-bar { + position: absolute; + top: var(--uui-size-2); + right: var(--uui-size-2); + } + + :host([drag-placeholder]) { + opacity: 0.2; + } + `, + ]; +} + +export default UmbBlockRteEntryElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-rte-block': UmbBlockRteEntryElement; + } +} diff --git a/src/packages/block/block-rte/components/block-rte-entry/index.ts b/src/packages/block/block-rte/components/block-rte-entry/index.ts new file mode 100644 index 0000000000..991ba3eea7 --- /dev/null +++ b/src/packages/block/block-rte/components/block-rte-entry/index.ts @@ -0,0 +1,2 @@ +export * from './block-rte-entry.element.js'; +export * from './block-rte-entry-inline.element.js'; diff --git a/src/packages/block/block-rte/components/index.ts b/src/packages/block/block-rte/components/index.ts new file mode 100644 index 0000000000..0f97db6dc9 --- /dev/null +++ b/src/packages/block/block-rte/components/index.ts @@ -0,0 +1 @@ +export * from './block-rte-entry/index.js'; diff --git a/src/packages/block/block-rte/components/ref-rte-block/index.ts b/src/packages/block/block-rte/components/ref-rte-block/index.ts new file mode 100644 index 0000000000..761e1b077d --- /dev/null +++ b/src/packages/block/block-rte/components/ref-rte-block/index.ts @@ -0,0 +1 @@ +export * from './ref-rte-block.element.js'; diff --git a/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts b/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts new file mode 100644 index 0000000000..16fc522e7c --- /dev/null +++ b/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts @@ -0,0 +1,55 @@ +import { UMB_BLOCK_ENTRY_CONTEXT } from '@umbraco-cms/backoffice/block'; +import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +/** + * @element umb-ref-rte-block + */ +@customElement('umb-ref-rte-block') +export class UmbRefRteBlockElement extends UmbLitElement { + // + @property({ type: String }) + label?: string; + + @property({ type: String }) + icon?: string; + + @state() + _workspaceEditPath?: string; + + constructor() { + super(); + + this.consumeContext(UMB_BLOCK_ENTRY_CONTEXT, (context) => { + this.observe( + context.workspaceEditContentPath, + (workspaceEditPath) => { + this._workspaceEditPath = workspaceEditPath; + }, + 'observeWorkspaceEditPath', + ); + }); + } + + render() { + return html``; + } + + static styles = [ + css` + uui-ref-node { + min-height: var(--uui-size-16); + } + `, + ]; +} + +export default UmbRefRteBlockElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-ref-rte-block': UmbRefRteBlockElement; + } +} diff --git a/src/packages/block/block-rte/context/block-rte-entries.context-token.ts b/src/packages/block/block-rte/context/block-rte-entries.context-token.ts new file mode 100644 index 0000000000..bda7bbbaa5 --- /dev/null +++ b/src/packages/block/block-rte/context/block-rte-entries.context-token.ts @@ -0,0 +1,5 @@ +import type { UmbBlockRteEntriesContext } from './block-rte-entries.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +// TODO: Make discriminator method for this: +export const UMB_BLOCK_RTE_ENTRIES_CONTEXT = new UmbContextToken('UmbBlockEntriesContext'); diff --git a/src/packages/block/block-rte/context/block-rte-entries.context.ts b/src/packages/block/block-rte/context/block-rte-entries.context.ts new file mode 100644 index 0000000000..d6e425370f --- /dev/null +++ b/src/packages/block/block-rte/context/block-rte-entries.context.ts @@ -0,0 +1,136 @@ +import type { UmbBlockDataType } from '../../block/index.js'; +import { UMB_BLOCK_CATALOGUE_MODAL, UmbBlockEntriesContext } from '../../block/index.js'; +import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js'; +import { + UMB_BLOCK_RTE_WORKSPACE_MODAL, + type UmbBlockRteWorkspaceData, +} from '../workspace/block-rte-workspace.modal-token.js'; +import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from './block-rte-manager.context-token.js'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; + +export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext< + typeof UMB_BLOCK_RTE_MANAGER_CONTEXT, + typeof UMB_BLOCK_RTE_MANAGER_CONTEXT.TYPE, + UmbBlockRteTypeModel, + UmbBlockRteLayoutModel +> { + // + #catalogueModal: UmbModalRouteRegistrationController; + #workspaceModal: UmbModalRouteRegistrationController; + + // We will just say its always allowed for RTE for now: [NL] + public readonly canCreate = new UmbBooleanState(true).asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_BLOCK_RTE_MANAGER_CONTEXT); + + this.#catalogueModal = new UmbModalRouteRegistrationController(this, UMB_BLOCK_CATALOGUE_MODAL) + .addUniquePaths(['propertyAlias', 'variantId']) + .addAdditionalPath(':view') + .onSetup((routingInfo) => { + return { + data: { + blocks: this._manager?.getBlockTypes() ?? [], + blockGroups: [], + openClipboard: routingInfo.view === 'clipboard', + blockOriginData: {}, + }, + }; + }) + .observeRouteBuilder((routeBuilder) => { + this._catalogueRouteBuilderState.setValue(routeBuilder); + }); + + this.#workspaceModal = new UmbModalRouteRegistrationController(this, UMB_BLOCK_RTE_WORKSPACE_MODAL) + .addUniquePaths(['propertyAlias', 'variantId']) + .addAdditionalPath('block') + .onSetup(() => { + return { data: { entityType: 'block', preset: {} }, modal: { size: 'medium' } }; + }) + .observeRouteBuilder((routeBuilder) => { + const newPath = routeBuilder({}); + this._workspacePath.setValue(newPath); + }); + } + + protected _gotBlockManager() { + if (!this._manager) return; + + this.observe( + this._manager.layouts, + (layouts) => { + this._layoutEntries.setValue(layouts); + }, + 'observeParentLayouts', + ); + this.observe( + this.layoutEntries, + (layouts) => { + this._manager?.setLayouts(layouts); + }, + 'observeThisLayouts', + ); + + this.observe( + this._manager.propertyAlias, + (alias) => { + this.#catalogueModal.setUniquePathValue('propertyAlias', alias ?? 'null'); + this.#workspaceModal.setUniquePathValue('propertyAlias', alias ?? 'null'); + }, + 'observePropertyAlias', + ); + + this.observe( + this._manager.variantId, + (variantId) => { + // TODO: This might not be the property variant ID, but the content variant ID. Check up on what makes most sense? + this.#catalogueModal.setUniquePathValue('variantId', variantId?.toString()); + this.#workspaceModal.setUniquePathValue('variantId', variantId?.toString()); + }, + 'observePropertyAlias', + ); + } + + getPathForCreateBlock() { + return this._catalogueRouteBuilderState.getValue()?.({ view: 'create' }); + } + + getPathForClipboard() { + return this._catalogueRouteBuilderState.getValue()?.({ view: 'clipboard' }); + } + + async setLayouts(layouts: Array) { + await this._retrieveManager; + this._manager?.setLayouts(layouts); + } + + async create( + contentElementTypeKey: string, + partialLayoutEntry?: Omit, + modalData?: UmbBlockRteWorkspaceData, + ) { + await this._retrieveManager; + return this._manager?.create(contentElementTypeKey, partialLayoutEntry, modalData); + } + + // insert Block? + + async insert( + layoutEntry: UmbBlockRteLayoutModel, + content: UmbBlockDataType, + settings: UmbBlockDataType | undefined, + modalData: UmbBlockRteWorkspaceData, + ) { + await this._retrieveManager; + return this._manager?.insert(layoutEntry, content, settings, modalData) ?? false; + } + + // create Block? + async delete(contentUdi: string) { + // TODO: Loop through children and delete them as well? + await super.delete(contentUdi); + this._manager?.deleteLayoutElement(contentUdi); + } +} diff --git a/src/packages/block/block-rte/context/block-rte-entry.context-token.ts b/src/packages/block/block-rte/context/block-rte-entry.context-token.ts new file mode 100644 index 0000000000..9bf50d97a4 --- /dev/null +++ b/src/packages/block/block-rte/context/block-rte-entry.context-token.ts @@ -0,0 +1,5 @@ +import type { UmbBlockRteEntryContext } from './block-rte-entry.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +// TODO: Make discriminator method for this: +export const UMB_BLOCK_RTE_ENTRY_CONTEXT = new UmbContextToken('UmbBlockEntryContext'); diff --git a/src/packages/block/block-rte/context/block-rte-entry.context.ts b/src/packages/block/block-rte/context/block-rte-entry.context.ts new file mode 100644 index 0000000000..ce54601d05 --- /dev/null +++ b/src/packages/block/block-rte/context/block-rte-entry.context.ts @@ -0,0 +1,49 @@ +import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js'; +import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from './block-rte-manager.context-token.js'; +import { UMB_BLOCK_RTE_ENTRIES_CONTEXT } from './block-rte-entries.context-token.js'; +import { UmbBlockEntryContext } from '@umbraco-cms/backoffice/block'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +export class UmbBlockRteEntryContext extends UmbBlockEntryContext< + typeof UMB_BLOCK_RTE_MANAGER_CONTEXT, + typeof UMB_BLOCK_RTE_MANAGER_CONTEXT.TYPE, + typeof UMB_BLOCK_RTE_ENTRIES_CONTEXT, + typeof UMB_BLOCK_RTE_ENTRIES_CONTEXT.TYPE, + UmbBlockRteTypeModel, + UmbBlockRteLayoutModel +> { + readonly displayInline = this._layout.asObservablePart((x) => (x ? x.displayInline ?? false : undefined)); + readonly displayInlineConfig = this._blockType.asObservablePart((x) => (x ? x.displayInline ?? false : undefined)); + + readonly forceHideContentEditorInOverlay = this._blockType.asObservablePart( + (x) => !!x?.forceHideContentEditorInOverlay, + ); + + readonly showContentEdit = this._blockType.asObservablePart((x) => !x?.forceHideContentEditorInOverlay); + + constructor(host: UmbControllerHost) { + super(host, UMB_BLOCK_RTE_MANAGER_CONTEXT, UMB_BLOCK_RTE_ENTRIES_CONTEXT); + } + + _gotManager() {} + + _gotEntries() { + // Secure displayInline fits configuration: + this.observe( + observeMultiple([this.displayInline, this.displayInlineConfig]), + ([displayInline, displayInlineConfig]) => { + if (displayInlineConfig !== undefined && displayInline !== undefined && displayInlineConfig !== displayInline) { + const layoutValue = this._layout.getValue(); + if (!layoutValue) return; + this._layout.setValue({ + ...layoutValue, + displayInline: displayInlineConfig, + }); + } + }, + 'displayInlineCorrection', + ); + } + + _gotContentType() {} +} diff --git a/src/packages/block/block-rte/context/block-rte-manager.context-token.ts b/src/packages/block/block-rte/context/block-rte-manager.context-token.ts new file mode 100644 index 0000000000..430263d86c --- /dev/null +++ b/src/packages/block/block-rte/context/block-rte-manager.context-token.ts @@ -0,0 +1,7 @@ +import type { UmbBlockRteManagerContext } from './block-rte-manager.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +// TODO: Make discriminator method for this: +export const UMB_BLOCK_RTE_MANAGER_CONTEXT = new UmbContextToken( + 'UmbBlockManagerContext', +); diff --git a/src/packages/block/block-rte/context/block-rte-manager.context.ts b/src/packages/block/block-rte/context/block-rte-manager.context.ts new file mode 100644 index 0000000000..db584d17be --- /dev/null +++ b/src/packages/block/block-rte/context/block-rte-manager.context.ts @@ -0,0 +1,92 @@ +import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js'; +import type { UmbBlockRteWorkspaceData } from '../index.js'; +import type { UmbBlockDataType } from '../../block/types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tinymce'; +import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block'; + +import '../components/block-rte-entry/index.js'; + +/** + * A implementation of the Block Manager specifically for the Rich Text Editor. + */ +export class UmbBlockRteManagerContext< + BlockLayoutType extends UmbBlockRteLayoutModel = UmbBlockRteLayoutModel, +> extends UmbBlockManagerContext { + // + #editor?: Editor; + + setTinyMceEditor(editor: Editor) { + this.#editor = editor; + } + + getTinyMceEditor() { + return this.#editor; + } + + removeOneLayout(contentUdi: string) { + this._layouts.removeOne(contentUdi); + } + + getLayouts(): Array { + return this._layouts.getValue(); + } + + create( + contentElementTypeKey: string, + partialLayoutEntry?: Omit, + modalData?: UmbBlockRteWorkspaceData, + ) { + const data = super.createBlockData(contentElementTypeKey, partialLayoutEntry); + + // Find block type. + const blockType = this.getBlockTypes().find((x) => x.contentElementTypeKey === contentElementTypeKey); + if (!blockType) { + throw new Error(`Cannot create block, missing block type for ${contentElementTypeKey}`); + } + + if (blockType.displayInline) { + data.layout.displayInline = true; + } + + return data; + } + + insert( + layoutEntry: BlockLayoutType, + content: UmbBlockDataType, + settings: UmbBlockDataType | undefined, + modalData: UmbBlockRteWorkspaceData, + ) { + if (!this.#editor) return false; + + this._layouts.appendOne(layoutEntry); + + this.insertBlockData(layoutEntry, content, settings, modalData); + + if (layoutEntry.displayInline) { + this.#editor.selection.setContent( + ``, + ); + } else { + this.#editor.selection.setContent( + ``, + ); + } + + this.#editor.fire('change'); + + return true; + } + + /** @internal */ + public deleteLayoutElement(contentUdi: string) { + if (!this.#editor) return; + + const blockElementsOfThisUdi = this.#editor.dom.select( + `umb-rte-block[data-content-udi='${contentUdi}'], umb-rte-block-inline[data-content-udi='${contentUdi}']`, + ); + blockElementsOfThisUdi.forEach((blockElement) => { + this.#editor?.dom.remove(blockElement); + }); + } +} diff --git a/src/packages/block/block-rte/context/index.ts b/src/packages/block/block-rte/context/index.ts new file mode 100644 index 0000000000..7148ec48b7 --- /dev/null +++ b/src/packages/block/block-rte/context/index.ts @@ -0,0 +1,5 @@ +export * from './block-rte-entries.context-token.js'; +export * from './block-rte-entries.context.js'; +export * from './block-rte-entry.context-token.js'; +export * from './block-rte-manager.context.js'; +export * from './block-rte-manager.context-token.js'; diff --git a/src/packages/block/block-rte/index.ts b/src/packages/block/block-rte/index.ts index 60f06132a7..5ce24a84b0 100644 --- a/src/packages/block/block-rte/index.ts +++ b/src/packages/block/block-rte/index.ts @@ -1 +1,4 @@ +export * from './components/index.js'; +export * from './context/index.js'; export * from './workspace/index.js'; +export * from './types.js'; diff --git a/src/packages/block/block-rte/manifests.ts b/src/packages/block/block-rte/manifests.ts index 71c165c901..151385f7c1 100644 --- a/src/packages/block/block-rte/manifests.ts +++ b/src/packages/block/block-rte/manifests.ts @@ -1,4 +1,5 @@ +import { manifests as tinyMcePluginManifests } from './tiny-mce-plugin/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [...workspaceManifests]; +export const manifests: Array = [...tinyMcePluginManifests, ...workspaceManifests]; diff --git a/src/packages/block/block-rte/tiny-mce-plugin/manifests.ts b/src/packages/block/block-rte/tiny-mce-plugin/manifests.ts new file mode 100644 index 0000000000..b549d60854 --- /dev/null +++ b/src/packages/block/block-rte/tiny-mce-plugin/manifests.ts @@ -0,0 +1,17 @@ +export const manifests = [ + { + type: 'tinyMcePlugin', + alias: 'Umb.TinyMcePlugin.BlockPicker', + name: 'Block Picker TinyMCE Plugin', + js: () => import('./tiny-mce-block-picker.plugin.js'), + meta: { + toolbar: [ + { + alias: 'umbblockpicker', + label: '#blockEditor_insertBlock', + icon: 'visualblocks', + }, + ], + }, + }, +]; diff --git a/src/packages/block/block-rte/tiny-mce-plugin/tiny-mce-block-picker.plugin.ts b/src/packages/block/block-rte/tiny-mce-plugin/tiny-mce-block-picker.plugin.ts new file mode 100644 index 0000000000..fd7e3dc831 --- /dev/null +++ b/src/packages/block/block-rte/tiny-mce-plugin/tiny-mce-block-picker.plugin.ts @@ -0,0 +1,76 @@ +import { UMB_BLOCK_RTE_ENTRIES_CONTEXT } from '../context/block-rte-entries.context-token.js'; +import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from '../context/block-rte-manager.context-token.js'; +import { type TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/tiny-mce'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; + +export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase { + #localize = new UmbLocalizationController(this._host); + + private _blocks?: Array; + #entriesContext?: typeof UMB_BLOCK_RTE_ENTRIES_CONTEXT.TYPE; + + constructor(args: TinyMcePluginArguments) { + super(args); + + args.editor.ui.registry.addToggleButton('umbblockpicker', { + icon: 'visualblocks', + tooltip: this.#localize.term('blockEditor_insertBlock'), + onAction: () => this.showDialog(), + onSetup: function (api) { + const changed = args.editor.selection.selectorChangedWithUnbind( + 'umb-rte-block[data-content-udi], umb-rte-block-inline[data-content-udi]', + (state) => api.setActive(state), + ); + return () => changed.unbind(); + }, + }); + + this.consumeContext(UMB_BLOCK_RTE_MANAGER_CONTEXT, (context) => { + context.setTinyMceEditor(args.editor); + + this.observe( + context.blockTypes, + (blockTypes) => { + this._blocks = blockTypes; + }, + 'blockType', + ); + }); + this.consumeContext(UMB_BLOCK_RTE_ENTRIES_CONTEXT, (context) => { + this.#entriesContext = context; + }); + } + + async showDialog() { + const blockEl = this.editor.selection.getNode(); + + /*if (blockEl.nodeName === 'UMB-RTE-BLOCK' || blockEl.nodeName === 'UMB-RTE-BLOCK-INLINE') { + const blockUdi = blockEl.getAttribute('data-content-udi') ?? undefined; + if (blockUdi) { + // TODO: Missing a solution to edit a block from this scope. [NL] + this.#editBlock(blockUdi); + return; + } + }*/ + + // If no block is selected, open the block picker: + this.#createBlock(); + } + + #createBlock() { + // TODO: Missing solution to skip catalogue if only one type available. [NL] + let createPath: string | undefined = undefined; + + if (this._blocks?.length === 1) { + const elementKey = this._blocks[0].contentElementTypeKey; + createPath = this.#entriesContext?.getPathForCreateBlock() + 'modal/umb-modal-workspace/create/' + elementKey; + } else { + createPath = this.#entriesContext?.getPathForCreateBlock(); + } + + if (createPath) { + window.history.pushState({}, '', createPath); + } + } +} diff --git a/src/packages/block/block-rte/types.ts b/src/packages/block/block-rte/types.ts new file mode 100644 index 0000000000..45404b976e --- /dev/null +++ b/src/packages/block/block-rte/types.ts @@ -0,0 +1,13 @@ +import type { UmbBlockTypeBaseModel } from '../block-type/index.js'; +import type { UmbBlockLayoutBaseModel, UmbBlockValueType } from '@umbraco-cms/backoffice/block'; + +export const UMB_BLOCK_RTE_TYPE = 'block-rte-type'; + +export interface UmbBlockRteTypeModel extends UmbBlockTypeBaseModel { + displayInline: boolean; +} +export interface UmbBlockRteLayoutModel extends UmbBlockLayoutBaseModel { + displayInline?: boolean; +} + +export interface UmbBlockRteValueModel extends UmbBlockValueType {} diff --git a/src/packages/block/block-rte/workspace/block-rte-workspace.modal-token.ts b/src/packages/block/block-rte/workspace/block-rte-workspace.modal-token.ts index da51f16a31..f93502f052 100644 --- a/src/packages/block/block-rte/workspace/block-rte-workspace.modal-token.ts +++ b/src/packages/block/block-rte/workspace/block-rte-workspace.modal-token.ts @@ -2,19 +2,16 @@ import type { UmbBlockWorkspaceData } from '@umbraco-cms/backoffice/block'; import type { UmbWorkspaceModalData, UmbWorkspaceModalValue } from '@umbraco-cms/backoffice/modal'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -export interface UmbBlockRTEWorkspaceData - extends UmbBlockWorkspaceData<{ - index: number; - }> {} +export interface UmbBlockRteWorkspaceData extends UmbBlockWorkspaceData {} -export const UMB_BLOCK_RTE_WORKSPACE_MODAL = new UmbModalToken( +export const UMB_BLOCK_RTE_WORKSPACE_MODAL = new UmbModalToken( 'Umb.Modal.Workspace', { modal: { type: 'sidebar', size: 'medium', }, - data: { entityType: 'block', preset: {}, originData: { index: -1 } }, + data: { entityType: 'block', preset: {}, originData: {} }, // Recast the type, so the entityType data prop is not required: }, ) as UmbModalToken, UmbWorkspaceModalValue>; diff --git a/src/packages/block/block-rte/workspace/views/block-rte-type-workspace-view.element.ts b/src/packages/block/block-rte/workspace/views/block-rte-type-workspace-view.element.ts index cbc331568e..530c8ff152 100644 --- a/src/packages/block/block-rte/workspace/views/block-rte-type-workspace-view.element.ts +++ b/src/packages/block/block-rte/workspace/views/block-rte-type-workspace-view.element.ts @@ -7,20 +7,15 @@ import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension- export class UmbBlockRteTypeWorkspaceViewSettingsElement extends UmbLitElement implements UmbWorkspaceViewElement { render() { return html` -
for RTE blocks
- + label="Display Inline" + alias="displayInline" + property-editor-ui-alias="Umb.PropertyEditorUi.Toggle"> - + ${this.iconFile + ? html`` + : html``} `; diff --git a/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts b/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts index ef77263298..b51cbbbe46 100644 --- a/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts +++ b/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts @@ -1,15 +1,20 @@ import type { UmbBlockTypeCardElement } from '../block-type-card/index.js'; import type { UmbBlockTypeBaseModel, UmbBlockTypeWithGroupKey } from '../../types.js'; -import { UmbModalRouteRegistrationController, umbConfirmModal } from '@umbraco-cms/backoffice/modal'; -import '../block-type-card/index.js'; +import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property'; import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; import { UmbDeleteEvent } from '@umbraco-cms/backoffice/event'; -import { UMB_DOCUMENT_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/document-type'; +import { + UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT, + UMB_DOCUMENT_TYPE_PICKER_MODAL, +} from '@umbraco-cms/backoffice/document-type'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; +import '../block-type-card/index.js'; + /** TODO: Look into sending a "change" event when there is a change, rather than create, delete, and change event. Make sure it doesn't break move for RTE/List/Grid. [LI] */ @customElement('umb-input-block-type') export class UmbInputBlockTypeElement< @@ -123,10 +128,13 @@ export class UmbInputBlockTypeElement< } async #onRequestDelete(item: BlockType) { + const store = await this.getContext(UMB_DOCUMENT_TYPE_ITEM_STORE_CONTEXT); + const contentType = store.getItems([item.contentElementTypeKey]); await umbConfirmModal(this, { color: 'danger', - headline: `Remove [TODO: Get name]?`, - content: 'Are you sure you want to remove this block type?', + headline: `Remove ${contentType[0]?.name}?`, + // TODO: Translations: [NL] + content: 'Are you sure you want to remove this Block Type Configuration?', confirmLabel: 'Remove', }); this.deleteItem(item.contentElementTypeKey); @@ -143,6 +151,7 @@ export class UmbInputBlockTypeElement< (propertyAlias: string) { diff --git a/src/packages/block/block/context/block-entries.context.ts b/src/packages/block/block/context/block-entries.context.ts index 0430a35b12..c16929d7b3 100644 --- a/src/packages/block/block/context/block-entries.context.ts +++ b/src/packages/block/block/context/block-entries.context.ts @@ -1,13 +1,13 @@ import type { UmbBlockDataType, UmbBlockLayoutBaseModel } from '../types.js'; -import { UMB_BLOCK_WORKSPACE_MODAL } from '../workspace/block-workspace.modal-token.js'; -import type { UmbBlockDataObjectModel, UmbBlockManagerContext } from './block-manager.context.js'; +import type { UmbBlockWorkspaceData } from '../workspace/block-workspace.modal-token.js'; import { UMB_BLOCK_ENTRIES_CONTEXT } from './block-entries.context-token.js'; +import type { UmbBlockDataObjectModel, UmbBlockManagerContext } from './block-manager.context.js'; +import { type Observable, UmbArrayState, UmbBasicState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { type Observable, UmbArrayState, UmbBasicState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; -import { type UmbModalRouteBuilder, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router'; export abstract class UmbBlockEntriesContext< BlockManagerContextTokenType extends UmbContextToken, @@ -21,13 +21,11 @@ export abstract class UmbBlockEntriesContext< _manager?: BlockManagerContextType; _retrieveManager; - _workspaceModal: UmbModalRouteRegistrationController; - protected _catalogueRouteBuilderState = new UmbBasicState(undefined); readonly catalogueRouteBuilder = this._catalogueRouteBuilderState.asObservable(); - #workspacePath = new UmbStringState(undefined); - workspacePath = this.#workspacePath.asObservable(); + protected _workspacePath = new UmbStringState(undefined); + workspacePath = this._workspacePath.asObservable(); public abstract readonly canCreate: Observable; @@ -42,34 +40,7 @@ export abstract class UmbBlockEntriesContext< this._retrieveManager = this.consumeContext(blockManagerContextToken, (blockGridManager) => { this._manager = blockGridManager; this._gotBlockManager(); - - this.observe( - this._manager.propertyAlias, - (alias) => { - this._workspaceModal.setUniquePathValue('propertyAlias', alias); - }, - 'observePropertyAlias', - ); - this.observe( - this._manager.variantId, - (variantId) => { - // TODO: This might not be the property variant ID, but the content variant ID. Check up on what makes most sense? - this._workspaceModal.setUniquePathValue('variantId', variantId?.toString()); - }, - 'observePropertyVariantId', - ); }).asPromise(); - - this._workspaceModal = new UmbModalRouteRegistrationController(this, UMB_BLOCK_WORKSPACE_MODAL) - .addUniquePaths(['propertyAlias', 'variantId']) - .addAdditionalPath('block') - .onSetup(() => { - return { data: { entityType: 'block', preset: {} }, modal: { size: 'medium' } }; - }) - .observeRouteBuilder((routeBuilder) => { - const newPath = routeBuilder({}); - this.#workspacePath.setValue(newPath); - }); } async getManager() { @@ -84,6 +55,9 @@ export abstract class UmbBlockEntriesContext< layoutOf(contentUdi: string) { return this._layoutEntries.asObservablePart((source) => source.find((x) => x.contentUdi === contentUdi)); } + getLayoutOf(contentUdi: string) { + return this._layoutEntries.getValue().find((x) => x.contentUdi === contentUdi); + } setLayouts(layouts: Array) { return this._layoutEntries.setValue(layouts); } @@ -97,14 +71,14 @@ export abstract class UmbBlockEntriesContext< public abstract create( contentElementTypeKey: string, layoutEntry?: Omit, - modalData?: typeof UMB_BLOCK_WORKSPACE_MODAL.DATA, + modalData?: UmbBlockWorkspaceData, ): Promise | undefined>; abstract insert( layoutEntry: BlockLayoutType, content: UmbBlockDataType, settings: UmbBlockDataType | undefined, - modalData: typeof UMB_BLOCK_WORKSPACE_MODAL.DATA, + modalData: UmbBlockWorkspaceData, ): Promise; //edit? //editSettings @@ -118,10 +92,9 @@ export abstract class UmbBlockEntriesContext< } if (layout.settingsUdi) { - this._manager?.removeOneSettings(layout.settingsUdi); + this._manager!.removeOneSettings(layout.settingsUdi); } - - this._manager?.removeOneContent(contentUdi); + this._manager!.removeOneContent(contentUdi); this._layoutEntries.removeOne(contentUdi); } diff --git a/src/packages/block/block/context/block-entry.context.ts b/src/packages/block/block/context/block-entry.context.ts index 1517afa740..422391fe62 100644 --- a/src/packages/block/block/context/block-entry.context.ts +++ b/src/packages/block/block/context/block-entry.context.ts @@ -9,6 +9,7 @@ import { UmbNumberState, UmbObjectState, UmbStringState, + mergeObservables, observeMultiple, } from '@umbraco-cms/backoffice/observable-api'; import { encodeFilePath } from '@umbraco-cms/backoffice/utils'; @@ -61,33 +62,16 @@ export abstract class UmbBlockEntryContext< this.#index.setValue(index); } - #createPath = new UmbStringState(undefined); - readonly createPath = this.#createPath.asObservable(); + #createBeforePath = new UmbStringState(undefined); + readonly createBeforePath = this.#createBeforePath.asObservable(); + #createAfterPath = new UmbStringState(undefined); + readonly createAfterPath = this.#createAfterPath.asObservable(); - #contentElementTypeName = new UmbStringState(undefined); - public readonly contentElementTypeName = this.#contentElementTypeName.asObservable(); - #contentElementTypeAlias = new UmbStringState(undefined); - public readonly contentElementTypeAlias = this.#contentElementTypeAlias.asObservable(); - - // TODO: index state + observable? - - #label = new UmbStringState(''); - public readonly label = this.#label.asObservable(); - - #generateWorkspaceEditContentPath = (path?: string) => - path ? path + 'edit/' + encodeFilePath(this.getContentUdi() ?? '') + '/view/content' : ''; - - #generateWorkspaceEditSettingsPath = (path?: string) => - path ? path + 'edit/' + encodeFilePath(this.getContentUdi() ?? '') + '/view/settings' : ''; - - #workspacePath = new UmbStringState(undefined); - public readonly workspacePath = this.#workspacePath.asObservable(); - public readonly workspaceEditContentPath = this.#workspacePath.asObservablePart( - this.#generateWorkspaceEditContentPath, - ); - public readonly workspaceEditSettingsPath = this.#workspacePath.asObservablePart( - this.#generateWorkspaceEditSettingsPath, - ); + #contentElementType = new UmbObjectState(undefined); + public readonly contentElementType = this.#contentElementType.asObservable(); + public readonly contentElementTypeName = this.#contentElementType.asObservablePart((x) => x?.name); + public readonly contentElementTypeAlias = this.#contentElementType.asObservablePart((x) => x?.alias); + public readonly contentElementTypeIcon = this.#contentElementType.asObservablePart((x) => x?.icon); _blockType = new UmbObjectState(undefined); public readonly blockType = this._blockType.asObservable(); @@ -102,6 +86,26 @@ export abstract class UmbBlockEntryContext< public readonly contentUdi = this._layout.asObservablePart((x) => x?.contentUdi); public readonly unique = this._layout.asObservablePart((x) => x?.contentUdi); + #label = new UmbStringState(''); + public readonly label = this.#label.asObservable(); + + #generateWorkspaceEditContentPath = (path?: string, contentUdi?: string) => + path && contentUdi ? path + 'edit/' + encodeFilePath(contentUdi) + '/view/content' : ''; + + #generateWorkspaceEditSettingsPath = (path?: string, contentUdi?: string) => + path && contentUdi ? path + 'edit/' + encodeFilePath(contentUdi) + '/view/settings' : ''; + + #workspacePath = new UmbStringState(undefined); + public readonly workspacePath = this.#workspacePath.asObservable(); + public readonly workspaceEditContentPath = mergeObservables( + [this.contentUdi, this.workspacePath], + ([contentUdi, path]) => this.#generateWorkspaceEditContentPath(path, contentUdi), + ); + public readonly workspaceEditSettingsPath = mergeObservables( + [this.contentUdi, this.workspacePath], + ([contentUdi, path]) => this.#generateWorkspaceEditSettingsPath(path, contentUdi), + ); + #content = new UmbObjectState(undefined); public readonly content = this.#content.asObservable(); public readonly contentTypeKey = this.#content.asObservablePart((x) => x?.contentTypeKey); @@ -173,7 +177,7 @@ export abstract class UmbBlockEntryContext< }); this.observe(this.index, () => { - this.#updateCreatePath(); + this.#updateCreatePaths(); }); } @@ -181,16 +185,18 @@ export abstract class UmbBlockEntryContext< return this._layout.value?.contentUdi; } - #updateCreatePath() { + #updateCreatePaths() { const index = this.#index.value; if (this._entries && index !== undefined) { this.observe( observeMultiple([this._entries.catalogueRouteBuilder, this._entries.canCreate]), ([catalogueRouteBuilder, canCreate]) => { if (catalogueRouteBuilder && canCreate) { - this.#createPath.setValue(this._entries!.getPathForCreateBlock(index)); + this.#createBeforePath.setValue(this._entries!.getPathForCreateBlock(index)); + this.#createAfterPath.setValue(this._entries!.getPathForCreateBlock(index + 1)); } else { - this.#createPath.setValue(undefined); + this.#createBeforePath.setValue(undefined); + this.#createAfterPath.setValue(undefined); } }, 'observeRouteBuilderCreate', @@ -204,7 +210,7 @@ export abstract class UmbBlockEntryContext< this.observe( this._entries.layoutOf(this.#contentUdi), (layout) => { - this._layout.setValue(this._gotLayout(layout)); + this._layout.setValue(layout); }, 'observeParentLayout', ); @@ -219,10 +225,6 @@ export abstract class UmbBlockEntryContext< ); } - protected _gotLayout(layout: BlockLayoutType | undefined) { - return layout; - } - #gotManager() { this.#observeBlockType(); this.#observeData(); @@ -231,7 +233,7 @@ export abstract class UmbBlockEntryContext< abstract _gotManager(): void; #gotEntries() { - this.#updateCreatePath(); + this.#updateCreatePaths(); this.#observeLayout(); if (this._entries) { this.observe( @@ -249,13 +251,11 @@ export abstract class UmbBlockEntryContext< abstract _gotEntries(): void; #observeData() { - if (!this._manager) return; - const contentUdi = this._layout.value?.contentUdi; - if (!contentUdi) return; + if (!this._manager || !this.#contentUdi) return; // observe content: this.observe( - this._manager.contentOf(contentUdi), + this._manager.contentOf(this.#contentUdi), (content) => { this.#content.setValue(content); }, @@ -302,8 +302,9 @@ export abstract class UmbBlockEntryContext< this.observe( this._manager.contentTypeOf(contentTypeKey), (contentType) => { - this.#contentElementTypeAlias.setValue(contentType?.alias); - this.#contentElementTypeName.setValue(contentType?.name); + //this.#contentElementTypeAlias.setValue(contentType?.alias); + //this.#contentElementTypeName.setValue(contentType?.name); + this.#contentElementType.setValue(contentType); this._gotContentType(contentType); }, 'observeContentElementType', @@ -354,16 +355,18 @@ export abstract class UmbBlockEntryContext< //activate public edit() { - window.location.href = this.#generateWorkspaceEditContentPath(this.#workspacePath.value); + window.location.href = this.#generateWorkspaceEditContentPath(this.#workspacePath.value, this.getContentUdi()); } public editSettings() { - window.location.href = this.#generateWorkspaceEditSettingsPath(this.#workspacePath.value); + window.location.href = this.#generateWorkspaceEditSettingsPath(this.#workspacePath.value, this.getContentUdi()); } async requestDelete() { + const blockName = this.getLabel(); + // TODO: Localizations missing [NL] await umbConfirmModal(this, { - headline: `Delete ${this.getLabel()}`, - content: 'Are you sure you want to delete this [INSERT BLOCK TYPE NAME]?', + headline: `Delete ${blockName}`, + content: `Are you sure you want to delete this ${blockName}?`, confirmLabel: 'Delete', color: 'danger', }); diff --git a/src/packages/block/block/context/block-manager.context.ts b/src/packages/block/block/context/block-manager.context.ts index 03d5ec7a0e..ac57bd8a24 100644 --- a/src/packages/block/block/context/block-manager.context.ts +++ b/src/packages/block/block/context/block-manager.context.ts @@ -26,6 +26,10 @@ export abstract class UmbBlockManagerContext< BlockLayoutType extends UmbBlockLayoutBaseModel = UmbBlockLayoutBaseModel, > extends UmbContextBase { // + get contentTypesLoaded() { + return Promise.all(this.#contentTypeRequests); + } + #contentTypeRequests: Array> = []; #contentTypeRepository = new UmbDocumentTypeDetailRepository(this); #propertyAlias = new UmbStringState(undefined); @@ -64,6 +68,9 @@ export abstract class UmbBlockManagerContext< setEditorConfiguration(configs: UmbPropertyEditorConfigCollection) { this._editorConfiguration.setValue(configs); } + getEditorConfiguration(): UmbPropertyEditorConfigCollection | undefined { + return this._editorConfiguration.getValue(); + } setBlockTypes(blockTypes: Array) { this.#blockTypes.setValue(blockTypes); @@ -115,7 +122,9 @@ export abstract class UmbBlockManagerContext< async #ensureContentType(unique: string) { if (this.#contentTypes.getValue().find((x) => x.unique === unique)) return; - const { data } = await this.#contentTypeRepository.requestByUnique(unique); + const contentTypeRequest = this.#contentTypeRepository.requestByUnique(unique); + this.#contentTypeRequests.push(contentTypeRequest); + const { data } = await contentTypeRequest; if (!data) { this.#contentTypes.removeOne(unique); return; @@ -132,6 +141,9 @@ export abstract class UmbBlockManagerContext< contentTypeNameOf(contentTypeKey: string) { return this.#contentTypes.asObservablePart((source) => source.find((x) => x.unique === contentTypeKey)?.name); } + getContentTypeNameOf(contentTypeKey: string) { + return this.#contentTypes.getValue().find((x) => x.unique === contentTypeKey)?.name; + } blockTypeOf(contentTypeKey: string) { return this.#blockTypes.asObservablePart((source) => source.find((x) => x.contentElementTypeKey === contentTypeKey), @@ -155,9 +167,9 @@ export abstract class UmbBlockManagerContext< return this.#contents.value.find((x) => x.udi === contentUdi); } - /*setOneLayout(layoutData: BlockLayoutType) { - return this._layouts.appendOne(layoutData); - }*/ + setOneLayout(layoutData: BlockLayoutType, modalData?: UmbBlockWorkspaceData) { + this._layouts.appendOne(layoutData); + } setOneContent(contentData: UmbBlockDataType) { this.#contents.appendOne(contentData); } @@ -178,10 +190,7 @@ export abstract class UmbBlockManagerContext< modalData?: UmbBlockWorkspaceData, ): UmbBlockDataObjectModel | undefined; - protected createBlockData( - contentElementTypeKey: string, - partialLayoutEntry?: Omit, - ) { + protected createBlockData(contentElementTypeKey: string, partialLayoutEntry?: Omit) { // Find block type. const blockType = this.#blockTypes.value.find((x) => x.contentElementTypeKey === contentElementTypeKey); if (!blockType) { diff --git a/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts b/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts index a84acce3f0..25ff3b0d14 100644 --- a/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts +++ b/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts @@ -3,11 +3,9 @@ import type { UmbBlockTypeGroup, UmbBlockTypeWithGroupKey } from '@umbraco-cms/b import type { UmbBlockCatalogueModalData, UmbBlockCatalogueModalValue } from '@umbraco-cms/backoffice/block'; import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; -import { - UMB_MODAL_CONTEXT, - UmbModalBaseElement, - UmbModalRouteRegistrationController, -} from '@umbraco-cms/backoffice/modal'; +import { UMB_MODAL_CONTEXT, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; + // TODO: This is across packages, how should we go about getting just a single element from another package? like here we just need the umb-block-type-card element import '@umbraco-cms/backoffice/block-type'; diff --git a/src/packages/block/block/types.ts b/src/packages/block/block/types.ts index 7f1289a083..a35e8e7038 100644 --- a/src/packages/block/block/types.ts +++ b/src/packages/block/block/types.ts @@ -22,6 +22,7 @@ export interface UmbBlockViewUrlsPropType { export interface UmbBlockViewPropsType { label?: string; + icon?: string; contentUdi: string; layout?: BlockLayoutType; content?: UmbBlockDataType; diff --git a/src/packages/block/block/workspace/block-element-manager.ts b/src/packages/block/block/workspace/block-element-manager.ts index afdfb4321b..d0c3ad6c42 100644 --- a/src/packages/block/block/workspace/block-element-manager.ts +++ b/src/packages/block/block/workspace/block-element-manager.ts @@ -65,9 +65,30 @@ export class UmbBlockElementManager extends UmbControllerBase { } async setPropertyValue(alias: string, value: unknown) { + this.initiatePropertyValueChange(); await this.#getDataPromise; this.#data.update({ [alias]: value }); + this.finishPropertyValueChange(); + } + + #updateLock = 0; + initiatePropertyValueChange() { + this.#updateLock++; + this.#data.mute(); + // TODO: When ready enable this code will enable handling a finish automatically by this implementation 'using myState.initiatePropertyValueChange()' (Relies on TS support of Using) [NL] + /*return { + [Symbol.dispose]: this.finishPropertyValueChange, + };*/ + } + finishPropertyValueChange = () => { + this.#updateLock--; + this.#triggerPropertyValueChanges(); + }; + #triggerPropertyValueChanges() { + if (this.#updateLock === 0) { + this.#data.unmute(); + } } public createPropertyDatasetContext(host: UmbControllerHost) { diff --git a/src/packages/block/block/workspace/block-workspace.context.ts b/src/packages/block/block/workspace/block-workspace.context.ts index 43e85a2c5f..867bfb06d2 100644 --- a/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/packages/block/block/workspace/block-workspace.context.ts @@ -242,7 +242,7 @@ export class UmbBlockWorkspaceContext { if (layoutData) { - this.#blockEntries?.setOneLayout(layoutData); + this.#blockManager?.setOneLayout(layoutData, this.#modalContext?.data as UmbBlockWorkspaceData); } }); this.observe(this.content.data, (contentData) => { @@ -324,7 +324,7 @@ export class UmbBlockWorkspaceContext { + const linkKey = await this.#makeLinkTokenRequest(provider); + + const form = document.createElement('form'); + form.method = 'POST'; + form.action = this.#link_endpoint; + form.style.display = 'none'; + + const providerInput = document.createElement('input'); + providerInput.name = 'provider'; + providerInput.value = provider; + form.appendChild(providerInput); + + const linkKeyInput = document.createElement('input'); + linkKeyInput.name = 'linkKey'; + linkKeyInput.value = linkKey; + form.appendChild(linkKeyInput); + + document.body.appendChild(form); + form.submit(); + } + + /** + * This method will unlink the current user from the specified provider. + */ + async unlinkLogin(loginProvider: string, providerKey: string): Promise { + const token = await this.performWithFreshTokens(); + const request = new Request(this.#unlink_endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify({ loginProvider, providerKey }), + }); + + const result = await fetch(request); + + if (!result.ok) { + const error = await result.json(); + throw error; + } + + await this.signOut(); + + return true; + } + /** * Save the current token response to local storage. */ @@ -384,4 +442,21 @@ export class UmbAuthFlow { return false; } } + + async #makeLinkTokenRequest(provider: string) { + const token = await this.performWithFreshTokens(); + + const request = await fetch(`${this.#link_key_endpoint}?provider=${provider}`, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!request.ok) { + throw new Error('Failed to link login'); + } + + return request.json(); + } } diff --git a/src/packages/core/auth/auth.context.ts b/src/packages/core/auth/auth.context.ts index f4a418727d..ee4fa9e5f8 100644 --- a/src/packages/core/auth/auth.context.ts +++ b/src/packages/core/auth/auth.context.ts @@ -12,12 +12,8 @@ export class UmbAuthContext extends UmbContextBase { #isAuthorized = new UmbBooleanState(false); // Timeout is different from `isAuthorized` because it can occur repeatedly #isTimeout = new Subject(); - /** - * Observable that emits true when the auth context is initialized. - * @remark It will only emit once and then complete itself. - */ #isInitialized = new ReplaySubject(1); - #isBypassed = false; + #isBypassed; #serverUrl; #backofficePath; #authFlow; @@ -25,6 +21,12 @@ export class UmbAuthContext extends UmbContextBase { #authWindowProxy?: WindowProxy | null; #previousAuthUrl?: string; + /** + * Observable that emits true when the auth context is initialized. + * @remark It will only emit once and then complete itself. + */ + readonly isInitialized = this.#isInitialized.asObservable(); + /** * Observable that emits true if the user is authorized, otherwise false. * @remark It will only emit when the authorization state changes. @@ -251,25 +253,55 @@ export class UmbAuthContext extends UmbContextBase { withCredentials: OpenAPI.WITH_CREDENTIALS, credentials: OpenAPI.CREDENTIALS, token: () => this.getLatestToken(), + encodePath: OpenAPI.ENCODE_PATH, }; } + /** + * Sets the auth context as initialized, which means that the auth context is ready to be used. + * @remark This is used to let the app context know that the core module is ready, which means that the core auth providers are available. + */ setInitialized() { this.#isInitialized.next(); this.#isInitialized.complete(); } + /** + * Gets all registered auth providers. + */ getAuthProviders(extensionsRegistry: UmbBackofficeExtensionRegistry) { return this.#isInitialized.pipe( switchMap(() => extensionsRegistry.byType<'authProvider', ManifestAuthProvider>('authProvider')), ); } + /** + * Gets the authorized redirect url. + * @returns The redirect url, which is the backoffice path. + */ getRedirectUrl() { return `${window.location.origin}${this.#backofficePath}${this.#backofficePath.endsWith('/') ? '' : '/'}oauth_complete`; } + /** + * Gets the post logout redirect url. + * @returns The post logout redirect url, which is the backoffice path with the logout path appended. + */ getPostLogoutRedirectUrl() { return `${window.location.origin}${this.#backofficePath}${this.#backofficePath.endsWith('/') ? '' : '/'}logout`; } + + /** + * @see UmbAuthFlow#linkLogin + */ + linkLogin(provider: string) { + return this.#authFlow.linkLogin(provider); + } + + /** + * @see UmbAuthFlow#unlinkLogin + */ + unlinkLogin(providerName: string, providerKey: string) { + return this.#authFlow.unlinkLogin(providerName, providerKey); + } } diff --git a/src/packages/core/auth/components/auth-provider-default.element.ts b/src/packages/core/auth/components/auth-provider-default.element.ts index eaa9cfe801..1a7bdec3fb 100644 --- a/src/packages/core/auth/components/auth-provider-default.element.ts +++ b/src/packages/core/auth/components/auth-provider-default.element.ts @@ -18,19 +18,25 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA this.setAttribute('part', 'auth-provider-default'); } + get #label() { + const label = this.manifest.meta?.label ?? this.manifest.forProviderName; + const labelLocalized = this.localize.string(label); + return this.localize.term('login_signInWith', labelLocalized); + } + render() { return html` this.onSubmit(this.manifest)} id="auth-provider-button" - .label=${this.manifest.meta?.label ?? this.manifest.forProviderName} + .label=${this.#label} .look=${this.manifest.meta?.defaultView?.look ?? 'outline'} .color=${this.manifest.meta?.defaultView?.color ?? 'default'}> ${this.manifest.meta?.defaultView?.icon - ? html`` + ? html`` : nothing} - ${this.manifest.meta?.label ?? this.manifest.forProviderName} + ${this.#label} `; } @@ -45,6 +51,10 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA #auth-provider-button { width: 100%; } + + #icon { + margin-right: var(--uui-size-space-1); + } `, ]; } diff --git a/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/packages/core/auth/modals/umb-app-auth-modal.element.ts index 98d3934121..96c445e42b 100644 --- a/src/packages/core/auth/modals/umb-app-auth-modal.element.ts +++ b/src/packages/core/auth/modals/umb-app-auth-modal.element.ts @@ -44,7 +44,7 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement