Merge pull request #1617 from umbraco/feature/validation-take-4

Feat: bind server feedback with validation system
This commit is contained in:
Niels Lyngsø
2024-04-16 14:14:58 +02:00
committed by GitHub
8 changed files with 53 additions and 68 deletions

View File

@@ -4,7 +4,7 @@ import { UMB_VALIDATION_CONTEXT } from './validation.context-token.js';
import { UMB_SERVER_MODEL_VALIDATION_CONTEXT } from './server-model-validation.context-token.js';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { ApiError, CancelError } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository';
type ServerFeedbackEntry = { path: string; messages: Array<string> };
@@ -40,10 +40,7 @@ export class UmbServerModelValidationContext
});
}
async askServerForValidation(
data: unknown,
requestPromise: Promise<{ data: unknown; error: ApiError | CancelError | undefined }>,
): Promise<void> {
async askServerForValidation(data: unknown, requestPromise: Promise<UmbDataSourceResponse<string>>): Promise<void> {
this.#context?.messages.removeMessagesByType('server');
this.#serverFeedback = [];
@@ -53,47 +50,32 @@ export class UmbServerModelValidationContext
this.#validatePromiseResolve = resolve;
});
// Ask the server for validation...
//const { data: feedback, error } = await requestPromise;
await requestPromise;
//console.log('VALIDATE — Got server response:');
//console.log(data, error);
// Store this state of the data for translator look ups:
this.#data = data;
/*
const fixedData = {
type: 'Error',
title: 'Validation failed',
status: 400,
detail: 'One or more properties did not pass validation',
operationStatus: 'PropertyValidationError',
errors: {
'$.values[0].value': ['#validation.invalidPattern'],
} as Record<string, Array<string>>,
missingProperties: [],
};
// Ask the server for validation...
const { error } = await requestPromise;
Object.keys(fixedData.errors).forEach((path) => {
this.#serverFeedback.push({ path, messages: fixedData.errors[path] });
});*/
this.#isValid = error ? false : true;
if (!this.#isValid) {
// We are missing some typing here, but we will just go wild with 'as any': [NL]
const readErrorBody = (error as any).body;
Object.keys(readErrorBody.errors).forEach((path) => {
this.#serverFeedback.push({ path, messages: readErrorBody.errors[path] });
});
}
//this.#isValid = data ? true : false;
//this.#isValid = false;
this.#isValid = true;
this.#validatePromiseResolve?.();
this.#validatePromiseResolve = undefined;
//this.#validatePromise = undefined;
// Translate feedback:
this.#serverFeedback = this.#serverFeedback.flatMap(this.#executeTranslatorsOnFeedback);
}
#executeTranslatorsOnFeedback = (feedback: ServerFeedbackEntry) => {
return this.#translators.flatMap((translator) => {
if (translator.match(feedback.path)) {
const newPath = translator.translate(feedback.path);
let newPath: string | undefined;
if ((newPath = translator.translate(feedback.path))) {
// TODO: I might need to restructure this part for adjusting existing feedback with a part-translation.
// Detect if some part is unhandled?
// If so only make a partial translation on the feedback, add a message for the handled part.
@@ -113,6 +95,7 @@ export class UmbServerModelValidationContext
if (this.#translators.indexOf(translator) === -1) {
this.#translators.push(translator);
}
// execute translators here?
}
removeTranslator(translator: UmbValidationMessageTranslator): void {

View File

@@ -34,7 +34,8 @@ export class UmbValidationMessagesManager {
return this.#messages.getValue().length !== 0;
}
/*messagesOf(path: string): Observable<Array<UmbValidationMessage>> {
/*
messagesOfPathAndDescendant(path: string): Observable<Array<UmbValidationMessage>> {
// Find messages that starts with the given path, if the path is longer then require a dot or [ as the next character. using a more performant way than Regex:
return this.#messages.asObservablePart((msgs) =>
msgs.filter(
@@ -43,7 +44,8 @@ export class UmbValidationMessagesManager {
(x.path.length === path.length || x.path[path.length] === '.' || x.path[path.length] === '['),
),
);
}*/
}
*/
messagesOfTypeAndPath(type: UmbValidationMessageType, path: string): Observable<Array<UmbValidationMessage>> {
// Find messages that matches the given type and path.

View File

@@ -6,7 +6,7 @@ const CtrlSymbol = Symbol();
const ObserveSymbol = Symbol();
export class UmbObserveValidationStateController extends UmbControllerBase {
constructor(host: UmbControllerHost, dataPath: string | undefined, callback: (invalid: boolean) => void) {
constructor(host: UmbControllerHost, dataPath: string | undefined, callback: (messages: boolean) => void) {
super(host, CtrlSymbol);
if (dataPath) {
this.consumeContext(UMB_VALIDATION_CONTEXT, (context) => {

View File

@@ -1,4 +1,3 @@
export interface UmbValidationMessageTranslator {
match(message: string): boolean;
translate(message: string): string;
translate(message: string): undefined | string;
}

View File

@@ -17,15 +17,24 @@ export class UmbVariantValuesValidationMessageTranslator
this.#context = context;
}
match(message: string): boolean {
//return message.startsWith('values[');
// regex match, which starts with "$.values[" and then a number and then continues:
return message.indexOf('$.values[') === 0;
}
translate(path: string): string {
// retrieve the number from the message values index:
const index = parseInt(path.substring(9, path.indexOf(']')));
//
translate(path: string) {
if (path.indexOf('$.values[') !== 0) {
// No translation anyway.
return;
}
const pathEnd = path.indexOf(']');
if (pathEnd === -1) {
// No translation anyway.
return;
}
// retrieve the number from the message values index: [NL]
const index = parseInt(path.substring(9, pathEnd));
if (isNaN(index)) {
// No translation anyway.
return;
}
// Get the data from the validation request, the context holds that for us: [NL]
const data = this.#context.getData();
const specificValue = data.values[index];

View File

@@ -24,9 +24,7 @@ export class UmbDocumentValidationRepository extends UmbRepositoryBase {
async validateCreate(model: DetailModelType, parentUnique: string | null) {
if (!model) throw new Error('Data is missing');
const { data, error } = await this.#validationDataSource.validateCreate(model, parentUnique);
return { data, error };
return this.#validationDataSource.validateCreate(model, parentUnique);
}
/**
@@ -39,8 +37,6 @@ export class UmbDocumentValidationRepository extends UmbRepositoryBase {
if (!model) throw new Error('Data is missing');
if (!model.unique) throw new Error('Unique is missing');
const { data, error } = await this.#validationDataSource.validateUpdate(model);
return { data, error };
return this.#validationDataSource.validateUpdate(model);
}
}

View File

@@ -45,18 +45,12 @@ export class UmbDocumentValidationServerDataSource {
};
// Maybe use: tryExecuteAndNotify
const { data, error } = await tryExecute(
return tryExecute(
//this.#host,
DocumentService.postDocumentValidate({
requestBody,
}),
);
if (data) {
return { data };
}
return { error };
}
/**
@@ -75,18 +69,12 @@ export class UmbDocumentValidationServerDataSource {
};
// Maybe use: tryExecuteAndNotify
const { data, error } = await tryExecute(
return tryExecute(
//this.#host,
DocumentService.putDocumentByIdValidate({
id: model.unique,
requestBody,
}),
);
if (!error) {
return { data };
}
return { error };
}
}

View File

@@ -54,6 +54,7 @@ import {
UmbVariantValuesValidationMessageTranslator,
} from '@umbraco-cms/backoffice/validation';
import { UmbDocumentBlueprintDetailRepository } from '@umbraco-cms/backoffice/document-blueprint';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
type EntityType = UmbDocumentDetailModel;
export class UmbDocumentWorkspaceContext
@@ -649,6 +650,13 @@ export class UmbDocumentWorkspaceContext
async () => {
// If data of the selection is not valid Then just save:
await this.#performSaveOrCreate(saveData);
// Notifying that the save was successful, but we did not publish, which is what we want to symbolize here. [NL]
const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT);
// TODO: Get rid of the save notification.
// TODO: Translate this message [NL]
notificationContext.peek('danger', {
data: { message: 'Document was not published, but we saved it for you.' },
});
// Reject even thought the save was successful, but we did not publish, which is what we want to symbolize here. [NL]
return await Promise.reject();
},