Skip to content

Commit acb687c

Browse files
Merge pull request #2997 from github/robertbrignull/model-table-validation
Show validation results in the model editor
2 parents 86b5056 + 31d654d commit acb687c

File tree

4 files changed

+161
-57
lines changed

4 files changed

+161
-57
lines changed

extensions/ql-vscode/src/stories/model-editor/MethodRow.stories.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,28 @@ MultipleModelings.args = {
153153
methodCanBeModeled: true,
154154
viewState,
155155
};
156+
157+
export const ValidationError = Template.bind({});
158+
ValidationError.args = {
159+
method,
160+
modeledMethods: [
161+
{ ...modeledMethod, type: "source" },
162+
{ ...modeledMethod, type: "source" },
163+
],
164+
methodCanBeModeled: true,
165+
viewState,
166+
};
167+
168+
export const MultipleValidationErrors = Template.bind({});
169+
MultipleValidationErrors.args = {
170+
method,
171+
modeledMethods: [
172+
{ ...modeledMethod, type: "source" },
173+
{ ...modeledMethod, type: "source" },
174+
{ ...modeledMethod, type: "sink" },
175+
{ ...modeledMethod, type: "sink" },
176+
{ ...modeledMethod, type: "neutral", kind: "source" },
177+
],
178+
methodCanBeModeled: true,
179+
viewState,
180+
};

extensions/ql-vscode/src/view/method-modeling/ModeledMethodAlert.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { useCallback } from "react";
66

77
type Props = {
88
error: ModeledMethodValidationError;
9-
setSelectedIndex: (index: number) => void;
9+
setSelectedIndex?: (index: number) => void;
1010
};
1111

1212
export const ModeledMethodAlert = ({ error, setSelectedIndex }: Props) => {
1313
const handleClick = useCallback(() => {
14-
setSelectedIndex(error.index);
14+
setSelectedIndex?.(error.index);
1515
}, [error.index, setSelectedIndex]);
1616

1717
return (
@@ -22,7 +22,11 @@ export const ModeledMethodAlert = ({ error, setSelectedIndex }: Props) => {
2222
message={
2323
<>
2424
{error.message}{" "}
25-
<TextButton onClick={handleClick}>{error.actionText}</TextButton>
25+
{setSelectedIndex ? (
26+
<TextButton onClick={handleClick}>{error.actionText}</TextButton>
27+
) : (
28+
error.actionText
29+
)}
2630
</>
2731
}
2832
/>

extensions/ql-vscode/src/view/model-editor/MethodRow.tsx

Lines changed: 71 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import { ModelEditorViewState } from "../../model-editor/shared/view-state";
3131
import { Codicon } from "../common";
3232
import { canAddNewModeledMethod } from "../../model-editor/shared/multiple-modeled-methods";
3333
import { DataGridCell, DataGridRow } from "../common/DataGrid";
34+
import { validateModeledMethods } from "../../model-editor/shared/validation";
35+
import { ModeledMethodAlert } from "../method-modeling/ModeledMethodAlert";
3436

3537
const ApiOrMethodRow = styled.div`
3638
min-height: calc(var(--input-height) * 1px);
@@ -111,6 +113,11 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
111113
[modeledMethodsProp, method, viewState],
112114
);
113115

116+
const validationErrors = useMemo(
117+
() => validateModeledMethods(modeledMethods),
118+
[modeledMethods],
119+
);
120+
114121
const modeledMethodChangedHandlers = useMemo(
115122
() =>
116123
modeledMethods.map((_, index) => (modeledMethod: ModeledMethod) => {
@@ -163,7 +170,9 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
163170
ref={ref}
164171
focused={revealedMethodSignature === method.signature}
165172
>
166-
<DataGridCell gridRow={`span ${modeledMethods.length}`}>
173+
<DataGridCell
174+
gridRow={`span ${modeledMethods.length + validationErrors.length}`}
175+
>
167176
<ApiOrMethodRow>
168177
<ModelingStatusIndicator status={modelingStatus} />
169178
<MethodClassifications method={method} />
@@ -200,61 +209,69 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
200209
)}
201210
</>
202211
)}
203-
{!props.modelingInProgress &&
204-
modeledMethods.map((modeledMethod, index) => (
205-
<Fragment key={index}>
206-
<DataGridCell>
207-
<ModelTypeDropdown
208-
method={method}
209-
modeledMethod={modeledMethod}
210-
onChange={modeledMethodChangedHandlers[index]}
211-
/>
212-
</DataGridCell>
213-
<DataGridCell>
214-
<ModelInputDropdown
215-
method={method}
216-
modeledMethod={modeledMethod}
217-
onChange={modeledMethodChangedHandlers[index]}
218-
/>
219-
</DataGridCell>
220-
<DataGridCell>
221-
<ModelOutputDropdown
222-
method={method}
223-
modeledMethod={modeledMethod}
224-
onChange={modeledMethodChangedHandlers[index]}
225-
/>
226-
</DataGridCell>
227-
<DataGridCell>
228-
<ModelKindDropdown
229-
method={method}
230-
modeledMethod={modeledMethod}
231-
onChange={modeledMethodChangedHandlers[index]}
232-
/>
233-
</DataGridCell>
234-
{viewState.showMultipleModels && (
212+
{!props.modelingInProgress && (
213+
<>
214+
{modeledMethods.map((modeledMethod, index) => (
215+
<Fragment key={index}>
216+
<DataGridCell>
217+
<ModelTypeDropdown
218+
method={method}
219+
modeledMethod={modeledMethod}
220+
onChange={modeledMethodChangedHandlers[index]}
221+
/>
222+
</DataGridCell>
223+
<DataGridCell>
224+
<ModelInputDropdown
225+
method={method}
226+
modeledMethod={modeledMethod}
227+
onChange={modeledMethodChangedHandlers[index]}
228+
/>
229+
</DataGridCell>
235230
<DataGridCell>
236-
{index === modeledMethods.length - 1 ? (
237-
<CodiconRow
238-
appearance="icon"
239-
aria-label="Add new model"
240-
onClick={handleAddModelClick}
241-
disabled={addModelButtonDisabled}
242-
>
243-
<Codicon name="add" />
244-
</CodiconRow>
245-
) : (
246-
<CodiconRow
247-
appearance="icon"
248-
aria-label="Remove model"
249-
onClick={removeModelClickedHandlers[index]}
250-
>
251-
<Codicon name="trash" />
252-
</CodiconRow>
253-
)}
231+
<ModelOutputDropdown
232+
method={method}
233+
modeledMethod={modeledMethod}
234+
onChange={modeledMethodChangedHandlers[index]}
235+
/>
254236
</DataGridCell>
255-
)}
256-
</Fragment>
257-
))}
237+
<DataGridCell>
238+
<ModelKindDropdown
239+
method={method}
240+
modeledMethod={modeledMethod}
241+
onChange={modeledMethodChangedHandlers[index]}
242+
/>
243+
</DataGridCell>
244+
{viewState.showMultipleModels && (
245+
<DataGridCell>
246+
{index === modeledMethods.length - 1 ? (
247+
<CodiconRow
248+
appearance="icon"
249+
aria-label="Add new model"
250+
onClick={handleAddModelClick}
251+
disabled={addModelButtonDisabled}
252+
>
253+
<Codicon name="add" />
254+
</CodiconRow>
255+
) : (
256+
<CodiconRow
257+
appearance="icon"
258+
aria-label="Remove model"
259+
onClick={removeModelClickedHandlers[index]}
260+
>
261+
<Codicon name="trash" />
262+
</CodiconRow>
263+
)}
264+
</DataGridCell>
265+
)}
266+
</Fragment>
267+
))}
268+
{validationErrors.map((error, index) => (
269+
<DataGridCell gridColumn="span 5" key={index}>
270+
<ModeledMethodAlert error={error} />
271+
</DataGridCell>
272+
))}
273+
</>
274+
)}
258275
</DataGridRow>
259276
);
260277
},

extensions/ql-vscode/src/view/model-editor/__tests__/MethodRow.spec.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,4 +438,62 @@ describe(MethodRow.name, () => {
438438
{ ...modeledMethod, type: "summary" },
439439
]);
440440
});
441+
442+
it("does not display validation errors when everything is valid", () => {
443+
render({
444+
modeledMethods: [
445+
{ ...modeledMethod, type: "source" },
446+
{ ...modeledMethod, type: "sink" },
447+
],
448+
viewState: {
449+
...viewState,
450+
showMultipleModels: true,
451+
},
452+
});
453+
454+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
455+
});
456+
457+
it("displays a single validation error", () => {
458+
render({
459+
modeledMethods: [
460+
{ ...modeledMethod, type: "source" },
461+
{ ...modeledMethod, type: "source" },
462+
],
463+
viewState: {
464+
...viewState,
465+
showMultipleModels: true,
466+
},
467+
});
468+
469+
expect(screen.getByRole("alert")).toBeInTheDocument();
470+
expect(
471+
screen.getByText("Error: Duplicated classification"),
472+
).toBeInTheDocument();
473+
expect(
474+
screen.queryByText("Error: Conflicting classification"),
475+
).not.toBeInTheDocument();
476+
});
477+
478+
it("displays multiple validation errors", () => {
479+
render({
480+
modeledMethods: [
481+
{ ...modeledMethod, type: "source" },
482+
{ ...modeledMethod, type: "source" },
483+
{ ...modeledMethod, type: "neutral", kind: "source" },
484+
],
485+
viewState: {
486+
...viewState,
487+
showMultipleModels: true,
488+
},
489+
});
490+
491+
expect(screen.getAllByRole("alert").length).toBe(2);
492+
expect(
493+
screen.getByText("Error: Duplicated classification"),
494+
).toBeInTheDocument();
495+
expect(
496+
screen.getByText("Error: Conflicting classification"),
497+
).toBeInTheDocument();
498+
});
441499
});

0 commit comments

Comments
 (0)