@bluemarble/bi 0.7.0 → 0.8.1

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.
@@ -1,7 +1,7 @@
1
1
  import { c as VALUE_FORMATS, f as validateModel, g as parseColumnRef, h as modelEdges, m as measureHome, o as AGGREGATIONS, s as AGGREGATION_LABEL, t as canonicalJson, u as isAggregate, v as referencedTables } from "./codegen-LoQzFAle.js";
2
2
  import { n as diffRows } from "./compare-DloZhg1T.js";
3
- import { C as DEFAULT_SCALE_MIN_COLOR, D as UNARY_OPERATORS, E as RULE_OPERATOR_LABEL, O as VALUE_RULE_OPERATORS, S as COLUMN_VERTICAL_ALIGNS, T as RULE_OPERATORS, a as FILTER_FIELDS, b as COLOR_SCALE_TARGETS, c as chartColumns, f as filterReferences, g as percentColumn, h as layoutModelWarnings, i as CARD_FIELDS, l as chartOfType, n as CANVAS_PRESETS, o as cardsGap, r as CARDS_FIELDS, t as BUILTIN_CHART_TYPES, u as chartReferences, v as tableColumns, w as NUMERIC_OPERATORS, x as COLUMN_ALIGNS, y as validateLayout } from "./layout-BS3nO6OT.js";
4
- import { C as parseLayoutItemKey, S as layoutItemKey, b as errorMessage, f as SERIES_COLORS, n as INSPECT_ATTRIBUTE, o as formatMeasure, t as ModelDictionary, x as LAYOUT_ITEM_ATTRIBUTE, y as useDashboard } from "./ModelDictionary-Cw4sa8tW.js";
3
+ import { C as COLUMN_VERTICAL_ALIGNS, D as RULE_OPERATOR_LABEL, E as RULE_OPERATORS, O as UNARY_OPERATORS, S as COLUMN_ALIGNS, T as NUMERIC_OPERATORS, _ as percentColumn, a as FILTER_FIELDS, b as validateLayout, d as chartReferences, g as layoutModelWarnings, i as CARD_FIELDS, k as VALUE_RULE_OPERATORS, l as chartColumns, n as CANVAS_PRESETS, o as PARAMETER_FIELDS, p as filterReferences, r as CARDS_FIELDS, s as cardsGap, t as BUILTIN_CHART_TYPES, u as chartOfType, w as DEFAULT_SCALE_MIN_COLOR, x as COLOR_SCALE_TARGETS, y as tableColumns } from "./layout-BuZhfoRI.js";
4
+ import { C as useDashboard, D as parseLayoutItemKey, E as layoutItemKey, T as LAYOUT_ITEM_ATTRIBUTE, g as SERIES_COLORS, n as INSPECT_ATTRIBUTE, s as formatMeasure, t as ModelDictionary, w as errorMessage } from "./ModelDictionary-g5d84D0n.js";
5
5
  import { Alert, Autocomplete, Box, Button, Checkbox, Chip, CircularProgress, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControlLabel, IconButton, List, ListItemButton, ListSubheader, Menu, MenuItem, MenuList, Paper, Popover, Popper, Portal, Switch, Tab, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TableSortLabel, Tabs, TextField, ThemeProvider, ToggleButton, ToggleButtonGroup, Tooltip, Typography, createTheme } from "@mui/material";
6
6
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1161,8 +1161,10 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1161
1161
  ] });
1162
1162
  const charts = editing.charts;
1163
1163
  const filters = editing.filters ?? [];
1164
+ const parameters = editing.parameters ?? [];
1164
1165
  const selectedFilter = selection?.kind === "filter" ? filters.find((filter) => filter.dimension === selection.key) ?? null : null;
1165
- const selected = selection?.kind === "filter" || selection?.kind === "canvas" || selection?.kind === "cards" ? null : charts.find((chart) => chart.id === selection?.key) ?? charts[0] ?? null;
1166
+ const selectedParameter = selection?.kind === "parameter" ? parameters.find((parameter) => parameter.name === selection.key) ?? null : null;
1167
+ const selected = selection?.kind === "filter" || selection?.kind === "parameter" || selection?.kind === "canvas" || selection?.kind === "cards" ? null : charts.find((chart) => chart.id === selection?.key) ?? charts[0] ?? null;
1166
1168
  const layoutKey = entry.registered.id;
1167
1169
  const differsFromFile = canonicalJson(editing) !== canonicalJson(entry.registered);
1168
1170
  const dirty = canonicalJson(editing) !== referenceKey;
@@ -1183,6 +1185,8 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1183
1185
  filters: next
1184
1186
  });
1185
1187
  const patchFilter = (dimension, change) => setFilters(filters.map((filter) => filter.dimension === dimension ? change(filter) : filter));
1188
+ const setParameters = (next) => update(withField(editing, "parameters", next.length ? next : void 0));
1189
+ const patchParameter = (name, change) => setParameters(parameters.map((parameter) => parameter.name === name ? change(parameter) : parameter));
1186
1190
  const toggleSidebar = () => {
1187
1191
  const next = !sidebar;
1188
1192
  setSidebar(next);
@@ -1210,8 +1214,11 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1210
1214
  },
1211
1215
  onDrop: () => {
1212
1216
  if (drag?.kind !== kind) return;
1213
- if (drag.to !== drag.from && drag.to !== drag.from + 1) if (kind === "chart") setCharts(moveTo(charts, drag.from, drag.to));
1214
- else setFilters(moveTo(filters, drag.from, drag.to));
1217
+ if (drag.to !== drag.from && drag.to !== drag.from + 1) {
1218
+ if (kind === "chart") setCharts(moveTo(charts, drag.from, drag.to));
1219
+ else if (kind === "parameter") setParameters(moveTo(parameters, drag.from, drag.to));
1220
+ else setFilters(moveTo(filters, drag.from, drag.to));
1221
+ }
1215
1222
  setDrag(null);
1216
1223
  },
1217
1224
  onDragEnd: () => setDrag(null)
@@ -1226,6 +1233,21 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1226
1233
  key: dimension
1227
1234
  });
1228
1235
  };
1236
+ const addParameter = () => {
1237
+ const measure = Object.keys(model.measures)[0];
1238
+ if (!measure) return;
1239
+ const used = new Set(parameters.map((parameter) => parameter.name));
1240
+ let name = "medida";
1241
+ for (let i = 2; used.has(name); i++) name = `medida-${i}`;
1242
+ setParameters([...parameters, {
1243
+ name,
1244
+ measures: [measure]
1245
+ }]);
1246
+ setSelection({
1247
+ kind: "parameter",
1248
+ key: name
1249
+ });
1250
+ };
1229
1251
  const focus = (item) => {
1230
1252
  setSelection(item);
1231
1253
  if (stacked) setSidebar(false);
@@ -1236,7 +1258,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1236
1258
  };
1237
1259
  const stacked = rootWidth < STACK_WIDTH;
1238
1260
  const formColumns = editorWidth < 420 ? 1 : editorWidth < 720 ? 2 : 4;
1239
- const selectedTitle = selection?.kind === "canvas" ? "Tela" : selection?.kind === "cards" ? "Cards" : selectedFilter ? selectedFilter.label || model.dimensions[selectedFilter.dimension]?.label || selectedFilter.dimension : selected ? selected.title || selected.id : "";
1261
+ const selectedTitle = selection?.kind === "canvas" ? "Tela" : selection?.kind === "cards" ? "Cards" : selectedFilter ? selectedFilter.label || model.dimensions[selectedFilter.dimension]?.label || selectedFilter.dimension : selectedParameter ? selectedParameter.label || selectedParameter.name : selected ? selected.title || selected.id : "";
1240
1262
  const hover = (item) => onHover(item && itemQueryId(layoutKey, item));
1241
1263
  const add = (type, base) => {
1242
1264
  setAddMenu(null);
@@ -1402,6 +1424,38 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1402
1424
  }, filter.dimension);
1403
1425
  })]
1404
1426
  }),
1427
+ /* @__PURE__ */ jsxs(Section, {
1428
+ title: `Parâmetros (${parameters.length})`,
1429
+ collapseKey: "layout:lista:parametros",
1430
+ actions: /* @__PURE__ */ jsx(Button, {
1431
+ size: "small",
1432
+ onClick: addParameter,
1433
+ disabled: !Object.keys(model.measures).length,
1434
+ children: "+ Novo"
1435
+ }),
1436
+ children: [!parameters.length && /* @__PURE__ */ jsx(Typography, {
1437
+ sx: {
1438
+ fontSize: 12,
1439
+ color: "text.disabled",
1440
+ mb: 1
1441
+ },
1442
+ children: "Nenhum parâmetro de medida."
1443
+ }), parameters.map((parameter, index) => {
1444
+ const item = {
1445
+ kind: "parameter",
1446
+ key: parameter.name
1447
+ };
1448
+ return /* @__PURE__ */ jsx(ItemRow, {
1449
+ title: parameter.label || "Medida",
1450
+ subtitle: `${parameter.name} · ${parameter.measures.length} medida(s)`,
1451
+ selected: selectedParameter?.name === parameter.name,
1452
+ onSelect: () => focus(item),
1453
+ onHover: (on) => hover(on ? item : null),
1454
+ ...dragProps("parameter", index, parameters.length),
1455
+ onRemove: () => setParameters(parameters.filter((other) => other.name !== parameter.name))
1456
+ }, parameter.name);
1457
+ })]
1458
+ }),
1405
1459
  /* @__PURE__ */ jsx(Section, {
1406
1460
  title: `Gráficos (${charts.length})`,
1407
1461
  collapseKey: "layout:lista:graficos",
@@ -1501,6 +1555,10 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1501
1555
  filter: selectedFilter,
1502
1556
  model,
1503
1557
  formatters: entry.parts.filters ?? [],
1558
+ charts: editing.charts.map((chart) => ({
1559
+ name: chart.id,
1560
+ label: chart.title || chart.id
1561
+ })),
1504
1562
  columns: formColumns,
1505
1563
  onChange: (change) => {
1506
1564
  patchFilter(selectedFilter.dimension, change);
@@ -1509,12 +1567,27 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1509
1567
  key: change(selectedFilter).dimension
1510
1568
  });
1511
1569
  }
1512
- }, `filter:${selectedFilter.dimension}`) : selected ? /* @__PURE__ */ jsx(ChartForm, {
1570
+ }, `filter:${selectedFilter.dimension}`) : selectedParameter ? /* @__PURE__ */ jsx(ParameterForm, {
1571
+ parameter: selectedParameter,
1572
+ model,
1573
+ columns: formColumns,
1574
+ onChange: (change) => {
1575
+ patchParameter(selectedParameter.name, change);
1576
+ setSelection({
1577
+ kind: "parameter",
1578
+ key: change(selectedParameter).name
1579
+ });
1580
+ }
1581
+ }, `parameter:${selectedParameter.name}`) : selected ? /* @__PURE__ */ jsx(ChartForm, {
1513
1582
  chart: selected,
1514
1583
  cards: editing.cards,
1515
1584
  types: chartTypes,
1516
1585
  model,
1517
1586
  formatters: entry.parts.charts ?? [],
1587
+ measureParameters: parameters.map((parameter) => ({
1588
+ name: parameter.name,
1589
+ label: parameter.label || "Medida"
1590
+ })),
1518
1591
  columns: formColumns,
1519
1592
  onChange: (change) => {
1520
1593
  patchChart(selected.id, change);
@@ -1820,7 +1893,7 @@ function SmallButton({ title, disabled, onClick, children }) {
1820
1893
  }) })
1821
1894
  });
1822
1895
  }
1823
- function ChartForm({ chart, cards, types, model, formatters, columns, onChange }) {
1896
+ function ChartForm({ chart, cards, types, model, formatters, measureParameters, columns, onChange }) {
1824
1897
  const set = (key, value) => onChange((current) => withField(current, key, value));
1825
1898
  const setMany = (changes) => onChange((current) => Object.entries(changes).reduce((chart, [key, value]) => withField(chart, key, value), current));
1826
1899
  const setCard = (key, value) => onChange((current) => withField(current, "card", nonEmpty(withField(current.card ?? {}, key, value))));
@@ -1831,6 +1904,7 @@ function ChartForm({ chart, cards, types, model, formatters, columns, onChange }
1831
1904
  onSetMany: setMany,
1832
1905
  model,
1833
1906
  formatters,
1907
+ measureParameters,
1834
1908
  columns: chartColumns(chart, types),
1835
1909
  formColumns: columns,
1836
1910
  chosenDimensions: chartReferences(chart, types).dimensions,
@@ -1866,23 +1940,42 @@ function ChartForm({ chart, cards, types, model, formatters, columns, onChange }
1866
1940
  formatters: [],
1867
1941
  columns: [],
1868
1942
  formColumns: columns,
1869
- chosenDimensions: []
1943
+ chosenDimensions: [],
1944
+ sectionActions: { Card: /* @__PURE__ */ jsx(Button, {
1945
+ size: "small",
1946
+ disabled: !chart.card || !Object.keys(chart.card).length,
1947
+ onClick: () => onChange((current) => withField(current, "card", void 0)),
1948
+ children: "Usar valor padrão"
1949
+ }) }
1870
1950
  })] });
1871
1951
  }
1872
- function FilterForm({ filter, model, formatters, columns, onChange }) {
1952
+ function FilterForm({ filter, model, formatters, charts, columns, onChange }) {
1873
1953
  return /* @__PURE__ */ jsx(FieldsForm, {
1874
1954
  fields: FILTER_FIELDS,
1875
1955
  values: filter,
1876
1956
  onSet: (key, value) => onChange((current) => withField(current, key, value)),
1877
1957
  model,
1878
1958
  formatters,
1959
+ charts,
1879
1960
  columns: [],
1880
1961
  formColumns: columns,
1881
1962
  chosenDimensions: [filter.dimension]
1882
1963
  });
1883
1964
  }
1965
+ function ParameterForm({ parameter, model, columns, onChange }) {
1966
+ return /* @__PURE__ */ jsx(FieldsForm, {
1967
+ fields: PARAMETER_FIELDS,
1968
+ values: parameter,
1969
+ onSet: (key, value) => onChange((current) => withField(current, key, value)),
1970
+ model,
1971
+ formatters: [],
1972
+ columns: parameter.measures,
1973
+ formColumns: columns,
1974
+ chosenDimensions: []
1975
+ });
1976
+ }
1884
1977
  /** Formulário gerado dos `FieldSpec` (campos do tipo de gráfico ou do filtro). */
1885
- function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters, columns, formColumns, chosenDimensions, chosenMeasures = [], header }) {
1978
+ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters, columns, formColumns, chosenDimensions, chosenMeasures = [], charts = [], measureParameters = [], sectionActions, header }) {
1886
1979
  const dimensions = Object.entries(model.dimensions).map(([name, dim]) => ({
1887
1980
  name,
1888
1981
  label: dim.label
@@ -2077,6 +2170,9 @@ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters,
2077
2170
  case "dimensions": return dimensions;
2078
2171
  case "measure":
2079
2172
  case "measures": return measures;
2173
+ case "filters": return dimensions;
2174
+ case "charts": return charts;
2175
+ case "parameter": return measureParameters;
2080
2176
  case "formatter": return formatters.map((name) => ({ name }));
2081
2177
  case "column": return spec.outside ? fieldOptions : columnOptionsOf(columns);
2082
2178
  case "enum": return spec.values.map((name) => ({
@@ -2165,6 +2261,28 @@ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters,
2165
2261
  }
2166
2262
  });
2167
2263
  }
2264
+ case "filters": return /* @__PURE__ */ jsx(ColumnList, {
2265
+ items: (value ?? []).map((name) => ({
2266
+ kind: "dimension",
2267
+ name
2268
+ })),
2269
+ options: { dimension: options(spec) },
2270
+ onChange: (next) => {
2271
+ const list = next.map((item) => item.name);
2272
+ set(key, list.length ? list : void 0);
2273
+ }
2274
+ });
2275
+ case "charts": return /* @__PURE__ */ jsx(ColumnList, {
2276
+ items: (value ?? []).map((name) => ({
2277
+ kind: "chart",
2278
+ name
2279
+ })),
2280
+ options: { chart: options(spec) },
2281
+ onChange: (next) => {
2282
+ const list = next.map((item) => item.name);
2283
+ set(key, list.length ? list : void 0);
2284
+ }
2285
+ });
2168
2286
  case "valueRule": return /* @__PURE__ */ jsx(ValueRuleInput, {
2169
2287
  rule: value,
2170
2288
  label: spec.label,
@@ -2259,13 +2377,14 @@ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters,
2259
2377
  ...pairedFormatters
2260
2378
  ].filter(Boolean));
2261
2379
  const sections = [...new Set(entries.filter(([key]) => !hidden.has(key)).map(([, spec]) => spec.section))];
2262
- const fullRow = (spec) => spec.kind === "dimensions" || spec.kind === "measures" || spec.kind === "formatters" || spec.kind === "formatRules" || spec.kind === "colorScales" || spec.kind === "colors" || spec.kind === "valueRule";
2380
+ const fullRow = (spec) => spec.kind === "dimensions" || spec.kind === "measures" || spec.kind === "filters" || spec.kind === "charts" || spec.kind === "formatters" || spec.kind === "formatRules" || spec.kind === "colorScales" || spec.kind === "colors" || spec.kind === "valueRule";
2263
2381
  const gridColumn = (spec, key) => fullRow(spec) ? "1 / -1" : key === "title" && formColumns > 1 ? "span 2" : void 0;
2264
2382
  return /* @__PURE__ */ jsx(Fragment, { children: sections.map((section) => {
2265
2383
  const inSection = entries.filter(([, spec]) => spec.section === section);
2266
2384
  return /* @__PURE__ */ jsx(Section, {
2267
2385
  title: section,
2268
2386
  collapseKey: `layout:form:${section}`,
2387
+ actions: sectionActions?.[section],
2269
2388
  children: /* @__PURE__ */ jsxs(Box, {
2270
2389
  sx: formGrid(formColumns),
2271
2390
  children: [section === sections[0] && header, inSection.filter(([key]) => !hidden.has(key)).map(([key, spec]) => /* @__PURE__ */ jsx(Box, {
@@ -3237,7 +3356,7 @@ function ColumnOptions({ kind, name, model, label, onLabel, format, onFormat, fo
3237
3356
  value: formatter,
3238
3357
  options: formatters.map((option) => ({ name: option })),
3239
3358
  onChange: onFormatter
3240
- }), "Formatters passados ao <DashboardCharts formatters={{ ... }}> da página"),
3359
+ }), "Built-in (to_month_name, to_date, ...) e os passados ao <DashboardCharts formatters={{ ... }}> da página"),
3241
3360
  format !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
3242
3361
  /* @__PURE__ */ jsxs(Box, {
3243
3362
  sx: {
@@ -3436,6 +3555,12 @@ const KIND_STYLE = {
3436
3555
  color: "#9775fa",
3437
3556
  noun: "% do total",
3438
3557
  group: "% do total (da medida)"
3558
+ },
3559
+ chart: {
3560
+ icon: "▦",
3561
+ color: "#51cf66",
3562
+ noun: "gráfico",
3563
+ group: "Gráficos"
3439
3564
  }
3440
3565
  };
3441
3566
  /**
@@ -3821,186 +3946,689 @@ function freeName(prefix, taken) {
3821
3946
  for (let index = 2;; index++) if (!taken.includes(`${prefix}${index}`)) return `${prefix}${index}`;
3822
3947
  }
3823
3948
  //#endregion
3824
- //#region src/devtools/SqlEditor.tsx
3825
- /**
3826
- * Campo de SQL com destaque de sintaxe e autocomplete de tabelas e colunas. As colunas vêm do
3827
- * `information_schema` (tabelas/views do banco) e das colunas de cada tabela do modelo (lidas do banco).
3828
- * O ⤢ abre o mesmo SQL num editor grande.
3829
- *
3830
- * O destaque é um `<pre>` colorido por baixo do `<textarea>` de texto transparente: mesma fonte,
3831
- * mesmo recuo e mesma quebra de linha, com a rolagem sincronizada. O cursor, a seleção e o
3832
- * autocomplete continuam sendo do textarea.
3833
- */
3834
- const PAD = {
3835
- top: 8.5,
3836
- right: 30,
3837
- bottom: 8.5,
3838
- left: 12
3839
- };
3840
- const LINE_HEIGHT = "18px";
3841
- const SqlCompletionsContext = createContext({
3842
- tables: [],
3843
- modelColumns: {}
3844
- });
3845
- const KEYWORDS = new Set("WHERE ON LEFT RIGHT INNER OUTER JOIN GROUP ORDER LIMIT USING AND OR AS SET HAVING UNION".split(" "));
3846
- /** `alias -> tabela` a partir dos `FROM x a` / `JOIN x AS a` do texto. */
3847
- function aliases(sql) {
3848
- const map = /* @__PURE__ */ new Map();
3849
- for (const match of sql.matchAll(/\b(?:FROM|JOIN)\s+`?(\w+)`?(?:\s+(?:AS\s+)?`?(\w+)`?)?/gi)) {
3850
- const [, table, alias] = match;
3851
- map.set(table.toLowerCase(), table);
3852
- if (alias && !KEYWORDS.has(alias.toUpperCase())) map.set(alias.toLowerCase(), table);
3853
- }
3854
- return map;
3949
+ //#region src/devtools/QueryBuilder.tsx
3950
+ const RESERVED = new Set("select from where group order by limit key keys index table desc asc and or not in is null like as on join case when then else end interval range rows".split(" "));
3951
+ const quote = (name) => /^[A-Za-z_]\w*$/.test(name) && !RESERVED.has(name.toLowerCase()) ? name : `\`${name}\``;
3952
+ const unquote = (name) => name.trim().replace(/^`(.*)`$/, "$1");
3953
+ function buildSelect(table, columns) {
3954
+ return `SELECT\n${columns.length ? columns.map((column) => ` ${quote(column)}`).join(",\n") : " *"}\nFROM ${quote(table)}`;
3855
3955
  }
3856
- function suggest(sql, caret, completions) {
3857
- const before = sql.slice(0, caret);
3858
- const tables = new Map(completions.tables.map((table) => [table.name.toLowerCase(), table]));
3859
- const columnsOf = (table) => tables.get(table.toLowerCase())?.columns.map((column) => ({
3860
- text: column.name,
3861
- detail: column.type
3862
- })) ?? [];
3863
- let prefix;
3864
- let candidates;
3865
- const qualified = /([A-Za-z_]\w*)\.(\w*)$/.exec(before);
3866
- if (qualified) {
3867
- const [, qualifier, typed] = qualified;
3868
- prefix = typed;
3869
- if (completions.modelColumns[qualifier]) candidates = completions.modelColumns[qualifier].map((text) => ({
3870
- text,
3871
- detail: qualifier
3872
- }));
3873
- else candidates = columnsOf(aliases(sql).get(qualifier.toLowerCase()) ?? qualifier);
3874
- } else {
3875
- const word = /[A-Za-z_]\w*$/.exec(before);
3876
- if (!word || word[0].length < 2) return null;
3877
- prefix = word[0];
3878
- const referenced = [...new Set(aliases(sql).values())];
3879
- candidates = [
3880
- ...Object.keys(completions.modelColumns).map((text) => ({
3881
- text,
3882
- detail: "tabela do modelo"
3883
- })),
3884
- ...completions.tables.map((table) => ({
3885
- text: table.name,
3886
- detail: table.type
3887
- })),
3888
- ...referenced.flatMap(columnsOf)
3889
- ];
3890
- }
3891
- const lower = prefix.toLowerCase();
3892
- const seen = /* @__PURE__ */ new Set();
3893
- const items = candidates.filter((item) => {
3894
- const text = item.text.toLowerCase();
3895
- if (seen.has(text) || text === lower || !text.includes(lower)) return false;
3896
- seen.add(text);
3897
- return true;
3898
- }).sort((a, b) => Number(!a.text.toLowerCase().startsWith(lower)) - Number(!b.text.toLowerCase().startsWith(lower)) || a.text.localeCompare(b.text)).slice(0, 8);
3899
- return items.length ? {
3900
- from: caret - prefix.length,
3901
- items
3902
- } : null;
3956
+ /** Tabela e colunas de um `SELECT a, b FROM t` (sem WHERE, JOIN etc.); senão `null`. */
3957
+ function parseSelect(sql) {
3958
+ const match = /^\s*SELECT\s+([\s\S]+?)\s+FROM\s+(`[^`]+`|\w+)\s*;?\s*$/i.exec(sql);
3959
+ if (!match) return null;
3960
+ const columns = match[1].trim() === "*" ? [] : match[1].split(",").map(unquote);
3961
+ if (columns.some((column) => !/^[^\s`().]+$/.test(column))) return null;
3962
+ return {
3963
+ table: unquote(match[2]),
3964
+ columns
3965
+ };
3903
3966
  }
3904
- function SqlEditor({ value, onChange, label, placeholder = "SQL", minRows, maxRows = 6, error, warning, expandable = true }) {
3905
- const completions = useContext(SqlCompletionsContext);
3906
- const anchor = useRef(null);
3907
- const input = useRef(null);
3908
- const highlight = useRef(null);
3909
- const [expanded, setExpanded] = useState(false);
3910
- const [state, setState] = useState(null);
3911
- const accept = (item) => {
3912
- const element = input.current;
3913
- if (!element || !state) return;
3914
- const caret = element.selectionStart;
3915
- onChange(value.slice(0, state.from) + item.text + value.slice(caret));
3916
- setState(null);
3917
- const position = state.from + item.text.length;
3918
- requestAnimationFrame(() => {
3919
- element.focus();
3920
- element.setSelectionRange(position, position);
3921
- });
3967
+ function QueryBuilder({ open, onClose, tables, sql, onApply }) {
3968
+ return /* @__PURE__ */ jsx(Dialog, {
3969
+ open,
3970
+ onClose,
3971
+ maxWidth: "md",
3972
+ fullWidth: true,
3973
+ children: open && /* @__PURE__ */ jsx(Builder, {
3974
+ tables,
3975
+ sql,
3976
+ onClose,
3977
+ onApply
3978
+ })
3979
+ });
3980
+ }
3981
+ function Builder({ tables, sql, onClose, onApply }) {
3982
+ const [initial] = useState(() => {
3983
+ const parsed = parseSelect(sql);
3984
+ const table = parsed && tables.find((item) => item.name === parsed.table);
3985
+ if (!parsed || !table) return {
3986
+ table: null,
3987
+ columns: []
3988
+ };
3989
+ const known = new Set(table.columns.map((column) => column.name));
3990
+ return {
3991
+ table,
3992
+ columns: parsed.columns.filter((column) => known.has(column))
3993
+ };
3994
+ });
3995
+ const [table, setTable] = useState(initial.table);
3996
+ const [selected, setSelected] = useState(initial.columns);
3997
+ const [search, setSearch] = useState("");
3998
+ const visible = useMemo(() => {
3999
+ const lower = search.trim().toLowerCase();
4000
+ return (table?.columns ?? []).filter((column) => column.name.toLowerCase().includes(lower));
4001
+ }, [table, search]);
4002
+ const ordered = (table?.columns ?? []).map((column) => column.name).filter((name) => selected.includes(name));
4003
+ const result = table ? buildSelect(table.name, ordered) : "";
4004
+ const allVisible = visible.length > 0 && visible.every((column) => selected.includes(column.name));
4005
+ const toggle = (name) => setSelected((current) => current.includes(name) ? current.filter((item) => item !== name) : [...current, name]);
4006
+ const toggleVisible = () => {
4007
+ const names = visible.map((column) => column.name);
4008
+ setSelected((current) => allVisible ? current.filter((name) => !names.includes(name)) : [.../* @__PURE__ */ new Set([...current, ...names])]);
3922
4009
  };
3923
4010
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3924
- /* @__PURE__ */ jsx(TextField, {
3925
- ref: anchor,
3926
- inputRef: input,
3927
- size: "small",
3928
- fullWidth: true,
3929
- multiline: true,
3930
- label,
3931
- minRows,
3932
- maxRows,
3933
- value,
3934
- placeholder,
3935
- error: !value.trim() || !!error,
3936
- helperText: error ?? warning,
3937
- onChange: (event) => {
3938
- onChange(event.target.value);
3939
- const found = suggest(event.target.value, event.target.selectionStart ?? 0, completions);
3940
- setState(found && {
3941
- ...found,
3942
- index: 0
3943
- });
3944
- },
3945
- onKeyDown: (event) => {
3946
- if (!state) return;
3947
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
3948
- event.preventDefault();
3949
- const step = event.key === "ArrowDown" ? 1 : -1;
3950
- setState({
3951
- ...state,
3952
- index: (state.index + step + state.items.length) % state.items.length
3953
- });
3954
- } else if (event.key === "Enter" || event.key === "Tab") {
3955
- event.preventDefault();
3956
- accept(state.items[state.index]);
3957
- } else if (event.key === "Escape") {
3958
- event.stopPropagation();
3959
- setState(null);
3960
- }
4011
+ /* @__PURE__ */ jsx(DialogTitle, {
4012
+ sx: { fontSize: 14 },
4013
+ children: "Criar query"
4014
+ }),
4015
+ /* @__PURE__ */ jsxs(DialogContent, {
4016
+ sx: {
4017
+ display: "flex",
4018
+ flexDirection: "column",
4019
+ gap: 1.5
3961
4020
  },
3962
- onBlur: () => setState(null),
3963
- slotProps: {
3964
- htmlInput: {
3965
- spellCheck: false,
3966
- onScroll: (event) => {
3967
- if (highlight.current) highlight.current.scrollTop = event.currentTarget.scrollTop;
3968
- }
3969
- },
3970
- input: { endAdornment: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Box, {
3971
- component: "pre",
3972
- ref: highlight,
3973
- "aria-hidden": true,
3974
- sx: {
3975
- ...textSx,
3976
- m: 0,
3977
- p: 0,
3978
- position: "absolute",
3979
- top: PAD.top,
3980
- right: PAD.right,
3981
- bottom: PAD.bottom,
3982
- left: PAD.left,
3983
- overflow: "hidden",
3984
- pointerEvents: "none",
3985
- color: "text.primary"
4021
+ children: [
4022
+ /* @__PURE__ */ jsx(Autocomplete, {
4023
+ size: "small",
4024
+ options: tables,
4025
+ value: table,
4026
+ onChange: (_, value) => {
4027
+ setTable(value);
4028
+ setSelected([]);
4029
+ setSearch("");
3986
4030
  },
3987
- children: [/* @__PURE__ */ jsx(HighlightedSql, { code: value }), "\n"]
3988
- }), expandable && /* @__PURE__ */ jsx(Box, {
3989
- component: "button",
3990
- type: "button",
3991
- title: "Expandir o SQL",
3992
- "aria-label": "Expandir o SQL",
3993
- onClick: () => setExpanded(true),
4031
+ getOptionLabel: (option) => option.name,
4032
+ isOptionEqualToValue: (option, value) => option.name === value.name,
4033
+ noOptionsText: tables.length ? "Nenhuma tabela" : "Carregando as tabelas do banco…",
4034
+ renderOption: ({ key, ...props }, option) => /* @__PURE__ */ jsxs(Box, {
4035
+ component: "li",
4036
+ ...props,
4037
+ sx: {
4038
+ gap: 2,
4039
+ fontFamily: MONO,
4040
+ fontSize: 12
4041
+ },
4042
+ children: [option.name, /* @__PURE__ */ jsxs(Box, {
4043
+ component: "span",
4044
+ sx: {
4045
+ ml: "auto",
4046
+ color: "text.secondary",
4047
+ fontSize: 11
4048
+ },
4049
+ children: [
4050
+ option.type,
4051
+ " · ",
4052
+ option.columns.length,
4053
+ " colunas"
4054
+ ]
4055
+ })]
4056
+ }, key),
4057
+ renderInput: (params) => /* @__PURE__ */ jsx(TextField, {
4058
+ ...params,
4059
+ label: "tabela",
4060
+ sx: { mt: 1 }
4061
+ })
4062
+ }),
4063
+ table && /* @__PURE__ */ jsxs(Box, {
3994
4064
  sx: {
3995
- position: "absolute",
3996
- top: 4,
3997
- right: 4,
3998
- zIndex: 2,
3999
- border: 0,
4000
- borderRadius: .5,
4001
- bgcolor: "transparent",
4002
- color: "text.secondary",
4003
- cursor: "pointer",
4065
+ display: "flex",
4066
+ flexDirection: "column",
4067
+ gap: .5
4068
+ },
4069
+ children: [/* @__PURE__ */ jsxs(Box, {
4070
+ sx: {
4071
+ display: "flex",
4072
+ alignItems: "center",
4073
+ gap: 1
4074
+ },
4075
+ children: [
4076
+ /* @__PURE__ */ jsx(FormControlLabel, {
4077
+ control: /* @__PURE__ */ jsx(Checkbox, {
4078
+ size: "small",
4079
+ checked: allVisible,
4080
+ indeterminate: !allVisible && visible.some((c) => selected.includes(c.name)),
4081
+ onChange: toggleVisible
4082
+ }),
4083
+ label: /* @__PURE__ */ jsx(Typography, {
4084
+ sx: { fontSize: 12 },
4085
+ children: "Todas"
4086
+ })
4087
+ }),
4088
+ /* @__PURE__ */ jsxs(Typography, {
4089
+ sx: {
4090
+ fontSize: 12,
4091
+ color: "text.secondary"
4092
+ },
4093
+ children: [
4094
+ selected.length,
4095
+ " de ",
4096
+ table.columns.length,
4097
+ " marcadas",
4098
+ !selected.length && " (nenhuma = *)"
4099
+ ]
4100
+ }),
4101
+ /* @__PURE__ */ jsx(TextField, {
4102
+ size: "small",
4103
+ placeholder: "filtrar colunas",
4104
+ value: search,
4105
+ onChange: (event) => setSearch(event.target.value),
4106
+ sx: {
4107
+ ml: "auto",
4108
+ width: 200
4109
+ }
4110
+ })
4111
+ ]
4112
+ }), /* @__PURE__ */ jsx(Box, {
4113
+ sx: {
4114
+ maxHeight: 280,
4115
+ overflow: "auto",
4116
+ border: 1,
4117
+ borderColor: "divider",
4118
+ borderRadius: 1,
4119
+ px: 1,
4120
+ display: "grid",
4121
+ gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))"
4122
+ },
4123
+ children: visible.map((column) => /* @__PURE__ */ jsx(FormControlLabel, {
4124
+ control: /* @__PURE__ */ jsx(Checkbox, {
4125
+ size: "small",
4126
+ checked: selected.includes(column.name),
4127
+ onChange: () => toggle(column.name)
4128
+ }),
4129
+ label: /* @__PURE__ */ jsxs(Box, {
4130
+ component: "span",
4131
+ sx: {
4132
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4133
+ fontSize: 12
4134
+ },
4135
+ children: [
4136
+ column.name,
4137
+ " ",
4138
+ /* @__PURE__ */ jsx(Box, {
4139
+ component: "span",
4140
+ sx: {
4141
+ color: "text.secondary",
4142
+ fontSize: 11
4143
+ },
4144
+ children: column.type
4145
+ })
4146
+ ]
4147
+ }),
4148
+ sx: {
4149
+ m: 0,
4150
+ minWidth: 0
4151
+ }
4152
+ }, column.name))
4153
+ })]
4154
+ }),
4155
+ result && /* @__PURE__ */ jsx(Box, {
4156
+ component: "pre",
4157
+ sx: {
4158
+ m: 0,
4159
+ p: 1,
4160
+ border: 1,
4161
+ borderColor: "divider",
4162
+ borderRadius: 1,
4163
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4164
+ fontSize: 12,
4165
+ maxHeight: 200,
4166
+ overflow: "auto"
4167
+ },
4168
+ children: /* @__PURE__ */ jsx(HighlightedSql, { code: result })
4169
+ })
4170
+ ]
4171
+ }),
4172
+ /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
4173
+ onClick: onClose,
4174
+ children: "Cancelar"
4175
+ }), /* @__PURE__ */ jsx(Button, {
4176
+ variant: "contained",
4177
+ disabled: !table,
4178
+ onClick: () => {
4179
+ onApply(result);
4180
+ onClose();
4181
+ },
4182
+ title: "Substitui o SQL atual",
4183
+ children: "Usar no SQL"
4184
+ })] })
4185
+ ] });
4186
+ }
4187
+ //#endregion
4188
+ //#region src/devtools/queryTemplateDefs.ts
4189
+ /**
4190
+ * `WITH` da origem: a tabela do banco entra pelo nome; o SQL de uma tabela do modelo vira o CTE
4191
+ * `origem`, antes dos CTEs do template.
4192
+ */
4193
+ function withSource(source) {
4194
+ if (source.kind === "database") return {
4195
+ ctes: [],
4196
+ from: quote(source.table)
4197
+ };
4198
+ return {
4199
+ ctes: [`origem AS (\n${source.sql.trim().replace(/;+\s*$/, "").replace(/^/gm, " ")}\n)`],
4200
+ from: "origem"
4201
+ };
4202
+ }
4203
+ /**
4204
+ * Um dia por linha, do menor ao maior valor da coluna, com ano e mês. É a tabela "Calendário" do
4205
+ * Power BI: relacione a coluna de data da fato com `dia`.
4206
+ *
4207
+ * Não é um `WITH RECURSIVE` de um dia por iteração: o MySQL (`cte_max_recursion_depth`) e o MariaDB
4208
+ * (`max_recursive_iterations`) param em 1000 iterações, e o MariaDB corta em silêncio — o calendário
4209
+ * acabaria ~2,7 anos depois do início. A recursão só gera 0–999 e o dia sai de `mil * 1000 + n`.
4210
+ */
4211
+ function calendarSql(source, column) {
4212
+ const { ctes, from } = withSource(source);
4213
+ const col = quote(column);
4214
+ return [
4215
+ `WITH RECURSIVE ${[
4216
+ ...ctes,
4217
+ [
4218
+ "limites AS (",
4219
+ ` SELECT DATE(MIN(${col})) AS inicio, DATE(MAX(${col})) AS fim`,
4220
+ ` FROM ${from}`,
4221
+ ")"
4222
+ ],
4223
+ [
4224
+ "numeros AS (",
4225
+ " SELECT 0 AS n",
4226
+ " UNION ALL",
4227
+ " SELECT n + 1 FROM numeros WHERE n < 999",
4228
+ ")"
4229
+ ],
4230
+ [
4231
+ "dias AS (",
4232
+ " SELECT inicio + INTERVAL (mil.n * 1000 + numeros.n) DAY AS dia",
4233
+ " FROM limites, numeros mil, numeros",
4234
+ " WHERE mil.n <= DATEDIFF(fim, inicio) DIV 1000",
4235
+ " AND mil.n * 1000 + numeros.n <= DATEDIFF(fim, inicio)",
4236
+ ")"
4237
+ ]
4238
+ ].map((part) => Array.isArray(part) ? part.join("\n") : part).join(",\n")}`,
4239
+ "SELECT",
4240
+ " dia,",
4241
+ " YEAR(dia) AS ano,",
4242
+ " MONTH(dia) AS mes",
4243
+ "FROM dias"
4244
+ ].join("\n");
4245
+ }
4246
+ const QUERY_TEMPLATES = [{
4247
+ id: "calendar",
4248
+ label: "Calendário",
4249
+ description: "Um dia por linha (com ano e mês) entre a menor e a maior data da coluna. Relacione a data da fato com `dia`.",
4250
+ columnLabel: "coluna de data",
4251
+ preferredTypes: /date|time/i,
4252
+ build: calendarSql
4253
+ }];
4254
+ //#endregion
4255
+ //#region src/devtools/QueryTemplates.tsx
4256
+ function QueryTemplates({ open, onClose, ...props }) {
4257
+ return /* @__PURE__ */ jsx(Dialog, {
4258
+ open,
4259
+ onClose,
4260
+ maxWidth: "md",
4261
+ fullWidth: true,
4262
+ children: open && /* @__PURE__ */ jsx(Templates, {
4263
+ onClose,
4264
+ ...props
4265
+ })
4266
+ });
4267
+ }
4268
+ function Templates({ tables, modelTables, onClose, onApply }) {
4269
+ const [templateId, setTemplateId] = useState(QUERY_TEMPLATES[0].id);
4270
+ const [source, setSource] = useState(null);
4271
+ const [column, setColumn] = useState(null);
4272
+ const template = QUERY_TEMPLATES.find((item) => item.id === templateId) ?? QUERY_TEMPLATES[0];
4273
+ const sources = useMemo(() => [...Object.entries(modelTables).map(([name, table]) => ({
4274
+ key: `model:${name}`,
4275
+ name,
4276
+ group: "Tabelas do modelo",
4277
+ columns: table.columns.map((column) => ({ name: column })),
4278
+ source: {
4279
+ kind: "model",
4280
+ sql: table.sql
4281
+ }
4282
+ })), ...tables.map((table) => ({
4283
+ key: `database:${table.name}`,
4284
+ name: table.name,
4285
+ group: "Tabelas do banco",
4286
+ columns: table.columns,
4287
+ source: {
4288
+ kind: "database",
4289
+ table: table.name
4290
+ }
4291
+ }))], [tables, modelTables]);
4292
+ const columns = useMemo(() => {
4293
+ const list = source?.columns ?? [];
4294
+ const preferred = (type) => !!type && !!template.preferredTypes?.test(type);
4295
+ return [...list.filter((c) => preferred(c.type)), ...list.filter((c) => !preferred(c.type))];
4296
+ }, [source, template]);
4297
+ const result = source && column ? template.build(source.source, column) : "";
4298
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4299
+ /* @__PURE__ */ jsx(DialogTitle, {
4300
+ sx: { fontSize: 14 },
4301
+ children: "Importar template"
4302
+ }),
4303
+ /* @__PURE__ */ jsxs(DialogContent, {
4304
+ sx: {
4305
+ display: "flex",
4306
+ flexDirection: "column",
4307
+ gap: 1.5
4308
+ },
4309
+ children: [
4310
+ /* @__PURE__ */ jsx(TextField, {
4311
+ select: true,
4312
+ size: "small",
4313
+ label: "template",
4314
+ value: template.id,
4315
+ onChange: (event) => setTemplateId(event.target.value),
4316
+ helperText: template.description,
4317
+ sx: { mt: 1 },
4318
+ children: QUERY_TEMPLATES.map((item) => /* @__PURE__ */ jsx(MenuItem, {
4319
+ value: item.id,
4320
+ children: item.label
4321
+ }, item.id))
4322
+ }),
4323
+ /* @__PURE__ */ jsx(Autocomplete, {
4324
+ size: "small",
4325
+ options: sources,
4326
+ value: source,
4327
+ onChange: (_, value) => {
4328
+ setSource(value);
4329
+ setColumn(null);
4330
+ },
4331
+ groupBy: (option) => option.group,
4332
+ getOptionLabel: (option) => option.name,
4333
+ isOptionEqualToValue: (option, value) => option.key === value.key,
4334
+ noOptionsText: "Nenhuma tabela",
4335
+ renderOption: ({ key, ...props }, option) => /* @__PURE__ */ jsxs(Box, {
4336
+ component: "li",
4337
+ ...props,
4338
+ sx: {
4339
+ gap: 2,
4340
+ fontFamily: MONO,
4341
+ fontSize: 12
4342
+ },
4343
+ children: [option.name, /* @__PURE__ */ jsxs(Box, {
4344
+ component: "span",
4345
+ sx: {
4346
+ ml: "auto",
4347
+ color: "text.secondary",
4348
+ fontSize: 11
4349
+ },
4350
+ children: [option.columns.length, " colunas"]
4351
+ })]
4352
+ }, key),
4353
+ renderInput: (params) => /* @__PURE__ */ jsx(TextField, {
4354
+ ...params,
4355
+ label: "tabela"
4356
+ })
4357
+ }),
4358
+ source && /* @__PURE__ */ jsx(Autocomplete, {
4359
+ size: "small",
4360
+ options: columns.map((item) => item.name),
4361
+ value: column,
4362
+ onChange: (_, value) => setColumn(value),
4363
+ noOptionsText: source.source.kind === "model" ? "Sem colunas: valide o SQL dessa tabela" : "Nenhuma coluna",
4364
+ renderOption: ({ key, ...props }, name) => /* @__PURE__ */ jsxs(Box, {
4365
+ component: "li",
4366
+ ...props,
4367
+ sx: {
4368
+ gap: 2,
4369
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4370
+ fontSize: 12
4371
+ },
4372
+ children: [name, /* @__PURE__ */ jsx(Box, {
4373
+ component: "span",
4374
+ sx: {
4375
+ ml: "auto",
4376
+ color: "text.secondary",
4377
+ fontSize: 11
4378
+ },
4379
+ children: columns.find((item) => item.name === name)?.type
4380
+ })]
4381
+ }, key),
4382
+ renderInput: (params) => /* @__PURE__ */ jsx(TextField, {
4383
+ ...params,
4384
+ label: template.columnLabel
4385
+ })
4386
+ }),
4387
+ result ? /* @__PURE__ */ jsx(Box, {
4388
+ component: "pre",
4389
+ sx: {
4390
+ m: 0,
4391
+ p: 1,
4392
+ border: 1,
4393
+ borderColor: "divider",
4394
+ borderRadius: 1,
4395
+ fontFamily: MONO,
4396
+ fontSize: 12,
4397
+ maxHeight: 280,
4398
+ overflow: "auto"
4399
+ },
4400
+ children: /* @__PURE__ */ jsx(HighlightedSql, { code: result })
4401
+ }) : /* @__PURE__ */ jsx(Typography, {
4402
+ sx: {
4403
+ fontSize: 12,
4404
+ color: "text.secondary"
4405
+ },
4406
+ children: "Escolha a tabela e a coluna para ver o SQL."
4407
+ })
4408
+ ]
4409
+ }),
4410
+ /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
4411
+ onClick: onClose,
4412
+ children: "Cancelar"
4413
+ }), /* @__PURE__ */ jsx(Button, {
4414
+ variant: "contained",
4415
+ disabled: !result,
4416
+ onClick: () => {
4417
+ onApply(result);
4418
+ onClose();
4419
+ },
4420
+ title: "Substitui o SQL atual",
4421
+ children: "Usar no SQL"
4422
+ })] })
4423
+ ] });
4424
+ }
4425
+ //#endregion
4426
+ //#region src/devtools/SqlEditor.tsx
4427
+ /**
4428
+ * Campo de SQL com destaque de sintaxe e autocomplete de tabelas e colunas. As colunas vêm do
4429
+ * `information_schema` (tabelas/views do banco) e das colunas de cada tabela do modelo (lidas do banco).
4430
+ * O ⤢ abre o mesmo SQL num editor grande; com `queryTools`, ele traz "Criar query" (escolher tabela e
4431
+ * colunas do banco), "Importar template" (calendário etc.) e "Validar SQL" (executa no banco e
4432
+ * mostra uma amostra).
4433
+ *
4434
+ * O destaque é um `<pre>` colorido por baixo do `<textarea>` de texto transparente: mesma fonte,
4435
+ * mesmo recuo e mesma quebra de linha, com a rolagem sincronizada. O cursor, a seleção e o
4436
+ * autocomplete continuam sendo do textarea.
4437
+ */
4438
+ const PAD = {
4439
+ top: 8.5,
4440
+ right: 30,
4441
+ bottom: 8.5,
4442
+ left: 12
4443
+ };
4444
+ const LINE_HEIGHT = "18px";
4445
+ const SqlCompletionsContext = createContext({
4446
+ tables: [],
4447
+ modelColumns: {}
4448
+ });
4449
+ const KEYWORDS = new Set("WHERE ON LEFT RIGHT INNER OUTER JOIN GROUP ORDER LIMIT USING AND OR AS SET HAVING UNION".split(" "));
4450
+ /** `alias -> tabela` a partir dos `FROM x a` / `JOIN x AS a` do texto. */
4451
+ function aliases(sql) {
4452
+ const map = /* @__PURE__ */ new Map();
4453
+ for (const match of sql.matchAll(/\b(?:FROM|JOIN)\s+`?(\w+)`?(?:\s+(?:AS\s+)?`?(\w+)`?)?/gi)) {
4454
+ const [, table, alias] = match;
4455
+ map.set(table.toLowerCase(), table);
4456
+ if (alias && !KEYWORDS.has(alias.toUpperCase())) map.set(alias.toLowerCase(), table);
4457
+ }
4458
+ return map;
4459
+ }
4460
+ function suggest(sql, caret, completions) {
4461
+ const before = sql.slice(0, caret);
4462
+ const tables = new Map(completions.tables.map((table) => [table.name.toLowerCase(), table]));
4463
+ const columnsOf = (table) => tables.get(table.toLowerCase())?.columns.map((column) => ({
4464
+ text: column.name,
4465
+ detail: column.type
4466
+ })) ?? [];
4467
+ let prefix;
4468
+ let candidates;
4469
+ const qualified = /([A-Za-z_]\w*)\.(\w*)$/.exec(before);
4470
+ if (qualified) {
4471
+ const [, qualifier, typed] = qualified;
4472
+ prefix = typed;
4473
+ if (completions.modelColumns[qualifier]) candidates = completions.modelColumns[qualifier].map((text) => ({
4474
+ text,
4475
+ detail: qualifier
4476
+ }));
4477
+ else candidates = columnsOf(aliases(sql).get(qualifier.toLowerCase()) ?? qualifier);
4478
+ } else {
4479
+ const word = /[A-Za-z_]\w*$/.exec(before);
4480
+ if (!word || word[0].length < 2) return null;
4481
+ prefix = word[0];
4482
+ const referenced = [...new Set(aliases(sql).values())];
4483
+ candidates = [
4484
+ ...Object.keys(completions.modelColumns).map((text) => ({
4485
+ text,
4486
+ detail: "tabela do modelo"
4487
+ })),
4488
+ ...completions.tables.map((table) => ({
4489
+ text: table.name,
4490
+ detail: table.type
4491
+ })),
4492
+ ...referenced.flatMap(columnsOf)
4493
+ ];
4494
+ }
4495
+ const lower = prefix.toLowerCase();
4496
+ const seen = /* @__PURE__ */ new Set();
4497
+ const items = candidates.filter((item) => {
4498
+ const text = item.text.toLowerCase();
4499
+ if (seen.has(text) || text === lower || !text.includes(lower)) return false;
4500
+ seen.add(text);
4501
+ return true;
4502
+ }).sort((a, b) => Number(!a.text.toLowerCase().startsWith(lower)) - Number(!b.text.toLowerCase().startsWith(lower)) || a.text.localeCompare(b.text)).slice(0, 8);
4503
+ return items.length ? {
4504
+ from: caret - prefix.length,
4505
+ items
4506
+ } : null;
4507
+ }
4508
+ function SqlEditor({ value, onChange, label, placeholder = "SQL", minRows, maxRows = 6, error, warning, expandable = true, queryTools = false }) {
4509
+ const completions = useContext(SqlCompletionsContext);
4510
+ const anchor = useRef(null);
4511
+ const input = useRef(null);
4512
+ const highlight = useRef(null);
4513
+ const [expanded, setExpanded] = useState(false);
4514
+ const [building, setBuilding] = useState(false);
4515
+ const [importing, setImporting] = useState(false);
4516
+ const [test, setTest] = useState(null);
4517
+ const [testing, setTesting] = useState(false);
4518
+ const currentTest = test?.sql === value ? test : null;
4519
+ const testSql = completions.testSql;
4520
+ const runTest = async () => {
4521
+ if (!testSql) return;
4522
+ const sql = value;
4523
+ setTesting(true);
4524
+ try {
4525
+ setTest({
4526
+ sql,
4527
+ result: await testSql(sql)
4528
+ });
4529
+ } catch (error) {
4530
+ setTest({
4531
+ sql,
4532
+ error: errorMessage(error)
4533
+ });
4534
+ } finally {
4535
+ setTesting(false);
4536
+ }
4537
+ };
4538
+ const [state, setState] = useState(null);
4539
+ const accept = (item) => {
4540
+ const element = input.current;
4541
+ if (!element || !state) return;
4542
+ const caret = element.selectionStart;
4543
+ onChange(value.slice(0, state.from) + item.text + value.slice(caret));
4544
+ setState(null);
4545
+ const position = state.from + item.text.length;
4546
+ requestAnimationFrame(() => {
4547
+ element.focus();
4548
+ element.setSelectionRange(position, position);
4549
+ });
4550
+ };
4551
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4552
+ /* @__PURE__ */ jsx(TextField, {
4553
+ ref: anchor,
4554
+ inputRef: input,
4555
+ size: "small",
4556
+ fullWidth: true,
4557
+ multiline: true,
4558
+ label,
4559
+ minRows,
4560
+ maxRows,
4561
+ value,
4562
+ placeholder,
4563
+ error: !value.trim() || !!error,
4564
+ helperText: error ?? warning,
4565
+ onChange: (event) => {
4566
+ onChange(event.target.value);
4567
+ const found = suggest(event.target.value, event.target.selectionStart ?? 0, completions);
4568
+ setState(found && {
4569
+ ...found,
4570
+ index: 0
4571
+ });
4572
+ },
4573
+ onKeyDown: (event) => {
4574
+ if (!state) return;
4575
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
4576
+ event.preventDefault();
4577
+ const step = event.key === "ArrowDown" ? 1 : -1;
4578
+ setState({
4579
+ ...state,
4580
+ index: (state.index + step + state.items.length) % state.items.length
4581
+ });
4582
+ } else if (event.key === "Enter" || event.key === "Tab") {
4583
+ event.preventDefault();
4584
+ accept(state.items[state.index]);
4585
+ } else if (event.key === "Escape") {
4586
+ event.stopPropagation();
4587
+ setState(null);
4588
+ }
4589
+ },
4590
+ onBlur: () => setState(null),
4591
+ slotProps: {
4592
+ htmlInput: {
4593
+ spellCheck: false,
4594
+ onScroll: (event) => {
4595
+ if (highlight.current) highlight.current.scrollTop = event.currentTarget.scrollTop;
4596
+ }
4597
+ },
4598
+ input: { endAdornment: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Box, {
4599
+ component: "pre",
4600
+ ref: highlight,
4601
+ "aria-hidden": true,
4602
+ sx: {
4603
+ ...textSx,
4604
+ m: 0,
4605
+ p: 0,
4606
+ position: "absolute",
4607
+ top: PAD.top,
4608
+ right: PAD.right,
4609
+ bottom: PAD.bottom,
4610
+ left: PAD.left,
4611
+ overflow: "hidden",
4612
+ pointerEvents: "none",
4613
+ color: "text.primary"
4614
+ },
4615
+ children: [/* @__PURE__ */ jsx(HighlightedSql, { code: value }), "\n"]
4616
+ }), expandable && /* @__PURE__ */ jsx(Box, {
4617
+ component: "button",
4618
+ type: "button",
4619
+ title: "Expandir o SQL",
4620
+ "aria-label": "Expandir o SQL",
4621
+ onClick: () => setExpanded(true),
4622
+ sx: {
4623
+ position: "absolute",
4624
+ top: 4,
4625
+ right: 4,
4626
+ zIndex: 2,
4627
+ border: 0,
4628
+ borderRadius: .5,
4629
+ bgcolor: "transparent",
4630
+ color: "text.secondary",
4631
+ cursor: "pointer",
4004
4632
  fontSize: 14,
4005
4633
  lineHeight: 1,
4006
4634
  p: .25,
@@ -4046,25 +4674,90 @@ function SqlEditor({ value, onChange, label, placeholder = "SQL", minRows, maxRo
4046
4674
  sx: { fontSize: 14 },
4047
4675
  children: label ?? "SQL"
4048
4676
  }),
4049
- /* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsx(Box, {
4050
- sx: { pt: 1 },
4051
- children: /* @__PURE__ */ jsx(SqlEditor, {
4052
- value,
4053
- onChange,
4054
- placeholder,
4055
- minRows: 20,
4056
- maxRows: 36,
4057
- error,
4058
- warning,
4059
- expandable: false
4677
+ /* @__PURE__ */ jsxs(DialogContent, { children: [
4678
+ /* @__PURE__ */ jsx(Box, {
4679
+ sx: { pt: 1 },
4680
+ children: /* @__PURE__ */ jsx(SqlEditor, {
4681
+ value,
4682
+ onChange,
4683
+ placeholder,
4684
+ minRows: 20,
4685
+ maxRows: 36,
4686
+ error,
4687
+ warning,
4688
+ expandable: false
4689
+ })
4690
+ }),
4691
+ currentTest?.error && /* @__PURE__ */ jsx(Alert, {
4692
+ severity: "error",
4693
+ sx: {
4694
+ mt: 1,
4695
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4696
+ fontSize: 12
4697
+ },
4698
+ children: currentTest.error
4699
+ }),
4700
+ currentTest?.result && /* @__PURE__ */ jsxs(Box, {
4701
+ sx: {
4702
+ mt: 1,
4703
+ display: "flex",
4704
+ flexDirection: "column",
4705
+ gap: 1
4706
+ },
4707
+ children: [/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Chip, {
4708
+ color: "success",
4709
+ size: "small",
4710
+ label: `SQL válido · ${currentTest.result.columns.length} colunas · ${currentTest.result.durationMs} ms`
4711
+ }) }), /* @__PURE__ */ jsx(ResultTable, {
4712
+ rows: currentTest.result.rows,
4713
+ maxHeight: 240
4714
+ })]
4060
4715
  })
4061
- }) }),
4062
- /* @__PURE__ */ jsx(DialogActions, { children: /* @__PURE__ */ jsx(Button, {
4063
- onClick: () => setExpanded(false),
4064
- children: "Fechar"
4065
- }) })
4716
+ ] }),
4717
+ /* @__PURE__ */ jsxs(DialogActions, { children: [
4718
+ queryTools && /* @__PURE__ */ jsxs(Fragment, { children: [
4719
+ /* @__PURE__ */ jsx(Button, {
4720
+ onClick: () => setBuilding(true),
4721
+ title: "Escolher a tabela e as colunas",
4722
+ children: "Criar query"
4723
+ }),
4724
+ /* @__PURE__ */ jsx(Button, {
4725
+ onClick: () => setImporting(true),
4726
+ title: "Calendário e outras queries prontas",
4727
+ children: "Importar template"
4728
+ }),
4729
+ testSql && /* @__PURE__ */ jsx(Button, {
4730
+ disabled: !value.trim() || testing,
4731
+ onClick: runTest,
4732
+ title: "Executa o SQL no banco e mostra as primeiras linhas",
4733
+ children: testing ? "Validando…" : "Validar SQL"
4734
+ })
4735
+ ] }),
4736
+ /* @__PURE__ */ jsx(Box, { sx: { flex: 1 } }),
4737
+ /* @__PURE__ */ jsx(Button, {
4738
+ onClick: () => setExpanded(false),
4739
+ children: "Fechar"
4740
+ })
4741
+ ] })
4066
4742
  ]
4067
4743
  }),
4744
+ queryTools && /* @__PURE__ */ jsx(QueryBuilder, {
4745
+ open: building,
4746
+ onClose: () => setBuilding(false),
4747
+ tables: completions.tables,
4748
+ sql: value,
4749
+ onApply: onChange
4750
+ }),
4751
+ queryTools && /* @__PURE__ */ jsx(QueryTemplates, {
4752
+ open: importing,
4753
+ onClose: () => setImporting(false),
4754
+ tables: completions.tables,
4755
+ modelTables: Object.fromEntries(Object.entries(completions.modelSql ?? {}).map(([name, sql]) => [name, {
4756
+ sql,
4757
+ columns: completions.modelColumns[name] ?? []
4758
+ }])),
4759
+ onApply: onChange
4760
+ }),
4068
4761
  /* @__PURE__ */ jsx(Popper, {
4069
4762
  open: !!state,
4070
4763
  anchorEl: anchor.current,
@@ -4524,6 +5217,28 @@ function DimensionSource({ row, editor, columns, error, warning, onPatch }) {
4524
5217
  }
4525
5218
  //#endregion
4526
5219
  //#region src/devtools/ModelDiagram.tsx
5220
+ const TARGET_NAME = {
5221
+ table: "tabela",
5222
+ dimension: "dimensão",
5223
+ measure: "medida"
5224
+ };
5225
+ /** Põe uma medida calculada (fórmula) nova, com nome livre, no fim; devolve o `uid` dela. */
5226
+ function addFormulaTo(setEditor) {
5227
+ const uid = newUid();
5228
+ setEditor((current) => {
5229
+ const taken = [...current.dimensions, ...current.measures].map((item) => item.name);
5230
+ const measure = emptyMeasure({
5231
+ uid,
5232
+ type: "formula",
5233
+ name: freeName("novaMedida", taken)
5234
+ });
5235
+ return {
5236
+ ...current,
5237
+ measures: [...current.measures, measure]
5238
+ };
5239
+ });
5240
+ return uid;
5241
+ }
4527
5242
  const CARD_WIDTH = 230;
4528
5243
  const HEADER = 34;
4529
5244
  const ROW = 22;
@@ -4690,6 +5405,12 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4690
5405
  });
4691
5406
  const [selection, setSelection] = useState(null);
4692
5407
  const [panel, setPanel] = useState(loadPanel);
5408
+ const [renaming, setRenaming] = useState(null);
5409
+ const [menu, setMenu] = useState(null);
5410
+ const closeMenu = () => setMenu((current) => current && {
5411
+ ...current,
5412
+ open: false
5413
+ });
4693
5414
  useEffect(() => {
4694
5415
  try {
4695
5416
  localStorage.setItem(PANEL_KEY, JSON.stringify(panel));
@@ -4810,15 +5531,16 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4810
5531
  const rect = canvas.current?.getBoundingClientRect();
4811
5532
  return toWorld((rect?.left ?? 0) + (rect?.width ?? 600) / 2 - CARD_WIDTH / 2, (rect?.top ?? 0) + (rect?.height ?? 400) / 3);
4812
5533
  };
4813
- const addTable = () => {
5534
+ /** `at`: canto do card (o ponto do botão direito); sem ele, perto do centro da tela. */
5535
+ const addTable = (at) => {
4814
5536
  const uid = newUid();
4815
5537
  const name = freeName("novaTabela", editor.tables.map((table) => table.name));
4816
- const center = visibleCenter();
5538
+ const position = at ?? visibleCenter();
4817
5539
  setEditor((current) => ({
4818
5540
  ...current,
4819
5541
  positions: {
4820
5542
  ...current.positions,
4821
- [uid]: center
5543
+ [uid]: position
4822
5544
  },
4823
5545
  tables: [...current.tables, {
4824
5546
  uid,
@@ -4834,6 +5556,20 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4834
5556
  uid
4835
5557
  });
4836
5558
  };
5559
+ const organize = () => {
5560
+ const layout = autoLayout(nodes, sizes);
5561
+ setPositions((current) => Object.fromEntries(Object.entries(layout).map(([id, point]) => [id, {
5562
+ ...current[id],
5563
+ ...point
5564
+ }])));
5565
+ fitToScreen(layout);
5566
+ };
5567
+ const addFormula = () => {
5568
+ setSelection({
5569
+ kind: "measure",
5570
+ uid: addFormulaTo(setEditor)
5571
+ });
5572
+ };
4837
5573
  const removeTable = (uid) => {
4838
5574
  setEditor((current) => ({
4839
5575
  ...current,
@@ -4842,6 +5578,19 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4842
5578
  }));
4843
5579
  setSelection(null);
4844
5580
  };
5581
+ const remove = (target) => {
5582
+ if (target.kind === "table") return removeTable(target.uid);
5583
+ const list = {
5584
+ relationship: "relationships",
5585
+ dimension: "dimensions",
5586
+ measure: "measures"
5587
+ }[target.kind];
5588
+ setEditor((current) => ({
5589
+ ...current,
5590
+ [list]: current[list].filter((row) => row.uid !== target.uid)
5591
+ }));
5592
+ setSelection((current) => current?.uid === target.uid ? null : current);
5593
+ };
4845
5594
  const commitRename = (uid, name) => setEditor((current) => renameTable(current, uid, name));
4846
5595
  const connect = (from, fromColumn, to, toColumn) => {
4847
5596
  if (from.uid === to.uid) return;
@@ -4902,20 +5651,7 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4902
5651
  });
4903
5652
  };
4904
5653
  const removeSelection = () => {
4905
- if (!selection) return;
4906
- if (selection.kind === "table") removeTable(selection.uid);
4907
- else {
4908
- const list = {
4909
- relationship: "relationships",
4910
- dimension: "dimensions",
4911
- measure: "measures"
4912
- }[selection.kind];
4913
- setEditor((current) => ({
4914
- ...current,
4915
- [list]: current[list].filter((row) => row.uid !== selection.uid)
4916
- }));
4917
- setSelection(null);
4918
- }
5654
+ if (selection) remove(selection);
4919
5655
  };
4920
5656
  const startPan = (event) => {
4921
5657
  if (event.button !== 0) return;
@@ -5103,305 +5839,392 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
5103
5839
  height: "100%",
5104
5840
  minHeight: 0
5105
5841
  },
5106
- children: [/* @__PURE__ */ jsxs(Box, {
5107
- ref: canvas,
5108
- tabIndex: -1,
5109
- onMouseDown: startPan,
5110
- onMouseDownCapture: () => canvas.current?.focus({ preventScroll: true }),
5111
- onKeyDown: (event) => {
5112
- if ((event.key === "Delete" || event.key === "Backspace") && event.target === canvas.current) removeSelection();
5113
- },
5114
- onWheel,
5115
- sx: {
5116
- outline: "none",
5117
- flex: 1,
5118
- position: "relative",
5119
- overflow: "hidden",
5120
- cursor: "grab",
5121
- bgcolor: "#15171b",
5122
- backgroundImage: "radial-gradient(rgba(255,255,255,.07) 1px, transparent 1px)",
5123
- backgroundSize: `${20 * view.zoom}px ${20 * view.zoom}px`,
5124
- backgroundPosition: `${view.x}px ${view.y}px`
5125
- },
5126
- children: [
5127
- /* @__PURE__ */ jsxs(Box, {
5128
- sx: {
5129
- position: "absolute",
5130
- left: 0,
5131
- top: 0,
5132
- transformOrigin: "0 0",
5133
- transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`
5134
- },
5135
- children: [/* @__PURE__ */ jsxs("svg", {
5136
- width: 1,
5137
- height: 1,
5138
- style: {
5842
+ children: [
5843
+ /* @__PURE__ */ jsxs(Box, {
5844
+ ref: canvas,
5845
+ tabIndex: -1,
5846
+ onMouseDown: startPan,
5847
+ onMouseDownCapture: () => canvas.current?.focus({ preventScroll: true }),
5848
+ onKeyDown: (event) => {
5849
+ if ((event.key === "Delete" || event.key === "Backspace") && event.target === canvas.current) removeSelection();
5850
+ },
5851
+ onWheel,
5852
+ onContextMenu: (event) => {
5853
+ event.preventDefault();
5854
+ setMenu({
5855
+ open: true,
5856
+ x: event.clientX,
5857
+ y: event.clientY,
5858
+ target: null
5859
+ });
5860
+ },
5861
+ sx: {
5862
+ outline: "none",
5863
+ flex: 1,
5864
+ position: "relative",
5865
+ overflow: "hidden",
5866
+ cursor: "grab",
5867
+ bgcolor: "#15171b",
5868
+ backgroundImage: "radial-gradient(rgba(255,255,255,.07) 1px, transparent 1px)",
5869
+ backgroundSize: `${20 * view.zoom}px ${20 * view.zoom}px`,
5870
+ backgroundPosition: `${view.x}px ${view.y}px`
5871
+ },
5872
+ children: [
5873
+ /* @__PURE__ */ jsxs(Box, {
5874
+ sx: {
5875
+ position: "absolute",
5876
+ left: 0,
5877
+ top: 0,
5878
+ transformOrigin: "0 0",
5879
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`
5880
+ },
5881
+ children: [/* @__PURE__ */ jsxs("svg", {
5882
+ width: 1,
5883
+ height: 1,
5884
+ style: {
5885
+ position: "absolute",
5886
+ overflow: "visible",
5887
+ pointerEvents: "none"
5888
+ },
5889
+ "aria-hidden": true,
5890
+ children: [lines.map((line) => /* @__PURE__ */ jsx(RelationshipLine, {
5891
+ line,
5892
+ hovered: hoveredEdge === line.id,
5893
+ onHover: (hovered) => setHoveredEdge(hovered ? line.id : null)
5894
+ }, line.id)), linkPreview && /* @__PURE__ */ jsx("path", {
5895
+ d: linkPreview,
5896
+ fill: "none",
5897
+ stroke: "#F58220",
5898
+ strokeWidth: 2,
5899
+ strokeDasharray: "5 4"
5900
+ })]
5901
+ }), nodes.map((node) => /* @__PURE__ */ jsx(TableCard, {
5902
+ node,
5903
+ position: placed(node),
5904
+ size: sizeOf(node),
5905
+ collapsed: !!collapsed[node.id],
5906
+ selected: !!isSelected(node),
5907
+ linking: !!linking && linking.node.id !== node.id,
5908
+ selectedField: selection?.kind === "dimension" || selection?.kind === "measure" ? selection.uid : null,
5909
+ onToggle: () => setCollapsed((current) => ({
5910
+ ...current,
5911
+ [node.id]: !current[node.id]
5912
+ })),
5913
+ onScroll: (top) => setScrolls((current) => ({
5914
+ ...current,
5915
+ [node.id]: top
5916
+ })),
5917
+ onHeaderDown: (event) => startMove(event, node),
5918
+ onResizeDown: (event, edges) => startResize(event, node, edges),
5919
+ onResizeReset: () => resetSize(node),
5920
+ onColumnDown: (event, column) => startLink(event, node, column),
5921
+ onFieldClick: (item) => setSelection({
5922
+ kind: item.kind,
5923
+ uid: item.uid
5924
+ }),
5925
+ onAddField: (kind, column) => addField(kind, node.name, column),
5926
+ renaming: renaming === node.uid,
5927
+ onRenameStart: () => setRenaming(node.uid),
5928
+ onRename: (name) => {
5929
+ setRenaming(null);
5930
+ if (name !== null && name !== node.name) commitRename(node.uid, name);
5931
+ },
5932
+ onContextMenu: (event, target) => {
5933
+ event.preventDefault();
5934
+ event.stopPropagation();
5935
+ setMenu({
5936
+ open: true,
5937
+ x: event.clientX,
5938
+ y: event.clientY,
5939
+ target
5940
+ });
5941
+ }
5942
+ }, node.id))]
5943
+ }),
5944
+ /* @__PURE__ */ jsxs(Box, {
5945
+ onMouseDown: (event) => event.stopPropagation(),
5946
+ onContextMenu: (event) => event.stopPropagation(),
5947
+ sx: {
5948
+ position: "absolute",
5949
+ top: 8,
5950
+ left: 8,
5951
+ right: 8,
5952
+ display: "flex",
5953
+ gap: .75,
5954
+ flexWrap: "wrap",
5955
+ alignItems: "center",
5956
+ pointerEvents: "none",
5957
+ "& > *": { pointerEvents: "auto" }
5958
+ },
5959
+ children: [
5960
+ /* @__PURE__ */ jsx(Button, {
5961
+ size: "small",
5962
+ variant: "outlined",
5963
+ onClick: () => addTable(),
5964
+ children: "+ Tabela"
5965
+ }),
5966
+ /* @__PURE__ */ jsx(Button, {
5967
+ size: "small",
5968
+ onClick: onLoadColumns,
5969
+ disabled: loadingColumns,
5970
+ title: "Lê uma linha de cada tabela para listar as colunas",
5971
+ sx: { bgcolor: "#1b1e23" },
5972
+ children: loadingColumns ? "Lendo colunas…" : "Recarregar colunas"
5973
+ }),
5974
+ /* @__PURE__ */ jsxs(Box, {
5975
+ sx: {
5976
+ ml: "auto",
5977
+ display: "flex",
5978
+ gap: .5,
5979
+ alignItems: "center"
5980
+ },
5981
+ children: [
5982
+ /* @__PURE__ */ jsx(Button, {
5983
+ size: "small",
5984
+ onClick: organize,
5985
+ title: "Dimensões em cima, fatos embaixo (troca as posições salvas)",
5986
+ sx: { bgcolor: "#1b1e23" },
5987
+ children: "Organizar"
5988
+ }),
5989
+ /* @__PURE__ */ jsx(Button, {
5990
+ size: "small",
5991
+ onClick: () => fitToScreen(),
5992
+ sx: { bgcolor: "#1b1e23" },
5993
+ children: "Ajustar à tela"
5994
+ }),
5995
+ /* @__PURE__ */ jsx(IconButton, {
5996
+ size: "small",
5997
+ "aria-label": "Diminuir zoom",
5998
+ onClick: () => setView((current) => ({
5999
+ ...current,
6000
+ zoom: Math.max(.3, current.zoom - .1)
6001
+ })),
6002
+ children: "−"
6003
+ }),
6004
+ /* @__PURE__ */ jsxs(Typography, {
6005
+ sx: {
6006
+ fontSize: 11,
6007
+ fontFamily: MONO,
6008
+ width: 36,
6009
+ textAlign: "center"
6010
+ },
6011
+ children: [Math.round(view.zoom * 100), "%"]
6012
+ }),
6013
+ /* @__PURE__ */ jsx(IconButton, {
6014
+ size: "small",
6015
+ "aria-label": "Aumentar zoom",
6016
+ onClick: () => setView((current) => ({
6017
+ ...current,
6018
+ zoom: Math.min(2, current.zoom + .1)
6019
+ })),
6020
+ children: "+"
6021
+ })
6022
+ ]
6023
+ })
6024
+ ]
6025
+ }),
6026
+ !nodes.length && /* @__PURE__ */ jsx(Box, {
6027
+ sx: {
6028
+ position: "absolute",
6029
+ inset: 0,
6030
+ display: "flex",
6031
+ alignItems: "center",
6032
+ justifyContent: "center",
6033
+ pointerEvents: "none",
6034
+ color: "text.secondary",
6035
+ fontSize: 13,
6036
+ textAlign: "center",
6037
+ px: 4
6038
+ },
6039
+ children: "Modelo vazio. Adicione as tabelas dimensão (cadastros: navio, calendário…) e fato (lançamentos: pesagens, custos…) e arraste a coluna da fato até a chave da dimensão para relacioná-las."
6040
+ }),
6041
+ !!nodes.length && /* @__PURE__ */ jsx(Typography, {
6042
+ sx: {
5139
6043
  position: "absolute",
5140
- overflow: "visible",
6044
+ left: 10,
6045
+ bottom: 6,
6046
+ fontSize: 11,
6047
+ color: "text.disabled",
5141
6048
  pointerEvents: "none"
5142
6049
  },
5143
- "aria-hidden": true,
5144
- children: [lines.map((line) => /* @__PURE__ */ jsx(RelationshipLine, {
5145
- line,
5146
- hovered: hoveredEdge === line.id,
5147
- onHover: (hovered) => setHoveredEdge(hovered ? line.id : null)
5148
- }, line.id)), linkPreview && /* @__PURE__ */ jsx("path", {
5149
- d: linkPreview,
5150
- fill: "none",
5151
- stroke: "#F58220",
5152
- strokeWidth: 2,
5153
- strokeDasharray: "5 4"
5154
- })]
5155
- }), nodes.map((node) => /* @__PURE__ */ jsx(TableCard, {
5156
- node,
5157
- position: placed(node),
5158
- size: sizeOf(node),
5159
- collapsed: !!collapsed[node.id],
5160
- selected: !!isSelected(node),
5161
- linking: !!linking && linking.node.id !== node.id,
5162
- selectedField: selection?.kind === "dimension" || selection?.kind === "measure" ? selection.uid : null,
5163
- onToggle: () => setCollapsed((current) => ({
5164
- ...current,
5165
- [node.id]: !current[node.id]
5166
- })),
5167
- onScroll: (top) => setScrolls((current) => ({
5168
- ...current,
5169
- [node.id]: top
5170
- })),
5171
- onHeaderDown: (event) => startMove(event, node),
5172
- onResizeDown: (event, edges) => startResize(event, node, edges),
5173
- onResizeReset: () => resetSize(node),
5174
- onColumnDown: (event, column) => startLink(event, node, column),
5175
- onFieldClick: (item) => setSelection({
5176
- kind: item.kind,
5177
- uid: item.uid
5178
- }),
5179
- onAddField: (kind, column) => addField(kind, node.name, column)
5180
- }, node.id))]
5181
- }),
5182
- /* @__PURE__ */ jsxs(Box, {
5183
- onMouseDown: (event) => event.stopPropagation(),
5184
- sx: {
5185
- position: "absolute",
5186
- top: 8,
5187
- left: 8,
5188
- right: 8,
5189
- display: "flex",
5190
- gap: .75,
5191
- flexWrap: "wrap",
5192
- alignItems: "center",
5193
- pointerEvents: "none",
5194
- "& > *": { pointerEvents: "auto" }
5195
- },
5196
- children: [
5197
- /* @__PURE__ */ jsx(Button, {
5198
- size: "small",
5199
- variant: "outlined",
5200
- onClick: addTable,
5201
- children: "+ Tabela"
5202
- }),
5203
- /* @__PURE__ */ jsx(Button, {
5204
- size: "small",
5205
- onClick: onLoadColumns,
5206
- disabled: loadingColumns,
5207
- title: "Lê uma linha de cada tabela para listar as colunas",
5208
- sx: { bgcolor: "#1b1e23" },
5209
- children: loadingColumns ? "Lendo colunas…" : "Recarregar colunas"
5210
- }),
5211
- /* @__PURE__ */ jsxs(Box, {
5212
- sx: {
5213
- ml: "auto",
5214
- display: "flex",
5215
- gap: .5,
5216
- alignItems: "center"
5217
- },
5218
- children: [
5219
- /* @__PURE__ */ jsx(Button, {
5220
- size: "small",
5221
- onClick: () => {
5222
- const layout = autoLayout(nodes, sizes);
5223
- setPositions((current) => Object.fromEntries(Object.entries(layout).map(([id, point]) => [id, {
5224
- ...current[id],
5225
- ...point
5226
- }])));
5227
- fitToScreen(layout);
5228
- },
5229
- title: "Dimensões em cima, fatos embaixo (troca as posições salvas)",
5230
- sx: { bgcolor: "#1b1e23" },
5231
- children: "Organizar"
5232
- }),
5233
- /* @__PURE__ */ jsx(Button, {
5234
- size: "small",
5235
- onClick: () => fitToScreen(),
5236
- sx: { bgcolor: "#1b1e23" },
5237
- children: "Ajustar à tela"
5238
- }),
5239
- /* @__PURE__ */ jsx(IconButton, {
5240
- size: "small",
5241
- "aria-label": "Diminuir zoom",
5242
- onClick: () => setView((current) => ({
5243
- ...current,
5244
- zoom: Math.max(.3, current.zoom - .1)
5245
- })),
5246
- children: "−"
5247
- }),
5248
- /* @__PURE__ */ jsxs(Typography, {
5249
- sx: {
5250
- fontSize: 11,
5251
- fontFamily: MONO,
5252
- width: 36,
5253
- textAlign: "center"
5254
- },
5255
- children: [Math.round(view.zoom * 100), "%"]
5256
- }),
5257
- /* @__PURE__ */ jsx(IconButton, {
5258
- size: "small",
5259
- "aria-label": "Aumentar zoom",
5260
- onClick: () => setView((current) => ({
5261
- ...current,
5262
- zoom: Math.min(2, current.zoom + .1)
5263
- })),
5264
- children: "+"
5265
- })
5266
- ]
5267
- })
5268
- ]
5269
- }),
5270
- !nodes.length && /* @__PURE__ */ jsx(Box, {
5271
- sx: {
5272
- position: "absolute",
5273
- inset: 0,
5274
- display: "flex",
5275
- alignItems: "center",
5276
- justifyContent: "center",
5277
- pointerEvents: "none",
5278
- color: "text.secondary",
5279
- fontSize: 13,
5280
- textAlign: "center",
5281
- px: 4
6050
+ children: "Arraste uma coluna até a coluna de outra tabela para relacionar · +◇/+Σ na coluna cria dimensão/medida · Duplo clique no nome renomeia · Botão direito: editar/remover (no fundo: nova tabela/medida, recarregar, organizar) · Delete remove · Ctrl+roda: zoom"
6051
+ })
6052
+ ]
6053
+ }),
6054
+ /* @__PURE__ */ jsx(Menu, {
6055
+ open: !!menu?.open,
6056
+ onClose: closeMenu,
6057
+ anchorReference: "anchorPosition",
6058
+ anchorPosition: menu ? {
6059
+ top: menu.y,
6060
+ left: menu.x
6061
+ } : void 0,
6062
+ transitionDuration: {
6063
+ enter: 90,
6064
+ exit: 60
6065
+ },
6066
+ onContextMenu: (event) => event.preventDefault(),
6067
+ children: menu?.target ? [/* @__PURE__ */ jsxs(MenuItem, {
6068
+ dense: true,
6069
+ onClick: () => {
6070
+ setSelection(menu.target);
6071
+ closeMenu();
5282
6072
  },
5283
- children: "Modelo vazio. Adicione as tabelas dimensão (cadastros: navio, calendário…) e fato (lançamentos: pesagens, custos…) e arraste a coluna da fato até a chave da dimensão para relacioná-las."
5284
- }),
5285
- !!nodes.length && /* @__PURE__ */ jsx(Typography, {
5286
- sx: {
5287
- position: "absolute",
5288
- left: 10,
5289
- bottom: 6,
5290
- fontSize: 11,
5291
- color: "text.disabled",
5292
- pointerEvents: "none"
6073
+ children: ["Editar ", TARGET_NAME[menu.target.kind]]
6074
+ }, "edit"), /* @__PURE__ */ jsxs(MenuItem, {
6075
+ dense: true,
6076
+ sx: { color: "error.main" },
6077
+ onClick: () => {
6078
+ if (menu.target) remove(menu.target);
6079
+ closeMenu();
5293
6080
  },
5294
- children: "Arraste uma coluna até a coluna de outra tabela para relacionar · +◇/+Σ na coluna cria dimensão/medida · Delete remove · Ctrl+roda: zoom"
5295
- })
5296
- ]
5297
- }), panel.open ? /* @__PURE__ */ jsxs(Box, {
5298
- sx: {
5299
- width: panel.width,
5300
- flexShrink: 0,
5301
- borderLeft: 1,
5302
- borderColor: "divider",
5303
- position: "relative",
5304
- display: "flex",
5305
- flexDirection: "column",
5306
- minHeight: 0
5307
- },
5308
- children: [
5309
- /* @__PURE__ */ jsx(Box, {
5310
- onMouseDown: startPanelResize,
5311
- title: "Arraste para redimensionar",
5312
- sx: {
5313
- position: "absolute",
5314
- left: -3,
5315
- top: 0,
5316
- bottom: 0,
5317
- width: 6,
5318
- cursor: "ew-resize",
5319
- zIndex: 2,
5320
- "&:hover": {
5321
- bgcolor: "primary.main",
5322
- opacity: .4
6081
+ children: ["Remover ", TARGET_NAME[menu.target.kind]]
6082
+ }, "remove")] : [
6083
+ /* @__PURE__ */ jsx(MenuItem, {
6084
+ dense: true,
6085
+ onClick: () => {
6086
+ if (menu) addTable(toWorld(menu.x, menu.y));
6087
+ closeMenu();
6088
+ },
6089
+ children: "Nova Tabela"
6090
+ }, "table"),
6091
+ /* @__PURE__ */ jsx(MenuItem, {
6092
+ dense: true,
6093
+ onClick: () => {
6094
+ addFormula();
6095
+ closeMenu();
6096
+ },
6097
+ children: "Nova Medida Calculada"
6098
+ }, "formula"),
6099
+ /* @__PURE__ */ jsx(Divider, {}, "divider"),
6100
+ /* @__PURE__ */ jsx(MenuItem, {
6101
+ dense: true,
6102
+ disabled: loadingColumns,
6103
+ onClick: () => {
6104
+ onLoadColumns();
6105
+ closeMenu();
6106
+ },
6107
+ children: "Recarregar colunas"
6108
+ }, "columns"),
6109
+ /* @__PURE__ */ jsx(MenuItem, {
6110
+ dense: true,
6111
+ onClick: () => {
6112
+ organize();
6113
+ closeMenu();
6114
+ },
6115
+ children: "Organizar"
6116
+ }, "organize")
6117
+ ]
6118
+ }),
6119
+ panel.open ? /* @__PURE__ */ jsxs(Box, {
6120
+ sx: {
6121
+ width: panel.width,
6122
+ flexShrink: 0,
6123
+ borderLeft: 1,
6124
+ borderColor: "divider",
6125
+ position: "relative",
6126
+ display: "flex",
6127
+ flexDirection: "column",
6128
+ minHeight: 0
6129
+ },
6130
+ children: [
6131
+ /* @__PURE__ */ jsx(Box, {
6132
+ onMouseDown: startPanelResize,
6133
+ title: "Arraste para redimensionar",
6134
+ sx: {
6135
+ position: "absolute",
6136
+ left: -3,
6137
+ top: 0,
6138
+ bottom: 0,
6139
+ width: 6,
6140
+ cursor: "ew-resize",
6141
+ zIndex: 2,
6142
+ "&:hover": {
6143
+ bgcolor: "primary.main",
6144
+ opacity: .4
6145
+ }
5323
6146
  }
5324
- }
5325
- }),
5326
- /* @__PURE__ */ jsxs(Box, {
5327
- sx: {
5328
- display: "flex",
5329
- alignItems: "center",
5330
- px: 1.5,
5331
- py: .5,
5332
- borderBottom: 1,
5333
- borderColor: "divider"
5334
- },
5335
- children: [/* @__PURE__ */ jsx(Typography, {
6147
+ }),
6148
+ /* @__PURE__ */ jsxs(Box, {
5336
6149
  sx: {
5337
- fontSize: 12,
5338
- fontWeight: 700,
5339
- color: "text.secondary",
5340
- flex: 1
6150
+ display: "flex",
6151
+ alignItems: "center",
6152
+ px: 1.5,
6153
+ py: .5,
6154
+ borderBottom: 1,
6155
+ borderColor: "divider"
5341
6156
  },
5342
- children: "Propriedades"
5343
- }), /* @__PURE__ */ jsx(IconButton, {
5344
- size: "small",
5345
- "aria-label": "Recolher propriedades",
5346
- title: "Recolher",
5347
- onClick: () => setPanel((current) => ({
5348
- ...current,
5349
- open: false
5350
- })),
5351
- children: "⟩"
5352
- })]
5353
- }),
5354
- /* @__PURE__ */ jsx(Box, {
5355
- sx: {
5356
- overflow: "auto",
5357
- flex: 1,
5358
- p: 1.5
5359
- },
5360
- children: /* @__PURE__ */ jsx(Properties, {
5361
- editor,
5362
- selection,
5363
- columns,
5364
- errors,
5365
- warnings,
5366
- usage,
5367
- onSelect: setSelection,
5368
- onTry,
5369
- setEditor,
5370
- patchTable,
5371
- patchRelationship,
5372
- patchDimension,
5373
- patchMeasure,
5374
- removeTable,
5375
- commitRename,
5376
- addField,
5377
- unplaced
6157
+ children: [/* @__PURE__ */ jsx(Typography, {
6158
+ sx: {
6159
+ fontSize: 12,
6160
+ fontWeight: 700,
6161
+ color: "text.secondary",
6162
+ flex: 1
6163
+ },
6164
+ children: "Propriedades"
6165
+ }), /* @__PURE__ */ jsx(IconButton, {
6166
+ size: "small",
6167
+ "aria-label": "Recolher propriedades",
6168
+ title: "Recolher",
6169
+ onClick: () => setPanel((current) => ({
6170
+ ...current,
6171
+ open: false
6172
+ })),
6173
+ children: "⟩"
6174
+ })]
6175
+ }),
6176
+ /* @__PURE__ */ jsx(Box, {
6177
+ sx: {
6178
+ overflow: "auto",
6179
+ flex: 1,
6180
+ p: 1.5
6181
+ },
6182
+ children: /* @__PURE__ */ jsx(Properties, {
6183
+ editor,
6184
+ selection,
6185
+ columns,
6186
+ errors,
6187
+ warnings,
6188
+ usage,
6189
+ onSelect: setSelection,
6190
+ onTry,
6191
+ setEditor,
6192
+ patchTable,
6193
+ patchRelationship,
6194
+ patchDimension,
6195
+ patchMeasure,
6196
+ removeTable,
6197
+ commitRename,
6198
+ addField,
6199
+ unplaced
6200
+ })
5378
6201
  })
5379
- })
5380
- ]
5381
- }) : /* @__PURE__ */ jsx(Box, {
5382
- component: "button",
5383
- type: "button",
5384
- title: "Mostrar as propriedades",
5385
- onClick: () => setPanel((current) => ({
5386
- ...current,
5387
- open: true
5388
- })),
5389
- sx: {
5390
- width: 28,
5391
- flexShrink: 0,
5392
- border: 0,
5393
- borderLeft: 1,
5394
- borderColor: "divider",
5395
- bgcolor: "background.paper",
5396
- color: "text.secondary",
5397
- cursor: "pointer",
5398
- writingMode: "vertical-rl",
5399
- fontSize: 12,
5400
- py: 1,
5401
- "&:hover": { color: "primary.main" }
5402
- },
5403
- children: "⟨ Propriedades"
5404
- })]
6202
+ ]
6203
+ }) : /* @__PURE__ */ jsx(Box, {
6204
+ component: "button",
6205
+ type: "button",
6206
+ title: "Mostrar as propriedades",
6207
+ onClick: () => setPanel((current) => ({
6208
+ ...current,
6209
+ open: true
6210
+ })),
6211
+ sx: {
6212
+ width: 28,
6213
+ flexShrink: 0,
6214
+ border: 0,
6215
+ borderLeft: 1,
6216
+ borderColor: "divider",
6217
+ bgcolor: "background.paper",
6218
+ color: "text.secondary",
6219
+ cursor: "pointer",
6220
+ writingMode: "vertical-rl",
6221
+ fontSize: 12,
6222
+ py: 1,
6223
+ "&:hover": { color: "primary.main" }
6224
+ },
6225
+ children: "⟨ Propriedades"
6226
+ })
6227
+ ]
5405
6228
  });
5406
6229
  }
5407
6230
  /** Curva com tangentes horizontais (saída e chegada pelas laterais dos cards). */
@@ -5470,11 +6293,15 @@ function RelationshipLine({ line, hovered, onHover }) {
5470
6293
  ]
5471
6294
  });
5472
6295
  }
5473
- function TableCard({ node, position, size, collapsed, selected, linking, selectedField, onToggle, onScroll, onHeaderDown, onResizeDown, onResizeReset, onColumnDown, onFieldClick, onAddField }) {
6296
+ function TableCard({ node, position, size, collapsed, selected, linking, selectedField, onToggle, onScroll, onHeaderDown, onResizeDown, onResizeReset, onColumnDown, onFieldClick, onAddField, renaming, onRenameStart, onRename, onContextMenu }) {
5474
6297
  const style = ROLE_STYLE[node.role];
5475
6298
  const columnActions = ["dimension", "measure"];
5476
6299
  return /* @__PURE__ */ jsxs(Box, {
5477
6300
  onMouseDown: (event) => event.stopPropagation(),
6301
+ onContextMenu: (event) => onContextMenu(event, {
6302
+ kind: "table",
6303
+ uid: node.uid
6304
+ }),
5478
6305
  sx: {
5479
6306
  position: "absolute",
5480
6307
  left: position.x,
@@ -5515,7 +6342,12 @@ function TableCard({ node, position, size, collapsed, selected, linking, selecte
5515
6342
  },
5516
6343
  children: "▦"
5517
6344
  }),
5518
- /* @__PURE__ */ jsx(Typography, {
6345
+ renaming ? /* @__PURE__ */ jsx(TableNameInput, {
6346
+ value: node.name,
6347
+ onDone: onRename
6348
+ }) : /* @__PURE__ */ jsx(Typography, {
6349
+ onDoubleClick: onRenameStart,
6350
+ title: "Duplo clique para renomear",
5519
6351
  sx: {
5520
6352
  fontWeight: 700,
5521
6353
  fontSize: 13,
@@ -5643,6 +6475,10 @@ function TableCard({ node, position, size, collapsed, selected, linking, selecte
5643
6475
  }, `c:${item.name}`) : /* @__PURE__ */ jsxs(Box, {
5644
6476
  onMouseDown: (event) => event.stopPropagation(),
5645
6477
  onClick: () => onFieldClick(item),
6478
+ onContextMenu: (event) => onContextMenu(event, {
6479
+ kind: item.kind,
6480
+ uid: item.uid
6481
+ }),
5646
6482
  sx: {
5647
6483
  height: ROW,
5648
6484
  display: "flex",
@@ -5738,23 +6574,60 @@ const resizeHandleSx = {
5738
6574
  position: "absolute",
5739
6575
  zIndex: 1
5740
6576
  };
5741
- function PropertyBlock({ title, children }) {
5742
- return /* @__PURE__ */ jsxs(Box, {
5743
- sx: {
5744
- display: "flex",
5745
- flexDirection: "column",
5746
- gap: 1,
5747
- mb: 2
6577
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
6578
+ /** Nome da tabela editado no cabeçalho do card: Enter/sair confirma, Esc cancela. */
6579
+ function TableNameInput({ value, onDone }) {
6580
+ const [text, setText] = useState(value);
6581
+ const valid = IDENTIFIER.test(text);
6582
+ const done = useRef(false);
6583
+ const finish = (name) => {
6584
+ if (done.current) return;
6585
+ done.current = true;
6586
+ onDone(name);
6587
+ };
6588
+ return /* @__PURE__ */ jsx(Box, {
6589
+ component: "input",
6590
+ autoFocus: true,
6591
+ value: text,
6592
+ "aria-label": "Nome da tabela",
6593
+ onFocus: (event) => event.currentTarget.select(),
6594
+ onChange: (event) => setText(event.target.value),
6595
+ onMouseDown: (event) => event.stopPropagation(),
6596
+ onKeyDown: (event) => {
6597
+ event.stopPropagation();
6598
+ if (event.key === "Enter") finish(valid ? text : null);
6599
+ if (event.key === "Escape") finish(null);
5748
6600
  },
5749
- children: [/* @__PURE__ */ jsx(Typography, {
5750
- variant: "overline",
6601
+ onBlur: () => finish(valid ? text : null),
6602
+ sx: {
6603
+ flex: 1,
6604
+ minWidth: 0,
6605
+ font: "inherit",
6606
+ fontSize: 13,
6607
+ fontWeight: 700,
6608
+ color: "text.primary",
6609
+ bgcolor: "#1b1e23",
6610
+ border: "1px solid",
6611
+ borderColor: valid ? "primary.main" : "error.main",
6612
+ borderRadius: .5,
6613
+ px: .5,
6614
+ outline: "none"
6615
+ }
6616
+ });
6617
+ }
6618
+ /** Bloco do painel; com `collapseKey`, o título recolhe/expande (lembrado no navegador). */
6619
+ function PropertyBlock({ title, collapseKey, children }) {
6620
+ return /* @__PURE__ */ jsx(Section, {
6621
+ title,
6622
+ collapseKey,
6623
+ children: /* @__PURE__ */ jsx(Box, {
5751
6624
  sx: {
5752
- color: "text.secondary",
5753
- fontWeight: 700,
5754
- lineHeight: 1.5
6625
+ display: "flex",
6626
+ flexDirection: "column",
6627
+ gap: 1
5755
6628
  },
5756
- children: title
5757
- }), children]
6629
+ children
6630
+ })
5758
6631
  });
5759
6632
  }
5760
6633
  function ColumnSelect({ label, value, options, onChange }) {
@@ -5818,29 +6691,23 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
5818
6691
  return /* @__PURE__ */ jsxs(Fragment, { children: [
5819
6692
  /* @__PURE__ */ jsxs(PropertyBlock, {
5820
6693
  title: "Tabela",
5821
- children: [
5822
- /* @__PURE__ */ jsx(RenameField, {
5823
- value: table.name,
5824
- onCommit: (name) => commitRename(table.uid, name)
5825
- }, table.uid),
5826
- /* @__PURE__ */ jsx(ColumnSelect, {
5827
- label: "chave (key)",
5828
- value: table.key,
5829
- options: columnsOf(table.uid).length ? ["", ...columnsOf(table.uid)] : [],
5830
- onChange: (key) => patchTable(table.uid, { key })
5831
- }),
5832
- /* @__PURE__ */ jsx(SqlEditor, {
5833
- label: "sql",
5834
- minRows: 3,
5835
- maxRows: 12,
5836
- value: table.sql,
5837
- error: errors[`table:${table.name}`],
5838
- onChange: (sql) => patchTable(table.uid, { sql })
5839
- })
5840
- ]
6694
+ collapseKey: "diagram:table",
6695
+ children: [/* @__PURE__ */ jsx(RenameField, {
6696
+ value: table.name,
6697
+ onCommit: (name) => commitRename(table.uid, name)
6698
+ }, table.uid), /* @__PURE__ */ jsx(SqlEditor, {
6699
+ label: "sql",
6700
+ minRows: 3,
6701
+ maxRows: 12,
6702
+ value: table.sql,
6703
+ error: errors[`table:${table.name}`],
6704
+ onChange: (sql) => patchTable(table.uid, { sql }),
6705
+ queryTools: true
6706
+ })]
5841
6707
  }),
5842
6708
  /* @__PURE__ */ jsx(PropertyBlock, {
5843
6709
  title: "Dimensões (marque as colunas)",
6710
+ collapseKey: "diagram:dimensions",
5844
6711
  children: /* @__PURE__ */ jsx(DimensionPicker, {
5845
6712
  editor,
5846
6713
  setEditor,
@@ -5854,6 +6721,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
5854
6721
  }),
5855
6722
  /* @__PURE__ */ jsx(PropertyBlock, {
5856
6723
  title: "Dicionário",
6724
+ collapseKey: "diagram:dictionary",
5857
6725
  children: /* @__PURE__ */ jsx(DictionaryFields, {
5858
6726
  row: table,
5859
6727
  onPatch: (changes) => patchTable(table.uid, changes)
@@ -6040,6 +6908,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6040
6908
  }),
6041
6909
  /* @__PURE__ */ jsx(PropertyBlock, {
6042
6910
  title: "Dicionário",
6911
+ collapseKey: "diagram:dictionary",
6043
6912
  children: /* @__PURE__ */ jsx(DictionaryFields, {
6044
6913
  row,
6045
6914
  onPatch: (changes) => patchDimension(row.uid, changes)
@@ -6092,6 +6961,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6092
6961
  }),
6093
6962
  /* @__PURE__ */ jsx(PropertyBlock, {
6094
6963
  title: "Dicionário",
6964
+ collapseKey: "diagram:dictionary",
6095
6965
  children: /* @__PURE__ */ jsx(DictionaryFields, {
6096
6966
  row,
6097
6967
  onPatch: (changes) => patchMeasure(row.uid, changes)
@@ -6118,6 +6988,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6118
6988
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6119
6989
  /* @__PURE__ */ jsx(PropertyBlock, {
6120
6990
  title: "Modelo",
6991
+ collapseKey: "diagram:model",
6121
6992
  children: /* @__PURE__ */ jsx(Typography, {
6122
6993
  sx: {
6123
6994
  fontSize: 12,
@@ -6128,6 +6999,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6128
6999
  }),
6129
7000
  /* @__PURE__ */ jsxs(PropertyBlock, {
6130
7001
  title: "Relacionamentos",
7002
+ collapseKey: "diagram:relationships",
6131
7003
  children: [!editor.relationships.length && /* @__PURE__ */ jsx(Typography, {
6132
7004
  sx: {
6133
7005
  fontSize: 12,
@@ -6196,6 +7068,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6196
7068
  }),
6197
7069
  /* @__PURE__ */ jsxs(PropertyBlock, {
6198
7070
  title: "Medidas calculadas",
7071
+ collapseKey: "diagram:formulas",
6199
7072
  children: [formulas.map((row) => /* @__PURE__ */ jsxs(Button, {
6200
7073
  size: "small",
6201
7074
  onClick: () => onSelect({
@@ -6213,22 +7086,10 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6213
7086
  ]
6214
7087
  }, row.uid)), /* @__PURE__ */ jsx(Button, {
6215
7088
  size: "small",
6216
- onClick: () => {
6217
- const uid = newUid();
6218
- const taken = [...editor.dimensions, ...editor.measures].map((item) => item.name);
6219
- setEditor((current) => ({
6220
- ...current,
6221
- measures: [...current.measures, emptyMeasure({
6222
- uid,
6223
- type: "formula",
6224
- name: freeName("novaMedida", taken)
6225
- })]
6226
- }));
6227
- onSelect({
6228
- kind: "measure",
6229
- uid
6230
- });
6231
- },
7089
+ onClick: () => onSelect({
7090
+ kind: "measure",
7091
+ uid: addFormulaTo(setEditor)
7092
+ }),
6232
7093
  children: "+ Medida calculada"
6233
7094
  })]
6234
7095
  }),
@@ -6251,7 +7112,7 @@ function RenameField({ value, onCommit }) {
6251
7112
  value: text,
6252
7113
  onChange: setText,
6253
7114
  onBlur: () => {
6254
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(text)) onCommit(text);
7115
+ if (IDENTIFIER.test(text)) onCommit(text);
6255
7116
  else setText(value);
6256
7117
  }
6257
7118
  });
@@ -6429,11 +7290,22 @@ function ModelPanel({ devtools, original, capabilities, tables, onTry, onSaved }
6429
7290
  });
6430
7291
  const completions = useMemo(() => ({
6431
7292
  tables,
6432
- modelColumns: Object.fromEntries(editor.tables.map((row) => [row.name, columns?.tables[row.name] ?? []]))
7293
+ modelColumns: Object.fromEntries(editor.tables.map((row) => [row.name, columns?.tables[row.name] ?? []])),
7294
+ modelSql: Object.fromEntries(editor.tables.map((row) => [row.name, row.sql])),
7295
+ testSql: async (sql) => {
7296
+ const result = await fetcher({
7297
+ model: modelName,
7298
+ devtools: { testSql: sql }
7299
+ });
7300
+ if (!result.devtools?.sqlTest) throw new Error("O servidor não devolveu o teste do SQL");
7301
+ return result.devtools.sqlTest;
7302
+ }
6433
7303
  }), [
6434
7304
  tables,
6435
7305
  columns,
6436
- editor.tables
7306
+ editor.tables,
7307
+ fetcher,
7308
+ modelName
6437
7309
  ]);
6438
7310
  const validationErrors = validation ? Object.entries(validation.errors) : [];
6439
7311
  return /* @__PURE__ */ jsxs(SqlCompletionsContext.Provider, {
@@ -8582,4 +9454,4 @@ function ToggleButton$1({ draft, loading, problems, inspecting, onOpen }) {
8582
9454
  //#endregion
8583
9455
  export { DashboardDevtools as default };
8584
9456
 
8585
- //# sourceMappingURL=DashboardDevtools-D0pzwSgn.js.map
9457
+ //# sourceMappingURL=DashboardDevtools-TFwjFdqn.js.map