Deploy AIOS web (React glide grid + FastAPI slice)
Browse files- RELEASES.json +7 -1
- VERSION +1 -1
- web/src/connectors/brandMarks.tsx +5 -3
- web/src/customer-grid/CustomerGrid.tsx +206 -202
RELEASES.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
{
|
| 2 |
-
"current": "
|
| 3 |
"releases": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
{
|
| 5 |
"version": "v44",
|
| 6 |
"sha": "a7877aa",
|
|
|
|
| 1 |
{
|
| 2 |
+
"current": "v45 (0b2b501)",
|
| 3 |
"releases": [
|
| 4 |
+
{
|
| 5 |
+
"version": "v45",
|
| 6 |
+
"sha": "0b2b501",
|
| 7 |
+
"date": "2026-08-23",
|
| 8 |
+
"subject": "release v45"
|
| 9 |
+
},
|
| 10 |
{
|
| 11 |
"version": "v44",
|
| 12 |
"sha": "a7877aa",
|
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
|
|
|
|
| 1 |
+
v45 (0b2b501)
|
web/src/connectors/brandMarks.tsx
CHANGED
|
@@ -198,6 +198,11 @@ export function BrandMark({
|
|
| 198 |
// β Hooks run for EVERY name, including the two house glyphs, because a hook
|
| 199 |
// cannot sit behind a conditional return β same reason `useId` is unconditional.
|
| 200 |
const [failed, setFailed] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
if (OFFICIAL.has(name) && !failed)
|
| 202 |
return (
|
| 203 |
<img
|
|
@@ -212,9 +217,6 @@ export function BrandMark({
|
|
| 212 |
onError={() => setFailed(true)}
|
| 213 |
/>
|
| 214 |
);
|
| 215 |
-
// Per-instance, for the reason spelled out at the `<defs>` below. Hooks run for every
|
| 216 |
-
// name, not only Instagram, because a hook cannot sit behind a conditional return.
|
| 217 |
-
const gradId = `aios-brand-ig-${useId()}`;
|
| 218 |
if (name === "instagram")
|
| 219 |
return (
|
| 220 |
<svg {...f} fill="none">
|
|
|
|
| 198 |
// β Hooks run for EVERY name, including the two house glyphs, because a hook
|
| 199 |
// cannot sit behind a conditional return β same reason `useId` is unconditional.
|
| 200 |
const [failed, setFailed] = useState(false);
|
| 201 |
+
// Per-instance, for the reason spelled out at the `<defs>` below. It must also run BEFORE
|
| 202 |
+
// the official-image return: `failed` flips on `onError`, and a hook first reached on that
|
| 203 |
+
// re-render is one more hook than the previous render β React #310 on the exact fallback
|
| 204 |
+
// path this component exists to provide.
|
| 205 |
+
const gradId = `aios-brand-ig-${useId()}`;
|
| 206 |
if (OFFICIAL.has(name) && !failed)
|
| 207 |
return (
|
| 208 |
<img
|
|
|
|
| 217 |
onError={() => setFailed(true)}
|
| 218 |
/>
|
| 219 |
);
|
|
|
|
|
|
|
|
|
|
| 220 |
if (name === "instagram")
|
| 221 |
return (
|
| 222 |
<svg {...f} fill="none">
|
web/src/customer-grid/CustomerGrid.tsx
CHANGED
|
@@ -8,12 +8,12 @@ import {
|
|
| 8 |
useState,
|
| 9 |
} from "react";
|
| 10 |
import { DataEditor, GridCellKind } from "@glideapps/glide-data-grid";
|
| 11 |
-
import type {
|
| 12 |
-
CellClickedEventArgs,
|
| 13 |
-
DataEditorRef,
|
| 14 |
-
EditableGridCell,
|
| 15 |
-
GridCell,
|
| 16 |
-
GridColumn,
|
| 17 |
GridKeyEventArgs,
|
| 18 |
GridMouseEventArgs,
|
| 19 |
GridSelection,
|
|
@@ -124,7 +124,7 @@ import type { GridFolder, ViewPermissions } from "./types";
|
|
| 124 |
import type { FieldStamps } from "./optimism";
|
| 125 |
import { evalFormula, orderFormulas, parseFormula } from "./formulaEngine";
|
| 126 |
import type { FormulaAst } from "./formulaEngine";
|
| 127 |
-
import { CalendarView, KanbanView, ListView, MapSettings, ModeSwitch } from "./viewModes";
|
| 128 |
import { SwipeView } from "./SwipeView";
|
| 129 |
import type { SwipeSpec } from "./SwipeView";
|
| 130 |
// The chart engine lives in `viz/` since EXIT wave 2 (W2-5/Y3) β the grid is now
|
|
@@ -132,8 +132,8 @@ import type { SwipeSpec } from "./SwipeView";
|
|
| 132 |
import { DashboardView } from "../viz/DashboardView";
|
| 133 |
import { cleanCharts } from "../viz/chartData";
|
| 134 |
import type { ChartSpec } from "../viz/chartData";
|
| 135 |
-
import { MapView } from "./MapView";
|
| 136 |
-
import { googleDirectionsUrl, isPlottable } from "./mapProjection";
|
| 137 |
/* βββ W18-C CATALOG βββ (owner item 4, contract C6) */
|
| 138 |
import { CatalogView } from "./CatalogView";
|
| 139 |
import { CATALOG_CODE_FIELD } from "./catalogData";
|
|
@@ -213,25 +213,25 @@ import { windowLabel } from "./windows";
|
|
| 213 |
*/
|
| 214 |
export const GRID_CHAT_OPEN_EVENT = "aios:grid-chat-open";
|
| 215 |
|
| 216 |
-
const ROW_PX: Record<RowHeightMode, number> = { short: 28, medium: 34, tall: 48 };
|
| 217 |
-
|
| 218 |
-
/**
|
| 219 |
-
* W39-T22 β a transport action, not a stored field. Coordinates already travel on every
|
| 220 |
-
* customer row for MapView, but deliberately are not columns. Keeping this synthetic column out
|
| 221 |
-
* of the workspace means a person cannot hide, reorder, rename, or accidentally persist it as
|
| 222 |
-
* though it were customer data.
|
| 223 |
-
*/
|
| 224 |
-
const DIRECTIONS_COLUMN_ID = "__directions__";
|
| 225 |
-
const DIRECTIONS_COLUMN: GridColumn = { id: DIRECTIONS_COLUMN_ID, title: "Go", width: 46 };
|
| 226 |
-
|
| 227 |
-
/** `null` means there is no honest Google destination, including a blank-address row. */
|
| 228 |
-
function directionsFor(row: Row): { lat: number; lon: number } | null {
|
| 229 |
-
const lat = row.lat;
|
| 230 |
-
const lon = row.lon;
|
| 231 |
-
return typeof lat === "number" && typeof lon === "number" && isPlottable(lat, lon)
|
| 232 |
-
? { lat, lon }
|
| 233 |
-
: null;
|
| 234 |
-
}
|
| 235 |
|
| 236 |
/**
|
| 237 |
* Wave-14 R7 β **a button label is ONE LINE.** Every button row in this file is a flex row with
|
|
@@ -1646,8 +1646,8 @@ function CustomerGridSurface({
|
|
| 1646 |
[config.groupBy, isQueryPreview, queryBinding]
|
| 1647 |
);
|
| 1648 |
|
| 1649 |
-
const {
|
| 1650 |
-
visibleCols,
|
| 1651 |
fieldByKey,
|
| 1652 |
order,
|
| 1653 |
visible,
|
|
@@ -1656,19 +1656,19 @@ function CustomerGridSurface({
|
|
| 1656 |
onColumnMoved,
|
| 1657 |
onColumnProposeMove,
|
| 1658 |
setColumnVisible,
|
| 1659 |
-
insertColumn,
|
| 1660 |
-
} = useGridColumns(fields, config, updateConfig);
|
| 1661 |
-
const revealRouteField = useCallback((key: string) => {
|
| 1662 |
-
setColumnVisible(key, true);
|
| 1663 |
-
signal(WORKSPACE_STALE_EVENT);
|
| 1664 |
-
signal(ROWS_STALE_EVENT);
|
| 1665 |
-
}, [setColumnVisible]);
|
| 1666 |
-
// W39-T22 β the transit link is an action, so it is appended to what Glide paints without
|
| 1667 |
-
// joining the saved-view column contract. `onCellClicked` below is the one tap door.
|
| 1668 |
-
const gridColumns = useMemo(
|
| 1669 |
-
() => scope === "customer" ? [...visibleCols, DIRECTIONS_COLUMN] : visibleCols,
|
| 1670 |
-
[scope, visibleCols]
|
| 1671 |
-
);
|
| 1672 |
|
| 1673 |
/**
|
| 1674 |
* β Wave-20 owner item 4 (ruling R8, contract C-ADDROW) β **THE GHOST ROW.**
|
|
@@ -1938,12 +1938,12 @@ function CustomerGridSurface({
|
|
| 1938 |
// Wave-6 item 10 β how this view displays. A WINDOWED table is always the grid: list/
|
| 1939 |
// calendar/kanban compute over the whole matched set, and one page is not it (CG-3's rule,
|
| 1940 |
// the same reason grouping is off there).
|
| 1941 |
-
const displaySpec = cleanDisplay(config.display);
|
| 1942 |
-
const displayMode: DisplayMode = serverWindowed ? "grid" : displaySpec?.mode ?? "grid";
|
| 1943 |
-
const [routePanelOpen, setRoutePanelOpen] = useState(false);
|
| 1944 |
-
useEffect(() => {
|
| 1945 |
-
if (displayMode !== "map") setRoutePanelOpen(false);
|
| 1946 |
-
}, [displayMode]);
|
| 1947 |
|
| 1948 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1949 |
// ββ WAVE 30 Β· W30-T42 (contract C2) β PAGING A READ-THROUGH GRID.
|
|
@@ -2689,8 +2689,8 @@ function CustomerGridSurface({
|
|
| 2689 |
return () => window.clearInterval(id);
|
| 2690 |
}, [pendingMeasureKeys]);
|
| 2691 |
const activePendingKeys = pendingGaveUp ? NO_PENDING_KEYS : pendingMeasureKeys;
|
| 2692 |
-
const baseGetCellContent = useGetCellContent(
|
| 2693 |
-
displayRows,
|
| 2694 |
visibleCols,
|
| 2695 |
fieldByKey,
|
| 2696 |
overlayEdits,
|
|
@@ -2700,44 +2700,44 @@ function CustomerGridSurface({
|
|
| 2700 |
groupLabelSpace,
|
| 2701 |
measureGroupText,
|
| 2702 |
activePendingKeys,
|
| 2703 |
-
pulse
|
| 2704 |
-
);
|
| 2705 |
-
const getCellContent = useCallback(
|
| 2706 |
-
(cell: Item): GridCell => {
|
| 2707 |
-
if (scope !== "customer" || cell[0] !== visibleCols.length) return baseGetCellContent(cell);
|
| 2708 |
-
const row = displayRows[cell[1]];
|
| 2709 |
-
const at = row?.kind === "data" ? directionsFor(row.record) : null;
|
| 2710 |
-
// A blank cell is deliberately not a disabled-looking `Go`: there is no destination to
|
| 2711 |
-
// offer, and a row whose address is blank must not acquire a plausible link to nowhere.
|
| 2712 |
-
if (!at) {
|
| 2713 |
-
return {
|
| 2714 |
-
kind: GridCellKind.Text,
|
| 2715 |
-
data: "",
|
| 2716 |
-
displayData: "",
|
| 2717 |
-
allowOverlay: false,
|
| 2718 |
-
readonly: true,
|
| 2719 |
-
};
|
| 2720 |
-
}
|
| 2721 |
-
return {
|
| 2722 |
-
kind: GridCellKind.Uri,
|
| 2723 |
-
data: googleDirectionsUrl(at.lat, at.lon),
|
| 2724 |
-
displayData: "Go",
|
| 2725 |
-
allowOverlay: false,
|
| 2726 |
-
readonly: true,
|
| 2727 |
-
hoverEffect: true,
|
| 2728 |
-
};
|
| 2729 |
-
},
|
| 2730 |
-
[baseGetCellContent, displayRows, scope, visibleCols.length]
|
| 2731 |
-
);
|
| 2732 |
const { gridSelection, selectedPids, onGridSelectionChange, selectPids, togglePid,
|
| 2733 |
setActiveCell, clearSelection } =
|
| 2734 |
-
useGridSelection(
|
| 2735 |
-
displayRows,
|
| 2736 |
-
displayPidToIndex,
|
| 2737 |
-
gridColumns.length,
|
| 2738 |
-
embeddedSelectable ? embeddedSelectedIds : [],
|
| 2739 |
-
!embedded ? `aios:grid-selection:${scope}` : undefined
|
| 2740 |
-
);
|
| 2741 |
const embeddedSelectedKey = embeddedSelectedIds.join(",");
|
| 2742 |
const selectedPidKey = [...selectedPids].join(",");
|
| 2743 |
useEffect(() => {
|
|
@@ -3775,7 +3775,7 @@ function CustomerGridSurface({
|
|
| 3775 |
// Owner item 10: clicking into the cells is "I am working now" β the frame folds its
|
| 3776 |
// navigation rail to the slim strip (a no-op in the embed; the shell listens).
|
| 3777 |
signal(NAV_MINIMIZE_EVENT);
|
| 3778 |
-
if (row.kind === "group-header") {
|
| 3779 |
event.preventDefault();
|
| 3780 |
setCollapsed((current) => {
|
| 3781 |
const next = new Set(current);
|
|
@@ -3783,19 +3783,19 @@ function CustomerGridSurface({
|
|
| 3783 |
else next.add(row.groupKey);
|
| 3784 |
return next;
|
| 3785 |
});
|
| 3786 |
-
return;
|
| 3787 |
-
}
|
| 3788 |
-
// W39-T22 β Glide paints links on its canvas, so this handler is the ACTUAL touch door.
|
| 3789 |
-
// The Go cell is virtual and always trails the persisted columns; no address-derived string
|
| 3790 |
-
// is used here because coordinates, not an address search, are what the map plotted.
|
| 3791 |
-
if (scope === "customer" && row.kind === "data" && cell[0] === visibleCols.length) {
|
| 3792 |
-
const at = directionsFor(row.record);
|
| 3793 |
-
if (!at) return;
|
| 3794 |
-
event.preventDefault();
|
| 3795 |
-
window.open(googleDirectionsUrl(at.lat, at.lon), "_blank", "noopener,noreferrer");
|
| 3796 |
-
return;
|
| 3797 |
-
}
|
| 3798 |
-
const column = visibleCols[cell[0]];
|
| 3799 |
const field = column ? fieldByKey.get(column.id!) : undefined;
|
| 3800 |
// A picked field (select / assignee) opens its choices where the cell is. It cannot use
|
| 3801 |
// glide's text overlay β a free-text editor on a constrained column is how you end up
|
|
@@ -3859,7 +3859,7 @@ function CustomerGridSurface({
|
|
| 3859 |
: null;
|
| 3860 |
if (href) {
|
| 3861 |
event.preventDefault();
|
| 3862 |
-
window.open(href, "_blank", "noopener,noreferrer");
|
| 3863 |
return;
|
| 3864 |
}
|
| 3865 |
}
|
|
@@ -3883,8 +3883,8 @@ function CustomerGridSurface({
|
|
| 3883 |
// open a record; the hover Expand button below is its replacement, and half of this
|
| 3884 |
// change is a table whose records cannot be opened at all.
|
| 3885 |
},
|
| 3886 |
-
[displayRows, visibleCols, scope, fieldByKey, canEditField, overlayEdits, lockedKey, togglePid,
|
| 3887 |
-
setActiveCell]
|
| 3888 |
);
|
| 3889 |
/**
|
| 3890 |
* Double-click / Enter. KEPT as a door to the record on purpose (owner item 17 names the
|
|
@@ -3986,7 +3986,7 @@ function CustomerGridSurface({
|
|
| 3986 |
gridSelection.current]
|
| 3987 |
);
|
| 3988 |
|
| 3989 |
-
const openHeaderMenu = useCallback(
|
| 3990 |
(column: number, bounds: Rectangle) => {
|
| 3991 |
if (embedded) return;
|
| 3992 |
const definition = visibleCols[column];
|
|
@@ -4003,27 +4003,27 @@ function CustomerGridSurface({
|
|
| 4003 |
},
|
| 4004 |
});
|
| 4005 |
},
|
| 4006 |
-
[embedded, visibleCols]
|
| 4007 |
-
);
|
| 4008 |
-
const onGridColumnResize = useCallback(
|
| 4009 |
-
(column: GridColumn, newSize: number) => {
|
| 4010 |
-
if (column.id === DIRECTIONS_COLUMN_ID) return;
|
| 4011 |
-
onColumnResize(column, newSize);
|
| 4012 |
-
},
|
| 4013 |
-
[onColumnResize]
|
| 4014 |
-
);
|
| 4015 |
-
const onGridColumnMoved = useCallback(
|
| 4016 |
-
(from: number, to: number) => {
|
| 4017 |
-
if (from >= visibleCols.length || to >= visibleCols.length) return;
|
| 4018 |
-
onColumnMoved(from, to);
|
| 4019 |
-
},
|
| 4020 |
-
[onColumnMoved, visibleCols.length]
|
| 4021 |
-
);
|
| 4022 |
-
const onGridColumnProposeMove = useCallback(
|
| 4023 |
-
(from: number, to: number) =>
|
| 4024 |
-
from < visibleCols.length && to < visibleCols.length && onColumnProposeMove(from, to),
|
| 4025 |
-
[onColumnProposeMove, visibleCols.length]
|
| 4026 |
-
);
|
| 4027 |
const onHeaderClicked = useCallback(
|
| 4028 |
(column: number, event: HeaderClickedEventArgs) => {
|
| 4029 |
if (event.isEdge) return;
|
|
@@ -4377,19 +4377,27 @@ function CustomerGridSurface({
|
|
| 4377 |
// β `railViews` and `openScriptView` join for W37-T41's mint-on-pick branch above.
|
| 4378 |
[fields, persistView, views, queryBinding, scope, railViews, openScriptView]
|
| 4379 |
);
|
| 4380 |
-
const renameView = useCallback(
|
| 4381 |
-
(id: string, name: string) => {
|
| 4382 |
-
const view = views.find((item) => item.id === id);
|
| 4383 |
-
if (view && mayEditView(view, viewer)) {
|
| 4384 |
-
const acceptedName = uniqueDisplayName(
|
| 4385 |
-
name,
|
| 4386 |
-
views.filter((item) => item.id !== id).map((item) => item.name)
|
| 4387 |
-
);
|
| 4388 |
-
if (acceptedName !== view.name) persistView({ ...view, name: acceptedName });
|
| 4389 |
-
}
|
| 4390 |
-
},
|
| 4391 |
-
[views, persistView, viewer]
|
| 4392 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4393 |
/**
|
| 4394 |
* I12 (contract C3) β freeze/unfreeze a view's DISPLAY MODE.
|
| 4395 |
*
|
|
@@ -6366,19 +6374,15 @@ function CustomerGridSurface({
|
|
| 6366 |
: false;
|
| 6367 |
// Item 11c β is the menu's field the current frozen boundary (its Pin would be a no-op)?
|
| 6368 |
const menuVisIndex = menuField ? visibleKeys.indexOf(menuField.key) : -1;
|
| 6369 |
-
const menuPinnedTo = menuVisIndex >= 0 && frozenN > 1 && menuVisIndex + 1 === frozenN;
|
| 6370 |
-
|
| 6371 |
-
(name: string) => renameView(activeViewId, name),
|
| 6372 |
-
[activeViewId, renameView]
|
| 6373 |
-
);
|
| 6374 |
-
// Item 10 β the toolbar's mode switcher (grid-only on windowed tables, see displayMode).
|
| 6375 |
const modeControl = serverWindowed || embedded || isQueryPreview ? undefined : (
|
| 6376 |
<ModeSwitch
|
| 6377 |
-
mode={displayMode}
|
| 6378 |
-
onMode={setDisplayMode}
|
| 6379 |
-
viewName={activeView?.name}
|
| 6380 |
-
canRenameView={!!activeView && mayEditView(activeView, viewer)}
|
| 6381 |
-
onRenameView={renameActiveView}
|
| 6382 |
// I12 (C3) β a locked view's mode is frozen, so the switcher stops being a switcher.
|
| 6383 |
// The field pickers beside it stay: C3 freezes the MODE, and a kanban's stack field is
|
| 6384 |
// config, not mode ("filters/sorts/columns stay editable").
|
|
@@ -6406,9 +6410,9 @@ function CustomerGridSurface({
|
|
| 6406 |
calendarMetrics={displaySpec?.calendarMetrics}
|
| 6407 |
// The numeric family, exactly what C-DISP makes eligible β and the same memo the map's
|
| 6408 |
// bubble-size picker feeds from, so "what can be totalled" has one definition.
|
| 6409 |
-
metricChoices={sizeFieldChoices}
|
| 6410 |
-
onCalendarMetrics={setCalendarMetrics}
|
| 6411 |
-
/>
|
| 6412 |
);
|
| 6413 |
// β Wave-23 C7 β the open document, resolved exactly like the picker's value below it:
|
| 6414 |
// the OPTIMISTIC edit wins over the raw record, so a save the server has not echoed yet is
|
|
@@ -6748,11 +6752,11 @@ function CustomerGridSurface({
|
|
| 6748 |
}}
|
| 6749 |
folderAddPreview={serverWindowed ? undefined : folderAddPreview}
|
| 6750 |
onFolderAddToList={serverWindowed ? undefined : folderAddToList}
|
| 6751 |
-
viewer={viewer}
|
| 6752 |
-
onToggleLock={toggleViewLock}
|
| 6753 |
-
onToggleImportant={toggleViewImportant}
|
| 6754 |
-
/>
|
| 6755 |
-
)}
|
| 6756 |
{/* ββ W37-T29 / CONTRACT C6 (rulings R2, R10) β E's CHAT, DOCKED AND OPENABLE.
|
| 6757 |
β THE LAUNCHER SHIPS IN THE SAME CHANGE AS THE MOUNT, and that is not tidiness: a panel
|
| 6758 |
whose only entry point is a prop nobody flips is mounted-but-never-offered, which is the
|
|
@@ -6867,22 +6871,22 @@ function CustomerGridSurface({
|
|
| 6867 |
pool={payload?.workspace?.pool}
|
| 6868 |
serverWindowed={serverWindowed}
|
| 6869 |
cohortAction={cohortAction}
|
| 6870 |
-
filterSeed={filterSeed}
|
| 6871 |
-
modeControl={modeControl}
|
| 6872 |
-
/>
|
| 6873 |
-
{displayMode === "map" && (
|
| 6874 |
-
<MapSettings
|
| 6875 |
-
coordField={mapCoordField}
|
| 6876 |
-
coordChoices={coordFieldChoices}
|
| 6877 |
-
onPickCoordField={onPickCoordField}
|
| 6878 |
-
sizeField={mapSizeField}
|
| 6879 |
-
sizeChoices={sizeFieldChoices}
|
| 6880 |
-
onPickSizeField={(key) => setDisplayField("sizeField", key)}
|
| 6881 |
-
routeOpen={routePanelOpen}
|
| 6882 |
-
selectedCount={selectedPids.size}
|
| 6883 |
-
onToggleRoute={() => setRoutePanelOpen((open) => !open)}
|
| 6884 |
-
/>
|
| 6885 |
-
)}
|
| 6886 |
{/* Wave-10 item 9: the view's description used to render HERE, as a full-width band
|
| 6887 |
between the toolbar and the grid β it cost every row a strip of vertical space and
|
| 6888 |
read as an alert rather than as a label. It now sits under the view's name in the
|
|
@@ -6909,7 +6913,7 @@ function CustomerGridSurface({
|
|
| 6909 |
<>
|
| 6910 |
<DataEditor
|
| 6911 |
ref={gridRef}
|
| 6912 |
-
columns={gridColumns}
|
| 6913 |
getCellContent={getCellContent}
|
| 6914 |
rows={displayRows.length}
|
| 6915 |
theme={lightTheme}
|
|
@@ -6970,9 +6974,9 @@ function CustomerGridSurface({
|
|
| 6970 |
// `false` from here takes the write over from glide's per-cell path.
|
| 6971 |
onDelete={onGridDelete}
|
| 6972 |
validateCell={validateCell}
|
| 6973 |
-
onColumnResize={onGridColumnResize}
|
| 6974 |
-
onColumnMoved={onGridColumnMoved}
|
| 6975 |
-
onColumnProposeMove={onGridColumnProposeMove}
|
| 6976 |
drawHeader={drawGridHeader}
|
| 6977 |
onHeaderMenuClick={(column, bounds) => openHeaderMenu(column, bounds)}
|
| 6978 |
onHeaderClicked={onHeaderClicked}
|
|
@@ -7380,17 +7384,17 @@ function CustomerGridSurface({
|
|
| 7380 |
The write goes through `patchAndRecord`, the SAME door a typed edit takes, so the
|
| 7381 |
undo stack and the permission check are the ones that already exist rather than a
|
| 7382 |
second writer sitting outside every guard. */
|
| 7383 |
-
onGeocoded={(writes) => {
|
| 7384 |
for (const w of writes)
|
| 7385 |
if (mapCoordField && canEditField(mapCoordField))
|
| 7386 |
patchAndRecord(w.pid, { [mapCoordField.key]: w.value }, "a lookup");
|
| 7387 |
-
}}
|
| 7388 |
-
onRouteFieldCreated={revealRouteField}
|
| 7389 |
-
routePanelOpen={routePanelOpen}
|
| 7390 |
-
selectedPids={selectedPids}
|
| 7391 |
-
onSelectPids={selectPids}
|
| 7392 |
-
onOpen={setDetailPid}
|
| 7393 |
-
/>
|
| 7394 |
)}
|
| 7395 |
{/* Owner item 19 β the hover-only Expand. The COUNTERPART to item 17: the single
|
| 7396 |
click that used to open a record now highlights it, and this is where opening
|
|
@@ -7819,7 +7823,7 @@ function CustomerGridSurface({
|
|
| 7819 |
: undefined
|
| 7820 |
}
|
| 7821 |
onDelete={
|
| 7822 |
-
menuField.custom && !menuField.shared && menuField.key !== lockedKey
|
| 7823 |
? () => {
|
| 7824 |
deleteField(menuField.key);
|
| 7825 |
setColumnMenu(null);
|
|
@@ -7852,7 +7856,7 @@ function CustomerGridSurface({
|
|
| 7852 |
}
|
| 7853 |
onDuplicate={
|
| 7854 |
// Creatable strata only β a base Odoo field offers no Duplicate (contract).
|
| 7855 |
-
menuField.custom && !menuField.shared ? () => duplicateField(menuField) : undefined
|
| 7856 |
}
|
| 7857 |
onFormat={(format) => saveField({ ...menuField, format })}
|
| 7858 |
/**
|
|
@@ -7905,21 +7909,21 @@ function CustomerGridSurface({
|
|
| 7905 |
}
|
| 7906 |
onGroupByField={() => updateConfig({ ...config, groupBy: menuField.key })}
|
| 7907 |
onClearGroup={() => updateConfig({ ...config, groupBy: null })}
|
| 7908 |
-
userOptions={payload?.userOptions ?? []}
|
| 7909 |
measures={measures}
|
| 7910 |
-
/* ββ W39-T17 β field shares use the registry's canonical topic key, not the
|
| 7911 |
-
per-user browser storage key carried by `workspace.storageKey`. These are the same
|
| 7912 |
-
server-issued names used by the field-grant wall and the shared overlay write path.
|
| 7913 |
-
Query previews have no durable field share target, so the action is omitted. */
|
| 7914 |
-
tableKey={
|
| 7915 |
-
isQueryPreview
|
| 7916 |
-
? undefined
|
| 7917 |
-
: scope === "product"
|
| 7918 |
-
? "product_data"
|
| 7919 |
-
: scope?.startsWith("ut_")
|
| 7920 |
-
? scope
|
| 7921 |
-
: "customer_data"
|
| 7922 |
-
}
|
| 7923 |
/>
|
| 7924 |
)}
|
| 7925 |
|
|
|
|
| 8 |
useState,
|
| 9 |
} from "react";
|
| 10 |
import { DataEditor, GridCellKind } from "@glideapps/glide-data-grid";
|
| 11 |
+
import type {
|
| 12 |
+
CellClickedEventArgs,
|
| 13 |
+
DataEditorRef,
|
| 14 |
+
EditableGridCell,
|
| 15 |
+
GridCell,
|
| 16 |
+
GridColumn,
|
| 17 |
GridKeyEventArgs,
|
| 18 |
GridMouseEventArgs,
|
| 19 |
GridSelection,
|
|
|
|
| 124 |
import type { FieldStamps } from "./optimism";
|
| 125 |
import { evalFormula, orderFormulas, parseFormula } from "./formulaEngine";
|
| 126 |
import type { FormulaAst } from "./formulaEngine";
|
| 127 |
+
import { CalendarView, KanbanView, ListView, MapSettings, ModeSwitch } from "./viewModes";
|
| 128 |
import { SwipeView } from "./SwipeView";
|
| 129 |
import type { SwipeSpec } from "./SwipeView";
|
| 130 |
// The chart engine lives in `viz/` since EXIT wave 2 (W2-5/Y3) β the grid is now
|
|
|
|
| 132 |
import { DashboardView } from "../viz/DashboardView";
|
| 133 |
import { cleanCharts } from "../viz/chartData";
|
| 134 |
import type { ChartSpec } from "../viz/chartData";
|
| 135 |
+
import { MapView } from "./MapView";
|
| 136 |
+
import { googleDirectionsUrl, isPlottable } from "./mapProjection";
|
| 137 |
/* βββ W18-C CATALOG βββ (owner item 4, contract C6) */
|
| 138 |
import { CatalogView } from "./CatalogView";
|
| 139 |
import { CATALOG_CODE_FIELD } from "./catalogData";
|
|
|
|
| 213 |
*/
|
| 214 |
export const GRID_CHAT_OPEN_EVENT = "aios:grid-chat-open";
|
| 215 |
|
| 216 |
+
const ROW_PX: Record<RowHeightMode, number> = { short: 28, medium: 34, tall: 48 };
|
| 217 |
+
|
| 218 |
+
/**
|
| 219 |
+
* W39-T22 β a transport action, not a stored field. Coordinates already travel on every
|
| 220 |
+
* customer row for MapView, but deliberately are not columns. Keeping this synthetic column out
|
| 221 |
+
* of the workspace means a person cannot hide, reorder, rename, or accidentally persist it as
|
| 222 |
+
* though it were customer data.
|
| 223 |
+
*/
|
| 224 |
+
const DIRECTIONS_COLUMN_ID = "__directions__";
|
| 225 |
+
const DIRECTIONS_COLUMN: GridColumn = { id: DIRECTIONS_COLUMN_ID, title: "Go", width: 46 };
|
| 226 |
+
|
| 227 |
+
/** `null` means there is no honest Google destination, including a blank-address row. */
|
| 228 |
+
function directionsFor(row: Row): { lat: number; lon: number } | null {
|
| 229 |
+
const lat = row.lat;
|
| 230 |
+
const lon = row.lon;
|
| 231 |
+
return typeof lat === "number" && typeof lon === "number" && isPlottable(lat, lon)
|
| 232 |
+
? { lat, lon }
|
| 233 |
+
: null;
|
| 234 |
+
}
|
| 235 |
|
| 236 |
/**
|
| 237 |
* Wave-14 R7 β **a button label is ONE LINE.** Every button row in this file is a flex row with
|
|
|
|
| 1646 |
[config.groupBy, isQueryPreview, queryBinding]
|
| 1647 |
);
|
| 1648 |
|
| 1649 |
+
const {
|
| 1650 |
+
visibleCols,
|
| 1651 |
fieldByKey,
|
| 1652 |
order,
|
| 1653 |
visible,
|
|
|
|
| 1656 |
onColumnMoved,
|
| 1657 |
onColumnProposeMove,
|
| 1658 |
setColumnVisible,
|
| 1659 |
+
insertColumn,
|
| 1660 |
+
} = useGridColumns(fields, config, updateConfig);
|
| 1661 |
+
const revealRouteField = useCallback((key: string) => {
|
| 1662 |
+
setColumnVisible(key, true);
|
| 1663 |
+
signal(WORKSPACE_STALE_EVENT);
|
| 1664 |
+
signal(ROWS_STALE_EVENT);
|
| 1665 |
+
}, [setColumnVisible]);
|
| 1666 |
+
// W39-T22 β the transit link is an action, so it is appended to what Glide paints without
|
| 1667 |
+
// joining the saved-view column contract. `onCellClicked` below is the one tap door.
|
| 1668 |
+
const gridColumns = useMemo(
|
| 1669 |
+
() => scope === "customer" ? [...visibleCols, DIRECTIONS_COLUMN] : visibleCols,
|
| 1670 |
+
[scope, visibleCols]
|
| 1671 |
+
);
|
| 1672 |
|
| 1673 |
/**
|
| 1674 |
* β Wave-20 owner item 4 (ruling R8, contract C-ADDROW) β **THE GHOST ROW.**
|
|
|
|
| 1938 |
// Wave-6 item 10 β how this view displays. A WINDOWED table is always the grid: list/
|
| 1939 |
// calendar/kanban compute over the whole matched set, and one page is not it (CG-3's rule,
|
| 1940 |
// the same reason grouping is off there).
|
| 1941 |
+
const displaySpec = cleanDisplay(config.display);
|
| 1942 |
+
const displayMode: DisplayMode = serverWindowed ? "grid" : displaySpec?.mode ?? "grid";
|
| 1943 |
+
const [routePanelOpen, setRoutePanelOpen] = useState(false);
|
| 1944 |
+
useEffect(() => {
|
| 1945 |
+
if (displayMode !== "map") setRoutePanelOpen(false);
|
| 1946 |
+
}, [displayMode]);
|
| 1947 |
|
| 1948 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1949 |
// ββ WAVE 30 Β· W30-T42 (contract C2) β PAGING A READ-THROUGH GRID.
|
|
|
|
| 2689 |
return () => window.clearInterval(id);
|
| 2690 |
}, [pendingMeasureKeys]);
|
| 2691 |
const activePendingKeys = pendingGaveUp ? NO_PENDING_KEYS : pendingMeasureKeys;
|
| 2692 |
+
const baseGetCellContent = useGetCellContent(
|
| 2693 |
+
displayRows,
|
| 2694 |
visibleCols,
|
| 2695 |
fieldByKey,
|
| 2696 |
overlayEdits,
|
|
|
|
| 2700 |
groupLabelSpace,
|
| 2701 |
measureGroupText,
|
| 2702 |
activePendingKeys,
|
| 2703 |
+
pulse
|
| 2704 |
+
);
|
| 2705 |
+
const getCellContent = useCallback(
|
| 2706 |
+
(cell: Item): GridCell => {
|
| 2707 |
+
if (scope !== "customer" || cell[0] !== visibleCols.length) return baseGetCellContent(cell);
|
| 2708 |
+
const row = displayRows[cell[1]];
|
| 2709 |
+
const at = row?.kind === "data" ? directionsFor(row.record) : null;
|
| 2710 |
+
// A blank cell is deliberately not a disabled-looking `Go`: there is no destination to
|
| 2711 |
+
// offer, and a row whose address is blank must not acquire a plausible link to nowhere.
|
| 2712 |
+
if (!at) {
|
| 2713 |
+
return {
|
| 2714 |
+
kind: GridCellKind.Text,
|
| 2715 |
+
data: "",
|
| 2716 |
+
displayData: "",
|
| 2717 |
+
allowOverlay: false,
|
| 2718 |
+
readonly: true,
|
| 2719 |
+
};
|
| 2720 |
+
}
|
| 2721 |
+
return {
|
| 2722 |
+
kind: GridCellKind.Uri,
|
| 2723 |
+
data: googleDirectionsUrl(at.lat, at.lon),
|
| 2724 |
+
displayData: "Go",
|
| 2725 |
+
allowOverlay: false,
|
| 2726 |
+
readonly: true,
|
| 2727 |
+
hoverEffect: true,
|
| 2728 |
+
};
|
| 2729 |
+
},
|
| 2730 |
+
[baseGetCellContent, displayRows, scope, visibleCols.length]
|
| 2731 |
+
);
|
| 2732 |
const { gridSelection, selectedPids, onGridSelectionChange, selectPids, togglePid,
|
| 2733 |
setActiveCell, clearSelection } =
|
| 2734 |
+
useGridSelection(
|
| 2735 |
+
displayRows,
|
| 2736 |
+
displayPidToIndex,
|
| 2737 |
+
gridColumns.length,
|
| 2738 |
+
embeddedSelectable ? embeddedSelectedIds : [],
|
| 2739 |
+
!embedded ? `aios:grid-selection:${scope}` : undefined
|
| 2740 |
+
);
|
| 2741 |
const embeddedSelectedKey = embeddedSelectedIds.join(",");
|
| 2742 |
const selectedPidKey = [...selectedPids].join(",");
|
| 2743 |
useEffect(() => {
|
|
|
|
| 3775 |
// Owner item 10: clicking into the cells is "I am working now" β the frame folds its
|
| 3776 |
// navigation rail to the slim strip (a no-op in the embed; the shell listens).
|
| 3777 |
signal(NAV_MINIMIZE_EVENT);
|
| 3778 |
+
if (row.kind === "group-header") {
|
| 3779 |
event.preventDefault();
|
| 3780 |
setCollapsed((current) => {
|
| 3781 |
const next = new Set(current);
|
|
|
|
| 3783 |
else next.add(row.groupKey);
|
| 3784 |
return next;
|
| 3785 |
});
|
| 3786 |
+
return;
|
| 3787 |
+
}
|
| 3788 |
+
// W39-T22 β Glide paints links on its canvas, so this handler is the ACTUAL touch door.
|
| 3789 |
+
// The Go cell is virtual and always trails the persisted columns; no address-derived string
|
| 3790 |
+
// is used here because coordinates, not an address search, are what the map plotted.
|
| 3791 |
+
if (scope === "customer" && row.kind === "data" && cell[0] === visibleCols.length) {
|
| 3792 |
+
const at = directionsFor(row.record);
|
| 3793 |
+
if (!at) return;
|
| 3794 |
+
event.preventDefault();
|
| 3795 |
+
window.open(googleDirectionsUrl(at.lat, at.lon), "_blank", "noopener,noreferrer");
|
| 3796 |
+
return;
|
| 3797 |
+
}
|
| 3798 |
+
const column = visibleCols[cell[0]];
|
| 3799 |
const field = column ? fieldByKey.get(column.id!) : undefined;
|
| 3800 |
// A picked field (select / assignee) opens its choices where the cell is. It cannot use
|
| 3801 |
// glide's text overlay β a free-text editor on a constrained column is how you end up
|
|
|
|
| 3859 |
: null;
|
| 3860 |
if (href) {
|
| 3861 |
event.preventDefault();
|
| 3862 |
+
window.open(href, "_blank", "noopener,noreferrer");
|
| 3863 |
return;
|
| 3864 |
}
|
| 3865 |
}
|
|
|
|
| 3883 |
// open a record; the hover Expand button below is its replacement, and half of this
|
| 3884 |
// change is a table whose records cannot be opened at all.
|
| 3885 |
},
|
| 3886 |
+
[displayRows, visibleCols, scope, fieldByKey, canEditField, overlayEdits, lockedKey, togglePid,
|
| 3887 |
+
setActiveCell]
|
| 3888 |
);
|
| 3889 |
/**
|
| 3890 |
* Double-click / Enter. KEPT as a door to the record on purpose (owner item 17 names the
|
|
|
|
| 3986 |
gridSelection.current]
|
| 3987 |
);
|
| 3988 |
|
| 3989 |
+
const openHeaderMenu = useCallback(
|
| 3990 |
(column: number, bounds: Rectangle) => {
|
| 3991 |
if (embedded) return;
|
| 3992 |
const definition = visibleCols[column];
|
|
|
|
| 4003 |
},
|
| 4004 |
});
|
| 4005 |
},
|
| 4006 |
+
[embedded, visibleCols]
|
| 4007 |
+
);
|
| 4008 |
+
const onGridColumnResize = useCallback(
|
| 4009 |
+
(column: GridColumn, newSize: number) => {
|
| 4010 |
+
if (column.id === DIRECTIONS_COLUMN_ID) return;
|
| 4011 |
+
onColumnResize(column, newSize);
|
| 4012 |
+
},
|
| 4013 |
+
[onColumnResize]
|
| 4014 |
+
);
|
| 4015 |
+
const onGridColumnMoved = useCallback(
|
| 4016 |
+
(from: number, to: number) => {
|
| 4017 |
+
if (from >= visibleCols.length || to >= visibleCols.length) return;
|
| 4018 |
+
onColumnMoved(from, to);
|
| 4019 |
+
},
|
| 4020 |
+
[onColumnMoved, visibleCols.length]
|
| 4021 |
+
);
|
| 4022 |
+
const onGridColumnProposeMove = useCallback(
|
| 4023 |
+
(from: number, to: number) =>
|
| 4024 |
+
from < visibleCols.length && to < visibleCols.length && onColumnProposeMove(from, to),
|
| 4025 |
+
[onColumnProposeMove, visibleCols.length]
|
| 4026 |
+
);
|
| 4027 |
const onHeaderClicked = useCallback(
|
| 4028 |
(column: number, event: HeaderClickedEventArgs) => {
|
| 4029 |
if (event.isEdge) return;
|
|
|
|
| 4377 |
// β `railViews` and `openScriptView` join for W37-T41's mint-on-pick branch above.
|
| 4378 |
[fields, persistView, views, queryBinding, scope, railViews, openScriptView]
|
| 4379 |
);
|
| 4380 |
+
const renameView = useCallback(
|
| 4381 |
+
(id: string, name: string) => {
|
| 4382 |
+
const view = views.find((item) => item.id === id);
|
| 4383 |
+
if (view && mayEditView(view, viewer)) {
|
| 4384 |
+
const acceptedName = uniqueDisplayName(
|
| 4385 |
+
name,
|
| 4386 |
+
views.filter((item) => item.id !== id).map((item) => item.name)
|
| 4387 |
+
);
|
| 4388 |
+
if (acceptedName !== view.name) persistView({ ...view, name: acceptedName });
|
| 4389 |
+
}
|
| 4390 |
+
},
|
| 4391 |
+
[views, persistView, viewer]
|
| 4392 |
+
);
|
| 4393 |
+
// β This hook must stay ABOVE the `loading` early-return far below: a hook after that
|
| 4394 |
+
// return first runs only once loading flips off, so the same mounted instance renders one
|
| 4395 |
+
// MORE hook than its loading render and React #310 takes down the whole surface. That is
|
| 4396 |
+
// exactly how v44 crashed Odoo customers for every mount that began on the spinner.
|
| 4397 |
+
const renameActiveView = useCallback(
|
| 4398 |
+
(name: string) => renameView(activeViewId, name),
|
| 4399 |
+
[activeViewId, renameView]
|
| 4400 |
+
);
|
| 4401 |
/**
|
| 4402 |
* I12 (contract C3) β freeze/unfreeze a view's DISPLAY MODE.
|
| 4403 |
*
|
|
|
|
| 6374 |
: false;
|
| 6375 |
// Item 11c β is the menu's field the current frozen boundary (its Pin would be a no-op)?
|
| 6376 |
const menuVisIndex = menuField ? visibleKeys.indexOf(menuField.key) : -1;
|
| 6377 |
+
const menuPinnedTo = menuVisIndex >= 0 && frozenN > 1 && menuVisIndex + 1 === frozenN;
|
| 6378 |
+
// Item 10 β the toolbar's mode switcher (grid-only on windowed tables, see displayMode).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6379 |
const modeControl = serverWindowed || embedded || isQueryPreview ? undefined : (
|
| 6380 |
<ModeSwitch
|
| 6381 |
+
mode={displayMode}
|
| 6382 |
+
onMode={setDisplayMode}
|
| 6383 |
+
viewName={activeView?.name}
|
| 6384 |
+
canRenameView={!!activeView && mayEditView(activeView, viewer)}
|
| 6385 |
+
onRenameView={renameActiveView}
|
| 6386 |
// I12 (C3) β a locked view's mode is frozen, so the switcher stops being a switcher.
|
| 6387 |
// The field pickers beside it stay: C3 freezes the MODE, and a kanban's stack field is
|
| 6388 |
// config, not mode ("filters/sorts/columns stay editable").
|
|
|
|
| 6410 |
calendarMetrics={displaySpec?.calendarMetrics}
|
| 6411 |
// The numeric family, exactly what C-DISP makes eligible β and the same memo the map's
|
| 6412 |
// bubble-size picker feeds from, so "what can be totalled" has one definition.
|
| 6413 |
+
metricChoices={sizeFieldChoices}
|
| 6414 |
+
onCalendarMetrics={setCalendarMetrics}
|
| 6415 |
+
/>
|
| 6416 |
);
|
| 6417 |
// β Wave-23 C7 β the open document, resolved exactly like the picker's value below it:
|
| 6418 |
// the OPTIMISTIC edit wins over the raw record, so a save the server has not echoed yet is
|
|
|
|
| 6752 |
}}
|
| 6753 |
folderAddPreview={serverWindowed ? undefined : folderAddPreview}
|
| 6754 |
onFolderAddToList={serverWindowed ? undefined : folderAddToList}
|
| 6755 |
+
viewer={viewer}
|
| 6756 |
+
onToggleLock={toggleViewLock}
|
| 6757 |
+
onToggleImportant={toggleViewImportant}
|
| 6758 |
+
/>
|
| 6759 |
+
)}
|
| 6760 |
{/* ββ W37-T29 / CONTRACT C6 (rulings R2, R10) β E's CHAT, DOCKED AND OPENABLE.
|
| 6761 |
β THE LAUNCHER SHIPS IN THE SAME CHANGE AS THE MOUNT, and that is not tidiness: a panel
|
| 6762 |
whose only entry point is a prop nobody flips is mounted-but-never-offered, which is the
|
|
|
|
| 6871 |
pool={payload?.workspace?.pool}
|
| 6872 |
serverWindowed={serverWindowed}
|
| 6873 |
cohortAction={cohortAction}
|
| 6874 |
+
filterSeed={filterSeed}
|
| 6875 |
+
modeControl={modeControl}
|
| 6876 |
+
/>
|
| 6877 |
+
{displayMode === "map" && (
|
| 6878 |
+
<MapSettings
|
| 6879 |
+
coordField={mapCoordField}
|
| 6880 |
+
coordChoices={coordFieldChoices}
|
| 6881 |
+
onPickCoordField={onPickCoordField}
|
| 6882 |
+
sizeField={mapSizeField}
|
| 6883 |
+
sizeChoices={sizeFieldChoices}
|
| 6884 |
+
onPickSizeField={(key) => setDisplayField("sizeField", key)}
|
| 6885 |
+
routeOpen={routePanelOpen}
|
| 6886 |
+
selectedCount={selectedPids.size}
|
| 6887 |
+
onToggleRoute={() => setRoutePanelOpen((open) => !open)}
|
| 6888 |
+
/>
|
| 6889 |
+
)}
|
| 6890 |
{/* Wave-10 item 9: the view's description used to render HERE, as a full-width band
|
| 6891 |
between the toolbar and the grid β it cost every row a strip of vertical space and
|
| 6892 |
read as an alert rather than as a label. It now sits under the view's name in the
|
|
|
|
| 6913 |
<>
|
| 6914 |
<DataEditor
|
| 6915 |
ref={gridRef}
|
| 6916 |
+
columns={gridColumns}
|
| 6917 |
getCellContent={getCellContent}
|
| 6918 |
rows={displayRows.length}
|
| 6919 |
theme={lightTheme}
|
|
|
|
| 6974 |
// `false` from here takes the write over from glide's per-cell path.
|
| 6975 |
onDelete={onGridDelete}
|
| 6976 |
validateCell={validateCell}
|
| 6977 |
+
onColumnResize={onGridColumnResize}
|
| 6978 |
+
onColumnMoved={onGridColumnMoved}
|
| 6979 |
+
onColumnProposeMove={onGridColumnProposeMove}
|
| 6980 |
drawHeader={drawGridHeader}
|
| 6981 |
onHeaderMenuClick={(column, bounds) => openHeaderMenu(column, bounds)}
|
| 6982 |
onHeaderClicked={onHeaderClicked}
|
|
|
|
| 7384 |
The write goes through `patchAndRecord`, the SAME door a typed edit takes, so the
|
| 7385 |
undo stack and the permission check are the ones that already exist rather than a
|
| 7386 |
second writer sitting outside every guard. */
|
| 7387 |
+
onGeocoded={(writes) => {
|
| 7388 |
for (const w of writes)
|
| 7389 |
if (mapCoordField && canEditField(mapCoordField))
|
| 7390 |
patchAndRecord(w.pid, { [mapCoordField.key]: w.value }, "a lookup");
|
| 7391 |
+
}}
|
| 7392 |
+
onRouteFieldCreated={revealRouteField}
|
| 7393 |
+
routePanelOpen={routePanelOpen}
|
| 7394 |
+
selectedPids={selectedPids}
|
| 7395 |
+
onSelectPids={selectPids}
|
| 7396 |
+
onOpen={setDetailPid}
|
| 7397 |
+
/>
|
| 7398 |
)}
|
| 7399 |
{/* Owner item 19 β the hover-only Expand. The COUNTERPART to item 17: the single
|
| 7400 |
click that used to open a record now highlights it, and this is where opening
|
|
|
|
| 7823 |
: undefined
|
| 7824 |
}
|
| 7825 |
onDelete={
|
| 7826 |
+
menuField.custom && !menuField.shared && menuField.key !== lockedKey
|
| 7827 |
? () => {
|
| 7828 |
deleteField(menuField.key);
|
| 7829 |
setColumnMenu(null);
|
|
|
|
| 7856 |
}
|
| 7857 |
onDuplicate={
|
| 7858 |
// Creatable strata only β a base Odoo field offers no Duplicate (contract).
|
| 7859 |
+
menuField.custom && !menuField.shared ? () => duplicateField(menuField) : undefined
|
| 7860 |
}
|
| 7861 |
onFormat={(format) => saveField({ ...menuField, format })}
|
| 7862 |
/**
|
|
|
|
| 7909 |
}
|
| 7910 |
onGroupByField={() => updateConfig({ ...config, groupBy: menuField.key })}
|
| 7911 |
onClearGroup={() => updateConfig({ ...config, groupBy: null })}
|
| 7912 |
+
userOptions={payload?.userOptions ?? []}
|
| 7913 |
measures={measures}
|
| 7914 |
+
/* ββ W39-T17 β field shares use the registry's canonical topic key, not the
|
| 7915 |
+
per-user browser storage key carried by `workspace.storageKey`. These are the same
|
| 7916 |
+
server-issued names used by the field-grant wall and the shared overlay write path.
|
| 7917 |
+
Query previews have no durable field share target, so the action is omitted. */
|
| 7918 |
+
tableKey={
|
| 7919 |
+
isQueryPreview
|
| 7920 |
+
? undefined
|
| 7921 |
+
: scope === "product"
|
| 7922 |
+
? "product_data"
|
| 7923 |
+
: scope?.startsWith("ut_")
|
| 7924 |
+
? scope
|
| 7925 |
+
: "customer_data"
|
| 7926 |
+
}
|
| 7927 |
/>
|
| 7928 |
)}
|
| 7929 |
|