Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature: media server validation #17591

Merged
merged 9 commits into from
Dec 13, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export * from './controller/merge-content-variant-data.controller.js';
export * from './manager/index.js';
export * from './property-dataset-context/index.js';
export * from './workspace/index.js';
export type * from './repository/index.js';
export type * from './types.js';
export type * from './variant-picker/index.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { UmbContentDetailModel } from '../types.js';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbRepositoryResponse } from '@umbraco-cms/backoffice/repository';

export interface UmbContentValidationRepository<DetailModelType extends UmbContentDetailModel> {
validateCreate(model: DetailModelType, parentUnique: string | null): Promise<UmbRepositoryResponse<string>>;
validateSave(model: DetailModelType, variantIds: Array<UmbVariantId>): Promise<UmbRepositoryResponse<string>>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type * from './content-validation-repository.interface.js';
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { UmbMergeContentVariantDataController } from '../controller/merge-content-variant-data.controller.js';
import type { UmbContentVariantPickerData, UmbContentVariantPickerValue } from '../variant-picker/index.js';
import type { UmbContentPropertyDatasetContext } from '../property-dataset-context/index.js';
import type { UmbContentValidationRepository } from '../repository/content-validation-repository.interface.js';
import type { UmbContentWorkspaceContext } from './content-workspace-context.interface.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbDetailRepository, UmbDetailRepositoryConstructor } from '@umbraco-cms/backoffice/repository';
Expand Down Expand Up @@ -33,6 +34,7 @@
UMB_VALIDATION_CONTEXT,
UMB_VALIDATION_EMPTY_LOCALIZATION_KEY,
UmbDataPathVariantQuery,
UmbServerModelValidatorContext,
UmbValidationContext,
UmbVariantsValidationPathTranslator,
UmbVariantValuesValidationPathTranslator,
Expand All @@ -44,6 +46,7 @@
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api';

export interface UmbContentDetailWorkspaceContextArgs<
DetailModelType extends UmbContentDetailModel<VariantModelType>,
Expand All @@ -54,6 +57,8 @@
VariantOptionModelType extends UmbEntityVariantOptionModel = UmbEntityVariantOptionModel<VariantModelType>,
> extends UmbEntityDetailWorkspaceContextArgs {
contentTypeDetailRepository: UmbDetailRepositoryConstructor<ContentTypeDetailModelType>;
contentValidationRepository?: ClassConstructor<UmbContentValidationRepository<DetailModelType>>;
skipValidationOnSubmit?: boolean;
contentVariantScaffold: VariantModelType;
saveModalToken?: UmbModalToken<UmbContentVariantPickerData<VariantOptionModelType>, UmbContentVariantPickerValue>;
}
Expand Down Expand Up @@ -118,6 +123,11 @@
// TODO: fix type error
public readonly variantOptions;

#validateOnSubmit: boolean;
#serverValidation = new UmbServerModelValidatorContext(this);
#validationRepositoryClass?: ClassConstructor<UmbContentValidationRepository<DetailModelType>>;
#validationRepository?: UmbContentValidationRepository<DetailModelType>;

#saveModalToken?: UmbModalToken<UmbContentVariantPickerData<VariantOptionModelType>, UmbContentVariantPickerValue>;

constructor(
Expand All @@ -135,6 +145,8 @@
this.#saveModalToken = args.saveModalToken;

const contentTypeDetailRepository = new args.contentTypeDetailRepository(this);
this.#validationRepositoryClass = args.contentValidationRepository;
this.#validateOnSubmit = args.skipValidationOnSubmit ? !args.skipValidationOnSubmit : true;

Check warning on line 149 in src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v15/dev)

❌ Getting worse: Large Method

constructor increases from 93 to 95 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
this.structure = new UmbContentTypeStructureManager<ContentTypeDetailModelType>(this, contentTypeDetailRepository);
this.variesByCulture = this.structure.ownerContentTypeObservablePart((x) => x?.variesByCulture);
this.variesBySegment = this.structure.ownerContentTypeObservablePart((x) => x?.variesBySegment);
Expand Down Expand Up @@ -470,6 +482,36 @@
}
}

protected async _askServerToValidate(saveData: DetailModelType, variantIds: Array<UmbVariantId>) {
if (this.#validationRepositoryClass) {
// Create the validation repository if it does not exist. (we first create this here when we need it) [NL]
this.#validationRepository ??= new this.#validationRepositoryClass(this);

// We ask the server first to get a concatenated set of validation messages. So we see both front-end and back-end validation messages [NL]
if (this.getIsNew()) {
const parent = this.getParent();
if (!parent) throw new Error('Parent is not set');
await this.#serverValidation.askServerForValidation(
saveData,
this.#validationRepository.validateCreate(saveData, parent.unique),
);
} else {
await this.#serverValidation.askServerForValidation(
saveData,
this.#validationRepository.validateSave(saveData, variantIds),
);
}
}
}

/**
* Request a submit of the workspace, in the case of Document Workspaces the validation does not need to be valid for this to be submitted.
* @returns {Promise<void>} a promise which resolves once it has been completed.
*/
public override requestSubmit() {
return this._handleSubmit();
}

public override submit() {
return this._handleSubmit();
}
Expand Down Expand Up @@ -513,7 +555,19 @@

const saveData = await this._data.constructData(variantIds);
await this._runMandatoryValidationForSaveData(saveData);
await this._performCreateOrUpdate(variantIds, saveData);
if (this.#validateOnSubmit) {
await this._askServerToValidate(saveData, variantIds);
return this.validateAndSubmit(
async () => {
return this._performCreateOrUpdate(variantIds, saveData);
},
async () => {
return this.invalidSubmit();
},
);
} else {
await this._performCreateOrUpdate(variantIds, saveData);
}

Check warning on line 570 in src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v15/dev)

❌ Getting worse: Complex Method

_handleSubmit increases in cyclomatic complexity from 12 to 13, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
}

protected async _performCreateOrUpdate(variantIds: Array<UmbVariantId>, saveData: DetailModelType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { UmbDocumentValidationServerDataSource } from './document-validation.ser
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
import type { UmbContentValidationRepository } from '@umbraco-cms/backoffice/content';

type DetailModelType = UmbDocumentDetailModel;

export class UmbDocumentValidationRepository extends UmbRepositoryBase {
export class UmbDocumentValidationRepository
extends UmbRepositoryBase
implements UmbContentValidationRepository<DetailModelType>
{
#validationDataSource: UmbDocumentValidationServerDataSource;

constructor(host: UmbControllerHost) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,10 @@ import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity';

/**
* A server data source for Document Validation
* @class UmbDocumentPublishingServerDataSource
* @implements {DocumentTreeDataSource}
*/
export class UmbDocumentValidationServerDataSource {
//#host: UmbControllerHost;

/**
* Creates an instance of UmbDocumentPublishingServerDataSource.
* @param {UmbControllerHost} host - The controller host for this controller to be appended to
* @memberof UmbDocumentPublishingServerDataSource
*/
// TODO: [v15]: ignoring unused var here here to prevent a breaking change
// eslint-disable-next-line @typescript-eslint/no-unused-vars
constructor(host: UmbControllerHost) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UmbServerModelValidatorContext } from '@umbraco-cms/backoffice/validation';
import { UmbDocumentBlueprintDetailRepository } from '@umbraco-cms/backoffice/document-blueprint';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
import {
Expand Down Expand Up @@ -62,9 +61,6 @@
{
public readonly publishingRepository = new UmbDocumentPublishingRepository(this);

#serverValidation = new UmbServerModelValidatorContext(this);
#validationRepository?: UmbDocumentValidationRepository;

readonly isTrashed = this._data.createObservablePartOfCurrent((data) => data?.isTrashed);
readonly contentTypeUnique = this._data.createObservablePartOfCurrent((data) => data?.documentType.unique);
readonly contentTypeHasCollection = this._data.createObservablePartOfCurrent(
Expand All @@ -81,6 +77,8 @@
workspaceAlias: UMB_DOCUMENT_WORKSPACE_ALIAS,
detailRepositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS,
contentTypeDetailRepository: UmbDocumentTypeDetailRepository,
contentValidationRepository: UmbDocumentValidationRepository,
skipValidationOnSubmit: true,
contentVariantScaffold: UMB_DOCUMENT_DETAIL_MODEL_VARIANT_SCAFFOLD,
saveModalToken: UMB_DOCUMENT_SAVE_MODAL,
});
Expand Down Expand Up @@ -206,19 +204,6 @@
this._data.updateCurrent({ template: { unique: templateUnique } });
}

/**
* Request a submit of the workspace, in the case of Document Workspaces the validation does not need to be valid for this to be submitted.
* @returns {Promise<void>} a promise which resolves once it has been completed.
*/
public override requestSubmit() {
return this._handleSubmit();
}

// Because we do not make validation prevent submission this also submits the workspace. [NL]
public override invalidSubmit() {
return this._handleSubmit();
}

async #handleSaveAndPreview() {
const unique = this.getUnique();
if (!unique) throw new Error('Unique is missing');
Expand Down Expand Up @@ -287,23 +272,7 @@
const saveData = await this._data.constructData(variantIds);
await this._runMandatoryValidationForSaveData(saveData);

// Create the validation repository if it does not exist. (we first create this here when we need it) [NL]
this.#validationRepository ??= new UmbDocumentValidationRepository(this);

// We ask the server first to get a concatenated set of validation messages. So we see both front-end and back-end validation messages [NL]
if (this.getIsNew()) {
const parent = this.getParent();
if (!parent) throw new Error('Parent is not set');
this.#serverValidation.askServerForValidation(
saveData,
this.#validationRepository.validateCreate(saveData, parent.unique),
);
} else {
this.#serverValidation.askServerForValidation(
saveData,
this.#validationRepository.validateSave(saveData, variantIds),
);
}
await this._askServerToValidate(saveData, variantIds);

Check notice on line 275 in src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v15/dev)

✅ Getting better: Complex Method

UmbDocumentWorkspaceContext.handleSaveAndPublish decreases in cyclomatic complexity from 14 to 11, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

Check notice on line 275 in src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v15/dev)

✅ No longer an issue: Bumpy Road Ahead

UmbDocumentWorkspaceContext.handleSaveAndPublish is no longer above the threshold for logical blocks with deeply nested code

// TODO: Only validate the specified selection.. [NL]
return this.validateAndSubmit(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { UmbMediaValidationServerDataSource } from './media-validation.server.da
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
import type { UmbContentValidationRepository } from '@umbraco-cms/backoffice/content';

type DetailModelType = UmbMediaDetailModel;

export class UmbMediaValidationRepository extends UmbRepositoryBase {
export class UmbMediaValidationRepository
extends UmbRepositoryBase
implements UmbContentValidationRepository<DetailModelType>
{
#validationDataSource: UmbMediaValidationServerDataSource;

constructor(host: UmbControllerHost) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,11 @@ import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity';

/**
* A server data source for Media Validatiom
* A server data source for Media Validation
*/
export class UmbMediaValidationServerDataSource {
//#host: UmbControllerHost;

/**
* Creates an instance of UmbDocumentPublishingServerDataSource.
* @param {UmbControllerHost} host - The controller host for this controller to be appended to
* @memberof UmbDocumentPublishingServerDataSource
*/
// TODO: [v15]: ignoring unused var here here to prevent a breaking change
// eslint-disable-next-line @typescript-eslint/no-unused-vars
constructor(host: UmbControllerHost) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js';
import { UMB_MEDIA_DETAIL_REPOSITORY_ALIAS } from '../constants.js';
import type { UmbMediaDetailModel, UmbMediaVariantModel } from '../types.js';
import { UMB_CREATE_MEDIA_WORKSPACE_PATH_PATTERN, UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN } from '../paths.js';
import { UmbMediaValidationRepository } from '../repository/validation/media-validation.repository.js';
import { UMB_MEDIA_COLLECTION_ALIAS } from '../collection/constants.js';
import type { UmbMediaDetailRepository } from '../repository/index.js';
import { UMB_MEDIA_WORKSPACE_ALIAS, UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD } from './constants.js';
Expand Down Expand Up @@ -48,6 +49,7 @@ export class UmbMediaWorkspaceContext
workspaceAlias: UMB_MEDIA_WORKSPACE_ALIAS,
detailRepositoryAlias: UMB_MEDIA_DETAIL_REPOSITORY_ALIAS,
contentTypeDetailRepository: UmbMediaTypeDetailRepository,
contentValidationRepository: UmbMediaValidationRepository,
contentVariantScaffold: UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { UmbMemberDetailRepository } from './detail/index.js';
export { UmbMemberItemRepository, type UmbMemberItemModel } from './item/index.js';
export * from './validation/index.js';
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { manifests as detailManifests } from './detail/manifests.js';
import { manifests as itemManifests } from './item/manifests.js';
import { manifests as validationManifests } from './validation/manifests.js';

export const manifests: Array<UmbExtensionManifest> = [...detailManifests, ...itemManifests];
export const manifests: Array<UmbExtensionManifest> = [...detailManifests, ...itemManifests, ...validationManifests];
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { UmbMemberValidationRepository } from './member-validation.repository.js';
export { UMB_MEMBER_VALIDATION_REPOSITORY_ALIAS } from './manifests.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const UMB_MEMBER_VALIDATION_REPOSITORY_ALIAS = 'Umb.Repository.Member.Validation';

export const manifests: Array<UmbExtensionManifest> = [
{
type: 'repository',
alias: UMB_MEMBER_VALIDATION_REPOSITORY_ALIAS,
name: 'Member Validation Repository',
api: () => import('./member-validation.repository.js'),
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { UmbMemberDetailModel } from '../../types.js';
import { UmbMemberValidationServerDataSource } from './member-validation.server.data-source.js';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
import type { UmbContentValidationRepository } from '@umbraco-cms/backoffice/content';

type DetailModelType = UmbMemberDetailModel;

export class UmbMemberValidationRepository
extends UmbRepositoryBase
implements UmbContentValidationRepository<DetailModelType>
{
#validationDataSource: UmbMemberValidationServerDataSource;

constructor(host: UmbControllerHost) {
super(host);

this.#validationDataSource = new UmbMemberValidationServerDataSource(this);
}

/**
* Returns a promise with an observable of the detail for the given unique
* @param {DetailModelType} model - The model to validate
* @param {string | null} [parentUnique] - The parent unique
* @returns {*}
*/
async validateCreate(model: DetailModelType, parentUnique: string | null) {
if (!model) throw new Error('Data is missing');

return this.#validationDataSource.validateCreate(model, parentUnique);
}

/**
* Saves the given data
* @param {DetailModelType} model - The model to save
* @param {Array<UmbVariantId>} variantIds - The variant ids to save
* @returns {*}
*/
async validateSave(model: DetailModelType, variantIds: Array<UmbVariantId>) {
if (!model) throw new Error('Data is missing');
if (!model.unique) throw new Error('Unique is missing');

return this.#validationDataSource.validateUpdate(model, variantIds);
}
}

export { UmbMemberValidationRepository as api };
Loading
Loading