diff --git a/projects/ngx-coding-components/src/lib/translations/de.json b/projects/ngx-coding-components/src/lib/translations/de.json
index d60db85..83d49d0 100644
--- a/projects/ngx-coding-components/src/lib/translations/de.json
+++ b/projects/ngx-coding-components/src/lib/translations/de.json
@@ -253,6 +253,12 @@
"TAKE_EMPTY_AS_VALID": "Leerer Antwortwert ist gültig",
"SORT": "Sortiere Werte",
"SOLVER_EXPRESSION": "Ausdruck für Solver",
+ "solver-source-warning": {
+ "unselected-sources": "Warnung: Im Solver-Ausdruck referenzierte Variable(n) sind nicht als Quell-Variable(n) ausgewählt"
+ },
+ "solver-insert-source": {
+ "label": "Quellvariable einfügen"
+ },
"solver-test": {
"title": "Solver-Test",
"action": "Testen",
diff --git a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.html b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.html
index 1d5a740..9e945e4 100644
--- a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.html
+++ b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.html
@@ -12,7 +12,8 @@
{{ (newVariableMode ? 'varList.add' : 'derive-processing.pr
@if (data.sourceType !== 'BASE') {
-
+
Quell-Variablen
@@ -46,36 +47,38 @@ {{ (newVariableMode ? 'varList.add' : 'derive-processing.pr
@if (data.sourceType === 'SOLVER') {
{{ 'derive-processing.SOLVER_EXPRESSION' | translate }}
-
+
-
diff --git a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts
index 86f2811..7cb4d1a 100644
--- a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts
+++ b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts
@@ -1,6 +1,7 @@
import { SourceType, VariableSourceParameters, SourceProcessingType } from
'@iqbspecs/coding-scheme/coding-scheme.interface';
import { TranslateService } from '@ngx-translate/core';
+import { fakeAsync, tick } from '@angular/core/testing';
import { EditSourceParametersDialog, EditSourceParametersDialogData } from './edit-source-parameters-dialog.component';
import { SchemerService } from '../../services/schemer.service';
@@ -13,7 +14,9 @@ describe('EditSourceParametersDialog', () => {
'derive-processing.solver-test.error-sources-missing': 'Bitte mindestens eine Quellvariable auswählen.',
'derive-processing.solver-test.error-unselected-source': 'Der Ausdruck verweist auf nicht ausgewählte Quelle(n)',
'derive-processing.solver-test.error-invalid-value': 'Bitte numerischen Testwert prüfen',
- 'derive-processing.solver-test.error-evaluation': 'Der Ausdruck konnte nicht ausgewertet werden.'
+ 'derive-processing.solver-test.error-evaluation': 'Der Ausdruck konnte nicht ausgewertet werden.',
+ 'derive-processing.solver-source-warning.unselected-sources':
+ 'Warnung: Im Solver-Ausdruck referenzierte Variable(n) sind nicht als Quell-Variable(n) ausgewählt'
};
const createDialog = (options: {
@@ -187,6 +190,98 @@ describe('EditSourceParametersDialog', () => {
expect(dialog.solverTestResult?.message).toContain('V2');
});
+ it('solverSourceWarning should show referenced variables that are not selected as sources', () => {
+ const dialog = createDialog({
+ selfAlias: 'D',
+ sourceType: 'SOLVER',
+ sourceParameters: {
+ solverExpression: `${solverRef('V1')} + ${solverRef('V2')}`
+ },
+ deriveSources: ['v1']
+ });
+
+ expect(dialog.solverSourceWarning).toContain(
+ 'Warnung: Im Solver-Ausdruck referenzierte Variable(n)'
+ );
+ expect(dialog.solverSourceWarning).toContain('V2');
+ });
+
+ it('solverSourceWarning should update when expression or source selection changes', () => {
+ const dialog = createDialog({
+ selfAlias: 'D',
+ sourceType: 'SOLVER',
+ sourceParameters: { solverExpression: `${solverRef('V2')}` },
+ deriveSources: ['v1']
+ });
+
+ expect(dialog.solverSourceWarning).toContain('V2');
+
+ dialog.selectedSources.setValue(['v1', 'v2']);
+ dialog.updateDeriveSources();
+ expect(dialog.solverSourceWarning).toBeNull();
+
+ dialog.data.sourceParameters.solverExpression = `${solverRef('V1')}`;
+ dialog.selectedSources.setValue(['v2']);
+ dialog.updateDeriveSources();
+ expect(dialog.solverSourceWarning).toContain('V1');
+
+ dialog.data.sourceParameters.solverExpression = '';
+ expect(dialog.solverSourceWarning).toBeNull();
+ });
+
+ it('insertSolverSourceReference should insert the selected source at the cursor', fakeAsync(() => {
+ const dialog = createDialog({
+ selfAlias: 'D',
+ sourceType: 'SOLVER',
+ sourceParameters: { solverExpression: 'round( + 1)' },
+ deriveSources: ['v1']
+ });
+ const input = document.createElement('input');
+ input.value = dialog.data.sourceParameters.solverExpression || '';
+ input.setSelectionRange(6, 6);
+
+ dialog.insertSolverSourceReference({ id: 'v1', label: 'V1' }, input);
+ tick();
+
+ expect(dialog.data.sourceParameters.solverExpression).toBe(
+ `round(${solverRef('V1')} + 1)`
+ );
+ expect(input.selectionStart).toBe(11);
+ expect(input.selectionEnd).toBe(11);
+ }));
+
+ it('insertSolverSourceReference should replace the selected expression range', fakeAsync(() => {
+ const dialog = createDialog({
+ selfAlias: 'D',
+ sourceType: 'SOLVER',
+ sourceParameters: { solverExpression: '1 + placeholder' },
+ deriveSources: ['v2']
+ });
+ const input = document.createElement('input');
+ input.value = dialog.data.sourceParameters.solverExpression || '';
+ input.setSelectionRange(4, 15);
+
+ dialog.insertSolverSourceReference({ id: 'v2', label: 'V2' }, input);
+ tick();
+
+ expect(dialog.data.sourceParameters.solverExpression).toBe(
+ `1 + ${solverRef('V2')}`
+ );
+ expect(dialog.solverTestResult).toBeNull();
+ }));
+
+ it('getSolverSourceReference should expose the inserted reference syntax', () => {
+ const dialog = createDialog({
+ selfAlias: 'D',
+ sourceType: 'SOLVER',
+ deriveSources: ['v1']
+ });
+
+ expect(dialog.getSolverSourceReference({ id: 'v1', label: 'V1' })).toBe(
+ solverRef('V1')
+ );
+ });
+
it('runSolverTest should report non-numeric test values', () => {
const dialog = createDialog({
selfAlias: 'D',
diff --git a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.ts b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.ts
index 6608a31..81b6f86 100644
--- a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.ts
+++ b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.ts
@@ -7,11 +7,13 @@ import {
MatDialogClose
} from '@angular/material/dialog';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
-import { MatButton } from '@angular/material/button';
+import { MatButton, MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
-import { MatFormField, MatLabel } from '@angular/material/form-field';
+import { MatFormField, MatLabel, MatSuffix } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { MatCheckbox } from '@angular/material/checkbox';
+import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
+import { MatTooltip } from '@angular/material/tooltip';
import {
MatOption,
MatSelect,
@@ -117,6 +119,37 @@ const SOLVER_VARIABLE_PREFIX = '$';
height: 16px;
width: 16px;
}
+
+ .solver-source-menu__item {
+ justify-content: flex-start;
+ }
+
+ .solver-source-menu__reference {
+ color: rgb(0 0 0 / 60%);
+ font-size: 12px;
+ margin-left: 8px;
+ }
+
+ .solver-source-warning {
+ align-items: flex-start;
+ background: #fff8e1;
+ border-left: 4px solid #ff8f00;
+ border-radius: 4px;
+ color: #5f3f00;
+ display: flex;
+ gap: 8px;
+ margin: -8px 0 12px;
+ max-width: 640px;
+ padding: 10px 14px;
+ }
+
+ .solver-source-warning mat-icon {
+ color: #f57c00;
+ flex: 0 0 auto;
+ font-size: 20px;
+ height: 20px;
+ width: 20px;
+ }
`,
`
.solver-test-area {
@@ -171,6 +204,7 @@ const SOLVER_VARIABLE_PREFIX = '$';
MatDialogContent,
MatDialogActions,
MatButton,
+ MatIconButton,
MatDialogClose,
TranslateModule,
FormsModule,
@@ -180,6 +214,11 @@ const SOLVER_VARIABLE_PREFIX = '$';
MatLabel,
MatSelect,
MatOption,
+ MatSuffix,
+ MatMenu,
+ MatMenuItem,
+ MatMenuTrigger,
+ MatTooltip,
KeyValuePipe,
NgForOf,
ReactiveFormsModule,
@@ -211,6 +250,8 @@ export class EditSourceParametersDialog {
}
];
+ readonly solverVariablePrefix = SOLVER_VARIABLE_PREFIX;
+
sourceTypeList: SourceType[] = [
'COPY_VALUE',
'CONCAT_CODE',
@@ -259,6 +300,17 @@ export class EditSourceParametersDialog {
}));
}
+ get solverSourceWarning(): string | null {
+ const missingSourceLabels = this.getMissingSolverSourceIds()
+ .map(sourceId => this.getSourceLabel(sourceId));
+
+ if (missingSourceLabels.length < 1) return null;
+
+ return `${this.tr(
+ 'derive-processing.solver-source-warning.unselected-sources'
+ )}: ${missingSourceLabels.join(', ')}`;
+ }
+
updatePossibleNewSources(): void {
const codingScheme = this.schemerService.codingScheme;
@@ -333,6 +385,35 @@ export class EditSourceParametersDialog {
this.updateDeriveSources();
}
+ insertSolverSourceReference(
+ source: { id: string; label: string },
+ input: HTMLInputElement
+ ): void {
+ const reference = this.getSolverVariableReference(
+ source.label || source.id
+ );
+ const expression = this.data.sourceParameters.solverExpression || '';
+ const selectionStart = input.selectionStart ?? expression.length;
+ const selectionEnd = input.selectionEnd ?? selectionStart;
+
+ this.data.sourceParameters.solverExpression =
+ `${expression.slice(0, selectionStart)}${reference}${
+ expression.slice(selectionEnd)
+ }`;
+
+ this.clearSolverTestResult();
+
+ const cursorPosition = selectionStart + reference.length;
+ window.setTimeout(() => {
+ input.focus();
+ input.setSelectionRange(cursorPosition, cursorPosition);
+ });
+ }
+
+ getSolverSourceReference(source: { id: string; label: string }): string {
+ return this.getSolverVariableReference(source.label || source.id);
+ }
+
clearSolverTestResult(): void {
this.solverTestResult = null;
}
@@ -360,9 +441,7 @@ export class EditSourceParametersDialog {
expression,
variableCodings
);
- const missingSources = referencedVariables.filter(
- variableId => !this.data.deriveSources.includes(variableId)
- );
+ const missingSources = this.getMissingSolverSourceIds(referencedVariables);
if (missingSources.length > 0) {
this.solverTestResult = {
@@ -465,6 +544,23 @@ export class EditSourceParametersDialog {
}));
}
+ private getMissingSolverSourceIds(referencedVariables?: string[]): string[] {
+ if (this.data.sourceType !== 'SOLVER') return [];
+
+ const expression = this.data.sourceParameters.solverExpression || '';
+ if (!expression.trim()) return [];
+
+ const solverReferences = referencedVariables ||
+ EditSourceParametersDialog.getReferencedSolverVariables(
+ expression,
+ this.getVariableCodingsForSolverTest()
+ );
+
+ return solverReferences.filter(
+ variableId => !this.data.deriveSources.includes(variableId)
+ );
+ }
+
private static getReferencedSolverVariables(
expression: string,
variableCodings: VariableCodingData[]
@@ -493,6 +589,10 @@ export class EditSourceParametersDialog {
return JSON.stringify(value) || '';
}
+ private getSolverVariableReference(variableName: string): string {
+ return `${this.solverVariablePrefix}{${variableName}}`;
+ }
+
private setSolverTestError(translationKey: string): void {
this.solverTestResult = {
type: 'error',
diff --git a/src/assets/de.json b/src/assets/de.json
index e9e3298..f7f8971 100644
--- a/src/assets/de.json
+++ b/src/assets/de.json
@@ -114,6 +114,12 @@
"TAKE_EMPTY_AS_VALID": "Leerer Antwortwert ist gültig",
"SORT": "Sortiere Werte",
"SOLVER_EXPRESSION": "Ausdruck für Solver",
+ "solver-source-warning": {
+ "unselected-sources": "Warnung: Im Solver-Ausdruck referenzierte Variable(n) sind nicht als Quell-Variable(n) ausgewählt"
+ },
+ "solver-insert-source": {
+ "label": "Quellvariable einfügen"
+ },
"solver-help": {
"title": "Syntaxhilfe für Solver-Ausdrücke",
"description": "Solver-Ausdrücke werden mit math.js ausgewertet. Verwenden Sie Zahlen, Rechenzeichen und math.js-Funktionen; Quellvariablen referenzieren Sie mit ${Variablenname} oder ${VariablenID}.",
diff --git a/src/assets/en.json b/src/assets/en.json
index 181b803..11a1d81 100644
--- a/src/assets/en.json
+++ b/src/assets/en.json
@@ -27,6 +27,12 @@
"TAKE_EMPTY_AS_VALID": "Empty response value is valid",
"SORT": "Sort values",
"SOLVER_EXPRESSION": "Expression for solver",
+ "solver-source-warning": {
+ "unselected-sources": "Warning: Variables referenced in the solver expression are not selected as source variables"
+ },
+ "solver-insert-source": {
+ "label": "Insert source variable"
+ },
"solver-help": {
"title": "Syntax help for solver expressions",
"description": "Solver expressions are evaluated with math.js. Use numbers, operators and math.js functions; refer to source variables with ${VariableName} or ${VariableID}.",