Browse Source

Fixes/fixes (#576)

Several bugfixes
pull/579/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
740a7c2294
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs
  2. 6
      frontend/app/features/content/pages/content/content-history-page.component.ts
  3. 36
      frontend/app/framework/angular/forms/control-errors.component.ts
  4. 2
      frontend/app/framework/angular/forms/editors/date-time-editor.component.html
  5. 10
      frontend/app/framework/angular/forms/editors/date-time-editor.component.ts
  6. 191
      frontend/app/framework/angular/forms/error-formatting.spec.ts
  7. 47
      frontend/app/framework/angular/forms/error-formatting.ts
  8. 9
      frontend/app/framework/services/localizer.service.spec.ts
  9. 73
      frontend/app/framework/services/localizer.service.ts
  10. 8
      frontend/app/shared/state/contents.forms.spec.ts
  11. 2
      frontend/app/shared/state/contents.forms.visitors.ts
  12. 1640
      frontend/package-lock.json
  13. 82
      frontend/package.json
  14. 3
      frontend/tsconfig.spec.json

2
backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs

@ -98,7 +98,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
.Hints("The title of the post."))
.AddString("Text", f => f
.AsRichText()
.Length(100)
.Required()
.Hints("The text of the post."))
.AddString("Slug", f => f
@ -124,7 +123,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates
.Hints("The title of the page."))
.AddString("Text", f => f
.AsRichText()
.Length(100)
.Required()
.Hints("The text of the page."))
.AddString("Slug", f => f

6
frontend/app/features/content/pages/content/content-history-page.component.ts

@ -79,7 +79,11 @@ export class ContentHistoryPageComponent extends ResourceOwner implements OnInit
}
public createDraft() {
this.contentsState.createDraft(this.content);
this.contentPage.checkPendingChangesBeforeChangingStatus().pipe(
filter(x => !!x),
switchMap(d => this.contentsState.createDraft(this.content)),
onErrorResumeNext())
.subscribe();
}
public delete() {

36
frontend/app/framework/angular/forms/control-errors.component.ts

@ -9,6 +9,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnC
import { AbstractControl, FormArray, FormGroupDirective } from '@angular/forms';
import { fadeAnimation, LocalizerService, StatefulComponent, Types } from '@app/framework/internal';
import { merge } from 'rxjs';
import { formatError } from './error-formatting';
interface State {
// The error messages to show.
@ -121,40 +122,7 @@ export class ControlErrorsComponent extends StatefulComponent<State> implements
if (this.control && this.control.invalid && this.isTouched && this.control.errors) {
for (const key in <any>this.control.errors) {
if (this.control.errors.hasOwnProperty(key)) {
let type = key.toLowerCase();
if (Types.isString(this.control.value)) {
if (type === 'minlength') {
type = 'minlengthstring';
}
if (type === 'maxlength') {
type = 'maxlengthstring';
}
if (type === 'exactlylength') {
type = 'exactlylengthstring';
}
if (type === 'betweenlength') {
type = 'betweenlengthstring';
}
}
const error = this.control.errors[key];
let message: string | null = null;
if (Types.isString(error['message'])) {
message = this.localizer.get(error['message']);
}
if (!message) {
const args = { ...error, field: this.displayFieldName };
message = this.localizer.getOrKey(`validation.${type}`, args);
}
const message = formatError(this.localizer, this.displayFieldName, key, this.control.errors[key], this.control.value);
if (message) {
errors.push(message);

2
frontend/app/framework/angular/forms/editors/date-time-editor.component.html

@ -27,7 +27,7 @@
</button>
</div>
<div class="form-group" *ngIf="!isDateTimeMode && shouldShowDateButtons">
<button type="button" class="btn btn-text-secondary" [disabled]="snapshot.isDisabled" (click)="writeNow()" title="i18n:common.dateTimeEditor.todayTooltip">
<button type="button" class="btn btn-text-secondary" [disabled]="snapshot.isDisabled" (click)="writeToday()" title="i18n:common.dateTimeEditor.todayTooltip">
{{ 'common.dateTimeEditor.today' | sqxTranslate }}
</button>
</div>

10
frontend/app/framework/angular/forms/editors/date-time-editor.component.ts

@ -142,6 +142,16 @@ export class DateTimeEditorComponent extends StatefulControlComponent<{}, string
this.updateControls();
}
public writeToday() {
this.dateTime = new DateTime(DateHelper.getLocalDate(DateTime.today().raw));
this.updateControls();
this.callChangeFormatted();
this.callTouched();
return false;
}
public writeNow() {
this.dateTime = DateTime.now();

191
frontend/app/framework/angular/forms/error-formatting.spec.ts

@ -0,0 +1,191 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { LocalizerService } from './../../services/localizer.service';
import { formatError } from './error-formatting';
import { ValidatorsEx } from './validators';
describe('formatErrors', () => {
const localizer = new LocalizerService({
'users.passwordConfirmValidationMessage': 'Passwords must be the same.',
'validation.between': '{field} must be between \'{min}\' and \'{max}\'.',
'validation.betweenlength': '{field|upper} must have between {minlength} and {maxlength} item(s).',
'validation.betweenlengthstring': '{field|upper} must have between {minlength} and {maxlength} character(s).',
'validation.email': '{field|upper} must be an email address.',
'validation.exactly': '{field|upper} must be exactly \'{expected}\'.',
'validation.exactlylength': '{field|upper} must have exactly {expected} item(s).',
'validation.exactlylengthstring': '{field|upper} must have exactly {expected} character(s).',
'validation.match': '{message}',
'validation.max': '{field|upper} must be less or equal to \'{max}\'.',
'validation.maxlength': '{field|upper} must not have more than {requiredlength} item(s).',
'validation.maxlengthstring': '{field|upper} must not have more than {requiredlength} character(s).',
'validation.min': '{field|upper} must be greater or equal to \'{min}\'.',
'validation.minlength': '{field|upper} must have at least {requiredlength} item(s).',
'validation.minlengthstring': '{field|upper} must have at least {requiredlength} character(s).',
'validation.pattern': '{field|upper} does not match to the pattern.',
'validation.patternmessage': '{message}',
'validation.required': '{field|upper} is required.',
'validation.requiredTrue': '{field|upper} is required.',
'validation.uniquestrings': '{field|upper} must not contain duplicate values.',
'validation.validarrayvalues': '{field|upper} contains an invalid value: {invalidvalue}.',
'validation.validdatetime': '{field|upper} is not a valid date time.',
'validation.validvalues': '{field|upper} is not a valid value.'
});
it('should format min', () => {
const error = validate(1, Validators.min(2));
expect(error).toEqual('MY_FIELD must be greater or equal to \'2\'.');
});
it('should format max', () => {
const error = validate(3, Validators.max(2));
expect(error).toEqual('MY_FIELD must be less or equal to \'2\'.');
});
it('should format required', () => {
const error = validate(undefined, Validators.required);
expect(error).toEqual('MY_FIELD is required.');
});
it('should format requiredTrue', () => {
const error = validate(undefined, Validators.requiredTrue);
expect(error).toEqual('MY_FIELD is required.');
});
it('should format email', () => {
const error = validate('invalid', Validators.email);
expect(error).toEqual('MY_FIELD must be an email address.');
});
it('should format minLength string', () => {
const error = validate('x', Validators.minLength(2));
expect(error).toEqual('MY_FIELD must have at least 2 character(s).');
});
it('should format maxLength string', () => {
const error = validate('xxx', Validators.maxLength(2));
expect(error).toEqual('MY_FIELD must not have more than 2 character(s).');
});
it('should format minLength array', () => {
const error = validate([1], Validators.minLength(2));
expect(error).toEqual('MY_FIELD must have at least 2 item(s).');
});
it('should format maxLength array', () => {
const error = validate([1, 1, 1], Validators.maxLength(2));
expect(error).toEqual('MY_FIELD must not have more than 2 item(s).');
});
it('should format match', () => {
const error = validate('123', Validators.pattern('[A-Z]'));
expect(error).toEqual('MY_FIELD does not match to the pattern.');
});
it('should format match with message', () => {
const error = validate('123', ValidatorsEx.pattern('[A-Z]', 'Custom Message'));
expect(error).toEqual('Custom Message');
});
it('should format between exactly', () => {
const error = validate(2, ValidatorsEx.between(3, 3));
expect(error).toEqual('MY_FIELD must be exactly \'3\'.');
});
it('should format between range', () => {
const error = validate(2, ValidatorsEx.between(3, 5));
expect(error).toEqual('MY_FIELD must be between \'3\' and \'5\'.');
});
it('should format betweenLength string exactly', () => {
const error = validate('xx', ValidatorsEx.betweenLength(3, 3));
expect(error).toEqual('MY_FIELD must have exactly 3 character(s).');
});
it('should format betweenLength string range', () => {
const error = validate('xx', ValidatorsEx.betweenLength(3, 5));
expect(error).toEqual('MY_FIELD must have between 3 and 5 character(s).');
});
it('should format betweenLength array exactly', () => {
const error = validate([1], ValidatorsEx.betweenLength(3, 3));
expect(error).toEqual('MY_FIELD must have exactly 3 item(s).');
});
it('should format betweenLength array range', () => {
const error = validate([1, 1], ValidatorsEx.betweenLength(3, 5));
expect(error).toEqual('MY_FIELD must have between 3 and 5 item(s).');
});
it('should format validDateTime', () => {
const error = validate('invalid', ValidatorsEx.validDateTime());
expect(error).toEqual('MY_FIELD is not a valid date time.');
});
it('should format validValues', () => {
const error = validate(5, ValidatorsEx.validValues([1, 2, 3]));
expect(error).toEqual('MY_FIELD is not a valid value.');
});
it('should format validArrayValues', () => {
const error = validate([2, 4], ValidatorsEx.validArrayValues([1, 2, 3]));
expect(error).toEqual('MY_FIELD contains an invalid value: 4.');
});
it('should format uniqueStrings', () => {
const error = validate(['1', '2', '2', '3'], ValidatorsEx.uniqueStrings());
expect(error).toEqual('MY_FIELD must not contain duplicate values.');
});
it('should format match', () => {
const formControl1 = new FormControl(1);
const formControl2 = new FormControl(2);
const formGroup = new FormGroup({
field1: formControl1,
field2: formControl2
});
const formError = ValidatorsEx.match('field2', 'i18n:users.passwordConfirmValidationMessage')!(formControl1)!;
const formMessage = formatError(localizer, 'MY_FIELD', Object.keys(formError)[0], Object.values(formError)[0], undefined);
expect(formMessage).toEqual('Passwords must be the same.');
formGroup.reset();
});
function validate(value: any, validator: ValidatorFn) {
const formControl = new FormControl(value);
const formError = validator(formControl)!;
const formMessage = formatError(localizer, 'MY_FIELD', Object.keys(formError)[0], Object.values(formError)[0], value);
return formMessage;
}
});

47
frontend/app/framework/angular/forms/error-formatting.ts

@ -0,0 +1,47 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights r vbeserved
*/
import { Types } from '@app/framework/internal';
import { LocalizerService } from '@app/shared';
export function formatError(localizer: LocalizerService, field: string, type: string, properties: any, value: any, errors?: any) {
type = type.toLowerCase();
if (Types.isString(value)) {
if (type === 'minlength') {
type = 'minlengthstring';
}
if (type === 'maxlength') {
type = 'maxlengthstring';
}
if (type === 'exactlylength') {
type = 'exactlylengthstring';
}
if (type === 'betweenlength') {
type = 'betweenlengthstring';
}
}
let message: string | null = properties['message'];
if (!Types.isString(message) && errors) {
message = errors[type];
}
if (!Types.isString(message)) {
message = `validation.${type}`;
}
const args = { ...properties, field };
message = localizer.getOrKey(message, args);
return message;
}

9
frontend/app/framework/services/localizer.service.spec.ts

@ -12,6 +12,7 @@ describe('LocalizerService', () => {
simple: 'Simple Result',
withLowerVar: 'Var: {var|lower}.',
withUpperVar: 'Var: {var|upper}.',
withMultiple: 'Text1: {text1}, Text2: {Text2}.',
withVar: 'Var: {var}.'
};
@ -82,4 +83,12 @@ describe('LocalizerService', () => {
expect(result).toEqual('Var: Upper.');
});
it('should return text with multiple variables', () => {
const localizer = new LocalizerService(translations);
const result = localizer.get('withMultiple', { Text1: 'Hello', Text2: 'World' });
expect(result).toEqual('Text1: Hello, Text2: World.');
});
});

73
frontend/app/framework/services/localizer.service.ts

@ -6,6 +6,7 @@
*/
import { Injectable } from '@angular/core';
import { compareStrings } from '../utils/array-helper';
export const LocalizerServiceFactory = (translations: Object) => {
return new LocalizerService(translations);
@ -56,46 +57,62 @@ export class LocalizerService {
return text;
}
private replaceVariables(text: string, args: ReadonlyArray<object>): string {
while (true) {
const indexOfStart = text.indexOf('{');
private replaceVariables(text: string, args: object): string {
text = text.replace(/{[^}]*}/g, (matched: string) => {
const inner = matched.substr(1, matched.length - 2);
if (indexOfStart < 0) {
break;
}
const indexOfEnd = text.indexOf('}');
let replaceValue: string;
const replace = text.substring(indexOfStart, indexOfEnd + 1);
if (matched.includes('|')) {
const splittedValue = inner.split('|');
text = text.replace(replace, (matched: string) => {
let replaceValue: string;
const key = splittedValue[0];
if (matched.includes('|')) {
const splittedValue = matched.split('|');
replaceValue = this.getVar(args, key);
replaceValue = this.handlePipeOption(args[splittedValue[0].substr(1)], splittedValue[1].slice(0, -1));
} else {
const key = matched.substring(1, matched.length - 1);
if (replaceValue) {
const transforms = splittedValue.slice(1);
replaceValue = args[key];
replaceValue = this.transform(replaceValue, transforms);
}
} else {
replaceValue = this.getVar(args, inner);
}
return replaceValue;
});
}
return replaceValue;
});
return text;
}
private handlePipeOption(value: string, pipeOption: string) {
switch (pipeOption) {
case 'lower':
return value.charAt(0).toLowerCase() + value.slice(1);
case 'upper':
return value.charAt(0).toUpperCase() + value.slice(1);
default:
return value;
private getVar(args: object, key: string) {
let value = args[key];
if (!value) {
for (const name in args) {
if (args.hasOwnProperty(name) && compareStrings(key, name) === 0) {
value = args[name];
break;
}
}
}
return value;
}
private transform(value: string, transforms: ReadonlyArray<string>) {
for (const transform of transforms) {
switch (transform) {
case 'lower':
value = value.charAt(0).toLowerCase() + value.slice(1);
break;
case 'upper':
value = value.charAt(0).toUpperCase() + value.slice(1);
break;
}
}
return value;
}
}

8
frontend/app/shared/state/contents.forms.spec.ts

@ -259,12 +259,18 @@ describe('DateTimeField', () => {
expect(FieldFormatter.format(dateField, '2017-12-12')).toBe('12/12/2017');
});
it('should format to date', () => {
it('should format datetime to date', () => {
const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) });
expect(FieldFormatter.format(dateField, '2017-12-12T16:00:00Z')).toBe('12/12/2017');
});
it('should format date to date', () => {
const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) });
expect(FieldFormatter.format(dateField, '2017-12-12T00:00:00Z')).toBe('12/12/2017');
});
it('should format to date time', () => {
const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime' }) });

2
frontend/app/shared/state/contents.forms.visitors.ts

@ -130,7 +130,7 @@ export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
const parsed = DateTime.parseISO(this.value);
if (properties.editor === 'Date') {
return parsed.toStringFormat('P');
return parsed.toStringFormatUTC('P');
} else {
return parsed.toStringFormat('Ppp');
}

1640
frontend/package-lock.json

File diff suppressed because it is too large

82
frontend/package.json

@ -17,31 +17,29 @@
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"
},
"dependencies": {
"@angular/animations": "10.0.11",
"@angular/cdk": "10.1.3",
"@angular/common": "10.0.11",
"@angular/core": "10.0.11",
"@angular/forms": "10.0.11",
"@angular/localize": "10.0.11",
"@angular/platform-browser": "10.0.11",
"@angular/platform-browser-dynamic": "10.0.11",
"@angular/platform-server": "10.0.11",
"@angular/router": "10.0.11",
"@angular/animations": "10.1.1",
"@angular/cdk": "10.2.0",
"@angular/common": "10.1.1",
"@angular/core": "10.1.1",
"@angular/forms": "10.1.1",
"@angular/localize": "10.1.1",
"@angular/platform-browser": "10.1.1",
"@angular/platform-browser-dynamic": "10.1.1",
"@angular/platform-server": "10.1.1",
"@angular/router": "10.1.1",
"@egjs/hammerjs": "2.0.17",
"@ngx-translate/core": "13.0.0",
"@ngx-translate/http-loader": "6.0.0",
"@types/codemirror": "0.0.97",
"@types/codemirror": "0.0.98",
"ace-builds": "1.4.12",
"angular-gridster2": "10.1.4",
"angular-gridster2": "10.1.5",
"angular-mentions": "1.2.0",
"angular2-chartjs": "0.5.1",
"babel-polyfill": "6.26.0",
"bootstrap": "4.5.2",
"core-js": "3.6.5",
"cropperjs": "2.0.0-alpha.1",
"date-fns": "2.15.0",
"date-fns": "2.16.1",
"font-awesome": "4.7.0",
"graphiql": "1.0.3",
"graphiql": "1.0.4",
"graphql": "15.3.0",
"image-focus": "1.1.2",
"keycharm": "0.3.1",
@ -54,46 +52,46 @@
"progressbar.js": "1.1.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"rxjs": "6.6.2",
"rxjs": "6.6.3",
"simplemde": "1.11.2",
"slugify": "1.4.5",
"tinymce": "5.4.2",
"tslib": "2.0.1",
"vis-data": "7.0.0",
"vis-network": "8.2.0",
"vis-network": "8.3.2",
"vis-util": "4.3.4",
"zone.js": "0.11.1"
},
"devDependencies": {
"@angular-devkit/build-optimizer": "0.1000.7",
"@angular/compiler": "10.0.11",
"@angular/compiler-cli": "10.0.11",
"@ngtools/webpack": "10.0.7",
"@types/core-js": "2.5.3",
"@types/jasmine": "3.5.13",
"@angular-devkit/build-optimizer": "0.1001.1",
"@angular/compiler": "10.1.1",
"@angular/compiler-cli": "10.1.1",
"@ngtools/webpack": "10.1.1",
"@types/core-js": "2.5.4",
"@types/jasmine": "3.5.14",
"@types/marked": "1.1.0",
"@types/mersenne-twister": "1.1.2",
"@types/mousetrap": "^1.6.3",
"@types/node": "14.6.0",
"@types/react": "16.9.46",
"@types/node": "14.10.1",
"@types/react": "16.9.49",
"@types/react-dom": "16.9.8",
"@types/simplemde": "1.11.7",
"@types/tinymce": "4.5.24",
"browserslist": "4.14.0",
"caniuse-lite": "1.0.30001117",
"browserslist": "4.14.2",
"caniuse-lite": "1.0.30001129",
"circular-dependency-plugin": "5.2.0",
"codelyzer": "6.0.0",
"copy-webpack-plugin": "6.0.3",
"css-loader": "4.2.1",
"copy-webpack-plugin": "6.1.0",
"css-loader": "4.3.0",
"cssnano": "4.1.10",
"entities": "2.0.3",
"file-loader": "6.0.0",
"html-loader": "1.2.1",
"html-webpack-plugin": "4.3.0",
"file-loader": "6.1.0",
"html-loader": "1.3.0",
"html-webpack-plugin": "4.4.1",
"ignore-loader": "0.1.2",
"istanbul-instrumenter-loader": "3.0.1",
"jasmine-core": "3.6.0",
"karma": "5.1.1",
"karma": "5.2.2",
"karma-chrome-launcher": "3.1.0",
"karma-cli": "2.0.0",
"karma-coverage-istanbul-reporter": "3.0.3",
@ -103,11 +101,11 @@
"karma-mocha-reporter": "2.2.5",
"karma-sourcemap-loader": "0.3.8",
"karma-webpack": "4.0.2",
"mini-css-extract-plugin": "0.10.0",
"mini-css-extract-plugin": "0.11.2",
"node-sass": "4.14.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
"optimize-css-assets-webpack-plugin": "5.0.4",
"postcss-import": "12.0.1",
"postcss-loader": "3.0.0",
"postcss-loader": "4.0.1",
"postcss-preset-env": "6.7.0",
"raw-loader": "4.0.1",
"resize-observer-polyfill": "1.5.1",
@ -115,18 +113,18 @@
"rxjs-tslint": "0.1.8",
"sass-lint": "1.13.1",
"sass-lint-webpack": "1.0.3",
"sass-loader": "9.0.3",
"sass-loader": "10.0.2",
"style-loader": "1.2.1",
"sugarss": "2.0.0",
"terser-webpack-plugin": "4.1.0",
"ts-loader": "8.0.2",
"terser-webpack-plugin": "4.2.0",
"ts-loader": "8.0.3",
"tsconfig-paths-webpack-plugin": "3.3.0",
"tslint": "6.1.3",
"tslint-immutable": "6.0.1",
"tslint-webpack-plugin": "2.1.0",
"typemoq": "2.1.0",
"typescript": "3.9",
"underscore": "1.10.2",
"typescript": "4.0",
"underscore": "1.11.0",
"webpack": "4.44.1",
"webpack-bundle-analyzer": "3.8.0",
"webpack-cli": "3.3.12",

3
frontend/tsconfig.spec.json

@ -1,5 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node", "jasmine"]
},
"include": [
"app/**/*.d.ts",
"app/**/*.spec.ts"

Loading…
Cancel
Save