Skip to content

Commit

Permalink
feat(edit content): Add settings to Binary Field content type (#27455)
Browse files Browse the repository at this point in the history
* create binary settings component

* make the formulary save and delete

* add variables in settings to blacklist

* apply settings to binary field

* Update dot-edit-content-binary-field.component.scss

* change divider color

* add test cases

* clean tests

* add test cases

* add translations

* fix sonarq

* add test cases

* add test cases

* Refactor, make PR suggestions

* Fixed behavior when dont have value

* Fixed getter, missed on merge

* Removed unnecesary code

---------

Co-authored-by: KevinDavilaDotCMS <144152756+KevinDavilaDotCMS@users.noreply.github.com>
  • Loading branch information
zJaaal and KevinDavilaDotCMS committed Feb 7, 2024
1 parent aca8ee5 commit fa55979
Show file tree
Hide file tree
Showing 17 changed files with 517 additions and 32 deletions.
@@ -0,0 +1,31 @@
<form class="wrapper" [formGroup]="form">
<div class="field">
<label for="allowed-file-type">{{ 'binary-field.settings.allow.type' | dm }}</label>
<input
id="allowed-file-type"
[style]="{ width: '100%' }"
formControlName="accept"
placeholder="ex: image/*"
pInputText
data-testId="setting-accept" />
<small style="display: inline-block; margin-top: 0.5rem">
<a
href="https://www.dotcms.com/docs/latest/field-variables#BinaryField"
target="_blank"
rel="noopener"
>{{ 'binary-field-settings.system.link.to.documentation' | dm }}</a
>
</small>
</div>
@for( option of systemOptions; track option.key ) {
<p-divider
[style]="{
margin: '1rem 0'
}"></p-divider>
<div class="horizontal-field" formGroupName="systemOptions">
<p class="info" [innerHTML]="option.message | dm"></p>
<p-inputSwitch [formControlName]="option.key" data-testId="setting-switch"></p-inputSwitch>
</div>

}
</form>
@@ -0,0 +1,21 @@
@use "variables" as *;

.wrapper {
padding: $spacing-6;

.field {
padding: 0 $spacing-1;
}
}

.horizontal-field {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 $spacing-1;

.info {
margin: 0;
line-height: 140%;
}
}
@@ -0,0 +1,164 @@
import { Spectator, SpyObject, byTestId, createComponentFactory } from '@ngneat/spectator';
import { of, throwError } from 'rxjs';

import { NgFor } from '@angular/common';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';

import { DividerModule } from 'primeng/divider';
import { InputSwitchModule } from 'primeng/inputswitch';
import { InputTextModule } from 'primeng/inputtext';

import { DotMessageService, DotHttpErrorManagerService } from '@dotcms/data-access';
import { DotMessagePipe } from '@dotcms/ui';
import { MockDotMessageService } from '@dotcms/utils-testing';

import { DotBinarySettingsComponent } from './dot-binary-settings.component';

import { DotFieldVariablesService } from '../fields/dot-content-type-fields-variables/services/dot-field-variables.service';

const messageServiceMock = new MockDotMessageService({
'contenttypes.dropzone.action.save': 'Save',
'contenttypes.dropzone.action.cancel': 'Cancel'
});

const SYSTEM_OPTIONS = JSON.stringify({
allowURLImport: false,
allowFileNameEdit: false,
allowCodeWrite: true
});

describe('DotBinarySettingsComponent', () => {
let spectator: Spectator<DotBinarySettingsComponent>;
let component: DotBinarySettingsComponent;
let dotFieldVariableService: SpyObject<DotFieldVariablesService>;
let dotHttpErrorManagerService: SpyObject<DotHttpErrorManagerService>;

const createComponent = createComponentFactory({
component: DotBinarySettingsComponent,
imports: [
NgFor,
FormsModule,
ReactiveFormsModule,
InputTextModule,
InputSwitchModule,
DividerModule,
DotMessagePipe
],
providers: [
FormBuilder,
{
provide: DotFieldVariablesService,
useValue: {
load: () =>
of([
{
clazz: 'com.dotcms.contenttype.model.field.ImmutableStoryBlockField',
fieldId: 'f965a51b-130a-435f-b646-41e07d685363',
id: '9671d2c3-793b-41af-a485-e2c5fcba5fb',
key: 'systemOptions',
value: SYSTEM_OPTIONS
},
{
clazz: 'com.dotcms.contenttype.model.field.ImmutableStoryBlockField',
fieldId: 'f965a51b-130a-435f-b646-41e07d685363',
id: '9671d2c3-793b-41af-a485-e2c5fcba5fb',
key: 'accept',
value: 'image/*'
}
]),
save: () => of([]),
delete: () => of([])
}
},
{
provide: DotMessageService,
useValue: messageServiceMock
},
{
provide: DotHttpErrorManagerService,
useValue: {
handle: () => of([])
}
}
]
});

beforeEach(async () => {
spectator = createComponent({});
dotFieldVariableService = spectator.inject(DotFieldVariablesService);
dotHttpErrorManagerService = spectator.inject(DotHttpErrorManagerService);

component = spectator.component;
});

it('should setup form values', () => {
expect(component.form.get('accept').value).toBe('image/*');
expect(component.form.get('systemOptions').value).toEqual({
allowURLImport: false,
allowCodeWrite: true,
allowFileNameEdit: false
});
});

it('should emit changeControls when isVisible input is true', () => {
spyOn(component.changeControls, 'emit');

spectator.setInput('isVisible', true);

expect(component.changeControls.emit).toHaveBeenCalled();
});

it('should emit valid output on form change', () => {
spyOn(component.valid, 'emit');

const acceptInput = spectator.query(byTestId('setting-accept'));
spectator.typeInElement('text/*', acceptInput);

expect(component.valid.emit).toHaveBeenCalled();
});

it('should handler error if save properties failed', () => {
spyOn(dotFieldVariableService, 'save').and.returnValue(throwError({}));
spyOn(dotHttpErrorManagerService, 'handle').and.returnValue(of());
spyOn(component.save, 'emit');

component.saveSettings();

expect(dotHttpErrorManagerService.handle).toHaveBeenCalledTimes(1);
expect(component.save.emit).not.toHaveBeenCalled();
});
it('should not call save or delete when is empty and not previous variable exist', () => {
spyOn(dotFieldVariableService, 'load');
spyOn(dotFieldVariableService, 'delete').and.returnValue(of([]));
spyOn(dotFieldVariableService, 'save').and.returnValue(of([]));

component.form.get('accept').setValue('');
component.saveSettings();

expect(dotFieldVariableService.delete).not.toHaveBeenCalled();
expect(dotFieldVariableService.save).not.toHaveBeenCalledTimes(2); // One for accept and one for systemOptions, accept should not call save or delete
});

it('should have 3 switches with the corresponding control name', () => {
const switches = spectator.queryAll(byTestId('setting-switch'));

expect(switches.length).toBe(3);
expect(
switches.find((s) => s.getAttribute('ng-reflect-name') === 'allowURLImport')
).not.toBeNull();
expect(
switches.find((s) => s.getAttribute('ng-reflect-name') === 'allowCodeWrite')
).not.toBeNull();
expect(
switches.find((s) => s.getAttribute('ng-reflect-name') === 'allowFileNameEdit')
).not.toBeNull();
});

it('should have 1 input with the control name accept', () => {
const [acceptInput] = spectator.queryAll(byTestId('setting-accept'));

expect(acceptInput).not.toBeNull();

expect(acceptInput.getAttribute('ng-reflect-name')).toBe('accept');
});
});
@@ -0,0 +1,170 @@
import { forkJoin, of } from 'rxjs';

import { NgFor } from '@angular/common';
import { HttpErrorResponse } from '@angular/common/http';
import {
ChangeDetectionStrategy,
Component,
DestroyRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
inject
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';

import { DividerModule } from 'primeng/divider';
import { InputSwitchModule } from 'primeng/inputswitch';
import { InputTextModule } from 'primeng/inputtext';

import { catchError, take, tap } from 'rxjs/operators';

import { DotDialogActions } from '@components/dot-dialog/dot-dialog.component';
import { DotHttpErrorManagerService, DotMessageService } from '@dotcms/data-access';
import { DotCMSContentTypeField, DotFieldVariable } from '@dotcms/dotcms-models';
import { DotMessagePipe } from '@dotcms/ui';

import { DotFieldVariablesService } from '../fields/dot-content-type-fields-variables/services/dot-field-variables.service';

@Component({
selector: 'dot-binary-settings',
standalone: true,
imports: [
NgFor,
FormsModule,
ReactiveFormsModule,
InputTextModule,
InputSwitchModule,
DividerModule,
DotMessagePipe
],
templateUrl: './dot-binary-settings.component.html',
styleUrl: './dot-binary-settings.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DotBinarySettingsComponent implements OnInit, OnChanges {
@Input() field: DotCMSContentTypeField;
@Input() isVisible: boolean = false;

@Output() changeControls = new EventEmitter<DotDialogActions>();
@Output() valid = new EventEmitter<boolean>();
@Output() save = new EventEmitter<DotFieldVariable[]>();

form: FormGroup;

private fb: FormBuilder = inject(FormBuilder);
private fieldVariablesService = inject(DotFieldVariablesService);
private dotMessageService = inject(DotMessageService);
private dotHttpErrorManagerService = inject(DotHttpErrorManagerService);
private readonly destroyRef = inject(DestroyRef);
private FIELD_VARIABLES: Record<string, DotFieldVariable> = {};

protected readonly systemOptions = [
{
key: 'allowURLImport',
message: 'binary-field.settings.system.options.allow.url.import'
},
{
key: 'allowCodeWrite',
message: 'binary-field.settings.system.options.allow.code.write'
},
{
key: 'allowFileNameEdit',
message: 'binary-field.settings.system.options.allow.file.name.edit'
}
];

ngOnChanges(changes: SimpleChanges) {
const { isVisible } = changes;
if (isVisible?.currentValue) {
this.changeControls.emit(this.dialogActions());
}
}

ngOnInit(): void {
this.form = this.fb.group({
accept: '',
systemOptions: this.fb.group({
allowURLImport: false,
allowCodeWrite: false,
allowFileNameEdit: false
})
});

this.form.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.valid.emit(this.form.valid);
});

this.fieldVariablesService.load(this.field).subscribe({
next: (fieldVariables: DotFieldVariable[]) => {
fieldVariables.forEach((variable) => {
const { key, value } = variable;
const control = this.form.get(key);
if (control instanceof FormGroup) {
const systemOptions = JSON.parse(value);

this.systemOptions.forEach(({ key }) => {
control.get(key)?.setValue(systemOptions[key]);
});
} else {
control.setValue(value);
}

this.FIELD_VARIABLES = { ...this.FIELD_VARIABLES, [key]: variable };
});
}
});
}

saveSettings(): void {
const updateActions = Object.keys(this.form.controls).map((key) => {
const control = this.form.get(key);
const value =
control instanceof FormGroup ? JSON.stringify(control.value) : control.value;
const fieldVariable: DotFieldVariable = {
...this.FIELD_VARIABLES[key],
key,
value
};

if (!value) {
return of({});
}

return (
value
? this.fieldVariablesService.save(this.field, fieldVariable)
: this.fieldVariablesService.delete(this.field, fieldVariable)
).pipe(tap((variable) => (this.FIELD_VARIABLES[key] = variable))); // Update Variable Reference
});

forkJoin(updateActions)
.pipe(
take(1),
catchError((err: HttpErrorResponse) =>
this.dotHttpErrorManagerService.handle(err).pipe(take(1))
)
)
.subscribe((value: DotFieldVariable[]) => {
this.form.markAsPristine();
this.save.emit(value);
});
}

private dialogActions() {
return {
cancel: {
label: this.dotMessageService.get('contenttypes.dropzone.action.cancel')
},
accept: {
action: () => this.saveSettings(),
disabled: this.form.invalid || this.form.pristine,
label: this.dotMessageService.get('contenttypes.dropzone.action.save')
}
};
}
}

0 comments on commit fa55979

Please sign in to comment.