fsanyoto commited on
Commit
c7eaf1d
Β·
verified Β·
1 Parent(s): b02098c

Deploy AIOS web (React glide grid + FastAPI slice)

Browse files
RELEASES.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "current": "582a6c8",
3
  "releases": [
4
  {
5
  "version": "v53",
 
1
  {
2
+ "current": "bf8387b",
3
  "releases": [
4
  {
5
  "version": "v53",
VERSION CHANGED
@@ -1 +1 @@
1
- 582a6c8
 
1
+ bf8387b
web/src/customer-grid/useVisibleRows.ts CHANGED
@@ -23,8 +23,8 @@ import type {
23
  VisibleRow,
24
  } from "./types";
25
  import { FILTER_OPS, cohortIds, isCohortRule, isFilterGroup, isMeasureRule, isRankOp,
26
- isViewRule, normalizeViewOp, viewRuleId,
27
- isRuleActive, normalizeCohortOp, rhsColId } from "./types";
28
  import { resolveAnchor, resolveWindow } from "./windows";
29
  import { computeAggs } from "./aggregations";
30
  import { NO_RANKING, hasRankLeaf, resolveRankLeaves, stripRankLeaves } from "./rankOps";
@@ -688,9 +688,29 @@ export function matchFilterTree(
688
  * numerics coerced to 0 and outranked real negatives. A blank is only a
689
  * tie-breaker skip, so later sort keys still decide between two blanks.
690
  */
691
- export function makeComparator(sorts: SortSpec, fieldByKey: Map<string, Field>) {
 
 
 
 
692
  return (a: Row, b: Row): number => {
693
  for (const s of sorts) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
  const f = fieldByKey.get(s.colId);
695
  const numeric = f ? isNumericType(f.type) : false;
696
  const av = a[s.colId];
@@ -1096,7 +1116,13 @@ export function runPipeline(input: PipelineInput): VisibleRowsResult {
1096
  const q = search.trim().toLowerCase();
1097
  if (q) out = out.filter((r) => matchSearch(q, r, fields));
1098
 
1099
- if (sorts.length) out = [...out].sort(makeComparator(sorts, fieldByKey));
 
 
 
 
 
 
1100
 
1101
  if (!groupBy) return asRows(out);
1102
 
 
23
  VisibleRow,
24
  } from "./types";
25
  import { FILTER_OPS, cohortIds, isCohortRule, isFilterGroup, isMeasureRule, isRankOp,
26
+ isViewRule, normalizeViewOp, viewRuleId, ROUTE_PROJECTION_KEY, routeStopPosition,
27
+ singleRouteScope, isRuleActive, normalizeCohortOp, rhsColId } from "./types";
28
  import { resolveAnchor, resolveWindow } from "./windows";
29
  import { computeAggs } from "./aggregations";
30
  import { NO_RANKING, hasRankLeaf, resolveRankLeaves, stripRankLeaves } from "./rankOps";
 
688
  * numerics coerced to 0 and outranked real negatives. A blank is only a
689
  * tie-breaker skip, so later sort keys still decide between two blanks.
690
  */
691
+ export function makeComparator(
692
+ sorts: SortSpec,
693
+ fieldByKey: Map<string, Field>,
694
+ routeLabel?: string,
695
+ ) {
696
  return (a: Row, b: Row): number => {
697
  for (const s of sorts) {
698
+ // W42 R22: `routes` carries prose such as "North - 10, South - 2". It is not a
699
+ // sortable text value. The pipeline supplies `routeLabel` only after it has proved that
700
+ // the whole view is inside one named route; a stale stored sort otherwise becomes a
701
+ // no-op here instead of a confident lexical wrong answer.
702
+ if (s.colId === ROUTE_PROJECTION_KEY) {
703
+ if (!routeLabel) continue;
704
+ const av = routeStopPosition(a[s.colId], routeLabel);
705
+ const bv = routeStopPosition(b[s.colId], routeLabel);
706
+ const aBlank = av === null;
707
+ const bBlank = bv === null;
708
+ if (aBlank !== bBlank) return aBlank ? 1 : -1;
709
+ if (aBlank && bBlank) continue;
710
+ const c = (av ?? 0) - (bv ?? 0);
711
+ if (c !== 0) return s.dir === "asc" ? c : -c;
712
+ continue;
713
+ }
714
  const f = fieldByKey.get(s.colId);
715
  const numeric = f ? isNumericType(f.type) : false;
716
  const av = a[s.colId];
 
1116
  const q = search.trim().toLowerCase();
1117
  if (q) out = out.filter((r) => matchSearch(q, r, fields));
1118
 
1119
+ // A route stop number is an ordinal only within one route. `singleRouteScope` is also the
1120
+ // builder's refusal decision, so persisted sorts take the same fail-closed path as new ones.
1121
+ // A missing label deliberately leaves the projection rule as a no-op; raw CSV text is never
1122
+ // used as a fallback comparator.
1123
+ const routeScope = singleRouteScope({ nodes: filters, conj: filterConj }, fields);
1124
+ const routeLabel = routeScope.ok ? fieldByKey.get(routeScope.routeKey)?.label : undefined;
1125
+ if (sorts.length) out = [...out].sort(makeComparator(sorts, fieldByKey, routeLabel));
1126
 
1127
  if (!groupBy) return asRows(out);
1128
 
web/src/filter-kit/FieldsHidePanel.tsx CHANGED
@@ -27,7 +27,7 @@ import { useMemo, useState } from "react";
27
  // miniature, where what a user searches and what they read answer the same question
28
  // differently. It lives in `types.ts` so the node harness can run it: this panel is JSX and
29
  // its search path only fires once somebody types, which no static render can do.
30
- import { fieldEditMode, fieldLabel, isPresetField, offerableFields,
31
  FIELD_EDIT_BLURBS, FIELD_EDIT_LABELS }
32
  from "../customer-grid/types";
33
  import { FieldTypeIcon } from "../customer-grid/icons";
@@ -493,8 +493,17 @@ export function FieldsHidePanel({
493
  * β›” PRESENTATION ONLY. Nothing is deleted and nothing is walled: the payload still carries
494
  * every withheld field, the grid still renders any that are visible, and a route column's own
495
  * doors (its column menu, `patchRouteOrderField`, `deleteRouteOrderField`) are untouched.
 
 
 
 
 
496
  */
497
- const fields = useMemo(() => offerableFields(allFields), [allFields]);
 
 
 
 
498
  /** W41-T27 - one name for "this mount is the Fields manager", so the title, the width class
499
  * and anything a later ticket adds all key off the same answer instead of re-deriving it. */
500
  const managerMode = manager;
 
27
  // miniature, where what a user searches and what they read answer the same question
28
  // differently. It lives in `types.ts` so the node harness can run it: this panel is JSX and
29
  // its search path only fires once somebody types, which no static render can do.
30
+ import { fieldEditMode, fieldLabel, isOfferableField, isPresetField,
31
  FIELD_EDIT_BLURBS, FIELD_EDIT_LABELS }
32
  from "../customer-grid/types";
33
  import { FieldTypeIcon } from "../customer-grid/icons";
 
493
  * β›” PRESENTATION ONLY. Nothing is deleted and nothing is walled: the payload still carries
494
  * every withheld field, the grid still renders any that are visible, and a route column's own
495
  * doors (its column menu, `patchRouteOrderField`, `deleteRouteOrderField`) are untouched.
496
+ *
497
+ * R1's narrow exception is a route field whose host has already offered a delete affordance.
498
+ * It stays in THIS manager only so that affordance remains reachable; it remains absent from
499
+ * every picker. `deletableKeys` is a ceiling supplied by the host, so this cannot make a
500
+ * withheld field deletable or offer any field outside the Fields manager.
501
  */
502
+ const fields = useMemo(
503
+ () => allFields.filter((field) =>
504
+ isOfferableField(field) || !!deletableKeys?.has(field.key)),
505
+ [allFields, deletableKeys]
506
+ );
507
  /** W41-T27 - one name for "this mount is the Fields manager", so the title, the width class
508
  * and anything a later ticket adds all key off the same answer instead of re-deriving it. */
509
  const managerMode = manager;
web/src/filter-kit/fieldClass.ts CHANGED
@@ -862,30 +862,28 @@ export function fieldAuthorBadge(
862
  }
863
 
864
  /**
865
- * β›”β›” R16, AND THE PAYLOAD CANNOT EXPRESS IT YET. The ruling: a SHARED
866
  * definition wins a key collision, the local one is superseded rather than
867
  * deleted, and the field manager says so. The first two halves are real
868
  * server-side facts (`core.field_permissions.migrate_legacy_fields` re-promotes
869
  * a per-user fork of a key on every read, and `routes_tables`' delete door
870
  * clears both strata precisely because a fork survives), but NOTHING ON THE
871
- * WIRE MARKS THE ROW: `routes_customers._merge_shared_fields` skips a shared
872
- * definition whose key the canonical contract already declares (`if k in have:
873
- * continue`) and `routes_tables`' field index is deduped first writer wins, so
874
- * the client receives exactly one entry per key and no record that a second one
875
- * exists.
876
- *
877
- * So this is a C8 READER, not an invented state: it returns the sentence when a
878
- * producer ships `supersededLocal: true` on the entry, and null on every field
879
- * today, which paints nothing. The missing half is named in the handoff rather
880
- * than guessed at here, because the only client-side alternative would be to
881
- * decide a collision happened from a key the server never sent.
882
  */
883
  export const SUPERSEDED_NOTE =
884
  "Superseded: the shared version of this column is the one in use, and your own copy is kept rather than deleted.";
885
 
886
  export function supersededNoteOf(field: unknown): string | null {
887
  if (typeof field !== "object" || field === null) return null;
888
- return (field as { supersededLocal?: unknown }).supersededLocal === true
 
889
  ? SUPERSEDED_NOTE
890
  : null;
891
  }
 
862
  }
863
 
864
  /**
865
+ * β›”β›” R16, AND THE PAYLOAD EXPRESSES IT UNDER THE SERVER'S OWN KEY. The ruling: a SHARED
866
  * definition wins a key collision, the local one is superseded rather than
867
  * deleted, and the field manager says so. The first two halves are real
868
  * server-side facts (`core.field_permissions.migrate_legacy_fields` re-promotes
869
  * a per-user fork of a key on every read, and `routes_tables`' delete door
870
  * clears both strata precisely because a fork survives), but NOTHING ON THE
871
+ * WIRE MARKS THE ROW: `routes_customers._merge_shared_fields` projects the shared
872
+ * definition in place and carries its thin `{ superseded: { stratum: "personal" } }`
873
+ * marker. The local definition stays in the workspace member, but nothing needs to
874
+ * copy it back onto the served field to tell the person what happened.
875
+ *
876
+ * This is a C8 reader, not an invented state: it returns the sentence only for
877
+ * that explicit personal-stratum marker. Any absent, malformed, or other-stratum
878
+ * payload stays null rather than turning an unrelated field into a collision.
 
 
 
879
  */
880
  export const SUPERSEDED_NOTE =
881
  "Superseded: the shared version of this column is the one in use, and your own copy is kept rather than deleted.";
882
 
883
  export function supersededNoteOf(field: unknown): string | null {
884
  if (typeof field !== "object" || field === null) return null;
885
+ const marker = (field as { superseded?: { stratum?: unknown } }).superseded;
886
+ return marker?.stratum === "personal"
887
  ? SUPERSEDED_NOTE
888
  : null;
889
  }