From 62c30e8bd679f43610804bad44dfc9905fe35d24 Mon Sep 17 00:00:00 2001 From: edcrichton Date: Thu, 20 Aug 2026 17:53:38 +0100 Subject: [PATCH] A version of the sourcecode editor that pins / fixes the language of the editor as JSON --- .../bulk-edit-editor.component.ts | 12 +++- .../source-code-cell-editor.component.html | 1 + .../source-code-cell-editor.component.ts | 44 +++++++++---- .../edit-profile-section.component.html | 9 ++- .../edit-profile-section.component.ts | 28 ++++++++- .../profile-section.component.html | 9 ++- .../profile-section.component.ts | 22 ++++++- .../source-code-editor.component.html | 2 +- .../source-code-editor.component.ts | 53 ++++++++++++---- .../shared/source-code/source-code-value.ts | 61 +++++++++++++++++-- 10 files changed, 196 insertions(+), 45 deletions(-) diff --git a/src/app/bulk-edit/bulk-edit-editor/bulk-edit-editor.component.ts b/src/app/bulk-edit/bulk-edit-editor/bulk-edit-editor.component.ts index c4ef2c0c0..5cae86a48 100644 --- a/src/app/bulk-edit/bulk-edit-editor/bulk-edit-editor.component.ts +++ b/src/app/bulk-edit/bulk-edit-editor/bulk-edit-editor.component.ts @@ -63,7 +63,8 @@ import { MatButton } from '@angular/material/button'; import { MatToolbar } from '@angular/material/toolbar'; import { decodeSourceCodeValue, - isSourceCodeProfileDataType + getSourceCodeEditorOptionsForDataType, + isCodeEditorProfileDataType } from '@mdm/shared/source-code/source-code-value'; @Component({ @@ -361,14 +362,19 @@ export class BulkEditEditorComponent implements OnInit { }; } - if (isSourceCodeProfileDataType(field.dataType as string)) { + if (isCodeEditorProfileDataType(field.dataType as string)) { + const sourceCodeEditorOptions = getSourceCodeEditorOptionsForDataType( + field.dataType as string + ); + column.cellEditor = 'sourceCodeCellEditor'; column.cellEditorParams = { + ...sourceCodeEditorOptions, onFilePickerOpen: () => this.setStopEditingWhenCellsLoseFocus(false), onFilePickerClose: () => this.setStopEditingWhenCellsLoseFocus(true) }; column.valueFormatter = (params) => { - const value = decodeSourceCodeValue(params.value); + const value = decodeSourceCodeValue(params.value, sourceCodeEditorOptions); return value.source ? `${value.language}: ${value.source}` : ''; }; } diff --git a/src/app/bulk-edit/bulk-edit-editor/cell-editors/source-code-cell-editor/source-code-cell-editor.component.html b/src/app/bulk-edit/bulk-edit-editor/cell-editors/source-code-cell-editor/source-code-cell-editor.component.html index da167943b..d5a001317 100644 --- a/src/app/bulk-edit/bulk-edit-editor/cell-editors/source-code-cell-editor/source-code-cell-editor.component.html +++ b/src/app/bulk-edit/bulk-edit-editor/cell-editors/source-code-cell-editor/source-code-cell-editor.component.html @@ -28,6 +28,7 @@ Language diff --git a/src/app/bulk-edit/bulk-edit-editor/cell-editors/source-code-cell-editor/source-code-cell-editor.component.ts b/src/app/bulk-edit/bulk-edit-editor/cell-editors/source-code-cell-editor/source-code-cell-editor.component.ts index 97058050c..b352d584a 100644 --- a/src/app/bulk-edit/bulk-edit-editor/cell-editors/source-code-cell-editor/source-code-cell-editor.component.ts +++ b/src/app/bulk-edit/bulk-edit-editor/cell-editors/source-code-cell-editor/source-code-cell-editor.component.ts @@ -45,12 +45,16 @@ import { import { decodeSourceCodeValue, defaultSourceCodeLanguage, - encodeSourceCodeValue + encodeSourceCodeValue, + SourceCodeValueFormat } from '@mdm/shared/source-code/source-code-value'; export type PopupPosition = 'under' | 'over'; interface SourceCodeCellEditorParams { + fixedLanguage?: string + pinLanguage?: boolean + valueFormat?: SourceCodeValueFormat onFilePickerOpen?: () => void onFilePickerClose?: () => void } @@ -82,7 +86,11 @@ export class SourceCodeCellEditorComponent implements ICellEditorAngularComp, On agInit(params: ICellEditorParams & SourceCodeCellEditorParams): void { this.params = params; this.originalValue = this.params.value ?? ''; - const value = decodeSourceCodeValue(this.params.value); + const value = decodeSourceCodeValue(this.params.value, { + fixedLanguage: this.params.fixedLanguage, + pinLanguage: this.params.pinLanguage, + valueFormat: this.params.valueFormat + }); this.language = value.language; this.source = value.source; } @@ -107,7 +115,7 @@ export class SourceCodeCellEditorComponent implements ICellEditorAngularComp, On return encodeSourceCodeValue({ language: this.language, source: this.source - }); + }, this.params.valueFormat); } isPopup(): boolean { @@ -119,11 +127,13 @@ export class SourceCodeCellEditorComponent implements ICellEditorAngularComp, On } onLanguageChange() { + this.applyPinnedLanguage(); this.hasChanged = true; } onSourceChange(value: string) { this.source = value; + this.applyPinnedLanguage(); this.hasChanged = true; } @@ -139,23 +149,29 @@ export class SourceCodeCellEditorComponent implements ICellEditorAngularComp, On const file = target.files[0]; const extension = file.name.split('.').pop(); - const language = this.supportedLanguages.find( - lang => lang.fileExt === extension - ); - if (!language) { - this.importFileName = ''; - target.value = ''; - return; + if (!this.params.pinLanguage) { + const language = this.supportedLanguages.find( + lang => lang.fileExt === extension + ); + + if (!language) { + this.importFileName = ''; + target.value = ''; + return; + } + + this.language = language.value; } this.importFileName = file.name; - this.language = language.value; + this.applyPinnedLanguage(); this.hasChanged = true; const reader = new FileReader(); reader.onload = () => { this.source = reader.result?.toString() ?? ''; + this.applyPinnedLanguage(); this.hasChanged = true; }; reader.readAsText(file); @@ -196,4 +212,10 @@ export class SourceCodeCellEditorComponent implements ICellEditorAngularComp, On stopEvent(event: Event) { event.stopPropagation(); } + + private applyPinnedLanguage() { + if (this.params.pinLanguage && this.params.fixedLanguage) { + this.language = this.params.fixedLanguage; + } + } } diff --git a/src/app/modals/edit-profile-section/edit-profile-section.component.html b/src/app/modals/edit-profile-section/edit-profile-section.component.html index dcef0e800..1cc66bb5f 100644 --- a/src/app/modals/edit-profile-section/edit-profile-section.component.html +++ b/src/app/modals/edit-profile-section/edit-profile-section.component.html @@ -43,12 +43,15 @@

{{ profileSection.label }}

id="{{ field.fieldName }}" [inEditMode]="!field.uneditable" [rootElement]="data.catalogueItem" - *ngIf="field.dataType === 'text' && !isSourceCode(field)" + *ngIf="field.dataType === 'text' && !isCodeEditor(field)" > {{ profileSection.label }} field.dataType !== 'folder' && field.dataType !== 'model' && field.dataType != 'text' && - !isSourceCode(field) && + !isCodeEditor(field) && field.dataType != 'date' && field.dataType != 'datetime' && field.dataType != 'boolean' && diff --git a/src/app/modals/edit-profile-section/edit-profile-section.component.ts b/src/app/modals/edit-profile-section/edit-profile-section.component.ts index f68adeb4d..ce743e181 100644 --- a/src/app/modals/edit-profile-section/edit-profile-section.component.ts +++ b/src/app/modals/edit-profile-section/edit-profile-section.component.ts @@ -45,7 +45,11 @@ import { MatTooltip } from '@angular/material/tooltip'; import { FormsModule } from '@angular/forms'; import { NgIf, NgFor, NgClass } from '@angular/common'; import { SourceCodeEditorComponent } from '@mdm/shared/source-code/source-code-editor.component'; -import { isSourceCodeProfileDataType } from '@mdm/shared/source-code/source-code-value'; +import { + getSourceCodeEditorOptionsForDataType, + isCodeEditorProfileDataType, + SourceCodeValueFormat +} from '@mdm/shared/source-code/source-code-value'; @Component({ selector: '[mdm-edit-profile-section]', @@ -147,8 +151,26 @@ export class EditProfileSectionComponent implements OnInit { return items.sort((a, b) => (a > b ? 1 : a === b ? 0 : -1)); } - isSourceCode(field: ProfileField): boolean { - return isSourceCodeProfileDataType(field.dataType as string); + isCodeEditor(field: ProfileField): boolean { + return isCodeEditorProfileDataType(field.dataType as string); + } + + codeEditorFixedLanguage(field: ProfileField): string | undefined { + return getSourceCodeEditorOptionsForDataType( + field.dataType as string + ).fixedLanguage; + } + + codeEditorPinLanguage(field: ProfileField): boolean { + return !!getSourceCodeEditorOptionsForDataType( + field.dataType as string + ).pinLanguage; + } + + codeEditorValueFormat(field: ProfileField): SourceCodeValueFormat { + return getSourceCodeEditorOptionsForDataType( + field.dataType as string + ).valueFormat ?? 'sourcecode'; } showAddElementToMarkdown(field: ProfileField) { diff --git a/src/app/shared/profile-section/profile-section.component.html b/src/app/shared/profile-section/profile-section.component.html index b96fb506c..e48e756d0 100644 --- a/src/app/shared/profile-section/profile-section.component.html +++ b/src/app/shared/profile-section/profile-section.component.html @@ -44,12 +44,15 @@ id="{{ field.fieldName }}" [inEditMode]="false" [rootElement]="rootObject" - *ngIf="field.dataType === 'text' && !isSourceCode(field.dataType)" + *ngIf="field.dataType === 'text' && !isCodeEditor(field.dataType)" > Language (); @Input() inEditMode = true; + @Input() fixedLanguage?: string; + @Input() pinLanguage = false; + @Input() valueFormat: SourceCodeValueFormat = 'sourcecode'; language = defaultSourceCodeLanguage; source = ''; @@ -83,8 +87,17 @@ export class SourceCodeEditorComponent implements OnChanges, OnDestroy { private copiedTimeout?: ReturnType; ngOnChanges(changes: SimpleChanges): void { - if (changes.value) { - const decoded = decodeSourceCodeValue(this.value); + if ( + changes.value + || changes.fixedLanguage + || changes.pinLanguage + || changes.valueFormat + ) { + const decoded = decodeSourceCodeValue(this.value, { + fixedLanguage: this.fixedLanguage, + pinLanguage: this.pinLanguage, + valueFormat: this.valueFormat + }); this.language = decoded.language; this.source = decoded.source; this.highlightedSource = this.highlightSource(); @@ -113,12 +126,14 @@ export class SourceCodeEditorComponent implements OnChanges, OnDestroy { } onLanguageChange() { + this.applyPinnedLanguage(); this.highlightedSource = this.highlightSource(); this.emitValue(); } onSourceChange(value: string) { this.source = value; + this.applyPinnedLanguage(); this.highlightedSource = this.highlightSource(); this.emitValue(); } @@ -132,22 +147,29 @@ export class SourceCodeEditorComponent implements OnChanges, OnDestroy { const file = target.files[0]; const extension = file.name.split('.').pop(); - const language = this.supportedLanguages.find( - lang => lang.fileExt === extension - ); - if (!language) { - this.importFileName = ''; - target.value = ''; - return; + if (!this.pinLanguage) { + const language = this.supportedLanguages.find( + lang => lang.fileExt === extension + ); + + if (!language) { + this.importFileName = ''; + target.value = ''; + return; + } + + this.language = language.value; } this.importFileName = file.name; - this.language = language.value; + this.applyPinnedLanguage(); const reader = new FileReader(); reader.onload = () => { this.source = reader.result?.toString() ?? ''; + this.applyPinnedLanguage(); + this.highlightedSource = this.highlightSource(); this.emitValue(); }; reader.readAsText(file); @@ -168,10 +190,17 @@ export class SourceCodeEditorComponent implements OnChanges, OnDestroy { } private emitValue() { + this.applyPinnedLanguage(); this.valueChange.emit(encodeSourceCodeValue({ language: this.language, source: this.source - })); + }, this.valueFormat)); + } + + private applyPinnedLanguage() { + if (this.pinLanguage && this.fixedLanguage) { + this.language = this.fixedLanguage; + } } private highlightSource(): string { diff --git a/src/app/shared/source-code/source-code-value.ts b/src/app/shared/source-code/source-code-value.ts index d7aae16f4..a7f7f766b 100644 --- a/src/app/shared/source-code/source-code-value.ts +++ b/src/app/shared/source-code/source-code-value.ts @@ -20,32 +20,62 @@ export interface SourceCodeValue { source: string } +export type SourceCodeValueFormat = 'sourcecode' | 'plain'; + +export interface SourceCodeEditorOptions { + fixedLanguage?: string + pinLanguage?: boolean + valueFormat?: SourceCodeValueFormat +} + export const defaultSourceCodeLanguage = 'text'; -export const encodeSourceCodeValue = (value: SourceCodeValue): string => - JSON.stringify({ +export const encodeSourceCodeValue = ( + value: SourceCodeValue, + valueFormat: SourceCodeValueFormat = 'sourcecode' +): string => { + if (valueFormat === 'plain') { + return value.source ?? ''; + } + + return JSON.stringify({ language: value.language || defaultSourceCodeLanguage, source: value.source ?? '' }); +}; + +export const decodeSourceCodeValue = ( + value?: string, + options?: SourceCodeEditorOptions +): SourceCodeValue => { + const language = options?.fixedLanguage || defaultSourceCodeLanguage; -export const decodeSourceCodeValue = (value?: string): SourceCodeValue => { if (!value) { return { - language: defaultSourceCodeLanguage, + language, source: '' }; } + if (options?.valueFormat === 'plain') { + return { + language, + source: value + }; + } + try { const parsed = JSON.parse(value); return { - language: parsed.language || defaultSourceCodeLanguage, + language: options?.pinLanguage + ? language + : parsed.language || language, source: parsed.source ?? parsed.representation ?? '' }; } catch { return { - language: defaultSourceCodeLanguage, + language, source: value }; } @@ -53,3 +83,22 @@ export const decodeSourceCodeValue = (value?: string): SourceCodeValue => { export const isSourceCodeProfileDataType = (dataType?: string): boolean => dataType === 'sourcecode'; + +export const isCodeEditorProfileDataType = (dataType?: string): boolean => + dataType === 'sourcecode' || dataType === 'json'; + +export const getSourceCodeEditorOptionsForDataType = ( + dataType?: string +): SourceCodeEditorOptions => { + if (dataType === 'json') { + return { + fixedLanguage: 'json', + pinLanguage: true, + valueFormat: 'plain' + }; + } + + return { + valueFormat: 'sourcecode' + }; +};