From 6edfc35d241580cd649f3ab4d890bf2276791016 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:24:40 +0200 Subject: [PATCH] Add single code paste validation --- .../src/lib/services/schemer-code-ops.spec.ts | 104 ++++++++++++++++ .../src/lib/services/schemer-code-ops.ts | 55 +++++++++ .../src/lib/services/schemer.service.spec.ts | 79 +++++++++++++ .../src/lib/services/schemer.service.ts | 111 +++++++++++++++++- .../src/lib/translations/de.json | 13 ++ .../var-coding/var-coding.component.spec.ts | 103 ++++++++++++++++ .../lib/var-coding/var-coding.component.ts | 82 ++++++++++++- 7 files changed, 542 insertions(+), 5 deletions(-) diff --git a/projects/ngx-coding-components/src/lib/services/schemer-code-ops.spec.ts b/projects/ngx-coding-components/src/lib/services/schemer-code-ops.spec.ts index 03fabbe..a83e25e 100644 --- a/projects/ngx-coding-components/src/lib/services/schemer-code-ops.spec.ts +++ b/projects/ngx-coding-components/src/lib/services/schemer-code-ops.spec.ts @@ -7,6 +7,7 @@ import { copySingleCode, deleteCode, duplicateCode, + getPasteSingleCodeWarningKeys, pasteSingleCode, sortCodes } from './schemer-code-ops'; @@ -89,6 +90,109 @@ describe('schemer-code-ops', () => { }); }); + describe('getPasteSingleCodeWarningKeys', () => { + it('should warn when copied rules reference incompatible target structures', () => { + const copied: CodeData = { + id: 1, + type: 'FULL_CREDIT', + label: '', + score: 1, + ruleSets: [ + { + valueArrayPos: 1, + ruleOperatorAnd: true, + rules: [ + { method: 'MATCH', parameters: ['A'], fragment: 0 }, + { method: 'NUMERIC_MATCH', parameters: ['1'] }, + { method: 'IS_TRUE' } + ] + } + ] + } as unknown as CodeData; + + expect( + getPasteSingleCodeWarningKeys( + copied, + { id: 'v1', sourceType: 'BASE', codes: [] } as unknown as never, + { + id: 'v1', + type: 'string', + multiple: false + } as unknown as never + ) + ).toEqual([ + 'code.paste-warning.array-reference', + 'code.paste-warning.fragment-reference', + 'code.paste-warning.numeric-rule', + 'code.paste-warning.boolean-rule' + ]); + }); + + it('should not warn when target supports copied rule references', () => { + const copied: CodeData = { + id: 1, + type: 'FULL_CREDIT', + label: '', + score: 1, + ruleSets: [ + { + valueArrayPos: 0, + ruleOperatorAnd: true, + rules: [ + { method: 'NUMERIC_MATCH', parameters: ['1'], fragment: 0 } + ] + } + ] + } as unknown as CodeData; + + expect( + getPasteSingleCodeWarningKeys( + copied, + { + id: 'v1', + sourceType: 'BASE', + fragmenting: '(\\d+)', + codes: [] + } as unknown as never, + { + id: 'v1', + type: 'integer', + multiple: true + } as unknown as never + ) + ).toEqual([]); + }); + + it('should warn for NUMERIC_RANGE rules copied into non-numeric targets', () => { + const copied: CodeData = { + id: 1, + type: 'FULL_CREDIT', + label: '', + score: 1, + ruleSets: [ + { + ruleOperatorAnd: true, + rules: [ + { method: 'NUMERIC_RANGE', parameters: ['1', '3'] } + ] + } + ] + } as unknown as CodeData; + + expect( + getPasteSingleCodeWarningKeys( + copied, + { id: 'v1', sourceType: 'BASE', codes: [] } as unknown as never, + { + id: 'v1', + type: 'string', + multiple: false + } as unknown as never + ) + ).toEqual(['code.paste-warning.numeric-rule']); + }); + }); + describe('addCode', () => { it('should return no-access for RO', () => { expect(addCode([], 'FULL_CREDIT', 'RO', orderOfCodeTypes)).toBe('code.error-message.no-access'); diff --git a/projects/ngx-coding-components/src/lib/services/schemer-code-ops.ts b/projects/ngx-coding-components/src/lib/services/schemer-code-ops.ts index a971b2b..287c2e3 100644 --- a/projects/ngx-coding-components/src/lib/services/schemer-code-ops.ts +++ b/projects/ngx-coding-components/src/lib/services/schemer-code-ops.ts @@ -1,8 +1,11 @@ import { CodeData, CodeType, + RuleMethod, + VariableCodingData, RuleSet } from '@iqbspecs/coding-scheme/coding-scheme.interface'; +import { VariableInfo } from '@iqbspecs/variable-info/variable-info.interface'; export type UserRoleType = 'RO' | 'RW_MINIMAL' | 'RW_MAXIMAL'; @@ -14,6 +17,18 @@ const residualTypes: CodeType[] = [ 'INTENDED_INCOMPLETE' ]; +const numericRuleMethods: RuleMethod[] = [ + 'NUMERIC_MATCH', + 'NUMERIC_RANGE', + 'NUMERIC_FULL_RANGE', + 'NUMERIC_MIN', + 'NUMERIC_MORE_THAN', + 'NUMERIC_LESS_THAN', + 'NUMERIC_MAX' +]; + +const booleanRuleMethods: RuleMethod[] = ['IS_TRUE', 'IS_FALSE']; + export const DEFAULT_RESIDUAL_MANUAL_INSTRUCTION = '

Alle anderen Antworten

'; @@ -49,6 +64,46 @@ export const canPasteSingleCodeInto = ( return true; }; +export const getPasteSingleCodeWarningKeys = ( + copiedCode: CodeData | null, + targetCoding?: VariableCodingData | null, + targetVarInfo?: VariableInfo +): string[] => { + if (!copiedCode) return []; + + const warningKeys = new Set(); + const ruleSets = copiedCode.ruleSets || []; + const rules = ruleSets.flatMap(ruleSet => ruleSet.rules || []); + + const hasArrayReference = ruleSets.some( + ruleSet => typeof ruleSet.valueArrayPos !== 'undefined' + ); + if (hasArrayReference && targetVarInfo && !targetVarInfo.multiple) { + warningKeys.add('code.paste-warning.array-reference'); + } + + const hasFragmentReference = rules.some(rule => typeof rule.fragment !== 'undefined'); + if (hasFragmentReference && targetCoding && !targetCoding.fragmenting) { + warningKeys.add('code.paste-warning.fragment-reference'); + } + + const hasNumericRule = rules.some(rule => numericRuleMethods.includes(rule.method)); + if ( + hasNumericRule && + targetVarInfo && + !['integer', 'number'].includes(targetVarInfo.type) + ) { + warningKeys.add('code.paste-warning.numeric-rule'); + } + + const hasBooleanRule = rules.some(rule => booleanRuleMethods.includes(rule.method)); + if (hasBooleanRule && targetVarInfo && targetVarInfo.type !== 'boolean') { + warningKeys.add('code.paste-warning.boolean-rule'); + } + + return Array.from(warningKeys); +}; + export const addCode = ( codeList: CodeData[], codeType: CodeType, diff --git a/projects/ngx-coding-components/src/lib/services/schemer.service.spec.ts b/projects/ngx-coding-components/src/lib/services/schemer.service.spec.ts index 1e3e3a2..61a7193 100644 --- a/projects/ngx-coding-components/src/lib/services/schemer.service.spec.ts +++ b/projects/ngx-coding-components/src/lib/services/schemer.service.spec.ts @@ -3,15 +3,22 @@ import { CodeType, VariableCodingData } from '@iqbspecs/coding-scheme/coding-scheme.interface'; +import { CodingSchemeFactory, CodingSchemeProblem } from '@iqb/responses'; import { SchemerService } from './schemer.service'; describe('SchemerService', () => { let service: SchemerService; + const copiedCodeStorageKey = 'iqb-schemer-copied-code'; beforeEach(() => { + sessionStorage.removeItem(copiedCodeStorageKey); service = new SchemerService(); }); + afterEach(() => { + sessionStorage.removeItem(copiedCodeStorageKey); + }); + describe('checkRenamedVarAliasOk', () => { it('should return false when alias or codingScheme is missing', () => { service.setCodingScheme(null); @@ -88,6 +95,22 @@ describe('SchemerService', () => { expect(service.copiedCode?.label).toBe('a'); }); + it('should restore copied code from session storage in a fresh service instance', () => { + service.setUserRole('RW_MAXIMAL'); + expect(service.copySingleCode({ + id: 1, + type: 'FULL_CREDIT', + label: 'from-storage', + score: 1, + ruleSets: [] + } as unknown as CodeData)).toBeTrue(); + + const freshService = new SchemerService(); + + expect(freshService.copiedCode?.label).toBe('from-storage'); + expect(freshService.canPasteSingleCodeInto([])).toBeTrue(); + }); + it('canPasteSingleCodeInto should block when nothing copied or user is RO', () => { service.setUserRole('RW_MAXIMAL'); expect(service.canPasteSingleCodeInto([])).toBeFalse(); @@ -284,6 +307,62 @@ describe('SchemerService', () => { }); }); + describe('getCodingProblemsForVarCoding', () => { + it('should return validation problems for the variable id or alias', () => { + service.setVarList([]); + service.setCodingScheme({ + variableCodings: [ + { id: 'v1', alias: 'A', sourceType: 'BASE' } as unknown as VariableCodingData, + { id: 'v2', alias: 'B', sourceType: 'BASE' } as unknown as VariableCodingData + ] + } as unknown as never); + spyOn(CodingSchemeFactory, 'validate').and.returnValue([ + { + variableId: 'A', + variableLabel: 'A', + type: 'RULE_PARAMETER_INVALID', + breaking: true, + code: '1' + }, + { + variableId: 'v1', + variableLabel: 'A', + type: 'RULE_REGEX_INVALID', + breaking: true, + code: '2' + }, + { + variableId: 'B', + variableLabel: 'B', + type: 'VACANT', + breaking: false + } + ] as CodingSchemeProblem[]); + + const problems = service.getCodingProblemsForVarCoding( + { id: 'v1', alias: 'A', sourceType: 'BASE' } as unknown as VariableCodingData + ); + + expect(problems.map(problem => problem.type)).toEqual([ + 'RULE_PARAMETER_INVALID', + 'RULE_REGEX_INVALID' + ]); + }); + + it('should return an empty list when validation fails', () => { + service.setCodingScheme({ + variableCodings: [ + { id: 'v1', alias: 'A', sourceType: 'BASE' } as unknown as VariableCodingData + ] + } as unknown as never); + spyOn(CodingSchemeFactory, 'validate').and.throwError('boom'); + + expect(service.getCodingProblemsForVarCoding( + { id: 'v1', alias: 'A', sourceType: 'BASE' } as unknown as VariableCodingData + )).toEqual([]); + }); + }); + describe('getVarInfoByCoding', () => { beforeEach(() => { service.setVarList([ diff --git a/projects/ngx-coding-components/src/lib/services/schemer.service.ts b/projects/ngx-coding-components/src/lib/services/schemer.service.ts index b325278..a04fbf7 100644 --- a/projects/ngx-coding-components/src/lib/services/schemer.service.ts +++ b/projects/ngx-coding-components/src/lib/services/schemer.service.ts @@ -1,5 +1,9 @@ import { Injectable, signal } from '@angular/core'; -import { CodingToTextMode } from '@iqb/responses'; +import { + CodingSchemeFactory, + CodingSchemeProblem, + CodingToTextMode +} from '@iqb/responses'; import { CodeData, CodeType, @@ -20,12 +24,76 @@ import { copySingleCode as copySingleCodeOp, deleteCode as deleteCodeOp, duplicateCode as duplicateCodeOp, + getPasteSingleCodeWarningKeys as getPasteSingleCodeWarningKeysOp, pasteSingleCode as pasteSingleCodeOp, sortCodes as sortCodesOp } from './schemer-code-ops'; export type UserRoleType = 'RO' | 'RW_MINIMAL' | 'RW_MAXIMAL'; export const VARIABLE_NAME_CHECK_PATTERN = /^[a-zA-Z0-9_]{2,}$/; +const COPIED_CODE_STORAGE_KEY = 'iqb-schemer-copied-code'; +const COPIED_CODE_STORAGE_TYPE = 'iqb-schemer-code-clipboard'; +const COPIED_CODE_STORAGE_VERSION = 1; + +interface StoredCopiedCode { + type: typeof COPIED_CODE_STORAGE_TYPE; + version: typeof COPIED_CODE_STORAGE_VERSION; + code: CodeData; +} + +const getSessionStorage = (): Storage | null => { + try { + if (typeof globalThis === 'undefined' || !globalThis.sessionStorage) { + return null; + } + return globalThis.sessionStorage; + } catch { + return null; + } +}; + +const readCopiedCodeFromStorage = (): CodeData | null => { + const storage = getSessionStorage(); + if (!storage) return null; + + try { + const serialized = storage.getItem(COPIED_CODE_STORAGE_KEY); + if (!serialized) return null; + const stored = JSON.parse(serialized) as StoredCopiedCode; + if ( + stored?.type !== COPIED_CODE_STORAGE_TYPE || + stored?.version !== COPIED_CODE_STORAGE_VERSION || + !stored?.code + ) { + return null; + } + return stored.code; + } catch { + return null; + } +}; + +const writeCopiedCodeToStorage = (code: CodeData | null): void => { + const storage = getSessionStorage(); + if (!storage) return; + + try { + if (!code) { + storage.removeItem(COPIED_CODE_STORAGE_KEY); + return; + } + storage.setItem( + COPIED_CODE_STORAGE_KEY, + JSON.stringify({ + type: COPIED_CODE_STORAGE_TYPE, + version: COPIED_CODE_STORAGE_VERSION, + code + } satisfies StoredCopiedCode) + ); + } catch { + // Ignore quota and privacy-mode failures; the in-memory clipboard still works. + } +}; @Injectable({ providedIn: 'root' @@ -75,11 +143,12 @@ export class SchemerService { } get copiedCode(): CodeData | null { - return this._copiedCode(); + return this.getCopiedCode(); } setCopiedCode(value: CodeData | null): void { this._copiedCode.set(value); + writeCopiedCodeToStorage(value); } get codingToTextMode(): CodingToTextMode { @@ -187,18 +256,43 @@ export class SchemerService { } canPasteSingleCodeInto(codeList: CodeData[]): boolean { - return canPasteSingleCodeIntoOp(this.copiedCode, codeList, this.userRole); + return canPasteSingleCodeIntoOp(this.getCopiedCode(), codeList, this.userRole); + } + + getPasteSingleCodeWarningKeys( + targetCoding?: VariableCodingData | null, + targetVarInfo?: VariableInfo + ): string[] { + return getPasteSingleCodeWarningKeysOp( + this.getCopiedCode(), + targetCoding, + targetVarInfo + ); } pasteSingleCode(codeList: CodeData[]): CodeData | string { return pasteSingleCodeOp( - this.copiedCode, + this.getCopiedCode(), codeList, this.userRole, this.orderOfCodeTypes ); } + getCodingProblemsForVarCoding(varCoding: VariableCodingData | null | undefined): CodingSchemeProblem[] { + if (!varCoding || !this.codingScheme?.variableCodings) return []; + + try { + const targetNames = [varCoding.id, varCoding.alias].filter(Boolean); + return CodingSchemeFactory.validate( + this.varList, + this.codingScheme.variableCodings + ).filter(problem => targetNames.includes(problem.variableId)); + } catch { + return []; + } + } + addCode(codeList: CodeData[], codeType: CodeType): CodeData | string { return addCodeOp(codeList, codeType, this.userRole, this.orderOfCodeTypes); } @@ -236,6 +330,15 @@ export class SchemerService { return returnValues.join(', '); } + private getCopiedCode(): CodeData | null { + const copiedCode = this._copiedCode(); + if (copiedCode) return copiedCode; + + const storedCode = readCopiedCodeFromStorage(); + if (storedCode) this._copiedCode.set(storedCode); + return storedCode; + } + getBaseVarsList() { if (this.codingScheme) { return this.codingScheme.variableCodings.filter( diff --git a/projects/ngx-coding-components/src/lib/translations/de.json b/projects/ngx-coding-components/src/lib/translations/de.json index 83d49d0..a833694 100644 --- a/projects/ngx-coding-components/src/lib/translations/de.json +++ b/projects/ngx-coding-components/src/lib/translations/de.json @@ -348,6 +348,19 @@ "type-not-supported": "Der Code-Typ wird nicht unterstützt.", "no-access": "Es fehlen für diese Aktion die Zugriffsrechte." }, + "paste-warning": { + "title": "Code einfügen?", + "content": "Der kopierte Code passt möglicherweise nicht vollständig zur Zielvariable. Nach dem Einfügen sollte die Kodierung geprüft werden.", + "confirm": "Trotzdem einfügen", + "array-reference": "Der Code enthält Positionsbezüge für Mehrfachantworten.", + "fragment-reference": "Der Code enthält Fragmentbezüge.", + "numeric-rule": "Der Code enthält numerische Regeln.", + "boolean-rule": "Der Code enthält Wahr/Falsch-Regeln." + }, + "post-paste-validation": { + "title": "Code eingefügt", + "content": "Der Code wurde eingefügt, die Zielkodierung enthält dadurch neue Validierungsprobleme:" + }, "no-codes": "Derzeit sind keine Codes definiert.", "is-invalid": "Kein Code wird vergeben, sondern Status 'ungültig'/'INVALID'.", "make-invalid": "Schalte Wert ungültig", diff --git a/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.spec.ts b/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.spec.ts index bc66d6a..a625725 100644 --- a/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.spec.ts +++ b/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.spec.ts @@ -18,6 +18,8 @@ describe('VarCodingComponent', () => { let schemerService: SchemerService; let addCodeSpy: jasmine.Spy; let canPasteSpy: jasmine.Spy; + let getPasteWarningsSpy: jasmine.Spy; + let getCodingProblemsSpy: jasmine.Spy; let pasteSpy: jasmine.Spy; let sortCodesSpy: jasmine.Spy; let getAliasSpy: jasmine.Spy; @@ -30,6 +32,8 @@ describe('VarCodingComponent', () => { addCodeSpy = jasmine.createSpy('addCode'); canPasteSpy = jasmine.createSpy('canPasteSingleCodeInto'); + getPasteWarningsSpy = jasmine.createSpy('getPasteSingleCodeWarningKeys').and.returnValue([]); + getCodingProblemsSpy = jasmine.createSpy('getCodingProblemsForVarCoding').and.returnValue([]); pasteSpy = jasmine.createSpy('pasteSingleCode'); sortCodesSpy = jasmine.createSpy('sortCodes'); getAliasSpy = jasmine.createSpy('getVariableAliasById').and.callFake((id: string) => `${id}_ALIAS`); @@ -60,6 +64,8 @@ describe('VarCodingComponent', () => { codingToTextMode: 'EXTENDED', addCode: addCodeSpy, canPasteSingleCodeInto: canPasteSpy, + getPasteSingleCodeWarningKeys: getPasteWarningsSpy, + getCodingProblemsForVarCoding: getCodingProblemsSpy, pasteSingleCode: pasteSpy, sortCodes: sortCodesSpy, getVariableAliasById: getAliasSpy, @@ -262,6 +268,103 @@ describe('VarCodingComponent', () => { expect(emitSpy).toHaveBeenCalledWith(component.varCoding); }); + it('pasteSingleCode should ask for confirmation before pasting incompatible copied codes', () => { + component.varCoding = { + id: 'v1', + alias: 'A', + sourceType: 'BASE', + codes: [] + } as unknown as VariableCodingData; + getPasteWarningsSpy.and.returnValue(['code.paste-warning.numeric-rule']); + + dialogOpenSpy.and.returnValue({ afterClosed: () => of(false) }); + component.pasteSingleCode(); + expect(dialogOpenSpy).toHaveBeenCalled(); + expect(pasteSpy).not.toHaveBeenCalled(); + + dialogOpenSpy.calls.reset(); + pasteSpy.calls.reset(); + dialogOpenSpy.and.returnValue({ afterClosed: () => of(undefined) }); + component.pasteSingleCode(); + expect(dialogOpenSpy).toHaveBeenCalled(); + expect(pasteSpy).not.toHaveBeenCalled(); + + dialogOpenSpy.calls.reset(); + pasteSpy.calls.reset(); + dialogOpenSpy.and.returnValue({ afterClosed: () => of(true) }); + pasteSpy.and.callFake((codeList: unknown[]) => { + codeList.push({ + id: 1, type: 'FULL_CREDIT', label: '', score: 1 + }); + return codeList[0]; + }); + + component.pasteSingleCode(); + expect(dialogOpenSpy).toHaveBeenCalled(); + expect(pasteSpy).toHaveBeenCalledWith(component.varCoding.codes); + }); + + it('pasteSingleCode should show validation warning when paste creates new coding problems', () => { + const emitSpy = spyOn(component.varCodingChanged, 'emit'); + component.varCoding = { + id: 'v1', + alias: 'A', + sourceType: 'BASE', + codes: [] + } as unknown as VariableCodingData; + getCodingProblemsSpy.and.returnValues( + [], + [{ + variableId: 'A', + variableLabel: 'A', + type: 'RULE_PARAMETER_INVALID', + breaking: true, + code: '1' + }] + ); + pasteSpy.and.callFake((codeList: unknown[]) => { + codeList.push({ + id: 1, type: 'FULL_CREDIT', label: '', score: 1 + }); + return codeList[0]; + }); + + component.pasteSingleCode(); + + expect(emitSpy).toHaveBeenCalledWith(component.varCoding); + expect(dialogOpenSpy).toHaveBeenCalled(); + expect(dialogOpenSpy.calls.mostRecent().args[1].data.content).toContain( + 'coding-problem.RULE_PARAMETER_INVALID' + ); + }); + + it('pasteSingleCode should not show validation warning for unchanged existing coding problems', () => { + component.varCoding = { + id: 'v1', + alias: 'A', + sourceType: 'BASE', + codes: [] + } as unknown as VariableCodingData; + const existingProblem = { + variableId: 'A', + variableLabel: 'A', + type: 'RULE_PARAMETER_INVALID', + breaking: true, + code: '1' + }; + getCodingProblemsSpy.and.returnValues([existingProblem], [existingProblem]); + pasteSpy.and.callFake((codeList: unknown[]) => { + codeList.push({ + id: 1, type: 'FULL_CREDIT', label: '', score: 1 + }); + return codeList[0]; + }); + + component.pasteSingleCode(); + + expect(dialogOpenSpy).not.toHaveBeenCalled(); + }); + it('smartSchemer ctrlKey should infer code types from labels, sort codes and emit', () => { const emitSpy = spyOn(component.varCodingChanged, 'emit'); component.varCoding = { diff --git a/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.ts b/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.ts index c8ca435..38db482 100644 --- a/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.ts +++ b/projects/ngx-coding-components/src/lib/var-coding/var-coding.component.ts @@ -11,7 +11,7 @@ import { import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { MatDialog } from '@angular/material/dialog'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; -import { CodingToTextMode } from '@iqb/responses'; +import { CodingSchemeProblem, CodingToTextMode } from '@iqb/responses'; import { BehaviorSubject, debounceTime, Subscription } from 'rxjs'; import { MatCard, @@ -365,6 +365,39 @@ export class VarCodingComponent implements OnInit, OnDestroy, OnChanges { if (!this.varCoding || !this.varCoding.codes) { return; } + const warningKeys = this.schemerService.getPasteSingleCodeWarningKeys( + this.varCoding, + this.varInfo + ); + if (warningKeys.length > 0) { + const warningContent = [ + this.translateService.instant('code.paste-warning.content'), + ...warningKeys.map(key => this.translateService.instant(key)) + ].join(' '); + const dialogRef = this.confirmDialog.open(ConfirmDialogComponent, { + width: '500px', + data: { + title: this.translateService.instant('code.paste-warning.title'), + content: warningContent, + confirmButtonLabel: this.translateService.instant('code.paste-warning.confirm'), + showCancel: true + } + }); + dialogRef.afterClosed().subscribe(result => { + if (result === true) this.performPasteSingleCode(); + }); + return; + } + this.performPasteSingleCode(); + } + + private performPasteSingleCode() { + if (!this.varCoding || !this.varCoding.codes) { + return; + } + const problemsBeforePaste = this.schemerService.getCodingProblemsForVarCoding( + this.varCoding + ); const pasteResult = this.schemerService.pasteSingleCode( this.varCoding.codes ); @@ -382,9 +415,56 @@ export class VarCodingComponent implements OnInit, OnDestroy, OnChanges { this.updateHasResidualAutoCode(); this.updateHasIntendedIncompleteAutoCode(); this.varCodingChanged.emit(this.varCoding); + const newProblems = VarCodingComponent.getNewCodingProblems( + problemsBeforePaste, + this.schemerService.getCodingProblemsForVarCoding(this.varCoding) + ); + if (newProblems.length > 0) this.showPasteValidationWarning(newProblems); } } + private static getNewCodingProblems( + beforeProblems: CodingSchemeProblem[], + afterProblems: CodingSchemeProblem[] + ): CodingSchemeProblem[] { + const beforeProblemKeys = new Set(beforeProblems.map(problem => ( + VarCodingComponent.getCodingProblemKey(problem) + ))); + return afterProblems.filter(problem => ( + !beforeProblemKeys.has(VarCodingComponent.getCodingProblemKey(problem)) + )); + } + + private static getCodingProblemKey(problem: CodingSchemeProblem): string { + return [ + problem.variableId, + problem.type, + problem.breaking ? 'breaking' : 'warning', + problem.code || '' + ].join('|'); + } + + private showPasteValidationWarning(problems: CodingSchemeProblem[]): void { + const problemDescriptions = Array.from( + new Set(problems.map(problem => this.translateService.instant( + `coding-problem.${problem.type}` + ))) + ); + this.messageDialog.open(MessageDialogComponent, { + width: '500px', + data: { + title: this.translateService.instant('code.post-paste-validation.title'), + content: [ + this.translateService.instant('code.post-paste-validation.content'), + ...problemDescriptions + ].join(' '), + type: problems.some(problem => problem.breaking) ? + MessageType.error : + MessageType.warning + } + }); + } + editSourceParameters() { if (this.schemerService.userRole !== 'RO' && this.varCoding) { const dialogRef = this.editSourceParametersDialog.open(