@gooddata/sdk-ui-ext 11.50.0-alpha.1 → 11.50.0-alpha.3
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/NOTICE +216 -203
- package/esm/internal/components/configurationControls/DataLabelsControl.d.ts +2 -1
- package/esm/internal/components/configurationControls/DataLabelsControl.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/DataLabelsControl.js +6 -4
- package/esm/internal/components/configurationControls/conditionalFormatting/CfDateValueDropdown.d.ts +25 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/CfDateValueDropdown.d.ts.map +1 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/CfDateValueDropdown.js +140 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/CfSelect.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/CfSelect.js +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts +6 -3
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.js +40 -36
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts +7 -2
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.js +17 -8
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts +7 -2
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.js +20 -8
- package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts +54 -19
- package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.js +160 -30
- package/esm/internal/components/configurationControls/conditionalFormatting/useCfDateFilterOptions.d.ts +12 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/useCfDateFilterOptions.d.ts.map +1 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/useCfDateFilterOptions.js +44 -0
- package/esm/internal/components/configurationPanels/PieChartConfigurationPanel.d.ts.map +1 -1
- package/esm/internal/components/configurationPanels/PieChartConfigurationPanel.js +3 -2
- package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts +6 -1
- package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts.map +1 -1
- package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.js +2 -2
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts +3 -0
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts.map +1 -1
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.js +14 -2
- package/esm/internal/constants/dropdowns.d.ts +1 -0
- package/esm/internal/constants/dropdowns.d.ts.map +1 -1
- package/esm/internal/constants/dropdowns.js +20 -2
- package/esm/internal/constants/supportedProperties.d.ts.map +1 -1
- package/esm/internal/constants/supportedProperties.js +1 -0
- package/esm/internal/translations/en-US.localization-bundle.d.ts +73 -1
- package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-US.localization-bundle.js +76 -4
- package/esm/internal/utils/translations.d.ts.map +1 -1
- package/esm/internal/utils/translations.js +2 -1
- package/esm/locales.d.ts +36 -1
- package/esm/locales.d.ts.map +1 -1
- package/esm/locales.js +22 -1
- package/esm/tsdoc-metadata.json +1 -1
- package/package.json +21 -21
- package/styles/css/main.css +4 -0
- package/styles/css/main.css.map +1 -1
- package/styles/internal/css/conditional_formatting.css +4 -0
- package/styles/internal/css/conditional_formatting.css.map +1 -1
- package/styles/internal/scss/conditional_formatting.scss +6 -0
|
@@ -8,23 +8,29 @@ import { Button, UiIconButton } from "@gooddata/sdk-ui-kit";
|
|
|
8
8
|
import { conditionalFormattingMessages } from "../../../../locales.js";
|
|
9
9
|
import { ConfigSection } from "../ConfigSection.js";
|
|
10
10
|
import { ConditionalFormattingDialog } from "./ConditionalFormattingDialog.js";
|
|
11
|
-
import { buildTargetOptions, findTargetOption, newRule, targetIcon, targetLocalId, } from "./conditionalFormattingModel.js";
|
|
11
|
+
import { buildTargetOptions, findTargetOption, isDateTarget, isRuleComplete, newRule, targetIcon, targetLocalId, } from "./conditionalFormattingModel.js";
|
|
12
12
|
import { ReorderList } from "./ReorderList.js";
|
|
13
|
+
import { useCfDateFilterOptions } from "./useCfDateFilterOptions.js";
|
|
13
14
|
const SECTION_ID = "conditionalFormatting_section";
|
|
14
15
|
// Array order = evaluation order (first-match-wins).
|
|
15
|
-
function RuleChip({ rule, option, labels, slot, onEdit, onDelete }) {
|
|
16
|
-
return (_jsxs("div", { className: cx("gd-cf-rule", slot.className), ...slot.rootProps, children: [slot.handle, _jsxs("button", { type: "button", className: "gd-cf-rule__body", onClick: onEdit, title: labels.edit, children: [
|
|
17
|
-
_jsx("span", { className: `gd-cf-type-icon ${targetIcon(rule.target.kind)}`, "aria-hidden": "true" }), _jsx("span", { className: "gd-cf-rule__title", children: option ? option.title : targetLocalId(rule.target) }),
|
|
16
|
+
function RuleChip({ rule, option, invalid, editable, labels, slot, onEdit, onDelete }) {
|
|
17
|
+
return (_jsxs("div", { className: cx("gd-cf-rule", slot.className), ...slot.rootProps, children: [slot.handle, _jsxs("button", { type: "button", className: "gd-cf-rule__body", onClick: onEdit, title: labels.edit, disabled: !editable, children: [
|
|
18
|
+
_jsx("span", { className: `gd-cf-type-icon ${targetIcon(rule.target.kind, isDateTarget(option))}`, "aria-hidden": "true" }), _jsx("span", { className: "gd-cf-rule__title", children: option ? option.title : targetLocalId(rule.target) }), invalid ? (_jsx("span", { className: "gd-cf-rule__invalid", children: option ? labels.invalidValue : labels.invalid })) : null] }), _jsx("span", { className: "gd-cf-rule__delete", children: _jsx(UiIconButton, { icon: "trash", size: "small", variant: "tertiary", isDesctructive: true, label: labels.delete, onClick: onDelete }) })
|
|
18
19
|
] }));
|
|
19
20
|
}
|
|
20
|
-
export function ConditionalFormattingSection({ properties, propertiesMeta, insight, targetData, separators, isLoading, pushData, }) {
|
|
21
|
+
export function ConditionalFormattingSection({ properties, propertiesMeta, insight, targetData, backend, workspace, dateSettings, separators, isLoading, pushData, }) {
|
|
21
22
|
const intl = useIntl();
|
|
22
23
|
const [dialog, setDialog] = useState(null);
|
|
23
24
|
const config = properties?.controls?.["conditionalFormatting"];
|
|
24
25
|
const rules = config?.rules ?? [];
|
|
25
26
|
const enabled = config?.enabled ?? false;
|
|
26
27
|
const targetOptions = insight ? buildTargetOptions(insight, targetData) : [];
|
|
27
|
-
|
|
28
|
+
// Only date pickers consume the catalog — skip the backend query when no target is date-eligible.
|
|
29
|
+
const dateFilterOptions = useCfDateFilterOptions(backend, workspace, targetOptions.some(isDateTarget));
|
|
30
|
+
// Completeness and authoring both need execution-resolved date metadata; before the first data view
|
|
31
|
+
// it's unknown — don't flash a false "Invalid" badge, don't author a plain-text rule on a date attr.
|
|
32
|
+
const targetDataReady = targetData?.dates !== undefined;
|
|
33
|
+
const canAddRule = !isLoading && targetOptions.length > 0 && targetDataReady;
|
|
28
34
|
const commit = (rulesNext, enabledNext) => {
|
|
29
35
|
// Spread the existing config so version (and any future cross-stack fields) survive an edit.
|
|
30
36
|
const next = { ...config, enabled: enabledNext, rules: rulesNext };
|
|
@@ -44,11 +50,17 @@ export function ConditionalFormattingSection({ properties, propertiesMeta, insig
|
|
|
44
50
|
const deleteRule = (id) => commit(rules.filter((rule) => rule.id !== id), enabled);
|
|
45
51
|
const chipLabels = {
|
|
46
52
|
invalid: intl.formatMessage(conditionalFormattingMessages.ruleInvalid),
|
|
53
|
+
invalidValue: intl.formatMessage(conditionalFormattingMessages.ruleInvalidValue),
|
|
47
54
|
edit: intl.formatMessage(conditionalFormattingMessages.ruleEdit),
|
|
48
55
|
delete: intl.formatMessage(conditionalFormattingMessages.ruleDelete),
|
|
49
56
|
};
|
|
50
57
|
return (_jsxs(ConfigSection, { id: SECTION_ID, className: "gd-cf-section", title: conditionalFormattingMessages.sectionTitle.id, propertiesMeta: propertiesMeta, properties: properties, pushData: pushData, canBeToggled: true, toggledOn: enabled, toggleDisabled: isLoading, onToggle: (checked) => commit(rules, checked), children: [
|
|
51
58
|
_jsx("span", { className: "s-cf-popover-anchor gd-cf-section__anchor" }), _jsxs("div", { className: "gd-cf-section__subcategory", children: [
|
|
52
|
-
_jsx("span", { className: "gd-cf-section__rules-label", children: intl.formatMessage(conditionalFormattingMessages.rulesLabel) }), _jsx("span", { className: "gd-cf-section__divider" }), _jsx(Button, { className: "gd-button-link gd-cf-section__add", iconLeft: "gd-icon-plus", value: intl.formatMessage(conditionalFormattingMessages.addRule), disabled: !canAddRule, onClick: () => setDialog({ rule: newRule(targetOptions[0]
|
|
53
|
-
] }), rules.length === 0 ? (_jsx("div", { className: "gd-cf-section__empty", children: intl.formatMessage(conditionalFormattingMessages.empty) })) : (_jsx("div", { className: "gd-cf-section__rules", children: _jsx(ReorderList, { items: rules, getKey: (rule) => rule.id, onReorder: (next) => commit(next, enabled), renderItem: (rule, slot) =>
|
|
59
|
+
_jsx("span", { className: "gd-cf-section__rules-label", children: intl.formatMessage(conditionalFormattingMessages.rulesLabel) }), _jsx("span", { className: "gd-cf-section__divider" }), _jsx(Button, { className: "gd-button-link gd-cf-section__add", iconLeft: "gd-icon-plus", value: intl.formatMessage(conditionalFormattingMessages.addRule), disabled: !canAddRule, onClick: () => setDialog({ rule: newRule(targetOptions[0]), isNew: true }) })
|
|
60
|
+
] }), rules.length === 0 ? (_jsx("div", { className: "gd-cf-section__empty", children: intl.formatMessage(conditionalFormattingMessages.empty) })) : (_jsx("div", { className: "gd-cf-section__rules", children: _jsx(ReorderList, { items: rules, getKey: (rule) => rule.id, onReorder: (next) => commit(next, enabled), renderItem: (rule, slot) => {
|
|
61
|
+
const option = findTargetOption(targetOptions, rule.target);
|
|
62
|
+
// A missing target flags immediately; value validation waits for date metadata.
|
|
63
|
+
const invalid = !option || (targetDataReady && !isRuleComplete(rule, option.date));
|
|
64
|
+
return (_jsx(RuleChip, { rule: rule, option: option, invalid: invalid, editable: targetDataReady, labels: chipLabels, slot: slot, onEdit: () => setDialog({ rule, isNew: false }), onDelete: () => deleteRule(rule.id) }));
|
|
65
|
+
} }) })), dialog ? (_jsx(ConditionalFormattingDialog, { rule: dialog.rule, isNew: dialog.isNew, targetOptions: targetOptions, separators: separators, dateFilterOptions: dateFilterOptions, dateSettings: dateSettings, alignTo: ".s-cf-popover-anchor", onSave: (rule) => saveRule(rule, dialog.isNew), onClose: () => setDialog(null) }, dialog.rule.id)) : null] }));
|
|
54
66
|
}
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import { type IInsightDefinition } from "@gooddata/sdk-model";
|
|
1
|
+
import { type DateFilterGranularity, type IInsightDefinition, type WeekStart } from "@gooddata/sdk-model";
|
|
2
2
|
import { type DataViewFacade } from "@gooddata/sdk-ui";
|
|
3
3
|
import { type ConditionalFormattingOperator, type ConditionalFormattingTarget, type ConditionalFormattingValue, type IConditionalFormattingCondition, type IConditionalFormattingRule } from "@gooddata/sdk-ui-pivot/next";
|
|
4
|
+
/**
|
|
5
|
+
* Date-attribute metadata date conditions author and validate against. Present only for granularities
|
|
6
|
+
* the evaluation engine supports (linear; fiscal joins once its labeling convention is verified).
|
|
7
|
+
*/
|
|
8
|
+
export interface ICfDateMeta {
|
|
9
|
+
granularity: DateFilterGranularity;
|
|
10
|
+
/** The column's timezone (descriptor `format.timezone`) — "today" resolves in it. */
|
|
11
|
+
timezone?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Workspace date-display settings the date value picker honors (derived by the host from settings). */
|
|
14
|
+
export interface ICfDateSettings {
|
|
15
|
+
dateFormat?: string;
|
|
16
|
+
weekStart?: WeekStart;
|
|
17
|
+
}
|
|
4
18
|
/** A selectable measure/attribute for the rule's "applies to" dropdown. */
|
|
5
19
|
export interface ITargetOption {
|
|
6
20
|
/** `measure:<localId>` | `attribute:<localId>` */
|
|
@@ -11,6 +25,8 @@ export interface ITargetOption {
|
|
|
11
25
|
isPercent?: boolean;
|
|
12
26
|
/** Element suggestions from the current (paged) result — a convenience over free text, never a constraint. */
|
|
13
27
|
elements?: readonly string[];
|
|
28
|
+
/** Present iff the attribute is a date-condition-eligible date attribute; switches the condition set. */
|
|
29
|
+
date?: ICfDateMeta;
|
|
14
30
|
}
|
|
15
31
|
/** Execution-resolved data the CF UI needs per localId; flows as one object from the pluggable down. */
|
|
16
32
|
export interface ICfTargetData {
|
|
@@ -20,6 +36,8 @@ export interface ICfTargetData {
|
|
|
20
36
|
formats?: Record<string, string>;
|
|
21
37
|
/** Distinct attribute element values by localId; drives value autocomplete. */
|
|
22
38
|
elements?: Record<string, string[]>;
|
|
39
|
+
/** Date metadata by localId for date-condition-eligible attributes (descriptor granularity/timezone). */
|
|
40
|
+
dates?: Record<string, ICfDateMeta>;
|
|
23
41
|
}
|
|
24
42
|
/**
|
|
25
43
|
* Extracts {@link ICfTargetData} from the executed data view. Titles must come from here — attribute
|
|
@@ -29,26 +47,41 @@ export interface ICfTargetData {
|
|
|
29
47
|
export declare function buildCfTargetData(dataView: DataViewFacade): Required<ICfTargetData>;
|
|
30
48
|
export declare function buildTargetOptions(insight: IInsightDefinition, data?: ICfTargetData): ITargetOption[];
|
|
31
49
|
export declare const findTargetOption: (options: ITargetOption[], target: ConditionalFormattingTarget) => ITargetOption | undefined;
|
|
50
|
+
/** Date-eligible target: the option carries execution-resolved date metadata. */
|
|
51
|
+
export declare const isDateTarget: (option: ITargetOption | undefined) => boolean;
|
|
32
52
|
export declare const targetLocalId: (target: ConditionalFormattingTarget) => string;
|
|
33
|
-
export declare const
|
|
53
|
+
export declare const operatorsForTarget: (kind: "attribute" | "measure", isDate: boolean) => ConditionalFormattingOperator[];
|
|
34
54
|
export declare const operatorIcon: (operator: ConditionalFormattingOperator) => string | undefined;
|
|
35
|
-
export declare const targetIcon: (kind: "attribute" | "measure") => string;
|
|
55
|
+
export declare const targetIcon: (kind: "attribute" | "measure", isDate?: boolean) => string;
|
|
36
56
|
export type OperatorArity = "none" | "single" | "range";
|
|
37
57
|
export declare const operatorArity: (operator: ConditionalFormattingOperator) => OperatorArity;
|
|
38
|
-
export type ConditionValueEditor = "none" | "number" | "combobox" | "text" | "range";
|
|
58
|
+
export type ConditionValueEditor = "none" | "number" | "combobox" | "text" | "range" | "date";
|
|
39
59
|
/**
|
|
40
60
|
* Which editor a condition's value renders with. Element suggestions apply only to attribute
|
|
41
|
-
* Is / Is not (per design) — substring operators keep the plain text input.
|
|
61
|
+
* Is / Is not (per design) — substring operators keep the plain text input. A date target's single
|
|
62
|
+
* operand is always the period picker.
|
|
42
63
|
*/
|
|
43
|
-
export declare const valueEditorKind: (condition: IConditionalFormattingCondition, kind: "attribute" | "measure", hasSuggestions: boolean) => ConditionValueEditor;
|
|
64
|
+
export declare const valueEditorKind: (condition: IConditionalFormattingCondition, kind: "attribute" | "measure", hasSuggestions: boolean, isDate: boolean) => ConditionValueEditor;
|
|
44
65
|
/** Fresh empty operand of the right shape for the operator. */
|
|
45
|
-
export declare const emptyValueForOperator: (operator: ConditionalFormattingOperator) => ConditionalFormattingValue;
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
export declare const emptyValueForOperator: (operator: ConditionalFormattingOperator, isDate?: boolean) => ConditionalFormattingValue;
|
|
67
|
+
/**
|
|
68
|
+
* An entered operand survives operator changes within the same shape (e.g. \> to \>=); a picked date
|
|
69
|
+
* period likewise survives switching among the single-operand date operators (Is on ↔ Is after…).
|
|
70
|
+
*/
|
|
71
|
+
export declare const valueForOperator: (operator: ConditionalFormattingOperator, previous: ConditionalFormattingValue, isDate: boolean) => ConditionalFormattingValue;
|
|
72
|
+
/** The text a free-text/combobox editor shows for a literal operand. */
|
|
73
|
+
export declare const literalText: (value: ConditionalFormattingValue) => string;
|
|
74
|
+
/** The finite number a numeric editor shows for a literal operand, or null (blank input). */
|
|
75
|
+
export declare const literalRaw: (value: ConditionalFormattingValue) => number | null;
|
|
76
|
+
/** One finite bound of a range operand, or null (blank input). */
|
|
77
|
+
export declare const rangeRaw: (value: ConditionalFormattingValue, bound: "from" | "to") => number | null;
|
|
78
|
+
export declare const newCondition: (isDate?: boolean) => IConditionalFormattingCondition;
|
|
79
|
+
export declare const newRule: (option: ITargetOption) => IConditionalFormattingRule;
|
|
48
80
|
/**
|
|
49
|
-
* Rule re-pointed at a new target. Crossing target kinds
|
|
50
|
-
*
|
|
51
|
-
*
|
|
81
|
+
* Rule re-pointed at a new target. Crossing target kinds — or the date/plain boundary within
|
|
82
|
+
* attributes — coerces operators to the new family and clears values (per-condition color/scope
|
|
83
|
+
* survive). Crossing the percent boundary, or a granularity change within dates, keeps operators
|
|
84
|
+
* but clears values. Either way Save disables until re-entered.
|
|
52
85
|
*/
|
|
53
86
|
export declare const ruleWithTarget: (rule: IConditionalFormattingRule, next: ITargetOption, previous: ITargetOption | undefined) => IConditionalFormattingRule;
|
|
54
87
|
/**
|
|
@@ -61,16 +94,18 @@ export declare const isPercentFormat: (format: string | undefined) => boolean;
|
|
|
61
94
|
export declare const rawToDisplayNumber: (raw: number, percent: boolean) => number;
|
|
62
95
|
/** Number entered in a percent input -> the raw number stored and compared (÷100). */
|
|
63
96
|
export declare const displayToRawNumber: (display: number, percent: boolean) => number;
|
|
64
|
-
export type ConditionalFormattingFieldError = "rangeOrder";
|
|
97
|
+
export type ConditionalFormattingFieldError = "rangeOrder" | "dateUnresolvable";
|
|
65
98
|
export interface IConditionErrors {
|
|
66
99
|
range?: ConditionalFormattingFieldError;
|
|
100
|
+
date?: ConditionalFormattingFieldError;
|
|
67
101
|
}
|
|
68
102
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
103
|
+
* Coerces stored condition shapes the editor cannot represent so an AAC-authored rule opens editable
|
|
104
|
+
* instead of dead-ended (disabled Save then covers the cleared values). A non-numeric measure literal
|
|
105
|
+
* clears to empty; on a date target, operators/values outside the date set reset to an unpicked
|
|
106
|
+
* "Is on" (id and format survive). The engine keeps evaluating the stored original until Save.
|
|
72
107
|
*/
|
|
73
|
-
export declare const sanitizeRuleForEditing: (rule: IConditionalFormattingRule) => IConditionalFormattingRule;
|
|
108
|
+
export declare const sanitizeRuleForEditing: (rule: IConditionalFormattingRule, isDate?: boolean) => IConditionalFormattingRule;
|
|
74
109
|
export interface IConditionValidation {
|
|
75
110
|
/** A required operand is absent — gates Save, never shown as an error. */
|
|
76
111
|
missing: boolean;
|
|
@@ -81,7 +116,7 @@ export interface IConditionValidation {
|
|
|
81
116
|
* Single source for both validation surfaces, making "empty is incomplete, not invalid" structural:
|
|
82
117
|
* {@link isRuleComplete} derives from the whole result, the editor renders only `errors`.
|
|
83
118
|
*/
|
|
84
|
-
export declare const validateCondition: (condition: IConditionalFormattingCondition, kind: "attribute" | "measure") => IConditionValidation;
|
|
119
|
+
export declare const validateCondition: (condition: IConditionalFormattingCondition, kind: "attribute" | "measure", date?: ICfDateMeta | undefined) => IConditionValidation;
|
|
85
120
|
/** A rule is saveable when every condition has a valid operand for its operator. */
|
|
86
|
-
export declare const isRuleComplete: (rule: IConditionalFormattingRule) => boolean;
|
|
121
|
+
export declare const isRuleComplete: (rule: IConditionalFormattingRule, date?: ICfDateMeta | undefined) => boolean;
|
|
87
122
|
//# sourceMappingURL=conditionalFormattingModel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conditionalFormattingModel.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.ts"],"names":[],"mappings":"AAIA,OAAO,EACH,KAAK,kBAAkB,
|
|
1
|
+
{"version":3,"file":"conditionalFormattingModel.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { v4 as uuid } from "uuid";
|
|
3
3
|
import { attributeAlias, attributeLocalId, bucketAttributes, insightBucket, insightMeasures, isResultAttributeHeader, measureAlias, measureDoesComputeRatio, measureFormat, measureLocalId, measureTitle, resultHeaderName, } from "@gooddata/sdk-model";
|
|
4
4
|
import { BucketNames } from "@gooddata/sdk-ui";
|
|
5
|
+
import { isDateConditionValue, normalizeDateConditionGranularity, resolveDateConditionBounds, } from "@gooddata/sdk-ui-pivot/next";
|
|
5
6
|
import { CF_DEFAULT_COLOR } from "./conditionalFormattingColors.js";
|
|
6
7
|
// Cap suggestions so a high-cardinality attribute can't build a huge suggestion list.
|
|
7
8
|
const MAX_ELEMENT_SUGGESTIONS = 200;
|
|
@@ -16,6 +17,7 @@ const targetToValue = (target) => target.kind === "measure"
|
|
|
16
17
|
export function buildCfTargetData(dataView) {
|
|
17
18
|
const titles = {};
|
|
18
19
|
const formats = {};
|
|
20
|
+
const dates = {};
|
|
19
21
|
for (const measure of dataView.meta().measureDescriptors()) {
|
|
20
22
|
const { localIdentifier, name, format } = measure.measureHeaderItem;
|
|
21
23
|
titles[localIdentifier] = name;
|
|
@@ -24,9 +26,18 @@ export function buildCfTargetData(dataView) {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
for (const attribute of dataView.meta().attributeDescriptors()) {
|
|
27
|
-
|
|
29
|
+
const { localIdentifier, formOf, granularity, format } = attribute.attributeHeader;
|
|
30
|
+
titles[localIdentifier] = formOf.name;
|
|
31
|
+
// Eligible date attributes (resolvable granularity) get date conditions; the rest keep plain text.
|
|
32
|
+
const normalizedGranularity = normalizeDateConditionGranularity(granularity);
|
|
33
|
+
if (normalizedGranularity) {
|
|
34
|
+
dates[localIdentifier] = {
|
|
35
|
+
granularity: normalizedGranularity,
|
|
36
|
+
...(format?.timezone ? { timezone: format.timezone } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
28
39
|
}
|
|
29
|
-
return { titles, formats, elements: buildElementsByLocalId(dataView) };
|
|
40
|
+
return { titles, formats, elements: buildElementsByLocalId(dataView), dates };
|
|
30
41
|
}
|
|
31
42
|
// Attribute header groups align by index with the dimension's attribute descriptors; the collected
|
|
32
43
|
// elements are only as complete as the loaded (paged) result.
|
|
@@ -59,7 +70,7 @@ function buildElementsByLocalId(dataView) {
|
|
|
59
70
|
return elements;
|
|
60
71
|
}
|
|
61
72
|
export function buildTargetOptions(insight, data = {}) {
|
|
62
|
-
const { titles = {}, formats = {}, elements = {} } = data;
|
|
73
|
+
const { titles = {}, formats = {}, elements = {}, dates = {} } = data;
|
|
63
74
|
const measures = insightMeasures(insight).map((measure) => {
|
|
64
75
|
const localId = measureLocalId(measure);
|
|
65
76
|
const target = { kind: "measure", measureIdentifier: localId };
|
|
@@ -89,11 +100,14 @@ export function buildTargetOptions(insight, data = {}) {
|
|
|
89
100
|
title: titles[localId] ?? attributeAlias(attribute) ?? localId,
|
|
90
101
|
target,
|
|
91
102
|
elements: elements[localId],
|
|
103
|
+
date: dates[localId],
|
|
92
104
|
};
|
|
93
105
|
});
|
|
94
106
|
return [...measures, ...attributes];
|
|
95
107
|
}
|
|
96
108
|
export const findTargetOption = (options, target) => options.find((option) => option.value === targetToValue(target));
|
|
109
|
+
/** Date-eligible target: the option carries execution-resolved date metadata. */
|
|
110
|
+
export const isDateTarget = (option) => option?.date !== undefined;
|
|
97
111
|
export const targetLocalId = (target) => target.kind === "measure" ? target.measureIdentifier : target.attributeIdentifier;
|
|
98
112
|
// --- Operators -------------------------------------------------------------------------------
|
|
99
113
|
const NO_VALUE_OPERATORS = new Set([
|
|
@@ -128,7 +142,25 @@ const ATTRIBUTE_OPERATORS = [
|
|
|
128
142
|
"IS_EMPTY",
|
|
129
143
|
"IS_NOT_EMPTY",
|
|
130
144
|
];
|
|
131
|
-
|
|
145
|
+
// Same operator constants as measures, date-specific labels. The inclusive pair (on-or-after/before)
|
|
146
|
+
// avoids the "Is after yesterday" off-by-one; no IS_NOT_EMPTY — on a date column it equals All time.
|
|
147
|
+
const DATE_OPERATORS = [
|
|
148
|
+
"ALL",
|
|
149
|
+
"EQUAL_TO",
|
|
150
|
+
"NOT_EQUAL_TO",
|
|
151
|
+
"GREATER_THAN",
|
|
152
|
+
"GREATER_THAN_OR_EQUAL_TO",
|
|
153
|
+
"LESS_THAN",
|
|
154
|
+
"LESS_THAN_OR_EQUAL_TO",
|
|
155
|
+
"IS_EMPTY",
|
|
156
|
+
];
|
|
157
|
+
const DATE_OPERATOR_SET = new Set(DATE_OPERATORS);
|
|
158
|
+
export const operatorsForTarget = (kind, isDate) => {
|
|
159
|
+
if (kind === "measure") {
|
|
160
|
+
return MEASURE_OPERATORS;
|
|
161
|
+
}
|
|
162
|
+
return isDate ? DATE_OPERATORS : ATTRIBUTE_OPERATORS;
|
|
163
|
+
};
|
|
132
164
|
// Shared operator glyphs from sdk-ui-kit (same icons the measure-value-filter uses). Numeric/common
|
|
133
165
|
// operators have icons; text and empty operators render label-only (matching the attribute filter).
|
|
134
166
|
const OPERATOR_ICON_NAMES = {
|
|
@@ -146,9 +178,14 @@ export const operatorIcon = (operator) => {
|
|
|
146
178
|
const name = OPERATOR_ICON_NAMES[operator];
|
|
147
179
|
return name ? `gd-icon-${name}` : undefined;
|
|
148
180
|
};
|
|
149
|
-
// Type icon distinguishing attribute (ABC)
|
|
150
|
-
// elsewhere for catalog/bucket items.
|
|
151
|
-
export const targetIcon = (kind
|
|
181
|
+
// Type icon distinguishing attribute (ABC), date (calendar), and measure (metric) targets — same
|
|
182
|
+
// icons AD uses elsewhere for catalog/bucket items.
|
|
183
|
+
export const targetIcon = (kind, isDate = false) => {
|
|
184
|
+
if (kind === "measure") {
|
|
185
|
+
return "gd-icon-metric";
|
|
186
|
+
}
|
|
187
|
+
return isDate ? "gd-icon-date" : "gd-icon-attribute";
|
|
188
|
+
};
|
|
152
189
|
export const operatorArity = (operator) => {
|
|
153
190
|
if (NO_VALUE_OPERATORS.has(operator)) {
|
|
154
191
|
return "none";
|
|
@@ -158,15 +195,19 @@ export const operatorArity = (operator) => {
|
|
|
158
195
|
const isEqualityOperator = (operator) => operator === "EQUAL_TO" || operator === "NOT_EQUAL_TO";
|
|
159
196
|
/**
|
|
160
197
|
* Which editor a condition's value renders with. Element suggestions apply only to attribute
|
|
161
|
-
* Is / Is not (per design) — substring operators keep the plain text input.
|
|
198
|
+
* Is / Is not (per design) — substring operators keep the plain text input. A date target's single
|
|
199
|
+
* operand is always the period picker.
|
|
162
200
|
*/
|
|
163
|
-
export const valueEditorKind = (condition, kind, hasSuggestions) => {
|
|
201
|
+
export const valueEditorKind = (condition, kind, hasSuggestions, isDate) => {
|
|
164
202
|
switch (operatorArity(condition.operator)) {
|
|
165
203
|
case "none":
|
|
166
204
|
return "none";
|
|
167
205
|
case "range":
|
|
168
206
|
return "range";
|
|
169
207
|
case "single":
|
|
208
|
+
if (isDate) {
|
|
209
|
+
return "date";
|
|
210
|
+
}
|
|
170
211
|
if (kind === "measure") {
|
|
171
212
|
return "number";
|
|
172
213
|
}
|
|
@@ -175,7 +216,7 @@ export const valueEditorKind = (condition, kind, hasSuggestions) => {
|
|
|
175
216
|
};
|
|
176
217
|
// --- Factories -------------------------------------------------------------------------------
|
|
177
218
|
/** Fresh empty operand of the right shape for the operator. */
|
|
178
|
-
export const emptyValueForOperator = (operator) => {
|
|
219
|
+
export const emptyValueForOperator = (operator, isDate = false) => {
|
|
179
220
|
switch (operatorArity(operator)) {
|
|
180
221
|
case "none":
|
|
181
222
|
return { kind: "none" };
|
|
@@ -185,37 +226,98 @@ export const emptyValueForOperator = (operator) => {
|
|
|
185
226
|
// JSON-serialize to null); the contract type can stay a tight `{ from: number; to: number }`.
|
|
186
227
|
return { kind: "literalRange", from: NaN, to: NaN };
|
|
187
228
|
case "single":
|
|
188
|
-
|
|
229
|
+
// A date operand starts unpicked ("none"); validation treats it as missing until picked.
|
|
230
|
+
return isDate ? { kind: "none" } : { kind: "literal", value: "" };
|
|
189
231
|
}
|
|
190
232
|
};
|
|
191
|
-
|
|
233
|
+
/**
|
|
234
|
+
* An entered operand survives operator changes within the same shape (e.g. \> to \>=); a picked date
|
|
235
|
+
* period likewise survives switching among the single-operand date operators (Is on ↔ Is after…).
|
|
236
|
+
*/
|
|
237
|
+
export const valueForOperator = (operator, previous, isDate) => {
|
|
238
|
+
const empty = emptyValueForOperator(operator, isDate);
|
|
239
|
+
// Single-operand only — no-operand operators (ALL, IS_EMPTY) must not retain a hidden period.
|
|
240
|
+
if (isDate &&
|
|
241
|
+
empty.kind === "none" &&
|
|
242
|
+
isDateConditionValue(previous) &&
|
|
243
|
+
operatorArity(operator) === "single") {
|
|
244
|
+
return previous;
|
|
245
|
+
}
|
|
246
|
+
return previous.kind === empty.kind ? previous : empty;
|
|
247
|
+
};
|
|
248
|
+
/** The text a free-text/combobox editor shows for a literal operand. */
|
|
249
|
+
export const literalText = (value) => value.kind === "literal" ? String(value.value) : "";
|
|
250
|
+
/** The finite number a numeric editor shows for a literal operand, or null (blank input). */
|
|
251
|
+
export const literalRaw = (value) => {
|
|
252
|
+
if (value.kind !== "literal" || String(value.value).trim() === "") {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const n = Number(value.value);
|
|
256
|
+
return Number.isFinite(n) ? n : null;
|
|
257
|
+
};
|
|
258
|
+
/** One finite bound of a range operand, or null (blank input). */
|
|
259
|
+
export const rangeRaw = (value, bound) => {
|
|
260
|
+
if (value.kind !== "literalRange") {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const n = value[bound];
|
|
264
|
+
return Number.isFinite(n) ? n : null;
|
|
265
|
+
};
|
|
266
|
+
// Date rules default to "Is on" with the period unpicked (per design); others to the catch-all.
|
|
267
|
+
const defaultOperator = (isDate) => (isDate ? "EQUAL_TO" : "ALL");
|
|
268
|
+
export const newCondition = (isDate = false) => ({
|
|
192
269
|
id: uuid(),
|
|
193
|
-
operator:
|
|
270
|
+
operator: defaultOperator(isDate),
|
|
194
271
|
value: { kind: "none" },
|
|
195
272
|
// Default: colored text on a transparent background (no fill).
|
|
196
273
|
format: { color: CF_DEFAULT_COLOR, scope: "cell" },
|
|
197
274
|
});
|
|
198
|
-
export const newRule = (
|
|
275
|
+
export const newRule = (option) => ({
|
|
199
276
|
id: uuid(),
|
|
200
|
-
target,
|
|
201
|
-
conditions: [newCondition()],
|
|
277
|
+
target: option.target,
|
|
278
|
+
conditions: [newCondition(isDateTarget(option))],
|
|
202
279
|
});
|
|
203
280
|
/**
|
|
204
|
-
* Rule re-pointed at a new target. Crossing target kinds
|
|
205
|
-
*
|
|
206
|
-
*
|
|
281
|
+
* Rule re-pointed at a new target. Crossing target kinds — or the date/plain boundary within
|
|
282
|
+
* attributes — coerces operators to the new family and clears values (per-condition color/scope
|
|
283
|
+
* survive). Crossing the percent boundary, or a granularity change within dates, keeps operators
|
|
284
|
+
* but clears values. Either way Save disables until re-entered.
|
|
207
285
|
*/
|
|
208
286
|
export const ruleWithTarget = (rule, next, previous) => {
|
|
209
|
-
|
|
210
|
-
|
|
287
|
+
const nextIsDate = isDateTarget(next);
|
|
288
|
+
// `previous` is undefined when retargeting an invalid rule (target already left the insight);
|
|
289
|
+
// sniff the rule's own values instead, so stale date values can't leak onto a non-date target.
|
|
290
|
+
// Shapes the sniff can't classify (ALL/IS_EMPTY-only) are valid in both families anyway.
|
|
291
|
+
const wasDate = previous
|
|
292
|
+
? isDateTarget(previous)
|
|
293
|
+
: rule.conditions.some((condition) => isDateConditionValue(condition.value));
|
|
294
|
+
if (next.target.kind !== rule.target.kind || nextIsDate !== wasDate) {
|
|
295
|
+
const validOperators = new Set(operatorsForTarget(next.target.kind, nextIsDate));
|
|
296
|
+
return {
|
|
297
|
+
...rule,
|
|
298
|
+
target: next.target,
|
|
299
|
+
// Explicit shape (no spread) so no future family-specific field rides across the boundary.
|
|
300
|
+
conditions: rule.conditions.map((condition) => {
|
|
301
|
+
const operator = validOperators.has(condition.operator)
|
|
302
|
+
? condition.operator
|
|
303
|
+
: defaultOperator(nextIsDate);
|
|
304
|
+
return {
|
|
305
|
+
id: condition.id,
|
|
306
|
+
operator,
|
|
307
|
+
value: emptyValueForOperator(operator, nextIsDate),
|
|
308
|
+
format: condition.format,
|
|
309
|
+
};
|
|
310
|
+
}),
|
|
311
|
+
};
|
|
211
312
|
}
|
|
212
|
-
|
|
313
|
+
const granularityChanged = nextIsDate && next.date?.granularity !== previous?.date?.granularity;
|
|
314
|
+
if (granularityChanged || (next.isPercent ?? false) !== (previous?.isPercent ?? false)) {
|
|
213
315
|
return {
|
|
214
316
|
...rule,
|
|
215
317
|
target: next.target,
|
|
216
318
|
conditions: rule.conditions.map((condition) => ({
|
|
217
319
|
...condition,
|
|
218
|
-
value: emptyValueForOperator(condition.operator),
|
|
320
|
+
value: emptyValueForOperator(condition.operator, nextIsDate),
|
|
219
321
|
})),
|
|
220
322
|
};
|
|
221
323
|
}
|
|
@@ -240,11 +342,31 @@ export const rawToDisplayNumber = (raw, percent) => percent ? denoise(raw * 100)
|
|
|
240
342
|
export const displayToRawNumber = (display, percent) => percent ? denoise(display / 100) : display;
|
|
241
343
|
const isBlank = (value) => String(value).trim() === "";
|
|
242
344
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
345
|
+
* Coerces stored condition shapes the editor cannot represent so an AAC-authored rule opens editable
|
|
346
|
+
* instead of dead-ended (disabled Save then covers the cleared values). A non-numeric measure literal
|
|
347
|
+
* clears to empty; on a date target, operators/values outside the date set reset to an unpicked
|
|
348
|
+
* "Is on" (id and format survive). The engine keeps evaluating the stored original until Save.
|
|
246
349
|
*/
|
|
247
|
-
export const sanitizeRuleForEditing = (rule) => {
|
|
350
|
+
export const sanitizeRuleForEditing = (rule, isDate = false) => {
|
|
351
|
+
if (rule.target.kind === "attribute" && isDate) {
|
|
352
|
+
return {
|
|
353
|
+
...rule,
|
|
354
|
+
conditions: rule.conditions.map((condition) => {
|
|
355
|
+
const operatorOk = DATE_OPERATOR_SET.has(condition.operator);
|
|
356
|
+
// A period is only a valid operand on single-operand operators.
|
|
357
|
+
const valueOk = condition.value.kind === "none" ||
|
|
358
|
+
(operatorArity(condition.operator) === "single" && isDateConditionValue(condition.value));
|
|
359
|
+
if (operatorOk && valueOk) {
|
|
360
|
+
return condition;
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
...condition,
|
|
364
|
+
operator: operatorOk ? condition.operator : "EQUAL_TO",
|
|
365
|
+
value: { kind: "none" },
|
|
366
|
+
};
|
|
367
|
+
}),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
248
370
|
if (rule.target.kind !== "measure") {
|
|
249
371
|
return rule;
|
|
250
372
|
}
|
|
@@ -261,7 +383,7 @@ export const sanitizeRuleForEditing = (rule) => {
|
|
|
261
383
|
* Single source for both validation surfaces, making "empty is incomplete, not invalid" structural:
|
|
262
384
|
* {@link isRuleComplete} derives from the whole result, the editor renders only `errors`.
|
|
263
385
|
*/
|
|
264
|
-
export const validateCondition = (condition, kind) => {
|
|
386
|
+
export const validateCondition = (condition, kind, date) => {
|
|
265
387
|
const { value } = condition;
|
|
266
388
|
switch (operatorArity(condition.operator)) {
|
|
267
389
|
case "none":
|
|
@@ -274,6 +396,14 @@ export const validateCondition = (condition, kind) => {
|
|
|
274
396
|
return { missing, errors: !missing && value.from > value.to ? { range: "rangeOrder" } : {} };
|
|
275
397
|
}
|
|
276
398
|
case "single":
|
|
399
|
+
if (date) {
|
|
400
|
+
if (!isDateConditionValue(value)) {
|
|
401
|
+
return { missing: true, errors: {} };
|
|
402
|
+
}
|
|
403
|
+
// Unresolvable (malformed value, fiscal granularity) never matches — block Save.
|
|
404
|
+
const resolvable = resolveDateConditionBounds(value, date.granularity, date.timezone) !== null;
|
|
405
|
+
return { missing: false, errors: resolvable ? {} : { date: "dateUnresolvable" } };
|
|
406
|
+
}
|
|
277
407
|
if (value.kind !== "literal") {
|
|
278
408
|
return { missing: true, errors: {} };
|
|
279
409
|
}
|
|
@@ -286,8 +416,8 @@ export const validateCondition = (condition, kind) => {
|
|
|
286
416
|
}
|
|
287
417
|
};
|
|
288
418
|
/** A rule is saveable when every condition has a valid operand for its operator. */
|
|
289
|
-
export const isRuleComplete = (rule) => rule.conditions.length > 0 &&
|
|
419
|
+
export const isRuleComplete = (rule, date) => rule.conditions.length > 0 &&
|
|
290
420
|
rule.conditions.every((condition) => {
|
|
291
|
-
const { missing, errors } = validateCondition(condition, rule.target.kind);
|
|
421
|
+
const { missing, errors } = validateCondition(condition, rule.target.kind, date);
|
|
292
422
|
return !missing && Object.keys(errors).length === 0;
|
|
293
423
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type IAnalyticalBackend } from "@gooddata/sdk-backend-spi";
|
|
2
|
+
import { type IDateFilterOptionsByType } from "@gooddata/sdk-ui-filters";
|
|
3
|
+
/**
|
|
4
|
+
* The workspace date-filter preset catalog for date-condition value pickers — the same catalog the
|
|
5
|
+
* dashboard date filter consumes. Undefined while loading (the picker degrades to its static form);
|
|
6
|
+
* falls back to platform defaults when backend/workspace is absent, the workspace has no custom
|
|
7
|
+
* config, the query fails, or the config yields nothing visible.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useCfDateFilterOptions(backend: IAnalyticalBackend | undefined, workspace: string | undefined,
|
|
10
|
+
/** False = no consumer in sight (no date-eligible target) — skip the backend query entirely. */
|
|
11
|
+
enabled: boolean): IDateFilterOptionsByType | undefined;
|
|
12
|
+
//# sourceMappingURL=useCfDateFilterOptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCfDateFilterOptions.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/useCfDateFilterOptions.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,OAAO,EACH,KAAK,wBAAwB,EAIhC,MAAM,0BAA0B,CAAC;AAMlC;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAClC,OAAO,EAAE,kBAAkB,GAAG,SAAS,EACvC,SAAS,EAAE,MAAM,GAAG,SAAS;AAC7B,kGAAgG;AAChG,OAAO,EAAE,OAAO,GACjB,wBAAwB,GAAG,SAAS,CAkCtC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { useCancelablePromise } from "@gooddata/sdk-ui";
|
|
3
|
+
import { convertDateFilterConfigToDateFilterOptions, defaultDateFilterConfig, flattenDateFilterOptions, } from "@gooddata/sdk-ui-filters";
|
|
4
|
+
// Must match the catalog the dashboard degrades to (the rich default config), not sdk-ui-filters'
|
|
5
|
+
// slim defaultDateFilterOptions — else this picker and the dashboard would show different presets.
|
|
6
|
+
const FALLBACK_DATE_FILTER_OPTIONS = convertDateFilterConfigToDateFilterOptions(defaultDateFilterConfig);
|
|
7
|
+
/**
|
|
8
|
+
* The workspace date-filter preset catalog for date-condition value pickers — the same catalog the
|
|
9
|
+
* dashboard date filter consumes. Undefined while loading (the picker degrades to its static form);
|
|
10
|
+
* falls back to platform defaults when backend/workspace is absent, the workspace has no custom
|
|
11
|
+
* config, the query fails, or the config yields nothing visible.
|
|
12
|
+
*/
|
|
13
|
+
export function useCfDateFilterOptions(backend, workspace,
|
|
14
|
+
/** False = no consumer in sight (no date-eligible target) — skip the backend query entirely. */
|
|
15
|
+
enabled) {
|
|
16
|
+
const { result } = useCancelablePromise({
|
|
17
|
+
promise: enabled
|
|
18
|
+
? async () => {
|
|
19
|
+
if (!backend || !workspace) {
|
|
20
|
+
return FALLBACK_DATE_FILTER_OPTIONS;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const custom = await backend
|
|
24
|
+
.workspace(workspace)
|
|
25
|
+
.dateFilterConfigs()
|
|
26
|
+
.withLimit(1)
|
|
27
|
+
.queryCustomDateFilterConfig();
|
|
28
|
+
const config = custom.items[0];
|
|
29
|
+
if (config) {
|
|
30
|
+
const options = convertDateFilterConfigToDateFilterOptions(config);
|
|
31
|
+
if (flattenDateFilterOptions(options).some((option) => option.visible)) {
|
|
32
|
+
return options;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.warn("Loading the workspace date filter config failed; date conditions fall back to the default preset catalog.", error);
|
|
38
|
+
}
|
|
39
|
+
return FALLBACK_DATE_FILTER_OPTIONS;
|
|
40
|
+
}
|
|
41
|
+
: null,
|
|
42
|
+
}, [backend, workspace, enabled]);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PieChartConfigurationPanel.d.ts","sourceRoot":"","sources":["../../../../src/internal/components/configurationPanels/PieChartConfigurationPanel.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAiBvC,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,qBAAa,0BAA2B,SAAQ,yBAAyB;IACrE,SAAS,CAAC,wBAAwB,IAAI,SAAS,
|
|
1
|
+
{"version":3,"file":"PieChartConfigurationPanel.d.ts","sourceRoot":"","sources":["../../../../src/internal/components/configurationPanels/PieChartConfigurationPanel.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAiBvC,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,qBAAa,0BAA2B,SAAQ,yBAAyB;IACrE,SAAS,CAAC,wBAAwB,IAAI,SAAS,CAsC9C;IAED,OAAO,CAAC,mBAAmB;CAK9B"}
|
|
@@ -9,10 +9,11 @@ import { DataLabelsControl } from "../configurationControls/DataLabelsControl.js
|
|
|
9
9
|
import { ConfigurationPanelContent } from "./ConfigurationPanelContent.js";
|
|
10
10
|
export class PieChartConfigurationPanel extends ConfigurationPanelContent {
|
|
11
11
|
renderConfigurationPanel() {
|
|
12
|
-
const { propertiesMeta, properties, pushData } = this.props;
|
|
12
|
+
const { propertiesMeta, properties, pushData, featureFlags } = this.props;
|
|
13
13
|
const controlsDisabled = this.isControlDisabled();
|
|
14
|
+
const donutDataLabelsEnabled = !!featureFlags?.enableDonutDataLabels;
|
|
14
15
|
return (_jsxs(BubbleHoverTrigger, { showDelay: SHOW_DELAY_DEFAULT, hideDelay: HIDE_DELAY_DEFAULT, children: [
|
|
15
|
-
_jsxs("div", { children: [this.renderColorSection(), this.renderLegendSection(), this.renderInteractionsSection(), _jsx(ConfigSection, { id: "canvas_section", title: messages["canvasTitle"].id, propertiesMeta: propertiesMeta, properties: properties, pushData: pushData, children: _jsx(DataLabelsControl, { pushData: pushData, properties: properties, isDisabled: controlsDisabled, defaultValue: false }) }), this.renderCustomTooltipSection(), this.renderAdvancedSection()] }), _jsx(Bubble, { className: this.getBubbleClassNames(), arrowOffsets: { "tc bc": [BUBBLE_ARROW_OFFSET_X, BUBBLE_ARROW_OFFSET_Y] }, alignPoints: [{ align: "tc bc" }], children: _jsx(FormattedMessage, { id: "properties.config.not_applicable" }) })
|
|
16
|
+
_jsxs("div", { children: [this.renderColorSection(), this.renderLegendSection(), this.renderInteractionsSection(), _jsx(ConfigSection, { id: "canvas_section", title: messages["canvasTitle"].id, propertiesMeta: propertiesMeta, properties: properties, pushData: pushData, children: _jsx(DataLabelsControl, { pushData: pushData, properties: properties, isDisabled: controlsDisabled, defaultValue: false, enablePositionSelector: donutDataLabelsEnabled }) }), this.renderCustomTooltipSection(), this.renderAdvancedSection()] }), _jsx(Bubble, { className: this.getBubbleClassNames(), arrowOffsets: { "tc bc": [BUBBLE_ARROW_OFFSET_X, BUBBLE_ARROW_OFFSET_Y] }, alignPoints: [{ align: "tc bc" }], children: _jsx(FormattedMessage, { id: "properties.config.not_applicable" }) })
|
|
16
17
|
] }));
|
|
17
18
|
}
|
|
18
19
|
getBubbleClassNames() {
|