@rebasepro/app 0.13.0 → 0.13.1-canary.g18cfeb7

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.
Files changed (43) hide show
  1. package/dist/collections/entity-display-cache.d.ts +75 -0
  2. package/dist/collections/entity-display.d.ts +35 -0
  3. package/dist/collections/entity_image_preview.d.ts +8 -0
  4. package/dist/collections/form-layout.d.ts +6 -0
  5. package/dist/collections/index.d.ts +4 -0
  6. package/dist/collections/property-path.d.ts +16 -0
  7. package/dist/collections/property_presentation.d.ts +18 -3
  8. package/dist/collections/summary-property.d.ts +65 -0
  9. package/dist/components/common/useColumnsIds.d.ts +22 -0
  10. package/dist/core/Rebase.d.ts +1 -1
  11. package/dist/core/RebaseProps.d.ts +10 -2
  12. package/dist/index.es.js +660 -210
  13. package/dist/index.es.js.map +1 -1
  14. package/dist/util/entity_cache.d.ts +19 -3
  15. package/dist/util/previews.d.ts +19 -0
  16. package/package.json +7 -7
  17. package/src/auth/useRebaseAuthController.ts +9 -1
  18. package/src/collections/entity-display-cache.ts +195 -0
  19. package/src/collections/entity-display.ts +105 -0
  20. package/src/collections/entity_image_preview.ts +13 -0
  21. package/src/collections/form-layout.ts +8 -1
  22. package/src/collections/index.ts +4 -0
  23. package/src/collections/property-path.ts +30 -0
  24. package/src/collections/property_presentation.ts +39 -4
  25. package/src/collections/summary-property.ts +114 -0
  26. package/src/collections/title-property.ts +13 -16
  27. package/src/components/common/useColumnsIds.tsx +67 -29
  28. package/src/core/Rebase.tsx +20 -2
  29. package/src/core/RebaseProps.tsx +10 -2
  30. package/src/hooks/data/useCollection.tsx +20 -3
  31. package/src/hooks/data/useFetch.tsx +17 -2
  32. package/src/hooks/data/useRelationSelector.tsx +20 -4
  33. package/src/hooks/useAuthSubscription.ts +18 -2
  34. package/src/hooks/useBuildLocalConfigurationPersistence.tsx +20 -12
  35. package/src/locales/de.ts +1 -0
  36. package/src/locales/en.ts +2 -0
  37. package/src/locales/es.ts +1 -0
  38. package/src/locales/fr.ts +1 -0
  39. package/src/locales/hi.ts +2 -1
  40. package/src/locales/it.ts +1 -0
  41. package/src/locales/pt.ts +1 -0
  42. package/src/util/entity_cache.ts +34 -31
  43. package/src/util/previews.ts +49 -29
package/src/locales/hi.ts CHANGED
@@ -69,7 +69,7 @@ export const hi: RebaseTranslations = {
69
69
  group_by: "इसके द्वारा समूहित करें",
70
70
  initialize_kanban_order: "कानबन क्रम प्रारंभ करें",
71
71
  copy_id: "ID कॉपी करें",
72
- add_specific: "विशिष्ट {{name}} जोड़ें",
72
+ add_specific: "{{name}} जोड़ें",
73
73
  select_specific: "विशिष्ट {{name}} चुनें",
74
74
  select_from: "{{name}} से चुनें",
75
75
  done: "हो गया",
@@ -77,6 +77,7 @@ export const hi: RebaseTranslations = {
77
77
  column_cannot_be_edited: "इस कॉलम को सीधे संपादित नहीं किया जा सकता",
78
78
  close: "बंद करें",
79
79
  hide_list: "सूची छिपाएँ",
80
+ show_list: "सूची दिखाएँ",
80
81
  unsaved_local_changes: "सहेजे नहीं गए स्थानीय परिवर्तन",
81
82
  unsaved_local_changes_description: "इस दस्तावेज़ को स्थानीय स्तर पर संपादित किया गया था और इसमें सहेजे नहीं गए परिवर्तन हैं। यदि आप इन्हें लागू नहीं करते हैं तो ये स्थानीय परिवर्तन खो जाएंगे।",
82
83
  preview_changes: "परिवर्तनों का पूर्वावलोकन करें",
package/src/locales/it.ts CHANGED
@@ -77,6 +77,7 @@ export const it: RebaseTranslations = {
77
77
  column_cannot_be_edited: "Questa colonna non può essere modificata direttamente",
78
78
  close: "Chiudi",
79
79
  hide_list: "Nascondi elenco",
80
+ show_list: "Mostra elenco",
80
81
  unsaved_local_changes: "Modifiche locali non salvate",
81
82
  unsaved_local_changes_description: "Questo documento è stato modificato localmente e ha modifiche non salvate. Queste modifiche locali andranno perse se non le applichi.",
82
83
  preview_changes: "Anteprima modifiche",
package/src/locales/pt.ts CHANGED
@@ -82,6 +82,7 @@ export const pt: RebaseTranslations = {
82
82
  column_cannot_be_edited: "Esta coluna não pode ser editada diretamente",
83
83
  close: "Fechar",
84
84
  hide_list: "Ocultar lista",
85
+ show_list: "Mostrar lista",
85
86
  unsaved_local_changes: "Alterações locais não guardadas",
86
87
  unsaved_local_changes_description: "Este documento foi editado localmente e tem alterações não guardadas. Estas alterações locais serão perdidas se não as aplicar.",
87
88
  preview_changes: "Pré-visualizar alterações",
@@ -4,7 +4,21 @@ import { isObject, isPlainObject } from "@rebasepro/utils";
4
4
  // Define a unique prefix for entity keys in sessionStorage to avoid key collisions
5
5
  const LOCAL_STORAGE_PREFIX = "entity_cache::";
6
6
 
7
- // In-memory cache to store entities for quick access
7
+ /**
8
+ * The in-session handoff channel: an edit in flight, parked here by the layout
9
+ * that is being replaced and picked up by the one taking over — the split's
10
+ * "hide list", full screen's "show list", the side panel's "open full screen".
11
+ * A record loaded from it opens *dirty*, because that is what it is for.
12
+ *
13
+ * Deliberately **not** hydrated from `sessionStorage`, and it is the one thing
14
+ * here that isn't. The persisted half of this file is the local-changes backup:
15
+ * a draft that outlives a reload, offered back through the banner or applied on
16
+ * open, according to the collection's `localChangesBackup`. Loading those
17
+ * drafts into this map on startup made every one of them look like a handoff —
18
+ * the first visit to the record after a reload silently opened dirty carrying a
19
+ * draft the user had walked away from, and the banner that exists to ask about
20
+ * it never appeared.
21
+ */
8
22
  const entityCache: Map<string, object> = new Map();
9
23
 
10
24
  // Check `sessionStorage` availability once during initialization
@@ -93,35 +107,12 @@ function customReviver(_key: string, value: unknown): unknown {
93
107
  return value;
94
108
  }
95
109
 
96
- // Initialize the in-memory cache by loading entities from `sessionStorage`
97
- if (isSessionStorageAvailable) {
98
- try {
99
- // Iterate over all keys in sessionStorage to find those with the specified prefix
100
- for (let i = 0; i < sessionStorage.length; i++) {
101
- const fullKey = sessionStorage.key(i);
102
- if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
103
- const path = fullKey.substring(LOCAL_STORAGE_PREFIX.length);
104
- const entityString = sessionStorage.getItem(fullKey);
105
- if (entityString) {
106
- try {
107
- const entity: object = JSON.parse(entityString, customReviver);
108
- entityCache.set(path, entity);
109
- } catch (parseError) {
110
- console.error(
111
- `Failed to parse entity for path "${path}" from sessionStorage:`,
112
- parseError
113
- );
114
- }
115
- }
116
- }
117
- }
118
- } catch (error) {
119
- console.error("Error accessing sessionStorage during initialization:", error);
120
- }
121
- }
122
-
123
110
  /**
124
- * Saves data to the in-memory cache and persists it individually in `sessionStorage`.
111
+ * Saves data to the local-changes backup in `sessionStorage`.
112
+ *
113
+ * The backup only — a draft that survives a reload is not an edit in flight.
114
+ * {@link saveEntityToMemoryCache} is the other channel; see {@link entityCache}
115
+ * for what separates them.
125
116
  * @param path - The unique path/key for the data.
126
117
  * @param data - The data to cache and persist.
127
118
  */
@@ -142,10 +133,19 @@ export function saveEntityToCache(path: string, data: object): void {
142
133
  }
143
134
  }
144
135
 
136
+ /**
137
+ * Consumes a handoff. The channel is one-shot: whoever picks an edit up owns it
138
+ * from then on, and an edit left behind here is one that reopens a record dirty
139
+ * long after the user stopped looking at it.
140
+ */
145
141
  export function removeEntityFromMemoryCache(path: string): void {
146
142
  entityCache.delete(path);
147
143
  }
148
144
 
145
+ /**
146
+ * Parks an edit in flight for the layout about to take over. See
147
+ * {@link entityCache}: this is the handoff, not the backup.
148
+ */
149
149
  export function saveEntityToMemoryCache(path: string, data: object): void {
150
150
  entityCache.set(path, data);
151
151
  }
@@ -156,8 +156,11 @@ export function getEntityFromMemoryCache(path: string): object | undefined {
156
156
 
157
157
 
158
158
  /**
159
- * Retrieves a entity from the in-memory cache or `sessionStorage`.
160
- * If the entity is not in the cache but exists in `sessionStorage`, it loads it into the cache.
159
+ * Retrieves a entity from the local-changes backup in `sessionStorage`.
160
+ *
161
+ * The backup only — the handoff channel is read through
162
+ * {@link getEntityFromMemoryCache}, and the two mean different things to the
163
+ * form that receives them.
161
164
  * @param path - The unique path/key for the entity.
162
165
  * @returns The cached entity or `undefined` if not found.
163
166
  */
@@ -3,6 +3,7 @@ import type { PropertyConfig, AdminCollection } from "@rebasepro/admin-types";
3
3
  import { AuthController } from "@rebasepro/admin-types";
4
4
  import { isPropertyBuilder } from "@rebasepro/common";
5
5
  import { getTitlePropertyKey } from "@rebasepro/app";
6
+ import { SUMMARY_RANK, isStorageProperty, rankSummaryProperty } from "../collections/summary-property";
6
7
 
7
8
  function isReferenceProperty(property: Property) {
8
9
  if (!property) return null;
@@ -30,24 +31,24 @@ function isHiddenProperty(property: Property | undefined): boolean {
30
31
  }
31
32
 
32
33
  /**
33
- * Returns true when the property holds file-storage content (single image,
34
- * array of images, generic upload, …). These properties are rendered by the
35
- * dedicated image-slot and should NOT appear as regular preview columns.
34
+ * The properties a preview surface should render for a record, best first.
35
+ *
36
+ * Three things decide the answer, in this order:
37
+ *
38
+ * 1. `previewProperties` — passed in, or declared on the collection. A stated
39
+ * list is returned verbatim, ranking and limit included: a developer who
40
+ * asks for the Markdown biography gets the Markdown biography.
41
+ * 2. Whether the value has a one-line form at all. A map renders as a
42
+ * key/value table and a Markdown field as a document; neither fits a card
43
+ * line, so they never take a slot from a value that does. See
44
+ * {@link rankSummaryProperty}.
45
+ * 3. `propertiesOrder`, which breaks ties.
46
+ *
47
+ * The middle step is the one that is easy to get wrong by leaving out.
48
+ * `propertiesOrder` states the *column* order of a collection table — it is
49
+ * not a statement that the first three columns summarise a record, and reading
50
+ * it as one is how a card ends up rendering somebody's entire biography.
36
51
  */
37
- function isStorageProperty(property: Property | undefined): boolean {
38
- if (!property) return false;
39
- // Single string with storage config
40
- if (property.type === "string" && property.storage) return true;
41
- // String displayed as image URL
42
- if (property.type === "string" && property.admin?.urlPreview === "image") return true;
43
- // Array whose inner element has storage config or image URL
44
- if (property.type === "array" && property.of && !Array.isArray(property.of)) {
45
- const inner = property.of;
46
- if (inner.type === "string" && (inner.storage || inner.admin?.urlPreview === "image")) return true;
47
- }
48
- return false;
49
- }
50
-
51
52
  export function getEntityPreviewKeys(
52
53
  authController: AuthController,
53
54
  targetCollection: AdminCollection<any>,
@@ -61,19 +62,38 @@ export function getEntityPreviewKeys(
61
62
  }
62
63
  if (listProperties && listProperties.length > 0) {
63
64
  return listProperties;
64
- } else {
65
- listProperties = (targetCollection.propertiesOrder as string[]) || allProperties;
66
- return listProperties
67
- .filter(key => {
68
- const prop = targetCollection.properties[key];
69
- const isIdProp = prop && typeof prop === "object" && "isId" in prop && Boolean((prop as { isId?: boolean }).isId);
70
- return !isIdProp && key !== "id";
71
- })
72
- .filter(key => {
73
- const property = targetCollection.properties[key];
74
- return property && !isPropertyBuilder(property) && !isReferenceProperty(property) && !isRelationProperty(property) && !isHiddenProperty(property) && !isStorageProperty(property as Property);
75
- }).slice(0, limit);
76
65
  }
66
+
67
+ // Relations read as a link to somewhere else rather than as a value here,
68
+ // so they are left to the dedicated relation chips — unless the collection
69
+ // ordered its properties explicitly, which puts them back in play.
70
+ const hasExplicitOrder = !!(targetCollection.propertiesOrder as string[] | undefined);
71
+ const order = (targetCollection.propertiesOrder as string[]) || allProperties;
72
+
73
+ const direct: string[] = [];
74
+ const excerpt: string[] = [];
75
+
76
+ for (const key of order) {
77
+ const property = targetCollection.properties[key];
78
+ if (!property || isPropertyBuilder(property)) continue;
79
+ if (key === "id" || ("isId" in property && Boolean((property as { isId?: boolean }).isId))) continue;
80
+ if (isReferenceProperty(property)) continue;
81
+ if (!hasExplicitOrder && isRelationProperty(property)) continue;
82
+ if (isHiddenProperty(property)) continue;
83
+ if (isStorageProperty(property as Property)) continue;
84
+
85
+ const rank = rankSummaryProperty(property as Property);
86
+ if (rank === SUMMARY_RANK.UNUSABLE) continue;
87
+ (rank === SUMMARY_RANK.EXCERPT ? excerpt : direct).push(key);
88
+
89
+ // Every remaining slot can already be filled by a value that fits
90
+ // whole, so nothing below can change the answer.
91
+ if (direct.length >= limit) break;
92
+ }
93
+
94
+ // A long-text field is a poor summary but a fine last resort: better an
95
+ // opening line than a card with nothing under the title.
96
+ return [...direct, ...excerpt].slice(0, limit);
77
97
  }
78
98
 
79
99
  // Returned as-is rather than rebuilt per call, so the result is referentially