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(