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 }} - + -
-
- - - {{ 'derive-processing.solver-help.title' | translate }} - + + @for (source of selectedSolverSources; track source.id) { + + } + + + @if (solverSourceWarning; as warning) { + -

{{ 'derive-processing.solver-help.description' | translate }}

-
{{ 'derive-processing.solver-help.examples-title' | translate }}
-
    - @for (example of solverExpressionExamples; track example.expression) { -
  • - {{ example.expression }} - - - {{ example.descriptionKey | translate }} -
  • - } -
- - {{ 'derive-processing.solver-help.docs-link' | translate }} - - -
+ }
@@ -112,6 +115,33 @@

{{ (newVariableMode ? 'varList.add' : 'derive-processing.pr

}
+ +
+
+ + + {{ 'derive-processing.solver-help.title' | translate }} + +
+

{{ 'derive-processing.solver-help.description' | translate }}

+
{{ 'derive-processing.solver-help.examples-title' | translate }}
+
    + @for (example of solverExpressionExamples; track example.expression) { +
  • + {{ example.expression }} + - + {{ example.descriptionKey | translate }} +
  • + } +
+ + {{ 'derive-processing.solver-help.docs-link' | 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}.",