@bluemarble/bi 0.7.0 → 0.8.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.
@@ -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
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";
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-DZiYK1mV.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";
@@ -3237,7 +3237,7 @@ function ColumnOptions({ kind, name, model, label, onLabel, format, onFormat, fo
3237
3237
  value: formatter,
3238
3238
  options: formatters.map((option) => ({ name: option })),
3239
3239
  onChange: onFormatter
3240
- }), "Formatters passados ao <DashboardCharts formatters={{ ... }}> da página"),
3240
+ }), "Built-in (to_month_name, to_date, ...) e os passados ao <DashboardCharts formatters={{ ... }}> da página"),
3241
3241
  format !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
3242
3242
  /* @__PURE__ */ jsxs(Box, {
3243
3243
  sx: {
@@ -3821,11 +3821,490 @@ function freeName(prefix, taken) {
3821
3821
  for (let index = 2;; index++) if (!taken.includes(`${prefix}${index}`)) return `${prefix}${index}`;
3822
3822
  }
3823
3823
  //#endregion
3824
+ //#region src/devtools/QueryBuilder.tsx
3825
+ 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(" "));
3826
+ const quote = (name) => /^[A-Za-z_]\w*$/.test(name) && !RESERVED.has(name.toLowerCase()) ? name : `\`${name}\``;
3827
+ const unquote = (name) => name.trim().replace(/^`(.*)`$/, "$1");
3828
+ function buildSelect(table, columns) {
3829
+ return `SELECT\n${columns.length ? columns.map((column) => ` ${quote(column)}`).join(",\n") : " *"}\nFROM ${quote(table)}`;
3830
+ }
3831
+ /** Tabela e colunas de um `SELECT a, b FROM t` (sem WHERE, JOIN etc.); senão `null`. */
3832
+ function parseSelect(sql) {
3833
+ const match = /^\s*SELECT\s+([\s\S]+?)\s+FROM\s+(`[^`]+`|\w+)\s*;?\s*$/i.exec(sql);
3834
+ if (!match) return null;
3835
+ const columns = match[1].trim() === "*" ? [] : match[1].split(",").map(unquote);
3836
+ if (columns.some((column) => !/^[^\s`().]+$/.test(column))) return null;
3837
+ return {
3838
+ table: unquote(match[2]),
3839
+ columns
3840
+ };
3841
+ }
3842
+ function QueryBuilder({ open, onClose, tables, sql, onApply }) {
3843
+ return /* @__PURE__ */ jsx(Dialog, {
3844
+ open,
3845
+ onClose,
3846
+ maxWidth: "md",
3847
+ fullWidth: true,
3848
+ children: open && /* @__PURE__ */ jsx(Builder, {
3849
+ tables,
3850
+ sql,
3851
+ onClose,
3852
+ onApply
3853
+ })
3854
+ });
3855
+ }
3856
+ function Builder({ tables, sql, onClose, onApply }) {
3857
+ const [initial] = useState(() => {
3858
+ const parsed = parseSelect(sql);
3859
+ const table = parsed && tables.find((item) => item.name === parsed.table);
3860
+ if (!parsed || !table) return {
3861
+ table: null,
3862
+ columns: []
3863
+ };
3864
+ const known = new Set(table.columns.map((column) => column.name));
3865
+ return {
3866
+ table,
3867
+ columns: parsed.columns.filter((column) => known.has(column))
3868
+ };
3869
+ });
3870
+ const [table, setTable] = useState(initial.table);
3871
+ const [selected, setSelected] = useState(initial.columns);
3872
+ const [search, setSearch] = useState("");
3873
+ const visible = useMemo(() => {
3874
+ const lower = search.trim().toLowerCase();
3875
+ return (table?.columns ?? []).filter((column) => column.name.toLowerCase().includes(lower));
3876
+ }, [table, search]);
3877
+ const ordered = (table?.columns ?? []).map((column) => column.name).filter((name) => selected.includes(name));
3878
+ const result = table ? buildSelect(table.name, ordered) : "";
3879
+ const allVisible = visible.length > 0 && visible.every((column) => selected.includes(column.name));
3880
+ const toggle = (name) => setSelected((current) => current.includes(name) ? current.filter((item) => item !== name) : [...current, name]);
3881
+ const toggleVisible = () => {
3882
+ const names = visible.map((column) => column.name);
3883
+ setSelected((current) => allVisible ? current.filter((name) => !names.includes(name)) : [.../* @__PURE__ */ new Set([...current, ...names])]);
3884
+ };
3885
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3886
+ /* @__PURE__ */ jsx(DialogTitle, {
3887
+ sx: { fontSize: 14 },
3888
+ children: "Criar query"
3889
+ }),
3890
+ /* @__PURE__ */ jsxs(DialogContent, {
3891
+ sx: {
3892
+ display: "flex",
3893
+ flexDirection: "column",
3894
+ gap: 1.5
3895
+ },
3896
+ children: [
3897
+ /* @__PURE__ */ jsx(Autocomplete, {
3898
+ size: "small",
3899
+ options: tables,
3900
+ value: table,
3901
+ onChange: (_, value) => {
3902
+ setTable(value);
3903
+ setSelected([]);
3904
+ setSearch("");
3905
+ },
3906
+ getOptionLabel: (option) => option.name,
3907
+ isOptionEqualToValue: (option, value) => option.name === value.name,
3908
+ noOptionsText: tables.length ? "Nenhuma tabela" : "Carregando as tabelas do banco…",
3909
+ renderOption: ({ key, ...props }, option) => /* @__PURE__ */ jsxs(Box, {
3910
+ component: "li",
3911
+ ...props,
3912
+ sx: {
3913
+ gap: 2,
3914
+ fontFamily: MONO,
3915
+ fontSize: 12
3916
+ },
3917
+ children: [option.name, /* @__PURE__ */ jsxs(Box, {
3918
+ component: "span",
3919
+ sx: {
3920
+ ml: "auto",
3921
+ color: "text.secondary",
3922
+ fontSize: 11
3923
+ },
3924
+ children: [
3925
+ option.type,
3926
+ " · ",
3927
+ option.columns.length,
3928
+ " colunas"
3929
+ ]
3930
+ })]
3931
+ }, key),
3932
+ renderInput: (params) => /* @__PURE__ */ jsx(TextField, {
3933
+ ...params,
3934
+ label: "tabela",
3935
+ sx: { mt: 1 }
3936
+ })
3937
+ }),
3938
+ table && /* @__PURE__ */ jsxs(Box, {
3939
+ sx: {
3940
+ display: "flex",
3941
+ flexDirection: "column",
3942
+ gap: .5
3943
+ },
3944
+ children: [/* @__PURE__ */ jsxs(Box, {
3945
+ sx: {
3946
+ display: "flex",
3947
+ alignItems: "center",
3948
+ gap: 1
3949
+ },
3950
+ children: [
3951
+ /* @__PURE__ */ jsx(FormControlLabel, {
3952
+ control: /* @__PURE__ */ jsx(Checkbox, {
3953
+ size: "small",
3954
+ checked: allVisible,
3955
+ indeterminate: !allVisible && visible.some((c) => selected.includes(c.name)),
3956
+ onChange: toggleVisible
3957
+ }),
3958
+ label: /* @__PURE__ */ jsx(Typography, {
3959
+ sx: { fontSize: 12 },
3960
+ children: "Todas"
3961
+ })
3962
+ }),
3963
+ /* @__PURE__ */ jsxs(Typography, {
3964
+ sx: {
3965
+ fontSize: 12,
3966
+ color: "text.secondary"
3967
+ },
3968
+ children: [
3969
+ selected.length,
3970
+ " de ",
3971
+ table.columns.length,
3972
+ " marcadas",
3973
+ !selected.length && " (nenhuma = *)"
3974
+ ]
3975
+ }),
3976
+ /* @__PURE__ */ jsx(TextField, {
3977
+ size: "small",
3978
+ placeholder: "filtrar colunas",
3979
+ value: search,
3980
+ onChange: (event) => setSearch(event.target.value),
3981
+ sx: {
3982
+ ml: "auto",
3983
+ width: 200
3984
+ }
3985
+ })
3986
+ ]
3987
+ }), /* @__PURE__ */ jsx(Box, {
3988
+ sx: {
3989
+ maxHeight: 280,
3990
+ overflow: "auto",
3991
+ border: 1,
3992
+ borderColor: "divider",
3993
+ borderRadius: 1,
3994
+ px: 1,
3995
+ display: "grid",
3996
+ gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))"
3997
+ },
3998
+ children: visible.map((column) => /* @__PURE__ */ jsx(FormControlLabel, {
3999
+ control: /* @__PURE__ */ jsx(Checkbox, {
4000
+ size: "small",
4001
+ checked: selected.includes(column.name),
4002
+ onChange: () => toggle(column.name)
4003
+ }),
4004
+ label: /* @__PURE__ */ jsxs(Box, {
4005
+ component: "span",
4006
+ sx: {
4007
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4008
+ fontSize: 12
4009
+ },
4010
+ children: [
4011
+ column.name,
4012
+ " ",
4013
+ /* @__PURE__ */ jsx(Box, {
4014
+ component: "span",
4015
+ sx: {
4016
+ color: "text.secondary",
4017
+ fontSize: 11
4018
+ },
4019
+ children: column.type
4020
+ })
4021
+ ]
4022
+ }),
4023
+ sx: {
4024
+ m: 0,
4025
+ minWidth: 0
4026
+ }
4027
+ }, column.name))
4028
+ })]
4029
+ }),
4030
+ result && /* @__PURE__ */ jsx(Box, {
4031
+ component: "pre",
4032
+ sx: {
4033
+ m: 0,
4034
+ p: 1,
4035
+ border: 1,
4036
+ borderColor: "divider",
4037
+ borderRadius: 1,
4038
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4039
+ fontSize: 12,
4040
+ maxHeight: 200,
4041
+ overflow: "auto"
4042
+ },
4043
+ children: /* @__PURE__ */ jsx(HighlightedSql, { code: result })
4044
+ })
4045
+ ]
4046
+ }),
4047
+ /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
4048
+ onClick: onClose,
4049
+ children: "Cancelar"
4050
+ }), /* @__PURE__ */ jsx(Button, {
4051
+ variant: "contained",
4052
+ disabled: !table,
4053
+ onClick: () => {
4054
+ onApply(result);
4055
+ onClose();
4056
+ },
4057
+ title: "Substitui o SQL atual",
4058
+ children: "Usar no SQL"
4059
+ })] })
4060
+ ] });
4061
+ }
4062
+ //#endregion
4063
+ //#region src/devtools/queryTemplates.ts
4064
+ /**
4065
+ * `WITH` da origem: a tabela do banco entra pelo nome; o SQL de uma tabela do modelo vira o CTE
4066
+ * `origem`, antes dos CTEs do template.
4067
+ */
4068
+ function withSource(source) {
4069
+ if (source.kind === "database") return {
4070
+ ctes: [],
4071
+ from: quote(source.table)
4072
+ };
4073
+ return {
4074
+ ctes: [`origem AS (\n${source.sql.trim().replace(/;+\s*$/, "").replace(/^/gm, " ")}\n)`],
4075
+ from: "origem"
4076
+ };
4077
+ }
4078
+ /**
4079
+ * Um dia por linha, do menor ao maior valor da coluna, com ano e mês. É a tabela "Calendário" do
4080
+ * Power BI: relacione a coluna de data da fato com `dia`.
4081
+ *
4082
+ * Não é um `WITH RECURSIVE` de um dia por iteração: o MySQL (`cte_max_recursion_depth`) e o MariaDB
4083
+ * (`max_recursive_iterations`) param em 1000 iterações, e o MariaDB corta em silêncio — o calendário
4084
+ * acabaria ~2,7 anos depois do início. A recursão só gera 0–999 e o dia sai de `mil * 1000 + n`.
4085
+ */
4086
+ function calendarSql(source, column) {
4087
+ const { ctes, from } = withSource(source);
4088
+ const col = quote(column);
4089
+ return [
4090
+ `WITH RECURSIVE ${[
4091
+ ...ctes,
4092
+ [
4093
+ "limites AS (",
4094
+ ` SELECT DATE(MIN(${col})) AS inicio, DATE(MAX(${col})) AS fim`,
4095
+ ` FROM ${from}`,
4096
+ ")"
4097
+ ],
4098
+ [
4099
+ "numeros AS (",
4100
+ " SELECT 0 AS n",
4101
+ " UNION ALL",
4102
+ " SELECT n + 1 FROM numeros WHERE n < 999",
4103
+ ")"
4104
+ ],
4105
+ [
4106
+ "dias AS (",
4107
+ " SELECT inicio + INTERVAL (mil.n * 1000 + numeros.n) DAY AS dia",
4108
+ " FROM limites, numeros mil, numeros",
4109
+ " WHERE mil.n <= DATEDIFF(fim, inicio) DIV 1000",
4110
+ " AND mil.n * 1000 + numeros.n <= DATEDIFF(fim, inicio)",
4111
+ ")"
4112
+ ]
4113
+ ].map((part) => Array.isArray(part) ? part.join("\n") : part).join(",\n")}`,
4114
+ "SELECT",
4115
+ " dia,",
4116
+ " YEAR(dia) AS ano,",
4117
+ " MONTH(dia) AS mes",
4118
+ "FROM dias"
4119
+ ].join("\n");
4120
+ }
4121
+ const QUERY_TEMPLATES = [{
4122
+ id: "calendar",
4123
+ label: "Calendário",
4124
+ 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`.",
4125
+ columnLabel: "coluna de data",
4126
+ preferredTypes: /date|time/i,
4127
+ build: calendarSql
4128
+ }];
4129
+ //#endregion
4130
+ //#region src/devtools/QueryTemplates.tsx
4131
+ function QueryTemplates({ open, onClose, ...props }) {
4132
+ return /* @__PURE__ */ jsx(Dialog, {
4133
+ open,
4134
+ onClose,
4135
+ maxWidth: "md",
4136
+ fullWidth: true,
4137
+ children: open && /* @__PURE__ */ jsx(Templates, {
4138
+ onClose,
4139
+ ...props
4140
+ })
4141
+ });
4142
+ }
4143
+ function Templates({ tables, modelTables, onClose, onApply }) {
4144
+ const [templateId, setTemplateId] = useState(QUERY_TEMPLATES[0].id);
4145
+ const [source, setSource] = useState(null);
4146
+ const [column, setColumn] = useState(null);
4147
+ const template = QUERY_TEMPLATES.find((item) => item.id === templateId) ?? QUERY_TEMPLATES[0];
4148
+ const sources = useMemo(() => [...Object.entries(modelTables).map(([name, table]) => ({
4149
+ key: `model:${name}`,
4150
+ name,
4151
+ group: "Tabelas do modelo",
4152
+ columns: table.columns.map((column) => ({ name: column })),
4153
+ source: {
4154
+ kind: "model",
4155
+ sql: table.sql
4156
+ }
4157
+ })), ...tables.map((table) => ({
4158
+ key: `database:${table.name}`,
4159
+ name: table.name,
4160
+ group: "Tabelas do banco",
4161
+ columns: table.columns,
4162
+ source: {
4163
+ kind: "database",
4164
+ table: table.name
4165
+ }
4166
+ }))], [tables, modelTables]);
4167
+ const columns = useMemo(() => {
4168
+ const list = source?.columns ?? [];
4169
+ const preferred = (type) => !!type && !!template.preferredTypes?.test(type);
4170
+ return [...list.filter((c) => preferred(c.type)), ...list.filter((c) => !preferred(c.type))];
4171
+ }, [source, template]);
4172
+ const result = source && column ? template.build(source.source, column) : "";
4173
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4174
+ /* @__PURE__ */ jsx(DialogTitle, {
4175
+ sx: { fontSize: 14 },
4176
+ children: "Importar template"
4177
+ }),
4178
+ /* @__PURE__ */ jsxs(DialogContent, {
4179
+ sx: {
4180
+ display: "flex",
4181
+ flexDirection: "column",
4182
+ gap: 1.5
4183
+ },
4184
+ children: [
4185
+ /* @__PURE__ */ jsx(TextField, {
4186
+ select: true,
4187
+ size: "small",
4188
+ label: "template",
4189
+ value: template.id,
4190
+ onChange: (event) => setTemplateId(event.target.value),
4191
+ helperText: template.description,
4192
+ sx: { mt: 1 },
4193
+ children: QUERY_TEMPLATES.map((item) => /* @__PURE__ */ jsx(MenuItem, {
4194
+ value: item.id,
4195
+ children: item.label
4196
+ }, item.id))
4197
+ }),
4198
+ /* @__PURE__ */ jsx(Autocomplete, {
4199
+ size: "small",
4200
+ options: sources,
4201
+ value: source,
4202
+ onChange: (_, value) => {
4203
+ setSource(value);
4204
+ setColumn(null);
4205
+ },
4206
+ groupBy: (option) => option.group,
4207
+ getOptionLabel: (option) => option.name,
4208
+ isOptionEqualToValue: (option, value) => option.key === value.key,
4209
+ noOptionsText: "Nenhuma tabela",
4210
+ renderOption: ({ key, ...props }, option) => /* @__PURE__ */ jsxs(Box, {
4211
+ component: "li",
4212
+ ...props,
4213
+ sx: {
4214
+ gap: 2,
4215
+ fontFamily: MONO,
4216
+ fontSize: 12
4217
+ },
4218
+ children: [option.name, /* @__PURE__ */ jsxs(Box, {
4219
+ component: "span",
4220
+ sx: {
4221
+ ml: "auto",
4222
+ color: "text.secondary",
4223
+ fontSize: 11
4224
+ },
4225
+ children: [option.columns.length, " colunas"]
4226
+ })]
4227
+ }, key),
4228
+ renderInput: (params) => /* @__PURE__ */ jsx(TextField, {
4229
+ ...params,
4230
+ label: "tabela"
4231
+ })
4232
+ }),
4233
+ source && /* @__PURE__ */ jsx(Autocomplete, {
4234
+ size: "small",
4235
+ options: columns.map((item) => item.name),
4236
+ value: column,
4237
+ onChange: (_, value) => setColumn(value),
4238
+ noOptionsText: source.source.kind === "model" ? "Sem colunas: valide o SQL dessa tabela" : "Nenhuma coluna",
4239
+ renderOption: ({ key, ...props }, name) => /* @__PURE__ */ jsxs(Box, {
4240
+ component: "li",
4241
+ ...props,
4242
+ sx: {
4243
+ gap: 2,
4244
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4245
+ fontSize: 12
4246
+ },
4247
+ children: [name, /* @__PURE__ */ jsx(Box, {
4248
+ component: "span",
4249
+ sx: {
4250
+ ml: "auto",
4251
+ color: "text.secondary",
4252
+ fontSize: 11
4253
+ },
4254
+ children: columns.find((item) => item.name === name)?.type
4255
+ })]
4256
+ }, key),
4257
+ renderInput: (params) => /* @__PURE__ */ jsx(TextField, {
4258
+ ...params,
4259
+ label: template.columnLabel
4260
+ })
4261
+ }),
4262
+ result ? /* @__PURE__ */ jsx(Box, {
4263
+ component: "pre",
4264
+ sx: {
4265
+ m: 0,
4266
+ p: 1,
4267
+ border: 1,
4268
+ borderColor: "divider",
4269
+ borderRadius: 1,
4270
+ fontFamily: MONO,
4271
+ fontSize: 12,
4272
+ maxHeight: 280,
4273
+ overflow: "auto"
4274
+ },
4275
+ children: /* @__PURE__ */ jsx(HighlightedSql, { code: result })
4276
+ }) : /* @__PURE__ */ jsx(Typography, {
4277
+ sx: {
4278
+ fontSize: 12,
4279
+ color: "text.secondary"
4280
+ },
4281
+ children: "Escolha a tabela e a coluna para ver o SQL."
4282
+ })
4283
+ ]
4284
+ }),
4285
+ /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
4286
+ onClick: onClose,
4287
+ children: "Cancelar"
4288
+ }), /* @__PURE__ */ jsx(Button, {
4289
+ variant: "contained",
4290
+ disabled: !result,
4291
+ onClick: () => {
4292
+ onApply(result);
4293
+ onClose();
4294
+ },
4295
+ title: "Substitui o SQL atual",
4296
+ children: "Usar no SQL"
4297
+ })] })
4298
+ ] });
4299
+ }
4300
+ //#endregion
3824
4301
  //#region src/devtools/SqlEditor.tsx
3825
4302
  /**
3826
4303
  * Campo de SQL com destaque de sintaxe e autocomplete de tabelas e colunas. As colunas vêm do
3827
4304
  * `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.
4305
+ * O ⤢ abre o mesmo SQL num editor grande; com `queryTools`, ele traz "Criar query" (escolher tabela e
4306
+ * colunas do banco), "Importar template" (calendário etc.) e "Validar SQL" (executa no banco e
4307
+ * mostra uma amostra).
3829
4308
  *
3830
4309
  * O destaque é um `<pre>` colorido por baixo do `<textarea>` de texto transparente: mesma fonte,
3831
4310
  * mesmo recuo e mesma quebra de linha, com a rolagem sincronizada. O cursor, a seleção e o
@@ -3901,12 +4380,36 @@ function suggest(sql, caret, completions) {
3901
4380
  items
3902
4381
  } : null;
3903
4382
  }
3904
- function SqlEditor({ value, onChange, label, placeholder = "SQL", minRows, maxRows = 6, error, warning, expandable = true }) {
4383
+ function SqlEditor({ value, onChange, label, placeholder = "SQL", minRows, maxRows = 6, error, warning, expandable = true, queryTools = false }) {
3905
4384
  const completions = useContext(SqlCompletionsContext);
3906
4385
  const anchor = useRef(null);
3907
4386
  const input = useRef(null);
3908
4387
  const highlight = useRef(null);
3909
4388
  const [expanded, setExpanded] = useState(false);
4389
+ const [building, setBuilding] = useState(false);
4390
+ const [importing, setImporting] = useState(false);
4391
+ const [test, setTest] = useState(null);
4392
+ const [testing, setTesting] = useState(false);
4393
+ const currentTest = test?.sql === value ? test : null;
4394
+ const testSql = completions.testSql;
4395
+ const runTest = async () => {
4396
+ if (!testSql) return;
4397
+ const sql = value;
4398
+ setTesting(true);
4399
+ try {
4400
+ setTest({
4401
+ sql,
4402
+ result: await testSql(sql)
4403
+ });
4404
+ } catch (error) {
4405
+ setTest({
4406
+ sql,
4407
+ error: errorMessage(error)
4408
+ });
4409
+ } finally {
4410
+ setTesting(false);
4411
+ }
4412
+ };
3910
4413
  const [state, setState] = useState(null);
3911
4414
  const accept = (item) => {
3912
4415
  const element = input.current;
@@ -4046,25 +4549,90 @@ function SqlEditor({ value, onChange, label, placeholder = "SQL", minRows, maxRo
4046
4549
  sx: { fontSize: 14 },
4047
4550
  children: label ?? "SQL"
4048
4551
  }),
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
4552
+ /* @__PURE__ */ jsxs(DialogContent, { children: [
4553
+ /* @__PURE__ */ jsx(Box, {
4554
+ sx: { pt: 1 },
4555
+ children: /* @__PURE__ */ jsx(SqlEditor, {
4556
+ value,
4557
+ onChange,
4558
+ placeholder,
4559
+ minRows: 20,
4560
+ maxRows: 36,
4561
+ error,
4562
+ warning,
4563
+ expandable: false
4564
+ })
4565
+ }),
4566
+ currentTest?.error && /* @__PURE__ */ jsx(Alert, {
4567
+ severity: "error",
4568
+ sx: {
4569
+ mt: 1,
4570
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4571
+ fontSize: 12
4572
+ },
4573
+ children: currentTest.error
4574
+ }),
4575
+ currentTest?.result && /* @__PURE__ */ jsxs(Box, {
4576
+ sx: {
4577
+ mt: 1,
4578
+ display: "flex",
4579
+ flexDirection: "column",
4580
+ gap: 1
4581
+ },
4582
+ children: [/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Chip, {
4583
+ color: "success",
4584
+ size: "small",
4585
+ label: `SQL válido · ${currentTest.result.columns.length} colunas · ${currentTest.result.durationMs} ms`
4586
+ }) }), /* @__PURE__ */ jsx(ResultTable, {
4587
+ rows: currentTest.result.rows,
4588
+ maxHeight: 240
4589
+ })]
4060
4590
  })
4061
- }) }),
4062
- /* @__PURE__ */ jsx(DialogActions, { children: /* @__PURE__ */ jsx(Button, {
4063
- onClick: () => setExpanded(false),
4064
- children: "Fechar"
4065
- }) })
4591
+ ] }),
4592
+ /* @__PURE__ */ jsxs(DialogActions, { children: [
4593
+ queryTools && /* @__PURE__ */ jsxs(Fragment, { children: [
4594
+ /* @__PURE__ */ jsx(Button, {
4595
+ onClick: () => setBuilding(true),
4596
+ title: "Escolher a tabela e as colunas",
4597
+ children: "Criar query"
4598
+ }),
4599
+ /* @__PURE__ */ jsx(Button, {
4600
+ onClick: () => setImporting(true),
4601
+ title: "Calendário e outras queries prontas",
4602
+ children: "Importar template"
4603
+ }),
4604
+ testSql && /* @__PURE__ */ jsx(Button, {
4605
+ disabled: !value.trim() || testing,
4606
+ onClick: runTest,
4607
+ title: "Executa o SQL no banco e mostra as primeiras linhas",
4608
+ children: testing ? "Validando…" : "Validar SQL"
4609
+ })
4610
+ ] }),
4611
+ /* @__PURE__ */ jsx(Box, { sx: { flex: 1 } }),
4612
+ /* @__PURE__ */ jsx(Button, {
4613
+ onClick: () => setExpanded(false),
4614
+ children: "Fechar"
4615
+ })
4616
+ ] })
4066
4617
  ]
4067
4618
  }),
4619
+ queryTools && /* @__PURE__ */ jsx(QueryBuilder, {
4620
+ open: building,
4621
+ onClose: () => setBuilding(false),
4622
+ tables: completions.tables,
4623
+ sql: value,
4624
+ onApply: onChange
4625
+ }),
4626
+ queryTools && /* @__PURE__ */ jsx(QueryTemplates, {
4627
+ open: importing,
4628
+ onClose: () => setImporting(false),
4629
+ tables: completions.tables,
4630
+ modelTables: Object.fromEntries(Object.entries(completions.modelSql ?? {}).map(([name, sql]) => [name, {
4631
+ sql,
4632
+ columns: completions.modelColumns[name] ?? []
4633
+ }])),
4634
+ onApply: onChange
4635
+ }),
4068
4636
  /* @__PURE__ */ jsx(Popper, {
4069
4637
  open: !!state,
4070
4638
  anchorEl: anchor.current,
@@ -4524,6 +5092,28 @@ function DimensionSource({ row, editor, columns, error, warning, onPatch }) {
4524
5092
  }
4525
5093
  //#endregion
4526
5094
  //#region src/devtools/ModelDiagram.tsx
5095
+ const TARGET_NAME = {
5096
+ table: "tabela",
5097
+ dimension: "dimensão",
5098
+ measure: "medida"
5099
+ };
5100
+ /** Põe uma medida calculada (fórmula) nova, com nome livre, no fim; devolve o `uid` dela. */
5101
+ function addFormulaTo(setEditor) {
5102
+ const uid = newUid();
5103
+ setEditor((current) => {
5104
+ const taken = [...current.dimensions, ...current.measures].map((item) => item.name);
5105
+ const measure = emptyMeasure({
5106
+ uid,
5107
+ type: "formula",
5108
+ name: freeName("novaMedida", taken)
5109
+ });
5110
+ return {
5111
+ ...current,
5112
+ measures: [...current.measures, measure]
5113
+ };
5114
+ });
5115
+ return uid;
5116
+ }
4527
5117
  const CARD_WIDTH = 230;
4528
5118
  const HEADER = 34;
4529
5119
  const ROW = 22;
@@ -4690,6 +5280,12 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4690
5280
  });
4691
5281
  const [selection, setSelection] = useState(null);
4692
5282
  const [panel, setPanel] = useState(loadPanel);
5283
+ const [renaming, setRenaming] = useState(null);
5284
+ const [menu, setMenu] = useState(null);
5285
+ const closeMenu = () => setMenu((current) => current && {
5286
+ ...current,
5287
+ open: false
5288
+ });
4693
5289
  useEffect(() => {
4694
5290
  try {
4695
5291
  localStorage.setItem(PANEL_KEY, JSON.stringify(panel));
@@ -4810,15 +5406,16 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4810
5406
  const rect = canvas.current?.getBoundingClientRect();
4811
5407
  return toWorld((rect?.left ?? 0) + (rect?.width ?? 600) / 2 - CARD_WIDTH / 2, (rect?.top ?? 0) + (rect?.height ?? 400) / 3);
4812
5408
  };
4813
- const addTable = () => {
5409
+ /** `at`: canto do card (o ponto do botão direito); sem ele, perto do centro da tela. */
5410
+ const addTable = (at) => {
4814
5411
  const uid = newUid();
4815
5412
  const name = freeName("novaTabela", editor.tables.map((table) => table.name));
4816
- const center = visibleCenter();
5413
+ const position = at ?? visibleCenter();
4817
5414
  setEditor((current) => ({
4818
5415
  ...current,
4819
5416
  positions: {
4820
5417
  ...current.positions,
4821
- [uid]: center
5418
+ [uid]: position
4822
5419
  },
4823
5420
  tables: [...current.tables, {
4824
5421
  uid,
@@ -4834,6 +5431,20 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4834
5431
  uid
4835
5432
  });
4836
5433
  };
5434
+ const organize = () => {
5435
+ const layout = autoLayout(nodes, sizes);
5436
+ setPositions((current) => Object.fromEntries(Object.entries(layout).map(([id, point]) => [id, {
5437
+ ...current[id],
5438
+ ...point
5439
+ }])));
5440
+ fitToScreen(layout);
5441
+ };
5442
+ const addFormula = () => {
5443
+ setSelection({
5444
+ kind: "measure",
5445
+ uid: addFormulaTo(setEditor)
5446
+ });
5447
+ };
4837
5448
  const removeTable = (uid) => {
4838
5449
  setEditor((current) => ({
4839
5450
  ...current,
@@ -4842,6 +5453,19 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4842
5453
  }));
4843
5454
  setSelection(null);
4844
5455
  };
5456
+ const remove = (target) => {
5457
+ if (target.kind === "table") return removeTable(target.uid);
5458
+ const list = {
5459
+ relationship: "relationships",
5460
+ dimension: "dimensions",
5461
+ measure: "measures"
5462
+ }[target.kind];
5463
+ setEditor((current) => ({
5464
+ ...current,
5465
+ [list]: current[list].filter((row) => row.uid !== target.uid)
5466
+ }));
5467
+ setSelection((current) => current?.uid === target.uid ? null : current);
5468
+ };
4845
5469
  const commitRename = (uid, name) => setEditor((current) => renameTable(current, uid, name));
4846
5470
  const connect = (from, fromColumn, to, toColumn) => {
4847
5471
  if (from.uid === to.uid) return;
@@ -4902,20 +5526,7 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
4902
5526
  });
4903
5527
  };
4904
5528
  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
- }
5529
+ if (selection) remove(selection);
4919
5530
  };
4920
5531
  const startPan = (event) => {
4921
5532
  if (event.button !== 0) return;
@@ -5103,305 +5714,392 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
5103
5714
  height: "100%",
5104
5715
  minHeight: 0
5105
5716
  },
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: {
5717
+ children: [
5718
+ /* @__PURE__ */ jsxs(Box, {
5719
+ ref: canvas,
5720
+ tabIndex: -1,
5721
+ onMouseDown: startPan,
5722
+ onMouseDownCapture: () => canvas.current?.focus({ preventScroll: true }),
5723
+ onKeyDown: (event) => {
5724
+ if ((event.key === "Delete" || event.key === "Backspace") && event.target === canvas.current) removeSelection();
5725
+ },
5726
+ onWheel,
5727
+ onContextMenu: (event) => {
5728
+ event.preventDefault();
5729
+ setMenu({
5730
+ open: true,
5731
+ x: event.clientX,
5732
+ y: event.clientY,
5733
+ target: null
5734
+ });
5735
+ },
5736
+ sx: {
5737
+ outline: "none",
5738
+ flex: 1,
5739
+ position: "relative",
5740
+ overflow: "hidden",
5741
+ cursor: "grab",
5742
+ bgcolor: "#15171b",
5743
+ backgroundImage: "radial-gradient(rgba(255,255,255,.07) 1px, transparent 1px)",
5744
+ backgroundSize: `${20 * view.zoom}px ${20 * view.zoom}px`,
5745
+ backgroundPosition: `${view.x}px ${view.y}px`
5746
+ },
5747
+ children: [
5748
+ /* @__PURE__ */ jsxs(Box, {
5749
+ sx: {
5139
5750
  position: "absolute",
5140
- overflow: "visible",
5141
- pointerEvents: "none"
5751
+ left: 0,
5752
+ top: 0,
5753
+ transformOrigin: "0 0",
5754
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`
5142
5755
  },
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"
5756
+ children: [/* @__PURE__ */ jsxs("svg", {
5757
+ width: 1,
5758
+ height: 1,
5759
+ style: {
5760
+ position: "absolute",
5761
+ overflow: "visible",
5762
+ pointerEvents: "none"
5217
5763
  },
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
5764
+ "aria-hidden": true,
5765
+ children: [lines.map((line) => /* @__PURE__ */ jsx(RelationshipLine, {
5766
+ line,
5767
+ hovered: hoveredEdge === line.id,
5768
+ onHover: (hovered) => setHoveredEdge(hovered ? line.id : null)
5769
+ }, line.id)), linkPreview && /* @__PURE__ */ jsx("path", {
5770
+ d: linkPreview,
5771
+ fill: "none",
5772
+ stroke: "#F58220",
5773
+ strokeWidth: 2,
5774
+ strokeDasharray: "5 4"
5775
+ })]
5776
+ }), nodes.map((node) => /* @__PURE__ */ jsx(TableCard, {
5777
+ node,
5778
+ position: placed(node),
5779
+ size: sizeOf(node),
5780
+ collapsed: !!collapsed[node.id],
5781
+ selected: !!isSelected(node),
5782
+ linking: !!linking && linking.node.id !== node.id,
5783
+ selectedField: selection?.kind === "dimension" || selection?.kind === "measure" ? selection.uid : null,
5784
+ onToggle: () => setCollapsed((current) => ({
5785
+ ...current,
5786
+ [node.id]: !current[node.id]
5787
+ })),
5788
+ onScroll: (top) => setScrolls((current) => ({
5789
+ ...current,
5790
+ [node.id]: top
5791
+ })),
5792
+ onHeaderDown: (event) => startMove(event, node),
5793
+ onResizeDown: (event, edges) => startResize(event, node, edges),
5794
+ onResizeReset: () => resetSize(node),
5795
+ onColumnDown: (event, column) => startLink(event, node, column),
5796
+ onFieldClick: (item) => setSelection({
5797
+ kind: item.kind,
5798
+ uid: item.uid
5799
+ }),
5800
+ onAddField: (kind, column) => addField(kind, node.name, column),
5801
+ renaming: renaming === node.uid,
5802
+ onRenameStart: () => setRenaming(node.uid),
5803
+ onRename: (name) => {
5804
+ setRenaming(null);
5805
+ if (name !== null && name !== node.name) commitRename(node.uid, name);
5806
+ },
5807
+ onContextMenu: (event, target) => {
5808
+ event.preventDefault();
5809
+ event.stopPropagation();
5810
+ setMenu({
5811
+ open: true,
5812
+ x: event.clientX,
5813
+ y: event.clientY,
5814
+ target
5815
+ });
5816
+ }
5817
+ }, node.id))]
5818
+ }),
5819
+ /* @__PURE__ */ jsxs(Box, {
5820
+ onMouseDown: (event) => event.stopPropagation(),
5821
+ onContextMenu: (event) => event.stopPropagation(),
5822
+ sx: {
5823
+ position: "absolute",
5824
+ top: 8,
5825
+ left: 8,
5826
+ right: 8,
5827
+ display: "flex",
5828
+ gap: .75,
5829
+ flexWrap: "wrap",
5830
+ alignItems: "center",
5831
+ pointerEvents: "none",
5832
+ "& > *": { pointerEvents: "auto" }
5833
+ },
5834
+ children: [
5835
+ /* @__PURE__ */ jsx(Button, {
5836
+ size: "small",
5837
+ variant: "outlined",
5838
+ onClick: () => addTable(),
5839
+ children: "+ Tabela"
5840
+ }),
5841
+ /* @__PURE__ */ jsx(Button, {
5842
+ size: "small",
5843
+ onClick: onLoadColumns,
5844
+ disabled: loadingColumns,
5845
+ title: "Lê uma linha de cada tabela para listar as colunas",
5846
+ sx: { bgcolor: "#1b1e23" },
5847
+ children: loadingColumns ? "Lendo colunas…" : "Recarregar colunas"
5848
+ }),
5849
+ /* @__PURE__ */ jsxs(Box, {
5850
+ sx: {
5851
+ ml: "auto",
5852
+ display: "flex",
5853
+ gap: .5,
5854
+ alignItems: "center"
5855
+ },
5856
+ children: [
5857
+ /* @__PURE__ */ jsx(Button, {
5858
+ size: "small",
5859
+ onClick: organize,
5860
+ title: "Dimensões em cima, fatos embaixo (troca as posições salvas)",
5861
+ sx: { bgcolor: "#1b1e23" },
5862
+ children: "Organizar"
5863
+ }),
5864
+ /* @__PURE__ */ jsx(Button, {
5865
+ size: "small",
5866
+ onClick: () => fitToScreen(),
5867
+ sx: { bgcolor: "#1b1e23" },
5868
+ children: "Ajustar à tela"
5869
+ }),
5870
+ /* @__PURE__ */ jsx(IconButton, {
5871
+ size: "small",
5872
+ "aria-label": "Diminuir zoom",
5873
+ onClick: () => setView((current) => ({
5874
+ ...current,
5875
+ zoom: Math.max(.3, current.zoom - .1)
5876
+ })),
5877
+ children: "−"
5878
+ }),
5879
+ /* @__PURE__ */ jsxs(Typography, {
5880
+ sx: {
5881
+ fontSize: 11,
5882
+ fontFamily: MONO,
5883
+ width: 36,
5884
+ textAlign: "center"
5885
+ },
5886
+ children: [Math.round(view.zoom * 100), "%"]
5887
+ }),
5888
+ /* @__PURE__ */ jsx(IconButton, {
5889
+ size: "small",
5890
+ "aria-label": "Aumentar zoom",
5891
+ onClick: () => setView((current) => ({
5892
+ ...current,
5893
+ zoom: Math.min(2, current.zoom + .1)
5894
+ })),
5895
+ children: "+"
5896
+ })
5897
+ ]
5898
+ })
5899
+ ]
5900
+ }),
5901
+ !nodes.length && /* @__PURE__ */ jsx(Box, {
5902
+ sx: {
5903
+ position: "absolute",
5904
+ inset: 0,
5905
+ display: "flex",
5906
+ alignItems: "center",
5907
+ justifyContent: "center",
5908
+ pointerEvents: "none",
5909
+ color: "text.secondary",
5910
+ fontSize: 13,
5911
+ textAlign: "center",
5912
+ px: 4
5913
+ },
5914
+ 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."
5915
+ }),
5916
+ !!nodes.length && /* @__PURE__ */ jsx(Typography, {
5917
+ sx: {
5918
+ position: "absolute",
5919
+ left: 10,
5920
+ bottom: 6,
5921
+ fontSize: 11,
5922
+ color: "text.disabled",
5923
+ pointerEvents: "none"
5924
+ },
5925
+ 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"
5926
+ })
5927
+ ]
5928
+ }),
5929
+ /* @__PURE__ */ jsx(Menu, {
5930
+ open: !!menu?.open,
5931
+ onClose: closeMenu,
5932
+ anchorReference: "anchorPosition",
5933
+ anchorPosition: menu ? {
5934
+ top: menu.y,
5935
+ left: menu.x
5936
+ } : void 0,
5937
+ transitionDuration: {
5938
+ enter: 90,
5939
+ exit: 60
5940
+ },
5941
+ onContextMenu: (event) => event.preventDefault(),
5942
+ children: menu?.target ? [/* @__PURE__ */ jsxs(MenuItem, {
5943
+ dense: true,
5944
+ onClick: () => {
5945
+ setSelection(menu.target);
5946
+ closeMenu();
5282
5947
  },
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"
5948
+ children: ["Editar ", TARGET_NAME[menu.target.kind]]
5949
+ }, "edit"), /* @__PURE__ */ jsxs(MenuItem, {
5950
+ dense: true,
5951
+ sx: { color: "error.main" },
5952
+ onClick: () => {
5953
+ if (menu.target) remove(menu.target);
5954
+ closeMenu();
5293
5955
  },
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
5956
+ children: ["Remover ", TARGET_NAME[menu.target.kind]]
5957
+ }, "remove")] : [
5958
+ /* @__PURE__ */ jsx(MenuItem, {
5959
+ dense: true,
5960
+ onClick: () => {
5961
+ if (menu) addTable(toWorld(menu.x, menu.y));
5962
+ closeMenu();
5963
+ },
5964
+ children: "Nova Tabela"
5965
+ }, "table"),
5966
+ /* @__PURE__ */ jsx(MenuItem, {
5967
+ dense: true,
5968
+ onClick: () => {
5969
+ addFormula();
5970
+ closeMenu();
5971
+ },
5972
+ children: "Nova Medida Calculada"
5973
+ }, "formula"),
5974
+ /* @__PURE__ */ jsx(Divider, {}, "divider"),
5975
+ /* @__PURE__ */ jsx(MenuItem, {
5976
+ dense: true,
5977
+ disabled: loadingColumns,
5978
+ onClick: () => {
5979
+ onLoadColumns();
5980
+ closeMenu();
5981
+ },
5982
+ children: "Recarregar colunas"
5983
+ }, "columns"),
5984
+ /* @__PURE__ */ jsx(MenuItem, {
5985
+ dense: true,
5986
+ onClick: () => {
5987
+ organize();
5988
+ closeMenu();
5989
+ },
5990
+ children: "Organizar"
5991
+ }, "organize")
5992
+ ]
5993
+ }),
5994
+ panel.open ? /* @__PURE__ */ jsxs(Box, {
5995
+ sx: {
5996
+ width: panel.width,
5997
+ flexShrink: 0,
5998
+ borderLeft: 1,
5999
+ borderColor: "divider",
6000
+ position: "relative",
6001
+ display: "flex",
6002
+ flexDirection: "column",
6003
+ minHeight: 0
6004
+ },
6005
+ children: [
6006
+ /* @__PURE__ */ jsx(Box, {
6007
+ onMouseDown: startPanelResize,
6008
+ title: "Arraste para redimensionar",
6009
+ sx: {
6010
+ position: "absolute",
6011
+ left: -3,
6012
+ top: 0,
6013
+ bottom: 0,
6014
+ width: 6,
6015
+ cursor: "ew-resize",
6016
+ zIndex: 2,
6017
+ "&:hover": {
6018
+ bgcolor: "primary.main",
6019
+ opacity: .4
6020
+ }
5323
6021
  }
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, {
6022
+ }),
6023
+ /* @__PURE__ */ jsxs(Box, {
5336
6024
  sx: {
5337
- fontSize: 12,
5338
- fontWeight: 700,
5339
- color: "text.secondary",
5340
- flex: 1
6025
+ display: "flex",
6026
+ alignItems: "center",
6027
+ px: 1.5,
6028
+ py: .5,
6029
+ borderBottom: 1,
6030
+ borderColor: "divider"
5341
6031
  },
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
6032
+ children: [/* @__PURE__ */ jsx(Typography, {
6033
+ sx: {
6034
+ fontSize: 12,
6035
+ fontWeight: 700,
6036
+ color: "text.secondary",
6037
+ flex: 1
6038
+ },
6039
+ children: "Propriedades"
6040
+ }), /* @__PURE__ */ jsx(IconButton, {
6041
+ size: "small",
6042
+ "aria-label": "Recolher propriedades",
6043
+ title: "Recolher",
6044
+ onClick: () => setPanel((current) => ({
6045
+ ...current,
6046
+ open: false
6047
+ })),
6048
+ children: "⟩"
6049
+ })]
6050
+ }),
6051
+ /* @__PURE__ */ jsx(Box, {
6052
+ sx: {
6053
+ overflow: "auto",
6054
+ flex: 1,
6055
+ p: 1.5
6056
+ },
6057
+ children: /* @__PURE__ */ jsx(Properties, {
6058
+ editor,
6059
+ selection,
6060
+ columns,
6061
+ errors,
6062
+ warnings,
6063
+ usage,
6064
+ onSelect: setSelection,
6065
+ onTry,
6066
+ setEditor,
6067
+ patchTable,
6068
+ patchRelationship,
6069
+ patchDimension,
6070
+ patchMeasure,
6071
+ removeTable,
6072
+ commitRename,
6073
+ addField,
6074
+ unplaced
6075
+ })
5378
6076
  })
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
- })]
6077
+ ]
6078
+ }) : /* @__PURE__ */ jsx(Box, {
6079
+ component: "button",
6080
+ type: "button",
6081
+ title: "Mostrar as propriedades",
6082
+ onClick: () => setPanel((current) => ({
6083
+ ...current,
6084
+ open: true
6085
+ })),
6086
+ sx: {
6087
+ width: 28,
6088
+ flexShrink: 0,
6089
+ border: 0,
6090
+ borderLeft: 1,
6091
+ borderColor: "divider",
6092
+ bgcolor: "background.paper",
6093
+ color: "text.secondary",
6094
+ cursor: "pointer",
6095
+ writingMode: "vertical-rl",
6096
+ fontSize: 12,
6097
+ py: 1,
6098
+ "&:hover": { color: "primary.main" }
6099
+ },
6100
+ children: "⟨ Propriedades"
6101
+ })
6102
+ ]
5405
6103
  });
5406
6104
  }
5407
6105
  /** Curva com tangentes horizontais (saída e chegada pelas laterais dos cards). */
@@ -5470,11 +6168,15 @@ function RelationshipLine({ line, hovered, onHover }) {
5470
6168
  ]
5471
6169
  });
5472
6170
  }
5473
- function TableCard({ node, position, size, collapsed, selected, linking, selectedField, onToggle, onScroll, onHeaderDown, onResizeDown, onResizeReset, onColumnDown, onFieldClick, onAddField }) {
6171
+ function TableCard({ node, position, size, collapsed, selected, linking, selectedField, onToggle, onScroll, onHeaderDown, onResizeDown, onResizeReset, onColumnDown, onFieldClick, onAddField, renaming, onRenameStart, onRename, onContextMenu }) {
5474
6172
  const style = ROLE_STYLE[node.role];
5475
6173
  const columnActions = ["dimension", "measure"];
5476
6174
  return /* @__PURE__ */ jsxs(Box, {
5477
6175
  onMouseDown: (event) => event.stopPropagation(),
6176
+ onContextMenu: (event) => onContextMenu(event, {
6177
+ kind: "table",
6178
+ uid: node.uid
6179
+ }),
5478
6180
  sx: {
5479
6181
  position: "absolute",
5480
6182
  left: position.x,
@@ -5515,7 +6217,12 @@ function TableCard({ node, position, size, collapsed, selected, linking, selecte
5515
6217
  },
5516
6218
  children: "▦"
5517
6219
  }),
5518
- /* @__PURE__ */ jsx(Typography, {
6220
+ renaming ? /* @__PURE__ */ jsx(TableNameInput, {
6221
+ value: node.name,
6222
+ onDone: onRename
6223
+ }) : /* @__PURE__ */ jsx(Typography, {
6224
+ onDoubleClick: onRenameStart,
6225
+ title: "Duplo clique para renomear",
5519
6226
  sx: {
5520
6227
  fontWeight: 700,
5521
6228
  fontSize: 13,
@@ -5643,6 +6350,10 @@ function TableCard({ node, position, size, collapsed, selected, linking, selecte
5643
6350
  }, `c:${item.name}`) : /* @__PURE__ */ jsxs(Box, {
5644
6351
  onMouseDown: (event) => event.stopPropagation(),
5645
6352
  onClick: () => onFieldClick(item),
6353
+ onContextMenu: (event) => onContextMenu(event, {
6354
+ kind: item.kind,
6355
+ uid: item.uid
6356
+ }),
5646
6357
  sx: {
5647
6358
  height: ROW,
5648
6359
  display: "flex",
@@ -5738,23 +6449,60 @@ const resizeHandleSx = {
5738
6449
  position: "absolute",
5739
6450
  zIndex: 1
5740
6451
  };
5741
- function PropertyBlock({ title, children }) {
5742
- return /* @__PURE__ */ jsxs(Box, {
5743
- sx: {
5744
- display: "flex",
5745
- flexDirection: "column",
5746
- gap: 1,
5747
- mb: 2
6452
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
6453
+ /** Nome da tabela editado no cabeçalho do card: Enter/sair confirma, Esc cancela. */
6454
+ function TableNameInput({ value, onDone }) {
6455
+ const [text, setText] = useState(value);
6456
+ const valid = IDENTIFIER.test(text);
6457
+ const done = useRef(false);
6458
+ const finish = (name) => {
6459
+ if (done.current) return;
6460
+ done.current = true;
6461
+ onDone(name);
6462
+ };
6463
+ return /* @__PURE__ */ jsx(Box, {
6464
+ component: "input",
6465
+ autoFocus: true,
6466
+ value: text,
6467
+ "aria-label": "Nome da tabela",
6468
+ onFocus: (event) => event.currentTarget.select(),
6469
+ onChange: (event) => setText(event.target.value),
6470
+ onMouseDown: (event) => event.stopPropagation(),
6471
+ onKeyDown: (event) => {
6472
+ event.stopPropagation();
6473
+ if (event.key === "Enter") finish(valid ? text : null);
6474
+ if (event.key === "Escape") finish(null);
5748
6475
  },
5749
- children: [/* @__PURE__ */ jsx(Typography, {
5750
- variant: "overline",
6476
+ onBlur: () => finish(valid ? text : null),
6477
+ sx: {
6478
+ flex: 1,
6479
+ minWidth: 0,
6480
+ font: "inherit",
6481
+ fontSize: 13,
6482
+ fontWeight: 700,
6483
+ color: "text.primary",
6484
+ bgcolor: "#1b1e23",
6485
+ border: "1px solid",
6486
+ borderColor: valid ? "primary.main" : "error.main",
6487
+ borderRadius: .5,
6488
+ px: .5,
6489
+ outline: "none"
6490
+ }
6491
+ });
6492
+ }
6493
+ /** Bloco do painel; com `collapseKey`, o título recolhe/expande (lembrado no navegador). */
6494
+ function PropertyBlock({ title, collapseKey, children }) {
6495
+ return /* @__PURE__ */ jsx(Section, {
6496
+ title,
6497
+ collapseKey,
6498
+ children: /* @__PURE__ */ jsx(Box, {
5751
6499
  sx: {
5752
- color: "text.secondary",
5753
- fontWeight: 700,
5754
- lineHeight: 1.5
6500
+ display: "flex",
6501
+ flexDirection: "column",
6502
+ gap: 1
5755
6503
  },
5756
- children: title
5757
- }), children]
6504
+ children
6505
+ })
5758
6506
  });
5759
6507
  }
5760
6508
  function ColumnSelect({ label, value, options, onChange }) {
@@ -5818,29 +6566,23 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
5818
6566
  return /* @__PURE__ */ jsxs(Fragment, { children: [
5819
6567
  /* @__PURE__ */ jsxs(PropertyBlock, {
5820
6568
  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
- ]
6569
+ collapseKey: "diagram:table",
6570
+ children: [/* @__PURE__ */ jsx(RenameField, {
6571
+ value: table.name,
6572
+ onCommit: (name) => commitRename(table.uid, name)
6573
+ }, table.uid), /* @__PURE__ */ jsx(SqlEditor, {
6574
+ label: "sql",
6575
+ minRows: 3,
6576
+ maxRows: 12,
6577
+ value: table.sql,
6578
+ error: errors[`table:${table.name}`],
6579
+ onChange: (sql) => patchTable(table.uid, { sql }),
6580
+ queryTools: true
6581
+ })]
5841
6582
  }),
5842
6583
  /* @__PURE__ */ jsx(PropertyBlock, {
5843
6584
  title: "Dimensões (marque as colunas)",
6585
+ collapseKey: "diagram:dimensions",
5844
6586
  children: /* @__PURE__ */ jsx(DimensionPicker, {
5845
6587
  editor,
5846
6588
  setEditor,
@@ -5854,6 +6596,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
5854
6596
  }),
5855
6597
  /* @__PURE__ */ jsx(PropertyBlock, {
5856
6598
  title: "Dicionário",
6599
+ collapseKey: "diagram:dictionary",
5857
6600
  children: /* @__PURE__ */ jsx(DictionaryFields, {
5858
6601
  row: table,
5859
6602
  onPatch: (changes) => patchTable(table.uid, changes)
@@ -6040,6 +6783,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6040
6783
  }),
6041
6784
  /* @__PURE__ */ jsx(PropertyBlock, {
6042
6785
  title: "Dicionário",
6786
+ collapseKey: "diagram:dictionary",
6043
6787
  children: /* @__PURE__ */ jsx(DictionaryFields, {
6044
6788
  row,
6045
6789
  onPatch: (changes) => patchDimension(row.uid, changes)
@@ -6092,6 +6836,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6092
6836
  }),
6093
6837
  /* @__PURE__ */ jsx(PropertyBlock, {
6094
6838
  title: "Dicionário",
6839
+ collapseKey: "diagram:dictionary",
6095
6840
  children: /* @__PURE__ */ jsx(DictionaryFields, {
6096
6841
  row,
6097
6842
  onPatch: (changes) => patchMeasure(row.uid, changes)
@@ -6118,6 +6863,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6118
6863
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6119
6864
  /* @__PURE__ */ jsx(PropertyBlock, {
6120
6865
  title: "Modelo",
6866
+ collapseKey: "diagram:model",
6121
6867
  children: /* @__PURE__ */ jsx(Typography, {
6122
6868
  sx: {
6123
6869
  fontSize: 12,
@@ -6128,6 +6874,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6128
6874
  }),
6129
6875
  /* @__PURE__ */ jsxs(PropertyBlock, {
6130
6876
  title: "Relacionamentos",
6877
+ collapseKey: "diagram:relationships",
6131
6878
  children: [!editor.relationships.length && /* @__PURE__ */ jsx(Typography, {
6132
6879
  sx: {
6133
6880
  fontSize: 12,
@@ -6196,6 +6943,7 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6196
6943
  }),
6197
6944
  /* @__PURE__ */ jsxs(PropertyBlock, {
6198
6945
  title: "Medidas calculadas",
6946
+ collapseKey: "diagram:formulas",
6199
6947
  children: [formulas.map((row) => /* @__PURE__ */ jsxs(Button, {
6200
6948
  size: "small",
6201
6949
  onClick: () => onSelect({
@@ -6213,22 +6961,10 @@ function Properties({ editor, selection, columns, errors, warnings, usage, onSel
6213
6961
  ]
6214
6962
  }, row.uid)), /* @__PURE__ */ jsx(Button, {
6215
6963
  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
- },
6964
+ onClick: () => onSelect({
6965
+ kind: "measure",
6966
+ uid: addFormulaTo(setEditor)
6967
+ }),
6232
6968
  children: "+ Medida calculada"
6233
6969
  })]
6234
6970
  }),
@@ -6251,7 +6987,7 @@ function RenameField({ value, onCommit }) {
6251
6987
  value: text,
6252
6988
  onChange: setText,
6253
6989
  onBlur: () => {
6254
- if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(text)) onCommit(text);
6990
+ if (IDENTIFIER.test(text)) onCommit(text);
6255
6991
  else setText(value);
6256
6992
  }
6257
6993
  });
@@ -6429,11 +7165,22 @@ function ModelPanel({ devtools, original, capabilities, tables, onTry, onSaved }
6429
7165
  });
6430
7166
  const completions = useMemo(() => ({
6431
7167
  tables,
6432
- modelColumns: Object.fromEntries(editor.tables.map((row) => [row.name, columns?.tables[row.name] ?? []]))
7168
+ modelColumns: Object.fromEntries(editor.tables.map((row) => [row.name, columns?.tables[row.name] ?? []])),
7169
+ modelSql: Object.fromEntries(editor.tables.map((row) => [row.name, row.sql])),
7170
+ testSql: async (sql) => {
7171
+ const result = await fetcher({
7172
+ model: modelName,
7173
+ devtools: { testSql: sql }
7174
+ });
7175
+ if (!result.devtools?.sqlTest) throw new Error("O servidor não devolveu o teste do SQL");
7176
+ return result.devtools.sqlTest;
7177
+ }
6433
7178
  }), [
6434
7179
  tables,
6435
7180
  columns,
6436
- editor.tables
7181
+ editor.tables,
7182
+ fetcher,
7183
+ modelName
6437
7184
  ]);
6438
7185
  const validationErrors = validation ? Object.entries(validation.errors) : [];
6439
7186
  return /* @__PURE__ */ jsxs(SqlCompletionsContext.Provider, {
@@ -8582,4 +9329,4 @@ function ToggleButton$1({ draft, loading, problems, inspecting, onOpen }) {
8582
9329
  //#endregion
8583
9330
  export { DashboardDevtools as default };
8584
9331
 
8585
- //# sourceMappingURL=DashboardDevtools-D0pzwSgn.js.map
9332
+ //# sourceMappingURL=DashboardDevtools-wk4vMfhy.js.map