Skip to content

Commit 078bee3

Browse files
committed
[IMP] errors: display error origin position
Purpose ------- Let's say a cell is evaluated to an error because one of its inputs is an error itself. The tooltip shows the error message, but you don't know where it comes from. If the formula has many reference, you have to check every one of those references to find the original error. As an example, if the formula =IF(OR(AND($D21<G$17,$D21>I$1),AND(I$1<$E21,I$1>$D21)),1,"") results with an error. Where does it come from ? D21 ? J1 ? I1 ? E21 ?  Spec ---- Add below the error message the cell from which the error is coming from. The cell reference should be clickable to jump to cell. Task: 4452745
1 parent 3bfd3d1 commit 078bee3

File tree

7 files changed

+204
-25
lines changed

7 files changed

+204
-25
lines changed

src/components/error_tooltip/error_tooltip.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Component } from "@odoo/owl";
2+
import { deepEquals, positionToZone } from "../../helpers";
23
import { _t } from "../../translation";
3-
import { CellValueType } from "../../types";
4+
import { CellPosition, CellValueType, EvaluatedCell, SpreadsheetChildEnv } from "../../types";
45
import { CellPopoverComponent, PopoverBuilders } from "../../types/cell_popovers";
56
import { css } from "../helpers/css";
67

@@ -29,28 +30,64 @@ export interface ErrorToolTipMessage {
2930
}
3031

3132
interface ErrorToolTipProps {
33+
cellPosition: CellPosition;
3234
errors: ErrorToolTipMessage[];
3335
onClosed?: () => void;
3436
}
3537

36-
export class ErrorToolTip extends Component<ErrorToolTipProps> {
38+
export class ErrorToolTip extends Component<ErrorToolTipProps, SpreadsheetChildEnv> {
3739
static maxSize = { maxHeight: ERROR_TOOLTIP_MAX_HEIGHT };
3840
static template = "o-spreadsheet-ErrorToolTip";
3941
static props = {
42+
cellPosition: Object,
4043
errors: Array,
4144
onClosed: { type: Function, optional: true },
4245
};
46+
47+
get evaluationError() {
48+
const cell = this.env.model.getters.getEvaluatedCell(this.props.cellPosition);
49+
if (cell.message) {
50+
return cell;
51+
}
52+
return undefined;
53+
}
54+
55+
get errorOriginPositionString() {
56+
const evaluationError = this.evaluationError;
57+
const position = evaluationError?.errorOriginPosition;
58+
if (!position || deepEquals(position, this.props.cellPosition)) {
59+
return "";
60+
}
61+
const sheetId = position.sheetId;
62+
return this.env.model.getters.getRangeString(
63+
this.env.model.getters.getRangeFromZone(sheetId, positionToZone(position)),
64+
this.env.model.getters.getActiveSheetId()
65+
);
66+
}
67+
68+
selectCell() {
69+
const position = this.evaluationError?.errorOriginPosition;
70+
if (!position) {
71+
return;
72+
}
73+
const activeSheetId = this.env.model.getters.getActiveSheetId();
74+
if (position.sheetId !== activeSheetId) {
75+
this.env.model.dispatch("ACTIVATE_SHEET", {
76+
sheetIdFrom: activeSheetId,
77+
sheetIdTo: position.sheetId,
78+
});
79+
}
80+
this.env.model.selection.selectCell(position.col, position.row);
81+
}
4382
}
4483

4584
export const ErrorToolTipPopoverBuilder: PopoverBuilders = {
4685
onHover: (position, getters): CellPopoverComponent<typeof ErrorToolTip> => {
4786
const cell = getters.getEvaluatedCell(position);
4887
const errors: ErrorToolTipMessage[] = [];
88+
let evaluationError: EvaluatedCell | undefined;
4989
if (cell.type === CellValueType.error && !!cell.message) {
50-
errors.push({
51-
title: _t("Error"),
52-
message: cell.message,
53-
});
90+
evaluationError = cell;
5491
}
5592

5693
const validationErrorMessage = getters.getInvalidDataValidationMessage(position);
@@ -61,13 +98,16 @@ export const ErrorToolTipPopoverBuilder: PopoverBuilders = {
6198
});
6299
}
63100

64-
if (!errors.length) {
101+
if (!errors.length && !evaluationError) {
65102
return { isOpen: false };
66103
}
67104

68105
return {
69106
isOpen: true,
70-
props: { errors: errors },
107+
props: {
108+
cellPosition: position,
109+
errors,
110+
},
71111
Component: ErrorToolTip,
72112
cellCorner: "TopRight",
73113
};

src/components/error_tooltip/error_tooltip.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
<templates>
22
<t t-name="o-spreadsheet-ErrorToolTip">
33
<div class="o-error-tooltip">
4+
<t t-if="evaluationError">
5+
<div class="o-error-tooltip-title fw-bold text-danger">Error</div>
6+
<div class="o-error-tooltip-message">
7+
<t t-esc="evaluationError.message"/>
8+
<div class="fst-italic" t-if="errorOriginPositionString">
9+
Caused by
10+
<span
11+
t-esc="errorOriginPositionString"
12+
class="o-button-link"
13+
t-on-click="selectCell"
14+
title="Click to select the cell"
15+
/>
16+
</div>
17+
</div>
18+
</t>
419
<t t-foreach="props.errors" t-as="error" t-key="error_index">
520
<div class="o-error-tooltip-title fw-bold text-danger">
621
<t t-esc="error.title"/>

src/plugins/ui_core_views/cell_evaluation/evaluator.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,11 +366,15 @@ export class Evaluator {
366366
);
367367

368368
if (!isMatrix(formulaReturn)) {
369-
return createEvaluatedCell(
369+
const evaluatedCell = createEvaluatedCell(
370370
nullValueToZeroValue(formulaReturn),
371371
this.getters.getLocale(),
372372
cellData
373373
);
374+
if (evaluatedCell.type === CellValueType.error) {
375+
evaluatedCell.errorOriginPosition = formulaReturn.errorOriginPosition ?? formulaPosition;
376+
}
377+
return evaluatedCell;
374378
}
375379

376380
this.assertSheetHasEnoughSpaceToSpreadFormulaResult(formulaPosition, formulaReturn);
@@ -494,6 +498,9 @@ export class Evaluator {
494498
this.getters.getLocale(),
495499
cell
496500
);
501+
if (evaluatedCell.type === CellValueType.error) {
502+
evaluatedCell.errorOriginPosition = matrixResult[i][j].errorOriginPosition ?? position;
503+
}
497504
this.evaluatedCells.set(position, evaluatedCell);
498505
};
499506
return spreadValues;

src/types/misc.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,14 @@ export interface RangeCompiledFormula extends Omit<CompiledFormula, "dependencie
198198
}
199199

200200
export type Matrix<T = unknown> = T[][];
201-
export type FunctionResultObject = { value: CellValue; format?: Format; message?: string };
201+
202+
export type FunctionResultObject = {
203+
value: CellValue;
204+
format?: Format;
205+
errorOriginPosition?: CellPosition;
206+
message?: string;
207+
};
208+
202209
export type FunctionResultNumber = { value: number; format?: string };
203210

204211
// FORMULA FUNCTION VALUE AND FORMAT INPUT

tests/evaluation/evaluation.test.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ import {
2222
getCellError,
2323
getEvaluatedCell,
2424
} from "../test_helpers/getters_helpers";
25-
import { evaluateCell, evaluateGrid, restoreDefaultFunctions } from "../test_helpers/helpers";
25+
import {
26+
evaluateCell,
27+
evaluateGrid,
28+
restoreDefaultFunctions,
29+
toCellPosition,
30+
} from "../test_helpers/helpers";
2631
import resetAllMocks = jest.resetAllMocks;
2732

2833
describe("evaluateCells", () => {
@@ -1086,8 +1091,51 @@ describe("evaluateCells", () => {
10861091
expect(getCellError(model, "C1")).toBe("Invalid expression");
10871092
});
10881093

1089-
// TO DO: add tests for exp format (ex: 4E10)
1090-
// RO DO: add tests for DATE string format (ex match: "28 02 2020")
1094+
test("error original position from the cell itself", () => {
1095+
const model = new Model();
1096+
const sheetId = model.getters.getActiveSheetId();
1097+
setCellContent(model, "A1", "=0/0");
1098+
expect(getEvaluatedCell(model, "A1").errorOriginPosition).toEqual(
1099+
toCellPosition(sheetId, "A1")
1100+
);
1101+
});
1102+
1103+
test("error original position from simple references", () => {
1104+
const model = new Model();
1105+
const sheetId = model.getters.getActiveSheetId();
1106+
setCellContent(model, "A1", "=0/0");
1107+
setCellContent(model, "A2", "=A1");
1108+
setCellContent(model, "A3", "=A2");
1109+
const A1 = toCellPosition(sheetId, "A1");
1110+
expect(getEvaluatedCell(model, "A2").errorOriginPosition).toEqual(A1);
1111+
expect(getEvaluatedCell(model, "A3").errorOriginPosition).toEqual(A1);
1112+
});
1113+
1114+
test("error original position from a range reference", () => {
1115+
const model = new Model();
1116+
const sheetId = model.getters.getActiveSheetId();
1117+
setCellContent(model, "A1", "1");
1118+
setCellContent(model, "A2", "=0/0");
1119+
setCellContent(model, "A3", "=MAX(A1:A2)");
1120+
expect(getEvaluatedCell(model, "A3").errorOriginPosition).toEqual(
1121+
toCellPosition(sheetId, "A2")
1122+
);
1123+
});
1124+
1125+
test("error original position in a spilled result", () => {
1126+
const model = new Model();
1127+
const sheetId = model.getters.getActiveSheetId();
1128+
setCellContent(model, "A1", "1");
1129+
setCellContent(model, "A2", "=0/0");
1130+
setCellContent(model, "A3", "=TRANSPOSE(A1:A2)");
1131+
setCellContent(model, "A4", "=TRANSPOSE(A3:B3)");
1132+
expect(getEvaluatedCell(model, "B3").errorOriginPosition).toEqual(
1133+
toCellPosition(sheetId, "A2")
1134+
);
1135+
expect(getEvaluatedCell(model, "A5").errorOriginPosition).toEqual(
1136+
toCellPosition(sheetId, "A2")
1137+
);
1138+
});
10911139
});
10921140

10931141
describe("evaluate formula getter", () => {

tests/popover/__snapshots__/error_tooltip_component.test.ts.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ exports[`Grid integration can display error tooltip 1`] = `
1717
class="o-error-tooltip-message"
1818
>
1919
The divisor must be different from zero.
20+
2021
</div>
2122
23+
24+
2225
</div>
2326
</div>
2427
`;

tests/popover/error_tooltip_component.test.ts

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import { Model } from "../../src";
2-
import {
3-
ErrorToolTip,
4-
ErrorToolTipMessage,
5-
} from "../../src/components/error_tooltip/error_tooltip";
1+
import { Model, PropsOf } from "../../src";
2+
import { ErrorToolTip } from "../../src/components/error_tooltip/error_tooltip";
63
import { DEFAULT_CELL_HEIGHT, DEFAULT_CELL_WIDTH } from "../../src/constants";
74
import {
85
addDataValidation,
96
createChart,
7+
createSheet,
108
merge,
119
setCellContent,
1210
} from "../test_helpers/commands_helpers";
1311
import { TEST_CHART_DATA } from "../test_helpers/constants";
1412
import {
13+
click,
1514
clickCell,
1615
gridMouseEvent,
1716
hoverCell,
@@ -24,28 +23,38 @@ import {
2423
mountComponent,
2524
mountSpreadsheet,
2625
nextTick,
26+
toCellPosition,
2727
} from "../test_helpers/helpers";
2828

2929
mockChart();
3030

3131
describe("Error tooltip component", () => {
3232
let fixture: HTMLElement;
3333

34-
async function mountErrorTooltip(errors: ErrorToolTipMessage[]) {
35-
({ fixture } = await mountComponent(ErrorToolTip, { props: { errors } }));
34+
async function mountErrorTooltip(model: Model, props: PropsOf<typeof ErrorToolTip>) {
35+
({ fixture } = await mountComponent(ErrorToolTip, {
36+
props,
37+
model,
38+
}));
3639
}
3740

3841
test("Can display an error message", async () => {
39-
await mountErrorTooltip([{ message: "This is an error", title: "Error" }]);
42+
await mountErrorTooltip(new Model(), {
43+
errors: [{ message: "This is an error", title: "Error" }],
44+
cellPosition: toCellPosition("sheet1", "A1"),
45+
});
4046
expect(fixture.querySelector(".o-error-tooltip-title")?.textContent).toBe("Error");
4147
expect(fixture.querySelector(".o-error-tooltip-message")?.textContent).toBe("This is an error");
4248
});
4349

4450
test("Can display multiple error messages", async () => {
45-
await mountErrorTooltip([
46-
{ message: "This is an error", title: "Error" },
47-
{ message: "Invalid data", title: "Invalid" },
48-
]);
51+
await mountErrorTooltip(new Model(), {
52+
errors: [
53+
{ message: "This is an error", title: "Error" },
54+
{ message: "Invalid data", title: "Invalid" },
55+
],
56+
cellPosition: toCellPosition("sheet1", "A1"),
57+
});
4958
const titles = fixture.querySelectorAll(".o-error-tooltip-title");
5059
const messages = fixture.querySelectorAll(".o-error-tooltip-message");
5160
expect(titles).toHaveLength(2);
@@ -57,6 +66,56 @@ describe("Error tooltip component", () => {
5766
expect(titles[1].textContent).toBe("Invalid");
5867
expect(messages[1].textContent).toBe("Invalid data");
5968
});
69+
70+
test("can display error origin position", async () => {
71+
const model = new Model();
72+
const sheetId = model.getters.getActiveSheetId();
73+
setCellContent(model, "A1", "=1/0");
74+
setCellContent(model, "A2", "=A1");
75+
await mountErrorTooltip(model, {
76+
errors: [],
77+
cellPosition: toCellPosition(sheetId, "A2"),
78+
});
79+
expect(".fst-italic").toHaveText(" Caused by A1");
80+
});
81+
82+
test("can display error position from another sheet", async () => {
83+
const model = new Model();
84+
const sheetId = model.getters.getActiveSheetId();
85+
createSheet(model, { sheetId: "sheet2" });
86+
setCellContent(model, "A1", "=1/0", "sheet2");
87+
setCellContent(model, "A2", "=Sheet2!A1");
88+
await mountErrorTooltip(model, {
89+
errors: [],
90+
cellPosition: toCellPosition(sheetId, "A2"),
91+
});
92+
expect(".fst-italic").toHaveText(" Caused by Sheet2!A1");
93+
});
94+
95+
test("clicking on error position selects the position", async () => {
96+
const model = new Model();
97+
const sheetId = model.getters.getActiveSheetId();
98+
createSheet(model, { sheetId: "sheet2" });
99+
setCellContent(model, "J10", "=1/0", "sheet2");
100+
setCellContent(model, "A2", "=Sheet2!J10");
101+
await mountErrorTooltip(model, {
102+
errors: [],
103+
cellPosition: toCellPosition(sheetId, "A2"),
104+
});
105+
click(fixture, ".o-button-link");
106+
expect(model.getters.getActivePosition()).toEqual(toCellPosition("sheet2", "J10"));
107+
});
108+
109+
test("does not display error origin position if it is the same cell", async () => {
110+
const model = new Model();
111+
const sheetId = model.getters.getActiveSheetId();
112+
setCellContent(model, "A1", "=1/0");
113+
await mountErrorTooltip(model, {
114+
errors: [],
115+
cellPosition: toCellPosition(sheetId, "A1"),
116+
});
117+
expect(".fst-italic").toHaveCount(0);
118+
});
60119
});
61120

62121
describe("Grid integration", () => {

0 commit comments

Comments
 (0)