@juicemantics/veloiq-ui 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -200,6 +200,10 @@ interface ModelDef {
200
200
  description?: string;
201
201
  pkField?: string;
202
202
  listViewType?: "table" | "gallery" | "calendar" | "totals-details";
203
+ /** Field keys whose values compose this model's record title (space-joined).
204
+ * Configured via `veloiq set-title` and stored on the model's
205
+ * `__veloiq_ui__["titleFields"]`; mirrors the backend `dc_title()`/`__str__`. */
206
+ titleFields?: string[];
203
207
  /** True when this ModelDef represents a NamedQuery rather than a plain model table. */
204
208
  isNamedQuery?: boolean;
205
209
  /** Resource name of the primary model (for show/edit navigation and write routing). */
@@ -314,6 +318,7 @@ declare const DynamicShow: React__default.FC<{
314
318
  allModels?: ModelDef[];
315
319
  idOverride?: string;
316
320
  embedded?: boolean;
321
+ beforeTabs?: React__default.ReactNode;
317
322
  }>;
318
323
 
319
324
  interface JourneyCallbacks$1 {
@@ -489,7 +494,7 @@ declare const LoginPage: React__default.FC<LoginPageProps>;
489
494
 
490
495
  declare const DashboardPage: React__default.FC;
491
496
 
492
- type CellSourceType = "model" | "named_query" | "field" | "relation" | "custom";
497
+ type CellSourceType = "model" | "named_query" | "field" | "relation" | "custom" | "plotly_chart";
493
498
  interface DashboardCell {
494
499
  id: string;
495
500
  model: string;
@@ -504,6 +509,8 @@ interface DashboardCell {
504
509
  max_height: string | null;
505
510
  section_name?: string;
506
511
  section_id?: string;
512
+ chart_url?: string;
513
+ chart_title?: string;
507
514
  }
508
515
  interface DashboardTab {
509
516
  id: string;
@@ -618,6 +625,13 @@ declare function generateResources(models: ModelDef[], moduleName: string, optio
618
625
  */
619
626
  declare const authenticatedFetch: (url: string, options?: RequestInit) => Promise<Response>;
620
627
 
628
+ declare function useAuthenticatedFileUrl(rawUrl: string): string;
629
+ declare const AuthenticatedImage: React__default.FC<{
630
+ url: string;
631
+ alt?: string;
632
+ style?: React__default.CSSProperties;
633
+ }>;
634
+
621
635
  type ModelTone = {
622
636
  solid: string;
623
637
  soft: string;
@@ -640,4 +654,4 @@ declare const getModelTone: (modelLike?: string | {
640
654
 
641
655
  declare const authSystemModels: ModelDef[];
642
656
 
643
- export { API_URL, AllModelsProvider, type BulkActionDef, type CellSourceType, ColorModeContext, ColorModeContextProvider, CommandCenterPortal, type CommandCenterPortalProps, CustomSider, type DashboardCell, type DashboardConfig, DashboardPage, type DashboardTab, DynamicCreate, DynamicEdit, DynamicList, DynamicShow, ExecutableHtml, type FieldDef, GlobalSearch, HierarchyView, HorizontalMenu, InlinePlotlyHtml, LayoutWrapper, type LayoutWrapperProps, LoginPage, type LoginPageProps, type MillerLeafConfig, type ModelDef, ModelHeading, type ModelSearchResult, MultiPaneLayout, type NavConfig, NavConfigContext, type NavConfigEntry, PaneNavigationContext, PinnedRecordsPanel, PrimaryShowContext, type PrimaryShowRendererProps, type RecentActivityData, type RecentActivityGroup, RecentActivityPanel, type RecentRecord, type RecordResult, ReferenceField, type RelationDef, ResourceContext, type ResourceDef, SectionsGrid, ShowFooterButtons, StandardList, StandardShow, type UseRecordSearchReturn, type ViewConfigRow, ViewsGrid, accessControlProvider, authProvider, authSystemModels, authenticatedFetch, buildShowTabFormOptions, generateResources, getModelTone, getNavEntry, guessIcon, httpClient, normalizeToneKey, renderRelationBlock, resolveIcon, setColorSchemas, sortItemsByNavConfig, useAllModels, useKeyboardShortcuts, useMetadataModal, useNavConfig, useNavModules, usePaneNavigation, useRecordSearch, useShowActionsPreferences, useShowEditableForm, useStandardShowTabs };
657
+ export { API_URL, AllModelsProvider, AuthenticatedImage, type BulkActionDef, type CellSourceType, ColorModeContext, ColorModeContextProvider, CommandCenterPortal, type CommandCenterPortalProps, CustomSider, type DashboardCell, type DashboardConfig, DashboardPage, type DashboardTab, DynamicCreate, DynamicEdit, DynamicList, DynamicShow, ExecutableHtml, type FieldDef, GlobalSearch, HierarchyView, HorizontalMenu, InlinePlotlyHtml, LayoutWrapper, type LayoutWrapperProps, LoginPage, type LoginPageProps, type MillerLeafConfig, type ModelDef, ModelHeading, type ModelSearchResult, MultiPaneLayout, type NavConfig, NavConfigContext, type NavConfigEntry, PaneNavigationContext, PinnedRecordsPanel, PrimaryShowContext, type PrimaryShowRendererProps, type RecentActivityData, type RecentActivityGroup, RecentActivityPanel, type RecentRecord, type RecordResult, ReferenceField, type RelationDef, ResourceContext, type ResourceDef, SectionsGrid, ShowFooterButtons, StandardList, StandardShow, type UseRecordSearchReturn, type ViewConfigRow, ViewsGrid, accessControlProvider, authProvider, authSystemModels, authenticatedFetch, buildShowTabFormOptions, generateResources, getModelTone, getNavEntry, guessIcon, httpClient, normalizeToneKey, renderRelationBlock, resolveIcon, setColorSchemas, sortItemsByNavConfig, useAllModels, useAuthenticatedFileUrl, useKeyboardShortcuts, useMetadataModal, useNavConfig, useNavModules, usePaneNavigation, useRecordSearch, useShowActionsPreferences, useShowEditableForm, useStandardShowTabs };
package/dist/index.d.ts CHANGED
@@ -200,6 +200,10 @@ interface ModelDef {
200
200
  description?: string;
201
201
  pkField?: string;
202
202
  listViewType?: "table" | "gallery" | "calendar" | "totals-details";
203
+ /** Field keys whose values compose this model's record title (space-joined).
204
+ * Configured via `veloiq set-title` and stored on the model's
205
+ * `__veloiq_ui__["titleFields"]`; mirrors the backend `dc_title()`/`__str__`. */
206
+ titleFields?: string[];
203
207
  /** True when this ModelDef represents a NamedQuery rather than a plain model table. */
204
208
  isNamedQuery?: boolean;
205
209
  /** Resource name of the primary model (for show/edit navigation and write routing). */
@@ -314,6 +318,7 @@ declare const DynamicShow: React__default.FC<{
314
318
  allModels?: ModelDef[];
315
319
  idOverride?: string;
316
320
  embedded?: boolean;
321
+ beforeTabs?: React__default.ReactNode;
317
322
  }>;
318
323
 
319
324
  interface JourneyCallbacks$1 {
@@ -489,7 +494,7 @@ declare const LoginPage: React__default.FC<LoginPageProps>;
489
494
 
490
495
  declare const DashboardPage: React__default.FC;
491
496
 
492
- type CellSourceType = "model" | "named_query" | "field" | "relation" | "custom";
497
+ type CellSourceType = "model" | "named_query" | "field" | "relation" | "custom" | "plotly_chart";
493
498
  interface DashboardCell {
494
499
  id: string;
495
500
  model: string;
@@ -504,6 +509,8 @@ interface DashboardCell {
504
509
  max_height: string | null;
505
510
  section_name?: string;
506
511
  section_id?: string;
512
+ chart_url?: string;
513
+ chart_title?: string;
507
514
  }
508
515
  interface DashboardTab {
509
516
  id: string;
@@ -618,6 +625,13 @@ declare function generateResources(models: ModelDef[], moduleName: string, optio
618
625
  */
619
626
  declare const authenticatedFetch: (url: string, options?: RequestInit) => Promise<Response>;
620
627
 
628
+ declare function useAuthenticatedFileUrl(rawUrl: string): string;
629
+ declare const AuthenticatedImage: React__default.FC<{
630
+ url: string;
631
+ alt?: string;
632
+ style?: React__default.CSSProperties;
633
+ }>;
634
+
621
635
  type ModelTone = {
622
636
  solid: string;
623
637
  soft: string;
@@ -640,4 +654,4 @@ declare const getModelTone: (modelLike?: string | {
640
654
 
641
655
  declare const authSystemModels: ModelDef[];
642
656
 
643
- export { API_URL, AllModelsProvider, type BulkActionDef, type CellSourceType, ColorModeContext, ColorModeContextProvider, CommandCenterPortal, type CommandCenterPortalProps, CustomSider, type DashboardCell, type DashboardConfig, DashboardPage, type DashboardTab, DynamicCreate, DynamicEdit, DynamicList, DynamicShow, ExecutableHtml, type FieldDef, GlobalSearch, HierarchyView, HorizontalMenu, InlinePlotlyHtml, LayoutWrapper, type LayoutWrapperProps, LoginPage, type LoginPageProps, type MillerLeafConfig, type ModelDef, ModelHeading, type ModelSearchResult, MultiPaneLayout, type NavConfig, NavConfigContext, type NavConfigEntry, PaneNavigationContext, PinnedRecordsPanel, PrimaryShowContext, type PrimaryShowRendererProps, type RecentActivityData, type RecentActivityGroup, RecentActivityPanel, type RecentRecord, type RecordResult, ReferenceField, type RelationDef, ResourceContext, type ResourceDef, SectionsGrid, ShowFooterButtons, StandardList, StandardShow, type UseRecordSearchReturn, type ViewConfigRow, ViewsGrid, accessControlProvider, authProvider, authSystemModels, authenticatedFetch, buildShowTabFormOptions, generateResources, getModelTone, getNavEntry, guessIcon, httpClient, normalizeToneKey, renderRelationBlock, resolveIcon, setColorSchemas, sortItemsByNavConfig, useAllModels, useKeyboardShortcuts, useMetadataModal, useNavConfig, useNavModules, usePaneNavigation, useRecordSearch, useShowActionsPreferences, useShowEditableForm, useStandardShowTabs };
657
+ export { API_URL, AllModelsProvider, AuthenticatedImage, type BulkActionDef, type CellSourceType, ColorModeContext, ColorModeContextProvider, CommandCenterPortal, type CommandCenterPortalProps, CustomSider, type DashboardCell, type DashboardConfig, DashboardPage, type DashboardTab, DynamicCreate, DynamicEdit, DynamicList, DynamicShow, ExecutableHtml, type FieldDef, GlobalSearch, HierarchyView, HorizontalMenu, InlinePlotlyHtml, LayoutWrapper, type LayoutWrapperProps, LoginPage, type LoginPageProps, type MillerLeafConfig, type ModelDef, ModelHeading, type ModelSearchResult, MultiPaneLayout, type NavConfig, NavConfigContext, type NavConfigEntry, PaneNavigationContext, PinnedRecordsPanel, PrimaryShowContext, type PrimaryShowRendererProps, type RecentActivityData, type RecentActivityGroup, RecentActivityPanel, type RecentRecord, type RecordResult, ReferenceField, type RelationDef, ResourceContext, type ResourceDef, SectionsGrid, ShowFooterButtons, StandardList, StandardShow, type UseRecordSearchReturn, type ViewConfigRow, ViewsGrid, accessControlProvider, authProvider, authSystemModels, authenticatedFetch, buildShowTabFormOptions, generateResources, getModelTone, getNavEntry, guessIcon, httpClient, normalizeToneKey, renderRelationBlock, resolveIcon, setColorSchemas, sortItemsByNavConfig, useAllModels, useAuthenticatedFileUrl, useKeyboardShortcuts, useMetadataModal, useNavConfig, useNavModules, usePaneNavigation, useRecordSearch, useShowActionsPreferences, useShowEditableForm, useStandardShowTabs };
package/dist/index.js CHANGED
@@ -5107,6 +5107,39 @@ var getSortPriority = (columnSort, fieldKey) => {
5107
5107
  const index = columnSort.findIndex((item) => item.fieldKey === fieldKey);
5108
5108
  return index === -1 ? 1 : columnSort.length - index + 1;
5109
5109
  };
5110
+ var _TOKEN_KEY = "jm_access_token";
5111
+ function useAuthenticatedFileUrl(rawUrl) {
5112
+ const [src, setSrc] = React5.useState("");
5113
+ React5.useEffect(() => {
5114
+ if (!rawUrl) {
5115
+ setSrc("");
5116
+ return;
5117
+ }
5118
+ if (!rawUrl.includes("/api/file/")) {
5119
+ setSrc(rawUrl);
5120
+ return;
5121
+ }
5122
+ const token = localStorage.getItem(_TOKEN_KEY) || "";
5123
+ const controller = new AbortController();
5124
+ let objectUrl = "";
5125
+ fetch(rawUrl, {
5126
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
5127
+ signal: controller.signal
5128
+ }).then((r) => r.ok ? r.blob() : Promise.reject()).then((blob) => {
5129
+ objectUrl = URL.createObjectURL(blob);
5130
+ setSrc(objectUrl);
5131
+ }).catch(() => setSrc(""));
5132
+ return () => {
5133
+ controller.abort();
5134
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
5135
+ };
5136
+ }, [rawUrl]);
5137
+ return src;
5138
+ }
5139
+ var AuthenticatedImage = ({ url, alt, style }) => {
5140
+ const src = useAuthenticatedFileUrl(url);
5141
+ return src ? /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: alt ?? "", style }) : null;
5142
+ };
5110
5143
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "tif", "tiff"]);
5111
5144
  var isImageRecord = (record) => {
5112
5145
  if (record?.avatar_url || record?.image_url || record?.photo_url) return true;
@@ -5164,7 +5197,7 @@ var renderSharedGalleryCard = ({
5164
5197
  style: { width: imageWidth, display: "grid", gap: 6, cursor: onClick ? "pointer" : "default" },
5165
5198
  onClick,
5166
5199
  children: [
5167
- contentUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: contentUrl, alt: label, style: imageStyle }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...imageStyle, display: "flex", alignItems: "center", justifyContent: "center", color: "#8c8c8c" }, children: /* @__PURE__ */ jsxRuntime.jsx(AntDIcons2.FileTextOutlined, { style: { fontSize: 24 } }) }),
5200
+ contentUrl ? /* @__PURE__ */ jsxRuntime.jsx(AuthenticatedImage, { url: contentUrl, alt: label, style: imageStyle }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...imageStyle, display: "flex", alignItems: "center", justifyContent: "center", color: "#8c8c8c" }, children: /* @__PURE__ */ jsxRuntime.jsx(AntDIcons2.FileTextOutlined, { style: { fontSize: 24 } }) }),
5168
5201
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 12, color: textColor, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: label })
5169
5202
  ]
5170
5203
  },
@@ -6109,6 +6142,8 @@ var AnalysisChart = ({
6109
6142
  };
6110
6143
  const primarySeriesKey = seriesKeys[0] || "__count__";
6111
6144
  const secondarySeriesKey = seriesKeys[1];
6145
+ const resolveNumericField = (fields, n) => fields[Math.min(n, fields.length - 1)] ?? { key: "__count__", label: _10("Count") };
6146
+ const resolveCategoryField = (field1, field2) => field2 ?? field1;
6112
6147
  const getNumericValue = (record, key) => {
6113
6148
  if (key === "__count__") return 1;
6114
6149
  const value = Number(record?.[key]);
@@ -6525,11 +6560,11 @@ var AnalysisChart = ({
6525
6560
  ] });
6526
6561
  };
6527
6562
  const renderScatter = (isBubble) => {
6528
- if (numericFields.length < 2) {
6529
- return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "Scatter needs at least two numeric fields." });
6563
+ if (numericFields.length === 0) {
6564
+ return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "Scatter needs at least one numeric field." });
6530
6565
  }
6531
- const xField = numericFields[0];
6532
- const yField = numericFields[1];
6566
+ const xField = resolveNumericField(numericFields, 0);
6567
+ const yField = resolveNumericField(numericFields, 1);
6533
6568
  const points = rawRows.map((row) => {
6534
6569
  const x = getNumericValue(row, xField.key);
6535
6570
  const y = getNumericValue(row, yField.key);
@@ -6807,11 +6842,12 @@ var AnalysisChart = ({
6807
6842
  ] });
6808
6843
  };
6809
6844
  const renderHeatmap = () => {
6810
- if (!categoryField1 || !categoryField2) {
6811
- return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "Heatmap needs two category fields." });
6845
+ if (!categoryField1) {
6846
+ return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "Heatmap needs a category field." });
6812
6847
  }
6848
+ const effectiveCat2 = resolveCategoryField(categoryField1, categoryField2);
6813
6849
  const cat1Field = modelField(categoryField1);
6814
- const cat2Field = modelField(categoryField2);
6850
+ const cat2Field = modelField(effectiveCat2);
6815
6851
  const rowLabels = [];
6816
6852
  const colLabels = [];
6817
6853
  const grid = /* @__PURE__ */ new Map();
@@ -6889,43 +6925,45 @@ var AnalysisChart = ({
6889
6925
  ] });
6890
6926
  };
6891
6927
  const renderCrosstab = () => {
6892
- if (!categoryField1 || !categoryField2) {
6893
- return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "Crosstab needs two category fields." });
6928
+ if (!categoryField1) {
6929
+ return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "Crosstab needs a category field." });
6894
6930
  }
6931
+ const effectiveCat2 = resolveCategoryField(categoryField1, categoryField2);
6895
6932
  const cat1Field = modelField(categoryField1);
6896
- const cat2Field = modelField(categoryField2);
6933
+ const cat2Field = modelField(effectiveCat2);
6897
6934
  return /* @__PURE__ */ jsxRuntime.jsx(
6898
6935
  CrosstabTable,
6899
6936
  {
6900
6937
  rows: rawRows,
6901
6938
  rowField: categoryField1,
6902
- colField: categoryField2,
6939
+ colField: effectiveCat2 ?? categoryField1,
6903
6940
  cellFieldKeys: seriesKeys,
6904
6941
  cellFieldLabels: seriesLabels,
6905
6942
  allFields,
6906
6943
  summaryFn,
6907
6944
  formatCategoryValue,
6908
6945
  numericBarColor,
6909
- caption: `${_10("Crosstab")}: ${cat1Field?.label || categoryField1} \xD7 ${cat2Field?.label || categoryField2} (${summaryFn})`
6946
+ caption: `${_10("Crosstab")}: ${cat1Field?.label || categoryField1} \xD7 ${cat2Field?.label || effectiveCat2} (${summaryFn})`
6910
6947
  }
6911
6948
  );
6912
6949
  };
6913
6950
  const renderRadar = () => {
6914
- if (seriesKeys.length < 3) {
6915
- return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "Radar needs at least three series." });
6951
+ if (seriesKeys.length === 0) {
6952
+ return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "Radar needs at least one series." });
6916
6953
  }
6954
+ const effectiveSeriesKeys = seriesKeys.length >= 3 ? seriesKeys : Array.from({ length: 3 }, (_43, i) => seriesKeys[i % seriesKeys.length]);
6917
6955
  const centerX = paddingLeft + chartWidth / 2;
6918
6956
  const centerY = paddingTop + chartHeight / 2;
6919
6957
  const radius = Math.min(chartWidth, chartHeight) * 0.35;
6920
- const maxBySeries = seriesKeys.reduce((acc, key) => {
6958
+ const maxBySeries = effectiveSeriesKeys.reduce((acc, key) => {
6921
6959
  acc[key] = Math.max(...data.map((group) => group.values[key] || 0), 1);
6922
6960
  return acc;
6923
6961
  }, {});
6924
- const angleStep = Math.PI * 2 / seriesKeys.length;
6962
+ const angleStep = Math.PI * 2 / effectiveSeriesKeys.length;
6925
6963
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, className: "chart-plot", viewBox: `0 0 ${width} ${height}`, width: "100%", height, role: "img", children: [
6926
6964
  renderTitle(),
6927
6965
  renderCaption("Radar chart"),
6928
- seriesKeys.map((seriesKey, index) => {
6966
+ effectiveSeriesKeys.map((seriesKey, index) => {
6929
6967
  const angle = -Math.PI / 2 + index * angleStep;
6930
6968
  const x = centerX + radius * Math.cos(angle);
6931
6969
  const y = centerY + radius * Math.sin(angle);
@@ -6935,7 +6973,7 @@ var AnalysisChart = ({
6935
6973
  ] }, `radar-axis-${seriesKey}`);
6936
6974
  }),
6937
6975
  data.map((group, groupIndex) => {
6938
- const points = seriesKeys.map((seriesKey, index) => {
6976
+ const points = effectiveSeriesKeys.map((seriesKey, index) => {
6939
6977
  const value = group.values[seriesKey] || 0;
6940
6978
  const ratio = value / Math.max(1, maxBySeries[seriesKey]);
6941
6979
  const angle = -Math.PI / 2 + index * angleStep;
@@ -6960,13 +6998,74 @@ var AnalysisChart = ({
6960
6998
  })
6961
6999
  ] });
6962
7000
  };
6963
- const renderCombo = () => {
6964
- if (!secondarySeriesKey) {
6965
- return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "Combo needs at least two series selected." });
7001
+ const render3D = () => {
7002
+ if (numericFields.length === 0) {
7003
+ return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "3D scatter needs at least one numeric field." });
6966
7004
  }
7005
+ const xField = resolveNumericField(numericFields, 0);
7006
+ const yField = resolveNumericField(numericFields, 1);
7007
+ const zField = resolveNumericField(numericFields, 2);
7008
+ const points = rawRows.map((row) => {
7009
+ const x = getNumericValue(row, xField.key);
7010
+ const y = getNumericValue(row, yField.key);
7011
+ const z = getNumericValue(row, zField.key);
7012
+ if (x === null || y === null || z === null) return null;
7013
+ return { x, y, z };
7014
+ }).filter((p) => !!p);
7015
+ if (points.length === 0) return renderNoChartDataMessage();
7016
+ const xs = points.map((p) => p.x);
7017
+ const ys = points.map((p) => p.y);
7018
+ const zs = points.map((p) => p.z);
7019
+ const xMin = Math.min(...xs), xMax = Math.max(...xs);
7020
+ const yMin = Math.min(...ys), yMax = Math.max(...ys);
7021
+ const zMin = Math.min(...zs), zMax = Math.max(...zs);
7022
+ const norm = (v, lo, hi) => hi === lo ? 0.5 : (v - lo) / (hi - lo);
7023
+ const isoScale = Math.min(chartWidth, chartHeight) * 0.38;
7024
+ const cx = paddingLeft + chartWidth * 0.5;
7025
+ const cy = paddingTop + chartHeight * 0.55;
7026
+ const cos30 = Math.cos(Math.PI / 6);
7027
+ const sin30 = Math.sin(Math.PI / 6);
7028
+ const project = (nx, ny, nz) => ({
7029
+ sx: cx + (nx - nz) * cos30 * isoScale,
7030
+ sy: cy - ny * isoScale + (nx + nz) * sin30 * isoScale
7031
+ });
7032
+ const axisEnd = (nx, ny, nz) => project(nx, ny, nz);
7033
+ const origin = project(0, 0, 0);
7034
+ const xTip = axisEnd(1, 0, 0);
7035
+ const yTip = axisEnd(0, 1, 0);
7036
+ const zTip = axisEnd(0, 0, 1);
7037
+ const projected = points.map(
7038
+ (p) => project(norm(p.x, xMin, xMax), norm(p.y, yMin, yMax), norm(p.z, zMin, zMax))
7039
+ );
7040
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, className: "chart-plot", viewBox: `0 0 ${width} ${height}`, width: "100%", height, role: "img", children: [
7041
+ renderTitle(),
7042
+ renderCaption(`3D: ${xField.label} \xD7 ${yField.label} \xD7 ${zField.label}`),
7043
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: origin.sx, y1: origin.sy, x2: xTip.sx, y2: xTip.sy, stroke: colors[0], strokeWidth: 1.5, opacity: 0.6 }),
7044
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: origin.sx, y1: origin.sy, x2: yTip.sx, y2: yTip.sy, stroke: colors[1], strokeWidth: 1.5, opacity: 0.6 }),
7045
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: origin.sx, y1: origin.sy, x2: zTip.sx, y2: zTip.sy, stroke: colors[2], strokeWidth: 1.5, opacity: 0.6 }),
7046
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: xTip.sx + 4, y: xTip.sy + 4, fontSize: "11", fill: colors[0], children: xField.label }),
7047
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: yTip.sx + 4, y: yTip.sy, fontSize: "11", fill: colors[1], children: yField.label }),
7048
+ /* @__PURE__ */ jsxRuntime.jsx("text", { x: zTip.sx + 4, y: zTip.sy + 4, fontSize: "11", fill: colors[2], children: zField.label }),
7049
+ projected.map((p, i) => /* @__PURE__ */ jsxRuntime.jsx(
7050
+ "circle",
7051
+ {
7052
+ className: "chart-item chart-point",
7053
+ style: { "--delay": `${i * 8}ms` },
7054
+ cx: p.sx,
7055
+ cy: p.sy,
7056
+ r: 4,
7057
+ fill: colors[0],
7058
+ opacity: 0.7
7059
+ },
7060
+ `3d-${i}`
7061
+ ))
7062
+ ] });
7063
+ };
7064
+ const renderCombo = () => {
7065
+ const effectiveSecondaryKey = secondarySeriesKey ?? primarySeriesKey;
6967
7066
  const valuesCombo = data.flatMap((group) => [
6968
7067
  group.values[primarySeriesKey] || 0,
6969
- group.values[secondarySeriesKey] || 0
7068
+ group.values[effectiveSecondaryKey] || 0
6970
7069
  ]);
6971
7070
  const maxCombo = Math.max(...valuesCombo, 1);
6972
7071
  const minCombo = Math.min(...valuesCombo, 0);
@@ -6975,7 +7074,7 @@ var AnalysisChart = ({
6975
7074
  const barWidth2 = groupWidth2 * 0.6;
6976
7075
  const points = data.map((group, index) => {
6977
7076
  const x = paddingLeft + index * groupWidth2 + groupWidth2 / 2;
6978
- const y = scaleYCombo(group.values[secondarySeriesKey] || 0);
7077
+ const y = scaleYCombo(group.values[effectiveSecondaryKey] || 0);
6979
7078
  return `${x},${y}`;
6980
7079
  }).join(" ");
6981
7080
  return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, className: "chart-plot", viewBox: `0 0 ${width} ${height}`, width: "100%", height, role: "img", children: [
@@ -6983,7 +7082,7 @@ var AnalysisChart = ({
6983
7082
  renderLegendItems(
6984
7083
  [
6985
7084
  { label: seriesLabels[primarySeriesKey] || primarySeriesKey, color: colors[0] },
6986
- { label: seriesLabels[secondarySeriesKey] || secondarySeriesKey, color: colors[2] }
7085
+ { label: seriesLabels[effectiveSecondaryKey] || effectiveSecondaryKey, color: colors[2] }
6987
7086
  ],
6988
7087
  8
6989
7088
  ),
@@ -7034,13 +7133,14 @@ var AnalysisChart = ({
7034
7133
  chartType === "histogram" && renderHistogram(),
7035
7134
  chartType === "scatter" && renderScatter(false),
7036
7135
  chartType === "bubble" && renderScatter(true),
7037
- chartType === "box" && renderBoxPlot(),
7136
+ (chartType === "box" || chartType === "boxplot") && renderBoxPlot(),
7038
7137
  chartType === "waterfall" && renderWaterfall(),
7039
7138
  chartType === "heatmap" && renderHeatmap(),
7040
7139
  chartType === "crosstab" && renderCrosstab(),
7041
7140
  chartType === "radar" && renderRadar(),
7042
7141
  chartType === "combo" && renderCombo(),
7043
- chartType !== "histogram" && chartType !== "scatter" && chartType !== "bubble" && chartType !== "box" && chartType !== "waterfall" && chartType !== "heatmap" && chartType !== "crosstab" && chartType !== "radar" && chartType !== "combo" && /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, className: "chart-plot", viewBox: `0 0 ${width} ${height}`, width: "100%", height, role: "img", children: [
7142
+ chartType === "3d" && render3D(),
7143
+ (chartType === "bar" || chartType === "line" || chartType === "area" || chartType === "stacked" || chartType === "bar-horizontal" || chartType === "stacked-horizontal" || chartType === "area-horizontal") && /* @__PURE__ */ jsxRuntime.jsxs("svg", { ref: svgRef, className: "chart-plot", viewBox: `0 0 ${width} ${height}`, width: "100%", height, role: "img", children: [
7044
7144
  renderTitle(),
7045
7145
  renderLegendItems(
7046
7146
  seriesKeys.map((seriesKey, index) => ({
@@ -7487,6 +7587,13 @@ body, table, th, td, input, button, select, textarea, div, span, p, li, ul, ol {
7487
7587
  if (syncHeightTimerRef.current) clearTimeout(syncHeightTimerRef.current);
7488
7588
  };
7489
7589
  }, []);
7590
+ const inlineHtml = React5.useMemo(
7591
+ () => (html || "").replace(
7592
+ /<script\b[^>]*\bsrc=["']?[^"'>]*cdn\.plot\.ly[^"'>]*["']?[^>]*><\/script>/gi,
7593
+ ""
7594
+ ),
7595
+ [html]
7596
+ );
7490
7597
  if (mode === "iframe") {
7491
7598
  return /* @__PURE__ */ jsxRuntime.jsx(
7492
7599
  "iframe",
@@ -7497,7 +7604,7 @@ body, table, th, td, input, button, select, textarea, div, span, p, li, ul, ol {
7497
7604
  }
7498
7605
  );
7499
7606
  }
7500
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: htmlRef, dangerouslySetInnerHTML: { __html: html || "" }, style });
7607
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: htmlRef, dangerouslySetInnerHTML: { __html: inlineHtml }, style });
7501
7608
  };
7502
7609
 
7503
7610
  // src/components/DynamicResource/relations/helpers.ts
@@ -8713,7 +8820,7 @@ function useRoleFilteredModel(model) {
8713
8820
  }, [model, userRoles]);
8714
8821
  }
8715
8822
  var _19 = window._ || ((text) => text);
8716
- var DynamicShow = ({ model: modelProp, allModels, idOverride, embedded }) => {
8823
+ var DynamicShow = ({ model: modelProp, allModels, idOverride, embedded, beforeTabs }) => {
8717
8824
  const model = useRoleFilteredModel(modelProp);
8718
8825
  applyI18nLabelsToModel(model);
8719
8826
  applyI18nLabelsToModels(allModels);
@@ -8775,6 +8882,7 @@ var DynamicShow = ({ model: modelProp, allModels, idOverride, embedded }) => {
8775
8882
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "jm-tone-scope", style: toneScopeStyle(modelTone), children: [
8776
8883
  /* @__PURE__ */ jsxRuntime.jsx(ToneSharedStyles, {}),
8777
8884
  !record ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", justifyContent: "center", padding: 32 }, children: /* @__PURE__ */ jsxRuntime.jsx(antd.Spin, {}) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
8885
+ beforeTabs,
8778
8886
  /* @__PURE__ */ jsxRuntime.jsx(antd.Tabs, { activeKey: activeTabKey, onChange: setActiveTabKey, items: lazyItems, destroyInactiveTabPane: true }),
8779
8887
  /* @__PURE__ */ jsxRuntime.jsx(
8780
8888
  ShowFooterButtons,
@@ -8802,6 +8910,7 @@ var DynamicShow = ({ model: modelProp, allModels, idOverride, embedded }) => {
8802
8910
  })),
8803
8911
  headerButtons,
8804
8912
  children: [
8913
+ beforeTabs,
8805
8914
  /* @__PURE__ */ jsxRuntime.jsx(antd.Tabs, { activeKey: activeTabKey, onChange: setActiveTabKey, items: lazyItems, destroyInactiveTabPane: true }),
8806
8915
  /* @__PURE__ */ jsxRuntime.jsx(
8807
8916
  ShowFooterButtons,
@@ -9779,7 +9888,9 @@ var CellConfigDrawer = ({ open, cell, tabId, config, onClose, onSave }) => {
9779
9888
  min_width: cell.min_width ?? "",
9780
9889
  max_width: cell.max_width ?? "",
9781
9890
  min_height: cell.min_height ?? "",
9782
- max_height: cell.max_height ?? ""
9891
+ max_height: cell.max_height ?? "",
9892
+ chart_url: cell.chart_url ?? "",
9893
+ chart_title: cell.chart_title ?? ""
9783
9894
  });
9784
9895
  }, [cell, tabId, config, form]);
9785
9896
  const handleSave = () => {
@@ -9795,7 +9906,9 @@ var CellConfigDrawer = ({ open, cell, tabId, config, onClose, onSave }) => {
9795
9906
  min_width: values.min_width || null,
9796
9907
  max_width: values.max_width || null,
9797
9908
  min_height: values.min_height || null,
9798
- max_height: values.max_height || null
9909
+ max_height: values.max_height || null,
9910
+ chart_url: values.chart_url || void 0,
9911
+ chart_title: values.chart_title || void 0
9799
9912
  };
9800
9913
  const currentTab = config.tabs.find((t) => t.id === tabId);
9801
9914
  const nameUnchanged = currentTab?.name.trim().toLowerCase() === newTabName.toLowerCase();
@@ -9842,7 +9955,7 @@ var CellConfigDrawer = ({ open, cell, tabId, config, onClose, onSave }) => {
9842
9955
  return /* @__PURE__ */ jsxRuntime.jsx(
9843
9956
  antd.Drawer,
9844
9957
  {
9845
- title: cell?.source_type !== "model" ? `Configure section: ${cell?.section_name ?? cell?.model ?? ""}` : `Configure cell: ${cell?.model ?? ""}`,
9958
+ title: cell?.source_type === "plotly_chart" ? `Configure chart: ${cell?.chart_title ?? cell?.model ?? ""}` : cell?.source_type !== "model" ? `Configure section: ${cell?.section_name ?? cell?.model ?? ""}` : `Configure cell: ${cell?.model ?? ""}`,
9846
9959
  placement: "right",
9847
9960
  width: 380,
9848
9961
  open,
@@ -9870,6 +9983,11 @@ var CellConfigDrawer = ({ open, cell, tabId, config, onClose, onSave }) => {
9870
9983
  /* @__PURE__ */ jsxRuntime.jsx(antd.Divider, { orientation: "left", children: "View" }),
9871
9984
  /* @__PURE__ */ jsxRuntime.jsx(antd.Form.Item, { name: "view_type", label: "View type", children: /* @__PURE__ */ jsxRuntime.jsx(antd.Select, { options: VIEW_TYPE_OPTIONS }) })
9872
9985
  ] }),
9986
+ cell?.source_type === "plotly_chart" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9987
+ /* @__PURE__ */ jsxRuntime.jsx(antd.Divider, { orientation: "left", children: "Chart" }),
9988
+ /* @__PURE__ */ jsxRuntime.jsx(antd.Form.Item, { name: "chart_title", label: "Chart title", children: /* @__PURE__ */ jsxRuntime.jsx(antd.Input, { placeholder: "e.g. Confidence by Month" }) }),
9989
+ /* @__PURE__ */ jsxRuntime.jsx(antd.Form.Item, { name: "chart_url", label: "Chart URL", children: /* @__PURE__ */ jsxRuntime.jsx(antd.Input, { placeholder: "/api/nl-answers-confidence-by-month-chart" }) })
9990
+ ] }),
9873
9991
  /* @__PURE__ */ jsxRuntime.jsx(antd.Divider, { orientation: "left", children: "Size" }),
9874
9992
  /* @__PURE__ */ jsxRuntime.jsxs(antd.Space, { wrap: true, children: [
9875
9993
  /* @__PURE__ */ jsxRuntime.jsx(antd.Form.Item, { name: "min_width", label: "Min width", style: { marginBottom: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(antd.Input, { placeholder: "e.g. 320px", style: { width: 130 } }) }),
@@ -10074,6 +10192,13 @@ var SectionsGrid = ({ cells, config, tabId, renderContent, onConfigChange, isCon
10074
10192
  if (!cells.length) return 1;
10075
10193
  return Math.max(...cells.map((c) => c.row)) + 1;
10076
10194
  }, [cells]);
10195
+ const soloRows = React5.useMemo(() => {
10196
+ const counts = /* @__PURE__ */ new Map();
10197
+ for (const c of cells) counts.set(c.row, (counts.get(c.row) ?? 0) + 1);
10198
+ const solo = /* @__PURE__ */ new Set();
10199
+ for (const [row, count] of counts) if (count === 1) solo.add(row);
10200
+ return solo;
10201
+ }, [cells]);
10077
10202
  const visibleCells = maximizedCellId ? cells.filter((c) => c.id === maximizedCellId) : cells;
10078
10203
  const gridStyle = {
10079
10204
  display: "grid",
@@ -10092,7 +10217,7 @@ var SectionsGrid = ({ cells, config, tabId, renderContent, onConfigChange, isCon
10092
10217
  "div",
10093
10218
  {
10094
10219
  style: {
10095
- gridColumn: maximizedCellId ? "1 / -1" : `${cell.col + 1}`,
10220
+ gridColumn: maximizedCellId || soloRows.has(cell.row) ? "1 / -1" : `${cell.col + 1}`,
10096
10221
  gridRow: maximizedCellId ? "1 / -1" : `${cell.row + 1}`
10097
10222
  },
10098
10223
  children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -15413,7 +15538,8 @@ var RelatedObjectsTable = ({ rel, record, relatedModel, parentModel, showActions
15413
15538
  { label: _34("Heatmap"), value: "heatmap" },
15414
15539
  { label: _34("Crosstab"), value: "crosstab" },
15415
15540
  { label: _34("Radar"), value: "radar" },
15416
- { label: _34("Combo (Bar + Line)"), value: "combo" }
15541
+ { label: _34("Combo (Bar + Line)"), value: "combo" },
15542
+ { label: _34("3D Scatter"), value: "3d" }
15417
15543
  ]
15418
15544
  }
15419
15545
  )
@@ -16688,7 +16814,7 @@ var DynamicList = ({ model: modelProp, allModels, filter, relationConfig, isEmbe
16688
16814
  if (["true", "1", "t", "yes", "y"].includes(normalized)) value = true;
16689
16815
  if (["false", "0", "f", "no", "n"].includes(normalized)) value = false;
16690
16816
  }
16691
- return [{ field: searchField.key, operator: "eq", value }];
16817
+ return [{ field: searchField.key, operator: "contains", value }];
16692
16818
  }
16693
16819
  });
16694
16820
  const [allRowsData, setAllRowsData] = React5.useState([]);
@@ -19944,7 +20070,8 @@ var DynamicList = ({ model: modelProp, allModels, filter, relationConfig, isEmbe
19944
20070
  { label: _36("Heatmap"), value: "heatmap" },
19945
20071
  { label: _36("Crosstab"), value: "crosstab" },
19946
20072
  { label: _36("Radar"), value: "radar" },
19947
- { label: _36("Combo (Bar + Line)"), value: "combo" }
20073
+ { label: _36("Combo (Bar + Line)"), value: "combo" },
20074
+ { label: _36("3D Scatter"), value: "3d" }
19948
20075
  ]
19949
20076
  }
19950
20077
  )
@@ -21104,6 +21231,48 @@ function useDashboardConfig() {
21104
21231
  }, [apiUrl]);
21105
21232
  return { config, enabled, loading, save, reload: load };
21106
21233
  }
21234
+ var PlotlyChartContent = ({ chartUrl, refreshNonce }) => {
21235
+ const [chartHtml, setChartHtml] = React5.useState("");
21236
+ const [loading, setLoading] = React5.useState(true);
21237
+ const [error, setError] = React5.useState("");
21238
+ const fetchChart = React5.useCallback(async () => {
21239
+ setLoading(true);
21240
+ setError("");
21241
+ try {
21242
+ const apiUrl = typeof API_URL3 === "string" ? API_URL3 : "";
21243
+ const fullUrl = chartUrl.startsWith("http") ? chartUrl : `${apiUrl}${chartUrl}`;
21244
+ const sep = fullUrl.includes("?") ? "&" : "?";
21245
+ const lang = (() => {
21246
+ try {
21247
+ return (localStorage.getItem("locale") || navigator.language || "en").split("-")[0].toLowerCase();
21248
+ } catch {
21249
+ return "en";
21250
+ }
21251
+ })();
21252
+ const res = await authenticatedFetch(`${fullUrl}${sep}lang=${encodeURIComponent(lang)}`);
21253
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
21254
+ const data = await res.json();
21255
+ setChartHtml(data.chart_html || "");
21256
+ } catch (e) {
21257
+ setError(e?.message ?? String(e));
21258
+ } finally {
21259
+ setLoading(false);
21260
+ }
21261
+ }, [chartUrl]);
21262
+ React5.useEffect(() => {
21263
+ fetchChart();
21264
+ }, [fetchChart, refreshNonce]);
21265
+ if (loading) {
21266
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", justifyContent: "center", alignItems: "center", height: "100%", minHeight: 200 }, children: /* @__PURE__ */ jsxRuntime.jsx(antd.Spin, {}) });
21267
+ }
21268
+ if (error) {
21269
+ return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: `Chart error: ${error}`, style: { padding: 20 }, image: antd.Empty.PRESENTED_IMAGE_SIMPLE });
21270
+ }
21271
+ if (!chartHtml) {
21272
+ return /* @__PURE__ */ jsxRuntime.jsx(antd.Empty, { description: "No chart data", style: { padding: 20 }, image: antd.Empty.PRESENTED_IMAGE_SIMPLE });
21273
+ }
21274
+ return /* @__PURE__ */ jsxRuntime.jsx(InlinePlotlyHtml, { html: chartHtml, style: { padding: 8, height: "100%", overflow: "auto" } });
21275
+ };
21107
21276
  var DashboardGridCell = ({ cell, allModels, isMaximized, isMinimized, canConfigureLayout, onConfigure, onMaximize, onMinimize, onResize, onMove }) => {
21108
21277
  const { token } = antd.theme.useToken();
21109
21278
  const model = findModelByName(allModels, cell.model);
@@ -21136,10 +21305,12 @@ var DashboardGridCell = ({ cell, allModels, isMaximized, isMinimized, canConfigu
21136
21305
  minHeight: 32,
21137
21306
  position: "relative"
21138
21307
  };
21308
+ const isPlotlyChart = cell.source_type === "plotly_chart";
21139
21309
  const resource = model?.resource || cell.model;
21140
21310
  const isModelLike = cell.source_type === "model" || cell.source_type === "named_query";
21141
- const cellTitle = isModelLike ? model?.label || cell.model : cell.section_name || cell.model;
21311
+ const cellTitle = isPlotlyChart ? cell.chart_title || cell.model : isModelLike ? model?.label || cell.model : cell.section_name || cell.model;
21142
21312
  const tone = isModelLike && model ? getModelTone(model) : null;
21313
+ const [chartRefreshNonce, setChartRefreshNonce] = React5.useState(0);
21143
21314
  const startResize = React5.useCallback((e, dir) => {
21144
21315
  e.preventDefault();
21145
21316
  e.stopPropagation();
@@ -21293,7 +21464,7 @@ var DashboardGridCell = ({ cell, allModels, isMaximized, isMinimized, canConfigu
21293
21464
  ) })
21294
21465
  ] })
21295
21466
  ] }),
21296
- !isMinimized && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1, overflow: "auto", minHeight: 0 }, children: model ? /* @__PURE__ */ jsxRuntime.jsx(
21467
+ !isMinimized && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1, overflow: "auto", minHeight: 0 }, children: isPlotlyChart && cell.chart_url ? /* @__PURE__ */ jsxRuntime.jsx(PlotlyChartContent, { chartUrl: cell.chart_url, refreshNonce: chartRefreshNonce }) : model ? /* @__PURE__ */ jsxRuntime.jsx(
21297
21468
  DynamicList,
21298
21469
  {
21299
21470
  model,
@@ -21971,6 +22142,7 @@ var authSystemModels = [
21971
22142
 
21972
22143
  exports.API_URL = API_URL3;
21973
22144
  exports.AllModelsProvider = AllModelsProvider;
22145
+ exports.AuthenticatedImage = AuthenticatedImage;
21974
22146
  exports.ColorModeContext = ColorModeContext;
21975
22147
  exports.ColorModeContextProvider = ColorModeContextProvider;
21976
22148
  exports.CommandCenterPortal = CommandCenterPortal;
@@ -22017,6 +22189,7 @@ exports.resolveIcon = resolveIcon;
22017
22189
  exports.setColorSchemas = setColorSchemas;
22018
22190
  exports.sortItemsByNavConfig = sortItemsByNavConfig;
22019
22191
  exports.useAllModels = useAllModels;
22192
+ exports.useAuthenticatedFileUrl = useAuthenticatedFileUrl;
22020
22193
  exports.useKeyboardShortcuts = useKeyboardShortcuts;
22021
22194
  exports.useMetadataModal = useMetadataModal;
22022
22195
  exports.useNavConfig = useNavConfig;