@rowsncolumns/spreadsheet 10.4.1 → 10.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/atoms/sheet-context.atom.d.ts +2 -0
- package/dist/cjs/atoms/sheet-context.atom.d.ts.map +1 -1
- package/dist/cjs/atoms/sheet-context.atom.js +8 -2
- package/dist/cjs/atoms/sort-dialog.atom.d.ts +6 -2
- package/dist/cjs/atoms/sort-dialog.atom.d.ts.map +1 -1
- package/dist/cjs/components/canvas-grid/index.d.ts.map +1 -1
- package/dist/cjs/components/canvas-grid/index.js +4 -2
- package/dist/cjs/components/canvas-grid/typings.d.ts +10 -0
- package/dist/cjs/components/canvas-grid/typings.d.ts.map +1 -1
- package/dist/cjs/components/filter-box/date-tree.d.ts +23 -5
- package/dist/cjs/components/filter-box/date-tree.d.ts.map +1 -1
- package/dist/cjs/components/filter-box/date-tree.js +76 -11
- package/dist/cjs/components/filter-box/filter-by-condition.d.ts +26 -1
- package/dist/cjs/components/filter-box/filter-by-condition.d.ts.map +1 -1
- package/dist/cjs/components/filter-box/filter-by-condition.js +108 -9
- package/dist/cjs/components/filter-box/index.d.ts.map +1 -1
- package/dist/cjs/components/filter-box/index.js +21 -5
- package/dist/cjs/components/formula-input/index.d.ts.map +1 -1
- package/dist/cjs/components/formula-input/index.js +26 -4
- package/dist/cjs/components/sheet-tabs/index.d.ts.map +1 -1
- package/dist/cjs/components/sheet-tabs/index.js +19 -5
- package/dist/cjs/components/sort-range-dialog/index.d.ts +20 -3
- package/dist/cjs/components/sort-range-dialog/index.d.ts.map +1 -1
- package/dist/cjs/components/sort-range-dialog/index.js +20 -6
- package/dist/cjs/components/toolbar/cell-style-selector.d.ts +9 -0
- package/dist/cjs/components/toolbar/cell-style-selector.d.ts.map +1 -1
- package/dist/cjs/components/toolbar/cell-style-selector.js +12 -3
- package/dist/cjs/components/toolbar/merge-cells-selector.d.ts +27 -1
- package/dist/cjs/components/toolbar/merge-cells-selector.d.ts.map +1 -1
- package/dist/cjs/components/toolbar/merge-cells-selector.js +106 -8
- package/dist/cjs/components/toolbar/table-style-editor.d.ts +85 -0
- package/dist/cjs/components/toolbar/table-style-editor.d.ts.map +1 -0
- package/dist/cjs/components/toolbar/table-style-editor.js +235 -0
- package/dist/cjs/components/toolbar/table-style-selector.d.ts +7 -7
- package/dist/cjs/components/toolbar/table-style-selector.d.ts.map +1 -1
- package/dist/cjs/components/toolbar/table-style-selector.js +77 -87
- package/dist/cjs/config.d.ts +6 -0
- package/dist/cjs/config.d.ts.map +1 -1
- package/dist/cjs/config.js +7 -1
- package/dist/cjs/hooks/use-conditional-formatting.d.ts +8 -0
- package/dist/cjs/hooks/use-conditional-formatting.d.ts.map +1 -1
- package/dist/cjs/hooks/use-conditional-formatting.js +128 -77
- package/dist/cjs/hooks/use-editable.d.ts.map +1 -1
- package/dist/cjs/hooks/use-editable.js +59 -1
- package/dist/cjs/hooks/use-selection.d.ts +6 -1
- package/dist/cjs/hooks/use-selection.d.ts.map +1 -1
- package/dist/cjs/hooks/use-selection.js +37 -1
- package/dist/cjs/release-date.js +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/cjs/types.d.ts +64 -0
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/esm/atoms/sheet-context.atom.d.ts +2 -0
- package/dist/esm/atoms/sheet-context.atom.d.ts.map +1 -1
- package/dist/esm/atoms/sheet-context.atom.js +3 -0
- package/dist/esm/atoms/sort-dialog.atom.d.ts +6 -2
- package/dist/esm/atoms/sort-dialog.atom.d.ts.map +1 -1
- package/dist/esm/components/canvas-grid/index.d.ts.map +1 -1
- package/dist/esm/components/canvas-grid/index.js +3 -1
- package/dist/esm/components/canvas-grid/typings.d.ts +10 -0
- package/dist/esm/components/canvas-grid/typings.d.ts.map +1 -1
- package/dist/esm/components/filter-box/date-tree.d.ts +23 -5
- package/dist/esm/components/filter-box/date-tree.d.ts.map +1 -1
- package/dist/esm/components/filter-box/date-tree.js +62 -10
- package/dist/esm/components/filter-box/filter-by-condition.d.ts +26 -1
- package/dist/esm/components/filter-box/filter-by-condition.d.ts.map +1 -1
- package/dist/esm/components/filter-box/filter-by-condition.js +78 -12
- package/dist/esm/components/filter-box/index.d.ts.map +1 -1
- package/dist/esm/components/filter-box/index.js +21 -5
- package/dist/esm/components/formula-input/index.d.ts.map +1 -1
- package/dist/esm/components/formula-input/index.js +26 -4
- package/dist/esm/components/sheet-tabs/index.d.ts.map +1 -1
- package/dist/esm/components/sheet-tabs/index.js +20 -6
- package/dist/esm/components/sort-range-dialog/index.d.ts +20 -3
- package/dist/esm/components/sort-range-dialog/index.d.ts.map +1 -1
- package/dist/esm/components/sort-range-dialog/index.js +17 -5
- package/dist/esm/components/toolbar/cell-style-selector.d.ts +9 -0
- package/dist/esm/components/toolbar/cell-style-selector.d.ts.map +1 -1
- package/dist/esm/components/toolbar/cell-style-selector.js +11 -2
- package/dist/esm/components/toolbar/merge-cells-selector.d.ts +27 -1
- package/dist/esm/components/toolbar/merge-cells-selector.d.ts.map +1 -1
- package/dist/esm/components/toolbar/merge-cells-selector.js +94 -10
- package/dist/esm/components/toolbar/table-style-editor.d.ts +85 -0
- package/dist/esm/components/toolbar/table-style-editor.d.ts.map +1 -0
- package/dist/esm/components/toolbar/table-style-editor.js +189 -0
- package/dist/esm/components/toolbar/table-style-selector.d.ts +7 -7
- package/dist/esm/components/toolbar/table-style-selector.d.ts.map +1 -1
- package/dist/esm/components/toolbar/table-style-selector.js +74 -81
- package/dist/esm/config.d.ts +6 -0
- package/dist/esm/config.d.ts.map +1 -1
- package/dist/esm/config.js +6 -0
- package/dist/esm/hooks/use-conditional-formatting.d.ts +8 -0
- package/dist/esm/hooks/use-conditional-formatting.d.ts.map +1 -1
- package/dist/esm/hooks/use-conditional-formatting.js +50 -13
- package/dist/esm/hooks/use-editable.d.ts.map +1 -1
- package/dist/esm/hooks/use-editable.js +48 -2
- package/dist/esm/hooks/use-selection.d.ts +6 -1
- package/dist/esm/hooks/use-selection.d.ts.map +1 -1
- package/dist/esm/hooks/use-selection.js +37 -1
- package/dist/esm/release-date.js +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/types.d.ts +64 -0
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/spreadsheet.min.css +1 -1
- package/package.json +5 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Button, Input, Select, SelectContent, SelectItem, SelectPortal, SelectTrigger, SelectValue, Stack, } from "@rowsncolumns/ui";
|
|
2
|
-
import React, { useState } from "react";
|
|
1
|
+
import { Button, Input, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectPortal, SelectSeparator, SelectTrigger, SelectValue, Stack, } from "@rowsncolumns/ui";
|
|
2
|
+
import React, { useMemo, useState } from "react";
|
|
3
3
|
import { CONDITION_LABELS, CONDITION_NONE } from "../../config";
|
|
4
4
|
import { shouldShowFromValue, shouldShowToValue } from "../../helpers";
|
|
5
5
|
import { useIsomorphicLayoutEffect } from "../../hooks/use-isomorphic-layout-effect";
|
|
@@ -23,13 +23,79 @@ export const top10ConditionType = (direction, unit) => direction === "top"
|
|
|
23
23
|
const SECOND_CLAUSE_LABELS = CONDITION_LABELS.filter(({ condition }) => !isTop10ConditionType(condition) &&
|
|
24
24
|
condition !== "NUMBER_ABOVE_AVERAGE" &&
|
|
25
25
|
condition !== "NUMBER_BELOW_AVERAGE");
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
export const DATE_PERIOD_GROUP_LABEL = "All dates in the period";
|
|
27
|
+
const FOREIGN_GROUP_LABEL = "Applied filter";
|
|
28
|
+
const isDatePeriodCondition = (type) => /^DATE_IN_[QM]\d+$/.test(type);
|
|
29
|
+
/**
|
|
30
|
+
* Which Excel AutoFilter family a condition belongs to. Anything outside the
|
|
31
|
+
* text / number / date prefixes ("Is empty", "Errors", "Custom formula") is
|
|
32
|
+
* offered on every column.
|
|
33
|
+
*/
|
|
34
|
+
const conditionFamily = (type) => {
|
|
35
|
+
if (type.startsWith("TEXT_"))
|
|
36
|
+
return "text";
|
|
37
|
+
if (type.startsWith("NUMBER_"))
|
|
38
|
+
return "number";
|
|
39
|
+
if (type.startsWith("DATE_"))
|
|
40
|
+
return isDatePeriodCondition(type) ? "period" : "date";
|
|
41
|
+
return "generic";
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The condition menu Excel shows for a column: one family — "Text Filters",
|
|
45
|
+
* "Number Filters" or "Date Filters" (with the sixteen period conditions
|
|
46
|
+
* grouped under "All Dates in the Period") — plus the generic items. With no
|
|
47
|
+
* `columnType` (an unclassified column) every family is offered, grouped.
|
|
48
|
+
* A `current` condition from another family (a date filter stored on a column
|
|
49
|
+
* that now holds text) stays listed and selected so the user can see and
|
|
50
|
+
* clear it instead of it silently vanishing from the menu.
|
|
51
|
+
*/
|
|
52
|
+
export const conditionOptionsFor = (labels, columnType, current) => {
|
|
53
|
+
const families = columnType
|
|
54
|
+
? [columnType]
|
|
55
|
+
: ["text", "number", "date"];
|
|
56
|
+
const groups = [];
|
|
57
|
+
for (const family of families) {
|
|
58
|
+
groups.push({
|
|
59
|
+
key: family,
|
|
60
|
+
options: labels.filter(({ condition }) => conditionFamily(condition) === family),
|
|
61
|
+
});
|
|
62
|
+
if (family === "date") {
|
|
63
|
+
groups.push({
|
|
64
|
+
key: "period",
|
|
65
|
+
label: DATE_PERIOD_GROUP_LABEL,
|
|
66
|
+
options: labels.filter(({ condition }) => conditionFamily(condition) === "period"),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
groups.push({
|
|
71
|
+
key: "generic",
|
|
72
|
+
options: labels.filter(({ condition }) => conditionFamily(condition) === "generic"),
|
|
73
|
+
});
|
|
74
|
+
const offered = groups.filter((group) => group.options.length > 0);
|
|
75
|
+
if (current &&
|
|
76
|
+
!offered.some((group) => group.options.some(({ condition }) => condition === current))) {
|
|
77
|
+
const foreign = labels.find(({ condition }) => condition === current) ??
|
|
78
|
+
CONDITION_LABELS.find(({ condition }) => condition === current);
|
|
79
|
+
if (foreign) {
|
|
80
|
+
offered.unshift({ key: "current", label: FOREIGN_GROUP_LABEL, options: [foreign] });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return offered;
|
|
84
|
+
};
|
|
85
|
+
const ConditionSelect = ({ value, labels, columnType, onChange, ariaLabel, }) => {
|
|
86
|
+
const groups = useMemo(() => conditionOptionsFor(labels, columnType, value), [labels, columnType, value]);
|
|
87
|
+
return (React.createElement(Select, { value: value ?? CONDITION_NONE, onValueChange: (next) => onChange(next === CONDITION_NONE ? undefined : next) },
|
|
88
|
+
React.createElement(SelectTrigger, { className: "mb-1", "aria-label": ariaLabel },
|
|
89
|
+
React.createElement(SelectValue, { placeholder: "None" })),
|
|
90
|
+
React.createElement(SelectPortal, null,
|
|
91
|
+
React.createElement(SelectContent, null,
|
|
92
|
+
React.createElement(SelectItem, { value: CONDITION_NONE }, "None"),
|
|
93
|
+
groups.map((group) => (React.createElement(React.Fragment, { key: group.key },
|
|
94
|
+
React.createElement(SelectSeparator, null),
|
|
95
|
+
React.createElement(SelectGroup, null,
|
|
96
|
+
group.label ? React.createElement(SelectLabel, null, group.label) : null,
|
|
97
|
+
group.options.map(({ condition, label }) => (React.createElement(SelectItem, { key: condition, value: condition }, label)))))))))));
|
|
98
|
+
};
|
|
33
99
|
const ValueInputs = ({ conditionType, fromValue, toValue, onChangeFromValue, onChangeToValue, namePrefix, }) => {
|
|
34
100
|
const [showToValue, setShowToValue] = useState(() => shouldShowToValue(conditionType));
|
|
35
101
|
const [showFromValue, setShowFromValue] = useState(() => shouldShowFromValue(conditionType));
|
|
@@ -72,19 +138,19 @@ const Top10Fields = ({ conditionType, count, onChangeConditionType, onChangeCoun
|
|
|
72
138
|
React.createElement(SelectItem, { value: "items" }, "Items"),
|
|
73
139
|
React.createElement(SelectItem, { value: "percent" }, "Percent"))))));
|
|
74
140
|
};
|
|
75
|
-
export const FilterByCondition = ({ conditionType, fromValue, toValue, onChangeToValue, onChangeFromValue, onChangeConditionType, condition2Type, condition2From, condition2To, conditionJoin, onChangeCondition2Type, onChangeCondition2From, onChangeCondition2To, onChangeConditionJoin, }) => {
|
|
141
|
+
export const FilterByCondition = ({ columnType, conditionType, fromValue, toValue, onChangeToValue, onChangeFromValue, onChangeConditionType, condition2Type, condition2From, condition2To, conditionJoin, onChangeCondition2Type, onChangeCondition2From, onChangeCondition2To, onChangeConditionJoin, }) => {
|
|
76
142
|
// Excel's Custom AutoFilter shows the second clause row only on request;
|
|
77
143
|
// it stays open while a second clause is stored on the column.
|
|
78
144
|
const [showSecondClause, setShowSecondClause] = useState(() => condition2Type !== undefined);
|
|
79
145
|
const isTop10 = isTop10ConditionType(conditionType);
|
|
80
146
|
return (React.createElement(React.Fragment, null,
|
|
81
147
|
React.createElement("label", { className: "text-sm font-semibold pb-1 block" }, "Filter by condition"),
|
|
82
|
-
React.createElement(ConditionSelect, { value: conditionType, labels: CONDITION_LABELS, onChange: (type) => onChangeConditionType?.(type), ariaLabel: "Condition" }),
|
|
148
|
+
React.createElement(ConditionSelect, { value: conditionType, labels: CONDITION_LABELS, columnType: columnType, onChange: (type) => onChangeConditionType?.(type), ariaLabel: "Condition" }),
|
|
83
149
|
isTop10 && conditionType ? (React.createElement(Top10Fields, { conditionType: conditionType, count: fromValue, onChangeConditionType: (type) => onChangeConditionType?.(type), onChangeCount: onChangeFromValue })) : (React.createElement(ValueInputs, { conditionType: conditionType, fromValue: fromValue, toValue: toValue, onChangeFromValue: onChangeFromValue, onChangeToValue: onChangeToValue, namePrefix: "" })),
|
|
84
150
|
conditionType && !isTop10 ? (showSecondClause ? (React.createElement("div", { className: "mt-1 flex flex-col gap-1" },
|
|
85
151
|
React.createElement("div", { role: "radiogroup", "aria-label": "Combine conditions", className: "flex gap-3 text-xs px-1" }, ["AND", "OR"].map((join) => (React.createElement("label", { key: join, className: "flex items-center gap-1 cursor-pointer" },
|
|
86
152
|
React.createElement("input", { type: "radio", name: "conditionJoin", value: join, checked: conditionJoin === join, onChange: () => onChangeConditionJoin(join) }),
|
|
87
153
|
join === "AND" ? "And" : "Or")))),
|
|
88
|
-
React.createElement(ConditionSelect, { value: condition2Type, labels: SECOND_CLAUSE_LABELS, onChange: onChangeCondition2Type, ariaLabel: "Second condition" }),
|
|
154
|
+
React.createElement(ConditionSelect, { value: condition2Type, labels: SECOND_CLAUSE_LABELS, columnType: columnType, onChange: onChangeCondition2Type, ariaLabel: "Second condition" }),
|
|
89
155
|
React.createElement(ValueInputs, { conditionType: condition2Type, fromValue: condition2From, toValue: condition2To, onChangeFromValue: onChangeCondition2From, onChangeToValue: onChangeCondition2To, namePrefix: "second" }))) : (React.createElement(Button, { type: "button", variant: "link", className: "text-xs px-1 mt-1 self-start", onClick: () => setShowSecondClause(true) }, "Custom filter (And / Or)\u2026"))) : null));
|
|
90
156
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/filter-box/index.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAsC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/filter-box/index.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAM3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAMV,cAAc,EACd,UAAU,EAGV,SAAS,EACV,MAAM,aAAa,CAAC;AAKrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAKvD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACnC,MAAM,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;IAC1C,mBAAmB,EAAE,cAAc,CAAC,qBAAqB,CAAC,CAAC;IAC3D,mBAAmB,EAAE,cAAc,CAAC,qBAAqB,CAAC,CAAC;IAC3D,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,sEAAsE;IACtE,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;IACjC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;CAC1B,CAAC;AAGF,eAAO,MAAM,WAAW,GACtB,QAAQ,UAAU,GAAG,SAAS,KAC7B,MAAM,IAAI,SAEZ,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,UAAU,cAAc,GAAG,SAAS,KACnC,OAQA,CAAC;AA0BJ,eAAO,MAAM,gBAAgB,4NAexB,cAAc,uBA8lBlB,CAAC;AAIF,MAAM,MAAM,cAAc,GAAG,kBAAkB,GAAG;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS;YAAW,cAAc;;CAuC9C,CAAC"}
|
|
@@ -4,9 +4,11 @@ import { Button, cn, DropdownLeftSlot, DropdownMenuSeparator, Popover, PopoverCo
|
|
|
4
4
|
import { isNil, number2Alpha } from "@rowsncolumns/utils";
|
|
5
5
|
import React, { useEffect, useRef, useState } from "react";
|
|
6
6
|
import { memo, useCallback, useMemo } from "react";
|
|
7
|
-
import { useFocusSheet } from "../../atoms";
|
|
8
|
-
import { StyledFakeTriggerButton } from "../context-menu/style";
|
|
7
|
+
import { useFocusSheet, useGetFormattedValue } from "../../atoms";
|
|
9
8
|
import { useOpenSortDialog } from "../../atoms/sort-dialog.atom";
|
|
9
|
+
import { BLANK_LABEL } from "../../config";
|
|
10
|
+
import { StyledFakeTriggerButton } from "../context-menu/style";
|
|
11
|
+
import { classifyFilterColumn } from "./date-tree";
|
|
10
12
|
import { FilterByColor } from "./filter-by-color";
|
|
11
13
|
import { FilterByCondition } from "./filter-by-condition";
|
|
12
14
|
import { FilterByValue } from "./filter-by-value";
|
|
@@ -76,6 +78,9 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
|
|
|
76
78
|
const [conditionJoin, onChangeConditionJoin] = useState(filterCriteria?.conditionJoin ?? "AND");
|
|
77
79
|
// "Sort by Color" is a collapsed sub-menu of the dropdown, like Excel's.
|
|
78
80
|
const [showSortByColor, setShowSortByColor] = useState(false);
|
|
81
|
+
// Excel offers one condition family per column (Text / Number / Date
|
|
82
|
+
// Filters), read from the same displayed values the checklist shows.
|
|
83
|
+
const columnType = useMemo(() => classifyFilterColumn(values, BLANK_LABEL), [values]);
|
|
79
84
|
// Auto apply (Excel parity). `pendingAutoApplyTick` counts user edits made
|
|
80
85
|
// while auto apply is on; the debounced effect below applies the newest
|
|
81
86
|
// state once per burst.
|
|
@@ -122,15 +127,23 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
|
|
|
122
127
|
// filter box is currently anchored on (that's the column we computed
|
|
123
128
|
// distinct colors/icons for). Other columns expose value + custom-list
|
|
124
129
|
// sort only.
|
|
130
|
+
// Excel labels sort levels by the header row — a table's column names, or
|
|
131
|
+
// the first row of a filtered range (an AutoFilter range always starts on
|
|
132
|
+
// its header row). A blank header falls back to the column letter.
|
|
133
|
+
const getFormattedValue = useGetFormattedValue();
|
|
125
134
|
const sortDialogColumns = useMemo(() => {
|
|
126
|
-
const { startColumnIndex, endColumnIndex } = filter.range;
|
|
135
|
+
const { startColumnIndex, endColumnIndex, startRowIndex } = filter.range;
|
|
127
136
|
const cols = [];
|
|
128
137
|
for (let c = startColumnIndex; c <= endColumnIndex; c++) {
|
|
129
138
|
const dim = c - startColumnIndex;
|
|
130
139
|
const isActive = dim === dimensionIndex;
|
|
140
|
+
const header = isTableView(filter)
|
|
141
|
+
? filter.columns?.[dim]?.name
|
|
142
|
+
: getFormattedValue?.(sheetId, startRowIndex, c);
|
|
131
143
|
cols.push({
|
|
132
144
|
dimensionIndex: dim,
|
|
133
145
|
label: `Column ${number2Alpha(c)}`,
|
|
146
|
+
header: header ?? undefined,
|
|
134
147
|
...(isActive
|
|
135
148
|
? {
|
|
136
149
|
availableCellColors,
|
|
@@ -142,7 +155,9 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
|
|
|
142
155
|
}
|
|
143
156
|
return cols;
|
|
144
157
|
}, [
|
|
145
|
-
filter
|
|
158
|
+
filter,
|
|
159
|
+
sheetId,
|
|
160
|
+
getFormattedValue,
|
|
146
161
|
dimensionIndex,
|
|
147
162
|
availableCellColors,
|
|
148
163
|
availableFontColors,
|
|
@@ -347,6 +362,7 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
|
|
|
347
362
|
openSortDialog({
|
|
348
363
|
columns: sortDialogColumns,
|
|
349
364
|
customLists,
|
|
365
|
+
initialHasHeader: true,
|
|
350
366
|
onApply: onApplySort,
|
|
351
367
|
});
|
|
352
368
|
onClose();
|
|
@@ -377,7 +393,7 @@ export const FilterBoxWrapper = memo(({ dimensionIndex, onOpenChange, onSort, on
|
|
|
377
393
|
React.createElement(Cross2Icon, null)),
|
|
378
394
|
clearLabel),
|
|
379
395
|
React.createElement("div", { className: "py-2 px-2" },
|
|
380
|
-
React.createElement(FilterByCondition, { conditionType: conditionType, fromValue: fromValue, toValue: toValue, onChangeConditionType: onUserChangeConditionType, onChangeFromValue: onUserChangeFromValue, onChangeToValue: onUserChangeToValue, condition2Type: condition2Type, condition2From: condition2From, condition2To: condition2To, conditionJoin: conditionJoin, onChangeCondition2Type: onUserChangeCondition2Type, onChangeCondition2From: onUserChangeCondition2From, onChangeCondition2To: onUserChangeCondition2To, onChangeConditionJoin: onUserChangeConditionJoin }),
|
|
396
|
+
React.createElement(FilterByCondition, { columnType: columnType, conditionType: conditionType, fromValue: fromValue, toValue: toValue, onChangeConditionType: onUserChangeConditionType, onChangeFromValue: onUserChangeFromValue, onChangeToValue: onUserChangeToValue, condition2Type: condition2Type, condition2From: condition2From, condition2To: condition2To, conditionJoin: conditionJoin, onChangeCondition2Type: onUserChangeCondition2Type, onChangeCondition2From: onUserChangeCondition2From, onChangeCondition2To: onUserChangeCondition2To, onChangeConditionJoin: onUserChangeConditionJoin }),
|
|
381
397
|
React.createElement(Separator, { className: "mt-2 mb-2" }),
|
|
382
398
|
React.createElement(FilterByColor, { colorFilter: colorFilter === undefined
|
|
383
399
|
? filterCriteria?.colorFilter
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/formula-input/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,kBAAkB,CAAC;AAGrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,KAAiB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/formula-input/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,kBAAkB,CAAC;AAGrE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,KAAiB,MAAM,OAAO,CAAC;AActC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE1C,KAAK,iBAAiB,GAAG,cAAc,CAAC,OAAO,SAAS,CAAC,GAAG;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,sGA8JxB,CAAC"}
|
|
@@ -4,6 +4,7 @@ import { forwardRef, useCallback } from "react";
|
|
|
4
4
|
import React, { useRef } from "react";
|
|
5
5
|
import { useActiveEditor, useFormulaMode, useGetGridRef, useGetSelectionsFromFormula, useOnEditorKeyDown, useSetActiveEditor, useSetFormulaSelections, } from "../../atoms";
|
|
6
6
|
import { useCellInputContext } from "../../atoms/cell-input-context.atom";
|
|
7
|
+
import { useIsomorphicLayoutEffect } from "../../hooks/use-isomorphic-layout-effect";
|
|
7
8
|
import { useMergeRefs } from "../../hooks/use-merge.refs";
|
|
8
9
|
import { CellInput } from "../cell-input";
|
|
9
10
|
/**
|
|
@@ -21,14 +22,35 @@ export const FormulaInput = forwardRef(({ value = "", onChange, onCancel, onBlur
|
|
|
21
22
|
const getSelectionsFromFormula = useGetSelectionsFromFormula();
|
|
22
23
|
const onEditorKeyDown = useOnEditorKeyDown();
|
|
23
24
|
const { getTableNames, getTableColumnNames, getNamedRanges } = useCellInputContext();
|
|
25
|
+
// Whether THIS input is the one holding formula mode on. `handleBlur` keeps the mode
|
|
26
|
+
// alive when focus moves to the grid — that is how a range gets picked — so an input
|
|
27
|
+
// that unmounts while the grid holds focus (Cancel / Save / the dialog's X / Esc) has
|
|
28
|
+
// to be the one to turn it off; otherwise the picked range keeps rendering and the
|
|
29
|
+
// grid stays in formula-selection mode with no editor left to receive the picks.
|
|
30
|
+
const ownsFormulaModeRef = useRef(false);
|
|
31
|
+
const enterFormulaMode = useCallback((nextValue) => {
|
|
32
|
+
const formulaMode = isAFormula(nextValue);
|
|
33
|
+
setFormulaMode(formulaMode);
|
|
34
|
+
ownsFormulaModeRef.current = formulaMode;
|
|
35
|
+
}, [setFormulaMode]);
|
|
24
36
|
const closeFormulaMode = useCallback(() => {
|
|
25
37
|
setFormulaMode(false);
|
|
26
38
|
setFormulaSelections([]);
|
|
39
|
+
ownsFormulaModeRef.current = false;
|
|
27
40
|
onCancel?.();
|
|
28
41
|
}, [onCancel, setFormulaMode, setFormulaSelections]);
|
|
42
|
+
// Layout (not passive) cleanup: when a host re-keys its range inputs ("Add range"),
|
|
43
|
+
// the departing input must release the mode before the new one's autoFocus claims it.
|
|
44
|
+
useIsomorphicLayoutEffect(() => () => {
|
|
45
|
+
if (ownsFormulaModeRef.current) {
|
|
46
|
+
ownsFormulaModeRef.current = false;
|
|
47
|
+
setFormulaMode(false);
|
|
48
|
+
setFormulaSelections([]);
|
|
49
|
+
}
|
|
50
|
+
}, [setFormulaMode, setFormulaSelections]);
|
|
29
51
|
const handleFocus = useCallback((e) => {
|
|
30
52
|
// Set formula mode
|
|
31
|
-
|
|
53
|
+
enterFormulaMode(value);
|
|
32
54
|
// Get selections from formula
|
|
33
55
|
const selections = getSelectionsFromFormula?.(value);
|
|
34
56
|
if (selections) {
|
|
@@ -40,7 +62,7 @@ export const FormulaInput = forwardRef(({ value = "", onChange, onCancel, onBlur
|
|
|
40
62
|
onChange?.(value);
|
|
41
63
|
onFocus?.(e);
|
|
42
64
|
}, [
|
|
43
|
-
|
|
65
|
+
enterFormulaMode,
|
|
44
66
|
setFormulaSelections,
|
|
45
67
|
getSelectionsFromFormula,
|
|
46
68
|
value,
|
|
@@ -66,7 +88,7 @@ export const FormulaInput = forwardRef(({ value = "", onChange, onCancel, onBlur
|
|
|
66
88
|
return;
|
|
67
89
|
}
|
|
68
90
|
// Set formula mode
|
|
69
|
-
|
|
91
|
+
enterFormulaMode(value);
|
|
70
92
|
// Get selections from formula
|
|
71
93
|
const selections = getSelectionsFromFormula?.(value);
|
|
72
94
|
if (selections) {
|
|
@@ -75,7 +97,7 @@ export const FormulaInput = forwardRef(({ value = "", onChange, onCancel, onBlur
|
|
|
75
97
|
}, [
|
|
76
98
|
activeEditor,
|
|
77
99
|
onChange,
|
|
78
|
-
|
|
100
|
+
enterFormulaMode,
|
|
79
101
|
setFormulaSelections,
|
|
80
102
|
getSelectionsFromFormula,
|
|
81
103
|
]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/sheet-tabs/index.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAyB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/sheet-tabs/index.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAyB,MAAM,OAAO,CAAC;AAK9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAIzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAAI,CAAC,SAAS,KAAK,EAAE,QAAQ,CAAC,EAAE,KAClB,MACjD,CAAC;AAEF;;;;GAIG;AACH,iBAAS,kBAAkB,CAAC,CAAC,SAAS,KAAK,EAAE,EAC3C,MAAM,EACN,aAAa,EACb,aAAa,EACb,eAAe,EACf,WAAiC,EACjC,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,QAAQ,EACR,sBAA8B,GAC/B,EAAE,cAAc,CAAC,CAAC,CAAC,qBAkVnB;AAED,eAAO,MAAM,SAAS,EAA+B,OAAO,kBAAkB,CAAC"}
|
|
@@ -3,10 +3,11 @@ import { restrictToHorizontalAxis, restrictToParentElement, } from "@dnd-kit/mod
|
|
|
3
3
|
import { horizontalListSortingStrategy, SortableContext, } from "@dnd-kit/sortable";
|
|
4
4
|
import { ChevronLeftIcon, ChevronRightIcon } from "@rowsncolumns/icons";
|
|
5
5
|
import { IconButton, Separator } from "@rowsncolumns/ui";
|
|
6
|
-
import { isNil, sortSheetsByIndex } from "@rowsncolumns/utils";
|
|
6
|
+
import { isNil, sortSheetsByIndex, validateSheetName } from "@rowsncolumns/utils";
|
|
7
7
|
import { useCallback, useId, useMemo, useRef, useState } from "react";
|
|
8
8
|
import React, { Fragment, memo } from "react";
|
|
9
9
|
import { isProtectedSheet } from "../../api/validation";
|
|
10
|
+
import { useShowValidationAlert } from "../../atoms/validation-alert.atom";
|
|
10
11
|
import { useIsomorphicLayoutEffect } from "../../hooks/use-isomorphic-layout-effect";
|
|
11
12
|
import { SheetTabContextMenu } from "./context-menu";
|
|
12
13
|
import { SheetTab } from "./sheet-tab";
|
|
@@ -45,16 +46,29 @@ function SheetTabsComponent({ sheets, activeSheetId, onRenameSheet, protectedRan
|
|
|
45
46
|
}
|
|
46
47
|
return !isProtectedSheet(sheetId, protectedRanges);
|
|
47
48
|
}, [protectedRanges, readonly]);
|
|
49
|
+
const showValidationAlert = useShowValidationAlert();
|
|
48
50
|
const canRename = useCallback((sheetId, newName) => {
|
|
49
|
-
|
|
50
|
-
if (isNil(newName) || newName.trim().length === 0) {
|
|
51
|
+
if (isNil(newName) || !canEditSheet(sheetId)) {
|
|
51
52
|
return false;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
// Excel's worksheet-name rules (rowsncolumns/spreadsheet#939 U-01): at most 31 characters,
|
|
55
|
+
// none of `\ / ? * [ ] :`, no leading / trailing apostrophe, not `History`, and unique
|
|
56
|
+
// case-insensitively (a sheet may re-case its own title). Every refusal shows Excel's own
|
|
57
|
+
// dialog; a blank name is the editor's "nothing typed" and just restores the title.
|
|
58
|
+
const validation = validateSheetName(newName, sheets, sheetId);
|
|
59
|
+
if (!validation.ok) {
|
|
60
|
+
if (validation.reason !== "blank") {
|
|
61
|
+
void showValidationAlert({
|
|
62
|
+
style: "stop",
|
|
63
|
+
title: "Error",
|
|
64
|
+
message: validation.message,
|
|
65
|
+
confirm: false,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
54
68
|
return false;
|
|
55
69
|
}
|
|
56
|
-
return
|
|
57
|
-
}, [canEditSheet, sheets]);
|
|
70
|
+
return true;
|
|
71
|
+
}, [canEditSheet, sheets, showValidationAlert]);
|
|
58
72
|
const canDeleteSheet = useCallback((sheetId) => {
|
|
59
73
|
if (!canEditSheet(sheetId)) {
|
|
60
74
|
return false;
|
|
@@ -10,7 +10,13 @@ import type { SortSpecs } from "../../types";
|
|
|
10
10
|
*/
|
|
11
11
|
export type SortDialogColumn = {
|
|
12
12
|
dimensionIndex: number;
|
|
13
|
+
/** Positional label ("Column B") — the fallback when the header cell is blank. */
|
|
13
14
|
label: string;
|
|
15
|
+
/**
|
|
16
|
+
* Text of the column's first-row cell. Shown instead of `label` while
|
|
17
|
+
* "My data has headers" is on (Excel labels sort levels by header).
|
|
18
|
+
*/
|
|
19
|
+
header?: string | null;
|
|
14
20
|
availableCellColors?: string[];
|
|
15
21
|
availableFontColors?: string[];
|
|
16
22
|
availableIcons?: AvailableIcon[];
|
|
@@ -33,13 +39,24 @@ export type SortDialogProps = {
|
|
|
33
39
|
* first column.
|
|
34
40
|
*/
|
|
35
41
|
initialSpecs?: SortSpecs[];
|
|
42
|
+
/**
|
|
43
|
+
* Initial state of Excel's "My data has headers". The toggle renders when
|
|
44
|
+
* this is set or any column carries a `header`; a host that omits both keeps
|
|
45
|
+
* the header-less dialog.
|
|
46
|
+
*/
|
|
47
|
+
initialHasHeader?: boolean;
|
|
36
48
|
/**
|
|
37
49
|
* Emits the assembled ordered `SortSpecs[]` (primary first, then
|
|
38
|
-
* tiebreakers). The host routes these to
|
|
50
|
+
* tiebreakers) plus the header choice. The host routes these to
|
|
51
|
+
* `onSortTable` / `onSortRange` (trimming the header row when `hasHeader`).
|
|
39
52
|
* The dialog never touches state directly.
|
|
40
53
|
*/
|
|
41
|
-
onApply(specs: SortSpecs[]
|
|
54
|
+
onApply(specs: SortSpecs[], options: {
|
|
55
|
+
hasHeader: boolean;
|
|
56
|
+
}): void;
|
|
42
57
|
};
|
|
58
|
+
/** The label a sort level shows for a column: its header while the data has headers, else its letter. */
|
|
59
|
+
export declare const sortColumnLabel: (column: Pick<SortDialogColumn, "label" | "header">, hasHeader: boolean) => string;
|
|
43
60
|
/**
|
|
44
61
|
* Custom multi-level sort dialog. Mirrors Excel's "Sort…" dialog and
|
|
45
62
|
* Google Sheets' "Sort range — advanced". Each row is a sort level:
|
|
@@ -51,7 +68,7 @@ export type SortDialogProps = {
|
|
|
51
68
|
* `SortSpecs[]` via `onApply`; the host disambiguates table vs range and
|
|
52
69
|
* routes through its existing sort callbacks.
|
|
53
70
|
*/
|
|
54
|
-
export declare const SortRangeDialog: React.MemoExoticComponent<({ open, onOpenChange, columns, customLists, initialSpecs, onApply, }: SortDialogProps) => React.JSX.Element>;
|
|
71
|
+
export declare const SortRangeDialog: React.MemoExoticComponent<({ open, onOpenChange, columns, customLists, initialSpecs, initialHasHeader, onApply, }: SortDialogProps) => React.JSX.Element>;
|
|
55
72
|
/**
|
|
56
73
|
* Host-mounted, atom-driven wrapper around {@link SortRangeDialog}. Mount this
|
|
57
74
|
* once in the host (alongside the other editor dialogs); the filter box opens
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/sort-range-dialog/index.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAA0D,MAAM,OAAO,CAAC;AAE/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,aAAa,CAAC;AAQxD;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,oCAAoC;IACpC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../components/sort-range-dialog/index.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAA0D,MAAM,OAAO,CAAC;AAE/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,aAAa,CAAC;AAQxD;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,oCAAoC;IACpC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,EAAE,CAAC;IAC3B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACpE,CAAC;AAEF,yGAAyG;AACzG,eAAO,MAAM,eAAe,GAC1B,QAAQ,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,QAAQ,CAAC,EAClD,WAAW,OAAO,KACjB,MAGF,CAAC;AA2DF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,qHASvB,eAAe,uBAiWnB,CAAC;AAIF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB;;;CAgB/B,CAAC"}
|
|
@@ -2,6 +2,11 @@ import { ArrowDownIcon, ArrowUpIcon, Cross1Icon, PlusIcon, SortDownIcon, SortUpI
|
|
|
2
2
|
import { Button, Dialog, DialogBody, DialogCloseButton, DialogContent, DialogTitle, IconButton, Select, SelectContent, SelectItem, SelectPortal, SelectTrigger, SelectValue, Separator, } from "@rowsncolumns/ui";
|
|
3
3
|
import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { useSortDialogState } from "../../atoms/sort-dialog.atom";
|
|
5
|
+
/** The label a sort level shows for a column: its header while the data has headers, else its letter. */
|
|
6
|
+
export const sortColumnLabel = (column, hasHeader) => {
|
|
7
|
+
const header = column.header?.trim();
|
|
8
|
+
return hasHeader && header ? header : column.label;
|
|
9
|
+
};
|
|
5
10
|
const CUSTOM_LIST_PREFIX = "custom:";
|
|
6
11
|
const ColorSwatch = ({ color }) => (React.createElement("span", { className: "inline-block h-3.5 w-3.5 rounded border border-gray-300 shrink-0", style: { backgroundColor: color } }));
|
|
7
12
|
const IconSwatch = ({ glyph, color }) => (React.createElement("span", { className: "inline-block h-3.5 w-3.5 leading-none text-center text-sm shrink-0", style: color ? { color } : undefined }, glyph));
|
|
@@ -36,17 +41,21 @@ const specToLevel = (spec, customLists) => {
|
|
|
36
41
|
* `SortSpecs[]` via `onApply`; the host disambiguates table vs range and
|
|
37
42
|
* routes through its existing sort callbacks.
|
|
38
43
|
*/
|
|
39
|
-
export const SortRangeDialog = memo(({ open, onOpenChange, columns, customLists, initialSpecs, onApply, }) => {
|
|
44
|
+
export const SortRangeDialog = memo(({ open, onOpenChange, columns, customLists, initialSpecs, initialHasHeader, onApply, }) => {
|
|
40
45
|
const lists = useMemo(() => customLists ?? [], [customLists]);
|
|
41
46
|
const [levels, setLevels] = useState(() => initialSpecs?.length
|
|
42
47
|
? initialSpecs.map((spec) => specToLevel(spec, lists))
|
|
43
48
|
: [makeDefaultLevel(columns)]);
|
|
49
|
+
const showHeaderToggle = initialHasHeader !== undefined ||
|
|
50
|
+
columns.some((column) => column.header !== undefined);
|
|
51
|
+
const [hasHeader, setHasHeader] = useState(initialHasHeader ?? false);
|
|
44
52
|
// Re-seed when reopened with different columns / specs.
|
|
45
53
|
useEffect(() => {
|
|
46
54
|
if (open) {
|
|
47
55
|
setLevels(initialSpecs?.length
|
|
48
56
|
? initialSpecs.map((spec) => specToLevel(spec, lists))
|
|
49
57
|
: [makeDefaultLevel(columns)]);
|
|
58
|
+
setHasHeader(initialHasHeader ?? false);
|
|
50
59
|
}
|
|
51
60
|
// Only reset on open transitions.
|
|
52
61
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -98,15 +107,18 @@ export const SortRangeDialog = memo(({ open, onOpenChange, columns, customLists,
|
|
|
98
107
|
}
|
|
99
108
|
return spec;
|
|
100
109
|
});
|
|
101
|
-
onApply(specs);
|
|
110
|
+
onApply(specs, { hasHeader });
|
|
102
111
|
onOpenChange(false);
|
|
103
|
-
}, [levels, lists, onApply, onOpenChange]);
|
|
112
|
+
}, [levels, lists, hasHeader, onApply, onOpenChange]);
|
|
104
113
|
return (React.createElement(Dialog, { open: open, onOpenChange: onOpenChange },
|
|
105
114
|
React.createElement(DialogContent, { className: "w-[640px] max-w-[92vw] pointer-events-auto" },
|
|
106
115
|
React.createElement(DialogTitle, null, "Sort"),
|
|
107
116
|
React.createElement(DialogCloseButton, { onClick: () => onOpenChange(false) },
|
|
108
117
|
React.createElement(Cross1Icon, null)),
|
|
109
118
|
React.createElement(DialogBody, { className: "flex flex-col gap-3" },
|
|
119
|
+
showHeaderToggle ? (React.createElement("label", { className: "font-system text-sm flex items-center gap-1" },
|
|
120
|
+
React.createElement("input", { type: "checkbox", name: "hasHeader", checked: hasHeader, onChange: (e) => setHasHeader(e.target.checked) }),
|
|
121
|
+
"My data has headers")) : null,
|
|
110
122
|
React.createElement("div", { className: "flex flex-col gap-2" }, levels.map((lvl, rowIndex) => {
|
|
111
123
|
const col = columnByIndex.get(lvl.dimensionIndex);
|
|
112
124
|
const cellColors = col?.availableCellColors ?? [];
|
|
@@ -124,7 +136,7 @@ export const SortRangeDialog = memo(({ open, onOpenChange, columns, customLists,
|
|
|
124
136
|
React.createElement(SelectTrigger, { className: "w-[160px]" },
|
|
125
137
|
React.createElement(SelectValue, { placeholder: "Column" })),
|
|
126
138
|
React.createElement(SelectPortal, null,
|
|
127
|
-
React.createElement(SelectContent, null, columns.map((c) => (React.createElement(SelectItem, { key: c.dimensionIndex, value: String(c.dimensionIndex) }, c
|
|
139
|
+
React.createElement(SelectContent, null, columns.map((c) => (React.createElement(SelectItem, { key: c.dimensionIndex, value: String(c.dimensionIndex) }, sortColumnLabel(c, hasHeader))))))),
|
|
128
140
|
React.createElement(Select, { value: lvl.customListIndex != null && lvl.sortBy === "value"
|
|
129
141
|
? `${CUSTOM_LIST_PREFIX}${lvl.customListIndex}`
|
|
130
142
|
: lvl.sortBy, onValueChange: (v) => {
|
|
@@ -210,6 +222,6 @@ export const SortRangeDialogHost = () => {
|
|
|
210
222
|
return (React.createElement(SortRangeDialog, { open: request != null, onOpenChange: (open) => {
|
|
211
223
|
if (!open)
|
|
212
224
|
close();
|
|
213
|
-
}, columns: request?.columns ?? [], customLists: request?.customLists, initialSpecs: request?.initialSpecs, onApply: (specs) => request?.onApply(specs) }));
|
|
225
|
+
}, columns: request?.columns ?? [], customLists: request?.customLists, initialSpecs: request?.initialSpecs, initialHasHeader: request?.initialHasHeader, onApply: (specs, options) => request?.onApply(specs, options) }));
|
|
214
226
|
};
|
|
215
227
|
SortRangeDialogHost.displayName = "SortRangeDialogHost";
|
|
@@ -33,6 +33,15 @@ export declare const getGalleryNamedCellStyles: (namedCellStyles: NamedCellStyle
|
|
|
33
33
|
/** CSS preview of a named style's format for the gallery swatch. */
|
|
34
34
|
export declare const getNamedCellStylePreview: (format: CellFormat | undefined, theme: SpreadsheetTheme) => React.CSSProperties;
|
|
35
35
|
export declare const getCellStyles: (theme: SpreadsheetTheme, currencySymbol?: string) => Map<string, StyleList[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Excel's built-in "Comma Style" / "Comma [0]" (numFmtId 43 / 41): accounting
|
|
38
|
+
* layout without a currency symbol — negatives in parentheses, zero as `-`,
|
|
39
|
+
* `_(`/`_)` padding so digits align. `-15` shows `(15.00)` in the cell's own
|
|
40
|
+
* colour; the `[Red]` parenthesised variants are separate built-ins (38/40)
|
|
41
|
+
* that the Format Cells dialog offers under Custom, not the toolbar style.
|
|
42
|
+
*/
|
|
43
|
+
export declare const PATTERN_COMMA_STYLE = "_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)";
|
|
44
|
+
export declare const PATTERN_COMMA_STYLE_ROUNDED = "_(* #,##0_);_(* (#,##0);_(* \"-\"??_);_(@_)";
|
|
36
45
|
export declare const CellStyleSelector: React.MemoExoticComponent<({ currency, locale, theme, selectedFormat, namedCellStyles, canAddCustomStyle, onChangeFormatting, onClearFormatting, }: CellStyleSelectorProps) => React.JSX.Element>;
|
|
37
46
|
export {};
|
|
38
47
|
//# sourceMappingURL=cell-style-selector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell-style-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/cell-style-selector.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAMrD,OAAO,KAAK,EACV,UAAU,EAEV,cAAc,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,KAAK,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAClD,KAAK,aAAa,CAAC,CAAC,SAAS,MAAM,UAAU,IAAI,CAC/C,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,KACjB,IAAI,CAAC;AAEV,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,gBAAgB,CAAC;IACxB,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC;IAC1C,cAAc,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACnC,qBAAqB,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,kBAAkB,EAAE,CAClB,GAAG,IAAI,EAAE,UAAU,CAAC,cAAc,GAAG,aAAa,CAAC,MAAM,UAAU,CAAC,CAAC,KAClE,IAAI,CAAC;IACV,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,EAAE,MAAM,IAAI,CAAC;CAC/B,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,UAAU;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa;CAC5B,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,GACpC,iBAAiB,cAAc,EAAE,GAAG,IAAI,GAAG,SAAS,KACnD,KAAK,CAAC,cAAc,GAAG;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAQ7C,CAAC;AAcJ,oEAAoE;AACpE,eAAO,MAAM,wBAAwB,GACnC,QAAQ,UAAU,GAAG,SAAS,EAC9B,OAAO,gBAAgB,KACtB,KAAK,CAAC,aAqBR,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,OAAO,gBAAgB,EACvB,iBAAiB,MAAM,6BAgYxB,CAAC;AAEF,eAAO,MAAM,iBAAiB,sJAUzB,sBAAsB,uBAsO1B,CAAC"}
|
|
1
|
+
{"version":3,"file":"cell-style-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/cell-style-selector.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAMrD,OAAO,KAAK,EACV,UAAU,EAEV,cAAc,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,KAAK,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;AAClD,KAAK,aAAa,CAAC,CAAC,SAAS,MAAM,UAAU,IAAI,CAC/C,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,KACjB,IAAI,CAAC;AAEV,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,gBAAgB,CAAC;IACxB,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;IAC3C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC;IAC1C,cAAc,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACnC,qBAAqB,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,kBAAkB,EAAE,CAClB,GAAG,IAAI,EAAE,UAAU,CAAC,cAAc,GAAG,aAAa,CAAC,MAAM,UAAU,CAAC,CAAC,KAClE,IAAI,CAAC;IACV,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,EAAE,MAAM,IAAI,CAAC;CAC/B,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,UAAU;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa;CAC5B,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,GACpC,iBAAiB,cAAc,EAAE,GAAG,IAAI,GAAG,SAAS,KACnD,KAAK,CAAC,cAAc,GAAG;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAQ7C,CAAC;AAcJ,oEAAoE;AACpE,eAAO,MAAM,wBAAwB,GACnC,QAAQ,UAAU,GAAG,SAAS,EAC9B,OAAO,gBAAgB,KACtB,KAAK,CAAC,aAqBR,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,OAAO,gBAAgB,EACvB,iBAAiB,MAAM,6BAgYxB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,sDAAoD,CAAC;AACrF,eAAO,MAAM,2BAA2B,gDAA8C,CAAC;AAEvF,eAAO,MAAM,iBAAiB,sJAUzB,sBAAsB,uBAsO1B,CAAC"}
|
|
@@ -422,6 +422,15 @@ export const getCellStyles = (theme, currencySymbol) => {
|
|
|
422
422
|
],
|
|
423
423
|
]);
|
|
424
424
|
};
|
|
425
|
+
/**
|
|
426
|
+
* Excel's built-in "Comma Style" / "Comma [0]" (numFmtId 43 / 41): accounting
|
|
427
|
+
* layout without a currency symbol — negatives in parentheses, zero as `-`,
|
|
428
|
+
* `_(`/`_)` padding so digits align. `-15` shows `(15.00)` in the cell's own
|
|
429
|
+
* colour; the `[Red]` parenthesised variants are separate built-ins (38/40)
|
|
430
|
+
* that the Format Cells dialog offers under Custom, not the toolbar style.
|
|
431
|
+
*/
|
|
432
|
+
export const PATTERN_COMMA_STYLE = '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)';
|
|
433
|
+
export const PATTERN_COMMA_STYLE_ROUNDED = '_(* #,##0_);_(* (#,##0);_(* "-"??_);_(@_)';
|
|
425
434
|
export const CellStyleSelector = memo(({ currency, locale, theme, selectedFormat, namedCellStyles, canAddCustomStyle = false, onChangeFormatting, onClearFormatting, }) => {
|
|
426
435
|
const colorIndexes = [4, 5, 6, 7, 8, 9];
|
|
427
436
|
const colorTints = [0.8, 0.6, 0.4, 0];
|
|
@@ -436,14 +445,14 @@ export const CellStyleSelector = memo(({ currency, locale, theme, selectedFormat
|
|
|
436
445
|
"Comma",
|
|
437
446
|
{
|
|
438
447
|
type: "NUMBER",
|
|
439
|
-
pattern:
|
|
448
|
+
pattern: PATTERN_COMMA_STYLE,
|
|
440
449
|
},
|
|
441
450
|
],
|
|
442
451
|
[
|
|
443
452
|
"Comma[0]",
|
|
444
453
|
{
|
|
445
454
|
type: "NUMBER",
|
|
446
|
-
pattern:
|
|
455
|
+
pattern: PATTERN_COMMA_STYLE_ROUNDED,
|
|
447
456
|
},
|
|
448
457
|
],
|
|
449
458
|
[
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CellInterface, GridRange, SelectionArea } from "@rowsncolumns/grid";
|
|
2
2
|
import { Direction } from "@rowsncolumns/grid";
|
|
3
|
+
import type { Maybe } from "@rowsncolumns/utils";
|
|
3
4
|
import React from "react";
|
|
4
5
|
import type { TableView } from "../../types";
|
|
5
6
|
import type { SelectionAttributes } from "../canvas-grid/typings";
|
|
@@ -12,7 +13,32 @@ type MergeCellsSelectorProps = {
|
|
|
12
13
|
merges?: GridRange[] | null;
|
|
13
14
|
/** Tables of the workbook — a selection touching one on `sheetId` cannot be merged (Excel). */
|
|
14
15
|
tables?: TableView[] | null;
|
|
16
|
+
/**
|
|
17
|
+
* Cell value reader behind Excel's "Merging cells only keeps the upper-left value" warning.
|
|
18
|
+
* Defaults to the reader the grid publishes; the warning is skipped when neither is available.
|
|
19
|
+
*/
|
|
20
|
+
getFormattedValue?: (sheetId: number, rowIndex: number, columnIndex: number) => Maybe<string>;
|
|
15
21
|
};
|
|
16
|
-
|
|
22
|
+
/** Excel's wording for a merge that would drop cell values. */
|
|
23
|
+
export declare const MERGE_DISCARDS_VALUES_TITLE = "Merge cells";
|
|
24
|
+
export declare const MERGE_DISCARDS_VALUES_MESSAGE = "Merging cells only keeps the upper-left value and discards other values.";
|
|
25
|
+
/**
|
|
26
|
+
* The regions `onMergeCells` merges for a toolbar request: each row (merge
|
|
27
|
+
* horizontally), each column (merge vertically), every area of a multi-area
|
|
28
|
+
* selection, or the last selection as one block — the store action's own rules.
|
|
29
|
+
*/
|
|
30
|
+
export declare const mergeRegionsForSelection: (selections: SelectionArea[], direction?: Direction) => GridRange[];
|
|
31
|
+
/**
|
|
32
|
+
* Would this merge discard values? Excel warns when more than one cell of a
|
|
33
|
+
* merged block holds a value (the merge keeps only the upper-left one). A
|
|
34
|
+
* headless / programmatic merge never asks — this gate lives in the toolbar.
|
|
35
|
+
*/
|
|
36
|
+
export declare const mergeDiscardsValues: ({ sheetId, selections, direction, getFormattedValue, }: {
|
|
37
|
+
sheetId: number;
|
|
38
|
+
selections: SelectionArea[];
|
|
39
|
+
direction?: Direction;
|
|
40
|
+
getFormattedValue: (sheetId: number, rowIndex: number, columnIndex: number) => Maybe<string>;
|
|
41
|
+
}) => boolean;
|
|
42
|
+
export declare const MergeCellsSelector: ({ activeCell, selections, sheetId, merges, tables, onMerge, onUnMerge, getFormattedValue: getFormattedValueProp, }: MergeCellsSelectorProps) => React.JSX.Element;
|
|
17
43
|
export {};
|
|
18
44
|
//# sourceMappingURL=merge-cells-selector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-cells-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/merge-cells-selector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,aAAa,EACd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAkB,SAAS,EAAgB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"merge-cells-selector.d.ts","sourceRoot":"","sources":["../../../../components/toolbar/merge-cells-selector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,aAAa,EACd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAkB,SAAS,EAAgB,MAAM,oBAAoB,CAAC;AAyB7E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAmB,MAAM,OAAO,CAAC;AAIxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,KAAK,uBAAuB,GAAG;IAC7B,UAAU,EAAE,aAAa,CAAC;IAC1B,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CACL,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,aAAa,EACzB,UAAU,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAAE,EAChD,SAAS,CAAC,EAAE,SAAS,GACpB,IAAI,CAAC;IACR,SAAS,CACP,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,aAAa,EACzB,UAAU,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAAE,GAC/C,IAAI,CAAC;IACR,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5B,+FAA+F;IAC/F,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAClB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,KAChB,KAAK,CAAC,MAAM,CAAC,CAAC;CACpB,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,2BAA2B,gBAAgB,CAAC;AACzD,eAAO,MAAM,6BAA6B,6EACkC,CAAC;AAE7E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GACnC,YAAY,aAAa,EAAE,EAC3B,YAAY,SAAS,KACpB,SAAS,EAmBX,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,wDAKjC;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,iBAAiB,EAAE,CACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,KAChB,KAAK,CAAC,MAAM,CAAC,CAAC;CACpB,KAAG,OAcH,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAAI,oHAShC,uBAAuB,sBA8IzB,CAAC"}
|