@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.
Files changed (52) hide show
  1. package/NOTICE +216 -203
  2. package/esm/internal/components/configurationControls/DataLabelsControl.d.ts +2 -1
  3. package/esm/internal/components/configurationControls/DataLabelsControl.d.ts.map +1 -1
  4. package/esm/internal/components/configurationControls/DataLabelsControl.js +6 -4
  5. package/esm/internal/components/configurationControls/conditionalFormatting/CfDateValueDropdown.d.ts +25 -0
  6. package/esm/internal/components/configurationControls/conditionalFormatting/CfDateValueDropdown.d.ts.map +1 -0
  7. package/esm/internal/components/configurationControls/conditionalFormatting/CfDateValueDropdown.js +140 -0
  8. package/esm/internal/components/configurationControls/conditionalFormatting/CfSelect.d.ts.map +1 -1
  9. package/esm/internal/components/configurationControls/conditionalFormatting/CfSelect.js +1 -1
  10. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts +6 -3
  11. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts.map +1 -1
  12. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.js +40 -36
  13. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts +7 -2
  14. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts.map +1 -1
  15. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.js +17 -8
  16. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts +7 -2
  17. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts.map +1 -1
  18. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.js +20 -8
  19. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts +54 -19
  20. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts.map +1 -1
  21. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.js +160 -30
  22. package/esm/internal/components/configurationControls/conditionalFormatting/useCfDateFilterOptions.d.ts +12 -0
  23. package/esm/internal/components/configurationControls/conditionalFormatting/useCfDateFilterOptions.d.ts.map +1 -0
  24. package/esm/internal/components/configurationControls/conditionalFormatting/useCfDateFilterOptions.js +44 -0
  25. package/esm/internal/components/configurationPanels/PieChartConfigurationPanel.d.ts.map +1 -1
  26. package/esm/internal/components/configurationPanels/PieChartConfigurationPanel.js +3 -2
  27. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts +6 -1
  28. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts.map +1 -1
  29. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.js +2 -2
  30. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts +3 -0
  31. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts.map +1 -1
  32. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.js +14 -2
  33. package/esm/internal/constants/dropdowns.d.ts +1 -0
  34. package/esm/internal/constants/dropdowns.d.ts.map +1 -1
  35. package/esm/internal/constants/dropdowns.js +20 -2
  36. package/esm/internal/constants/supportedProperties.d.ts.map +1 -1
  37. package/esm/internal/constants/supportedProperties.js +1 -0
  38. package/esm/internal/translations/en-US.localization-bundle.d.ts +73 -1
  39. package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
  40. package/esm/internal/translations/en-US.localization-bundle.js +76 -4
  41. package/esm/internal/utils/translations.d.ts.map +1 -1
  42. package/esm/internal/utils/translations.js +2 -1
  43. package/esm/locales.d.ts +36 -1
  44. package/esm/locales.d.ts.map +1 -1
  45. package/esm/locales.js +22 -1
  46. package/esm/tsdoc-metadata.json +1 -1
  47. package/package.json +21 -21
  48. package/styles/css/main.css +4 -0
  49. package/styles/css/main.css.map +1 -1
  50. package/styles/internal/css/conditional_formatting.css +4 -0
  51. package/styles/internal/css/conditional_formatting.css.map +1 -1
  52. 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) }), option ? null : _jsx("span", { className: "gd-cf-rule__invalid", children: labels.invalid })] }), _jsx("span", { className: "gd-cf-rule__delete", children: _jsx(UiIconButton, { icon: "trash", size: "small", variant: "tertiary", isDesctructive: true, label: labels.delete, onClick: onDelete }) })
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
- const canAddRule = !isLoading && targetOptions.length > 0;
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].target), isNew: true }) })
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) => (_jsx(RuleChip, { rule: rule, option: findTargetOption(targetOptions, rule.target), labels: chipLabels, slot: slot, onEdit: () => setDialog({ rule, isNew: false }), onDelete: () => deleteRule(rule.id) })) }) })), dialog ? (_jsx(ConditionalFormattingDialog, { rule: dialog.rule, isNew: dialog.isNew, targetOptions: targetOptions, separators: separators, alignTo: ".s-cf-popover-anchor", onSave: (rule) => saveRule(rule, dialog.isNew), onClose: () => setDialog(null) }, dialog.rule.id)) : null] }));
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 operatorsForKind: (kind: "attribute" | "measure") => ConditionalFormattingOperator[];
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
- export declare const newCondition: () => IConditionalFormattingCondition;
47
- export declare const newRule: (target: ConditionalFormattingTarget) => IConditionalFormattingRule;
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 invalidates the operators, so conditions
50
- * reset; crossing the percent boundary within measures changes the value units out from under the
51
- * user, so operators stay but values clear (Save disables until re-entered).
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
- * A non-numeric measure literal can only come from stored data (the number input cannot produce one)
70
- * and would render as an invisible value; coerce it to the empty sentinel the UI can actually show —
71
- * the disabled Save button then covers it like any other missing value.
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,EAa1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAe,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EACH,KAAK,6BAA6B,EAClC,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,+BAA+B,EACpC,KAAK,0BAA0B,EAClC,MAAM,6BAA6B,CAAC;AAIrC,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC1B,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,2BAA2B,CAAC;IACpC,sGAAsG;IACtG,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gHAA8G;IAC9G,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAUD,wGAAwG;AACxG,MAAM,WAAW,aAAa;IAC1B,6GAA6G;IAC7G,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,wFAAsF;IACtF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACvC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,cAAc,GAAG,QAAQ,CAAC,aAAa,CAAC,CAcnF;AAiCD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,EAAE,IAAI,GAAE,aAAkB,GAAG,aAAa,EAAE,CAkCzG;AAED,eAAO,MAAM,gBAAgB,8FAGmE,CAAC;AAEjG,eAAO,MAAM,aAAa,iDAC2D,CAAC;AAwCtF,eAAO,MAAM,gBAAgB,oEAEuE,CAAC;AAgBrG,eAAO,MAAM,YAAY,iEAGxB,CAAC;AAIF,eAAO,MAAM,UAAU,2CAC0C,CAAC;AAElE,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAExD,eAAO,MAAM,aAAa,4DAKzB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;AAKrF;;;GAGG;AACH,eAAO,MAAM,eAAe,8HAgB3B,CAAC;AAIF,+DAA+D;AAC/D,eAAO,MAAM,qBAAqB,yEAcjC,CAAC;AAEF,eAAO,MAAM,YAAY,uCAMvB,CAAC;AAEH,eAAO,MAAM,OAAO,qEAIlB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc,4HAmB1B,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,eAAe,yCAKN,CAAC;AAKvB,gFAA+E;AAC/E,eAAO,MAAM,kBAAkB,2CACO,CAAC;AAEvC,uFAAsF;AACtF,eAAO,MAAM,kBAAkB,+CACe,CAAC;AAI/C,MAAM,MAAM,+BAA+B,GAAG,YAAY,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC7B,KAAK,CAAC,EAAE,+BAA+B,CAAC;CAC3C;AAID;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,kEAclC,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACjC,4EAA0E;IAC1E,OAAO,EAAE,OAAO,CAAC;IACjB,yFAAuF;IACvF,MAAM,EAAE,gBAAgB,CAAC;CAC5B;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,qGA0B7B,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,cAAc,+CAKrB,CAAC"}
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
- titles[attribute.attributeHeader.localIdentifier] = attribute.attributeHeader.formOf.name;
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
- export const operatorsForKind = (kind) => (kind === "measure" ? MEASURE_OPERATORS : ATTRIBUTE_OPERATORS);
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) from measure (metric) targets — same icons AD uses
150
- // elsewhere for catalog/bucket items.
151
- export const targetIcon = (kind) => kind === "attribute" ? "gd-icon-attribute" : "gd-icon-metric";
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
- return { kind: "literal", value: "" };
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
- export const newCondition = () => ({
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: "ALL",
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 = (target) => ({
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 invalidates the operators, so conditions
205
- * reset; crossing the percent boundary within measures changes the value units out from under the
206
- * user, so operators stay but values clear (Save disables until re-entered).
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
- if (next.target.kind !== rule.target.kind) {
210
- return { ...rule, target: next.target, conditions: [newCondition()] };
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
- if ((next.isPercent ?? false) !== (previous?.isPercent ?? false)) {
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
- * A non-numeric measure literal can only come from stored data (the number input cannot produce one)
244
- * and would render as an invisible value; coerce it to the empty sentinel the UI can actually show —
245
- * the disabled Save button then covers it like any other missing value.
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,CAoC9C;IAED,OAAO,CAAC,mBAAmB;CAK9B"}
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() {