-
Notifications
You must be signed in to change notification settings - Fork 468
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(edit content): Add settings to Binary Field content type (#27455)
* 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
1 parent
bc7c121
commit 356b1cc
Showing
17 changed files
with
517 additions
and
32 deletions.
There are no files selected for viewing
31 changes: 31 additions & 0 deletions
31
.../dot-content-types-edit/components/dot-binary-settings/dot-binary-settings.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
21 changes: 21 additions & 0 deletions
21
.../dot-content-types-edit/components/dot-binary-settings/dot-binary-settings.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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%; | ||
} | ||
} |
164 changes: 164 additions & 0 deletions
164
...t-content-types-edit/components/dot-binary-settings/dot-binary-settings.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); | ||
}); |
170 changes: 170 additions & 0 deletions
170
...ed/dot-content-types-edit/components/dot-binary-settings/dot-binary-settings.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') | ||
} | ||
}; | ||
} | ||
} |
Oops, something went wrong.