mirror of https://github.com/Squidex/squidex.git
Browse Source
* Small UI improvements. * New validator rule and a small frontend improvements. * API tests. * Fix tests * Fix tests again.pull/1234/head
committed by
GitHub
124 changed files with 3987 additions and 2103 deletions
@ -0,0 +1,43 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Apps; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Infrastructure.Translations; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators; |
||||
|
|
||||
|
public sealed class NotChangedValidator(IRootField field, ContentData previousData) : IValidator |
||||
|
{ |
||||
|
public void Validate(object? value, ValidationContext context) |
||||
|
{ |
||||
|
var previousFieldData = |
||||
|
previousData.GetValueOrDefault(field.Name); |
||||
|
|
||||
|
var newFieldData = |
||||
|
value as ContentFieldData; |
||||
|
|
||||
|
var partitions = context.Root.App.PartitionResolver()(field.Partitioning); |
||||
|
|
||||
|
foreach (var partition in partitions.AllKeys) |
||||
|
{ |
||||
|
var previousLanguageValue = |
||||
|
previousFieldData?.GetValueOrDefault(partition); |
||||
|
|
||||
|
var newLanguageValue = |
||||
|
newFieldData?.GetValueOrDefault(partition); |
||||
|
|
||||
|
if (!Equals(previousLanguageValue, newLanguageValue)) |
||||
|
{ |
||||
|
var path = context.Path.Enqueue(partition); |
||||
|
|
||||
|
context.AddError(T.Get("contents.validation.createOnly"), path); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,144 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Core.TestHelpers; |
||||
|
using Squidex.Domain.Apps.Core.ValidateContent.Validators; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators; |
||||
|
|
||||
|
public class NotChangedValidatorTests : IClassFixture<TranslationsFixture> |
||||
|
{ |
||||
|
private readonly IRootField field = |
||||
|
Fields.String(1, "myField", Partitioning.Invariant, |
||||
|
new StringFieldProperties { IsCreateOnly = true }); |
||||
|
|
||||
|
private readonly List<string> errors = []; |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_add_error_if_value_is_wrong_type() |
||||
|
{ |
||||
|
var sut = new NotChangedValidator(field, []); |
||||
|
|
||||
|
await sut.ValidateAsync(true, errors); |
||||
|
|
||||
|
Assert.Empty(errors); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_not_add_error_if_values_are_the_same() |
||||
|
{ |
||||
|
var previousData = |
||||
|
new ContentData() |
||||
|
.AddField("myField", |
||||
|
new ContentFieldData() |
||||
|
.AddInvariant("Value1")); |
||||
|
|
||||
|
var newData = |
||||
|
new ContentFieldData() |
||||
|
.AddInvariant("Value1"); |
||||
|
|
||||
|
var sut = new NotChangedValidator(field, previousData); |
||||
|
|
||||
|
await sut.ValidateAsync(newData, errors); |
||||
|
|
||||
|
Assert.Empty(errors); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_values_differ() |
||||
|
{ |
||||
|
var previousData = |
||||
|
new ContentData() |
||||
|
.AddField("myField", |
||||
|
new ContentFieldData() |
||||
|
.AddInvariant("Value1")); |
||||
|
|
||||
|
var newData = |
||||
|
new ContentFieldData() |
||||
|
.AddInvariant("Value2"); |
||||
|
|
||||
|
var sut = new NotChangedValidator(field, previousData); |
||||
|
|
||||
|
await sut.ValidateAsync(newData, errors); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
["iv: Field cannot be changed after creation."]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_values_differ_as_new_data_does_not_have_field() |
||||
|
{ |
||||
|
var previousData = |
||||
|
new ContentData() |
||||
|
.AddField("myField", |
||||
|
new ContentFieldData() |
||||
|
.AddInvariant("Value1")); |
||||
|
|
||||
|
var newData = |
||||
|
new ContentFieldData(); |
||||
|
|
||||
|
var sut = new NotChangedValidator(field, previousData); |
||||
|
|
||||
|
await sut.ValidateAsync(newData, errors); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
["iv: Field cannot be changed after creation."]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_values_differ_as_new_data_is_null() |
||||
|
{ |
||||
|
var previousData = |
||||
|
new ContentData() |
||||
|
.AddField("myField", |
||||
|
new ContentFieldData() |
||||
|
.AddInvariant("Value1")); |
||||
|
|
||||
|
var sut = new NotChangedValidator(field, previousData); |
||||
|
|
||||
|
await sut.ValidateAsync(null, errors); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
["iv: Field cannot be changed after creation."]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_values_differ_as_previous_data_does_not_have_field() |
||||
|
{ |
||||
|
var newData = |
||||
|
new ContentFieldData() |
||||
|
.AddInvariant("Value2"); |
||||
|
|
||||
|
var sut = new NotChangedValidator(field, []); |
||||
|
|
||||
|
await sut.ValidateAsync(newData, errors); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
["iv: Field cannot be changed after creation."]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_error_if_values_differ_as_previous_data_does_not_have_value() |
||||
|
{ |
||||
|
var previousData = |
||||
|
new ContentData() |
||||
|
.AddField("myField", []); |
||||
|
|
||||
|
var newData = |
||||
|
new ContentFieldData() |
||||
|
.AddInvariant("Value2"); |
||||
|
|
||||
|
var sut = new NotChangedValidator(field, []); |
||||
|
|
||||
|
await sut.ValidateAsync(newData, errors); |
||||
|
|
||||
|
errors.Should().BeEquivalentTo( |
||||
|
["iv: Field cannot be changed after creation."]); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -1,30 +1,28 @@ |
|||||
<div class="card"> |
<div class="card card-body card-create mb-4"> |
||||
<div class="card-body"> |
<h5 class="card-title">{{ "roles.add.title" | sqxTranslate }}</h5> |
||||
<h5 class="card-title">{{ "roles.add.title" | sqxTranslate }}</h5> |
|
||||
|
|
||||
<form [formGroup]="addRoleForm.form" (ngSubmit)="addRole()"> |
<form [formGroup]="addRoleForm.form" (ngSubmit)="addRole()"> |
||||
<div class="row gx-2"> |
<div class="row gx-2"> |
||||
<div class="col"> |
<div class="col"> |
||||
<sqx-control-errors for="name" /> |
<sqx-control-errors for="name" /> |
||||
<input |
<input |
||||
class="form-control" |
class="form-control" |
||||
autocomplete="off" |
autocomplete="off" |
||||
formControlName="name" |
formControlName="name" |
||||
maxlength="40" |
maxlength="40" |
||||
placeholder="{{ 'roles.roleNamePlaceholder' | sqxTranslate }}" /> |
placeholder="{{ 'roles.roleNamePlaceholder' | sqxTranslate }}" /> |
||||
</div> |
</div> |
||||
|
|
||||
<div class="col-auto"> |
<div class="col-auto"> |
||||
<button class="btn btn-success" [disabled]="addRoleForm.hasNoName | async" type="submit"> |
<button class="btn btn-success" [disabled]="addRoleForm.hasNoName | async" type="submit"> |
||||
{{ "roles.add" | sqxTranslate }} |
{{ "roles.add" | sqxTranslate }} |
||||
</button> |
</button> |
||||
</div> |
</div> |
||||
|
|
||||
<div class="col-auto"> |
<div class="col-auto"> |
||||
<button class="btn btn-text-secondary" (click)="cancel()" type="button">{{ "common.cancel" | sqxTranslate }}</button> |
<button class="btn btn-text-secondary" (click)="cancel()" type="button">{{ "common.cancel" | sqxTranslate }}</button> |
||||
</div> |
|
||||
</div> |
</div> |
||||
</form> |
</div> |
||||
<sqx-form-hint> {{ "roles.add.description" | sqxTranslate }} </sqx-form-hint> |
</form> |
||||
</div> |
<sqx-form-hint> {{ "roles.add.description" | sqxTranslate }} </sqx-form-hint> |
||||
</div> |
</div> |
||||
|
|||||
@ -1,30 +1,28 @@ |
|||||
<div class="card mt-4"> |
<div class="card card-body card-create mb-4"> |
||||
<div class="card-body"> |
<h5 class="card-title">{{ "workflows.add.title" | sqxTranslate }}</h5> |
||||
<h5 class="card-title">{{ "workflows.add.title" | sqxTranslate }}</h5> |
|
||||
|
|
||||
<form [formGroup]="addWorkflowForm.form" (ngSubmit)="addWorkflow()"> |
<form [formGroup]="addWorkflowForm.form" (ngSubmit)="addWorkflow()"> |
||||
<div class="row gx-2"> |
<div class="row gx-2"> |
||||
<div class="col"> |
<div class="col"> |
||||
<sqx-control-errors for="name" /> |
<sqx-control-errors for="name" /> |
||||
<input |
<input |
||||
class="form-control" |
class="form-control" |
||||
autocomplete="off" |
autocomplete="off" |
||||
formControlName="name" |
formControlName="name" |
||||
maxlength="40" |
maxlength="40" |
||||
placeholder="{{ 'workflows.workflowNamePlaceholder' | sqxTranslate }}" /> |
placeholder="{{ 'workflows.workflowNamePlaceholder' | sqxTranslate }}" /> |
||||
</div> |
</div> |
||||
|
|
||||
<div class="col-auto"> |
<div class="col-auto"> |
||||
<button class="btn btn-success" [disabled]="addWorkflowForm.hasNoName | async" type="submit"> |
<button class="btn btn-success" [disabled]="addWorkflowForm.hasNoName | async" type="submit"> |
||||
{{ "workflows.add" | sqxTranslate }} |
{{ "workflows.add" | sqxTranslate }} |
||||
</button> |
</button> |
||||
</div> |
</div> |
||||
|
|
||||
<div class="col-auto"> |
<div class="col-auto"> |
||||
<button class="btn btn-text-secondary" (click)="cancel()" type="reset">{{ "common.cancel" | sqxTranslate }}</button> |
<button class="btn btn-text-secondary" (click)="cancel()" type="reset">{{ "common.cancel" | sqxTranslate }}</button> |
||||
</div> |
|
||||
</div> |
</div> |
||||
</form> |
</div> |
||||
<sqx-form-hint> {{ "workflows.add.description" | sqxTranslate }} </sqx-form-hint> |
</form> |
||||
</div> |
<sqx-form-hint> {{ "workflows.add.description" | sqxTranslate }} </sqx-form-hint> |
||||
</div> |
</div> |
||||
|
|||||
@ -0,0 +1,139 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { Component, inject, Input } from '@angular/core'; |
||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; |
||||
|
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; |
||||
|
import { ConfirmClickDirective, DialogRendererComponent, DialogService, ErrorDto, LocalizerService, RootViewComponent, TooltipDirective } from '@app/framework'; |
||||
|
|
||||
|
type TestMode = 'ErrorText' | 'ErrorDetails' | 'Info'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'sqx-test', |
||||
|
template: ` |
||||
|
<button class="btn btn-primary" (click)="test()"> |
||||
|
Show {{ mode }} |
||||
|
</button> |
||||
|
`,
|
||||
|
imports: [ |
||||
|
DialogRendererComponent, |
||||
|
], |
||||
|
}) |
||||
|
class TestComponent { |
||||
|
public readonly dialogs = inject(DialogService); |
||||
|
|
||||
|
@Input() |
||||
|
public mode: TestMode = 'Info'; |
||||
|
|
||||
|
public test() { |
||||
|
if (this.mode === 'ErrorDetails') { |
||||
|
const error = new ErrorDto(500, |
||||
|
'Error in Server', |
||||
|
'Error Code', |
||||
|
[ |
||||
|
'Details 1', |
||||
|
'Details 2', |
||||
|
'Details 3', |
||||
|
'Details 4', |
||||
|
], |
||||
|
); |
||||
|
|
||||
|
this.dialogs.notifyError(error); |
||||
|
} else if (this.mode === 'ErrorText') { |
||||
|
this.dialogs.notifyError('Error'); |
||||
|
} else { |
||||
|
this.dialogs.notifyInfo('Info'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
title: 'Framework/Dialogs', |
||||
|
component: DialogRendererComponent, |
||||
|
decorators: [ |
||||
|
moduleMetadata({ |
||||
|
imports: [ |
||||
|
BrowserAnimationsModule, |
||||
|
ConfirmClickDirective, |
||||
|
RootViewComponent, |
||||
|
TestComponent, |
||||
|
TooltipDirective, |
||||
|
], |
||||
|
providers: [ |
||||
|
DialogService, |
||||
|
{ |
||||
|
provide: LocalizerService, |
||||
|
useValue: new LocalizerService({ |
||||
|
'common.no': 'No', |
||||
|
'common.remember': 'Remember', |
||||
|
'common.yes': 'Yes', |
||||
|
}), |
||||
|
}, |
||||
|
], |
||||
|
}), |
||||
|
], |
||||
|
render: args => ({ |
||||
|
props: args, |
||||
|
template: ` |
||||
|
<sqx-root-view> |
||||
|
<div class="p-4"> |
||||
|
<h3>Dialogs</h3> |
||||
|
<div class="p-2 d-flex gap-2"> |
||||
|
<sqx-test [mode]="'ErrorText'" /> |
||||
|
<sqx-test [mode]="'ErrorDetails'" /> |
||||
|
<sqx-test [mode]="'Info'" /> |
||||
|
</div> |
||||
|
|
||||
|
<h3 class="mt-4">Tooltips</h3> |
||||
|
<div class="p-2 d-flex gap-2"> |
||||
|
<button class="btn btn-secondary" title="Tooltip" titlePosition="top">Top</button> |
||||
|
<button class="btn btn-secondary" title="Tooltip" titlePosition="left">Left</button> |
||||
|
<button class="btn btn-secondary" title="Tooltip" titlePosition="right">Right</button> |
||||
|
<button class="btn btn-secondary" title="Tooltip" titlePosition="bottom">Bottom</button> |
||||
|
</div> |
||||
|
|
||||
|
<h3 class="mt-4">Immediate Tooltips</h3> |
||||
|
<div class="p-2 d-flex gap-2"> |
||||
|
<button class="btn btn-secondary" [titleDelay]="0" title="Tooltip" titlePosition="top">Top</button> |
||||
|
<button class="btn btn-secondary" [titleDelay]="0" title="Tooltip" titlePosition="left">Left</button> |
||||
|
<button class="btn btn-secondary" [titleDelay]="0" title="Tooltip" titlePosition="right">Right</button> |
||||
|
<button class="btn btn-secondary" [titleDelay]="0" title="Tooltip" titlePosition="bottom">Bottom</button> |
||||
|
</div> |
||||
|
|
||||
|
<h3 class="mt-4">Confirm</h3> |
||||
|
<div class="p-2 d-flex gap-2"> |
||||
|
<button |
||||
|
class="btn btn-secondary" |
||||
|
confirmTitle="Show alert?" |
||||
|
confirmText="Really?" |
||||
|
(sqxConfirmClick)="alert('Click')"> |
||||
|
Confirm |
||||
|
</button> |
||||
|
|
||||
|
<button |
||||
|
class="btn btn-secondary" |
||||
|
confirmTitle="Show alert?" |
||||
|
confirmText="Really?" |
||||
|
confirmRememberKey="test" |
||||
|
(sqxConfirmClick)="alert('Click')"> |
||||
|
Confirm Remember |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<sqx-dialog-renderer /> |
||||
|
</sqx-root-view> |
||||
|
`,
|
||||
|
}), |
||||
|
} as Meta; |
||||
|
|
||||
|
type Story = StoryObj<DialogRendererComponent & { mode: TestMode }>; |
||||
|
|
||||
|
export const Primary: Story = { |
||||
|
args: { |
||||
|
}, |
||||
|
}; |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue