diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 781e2e69fd..767c0848cf 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -593,8 +593,8 @@ stages: workingDirectory: tests/Umbraco.Tests.AcceptanceTest # Install Playwright and dependencies - - pwsh: npx playwright install --with-deps - displayName: Install Playwright + - pwsh: npx playwright install chromium + displayName: Install Playwright only with Chromium browser workingDirectory: tests/Umbraco.Tests.AcceptanceTest # Test @@ -639,7 +639,7 @@ stages: testResultsFormat: 'JUnit' testResultsFiles: '*.xml' searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results" - testRunTitle: "$(Agent.JobName)" + testRunTitle: "$(Agent.JobName)" - job: displayName: E2E Tests (SQL Server) @@ -760,8 +760,8 @@ stages: workingDirectory: tests/Umbraco.Tests.AcceptanceTest # Install Playwright and dependencies - - pwsh: npx playwright install --with-deps - displayName: Install Playwright + - pwsh: npx playwright install chromium + displayName: Install Playwright only with Chromium browser workingDirectory: tests/Umbraco.Tests.AcceptanceTest # Test @@ -806,7 +806,7 @@ stages: inputs: targetPath: $(Build.ArtifactStagingDirectory) artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)" - + # Publish test results - task: PublishTestResults@2 displayName: "Publish test results" diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs index 62f0699342..869bc4c880 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs @@ -69,11 +69,6 @@ public class PublishDocumentWithDescendantsController : DocumentControllerBase publishBranchFilter |= PublishBranchFilter.IncludeUnpublished; } - if (requestModel.ForceRepublish) - { - publishBranchFilter |= PublishBranchFilter.ForceRepublish; - } - return publishBranchFilter; } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 73c031040e..e2f18da0cd 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -42851,7 +42851,6 @@ "PublishDocumentWithDescendantsRequestModel": { "required": [ "cultures", - "forceRepublish", "includeUnpublishedDescendants" ], "type": "object", @@ -42859,9 +42858,6 @@ "includeUnpublishedDescendants": { "type": "boolean" }, - "forceRepublish": { - "type": "boolean" - }, "cultures": { "type": "array", "items": { diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs index 2557adf9a4..bfd9aa0e3b 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs @@ -4,7 +4,5 @@ public class PublishDocumentWithDescendantsRequestModel { public bool IncludeUnpublishedDescendants { get; set; } - public bool ForceRepublish { get; set; } - public required IEnumerable Cultures { get; set; } } diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 8ff77729b0..f07bbccdb5 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -12,6 +12,9 @@ public class PackageManifest public bool AllowTelemetry { get; set; } = true; + [Obsolete("Use AllowTelemetry instead. This property will be removed in future versions.")] + public bool AllowPackageTelemetry { get; set; } = true; + public required object[] Extensions { get; set; } public PackageManifestImportmap? Importmap { get; set; } diff --git a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs index 5b10221578..a5ad0d84e8 100644 --- a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs +++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs @@ -29,7 +29,7 @@ public interface ITwoFactorLoginService : IService /// The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by /// the provider. /// - [Obsolete("Use IUserTwoFactorLoginService.GetSetupInfoWithStatusAsync. This will be removed in Umbraco 15.")] + [Obsolete("Use IUserTwoFactorLoginService.GetSetupInfoAsync. This will be removed in Umbraco 15.")] Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName); /// @@ -60,13 +60,13 @@ public interface ITwoFactorLoginService : IService /// /// Disables 2FA with Code. /// - [Obsolete("Use IUserTwoFactorLoginService.DisableByCodeWithStatusAsync. This will be removed in Umbraco 15.")] + [Obsolete("Use IUserTwoFactorLoginService.DisableByCodeAsync. This will be removed in Umbraco 15.")] Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code); /// /// Validates and Saves. /// - [Obsolete("Use IUserTwoFactorLoginService.ValidateAndSaveWithStatusAsync. This will be removed in Umbraco 15.")] + [Obsolete("Use IUserTwoFactorLoginService.ValidateAndSaveAsync. This will be removed in Umbraco 15.")] Task ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code); } diff --git a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs index 166e7e35a0..2e3a3a5776 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs @@ -353,7 +353,7 @@ public class PackagingService : IPackagingService } // Set additional values - installedPackage.AllowPackageTelemetry = packageManifest.AllowTelemetry; + installedPackage.AllowPackageTelemetry = packageManifest is { AllowTelemetry: true, AllowPackageTelemetry: true }; if (!string.IsNullOrEmpty(packageManifest.Version)) { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 55a836720f..ca5c5f5289 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -173,8 +173,8 @@ "generate:icons": "node ./devops/icons/index.js", "generate:overrides": "node ./devops/tsc/index.js", "generate:jsonschema:imports": "node ./devops/json-schema-generator/index.js", - "generate:jsonschema:dist": "typescript-json-schema --required --include \"./src/json-schema/umbraco-package-schema.ts\" --out dist-cms/umbraco-package-schema.json tsconfig.json UmbracoPackage", - "generate:jsonschema": "typescript-json-schema --required --include \"./src/json-schema/umbraco-package-schema.ts\"", + "generate:jsonschema:dist": "npm run generate:jsonschema -- --out dist-cms/umbraco-package-schema.json tsconfig.json UmbracoPackage", + "generate:jsonschema": "typescript-json-schema --skipLibCheck --ignoreErrors --excludePrivate --required --include \"./src/json-schema/umbraco-package-schema.ts\"", "generate:check-const-test": "node ./devops/generate-check-const-test/index.js", "lint:errors": "npm run lint -- --quiet", "lint:fix": "npm run lint -- --fix", diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index f8452c5332..16ffe924be 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -298,9 +298,6 @@ export default { removeTextBox: 'Fjern denne tekstboks', contentRoot: 'Indholdsrod', includeUnpublished: 'Inkluder ikke-udgivet indhold.', - forceRepublish: 'Udgiv uændrede elementer.', - forceRepublishWarning: 'ADVARSEL: Udgivelse af alle sider under denne i indholdstræet, uanset om de er ændret eller ej, kan være en ressourcekrævende og langvarig proces.', - forceRepublishAdvisory: 'Dette bør ikke være nødvendigt under normale omstændigheder, så fortsæt kun med denne handling, hvis du er sikker på, at det er nødvendigt.', isSensitiveValue: 'Denne værdi er skjult.Hvis du har brug for adgang til at se denne værdi, bedes du\n kontakte din web-administrator.\n ', isSensitiveValue_short: 'Denne værdi er skjult.', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index b22f92f872..d864c5ce7f 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -320,9 +320,6 @@ export default { removeTextBox: 'Remove this text box', contentRoot: 'Content root', includeUnpublished: 'Include unpublished content items.', - forceRepublish: 'Publish unchanged items.', - forceRepublishWarning: 'WARNING: Publishing all pages below this one in the content tree, whether or not they have changed, can be an expensive and long-running operation.', - forceRepublishAdvisory: 'This should not be necessary in normal circumstances so please only proceed with this option selected if you are certain it is required.', isSensitiveValue: 'This value is hidden. If you need access to view this value please contact your\n website administrator.\n ', isSensitiveValue_short: 'This value is hidden.', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index def637a8b3..84ffec33a5 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -317,9 +317,6 @@ export default { removeTextBox: 'Remove this text box', contentRoot: 'Content root', includeUnpublished: 'Include unpublished content items.', - forceRepublish: 'Publish unchanged items.', - forceRepublishWarning: 'WARNING: Publishing all pages below this one in the content tree, whether or not they have changed, can be an expensive and long-running operation.', - forceRepublishAdvisory: 'This should not be necessary in normal circumstances so please only proceed with this option selected if you are certain it is required.', isSensitiveValue: 'This value is hidden. If you need access to view this value please contact your\n website administrator.\n ', isSensitiveValue_short: 'This value is hidden.', diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts index b1a16ccdb4..5e831e2da3 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts @@ -2050,7 +2050,6 @@ export type PublishDocumentRequestModel = { export type PublishDocumentWithDescendantsRequestModel = { includeUnpublishedDescendants: boolean; - forceRepublish: boolean; cultures: Array<(string)>; }; diff --git a/src/Umbraco.Web.UI.Client/src/external/monaco-editor/index.ts b/src/Umbraco.Web.UI.Client/src/external/monaco-editor/index.ts index 4397dee0f9..c4dbacf95f 100644 --- a/src/Umbraco.Web.UI.Client/src/external/monaco-editor/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/monaco-editor/index.ts @@ -6,18 +6,18 @@ import styles from 'monaco-editor/min/vs/editor/editor.main.css?inline'; const initializeWorkers = () => { self.MonacoEnvironment = { getWorker(workerId: string, label: string): Promise | Worker { - let url = '/umbraco/backoffice/monaco-editor/esm/vs/editor/editor.worker.js'; + let url = '/umbraco/backoffice/monaco-editor/vs/editor/editor.worker.js'; if (label === 'json') { - url = '/umbraco/backoffice/monaco-editor/esm/vs/language/json/json.worker.js'; + url = '/umbraco/backoffice/monaco-editor/vs/language/json/json.worker.js'; } if (label === 'css' || label === 'scss' || label === 'less') { - url = '/umbraco/backoffice/monaco-editor/esm/vs/language/css/css.worker.js'; + url = '/umbraco/backoffice/monaco-editor/vs/language/css/css.worker.js'; } if (label === 'html' || label === 'handlebars' || label === 'razor') { - url = '/umbraco/backoffice/monaco-editor/esm/vs/language/html/html.worker.js'; + url = '/umbraco/backoffice/monaco-editor/vs/language/html/html.worker.js'; } if (label === 'typescript' || label === 'javascript') { - url = '/umbraco/backoffice/monaco-editor/esm/vs/language/typescript/ts.worker.js'; + url = '/umbraco/backoffice/monaco-editor/vs/language/typescript/ts.worker.js'; } return new Worker(url, { name: workerId, type: 'module' }); }, diff --git a/src/Umbraco.Web.UI.Client/src/external/tinymce/index.ts b/src/Umbraco.Web.UI.Client/src/external/tinymce/index.ts index 3d2e9a2f41..3d2a1e8fd6 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tinymce/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tinymce/index.ts @@ -25,6 +25,9 @@ export function renderEditor(userConfig?: RawEditorOptions) { // Declare a global variable to hold the TinyMCE instance declare global { interface Window { + /** + * @TJS-ignore + */ tinymce: TinyMCE; } } diff --git a/src/Umbraco.Web.UI.Client/src/json-schema/all-packages.ts b/src/Umbraco.Web.UI.Client/src/json-schema/all-packages.ts deleted file mode 100644 index 2f8a997a00..0000000000 --- a/src/Umbraco.Web.UI.Client/src/json-schema/all-packages.ts +++ /dev/null @@ -1,117 +0,0 @@ -import '@umbraco-cms/backoffice/app'; -import '@umbraco-cms/backoffice/class-api'; -import '@umbraco-cms/backoffice/context-api'; -import '@umbraco-cms/backoffice/controller-api'; -import '@umbraco-cms/backoffice/element-api'; -import '@umbraco-cms/backoffice/embedded-media'; -import '@umbraco-cms/backoffice/extension-api'; -import '@umbraco-cms/backoffice/formatting-api'; -import '@umbraco-cms/backoffice/localization-api'; -import '@umbraco-cms/backoffice/observable-api'; -import '@umbraco-cms/backoffice/action'; -import '@umbraco-cms/backoffice/audit-log'; -import '@umbraco-cms/backoffice/auth'; -import '@umbraco-cms/backoffice/block-custom-view'; -import '@umbraco-cms/backoffice/block-grid'; -import '@umbraco-cms/backoffice/block-list'; -import '@umbraco-cms/backoffice/block-rte'; -import '@umbraco-cms/backoffice/block-type'; -import '@umbraco-cms/backoffice/block'; -import '@umbraco-cms/backoffice/code-editor'; -import '@umbraco-cms/backoffice/collection'; -import '@umbraco-cms/backoffice/components'; -import '@umbraco-cms/backoffice/content-type'; -import '@umbraco-cms/backoffice/content'; -import '@umbraco-cms/backoffice/culture'; -import '@umbraco-cms/backoffice/current-user'; -import '@umbraco-cms/backoffice/dashboard'; -import '@umbraco-cms/backoffice/data-type'; -import '@umbraco-cms/backoffice/debug'; -import '@umbraco-cms/backoffice/dictionary'; -import '@umbraco-cms/backoffice/document-blueprint'; -import '@umbraco-cms/backoffice/document-type'; -import '@umbraco-cms/backoffice/document'; -import '@umbraco-cms/backoffice/entity-action'; -import '@umbraco-cms/backoffice/entity-bulk-action'; -import '@umbraco-cms/backoffice/entity-create-option-action'; -import '@umbraco-cms/backoffice/entity'; -import '@umbraco-cms/backoffice/event'; -import '@umbraco-cms/backoffice/extension-registry'; -import '@umbraco-cms/backoffice/health-check'; -import '@umbraco-cms/backoffice/help'; -import '@umbraco-cms/backoffice/icon'; -import '@umbraco-cms/backoffice/id'; -import '@umbraco-cms/backoffice/imaging'; -import '@umbraco-cms/backoffice/language'; -import '@umbraco-cms/backoffice/lit-element'; -import '@umbraco-cms/backoffice/localization'; -import '@umbraco-cms/backoffice/log-viewer'; -import '@umbraco-cms/backoffice/markdown-editor'; -import '@umbraco-cms/backoffice/media-type'; -import '@umbraco-cms/backoffice/media'; -import '@umbraco-cms/backoffice/member-group'; -import '@umbraco-cms/backoffice/member-type'; -import '@umbraco-cms/backoffice/member'; -import '@umbraco-cms/backoffice/menu'; -import '@umbraco-cms/backoffice/modal'; -import '@umbraco-cms/backoffice/multi-url-picker'; -import '@umbraco-cms/backoffice/notification'; -import '@umbraco-cms/backoffice/object-type'; -import '@umbraco-cms/backoffice/package'; -import '@umbraco-cms/backoffice/partial-view'; -import '@umbraco-cms/backoffice/picker-input'; -import '@umbraco-cms/backoffice/picker'; -import '@umbraco-cms/backoffice/property-action'; -import '@umbraco-cms/backoffice/property-editor'; -import '@umbraco-cms/backoffice/property-type'; -import '@umbraco-cms/backoffice/property'; -import '@umbraco-cms/backoffice/recycle-bin'; -import '@umbraco-cms/backoffice/relation-type'; -import '@umbraco-cms/backoffice/relations'; -import '@umbraco-cms/backoffice/repository'; -import '@umbraco-cms/backoffice/resources'; -import '@umbraco-cms/backoffice/router'; -import '@umbraco-cms/backoffice/rte'; -import '@umbraco-cms/backoffice/script'; -import '@umbraco-cms/backoffice/search'; -import '@umbraco-cms/backoffice/section'; -import '@umbraco-cms/backoffice/server-file-system'; -import '@umbraco-cms/backoffice/settings'; -import '@umbraco-cms/backoffice/sorter'; -import '@umbraco-cms/backoffice/static-file'; -import '@umbraco-cms/backoffice/store'; -import '@umbraco-cms/backoffice/style'; -import '@umbraco-cms/backoffice/stylesheet'; -import '@umbraco-cms/backoffice/sysinfo'; -import '@umbraco-cms/backoffice/tags'; -import '@umbraco-cms/backoffice/template'; -import '@umbraco-cms/backoffice/temporary-file'; -import '@umbraco-cms/backoffice/themes'; -import '@umbraco-cms/backoffice/tiny-mce'; -import '@umbraco-cms/backoffice/tiptap'; -import '@umbraco-cms/backoffice/translation'; -import '@umbraco-cms/backoffice/tree'; -import '@umbraco-cms/backoffice/ufm'; -import '@umbraco-cms/backoffice/user-change-password'; -import '@umbraco-cms/backoffice/user-group'; -import '@umbraco-cms/backoffice/user-permission'; -import '@umbraco-cms/backoffice/user'; -import '@umbraco-cms/backoffice/utils'; -import '@umbraco-cms/backoffice/validation'; -import '@umbraco-cms/backoffice/variant'; -import '@umbraco-cms/backoffice/webhook'; -import '@umbraco-cms/backoffice/workspace'; -import '@umbraco-cms/backoffice/external/backend-api'; -import '@umbraco-cms/backoffice/external/base64-js'; -import '@umbraco-cms/backoffice/external/diff'; -import '@umbraco-cms/backoffice/external/dompurify'; -import '@umbraco-cms/backoffice/external/lit'; -import '@umbraco-cms/backoffice/external/marked'; -import '@umbraco-cms/backoffice/external/monaco-editor'; -import '@umbraco-cms/backoffice/external/openid'; -import '@umbraco-cms/backoffice/external/router-slot'; -import '@umbraco-cms/backoffice/external/rxjs'; -import '@umbraco-cms/backoffice/external/tinymce'; -import '@umbraco-cms/backoffice/external/tiptap'; -import '@umbraco-cms/backoffice/external/uui'; -import '@umbraco-cms/backoffice/external/uuid'; diff --git a/src/Umbraco.Web.UI.Client/src/json-schema/umbraco-package-schema.ts b/src/Umbraco.Web.UI.Client/src/json-schema/umbraco-package-schema.ts index 9bd0fa2930..fae9b07e65 100644 --- a/src/Umbraco.Web.UI.Client/src/json-schema/umbraco-package-schema.ts +++ b/src/Umbraco.Web.UI.Client/src/json-schema/umbraco-package-schema.ts @@ -1,4 +1,4 @@ -import './all-packages.js'; +import '@umbraco-cms/backoffice/extension-registry'; /** * Umbraco package manifest JSON @@ -27,6 +27,13 @@ export interface UmbracoPackage { */ allowTelemetry?: boolean; + /** + * @title Decides if the package sends telemetry data for collection + * @default true + * @deprecated Use allowTelemetry instead + */ + allowPackageTelemetry?: boolean; + /** * @title Decides if the package is allowed to be accessed by the public, e.g. on the login screen * @default false diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/move/move-to.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/move/move-to.action.ts index e6921c7c19..239ddb10b3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/move/move-to.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/move/move-to.action.ts @@ -16,6 +16,7 @@ export class UmbMoveToEntityAction extends UmbEntityActionBase treeItem.unique !== this.args.unique, }, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.token.ts index 4d98367e34..7bb443bad8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.token.ts @@ -1,4 +1,4 @@ -import type { UmbTreeStartNode } from '../types.js'; +import type { UmbTreeItemModel, UmbTreeStartNode } from '../types.js'; import { UMB_TREE_PICKER_MODAL_ALIAS } from './constants.js'; import type { UmbPickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal'; import type { UmbWorkspaceModalData } from '@umbraco-cms/backoffice/workspace'; @@ -14,7 +14,7 @@ export interface UmbTreePickerModalCreateActionData extends UmbPickerModalData { hideTreeRoot?: boolean; @@ -28,7 +28,7 @@ export interface UmbTreePickerModalData< // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface UmbTreePickerModalValue extends UmbPickerModalValue {} -export const UMB_TREE_PICKER_MODAL = new UmbModalToken, UmbTreePickerModalValue>( +export const UMB_TREE_PICKER_MODAL = new UmbModalToken( UMB_TREE_PICKER_MODAL_ALIAS, { modal: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts index 0705b49b74..f5f84f3290 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts @@ -5,7 +5,7 @@ import type { UmbDocumentPublishWithDescendantsModalValue, } from './document-publish-with-descendants-modal.token.js'; import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit'; -import { umbConfirmModal, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; @@ -18,7 +18,6 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE > { #selectionManager = new UmbSelectionManager(this); #includeUnpublishedDescendants = false; - #forceRepublish = false; @state() _options: Array = []; @@ -84,25 +83,11 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE this.#includeUnpublishedDescendants = !this.#includeUnpublishedDescendants; } - async #onForceRepublishChange() { - this.#forceRepublish = !this.#forceRepublish; - } - async #submit() { - if (this.#forceRepublish) { - await umbConfirmModal(this, { - headline: this.localize.term('content_forceRepublishWarning'), - content: this.localize.term('content_forceRepublishAdvisory'), - color: 'warning', - confirmLabel: this.localize.term('actions_publish'), - }); - } - this.value = { selection: this.#selectionManager.getSelection(), includeUnpublishedDescendants: this.#includeUnpublishedDescendants, - forceRepublish: this.#forceRepublish, }; this.modalContext?.submit(); } @@ -143,14 +128,6 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE @change=${this.#onIncludeUnpublishedDescendantsChange}> - - - -
, includeUnpublishedDescendants: boolean, - forceRepublish: boolean, ) { if (!id) throw new Error('id is missing'); if (!variantIds) throw new Error('variant IDs are missing'); @@ -88,7 +86,6 @@ export class UmbDocumentPublishingRepository extends UmbRepositoryBase { id, variantIds, includeUnpublishedDescendants, - forceRepublish, ); if (!error) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts index dcb0c72d76..d4a12593df 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts @@ -92,21 +92,18 @@ export class UmbDocumentPublishingServerDataSource { * @param unique * @param variantIds * @param includeUnpublishedDescendants - * @param forceRepublish * @memberof UmbDocumentPublishingServerDataSource */ async publishWithDescendants( unique: string, variantIds: Array, includeUnpublishedDescendants: boolean, - forceRepublish: boolean, ) { if (!unique) throw new Error('Id is missing'); const requestBody: PublishDocumentWithDescendantsRequestModel = { cultures: variantIds.map((variant) => variant.toCultureString()), includeUnpublishedDescendants, - forceRepublish, }; return tryExecuteAndNotify( diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts index 2d2c9940bf..c2d59220e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts @@ -229,7 +229,6 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase(UmbLitElement, undefined) { @property({ attribute: false }) public list: Array = []; @@ -38,8 +42,24 @@ export class UmbInputCheckboxListElement extends UUIFormControlMixin(UmbLitEleme @property({ type: Boolean, reflect: true }) readonly = false; - protected override getFormElement() { - return undefined; + /** + * Sets the input to required, meaning validation will fail if the value is empty. + * @type {boolean} + */ + @property({ type: Boolean }) + required?: boolean; + + @property({ type: String }) + requiredMessage?: string; + + constructor() { + super(); + + this.addValidator( + 'valueMissing', + () => this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, + () => !this.readonly && !!this.required && (this.value === undefined || this.value === null || this.value === ''), + ); } #onChange(event: UUIBooleanInputEvent) { @@ -73,13 +93,15 @@ export class UmbInputCheckboxListElement extends UUIFormControlMixin(UmbLitEleme } #renderCheckbox(item: (typeof this.list)[0]) { - return html``; + return html` + + `; } static override readonly styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts index 1b5cdda25d..a9cc829e44 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts @@ -2,28 +2,35 @@ import type { UmbCheckboxListItem, UmbInputCheckboxListElement, } from './components/input-checkbox-list/input-checkbox-list.element.js'; -import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; import './components/input-checkbox-list/input-checkbox-list.element.js'; -import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; /** * @element umb-property-editor-ui-checkbox-list */ @customElement('umb-property-editor-ui-checkbox-list') -export class UmbPropertyEditorUICheckboxListElement extends UmbLitElement implements UmbPropertyEditorUiElement { +export class UmbPropertyEditorUICheckboxListElement + extends UmbFormControlMixin | string | undefined, typeof UmbLitElement, undefined>( + UmbLitElement, + undefined, + ) + implements UmbPropertyEditorUiElement +{ #selection: Array = []; @property({ type: Array }) - public set value(value: Array | string | undefined) { + public override set value(value: Array | string | undefined) { this.#selection = Array.isArray(value) ? value : value ? [value] : []; } - public get value(): Array | undefined { + public override get value(): Array | undefined { return this.#selection; } @@ -60,9 +67,23 @@ export class UmbPropertyEditorUICheckboxListElement extends UmbLitElement implem @property({ type: Boolean, reflect: true }) readonly = false; + /** + * Sets the input to mandatory, meaning validation will fail if the value is empty. + * @type {boolean} + */ + @property({ type: Boolean }) + mandatory?: boolean; + + @property({ type: String }) + mandatoryMessage = UMB_VALIDATION_EMPTY_LOCALIZATION_KEY; + @state() private _list: Array = []; + protected override firstUpdated() { + this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-checkbox-list')!); + } + #onChange(event: CustomEvent & { target: UmbInputCheckboxListElement }) { this.value = event.target.selection; this.dispatchEvent(new UmbChangeEvent()); @@ -72,9 +93,12 @@ export class UmbPropertyEditorUICheckboxListElement extends UmbLitElement implem return html` + @change=${this.#onChange}> + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/Umbraco.Tags.ts b/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/Umbraco.Tags.ts index 5726481026..0da099b29a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/Umbraco.Tags.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/Umbraco.Tags.ts @@ -10,14 +10,15 @@ export const manifest: ManifestPropertyEditorSchema = { properties: [ { alias: 'group', - label: 'Define a tag group', + label: 'Tag group', description: '', propertyEditorUiAlias: 'Umb.PropertyEditorUi.TextBox', }, { alias: 'storageType', label: 'Storage Type', - description: '', + description: + 'Select whether to store the tags in cache as JSON (default) or CSV format. Notice that CSV does not support commas in the tag value.', propertyEditorUiAlias: 'Umb.PropertyEditorUi.Select', config: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/query-builder/query-builder-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/query-builder/query-builder-modal.element.ts index a3e2863471..2ecf70b819 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/query-builder/query-builder-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/query-builder/query-builder-modal.element.ts @@ -135,29 +135,21 @@ export default class UmbTemplateQueryBuilderModalElement extends UmbModalBaseEle #setSortProperty(event: Event) { const target = event.target as UUIComboboxListElement; - - if (!this._queryRequest.sort) this.#setSortDirection(); - - this.#updateQueryRequest({ - sort: { ...this._queryRequest.sort, propertyAlias: target.value as string }, - }); + this.#setSort(target.value as string, this._queryRequest.sort?.direction as SortOrder ?? this._defaultSortDirection); } #setSortDirection() { - if (!this._queryRequest.sort?.direction) { - this.#updateQueryRequest({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - sort: { ...this._queryRequest.sort, direction: this._defaultSortDirection }, - }); - return; - } + const direction = this._queryRequest.sort?.direction + ? this._queryRequest.sort.direction === SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending + : this._defaultSortDirection; + this.#setSort(this._queryRequest.sort?.propertyAlias ?? "", direction); + } + #setSort(propertyAlias: string, direction: SortOrder) { this.#updateQueryRequest({ sort: { - ...this._queryRequest.sort, - direction: - this._queryRequest.sort?.direction === SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending, + propertyAlias, + direction }, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tinymce-plugin.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tinymce-plugin.extension.ts index a4921daf05..2506a66597 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tinymce-plugin.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tinymce-plugin.extension.ts @@ -28,9 +28,10 @@ export interface MetaTinyMcePlugin { }>; /** - * Sets the default configuration for the TinyMCE editor. This configuration will be used when the editor is initialized. - * @see [TinyMCE Configuration](https://www.tiny.cloud/docs/configure/) for more information. + * @title Sets the default configuration for the TinyMCE editor. + * @description This configuration will be used when the editor is initialized. See the [TinyMCE Configuration](https://www.tiny.cloud/docs/configure/) for more information. * @optional + * @TJS-type object * @examples [ * { * "plugins": "wordcount", diff --git a/src/Umbraco.Web.UI.Client/src/rollup.config.js b/src/Umbraco.Web.UI.Client/src/rollup.config.js index 5d64dd78a0..f4a313b69c 100644 --- a/src/Umbraco.Web.UI.Client/src/rollup.config.js +++ b/src/Umbraco.Web.UI.Client/src/rollup.config.js @@ -52,6 +52,10 @@ console.log('--- Copying TinyMCE i18n done ---'); // Copy monaco-editor console.log('--- Copying monaco-editor ---'); cpSync('./node_modules/monaco-editor/esm/vs/editor/editor.worker.js', `${DIST_DIRECTORY}/monaco-editor/vs/editor/editor.worker.js`); +cpSync('./node_modules/monaco-editor/esm/vs/base', `${DIST_DIRECTORY}/monaco-editor/vs/base`, { recursive: true }); +cpSync('./node_modules/monaco-editor/esm/vs/nls.js', `${DIST_DIRECTORY}/monaco-editor/vs/nls.js`, { recursive: true }); +cpSync('./node_modules/monaco-editor/esm/vs/nls.messages.js', `${DIST_DIRECTORY}/monaco-editor/vs/nls.messages.js`, { recursive: true }); +cpSync('./node_modules/monaco-editor/esm/vs/editor/common', `${DIST_DIRECTORY}/monaco-editor/vs/editor/common`, { recursive: true }); cpSync('./node_modules/monaco-editor/esm/vs/language', `${DIST_DIRECTORY}/monaco-editor/vs/language`, { recursive: true }); cpSync('./node_modules/monaco-editor/min/vs/base/browser/ui/codicons', `${DIST_DIRECTORY}/assets/fonts`, { recursive: true }); console.log('--- Copying monaco-editor done ---'); diff --git a/src/Umbraco.Web.UI.Client/vite.config.ts b/src/Umbraco.Web.UI.Client/vite.config.ts index 53747768db..c57b7aa65d 100644 --- a/src/Umbraco.Web.UI.Client/vite.config.ts +++ b/src/Umbraco.Web.UI.Client/vite.config.ts @@ -35,7 +35,7 @@ export const plugins: PluginOption[] = [ }, { src: 'node_modules/monaco-editor/esm/**/*', - dest: 'umbraco/backoffice/monaco-editor/esm', + dest: 'umbraco/backoffice/monaco-editor/vs', }, ], }), diff --git a/templates/UmbracoExtension/.template.config/template.json b/templates/UmbracoExtension/.template.config/template.json index 5ada4ae33f..b915adc90f 100644 --- a/templates/UmbracoExtension/.template.config/template.json +++ b/templates/UmbracoExtension/.template.config/template.json @@ -136,22 +136,6 @@ ], "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", "continueOnError": true - }, - { - "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", - "args": { - "executable": "powershell", - "args": "cd Client;npm install;npm run build;", - "redirectStandardError": false, - "redirectStandardOutput": false - }, - "manualInstructions": [ - { - "text": "From the 'Client' folder run 'npm install' and then 'npm run build'" - } - ], - "continueOnError": true, - "description ": "Installs node modules" } ], "sources": [ @@ -160,11 +144,10 @@ { "condition": "(!IncludeExample)", "exclude": [ - "[Cc]lient/src/dashboards/**", - "[Cc]lient/src/api/schemas.gen.ts" + "[Cc]lient/src/dashboards/**" ] } ] } ] -} +} \ No newline at end of file diff --git a/templates/UmbracoExtension/Client/package.json b/templates/UmbracoExtension/Client/package.json index c4b1ae9b6b..f62550adfe 100644 --- a/templates/UmbracoExtension/Client/package.json +++ b/templates/UmbracoExtension/Client/package.json @@ -9,13 +9,13 @@ "generate-client": "node scripts/generate-openapi.js https://localhost:44339/umbraco/swagger/umbracoextension/swagger.json" }, "devDependencies": { - "@hey-api/client-fetch": "^0.4.2", - "@hey-api/openapi-ts": "^0.53.11", + "@hey-api/client-fetch": "^0.8.3", + "@hey-api/openapi-ts": "^0.64.10", "@umbraco-cms/backoffice": "^UMBRACO_VERSION_FROM_TEMPLATE", - "chalk": "^5.3.0", + "chalk": "^5.4.1", "cross-env": "^7.0.3", "node-fetch": "^3.3.2", - "typescript": "^5.6.3", - "vite": "^5.4.9" + "typescript": "^5.8.2", + "vite": "^6.2.0" } -} +} \ No newline at end of file diff --git a/templates/UmbracoExtension/Client/public/umbraco-package.json b/templates/UmbracoExtension/Client/public/umbraco-package.json index 2ca2f83317..ca3d52d518 100644 --- a/templates/UmbracoExtension/Client/public/umbraco-package.json +++ b/templates/UmbracoExtension/Client/public/umbraco-package.json @@ -2,7 +2,7 @@ "id": "Umbraco.Extension", "name": "Umbraco.Extension", "version": "0.0.0", - "allowPackageTelemetry": true, + "allowTelemetry": true, "extensions": [ { "name": "Umbraco ExtensionBundle", @@ -11,4 +11,4 @@ "js": "/App_Plugins/UmbracoExtension/umbraco-extension.js" } ] -} +} \ No newline at end of file diff --git a/templates/UmbracoExtension/Client/scripts/generate-openapi.js b/templates/UmbracoExtension/Client/scripts/generate-openapi.js index 93bb481289..b320781bc7 100644 --- a/templates/UmbracoExtension/Client/scripts/generate-openapi.js +++ b/templates/UmbracoExtension/Client/scripts/generate-openapi.js @@ -1,6 +1,6 @@ import fetch from 'node-fetch'; import chalk from 'chalk'; -import { createClient } from '@hey-api/openapi-ts'; +import { createClient, defaultPlugins } from '@hey-api/openapi-ts'; // Start notifying user we are generating the TypeScript client console.log(chalk.green("Generating OpenAPI client...")); @@ -20,7 +20,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; console.log("Ensure your Umbraco instance is running"); console.log(`Fetching OpenAPI definition from ${chalk.yellow(swaggerUrl)}`); -fetch(swaggerUrl).then(response => { +fetch(swaggerUrl).then(async (response) => { if (!response.ok) { console.error(chalk.red(`ERROR: OpenAPI spec returned with a non OK (200) response: ${response.status} ${response.statusText}`)); console.error(`The URL to your Umbraco instance may be wrong or the instance is not running`); @@ -31,13 +31,21 @@ fetch(swaggerUrl).then(response => { console.log(`OpenAPI spec fetched successfully`); console.log(`Calling ${chalk.yellow('hey-api')} to generate TypeScript client`); - createClient({ - client: '@hey-api/client-fetch', + await createClient({ input: swaggerUrl, output: 'src/api', - services: { - asClass: true, - } + plugins: [ + ...defaultPlugins, + '@hey-api/client-fetch', + { + name: '@hey-api/typescript', + enums: 'typescript' + }, + { + name: '@hey-api/sdk', + asClass: true + } + ], }); }) diff --git a/templates/UmbracoExtension/Client/src/api/client.gen.ts b/templates/UmbracoExtension/Client/src/api/client.gen.ts new file mode 100644 index 0000000000..d7bb5e8272 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/client.gen.ts @@ -0,0 +1,18 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ClientOptions } from './types.gen'; +import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig({ + baseUrl: 'https://localhost:44389' +})); \ No newline at end of file diff --git a/templates/UmbracoExtension/Client/src/api/index.ts b/templates/UmbracoExtension/Client/src/api/index.ts index 7661d65371..e64537d212 100644 --- a/templates/UmbracoExtension/Client/src/api/index.ts +++ b/templates/UmbracoExtension/Client/src/api/index.ts @@ -1,6 +1,3 @@ // This file is auto-generated by @hey-api/openapi-ts -//#if(IncludeExample) -export * from './schemas.gen'; -//#endif -export * from './services.gen'; export * from './types.gen'; +export * from './sdk.gen'; \ No newline at end of file diff --git a/templates/UmbracoExtension/Client/src/api/schemas.gen.ts b/templates/UmbracoExtension/Client/src/api/schemas.gen.ts deleted file mode 100644 index 1a1885f5d5..0000000000 --- a/templates/UmbracoExtension/Client/src/api/schemas.gen.ts +++ /dev/null @@ -1,391 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export const DocumentGranularPermissionModelSchema = { - required: ['context', 'key', 'permission'], - type: 'object', - properties: { - key: { - type: 'string', - format: 'uuid' - }, - context: { - type: 'string', - readOnly: true - }, - permission: { - type: 'string' - } - }, - additionalProperties: false -} as const; - -export const ReadOnlyUserGroupModelSchema = { - required: ['alias', 'allowedLanguages', 'allowedSections', 'granularPermissions', 'hasAccessToAllLanguages', 'id', 'key', 'name', 'permissions'], - type: 'object', - properties: { - id: { - type: 'integer', - format: 'int32' - }, - key: { - type: 'string', - format: 'uuid' - }, - name: { - type: 'string' - }, - icon: { - type: 'string', - nullable: true - }, - startContentId: { - type: 'integer', - format: 'int32', - nullable: true - }, - startMediaId: { - type: 'integer', - format: 'int32', - nullable: true - }, - alias: { - type: 'string' - }, - hasAccessToAllLanguages: { - type: 'boolean' - }, - allowedLanguages: { - type: 'array', - items: { - type: 'integer', - format: 'int32' - } - }, - permissions: { - uniqueItems: true, - type: 'array', - items: { - type: 'string' - } - }, - granularPermissions: { - uniqueItems: true, - type: 'array', - items: { - oneOf: [ - { - '$ref': '#/components/schemas/DocumentGranularPermissionModel' - }, - { - '$ref': '#/components/schemas/UnknownTypeGranularPermissionModel' - } - ] - } - }, - allowedSections: { - type: 'array', - items: { - type: 'string' - } - } - }, - additionalProperties: false -} as const; - -export const UnknownTypeGranularPermissionModelSchema = { - required: ['context', 'permission'], - type: 'object', - properties: { - context: { - type: 'string' - }, - permission: { - type: 'string' - } - }, - additionalProperties: false -} as const; - -export const UserGroupModelSchema = { - required: ['alias', 'allowedLanguages', 'allowedSections', 'createDate', 'granularPermissions', 'hasAccessToAllLanguages', 'hasIdentity', 'id', 'key', 'permissions', 'updateDate', 'userCount'], - type: 'object', - properties: { - id: { - type: 'integer', - format: 'int32' - }, - key: { - type: 'string', - format: 'uuid' - }, - createDate: { - type: 'string', - format: 'date-time' - }, - updateDate: { - type: 'string', - format: 'date-time' - }, - deleteDate: { - type: 'string', - format: 'date-time', - nullable: true - }, - hasIdentity: { - type: 'boolean', - readOnly: true - }, - startMediaId: { - type: 'integer', - format: 'int32', - nullable: true - }, - startContentId: { - type: 'integer', - format: 'int32', - nullable: true - }, - icon: { - type: 'string', - nullable: true - }, - alias: { - type: 'string' - }, - name: { - type: 'string', - nullable: true - }, - hasAccessToAllLanguages: { - type: 'boolean' - }, - permissions: { - uniqueItems: true, - type: 'array', - items: { - type: 'string' - } - }, - granularPermissions: { - uniqueItems: true, - type: 'array', - items: { - oneOf: [ - { - '$ref': '#/components/schemas/DocumentGranularPermissionModel' - }, - { - '$ref': '#/components/schemas/UnknownTypeGranularPermissionModel' - } - ] - } - }, - allowedSections: { - type: 'array', - items: { - type: 'string' - }, - readOnly: true - }, - userCount: { - type: 'integer', - format: 'int32', - readOnly: true - }, - allowedLanguages: { - type: 'array', - items: { - type: 'integer', - format: 'int32' - }, - readOnly: true - } - }, - additionalProperties: false -} as const; - -export const UserKindModelSchema = { - enum: ['Default', 'Api'], - type: 'string' -} as const; - -export const UserModelSchema = { - required: ['allowedSections', 'createDate', 'email', 'failedPasswordAttempts', 'groups', 'hasIdentity', 'id', 'isApproved', 'isLockedOut', 'key', 'kind', 'profileData', 'sessionTimeout', 'updateDate', 'username', 'userState'], - type: 'object', - properties: { - id: { - type: 'integer', - format: 'int32' - }, - key: { - type: 'string', - format: 'uuid' - }, - createDate: { - type: 'string', - format: 'date-time' - }, - updateDate: { - type: 'string', - format: 'date-time' - }, - deleteDate: { - type: 'string', - format: 'date-time', - nullable: true - }, - hasIdentity: { - type: 'boolean', - readOnly: true - }, - emailConfirmedDate: { - type: 'string', - format: 'date-time', - nullable: true - }, - invitedDate: { - type: 'string', - format: 'date-time', - nullable: true - }, - username: { - type: 'string' - }, - email: { - type: 'string' - }, - rawPasswordValue: { - type: 'string', - nullable: true - }, - passwordConfiguration: { - type: 'string', - nullable: true - }, - isApproved: { - type: 'boolean' - }, - isLockedOut: { - type: 'boolean' - }, - lastLoginDate: { - type: 'string', - format: 'date-time', - nullable: true - }, - lastPasswordChangeDate: { - type: 'string', - format: 'date-time', - nullable: true - }, - lastLockoutDate: { - type: 'string', - format: 'date-time', - nullable: true - }, - failedPasswordAttempts: { - type: 'integer', - format: 'int32' - }, - comments: { - type: 'string', - nullable: true - }, - userState: { - '$ref': '#/components/schemas/UserStateModel' - }, - name: { - type: 'string', - nullable: true - }, - allowedSections: { - type: 'array', - items: { - type: 'string' - }, - readOnly: true - }, - profileData: { - oneOf: [ - { - '$ref': '#/components/schemas/UserModel' - }, - { - '$ref': '#/components/schemas/UserProfileModel' - } - ], - readOnly: true - }, - securityStamp: { - type: 'string', - nullable: true - }, - avatar: { - type: 'string', - nullable: true - }, - sessionTimeout: { - type: 'integer', - format: 'int32' - }, - startContentIds: { - type: 'array', - items: { - type: 'integer', - format: 'int32' - }, - nullable: true - }, - startMediaIds: { - type: 'array', - items: { - type: 'integer', - format: 'int32' - }, - nullable: true - }, - language: { - type: 'string', - nullable: true - }, - kind: { - '$ref': '#/components/schemas/UserKindModel' - }, - groups: { - type: 'array', - items: { - oneOf: [ - { - '$ref': '#/components/schemas/ReadOnlyUserGroupModel' - }, - { - '$ref': '#/components/schemas/UserGroupModel' - } - ] - }, - readOnly: true - } - }, - additionalProperties: false -} as const; - -export const UserProfileModelSchema = { - required: ['id'], - type: 'object', - properties: { - id: { - type: 'integer', - format: 'int32' - }, - name: { - type: 'string', - nullable: true - } - }, - additionalProperties: false -} as const; - -export const UserStateModelSchema = { - enum: ['Active', 'Disabled', 'LockedOut', 'Invited', 'Inactive', 'All'], - type: 'string' -} as const; \ No newline at end of file diff --git a/templates/UmbracoExtension/Client/src/api/sdk.gen.ts b/templates/UmbracoExtension/Client/src/api/sdk.gen.ts new file mode 100644 index 0000000000..0465ad97b5 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/sdk.gen.ts @@ -0,0 +1,78 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; +//#if(IncludeExample) +import type { PingData, PingResponse, WhatsMyNameData, WhatsMyNameResponse, WhatsTheTimeMrWolfData, WhatsTheTimeMrWolfResponse, WhoAmIData, WhoAmIResponse } from './types.gen'; +//#else +import type { PingData, PingResponse } from './types.gen'; +//#endif +import { client as _heyApiClient } from './client.gen'; + +export type Options = ClientOptions & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +export class UmbracoExtensionService { + public static ping(options?: Options) { + return (options?.client ?? _heyApiClient).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/umbraco/umbracoextension/api/v1/ping', + ...options + }); + } +//#if(IncludeExample) + public static whatsMyName(options?: Options) { + return (options?.client ?? _heyApiClient).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/umbraco/umbracoextension/api/v1/whatsMyName', + ...options + }); + } + + public static whatsTheTimeMrWolf(options?: Options) { + return (options?.client ?? _heyApiClient).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/umbraco/umbracoextension/api/v1/whatsTheTimeMrWolf', + ...options + }); + } + + public static whoAmI(options?: Options) { + return (options?.client ?? _heyApiClient).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/umbraco/umbracoextension/api/v1/whoAmI', + ...options + }); + } +//#endif +} \ No newline at end of file diff --git a/templates/UmbracoExtension/Client/src/api/services.gen.ts b/templates/UmbracoExtension/Client/src/api/services.gen.ts deleted file mode 100644 index c9d1c0b91b..0000000000 --- a/templates/UmbracoExtension/Client/src/api/services.gen.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createClient, createConfig, type Options } from '@hey-api/client-fetch'; -//#if(IncludeExample) -import type { PingError, PingResponse, WhatsMyNameError, WhatsMyNameResponse, WhatsTheTimeMrWolfError, WhatsTheTimeMrWolfResponse, WhoAmIError, WhoAmIResponse } from './types.gen'; -//#else -import type { PingError, PingResponse } from './types.gen'; -//#endif - -export const client = createClient(createConfig()); - -export class UmbracoExtensionService { - public static ping(options?: Options) { - return (options?.client ?? client).get({ - ...options, - url: '/umbraco/umbracoextension/api/v1/ping' - }); - } -//#if(IncludeExample) - public static whatsMyName(options?: Options) { - return (options?.client ?? client).get({ - ...options, - url: '/umbraco/umbracoextension/api/v1/whatsMyName' - }); - } - - public static whatsTheTimeMrWolf(options?: Options) { - return (options?.client ?? client).get({ - ...options, - url: '/umbraco/umbracoextension/api/v1/whatsTheTimeMrWolf' - }); - } - - public static whoAmI(options?: Options) { - return (options?.client ?? client).get({ - ...options, - url: '/umbraco/umbracoextension/api/v1/whoAmI' - }); - } -//#endif -} diff --git a/templates/UmbracoExtension/Client/src/api/types.gen.ts b/templates/UmbracoExtension/Client/src/api/types.gen.ts index e666792803..9da3ce6897 100644 --- a/templates/UmbracoExtension/Client/src/api/types.gen.ts +++ b/templates/UmbracoExtension/Client/src/api/types.gen.ts @@ -1,5 +1,5 @@ // This file is auto-generated by @hey-api/openapi-ts -//#if(IncludeExample) +//#if(IncludeExample) export type DocumentGranularPermissionModel = { key: string; readonly context: string; @@ -10,15 +10,15 @@ export type ReadOnlyUserGroupModel = { id: number; key: string; name: string; - icon?: (string) | null; - startContentId?: (number) | null; - startMediaId?: (number) | null; + icon?: string | null; + startContentId?: number | null; + startMediaId?: number | null; alias: string; hasAccessToAllLanguages: boolean; - allowedLanguages: Array<(number)>; - permissions: Array<(string)>; - granularPermissions: Array<(DocumentGranularPermissionModel | UnknownTypeGranularPermissionModel)>; - allowedSections: Array<(string)>; + allowedLanguages: Array; + permissions: Array; + granularPermissions: Array; + allowedSections: Array; }; export type UnknownTypeGranularPermissionModel = { @@ -31,77 +31,166 @@ export type UserGroupModel = { key: string; createDate: string; updateDate: string; - deleteDate?: (string) | null; + deleteDate?: string | null; readonly hasIdentity: boolean; - startMediaId?: (number) | null; - startContentId?: (number) | null; - icon?: (string) | null; + startMediaId?: number | null; + startContentId?: number | null; + icon?: string | null; alias: string; - name?: (string) | null; + name?: string | null; hasAccessToAllLanguages: boolean; - permissions: Array<(string)>; - granularPermissions: Array<(DocumentGranularPermissionModel | UnknownTypeGranularPermissionModel)>; - readonly allowedSections: Array<(string)>; + permissions: Array; + granularPermissions: Array; + readonly allowedSections: Array; readonly userCount: number; - readonly allowedLanguages: Array<(number)>; + readonly allowedLanguages: Array; }; -export type UserKindModel = 'Default' | 'Api'; +export enum UserKindModel { + DEFAULT = 'Default', + API = 'Api' +} export type UserModel = { id: number; key: string; createDate: string; updateDate: string; - deleteDate?: (string) | null; + deleteDate?: string | null; readonly hasIdentity: boolean; - emailConfirmedDate?: (string) | null; - invitedDate?: (string) | null; + emailConfirmedDate?: string | null; + invitedDate?: string | null; username: string; email: string; - rawPasswordValue?: (string) | null; - passwordConfiguration?: (string) | null; + rawPasswordValue?: string | null; + passwordConfiguration?: string | null; isApproved: boolean; isLockedOut: boolean; - lastLoginDate?: (string) | null; - lastPasswordChangeDate?: (string) | null; - lastLockoutDate?: (string) | null; + lastLoginDate?: string | null; + lastPasswordChangeDate?: string | null; + lastLockoutDate?: string | null; failedPasswordAttempts: number; - comments?: (string) | null; + comments?: string | null; userState: UserStateModel; - name?: (string) | null; - readonly allowedSections: Array<(string)>; - readonly profileData: (UserModel | UserProfileModel); - securityStamp?: (string) | null; - avatar?: (string) | null; + name?: string | null; + readonly allowedSections: Array; + profileData: UserModel | UserProfileModel; + securityStamp?: string | null; + avatar?: string | null; sessionTimeout: number; - startContentIds?: Array<(number)> | null; - startMediaIds?: Array<(number)> | null; - language?: (string) | null; + startContentIds?: Array | null; + startMediaIds?: Array | null; + language?: string | null; kind: UserKindModel; - readonly groups: Array<(ReadOnlyUserGroupModel | UserGroupModel)>; + readonly groups: Array; }; export type UserProfileModel = { id: number; - name?: (string) | null; + name?: string | null; }; -export type UserStateModel = 'Active' | 'Disabled' | 'LockedOut' | 'Invited' | 'Inactive' | 'All'; +export enum UserStateModel { + ACTIVE = 'Active', + DISABLED = 'Disabled', + LOCKED_OUT = 'LockedOut', + INVITED = 'Invited', + INACTIVE = 'Inactive', + ALL = 'All' +} //#endif -export type PingResponse = (string); +export type PingData = { + body?: never; + path?: never; + query?: never; + url: '/umbraco/hackclient/api/v1/ping'; +}; -export type PingError = (unknown); -//#if(IncludeExample) -export type WhatsMyNameResponse = (string); +export type PingErrors = { + /** + * The resource is protected and requires an authentication token + */ + 401: unknown; +}; -export type WhatsMyNameError = (unknown); +export type PingResponses = { + /** + * OK + */ + 200: string; +}; -export type WhatsTheTimeMrWolfResponse = (string); +export type PingResponse = PingResponses[keyof PingResponses]; +//#if(IncludeExample) +export type WhatsMyNameData = { + body?: never; + path?: never; + query?: never; + url: '/umbraco/hackclient/api/v1/whatsMyName'; +}; -export type WhatsTheTimeMrWolfError = (unknown); +export type WhatsMyNameErrors = { + /** + * The resource is protected and requires an authentication token + */ + 401: unknown; +}; -export type WhoAmIResponse = ((UserModel)); +export type WhatsMyNameResponses = { + /** + * OK + */ + 200: string; +}; -export type WhoAmIError = (unknown); +export type WhatsMyNameResponse = WhatsMyNameResponses[keyof WhatsMyNameResponses]; + +export type WhatsTheTimeMrWolfData = { + body?: never; + path?: never; + query?: never; + url: '/umbraco/hackclient/api/v1/whatsTheTimeMrWolf'; +}; + +export type WhatsTheTimeMrWolfErrors = { + /** + * The resource is protected and requires an authentication token + */ + 401: unknown; +}; + +export type WhatsTheTimeMrWolfResponses = { + /** + * OK + */ + 200: string; +}; + +export type WhatsTheTimeMrWolfResponse = WhatsTheTimeMrWolfResponses[keyof WhatsTheTimeMrWolfResponses]; + +export type WhoAmIData = { + body?: never; + path?: never; + query?: never; + url: '/umbraco/hackclient/api/v1/whoAmI'; +}; + +export type WhoAmIErrors = { + /** + * The resource is protected and requires an authentication token + */ + 401: unknown; +}; + +export type WhoAmIResponses = { + /** + * OK + */ + 200: UserModel; +}; + +export type WhoAmIResponse = WhoAmIResponses[keyof WhoAmIResponses]; //#endif +export type ClientOptions = { + baseUrl: 'https://localhost:44389' | (string & {}); +}; diff --git a/templates/UmbracoExtension/Client/src/bundle.manifests.ts b/templates/UmbracoExtension/Client/src/bundle.manifests.ts index fb2d2bfa09..6186b361f3 100644 --- a/templates/UmbracoExtension/Client/src/bundle.manifests.ts +++ b/templates/UmbracoExtension/Client/src/bundle.manifests.ts @@ -1,6 +1,6 @@ -import { manifests as entrypoints } from './entrypoints/manifest'; +import { manifests as entrypoints } from "./entrypoints/manifest.js"; //#if IncludeExample -import { manifests as dashboards } from './dashboards/manifest'; +import { manifests as dashboards } from "./dashboards/manifest.js"; //#endif // Job of the bundle is to collate all the manifests from different parts of the extension and load other manifests diff --git a/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts b/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts index 287464086e..7a2d0756ec 100644 --- a/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts +++ b/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts @@ -1,13 +1,24 @@ -import { LitElement, css, html, customElement, state } from "@umbraco-cms/backoffice/external/lit"; +import { + LitElement, + css, + html, + customElement, + state, +} from "@umbraco-cms/backoffice/external/lit"; import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; -import { UmbracoExtensionService, UserModel } from "../api"; import { UUIButtonElement } from "@umbraco-cms/backoffice/external/uui"; -import { UMB_NOTIFICATION_CONTEXT, UmbNotificationContext } from "@umbraco-cms/backoffice/notification"; -import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserModel } from "@umbraco-cms/backoffice/current-user"; +import { + UMB_NOTIFICATION_CONTEXT, + UmbNotificationContext, +} from "@umbraco-cms/backoffice/notification"; +import { + UMB_CURRENT_USER_CONTEXT, + UmbCurrentUserModel, +} from "@umbraco-cms/backoffice/current-user"; +import { UmbracoExtensionService, UserModel } from "../api/index.js"; -@customElement('example-dashboard') +@customElement("example-dashboard") export class ExampleDashboardElement extends UmbElementMixin(LitElement) { - @state() private _yourName: string | undefined = "Press the button!"; @@ -28,7 +39,6 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) { }); this.consumeContext(UMB_CURRENT_USER_CONTEXT, (currentUserContext) => { - // When we have the current user context // We can observe properties from it, such as the current user or perhaps just individual properties // When the currentUser object changes we will get notified and can reset the @state properrty @@ -62,10 +72,10 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) { data: { headline: `You are ${this._serverUserData?.name}`, message: `Your email is ${this._serverUserData?.email}`, - } - }) + }, + }); } - } + }; #onClickWhatsTheTimeMrWolf = async (ev: Event) => { const buttonElement = ev.target as UUIButtonElement; @@ -84,7 +94,7 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) { this._timeFromMrWolf = new Date(data); buttonElement.state = "success"; } - } + }; #onClickWhatsMyName = async (ev: Event) => { const buttonElement = ev.target as UUIButtonElement; @@ -100,77 +110,111 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) { this._yourName = data; buttonElement.state = "success"; - } + }; render() { return html` - -
[Server]
-

${this._serverUserData?.email ? this._serverUserData.email : 'Press the button!'}

-
    - ${this._serverUserData?.groups.map(group => html`
  • ${group.name}
  • `)} -
- - Who am I? - -

This endpoint gets your current user from the server and displays your email and list of user groups. - It also displays a Notification with your details.

-
+ +
[Server]
+

+ ${this._serverUserData?.email + ? this._serverUserData.email + : "Press the button!"} +

+
    + ${this._serverUserData?.groups.map( + (group) => html`
  • ${group.name}
  • ` + )} +
+ + Who am I? + +

+ This endpoint gets your current user from the server and displays your + email and list of user groups. It also displays a Notification with + your details. +

+
- -
[Server]
-

${this._yourName }

- - Whats my name? - -

This endpoint has a forced delay to show the button 'waiting' state for a few seconds before completing the request.

-
+ +
[Server]
+

${this._yourName}

+ + Whats my name? + +

+ This endpoint has a forced delay to show the button 'waiting' state + for a few seconds before completing the request. +

+
- -
[Server]
-

${this._timeFromMrWolf ? this._timeFromMrWolf.toLocaleString() : 'Press the button!'}

- - Whats the time Mr Wolf? - -

This endpoint gets the current date and time from the server.

-
+ +
[Server]
+

+ ${this._timeFromMrWolf + ? this._timeFromMrWolf.toLocaleString() + : "Press the button!"} +

+ + Whats the time Mr Wolf? + +

This endpoint gets the current date and time from the server.

+
- -
[Context]
-

Current user email: ${this._contextCurrentUser?.email}

-

This is the JSON object available by consuming the 'UMB_CURRENT_USER_CONTEXT' context:

- ${JSON.stringify(this._contextCurrentUser, null, 2)} -
+ +
[Context]
+

Current user email: ${this._contextCurrentUser?.email}

+

+ This is the JSON object available by consuming the + 'UMB_CURRENT_USER_CONTEXT' context: +

+ ${JSON.stringify(this._contextCurrentUser, null, 2)} +
`; } static styles = [ css` - :host { - display: grid; - gap: var(--uui-size-layout-1); - padding: var(--uui-size-layout-1); - grid-template-columns: 1fr 1fr 1fr; - } + :host { + display: grid; + gap: var(--uui-size-layout-1); + padding: var(--uui-size-layout-1); + grid-template-columns: 1fr 1fr 1fr; + } - uui-box { - margin-bottom: var(--uui-size-layout-1); - } + uui-box { + margin-bottom: var(--uui-size-layout-1); + } - h2 { - margin-top:0; - } + h2 { + margin-top: 0; + } - .wide { - grid-column: span 3; - } - `]; + .wide { + grid-column: span 3; + } + `, + ]; } export default ExampleDashboardElement; declare global { interface HTMLElementTagNameMap { - 'example-dashboard': ExampleDashboardElement; + "example-dashboard": ExampleDashboardElement; } } diff --git a/templates/UmbracoExtension/Client/src/dashboards/manifest.ts b/templates/UmbracoExtension/Client/src/dashboards/manifest.ts index a020e3ab2c..1fa7c360b8 100644 --- a/templates/UmbracoExtension/Client/src/dashboards/manifest.ts +++ b/templates/UmbracoExtension/Client/src/dashboards/manifest.ts @@ -2,17 +2,17 @@ export const manifests: Array = [ { name: "Umbraco ExtensionDashboard", alias: "Umbraco.Extension.Dashboard", - type: 'dashboard', - js: () => import("./dashboard.element"), + type: "dashboard", + js: () => import("./dashboard.element.js"), meta: { label: "Example Dashboard", - pathname: "example-dashboard" + pathname: "example-dashboard", }, conditions: [ { - alias: 'Umb.Condition.SectionAlias', - match: 'Umb.Section.Content', - } + alias: "Umb.Condition.SectionAlias", + match: "Umb.Section.Content", + }, ], - } + }, ]; diff --git a/templates/UmbracoExtension/Client/src/entrypoints/entrypoint.ts b/templates/UmbracoExtension/Client/src/entrypoints/entrypoint.ts index 65796c3202..d6115312f4 100644 --- a/templates/UmbracoExtension/Client/src/entrypoints/entrypoint.ts +++ b/templates/UmbracoExtension/Client/src/entrypoints/entrypoint.ts @@ -1,38 +1,31 @@ -import { UmbEntryPointOnInit, UmbEntryPointOnUnload } from '@umbraco-cms/backoffice/extension-api'; +import { + UmbEntryPointOnInit, + UmbEntryPointOnUnload, +} from "@umbraco-cms/backoffice/extension-api"; //#if IncludeExample -import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; -import { client } from '../api'; +import { UMB_AUTH_CONTEXT } from "@umbraco-cms/backoffice/auth"; +import { client } from "../api/client.gen.js"; //#endif // load up the manifests here export const onInit: UmbEntryPointOnInit = (_host, _extensionRegistry) => { - - console.log('Hello from my extension 🎉'); + console.log("Hello from my extension 🎉"); //#if IncludeExample // Will use only to add in Open API config with generated TS OpenAPI HTTPS Client // Do the OAuth token handshake stuff _host.consumeContext(UMB_AUTH_CONTEXT, async (authContext) => { - // Get the token info from Umbraco const config = authContext.getOpenApiConfiguration(); client.setConfig({ + auth: config.token, baseUrl: config.base, - credentials: config.credentials - }); - - // For every request being made, add the token to the headers - // Can't use the setConfig approach above as its set only once and - // tokens expire and get refreshed - client.interceptors.request.use(async (request, _options) => { - const token = await config.token(); - request.headers.set('Authorization', `Bearer ${token}`); - return request; + credentials: config.credentials, }); }); //#endif }; export const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => { - console.log('Goodbye from my extension 👋'); + console.log("Goodbye from my extension 👋"); }; diff --git a/templates/UmbracoExtension/Client/src/entrypoints/manifest.ts b/templates/UmbracoExtension/Client/src/entrypoints/manifest.ts index 5dcda9de89..cd97f1e6a7 100644 --- a/templates/UmbracoExtension/Client/src/entrypoints/manifest.ts +++ b/templates/UmbracoExtension/Client/src/entrypoints/manifest.ts @@ -3,6 +3,6 @@ export const manifests: Array = [ name: "Umbraco ExtensionEntrypoint", alias: "Umbraco.Extension.Entrypoint", type: "backofficeEntryPoint", - js: () => import("./entrypoint"), - } + js: () => import("./entrypoint.js"), + }, ]; diff --git a/templates/UmbracoExtension/Client/vite.config.ts b/templates/UmbracoExtension/Client/vite.config.ts index 5232076b8c..d2db7d4ffd 100644 --- a/templates/UmbracoExtension/Client/vite.config.ts +++ b/templates/UmbracoExtension/Client/vite.config.ts @@ -13,5 +13,5 @@ export default defineConfig({ rollupOptions: { external: [/^@umbraco/], }, - } + }, }); diff --git a/templates/UmbracoExtension/Umbraco.Extension.csproj b/templates/UmbracoExtension/Umbraco.Extension.csproj index 3b70140f35..d6b03e516c 100644 --- a/templates/UmbracoExtension/Umbraco.Extension.csproj +++ b/templates/UmbracoExtension/Umbraco.Extension.csproj @@ -24,13 +24,34 @@ - + + + - + + + + + + + + + + + + + + + + <_ClientAssetsBuildOutput Include="wwwroot\App_Plugins\**" /> + + + + diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 935ad9dc02..0518f7a7f5 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.29", - "@umbraco/playwright-testhelpers": "^15.0.33", + "@umbraco/playwright-testhelpers": "^15.0.34", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -67,9 +67,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.33", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.33.tgz", - "integrity": "sha512-EboW4KNFN5wG4UR8tsLWhjpQVZY0lkVNDbNFu9iohFE2bSfrV2CETcWAthVx8IwJja4nP3dOdwwMKb39/MUNdw==", + "version": "15.0.34", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.34.tgz", + "integrity": "sha512-dEWjiUCWdxBpvDnCoShqRZ5xEfNEo02BBgNIKqDAUDEBht/lAM/pDaUgzu2sUbb7D8AbkrrIxPiNn69XakoNSg==", "dependencies": { "@umbraco/json-models-builders": "2.0.30", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 642f701cd6..da652bf8fc 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.29", - "@umbraco/playwright-testhelpers": "^15.0.33", + "@umbraco/playwright-testhelpers": "^15.0.34", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/BlockGridArea.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/BlockGridArea.spec.ts new file mode 100644 index 0000000000..a669c4f527 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/BlockGridArea.spec.ts @@ -0,0 +1,220 @@ +import {expect} from '@playwright/test'; +import {AliasHelper, ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; + +// Document Type +const documentTypeName = 'TestDocumentTypeForContent'; +let documentTypeId = ''; + +// Content +const contentName = 'TestContent'; + +// Element Types +const firstElementTypeName = 'FirstBlockGridElement'; +let firstElementTypeId = null; +const secondElementTypeName = 'SecondBlockGridElement'; + +// Block Grid Data Type +const blockGridDataTypeName = 'BlockGridTester'; +let blockGridDataTypeId = null; +const firstAreaName = 'FirstArea'; +const areaCreateLabel = 'CreateLabel'; +const toAllowInAreas = true; + +test.beforeEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(firstElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(secondElementTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.documentType.ensureNameNotExists(firstElementTypeName); + await umbracoApi.documentType.ensureNameNotExists(secondElementTypeName); + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.dataType.ensureNameNotExists(blockGridDataTypeName); +}); + +test('can create content with a block grid with an empty block in a area', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + firstElementTypeId = await umbracoApi.documentType.createEmptyElementType(firstElementTypeName); + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithAnAreaInABlockWithAllowInAreas(blockGridDataTypeName, firstElementTypeId, firstAreaName, toAllowInAreas, areaCreateLabel); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockGridElementWithName(firstElementTypeName); + await umbracoUi.content.clickSelectBlockElementWithName(firstElementTypeName); + await umbracoUi.content.clickLinkWithName(areaCreateLabel); + await umbracoUi.content.clickSelectBlockElementInAreaWithName(firstElementTypeName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + await umbracoUi.reloadPage(); + await umbracoUi.content.doesBlockContainBlockInAreaWithName(firstElementTypeName, firstAreaName, firstElementTypeName); + await umbracoUi.content.doesBlockContainBlockCountInArea(firstElementTypeName, firstAreaName, 1); +}); + +test('can create content with a block grid with two empty blocks in a area', async ({umbracoApi, umbracoUi}) => { + // Arrange + firstElementTypeId = await umbracoApi.documentType.createEmptyElementType(firstElementTypeName); + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithAnAreaInABlockWithAllowInAreas(blockGridDataTypeName, firstElementTypeId, firstAreaName, toAllowInAreas, areaCreateLabel); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockGridElementWithName(firstElementTypeName); + await umbracoUi.content.clickSelectBlockElementWithName(firstElementTypeName); + await umbracoUi.content.clickLinkWithName(areaCreateLabel); + await umbracoUi.content.clickSelectBlockElementInAreaWithName(firstElementTypeName); + await umbracoUi.content.addBlockToAreasWithExistingBlock(firstElementTypeName, firstAreaName, 0, 0); + await umbracoUi.content.clickSelectBlockElementInAreaWithName(firstElementTypeName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + await umbracoUi.reloadPage(); + await umbracoUi.content.doesBlockContainCountOfBlockInArea(firstElementTypeName, firstAreaName, firstElementTypeName, 2); + + // Checks if the block grid contains the blocks through the API + const parentBlockKey = await umbracoUi.content.getBlockAtRootDataElementKey(firstElementTypeName, 0); + const areaKey = await umbracoUi.content.getBlockAreaKeyFromParentBlockDataElementKey(parentBlockKey, 0); + const firstBlockInAreaKey = await umbracoUi.content.getBlockDataElementKeyInArea(firstElementTypeName, firstAreaName, firstElementTypeName, 0, 0); + const secondBlockInAreaKey = await umbracoUi.content.getBlockDataElementKeyInArea(firstElementTypeName, firstAreaName, firstElementTypeName, 0, 1); + expect(await umbracoApi.document.doesBlockGridContainBlocksWithDataElementKeyInAreaWithKey(contentName, AliasHelper.toAlias(blockGridDataTypeName), parentBlockKey, areaKey, [firstBlockInAreaKey, secondBlockInAreaKey])).toBeTruthy(); +}); + +test('can create content with block grid area with a create label', async ({umbracoApi, umbracoUi}) => { + // Arrange + firstElementTypeId = await umbracoApi.documentType.createEmptyElementType(firstElementTypeName); + const createLabel = 'ThisIsACreateLabel'; + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithAnAreaInABlockWithACreateLabel(blockGridDataTypeName, firstElementTypeId, createLabel, firstAreaName); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockGridElementWithName(firstElementTypeName); + await umbracoUi.content.clickSelectBlockElementWithName(firstElementTypeName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + await umbracoUi.content.doesBlockGridBlockWithAreaContainCreateLabel(firstElementTypeName, createLabel); +}); + +test('can create content with block grid area with column span', async ({umbracoApi, umbracoUi}) => { + // Arrange + firstElementTypeId = await umbracoApi.documentType.createEmptyElementType(firstElementTypeName); + const columnSpan = 2; + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithAnAreaInABlockWithColumnSpanAndRowSpan(blockGridDataTypeName, firstElementTypeId, columnSpan, 1, firstAreaName, areaCreateLabel); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockGridElementWithName(firstElementTypeName); + await umbracoUi.content.clickSelectBlockElementWithName(firstElementTypeName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + await umbracoUi.content.doesBlockAreaContainColumnSpan(firstElementTypeName, firstAreaName, columnSpan, 0); +}); + +test('can create content with block grid area with row span', async ({umbracoApi, umbracoUi}) => { + // Arrange + firstElementTypeId = await umbracoApi.documentType.createEmptyElementType(firstElementTypeName); + const rowSpan = 4; + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithAnAreaInABlockWithColumnSpanAndRowSpan(blockGridDataTypeName, firstElementTypeId, 12, rowSpan, firstAreaName, areaCreateLabel); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockGridElementWithName(firstElementTypeName); + await umbracoUi.content.clickSelectBlockElementWithName(firstElementTypeName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + await umbracoUi.content.doesBlockAreaContainRowSpan(firstElementTypeName, firstAreaName, rowSpan, 0); +}); + +// Remove fixme when this issue is fixed https://github.com/umbraco/Umbraco-CMS/issues/18639 +test.fixme('can create content with block grid area with min allowed', async ({umbracoApi, umbracoUi}) => { + // Arrange + firstElementTypeId = await umbracoApi.documentType.createEmptyElementType(firstElementTypeName); + const secondElementTypeId = await umbracoApi.documentType.createEmptyElementType(secondElementTypeName); + const minAllowed = 2; + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithAnAreaInABlockWithMinAndMaxAllowed(blockGridDataTypeName, firstElementTypeId, secondElementTypeId, minAllowed, 10, firstAreaName, areaCreateLabel); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockGridElementWithName(firstElementTypeName); + await umbracoUi.content.clickSelectBlockElementWithName(firstElementTypeName); + await umbracoUi.content.clickLinkWithName(areaCreateLabel); + await umbracoUi.content.clickSelectBlockElementInAreaWithName(secondElementTypeName); + await umbracoUi.content.isTextWithExactNameVisible('Minimum 2 entries, requires 1 more.'); + await umbracoUi.content.clickSaveAndPublishButton(); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); + await umbracoUi.content.clickInlineAddToAreaButton(firstElementTypeName, firstAreaName, 0, 1); + await umbracoUi.content.clickSelectBlockElementInAreaWithName(secondElementTypeName); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(secondElementTypeName); +}); + +// Remove fixme when this issue is fixed https://github.com/umbraco/Umbraco-CMS/issues/18639 +test.fixme('can create content with block grid area with max allowed', async ({umbracoApi, umbracoUi}) => { + // Arrange + firstElementTypeId = await umbracoApi.documentType.createEmptyElementType(firstElementTypeName); + const secondElementTypeId = await umbracoApi.documentType.createEmptyElementType(secondElementTypeName); + const maxAllowed = 0; + blockGridDataTypeId = await umbracoApi.dataType.createBlockGridWithAnAreaInABlockWithMinAndMaxAllowed(blockGridDataTypeName, firstElementTypeId, secondElementTypeId, 0, maxAllowed, firstAreaName, areaCreateLabel); + documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridDataTypeName, blockGridDataTypeId); + await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + await umbracoUi.content.goToContentWithName(contentName); + + // Act + await umbracoUi.content.clickAddBlockGridElementWithName(firstElementTypeName); + await umbracoUi.content.clickSelectBlockElementWithName(firstElementTypeName); + await umbracoUi.content.clickLinkWithName(areaCreateLabel); + await umbracoUi.content.clickSelectBlockElementInAreaWithName(secondElementTypeName); + await umbracoUi.content.isTextWithExactNameVisible('Maximum 0 entries, 1 too many.'); + await umbracoUi.content.clickSaveAndPublishButton(); + await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished); + await umbracoUi.content.removeBlockFromArea(firstElementTypeName, firstAreaName, secondElementTypeName); + await umbracoUi.content.clickConfirmToDeleteButton(); + await umbracoUi.content.clickSaveAndPublishButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(secondElementTypeName); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithBlockGrid.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts similarity index 100% rename from tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithBlockGrid.spec.ts rename to tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent.spec.ts new file mode 100644 index 0000000000..c008ea38ba --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/TrashContent.spec.ts @@ -0,0 +1,257 @@ +import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +let dataTypeId = ''; +const contentName = 'TestContent'; +const documentTypeName = 'TestDocumentTypeForContent'; +const dataTypeName = 'Textstring'; +const contentText = 'This is test content text'; +const referenceHeadline = 'The following items depend on this'; +const documentPickerName = ['TestPicker', 'DocumentTypeForPicker']; + +test.beforeEach(async ({umbracoApi}) => { + const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName); + dataTypeId = dataTypeData.id; + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.document.ensureNameNotExists(contentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + await umbracoApi.document.emptyRecycleBin(); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName[1]); +}); + +test('can trash an invariant content node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, contentText, dataTypeName); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuForContent(contentName); + await umbracoUi.content.clickTrashButton(); + // Verify the references list not displayed + await umbracoUi.content.isReferenceHeadlineVisible(false); + await umbracoUi.content.clickConfirmTrashButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(contentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(contentName)).toBeTruthy(); +}); + +test('can trash a variant content node', async ({umbracoApi, umbracoUi}) => { + // Arrange + const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + await umbracoApi.document.createDocumentWithEnglishCultureAndTextContent(contentName, documentTypeId, contentText, dataTypeName); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuForContent(contentName); + await umbracoUi.content.clickTrashButton(); + // Verify the references list not displayed + await umbracoUi.content.isReferenceHeadlineVisible(false); + await umbracoUi.content.clickConfirmTrashButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(contentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(contentName)).toBeTruthy(); +}); + +test('can trash a published content node', async ({umbracoApi, umbracoUi}) => { + // Arrange + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + const contentId = await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, contentText, dataTypeName); + await umbracoApi.document.publish(contentId); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuForContent(contentName); + await umbracoUi.content.clickTrashButton(); + // Verify the references list not displayed + await umbracoUi.content.isReferenceHeadlineVisible(false); + await umbracoUi.content.clickConfirmTrashButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(contentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(contentName)).toBeTruthy(); +}); + +test('can trash an invariant content node that references one item', async ({umbracoApi, umbracoUi}) => { + // Arrange + // Create an invariant published content node + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + const contentId = await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, contentText, dataTypeName); + await umbracoApi.document.publish(contentId); + // Create a document link picker + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName[0], contentName, contentId, documentPickerName[1]); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuForContent(contentName); + await umbracoUi.content.clickTrashButton(); + // Verify the references list + await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); + await umbracoUi.content.doesReferenceItemsHaveCount(1); + await umbracoUi.content.isReferenceItemNameVisible(documentPickerName[0]); + await umbracoUi.content.clickConfirmTrashButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(contentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(contentName)).toBeTruthy(); +}); + +test('can trash a variant content node that references one item', async ({umbracoApi, umbracoUi}) => { + // Arrange + // Create a variant published content node + const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + const contentId = await umbracoApi.document.createDocumentWithEnglishCultureAndTextContent(contentName, documentTypeId, contentText, dataTypeName); + await umbracoApi.document.publishDocumentWithCulture(contentId, 'en-US'); + // Create a document link picker + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName[0], contentName, contentId, documentPickerName[1]); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuForContent(contentName); + await umbracoUi.content.clickTrashButton(); + // Verify the references list + await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); + await umbracoUi.content.doesReferenceItemsHaveCount(1); + await umbracoUi.content.isReferenceItemNameVisible(documentPickerName[0]); + await umbracoUi.content.clickConfirmTrashButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(contentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(contentName)).toBeTruthy(); +}); + +test('can trash an invariant content node that references more than 3 items', async ({umbracoApi, umbracoUi}) => { + // Arrange + const documentPickerName2 = ['TestPicker2', 'DocumentTypeForPicker2']; + const documentPickerName3 = ['TestPicker3', 'DocumentTypeForPicker3']; + const documentPickerName4 = ['TestPicker4', 'DocumentTypeForPicker4']; + // Create an invariant published content node + const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + const contentId = await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, contentText, dataTypeName); + await umbracoApi.document.publish(contentId); + // Create 4 document link pickers + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName[0], contentName, contentId, documentPickerName[1]); + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName2[0], contentName, contentId, documentPickerName2[1]); + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName3[0], contentName, contentId, documentPickerName3[1]); + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName4[0], contentName, contentId, documentPickerName4[1]); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuForContent(contentName); + await umbracoUi.content.clickTrashButton(); + // Verify the references list has 3 items and has the text '...and one more item' + await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); + await umbracoUi.content.doesReferenceItemsHaveCount(3); + await umbracoUi.content.isReferenceItemNameVisible(documentPickerName[0]); + await umbracoUi.content.isReferenceItemNameVisible(documentPickerName2[0]); + await umbracoUi.content.isReferenceItemNameVisible(documentPickerName3[0]); + await umbracoUi.content.doesReferencesContainText('...and one more item'); + await umbracoUi.content.clickConfirmTrashButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(contentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(contentName)).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(documentPickerName2[1]); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName3[1]); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName4[1]); +}); + +test('can trash a variant content node that references more than 3 items', async ({umbracoApi, umbracoUi}) => { + // Arrange + const documentPickerName2 = ['TestPicker2', 'DocumentTypeForPicker2']; + const documentPickerName3 = ['TestPicker3', 'DocumentTypeForPicker3']; + const documentPickerName4 = ['TestPicker4', 'DocumentTypeForPicker4']; + // Create a variant published content node + const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + const contentId = await umbracoApi.document.createDocumentWithEnglishCultureAndTextContent(contentName, documentTypeId, contentText, dataTypeName); + await umbracoApi.document.publishDocumentWithCulture(contentId, 'en-US'); + // Create 4 document link pickers + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName[0], contentName, contentId, documentPickerName[1]); + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName2[0], contentName, contentId, documentPickerName2[1]); + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName3[0], contentName, contentId, documentPickerName3[1]); + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName4[0], contentName, contentId, documentPickerName4[1]); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuForContent(contentName); + await umbracoUi.content.clickTrashButton(); + // Verify the references list has 3 items and has the text '...and one more item' + await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); + await umbracoUi.content.doesReferenceItemsHaveCount(3); + await umbracoUi.content.isReferenceItemNameVisible(documentPickerName[0]); + await umbracoUi.content.isReferenceItemNameVisible(documentPickerName2[0]); + await umbracoUi.content.isReferenceItemNameVisible(documentPickerName3[0]); + await umbracoUi.content.doesReferencesContainText('...and one more item'); + await umbracoUi.content.clickConfirmTrashButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(contentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(contentName)).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(documentPickerName2[1]); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName3[1]); + await umbracoApi.documentType.ensureNameNotExists(documentPickerName4[1]); +}); + +test('can trash a content node with multiple cultures that references one item', async ({umbracoApi, umbracoUi}) => { + // Arrange + const firstCulture = 'en-US'; + const secondCulture = 'da'; + await umbracoApi.language.createDanishLanguage(); + // Create a content node with multiple cultures + const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, dataTypeName, dataTypeId); + const contentId = await umbracoApi.document.createDocumentWithTwoCulturesAndTextContent(contentName, documentTypeId, contentText, dataTypeName, firstCulture, secondCulture); + await umbracoApi.document.publishDocumentWithCulture(contentId, firstCulture); + await umbracoApi.document.publishDocumentWithCulture(contentId, secondCulture); + // Create a document link picker + await umbracoApi.document.createDefaultDocumentWithOneDocumentLink(documentPickerName[0], contentName, contentId, documentPickerName[1]); + await umbracoUi.goToBackOffice(); + await umbracoUi.content.goToSection(ConstantHelper.sections.content); + + // Act + await umbracoUi.content.clickActionsMenuForContent(contentName); + await umbracoUi.content.clickTrashButton(); + // Verify the references list + await umbracoUi.content.doesReferenceHeadlineHaveText(referenceHeadline); + await umbracoUi.content.doesReferenceItemsHaveCount(1); + await umbracoUi.content.isReferenceItemNameVisible(documentPickerName[0]); + await umbracoUi.content.clickConfirmTrashButton(); + + // Assert + await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.movedToRecycleBin); + expect(await umbracoApi.document.doesNameExist(contentName)).toBeFalsy(); + await umbracoUi.content.isItemVisibleInRecycleBin(contentName); + expect(await umbracoApi.document.doesItemExistInRecycleBin(contentName)).toBeTruthy(); + + // Clean + await umbracoApi.language.ensureIsoCodeNotExists(secondCulture); +}); \ No newline at end of file