@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.
- package/dist/collections/entity-display-cache.d.ts +75 -0
- package/dist/collections/entity-display.d.ts +35 -0
- package/dist/collections/entity_image_preview.d.ts +8 -0
- package/dist/collections/form-layout.d.ts +6 -0
- package/dist/collections/index.d.ts +4 -0
- package/dist/collections/property-path.d.ts +16 -0
- package/dist/collections/property_presentation.d.ts +18 -3
- package/dist/collections/summary-property.d.ts +65 -0
- package/dist/components/common/useColumnsIds.d.ts +22 -0
- package/dist/core/Rebase.d.ts +1 -1
- package/dist/core/RebaseProps.d.ts +10 -2
- package/dist/index.es.js +660 -210
- package/dist/index.es.js.map +1 -1
- package/dist/util/entity_cache.d.ts +19 -3
- package/dist/util/previews.d.ts +19 -0
- package/package.json +7 -7
- package/src/auth/useRebaseAuthController.ts +9 -1
- package/src/collections/entity-display-cache.ts +195 -0
- package/src/collections/entity-display.ts +105 -0
- package/src/collections/entity_image_preview.ts +13 -0
- package/src/collections/form-layout.ts +8 -1
- package/src/collections/index.ts +4 -0
- package/src/collections/property-path.ts +30 -0
- package/src/collections/property_presentation.ts +39 -4
- package/src/collections/summary-property.ts +114 -0
- package/src/collections/title-property.ts +13 -16
- package/src/components/common/useColumnsIds.tsx +67 -29
- package/src/core/Rebase.tsx +20 -2
- package/src/core/RebaseProps.tsx +10 -2
- package/src/hooks/data/useCollection.tsx +20 -3
- package/src/hooks/data/useFetch.tsx +17 -2
- package/src/hooks/data/useRelationSelector.tsx +20 -4
- package/src/hooks/useAuthSubscription.ts +18 -2
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +20 -12
- package/src/locales/de.ts +1 -0
- package/src/locales/en.ts +2 -0
- package/src/locales/es.ts +1 -0
- package/src/locales/fr.ts +1 -0
- package/src/locales/hi.ts +2 -1
- package/src/locales/it.ts +1 -0
- package/src/locales/pt.ts +1 -0
- package/src/util/entity_cache.ts +34 -31
- 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: "
|
|
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",
|
package/src/util/entity_cache.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
160
|
-
*
|
|
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
|
*/
|
package/src/util/previews.ts
CHANGED
|
@@ -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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|