@casualoffice/sheets 0.17.0 → 0.18.0
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/chrome.cjs +3339 -244
- package/dist/chrome.cjs.map +1 -1
- package/dist/chrome.js +3298 -203
- package/dist/chrome.js.map +1 -1
- package/dist/embed/embed-runtime.js +152 -150
- package/package.json +1 -1
- package/src/chrome/ConditionalFormattingDialog.tsx +534 -0
- package/src/chrome/CustomSortDialog.tsx +357 -0
- package/src/chrome/DataValidationDialog.tsx +536 -0
- package/src/chrome/DeleteCellsDialog.tsx +183 -0
- package/src/chrome/GoalSeekDialog.tsx +370 -0
- package/src/chrome/InsertCellsDialog.tsx +185 -0
- package/src/chrome/InsertChartDialog.tsx +490 -0
- package/src/chrome/InsertFunctionDialog.tsx +493 -0
- package/src/chrome/InsertPivotDialog.tsx +488 -0
- package/src/chrome/InsertSparklineDialog.tsx +344 -0
- package/src/chrome/NameManagerDialog.tsx +378 -0
- package/src/chrome/PasteSpecialDialog.tsx +286 -0
- package/src/chrome/dialog-context.tsx +24 -0
package/package.json
CHANGED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ConditionalFormattingDialog — the SDK chrome's built-in Conditional Formatting
|
|
19
|
+
* modal. Sibling of DataValidationDialog / FormatCellsDialog: it reads the active
|
|
20
|
+
* A1 selection off the FUniver facade, gathers a rule via a small form, and
|
|
21
|
+
* applies it through the `@univerjs/sheets-conditional-formatting` facade.
|
|
22
|
+
*
|
|
23
|
+
* Pattern (grounded in
|
|
24
|
+
* `sheets-conditional-formatting/lib/types/facade/*.d.ts`):
|
|
25
|
+
* worksheet.newConditionalFormattingRule() // FConditionalFormattingBuilder
|
|
26
|
+
* .<condition>() // → ConditionalFormatHighlightRuleBuilder
|
|
27
|
+
* .setBackground(fill) / .setFontColor(text)
|
|
28
|
+
* .setRanges([range.getRange()])
|
|
29
|
+
* .build() // → IConditionFormattingRule
|
|
30
|
+
* worksheet.addConditionalFormattingRule(rule)
|
|
31
|
+
*
|
|
32
|
+
* Condition families (each → a real builder method, verified in
|
|
33
|
+
* f-conditional-formatting-builder.d.ts):
|
|
34
|
+
* - Cell value: greater / greaterEqual / less / lessEqual / equal / notEqual /
|
|
35
|
+
* between / notBetween → `.whenNumber*` (L461-609)
|
|
36
|
+
* - Text contains → `.whenTextContains(text)` (L630)
|
|
37
|
+
* - Top N / Bottom N → `.setRank({ isBottom, isPercent, value })` (L227)
|
|
38
|
+
* - Color scale → `.setColorScale(config)` (L853) — its own visual,
|
|
39
|
+
* no fill/text format
|
|
40
|
+
*
|
|
41
|
+
* The "Clear rules" action calls `worksheet.clearConditionalFormatRules()`
|
|
42
|
+
* (f-worksheet.d.ts L172) — the whole-sheet clear the facade exposes (there's no
|
|
43
|
+
* range-scoped clear on FWorksheet; FRange.clearConditionalFormatRules exists but
|
|
44
|
+
* is deprecated — see limitations).
|
|
45
|
+
*
|
|
46
|
+
* Mounted by `<DialogHost>` when `openDialog('conditional-formatting')` is called
|
|
47
|
+
* and no host override is registered.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { useMemo, useState, type CSSProperties } from 'react';
|
|
51
|
+
// Side-effect import: installs `FWorksheet.newConditionalFormattingRule /
|
|
52
|
+
// addConditionalFormattingRule / clearConditionalFormatRules` (and the FRange
|
|
53
|
+
// mixin) on the facade prototypes AND augments the `@univerjs/sheets/facade`
|
|
54
|
+
// FWorksheet/FRange types. The `cf` plugin group registers the plugin but does
|
|
55
|
+
// NOT import this facade module, so without it the builder calls below are
|
|
56
|
+
// undefined at runtime. Mirrors DataValidationDialog's `/facade` side-effect.
|
|
57
|
+
import '@univerjs/sheets-conditional-formatting/facade';
|
|
58
|
+
import type { DialogComponentProps } from './extensions';
|
|
59
|
+
import type { CasualSheetsAPI } from '../sheets/api';
|
|
60
|
+
import { Dialog } from './Dialog';
|
|
61
|
+
import {
|
|
62
|
+
DIALOG_BTN_PRIMARY_STYLE,
|
|
63
|
+
DIALOG_BTN_SECONDARY_STYLE,
|
|
64
|
+
DIALOG_FIELD_STYLE,
|
|
65
|
+
DIALOG_INPUT_STYLE,
|
|
66
|
+
DIALOG_LABEL_STYLE,
|
|
67
|
+
} from './dialog-styles';
|
|
68
|
+
|
|
69
|
+
/** Rule families the dialog can build. */
|
|
70
|
+
type RuleType = 'cellValue' | 'textContains' | 'topN' | 'bottomN' | 'colorScale';
|
|
71
|
+
|
|
72
|
+
/** Operators for the cell-value family. */
|
|
73
|
+
type Operator =
|
|
74
|
+
| 'greater'
|
|
75
|
+
| 'greaterEqual'
|
|
76
|
+
| 'less'
|
|
77
|
+
| 'lessEqual'
|
|
78
|
+
| 'equal'
|
|
79
|
+
| 'notEqual'
|
|
80
|
+
| 'between'
|
|
81
|
+
| 'notBetween';
|
|
82
|
+
|
|
83
|
+
const RULE_TYPE_OPTIONS: Array<{ value: RuleType; label: string }> = [
|
|
84
|
+
{ value: 'cellValue', label: 'Cell value' },
|
|
85
|
+
{ value: 'textContains', label: 'Text contains' },
|
|
86
|
+
{ value: 'topN', label: 'Top N' },
|
|
87
|
+
{ value: 'bottomN', label: 'Bottom N' },
|
|
88
|
+
{ value: 'colorScale', label: 'Color scale' },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const OPERATOR_OPTIONS: Array<{ value: Operator; label: string }> = [
|
|
92
|
+
{ value: 'greater', label: 'Greater than' },
|
|
93
|
+
{ value: 'greaterEqual', label: 'Greater than or equal to' },
|
|
94
|
+
{ value: 'less', label: 'Less than' },
|
|
95
|
+
{ value: 'lessEqual', label: 'Less than or equal to' },
|
|
96
|
+
{ value: 'equal', label: 'Equal to' },
|
|
97
|
+
{ value: 'notEqual', label: 'Not equal to' },
|
|
98
|
+
{ value: 'between', label: 'Between' },
|
|
99
|
+
{ value: 'notBetween', label: 'Not between' },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/** True when the operator needs a second operand. */
|
|
103
|
+
function isRangeOperator(op: Operator): boolean {
|
|
104
|
+
return op === 'between' || op === 'notBetween';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface DialogState {
|
|
108
|
+
ruleType: RuleType;
|
|
109
|
+
operator: Operator;
|
|
110
|
+
/** Cell-value first operand (raw string; parsed on apply). */
|
|
111
|
+
operand1: string;
|
|
112
|
+
/** Second operand, used only for between / notBetween. */
|
|
113
|
+
operand2: string;
|
|
114
|
+
/** Substring for the text-contains family. */
|
|
115
|
+
text: string;
|
|
116
|
+
/** N for top/bottom N. */
|
|
117
|
+
rankN: string;
|
|
118
|
+
/** Interpret rankN as a percent instead of a count. */
|
|
119
|
+
rankPercent: boolean;
|
|
120
|
+
/** Highlight fill color. */
|
|
121
|
+
fillColor: string;
|
|
122
|
+
/** Highlight text color. */
|
|
123
|
+
textColor: string;
|
|
124
|
+
/** Color-scale endpoints (2-color: min → max). */
|
|
125
|
+
scaleMinColor: string;
|
|
126
|
+
scaleMaxColor: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const INITIAL_STATE: DialogState = {
|
|
130
|
+
ruleType: 'cellValue',
|
|
131
|
+
operator: 'greater',
|
|
132
|
+
operand1: '',
|
|
133
|
+
operand2: '',
|
|
134
|
+
text: '',
|
|
135
|
+
rankN: '10',
|
|
136
|
+
rankPercent: false,
|
|
137
|
+
fillColor: '#fce8b2',
|
|
138
|
+
textColor: '#7f6000',
|
|
139
|
+
scaleMinColor: '#ffffff',
|
|
140
|
+
scaleMaxColor: '#57bb8a',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** The active FRange, or null when there is no selection. */
|
|
144
|
+
function activeRange(api: CasualSheetsAPI) {
|
|
145
|
+
return api.univer.getActiveWorkbook()?.getActiveSheet()?.getActiveRange() ?? null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The active FWorksheet, or null. */
|
|
149
|
+
function activeSheet(api: CasualSheetsAPI) {
|
|
150
|
+
return api.univer.getActiveWorkbook()?.getActiveSheet() ?? null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** True when the rule family carries a fill/text highlight format. */
|
|
154
|
+
function usesHighlight(type: RuleType): boolean {
|
|
155
|
+
return type !== 'colorScale';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build a conditional-formatting rule from the form via the facade builder and
|
|
160
|
+
* add it to the active sheet, scoped to the active range. Returns false when
|
|
161
|
+
* there's no selection or the inputs are unusable.
|
|
162
|
+
*/
|
|
163
|
+
function applyRule(api: CasualSheetsAPI, s: DialogState): boolean {
|
|
164
|
+
const range = activeRange(api);
|
|
165
|
+
const sheet = activeSheet(api);
|
|
166
|
+
if (!range || !sheet) return false;
|
|
167
|
+
|
|
168
|
+
// The conditional-formatting facade contributes these to FWorksheet at
|
|
169
|
+
// runtime; typed loosely here so the SDK doesn't hard-depend on the ambient
|
|
170
|
+
// FWorksheet augmentation at this call site.
|
|
171
|
+
const cfSheet = sheet as unknown as {
|
|
172
|
+
newConditionalFormattingRule: () => CfBuilder;
|
|
173
|
+
addConditionalFormattingRule: (rule: unknown) => unknown;
|
|
174
|
+
};
|
|
175
|
+
const iRange = (range as unknown as { getRange: () => unknown }).getRange();
|
|
176
|
+
|
|
177
|
+
const n1 = Number(s.operand1);
|
|
178
|
+
const n2 = Number(s.operand2);
|
|
179
|
+
const rankN = Number(s.rankN);
|
|
180
|
+
|
|
181
|
+
let builder = cfSheet.newConditionalFormattingRule();
|
|
182
|
+
|
|
183
|
+
switch (s.ruleType) {
|
|
184
|
+
case 'cellValue': {
|
|
185
|
+
if (!Number.isFinite(n1)) return false;
|
|
186
|
+
switch (s.operator) {
|
|
187
|
+
case 'greater':
|
|
188
|
+
builder = builder.whenNumberGreaterThan(n1);
|
|
189
|
+
break;
|
|
190
|
+
case 'greaterEqual':
|
|
191
|
+
builder = builder.whenNumberGreaterThanOrEqualTo(n1);
|
|
192
|
+
break;
|
|
193
|
+
case 'less':
|
|
194
|
+
builder = builder.whenNumberLessThan(n1);
|
|
195
|
+
break;
|
|
196
|
+
case 'lessEqual':
|
|
197
|
+
builder = builder.whenNumberLessThanOrEqualTo(n1);
|
|
198
|
+
break;
|
|
199
|
+
case 'equal':
|
|
200
|
+
builder = builder.whenNumberEqualTo(n1);
|
|
201
|
+
break;
|
|
202
|
+
case 'notEqual':
|
|
203
|
+
builder = builder.whenNumberNotEqualTo(n1);
|
|
204
|
+
break;
|
|
205
|
+
case 'between':
|
|
206
|
+
if (!Number.isFinite(n2)) return false;
|
|
207
|
+
builder = builder.whenNumberBetween(n1, n2);
|
|
208
|
+
break;
|
|
209
|
+
case 'notBetween':
|
|
210
|
+
if (!Number.isFinite(n2)) return false;
|
|
211
|
+
builder = builder.whenNumberNotBetween(n1, n2);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'textContains': {
|
|
217
|
+
const text = s.text.trim();
|
|
218
|
+
if (text.length === 0) return false;
|
|
219
|
+
builder = builder.whenTextContains(text);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case 'topN':
|
|
223
|
+
case 'bottomN': {
|
|
224
|
+
if (!Number.isFinite(rankN) || rankN <= 0) return false;
|
|
225
|
+
builder = builder.setRank({
|
|
226
|
+
isBottom: s.ruleType === 'bottomN',
|
|
227
|
+
isPercent: s.rankPercent,
|
|
228
|
+
value: rankN,
|
|
229
|
+
});
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case 'colorScale': {
|
|
233
|
+
// 2-color scale: min endpoint → max endpoint. Value types 'min'/'max'
|
|
234
|
+
// are the CFValueType string literals (base/const.d.ts CFValueType).
|
|
235
|
+
builder = builder.setColorScale([
|
|
236
|
+
{ index: 0, color: s.scaleMinColor, value: { type: 'min' } },
|
|
237
|
+
{ index: 1, color: s.scaleMaxColor, value: { type: 'max' } },
|
|
238
|
+
]);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (usesHighlight(s.ruleType)) {
|
|
244
|
+
builder = builder.setBackground(s.fillColor);
|
|
245
|
+
builder = builder.setFontColor(s.textColor);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const rule = builder.setRanges([iRange]).build();
|
|
249
|
+
cfSheet.addConditionalFormattingRule(rule);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The subset of the conditional-formatting builder chain we use, typed here so
|
|
255
|
+
* the call site is checked without importing the facade's private builder
|
|
256
|
+
* classes. Every method is verified in
|
|
257
|
+
* `sheets-conditional-formatting/lib/types/facade/f-conditional-formatting-builder.d.ts`.
|
|
258
|
+
*/
|
|
259
|
+
interface CfBuilder {
|
|
260
|
+
whenNumberGreaterThan: (v: number) => CfBuilder;
|
|
261
|
+
whenNumberGreaterThanOrEqualTo: (v: number) => CfBuilder;
|
|
262
|
+
whenNumberLessThan: (v: number) => CfBuilder;
|
|
263
|
+
whenNumberLessThanOrEqualTo: (v: number) => CfBuilder;
|
|
264
|
+
whenNumberEqualTo: (v: number) => CfBuilder;
|
|
265
|
+
whenNumberNotEqualTo: (v: number) => CfBuilder;
|
|
266
|
+
whenNumberBetween: (a: number, b: number) => CfBuilder;
|
|
267
|
+
whenNumberNotBetween: (a: number, b: number) => CfBuilder;
|
|
268
|
+
whenTextContains: (t: string) => CfBuilder;
|
|
269
|
+
setRank: (config: { isBottom: boolean; isPercent: boolean; value: number }) => CfBuilder;
|
|
270
|
+
setColorScale: (
|
|
271
|
+
config: Array<{ index: number; color: string; value: { type: string; value?: number } }>,
|
|
272
|
+
) => CfBuilder;
|
|
273
|
+
setBackground: (color?: string) => CfBuilder;
|
|
274
|
+
setFontColor: (color?: string) => CfBuilder;
|
|
275
|
+
setRanges: (ranges: unknown[]) => CfBuilder;
|
|
276
|
+
build: () => unknown;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const RANGE_NOTE_STYLE: CSSProperties = {
|
|
280
|
+
fontSize: 12,
|
|
281
|
+
color: 'var(--cs-chrome-muted, #605e5c)',
|
|
282
|
+
marginBottom: 12,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const CHECK_STYLE: CSSProperties = {
|
|
286
|
+
display: 'flex',
|
|
287
|
+
alignItems: 'center',
|
|
288
|
+
gap: 6,
|
|
289
|
+
marginBottom: 8,
|
|
290
|
+
cursor: 'pointer',
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const COLOR_INPUT_STYLE: CSSProperties = {
|
|
294
|
+
width: 48,
|
|
295
|
+
height: 30,
|
|
296
|
+
padding: 2,
|
|
297
|
+
border: '1px solid var(--cs-chrome-border, #cdd3db)',
|
|
298
|
+
borderRadius: 6,
|
|
299
|
+
background: 'var(--cs-chrome-input-bg, #fff)',
|
|
300
|
+
cursor: 'pointer',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const TWO_COL_STYLE: CSSProperties = {
|
|
304
|
+
display: 'grid',
|
|
305
|
+
gridTemplateColumns: '1fr 1fr',
|
|
306
|
+
gap: 8,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export function ConditionalFormattingDialog({ api, onClose }: DialogComponentProps) {
|
|
310
|
+
const [state, setState] = useState<DialogState>(INITIAL_STATE);
|
|
311
|
+
|
|
312
|
+
// Read the selection once for the header hint. `getA1Notation` off the live
|
|
313
|
+
// FRange (verified in @univerjs/sheets/facade f-range.d.ts) gives the
|
|
314
|
+
// user-facing A1 label, e.g. "A1:B2".
|
|
315
|
+
const rangeLabel = useMemo(() => {
|
|
316
|
+
const fRange = activeRange(api) as unknown as { getA1Notation?: () => string } | null;
|
|
317
|
+
return fRange?.getA1Notation?.() ?? null;
|
|
318
|
+
}, [api]);
|
|
319
|
+
|
|
320
|
+
const hasSelection = activeRange(api) !== null;
|
|
321
|
+
|
|
322
|
+
const update = <K extends keyof DialogState>(key: K, value: DialogState[K]) =>
|
|
323
|
+
setState((prev) => ({ ...prev, [key]: value }));
|
|
324
|
+
|
|
325
|
+
const apply = () => {
|
|
326
|
+
if (applyRule(api, state)) onClose();
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const clearRules = () => {
|
|
330
|
+
const sheet = activeSheet(api) as unknown as {
|
|
331
|
+
clearConditionalFormatRules?: () => unknown;
|
|
332
|
+
} | null;
|
|
333
|
+
sheet?.clearConditionalFormatRules?.();
|
|
334
|
+
onClose();
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const showOperator = state.ruleType === 'cellValue';
|
|
338
|
+
const showSecondOperand = showOperator && isRangeOperator(state.operator);
|
|
339
|
+
const isRank = state.ruleType === 'topN' || state.ruleType === 'bottomN';
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
<Dialog
|
|
343
|
+
title="Conditional formatting"
|
|
344
|
+
onClose={onClose}
|
|
345
|
+
width={440}
|
|
346
|
+
data-testid="cs-conditional-formatting-dialog"
|
|
347
|
+
footer={
|
|
348
|
+
<>
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
style={DIALOG_BTN_SECONDARY_STYLE}
|
|
352
|
+
data-testid="cs-conditional-formatting-clear"
|
|
353
|
+
onClick={clearRules}
|
|
354
|
+
>
|
|
355
|
+
Clear rules
|
|
356
|
+
</button>
|
|
357
|
+
<span style={{ flex: 1 }} />
|
|
358
|
+
<button type="button" style={DIALOG_BTN_SECONDARY_STYLE} onClick={onClose}>
|
|
359
|
+
Cancel
|
|
360
|
+
</button>
|
|
361
|
+
<button
|
|
362
|
+
type="button"
|
|
363
|
+
style={DIALOG_BTN_PRIMARY_STYLE}
|
|
364
|
+
data-testid="cs-conditional-formatting-apply"
|
|
365
|
+
disabled={!hasSelection}
|
|
366
|
+
onClick={apply}
|
|
367
|
+
>
|
|
368
|
+
Apply
|
|
369
|
+
</button>
|
|
370
|
+
</>
|
|
371
|
+
}
|
|
372
|
+
>
|
|
373
|
+
{hasSelection ? (
|
|
374
|
+
<div style={RANGE_NOTE_STYLE} data-testid="cs-conditional-formatting-range">
|
|
375
|
+
Applies to <strong>{rangeLabel ?? 'the current selection'}</strong>
|
|
376
|
+
</div>
|
|
377
|
+
) : (
|
|
378
|
+
<div style={RANGE_NOTE_STYLE} data-testid="cs-conditional-formatting-no-selection">
|
|
379
|
+
Select one or more cells first, then reopen this dialog.
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
384
|
+
<span style={DIALOG_LABEL_STYLE}>Condition type</span>
|
|
385
|
+
<select
|
|
386
|
+
style={DIALOG_INPUT_STYLE}
|
|
387
|
+
data-testid="cs-conditional-formatting-type"
|
|
388
|
+
value={state.ruleType}
|
|
389
|
+
onChange={(e) => update('ruleType', e.target.value as RuleType)}
|
|
390
|
+
>
|
|
391
|
+
{RULE_TYPE_OPTIONS.map((opt) => (
|
|
392
|
+
<option key={opt.value} value={opt.value}>
|
|
393
|
+
{opt.label}
|
|
394
|
+
</option>
|
|
395
|
+
))}
|
|
396
|
+
</select>
|
|
397
|
+
</label>
|
|
398
|
+
|
|
399
|
+
{showOperator && (
|
|
400
|
+
<>
|
|
401
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
402
|
+
<span style={DIALOG_LABEL_STYLE}>Condition</span>
|
|
403
|
+
<select
|
|
404
|
+
style={DIALOG_INPUT_STYLE}
|
|
405
|
+
data-testid="cs-conditional-formatting-operator"
|
|
406
|
+
value={state.operator}
|
|
407
|
+
onChange={(e) => update('operator', e.target.value as Operator)}
|
|
408
|
+
>
|
|
409
|
+
{OPERATOR_OPTIONS.map((opt) => (
|
|
410
|
+
<option key={opt.value} value={opt.value}>
|
|
411
|
+
{opt.label}
|
|
412
|
+
</option>
|
|
413
|
+
))}
|
|
414
|
+
</select>
|
|
415
|
+
</label>
|
|
416
|
+
|
|
417
|
+
<div style={showSecondOperand ? TWO_COL_STYLE : undefined}>
|
|
418
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
419
|
+
<span style={DIALOG_LABEL_STYLE}>{showSecondOperand ? 'Minimum' : 'Value'}</span>
|
|
420
|
+
<input
|
|
421
|
+
style={DIALOG_INPUT_STYLE}
|
|
422
|
+
data-testid="cs-conditional-formatting-operand1"
|
|
423
|
+
type="number"
|
|
424
|
+
value={state.operand1}
|
|
425
|
+
onChange={(e) => update('operand1', e.target.value)}
|
|
426
|
+
/>
|
|
427
|
+
</label>
|
|
428
|
+
{showSecondOperand && (
|
|
429
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
430
|
+
<span style={DIALOG_LABEL_STYLE}>Maximum</span>
|
|
431
|
+
<input
|
|
432
|
+
style={DIALOG_INPUT_STYLE}
|
|
433
|
+
data-testid="cs-conditional-formatting-operand2"
|
|
434
|
+
type="number"
|
|
435
|
+
value={state.operand2}
|
|
436
|
+
onChange={(e) => update('operand2', e.target.value)}
|
|
437
|
+
/>
|
|
438
|
+
</label>
|
|
439
|
+
)}
|
|
440
|
+
</div>
|
|
441
|
+
</>
|
|
442
|
+
)}
|
|
443
|
+
|
|
444
|
+
{state.ruleType === 'textContains' && (
|
|
445
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
446
|
+
<span style={DIALOG_LABEL_STYLE}>Text contains</span>
|
|
447
|
+
<input
|
|
448
|
+
style={DIALOG_INPUT_STYLE}
|
|
449
|
+
data-testid="cs-conditional-formatting-text"
|
|
450
|
+
value={state.text}
|
|
451
|
+
placeholder="e.g. urgent"
|
|
452
|
+
onChange={(e) => update('text', e.target.value)}
|
|
453
|
+
/>
|
|
454
|
+
</label>
|
|
455
|
+
)}
|
|
456
|
+
|
|
457
|
+
{isRank && (
|
|
458
|
+
<>
|
|
459
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
460
|
+
<span style={DIALOG_LABEL_STYLE}>
|
|
461
|
+
{state.ruleType === 'topN' ? 'Top' : 'Bottom'}{' '}
|
|
462
|
+
{state.rankPercent ? 'percent' : 'count'}
|
|
463
|
+
</span>
|
|
464
|
+
<input
|
|
465
|
+
style={DIALOG_INPUT_STYLE}
|
|
466
|
+
data-testid="cs-conditional-formatting-rank-n"
|
|
467
|
+
type="number"
|
|
468
|
+
min={1}
|
|
469
|
+
value={state.rankN}
|
|
470
|
+
onChange={(e) => update('rankN', e.target.value)}
|
|
471
|
+
/>
|
|
472
|
+
</label>
|
|
473
|
+
<label style={CHECK_STYLE}>
|
|
474
|
+
<input
|
|
475
|
+
type="checkbox"
|
|
476
|
+
data-testid="cs-conditional-formatting-rank-percent"
|
|
477
|
+
checked={state.rankPercent}
|
|
478
|
+
onChange={(e) => update('rankPercent', e.target.checked)}
|
|
479
|
+
/>
|
|
480
|
+
<span>Interpret as percent</span>
|
|
481
|
+
</label>
|
|
482
|
+
</>
|
|
483
|
+
)}
|
|
484
|
+
|
|
485
|
+
{state.ruleType === 'colorScale' ? (
|
|
486
|
+
<div style={TWO_COL_STYLE}>
|
|
487
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
488
|
+
<span style={DIALOG_LABEL_STYLE}>Min color</span>
|
|
489
|
+
<input
|
|
490
|
+
style={COLOR_INPUT_STYLE}
|
|
491
|
+
data-testid="cs-conditional-formatting-scale-min"
|
|
492
|
+
type="color"
|
|
493
|
+
value={state.scaleMinColor}
|
|
494
|
+
onChange={(e) => update('scaleMinColor', e.target.value)}
|
|
495
|
+
/>
|
|
496
|
+
</label>
|
|
497
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
498
|
+
<span style={DIALOG_LABEL_STYLE}>Max color</span>
|
|
499
|
+
<input
|
|
500
|
+
style={COLOR_INPUT_STYLE}
|
|
501
|
+
data-testid="cs-conditional-formatting-scale-max"
|
|
502
|
+
type="color"
|
|
503
|
+
value={state.scaleMaxColor}
|
|
504
|
+
onChange={(e) => update('scaleMaxColor', e.target.value)}
|
|
505
|
+
/>
|
|
506
|
+
</label>
|
|
507
|
+
</div>
|
|
508
|
+
) : (
|
|
509
|
+
<div style={TWO_COL_STYLE}>
|
|
510
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
511
|
+
<span style={DIALOG_LABEL_STYLE}>Fill color</span>
|
|
512
|
+
<input
|
|
513
|
+
style={COLOR_INPUT_STYLE}
|
|
514
|
+
data-testid="cs-conditional-formatting-fill-color"
|
|
515
|
+
type="color"
|
|
516
|
+
value={state.fillColor}
|
|
517
|
+
onChange={(e) => update('fillColor', e.target.value)}
|
|
518
|
+
/>
|
|
519
|
+
</label>
|
|
520
|
+
<label style={DIALOG_FIELD_STYLE}>
|
|
521
|
+
<span style={DIALOG_LABEL_STYLE}>Text color</span>
|
|
522
|
+
<input
|
|
523
|
+
style={COLOR_INPUT_STYLE}
|
|
524
|
+
data-testid="cs-conditional-formatting-text-color"
|
|
525
|
+
type="color"
|
|
526
|
+
value={state.textColor}
|
|
527
|
+
onChange={(e) => update('textColor', e.target.value)}
|
|
528
|
+
/>
|
|
529
|
+
</label>
|
|
530
|
+
</div>
|
|
531
|
+
)}
|
|
532
|
+
</Dialog>
|
|
533
|
+
);
|
|
534
|
+
}
|