mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-47579 feat(core): Introduce EDIT_JSON_EDITOR injection token and add copy button to EditJsonDialogComponent (#12046)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InjectionToken, InputSignal, ModelSignal, Type } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Contract for a pluggable JSON editor component.
|
||||
*
|
||||
* The component participates in a two-way binding for its content: the dialog binds
|
||||
* its own value to the `value` model, so user edits made inside the editor flow back
|
||||
* out automatically (no manual signal mutation across the component boundary).
|
||||
*/
|
||||
export interface JsonEditorComponent {
|
||||
/** Two-way bound JSON content. */
|
||||
value: ModelSignal<string>;
|
||||
/** Whether the editor is read-only. */
|
||||
readOnly: InputSignal<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* InjectionToken for swapping ONLY the editor control inside the JSON dialog.
|
||||
*
|
||||
* Default: `null` — the dialog uses the built-in textarea (backward-compatible).
|
||||
* Override: provide a component type that implements JsonEditorComponent.
|
||||
*
|
||||
* Example (Monaco editor):
|
||||
* ```typescript
|
||||
* export const provideMonacoJsonEditor = (): EnvironmentProviders =>
|
||||
* makeEnvironmentProviders([
|
||||
* { provide: EDIT_JSON_EDITOR, useValue: MonacoJsonEditorComponent }
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* The custom editor component must:
|
||||
* - Expose a two-way `value = model<string>()` for the JSON content
|
||||
* - Expose a `readOnly = input<boolean>()` and disable editing when it is `true`
|
||||
*/
|
||||
export const EDIT_JSON_EDITOR = new InjectionToken<Type<JsonEditorComponent> | null>('EDIT_JSON_EDITOR', {
|
||||
providedIn: 'root',
|
||||
factory: () => null
|
||||
});
|
||||
@@ -1,13 +1,21 @@
|
||||
<h1 mat-dialog-title>{{ title | translate }}</h1>
|
||||
<mat-dialog-content class="adf-edit-json-dialog-content">
|
||||
@if (customEditor) {
|
||||
<ng-container #editorHost />
|
||||
} @else {
|
||||
<textarea [(ngModel)]="value" [attr.readonly]="!editable ? true : null"></textarea>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close cdkFocusInitial>
|
||||
{{ 'CORE.DIALOG.EDIT_JSON.CLOSE' | translate }}
|
||||
</button>
|
||||
<button *ngIf="editable" mat-button [mat-dialog-close]="value">
|
||||
{{ 'CORE.DIALOG.EDIT_JSON.UPDATE' | translate }}
|
||||
@if (customEditor) {
|
||||
<button mat-button data-automation-id="adf-edit-json-dialog-copy" (click)="copyToClipboard()">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
{{ 'CORE.DIALOG.EDIT_JSON.COPY' | translate }}
|
||||
</button>
|
||||
}
|
||||
<button mat-button mat-dialog-close cdkFocusInitial>{{ 'CORE.DIALOG.EDIT_JSON.CLOSE' | translate }}</button>
|
||||
@if (editable) {
|
||||
<button mat-button [mat-dialog-close]="value()">{{ 'CORE.DIALOG.EDIT_JSON.UPDATE' | translate }}</button>
|
||||
}
|
||||
</mat-dialog-actions>
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Component, input, model } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ClipboardService } from '../../clipboard';
|
||||
import { NoopTranslateModule } from '../../testing';
|
||||
import { UnitTestingUtils } from '../../testing/unit-testing-utils';
|
||||
import { EditJsonDialogComponent } from './edit-json.dialog';
|
||||
import { EDIT_JSON_EDITOR, JsonEditorComponent } from './edit-json-editor.token';
|
||||
|
||||
@Component({ template: '<span data-automation-id="stub-json-editor">{{ value() }}</span>' })
|
||||
class StubJsonEditorComponent implements JsonEditorComponent {
|
||||
readonly value = model('');
|
||||
readonly readOnly = input(false);
|
||||
}
|
||||
|
||||
describe('EditJsonDialogComponent', () => {
|
||||
let fixture: ComponentFixture<EditJsonDialogComponent>;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopTranslateModule, EditJsonDialogComponent],
|
||||
providers: [
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { value: '{"key": "value"}', title: 'Test', editable: false } },
|
||||
{ provide: MatDialogRef, useValue: {} }
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(EditJsonDialogComponent);
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
describe('copy button', () => {
|
||||
it('should not be visible when no custom editor is provided', () => {
|
||||
const copyButton = testingUtils.getByDataAutomationId('adf-edit-json-dialog-copy');
|
||||
expect(copyButton).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('editor fallback', () => {
|
||||
it('should render the textarea when no custom editor is provided', () => {
|
||||
expect(fixture.nativeElement.querySelector('textarea')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('editable state', () => {
|
||||
it('should render the textarea as read-only when editable is false', () => {
|
||||
const textarea = fixture.nativeElement.querySelector('textarea');
|
||||
expect(textarea.getAttribute('readonly')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditJsonDialogComponent — editable', () => {
|
||||
let fixture: ComponentFixture<EditJsonDialogComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopTranslateModule, EditJsonDialogComponent],
|
||||
providers: [
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { value: '', editable: true } },
|
||||
{ provide: MatDialogRef, useValue: {} }
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(EditJsonDialogComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should render the textarea as editable when editable is true', () => {
|
||||
const textarea = fixture.nativeElement.querySelector('textarea');
|
||||
expect(textarea.getAttribute('readonly')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditJsonDialogComponent — custom editor', () => {
|
||||
let fixture: ComponentFixture<EditJsonDialogComponent>;
|
||||
|
||||
const getEditor = (): StubJsonEditorComponent => fixture.debugElement.query(By.directive(StubJsonEditorComponent)).componentInstance;
|
||||
|
||||
const setup = async (data: { value?: string; editable?: boolean }) => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopTranslateModule, EditJsonDialogComponent],
|
||||
providers: [
|
||||
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: EDIT_JSON_EDITOR, useValue: StubJsonEditorComponent }
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(EditJsonDialogComponent);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should render the provided custom editor instead of the textarea', async () => {
|
||||
await setup({ value: '{"key": "value"}', editable: true });
|
||||
|
||||
expect(fixture.nativeElement.querySelector('textarea')).toBeNull();
|
||||
expect(fixture.nativeElement.querySelector('[data-automation-id="stub-json-editor"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should pass the initial value into the custom editor', async () => {
|
||||
await setup({ value: '{"key": "value"}', editable: true });
|
||||
|
||||
expect(getEditor().value()).toBe('{"key": "value"}');
|
||||
});
|
||||
|
||||
it('should bind readOnly to the negation of the editable flag', async () => {
|
||||
await setup({ value: '{}', editable: false });
|
||||
|
||||
expect(getEditor().readOnly()).toBe(true);
|
||||
});
|
||||
|
||||
it('should reflect edits from the custom editor back into the dialog value (two-way binding)', async () => {
|
||||
await setup({ value: '{"key": "value"}', editable: true });
|
||||
|
||||
getEditor().value.set('{"updated": true}');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.value()).toBe('{"updated": true}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('EditJsonDialogComponent — copy button with custom editor', () => {
|
||||
let fixture: ComponentFixture<EditJsonDialogComponent>;
|
||||
let clipboardService: ClipboardService;
|
||||
let translateService: TranslateService;
|
||||
let testingUtils: UnitTestingUtils;
|
||||
|
||||
const setup = (data: { value?: string; editable?: boolean }) => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopTranslateModule, EditJsonDialogComponent],
|
||||
providers: [
|
||||
{ provide: MAT_DIALOG_DATA, useValue: data },
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: EDIT_JSON_EDITOR, useValue: StubJsonEditorComponent }
|
||||
]
|
||||
});
|
||||
|
||||
clipboardService = TestBed.inject(ClipboardService);
|
||||
translateService = TestBed.inject(TranslateService);
|
||||
|
||||
fixture = TestBed.createComponent(EditJsonDialogComponent);
|
||||
testingUtils = new UnitTestingUtils(fixture.debugElement);
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should be visible', () => {
|
||||
setup({ value: '{"key": "value"}', editable: false });
|
||||
|
||||
const copyButton = testingUtils.getByDataAutomationId('adf-edit-json-dialog-copy');
|
||||
expect(copyButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should copy the dialog value to clipboard when clicked', () => {
|
||||
setup({ value: '{"key": "value"}', editable: false });
|
||||
spyOn(clipboardService, 'copyContentToClipboard');
|
||||
|
||||
testingUtils.clickByDataAutomationId('adf-edit-json-dialog-copy');
|
||||
|
||||
expect(clipboardService.copyContentToClipboard).toHaveBeenCalledWith('{"key": "value"}', jasmine.any(String));
|
||||
expect(clipboardService.copyContentToClipboard).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should show a confirmation notification when clicked', () => {
|
||||
setup({ value: '{"key": "value"}', editable: false });
|
||||
const translatedMessage = 'Copied to clipboard';
|
||||
spyOn(translateService, 'instant').and.returnValue(translatedMessage);
|
||||
spyOn(clipboardService, 'copyContentToClipboard');
|
||||
|
||||
testingUtils.clickByDataAutomationId('adf-edit-json-dialog-copy');
|
||||
|
||||
expect(clipboardService.copyContentToClipboard).toHaveBeenCalledWith('{"key": "value"}', translatedMessage);
|
||||
expect(clipboardService.copyContentToClipboard).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should copy the updated value when the dialog value changes', () => {
|
||||
setup({ value: '{"key": "value"}', editable: false });
|
||||
const updatedValue = '{"updated": true}';
|
||||
spyOn(clipboardService, 'copyContentToClipboard');
|
||||
fixture.componentInstance.value.set(updatedValue);
|
||||
fixture.detectChanges();
|
||||
|
||||
testingUtils.clickByDataAutomationId('adf-edit-json-dialog-copy');
|
||||
|
||||
expect(clipboardService.copyContentToClipboard).toHaveBeenCalledWith(updatedValue, jasmine.any(String));
|
||||
expect(clipboardService.copyContentToClipboard).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should copy an empty value when the dialog has no content', () => {
|
||||
setup({ value: '', editable: false });
|
||||
spyOn(clipboardService, 'copyContentToClipboard');
|
||||
|
||||
testingUtils.clickByDataAutomationId('adf-edit-json-dialog-copy');
|
||||
|
||||
expect(clipboardService.copyContentToClipboard).toHaveBeenCalledWith('', jasmine.any(String));
|
||||
expect(clipboardService.copyContentToClipboard).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -15,11 +15,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, Input, ViewEncapsulation, inject } from '@angular/core';
|
||||
import { AfterViewInit, Component, ViewEncapsulation, inject, model, viewChild, ViewContainerRef, inputBinding, twoWayBinding } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ClipboardService } from '../../clipboard';
|
||||
import { EDIT_JSON_EDITOR } from './edit-json-editor.token';
|
||||
|
||||
export interface EditJsonDialogSettings {
|
||||
title?: string;
|
||||
@@ -29,26 +32,40 @@ export interface EditJsonDialogSettings {
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [MatDialogModule, FormsModule, MatButtonModule, TranslatePipe],
|
||||
imports: [MatDialogModule, FormsModule, MatButtonModule, MatIconModule, TranslatePipe],
|
||||
templateUrl: './edit-json.dialog.html',
|
||||
styleUrls: ['./edit-json.dialog.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
host: { class: 'adf-edit-json-dialog' }
|
||||
})
|
||||
export class EditJsonDialogComponent implements OnInit {
|
||||
private readonly settings = inject<EditJsonDialogSettings>(MAT_DIALOG_DATA);
|
||||
export class EditJsonDialogComponent implements AfterViewInit {
|
||||
private readonly settings = inject<EditJsonDialogSettings | null>(MAT_DIALOG_DATA);
|
||||
private readonly clipboardService = inject(ClipboardService);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
private readonly editorHost = viewChild('editorHost', { read: ViewContainerRef });
|
||||
|
||||
editable: boolean = false;
|
||||
title: string = 'JSON';
|
||||
protected readonly customEditor = inject(EDIT_JSON_EDITOR);
|
||||
protected title = this.settings?.title ?? 'JSON';
|
||||
|
||||
@Input()
|
||||
value: string = '';
|
||||
editable = this.settings?.editable ?? false;
|
||||
readonly value = model(this.settings?.value ?? '');
|
||||
|
||||
ngOnInit() {
|
||||
if (this.settings) {
|
||||
this.editable = this.settings.editable;
|
||||
this.value = this.settings.value || '';
|
||||
this.title = this.settings.title || 'JSON';
|
||||
}
|
||||
ngAfterViewInit(): void {
|
||||
this.renderCustomEditor();
|
||||
}
|
||||
|
||||
protected copyToClipboard(): void {
|
||||
const key = 'CORE.DIALOG.EDIT_JSON.COPIED';
|
||||
const message = this.translateService.instant(key);
|
||||
this.clipboardService.copyContentToClipboard(this.value(), message);
|
||||
}
|
||||
|
||||
private renderCustomEditor(): void {
|
||||
const host = this.editorHost();
|
||||
|
||||
if (!this.customEditor || !host) return;
|
||||
|
||||
host.clear();
|
||||
host.createComponent(this.customEditor, { bindings: [twoWayBinding('value', this.value), inputBinding('readOnly', () => !this.editable)] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './edit-json/edit-json-editor.token';
|
||||
export * from './edit-json/edit-json.dialog';
|
||||
|
||||
export * from './unsaved-changes-dialog/unsaved-changes-dialog.component';
|
||||
|
||||
@@ -171,7 +171,9 @@
|
||||
},
|
||||
"EDIT_JSON": {
|
||||
"CLOSE": "Close",
|
||||
"UPDATE": "Update"
|
||||
"UPDATE": "Update",
|
||||
"COPY": "Copy",
|
||||
"COPIED": "Copied to clipboard"
|
||||
},
|
||||
"UNSAVED_CHANGES": {
|
||||
"TITLE": "Unsaved changes",
|
||||
|
||||
Reference in New Issue
Block a user