Merge pull request #1617 from umbraco/feature/validation-take-4
Feat: bind server feedback with validation system
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export interface UmbValidationMessageTranslator {
|
||||
match(message: string): boolean;
|
||||
translate(message: string): string;
|
||||
translate(message: string): undefined | string;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user