Merge branch 'v15/dev' into v16/dev

This commit is contained in:
Andy Butland
2025-03-12 12:29:09 +01:00
52 changed files with 1012 additions and 847 deletions

View File

@@ -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
@@ -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

View File

@@ -69,11 +69,6 @@ public class PublishDocumentWithDescendantsController : DocumentControllerBase
publishBranchFilter |= PublishBranchFilter.IncludeUnpublished;
}
if (requestModel.ForceRepublish)
{
publishBranchFilter |= PublishBranchFilter.ForceRepublish;
}
return publishBranchFilter;
}
}

View File

@@ -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": {

View File

@@ -4,7 +4,5 @@ public class PublishDocumentWithDescendantsRequestModel
{
public bool IncludeUnpublishedDescendants { get; set; }
public bool ForceRepublish { get; set; }
public required IEnumerable<string> Cultures { get; set; }
}

View File

@@ -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; }

View File

@@ -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.
/// </remarks>
[Obsolete("Use IUserTwoFactorLoginService.GetSetupInfoWithStatusAsync. This will be removed in Umbraco 15.")]
[Obsolete("Use IUserTwoFactorLoginService.GetSetupInfoAsync. This will be removed in Umbraco 15.")]
Task<object?> GetSetupInfoAsync(Guid userOrMemberKey, string providerName);
/// <summary>
@@ -60,13 +60,13 @@ public interface ITwoFactorLoginService : IService
/// <summary>
/// Disables 2FA with Code.
/// </summary>
[Obsolete("Use IUserTwoFactorLoginService.DisableByCodeWithStatusAsync. This will be removed in Umbraco 15.")]
[Obsolete("Use IUserTwoFactorLoginService.DisableByCodeAsync. This will be removed in Umbraco 15.")]
Task<bool> DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code);
/// <summary>
/// Validates and Saves.
/// </summary>
[Obsolete("Use IUserTwoFactorLoginService.ValidateAndSaveWithStatusAsync. This will be removed in Umbraco 15.")]
[Obsolete("Use IUserTwoFactorLoginService.ValidateAndSaveAsync. This will be removed in Umbraco 15.")]
Task<bool> ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code);
}

View File

@@ -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))
{

View File

@@ -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",

View File

@@ -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.',

View File

@@ -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.',

View File

@@ -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.',

View File

@@ -2050,7 +2050,6 @@ export type PublishDocumentRequestModel = {
export type PublishDocumentWithDescendantsRequestModel = {
includeUnpublishedDescendants: boolean;
forceRepublish: boolean;
cultures: Array<(string)>;
};

View File

@@ -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> | 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' });
},

View File

@@ -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;
}
}

View File

@@ -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';

View File

@@ -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

View File

@@ -16,6 +16,7 @@ export class UmbMoveToEntityAction extends UmbEntityActionBase<MetaEntityActionM
data: {
treeAlias: this.args.meta.treeAlias,
foldersOnly: this.args.meta.foldersOnly,
pickableFilter: (treeItem) => treeItem.unique !== this.args.unique,
},
});

View File

@@ -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<PathPatternParamsType extend
}
export interface UmbTreePickerModalData<
TreeItemType,
TreeItemType = UmbTreeItemModel,
PathPatternParamsType extends UmbPathPatternParamsType = UmbPathPatternParamsType,
> extends UmbPickerModalData<TreeItemType> {
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<UmbTreePickerModalData<unknown>, UmbTreePickerModalValue>(
export const UMB_TREE_PICKER_MODAL = new UmbModalToken<UmbTreePickerModalData, UmbTreePickerModalValue>(
UMB_TREE_PICKER_MODAL_ALIAS,
{
modal: {

View File

@@ -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<string>(this);
#includeUnpublishedDescendants = false;
#forceRepublish = false;
@state()
_options: Array<UmbDocumentVariantOptionModel> = [];
@@ -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}></uui-toggle>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-toggle
id="forceRepublish"
label=${this.localize.term('content_forceRepublish')}
?checked=${this.value?.forceRepublish}
@change=${this.#onForceRepublishChange}></uui-toggle>
</uui-form-layout-item>
<div slot="actions">
<uui-button label=${this.localize.term('general_close')} @click=${this.#close}></uui-button>
<uui-button

View File

@@ -8,7 +8,6 @@ export interface UmbDocumentPublishWithDescendantsModalData extends UmbDocumentV
export interface UmbDocumentPublishWithDescendantsModalValue extends UmbDocumentVariantPickerValue {
includeUnpublishedDescendants?: boolean;
forceRepublish?: boolean;
}
export const UMB_DOCUMENT_PUBLISH_WITH_DESCENDANTS_MODAL = new UmbModalToken<

View File

@@ -71,14 +71,12 @@ export class UmbDocumentPublishingRepository extends UmbRepositoryBase {
* @param id
* @param variantIds
* @param includeUnpublishedDescendants
* @param forceRepublish
* @memberof UmbDocumentPublishingRepository
*/
async publishWithDescendants(
id: string,
variantIds: Array<UmbVariantId>,
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) {

View File

@@ -92,21 +92,18 @@ export class UmbDocumentPublishingServerDataSource {
* @param unique
* @param variantIds
* @param includeUnpublishedDescendants
* @param forceRepublish
* @memberof UmbDocumentPublishingServerDataSource
*/
async publishWithDescendants(
unique: string,
variantIds: Array<UmbVariantId>,
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(

View File

@@ -229,7 +229,6 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase<UmbDoc
unique,
variantIds,
result.includeUnpublishedDescendants ?? false,
result.forceRepublish ?? false,
);
if (!error) {

View File

@@ -1,13 +1,17 @@
import { css, html, nothing, repeat, customElement, property, classMap } from '@umbraco-cms/backoffice/external/lit';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { classMap, css, customElement, html, nothing, property, repeat } 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 { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui';
export type UmbCheckboxListItem = { label: string; value: string; checked: boolean; invalid?: boolean };
@customElement('umb-input-checkbox-list')
export class UmbInputCheckboxListElement extends UUIFormControlMixin(UmbLitElement, '') {
export class UmbInputCheckboxListElement extends UmbFormControlMixin<
string | undefined,
typeof UmbLitElement,
undefined
>(UmbLitElement, undefined) {
@property({ attribute: false })
public list: Array<UmbCheckboxListItem> = [];
@@ -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`<uui-checkbox
return html`
<uui-checkbox
class=${classMap({ invalid: !!item.invalid })}
?checked=${item.checked}
label=${item.label + (item.invalid ? ` (${this.localize.term('validation_legacyOption')})` : '')}
title=${item.invalid ? this.localize.term('validation_legacyOptionDescription') : ''}
value=${item.value}
?readonly=${this.readonly}></uui-checkbox>`;
?checked=${item.checked}
?readonly=${this.readonly}></uui-checkbox>
`;
}
static override readonly styles = [

View File

@@ -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<Array<string> | string | undefined, typeof UmbLitElement, undefined>(
UmbLitElement,
undefined,
)
implements UmbPropertyEditorUiElement
{
#selection: Array<string> = [];
@property({ type: Array })
public set value(value: Array<string> | string | undefined) {
public override set value(value: Array<string> | string | undefined) {
this.#selection = Array.isArray(value) ? value : value ? [value] : [];
}
public get value(): Array<string> | undefined {
public override get value(): Array<string> | 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<UmbCheckboxListItem> = [];
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`
<umb-input-checkbox-list
.list=${this._list}
.required=${this.mandatory}
.requiredMessage=${this.mandatoryMessage}
.selection=${this.#selection}
?readonly=${this.readonly}
@change=${this.#onChange}></umb-input-checkbox-list>
@change=${this.#onChange}>
</umb-input-checkbox-list>
`;
}
}

View File

@@ -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: [
{

View File

@@ -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
},
});
}

View File

@@ -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",

View File

@@ -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 ---');

View File

@@ -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',
},
],
}),

View File

@@ -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,8 +144,7 @@
{
"condition": "(!IncludeExample)",
"exclude": [
"[Cc]lient/src/dashboards/**",
"[Cc]lient/src/api/schemas.gen.ts"
"[Cc]lient/src/dashboards/**"
]
}
]

View File

@@ -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"
}
}

View File

@@ -2,7 +2,7 @@
"id": "Umbraco.Extension",
"name": "Umbraco.Extension",
"version": "0.0.0",
"allowPackageTelemetry": true,
"allowTelemetry": true,
"extensions": [
{
"name": "Umbraco ExtensionBundle",

View File

@@ -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
}
],
});
})

View File

@@ -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<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions>({
baseUrl: 'https://localhost:44389'
}));

View File

@@ -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';

View File

@@ -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;

View File

@@ -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<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
/**
* 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<string, unknown>;
};
export class UmbracoExtensionService {
public static ping<ThrowOnError extends boolean = false>(options?: Options<PingData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<PingResponse, unknown, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/umbracoextension/api/v1/ping',
...options
});
}
//#if(IncludeExample)
public static whatsMyName<ThrowOnError extends boolean = false>(options?: Options<WhatsMyNameData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<WhatsMyNameResponse, unknown, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/umbracoextension/api/v1/whatsMyName',
...options
});
}
public static whatsTheTimeMrWolf<ThrowOnError extends boolean = false>(options?: Options<WhatsTheTimeMrWolfData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<WhatsTheTimeMrWolfResponse, unknown, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/umbracoextension/api/v1/whatsTheTimeMrWolf',
...options
});
}
public static whoAmI<ThrowOnError extends boolean = false>(options?: Options<WhoAmIData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<WhoAmIResponse, unknown, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/umbracoextension/api/v1/whoAmI',
...options
});
}
//#endif
}

View File

@@ -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<ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) {
return (options?.client ?? client).get<PingResponse, PingError, ThrowOnError>({
...options,
url: '/umbraco/umbracoextension/api/v1/ping'
});
}
//#if(IncludeExample)
public static whatsMyName<ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) {
return (options?.client ?? client).get<WhatsMyNameResponse, WhatsMyNameError, ThrowOnError>({
...options,
url: '/umbraco/umbracoextension/api/v1/whatsMyName'
});
}
public static whatsTheTimeMrWolf<ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) {
return (options?.client ?? client).get<WhatsTheTimeMrWolfResponse, WhatsTheTimeMrWolfError, ThrowOnError>({
...options,
url: '/umbraco/umbracoextension/api/v1/whatsTheTimeMrWolf'
});
}
public static whoAmI<ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) {
return (options?.client ?? client).get<WhoAmIResponse, WhoAmIError, ThrowOnError>({
...options,
url: '/umbraco/umbracoextension/api/v1/whoAmI'
});
}
//#endif
}

View File

@@ -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<number>;
permissions: Array<string>;
granularPermissions: Array<DocumentGranularPermissionModel | UnknownTypeGranularPermissionModel>;
allowedSections: Array<string>;
};
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<string>;
granularPermissions: Array<DocumentGranularPermissionModel | UnknownTypeGranularPermissionModel>;
readonly allowedSections: Array<string>;
readonly userCount: number;
readonly allowedLanguages: Array<(number)>;
readonly allowedLanguages: Array<number>;
};
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<string>;
profileData: UserModel | UserProfileModel;
securityStamp?: string | null;
avatar?: string | null;
sessionTimeout: number;
startContentIds?: Array<(number)> | null;
startMediaIds?: Array<(number)> | null;
language?: (string) | null;
startContentIds?: Array<number> | null;
startMediaIds?: Array<number> | null;
language?: string | null;
kind: UserKindModel;
readonly groups: Array<(ReadOnlyUserGroupModel | UserGroupModel)>;
readonly groups: Array<ReadOnlyUserGroupModel | UserGroupModel>;
};
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);
export type PingErrors = {
/**
* The resource is protected and requires an authentication token
*/
401: unknown;
};
export type PingResponses = {
/**
* OK
*/
200: string;
};
export type PingResponse = PingResponses[keyof PingResponses];
//#if(IncludeExample)
export type WhatsMyNameResponse = (string);
export type WhatsMyNameData = {
body?: never;
path?: never;
query?: never;
url: '/umbraco/hackclient/api/v1/whatsMyName';
};
export type WhatsMyNameError = (unknown);
export type WhatsMyNameErrors = {
/**
* The resource is protected and requires an authentication token
*/
401: unknown;
};
export type WhatsTheTimeMrWolfResponse = (string);
export type WhatsMyNameResponses = {
/**
* OK
*/
200: string;
};
export type WhatsTheTimeMrWolfError = (unknown);
export type WhatsMyNameResponse = WhatsMyNameResponses[keyof WhatsMyNameResponses];
export type WhoAmIResponse = ((UserModel));
export type WhatsTheTimeMrWolfData = {
body?: never;
path?: never;
query?: never;
url: '/umbraco/hackclient/api/v1/whatsTheTimeMrWolf';
};
export type WhoAmIError = (unknown);
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 & {});
};

View File

@@ -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

View File

@@ -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,36 +110,64 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) {
this._yourName = data;
buttonElement.state = "success";
}
};
render() {
return html`
<uui-box headline="Who am I?">
<div slot="header">[Server]</div>
<h2><uui-icon name="icon-user"></uui-icon>${this._serverUserData?.email ? this._serverUserData.email : 'Press the button!'}</h2>
<h2>
<uui-icon name="icon-user"></uui-icon>${this._serverUserData?.email
? this._serverUserData.email
: "Press the button!"}
</h2>
<ul>
${this._serverUserData?.groups.map(group => html`<li>${group.name}</li>`)}
${this._serverUserData?.groups.map(
(group) => html`<li>${group.name}</li>`
)}
</ul>
<uui-button color="default" look="primary" @click="${this.#onClickWhoAmI}">
<uui-button
color="default"
look="primary"
@click="${this.#onClickWhoAmI}"
>
Who am I?
</uui-button>
<p>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.</p>
<p>
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.
</p>
</uui-box>
<uui-box headline="What's my Name?">
<div slot="header">[Server]</div>
<h2><uui-icon name="icon-user"></uui-icon> ${this._yourName}</h2>
<uui-button color="default" look="primary" @click="${this.#onClickWhatsMyName}">
<uui-button
color="default"
look="primary"
@click="${this.#onClickWhatsMyName}"
>
Whats my name?
</uui-button>
<p>This endpoint has a forced delay to show the button 'waiting' state for a few seconds before completing the request.</p>
<p>
This endpoint has a forced delay to show the button 'waiting' state
for a few seconds before completing the request.
</p>
</uui-box>
<uui-box headline="What's the Time?">
<div slot="header">[Server]</div>
<h2><uui-icon name="icon-alarm-clock"></uui-icon> ${this._timeFromMrWolf ? this._timeFromMrWolf.toLocaleString() : 'Press the button!'}</h2>
<uui-button color="default" look="primary" @click="${this.#onClickWhatsTheTimeMrWolf}">
<h2>
<uui-icon name="icon-alarm-clock"></uui-icon> ${this._timeFromMrWolf
? this._timeFromMrWolf.toLocaleString()
: "Press the button!"}
</h2>
<uui-button
color="default"
look="primary"
@click="${this.#onClickWhatsTheTimeMrWolf}"
>
Whats the time Mr Wolf?
</uui-button>
<p>This endpoint gets the current date and time from the server.</p>
@@ -138,8 +176,13 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) {
<uui-box headline="Who am I?" class="wide">
<div slot="header">[Context]</div>
<p>Current user email: <b>${this._contextCurrentUser?.email}</b></p>
<p>This is the JSON object available by consuming the 'UMB_CURRENT_USER_CONTEXT' context:</p>
<umb-code-block language="json" copy>${JSON.stringify(this._contextCurrentUser, null, 2)}</umb-code-block>
<p>
This is the JSON object available by consuming the
'UMB_CURRENT_USER_CONTEXT' context:
</p>
<umb-code-block language="json" copy
>${JSON.stringify(this._contextCurrentUser, null, 2)}</umb-code-block
>
</uui-box>
`;
}
@@ -164,13 +207,14 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) {
.wide {
grid-column: span 3;
}
`];
`,
];
}
export default ExampleDashboardElement;
declare global {
interface HTMLElementTagNameMap {
'example-dashboard': ExampleDashboardElement;
"example-dashboard": ExampleDashboardElement;
}
}

View File

@@ -2,17 +2,17 @@ export const manifests: Array<UmbExtensionManifest> = [
{
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",
},
],
}
},
];

View File

@@ -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 👋");
};

View File

@@ -3,6 +3,6 @@ export const manifests: Array<UmbExtensionManifest> = [
name: "Umbraco ExtensionEntrypoint",
alias: "Umbraco.Extension.Entrypoint",
type: "backofficeEntryPoint",
js: () => import("./entrypoint"),
}
js: () => import("./entrypoint.js"),
},
];

View File

@@ -13,5 +13,5 @@ export default defineConfig({
rollupOptions: {
external: [/^@umbraco/],
},
}
},
});

View File

@@ -26,6 +26,8 @@
</ItemGroup>
<ItemGroup>
<ClientAssetsInputs Include="Client\**" Exclude="$(DefaultItemExcludes)" />
<!-- Dont include the client folder as part of packaging nuget build -->
<Content Remove="Client\**" />
@@ -33,4 +35,23 @@
<None Include="Client\public\umbraco-package.json" Pack="false" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<!-- Restore and build Client files -->
<Target Name="RestoreClient" Inputs="Client\package.json;Client\package-lock.json" Outputs="Client\node_modules\.package-lock.json">
<Message Importance="high" Text="Restoring Client NPM packages..." />
<Exec Command="npm i" WorkingDirectory="Client" />
</Target>
<Target Name="BuildClient" BeforeTargets="AssignTargetPaths" DependsOnTargets="RestoreClient" Inputs="@(ClientAssetsInputs)" Outputs="$(IntermediateOutputPath)client.complete.txt">
<Message Importance="high" Text="Executing Client NPM build script..." />
<Exec Command="npm run build" WorkingDirectory="Client" />
<ItemGroup>
<_ClientAssetsBuildOutput Include="wwwroot\App_Plugins\**" />
</ItemGroup>
<WriteLinesToFile File="$(IntermediateOutputPath)client.complete.txt" Lines="@(_ClientAssetsBuildOutput)" Overwrite="true" />
</Target>
</Project>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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);
});

View File

@@ -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);
});