@rebasepro/app 0.12.1-canary.gf5f1d39 → 0.13.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.
- package/README.md +1 -1
- package/dist/auth/api.d.ts +3 -8
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/types.d.ts +1 -5
- package/dist/collections/form-layout.d.ts +107 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/title-property.d.ts +22 -0
- package/dist/components/Debug/collection-views/sample_data.d.ts +3 -2
- package/dist/hooks/ApiConfigContext.d.ts +36 -2
- package/dist/hooks/useBackendStorageSource.d.ts +6 -1
- package/dist/hooks/useNavigationBlocker.d.ts +1 -1
- package/dist/hooks/useSnackbarController.d.ts +2 -1
- package/dist/hooks/useStudioBridge.d.ts +27 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +681 -186
- package/dist/index.es.js.map +1 -1
- package/dist/internal/common.d.ts +14 -2
- package/package.json +29 -25
- package/src/auth/api.ts +8 -9
- package/src/auth/index.ts +2 -2
- package/src/auth/types.ts +5 -6
- package/src/collections/form-layout.ts +386 -0
- package/src/collections/index.ts +1 -0
- package/src/collections/title-property.ts +41 -0
- package/src/components/Debug/UIReferenceView.tsx +9 -1
- package/src/components/Debug/collection-views/sample_data.ts +4 -2
- package/src/components/NotFoundPage.tsx +1 -1
- package/src/components/common/useDataTableController.tsx +1 -1
- package/src/core/Rebase.tsx +5 -1
- package/src/core/RebaseRouter.tsx +2 -1
- package/src/core/RebaseRoutes.tsx +1 -1
- package/src/hooks/ApiConfigContext.tsx +45 -3
- package/src/hooks/useAuthSubscription.ts +29 -3
- package/src/hooks/useBackendStorageSource.ts +8 -1
- package/src/hooks/useNavigationBlocker.tsx +1 -1
- package/src/hooks/useSnackbarController.tsx +34 -6
- package/src/hooks/useStudioBridge.tsx +50 -2
- package/src/index.ts +1 -1
- package/src/internal/common.tsx +15 -3
- package/src/internal/useRestoreScroll.tsx +1 -1
- package/src/locales/de.ts +23 -2
- package/src/locales/en.ts +23 -1
- package/src/locales/es.ts +23 -1
- package/src/locales/fr.ts +23 -1
- package/src/locales/hi.ts +23 -1
- package/src/locales/it.ts +23 -1
- package/src/locales/pt.ts +23 -1
- package/src/util/enums.ts +4 -2
- package/src/util/useStorageUploadController.tsx +5 -1
|
@@ -687,6 +687,14 @@ selected: true }, { name: "Tags" }].map(c => (
|
|
|
687
687
|
<TextField label="With value" value="Filled value" onChange={() => {}}/>
|
|
688
688
|
<TextField label="Error state" error value="Bad value" onChange={() => {}}/>
|
|
689
689
|
<TextField label="Disabled" disabled value="Read only" onChange={() => {}}/>
|
|
690
|
+
{/* The smaller sizes are the ones dialogs use, and they
|
|
691
|
+
were absent here — which is how a labelled `small`
|
|
692
|
+
field shipped with its label sitting on top of its
|
|
693
|
+
placeholder. Keep all four so the collision is
|
|
694
|
+
visible on this page rather than in a modal. */}
|
|
695
|
+
<TextField size="medium" label="Medium" placeholder="Placeholder under the label"/>
|
|
696
|
+
<TextField size="small" label="Small" placeholder="Placeholder under the label"/>
|
|
697
|
+
<TextField size="smallest" label="Smallest" value="Filled value" onChange={() => {}}/>
|
|
690
698
|
</div>
|
|
691
699
|
</div>
|
|
692
700
|
<div className="col-span-12 sm:col-span-6 flex flex-col gap-4">
|
|
@@ -851,7 +859,7 @@ roles: [] }
|
|
|
851
859
|
<TableCell>
|
|
852
860
|
<div className="flex flex-wrap gap-2">
|
|
853
861
|
{user.roles.map(role => (
|
|
854
|
-
<Chip key={role.id} colorScheme={role.isAdmin ? "
|
|
862
|
+
<Chip key={role.id} colorScheme={role.isAdmin ? "purple" : "blue"} size="small">
|
|
855
863
|
{role.name}
|
|
856
864
|
</Chip>
|
|
857
865
|
))}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ChipColorKey } from "@rebasepro/ui";
|
|
2
|
+
|
|
1
3
|
export type ProjectStatus = "backlog" | "in_progress" | "in_review" | "done";
|
|
2
4
|
export type ProjectPriority = "low" | "medium" | "high" | "critical";
|
|
3
5
|
|
|
@@ -14,14 +16,14 @@ export interface SampleProject {
|
|
|
14
16
|
createdAt: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
export const STATUS_CONFIG: Record<ProjectStatus, { label: string; color:
|
|
19
|
+
export const STATUS_CONFIG: Record<ProjectStatus, { label: string; color: ChipColorKey }> = {
|
|
18
20
|
backlog: { label: "Backlog", color: "gray" },
|
|
19
21
|
in_progress: { label: "In Progress", color: "blue" },
|
|
20
22
|
in_review: { label: "In Review", color: "purple" },
|
|
21
23
|
done: { label: "Done", color: "green" }
|
|
22
24
|
};
|
|
23
25
|
|
|
24
|
-
export const PRIORITY_CONFIG: Record<ProjectPriority, { label: string; color:
|
|
26
|
+
export const PRIORITY_CONFIG: Record<ProjectPriority, { label: string; color: ChipColorKey }> = {
|
|
25
27
|
low: { label: "Low", color: "gray" },
|
|
26
28
|
medium: { label: "Medium", color: "yellow" },
|
|
27
29
|
high: { label: "High", color: "orange" },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { useLocation } from "react-router
|
|
3
|
+
import { useLocation } from "react-router";
|
|
4
4
|
|
|
5
5
|
import { useData, useRebaseContext } from "../../hooks";
|
|
6
6
|
import { useDataOrder } from "../../hooks/data/useDataOrder";
|
package/src/core/Rebase.tsx
CHANGED
|
@@ -415,7 +415,11 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
415
415
|
|
|
416
416
|
if (resolvedApiUrl) {
|
|
417
417
|
return (
|
|
418
|
-
<ApiConfigProvider
|
|
418
|
+
<ApiConfigProvider
|
|
419
|
+
apiUrl={resolvedApiUrl}
|
|
420
|
+
apiPath={client?.apiPath}
|
|
421
|
+
getAuthToken={authController.getAuthToken}
|
|
422
|
+
>
|
|
419
423
|
{content}
|
|
420
424
|
</ApiConfigProvider>
|
|
421
425
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createBrowserRouter
|
|
1
|
+
import { createBrowserRouter } from "react-router"
|
|
2
|
+
import { RouterProvider } from "react-router/dom"
|
|
2
3
|
import { removeInitialAndTrailingSlashes } from "@rebasepro/app";
|
|
3
4
|
|
|
4
5
|
export function RebaseRouter({
|
|
@@ -9,9 +9,24 @@ import React, { useContext } from "react";
|
|
|
9
9
|
*/
|
|
10
10
|
export interface ApiConfig {
|
|
11
11
|
apiUrl: string;
|
|
12
|
+
/**
|
|
13
|
+
* The path the backend mounts its API under, appended to {@link apiUrl}.
|
|
14
|
+
*
|
|
15
|
+
* `"/api"` by default, which is the server's default `basePath`. It is
|
|
16
|
+
* carried here because a good deal of UI builds request URLs by hand
|
|
17
|
+
* instead of going through the client's typed methods, and every one of
|
|
18
|
+
* those sites used to write `/api` as a literal — so a backend configured
|
|
19
|
+
* with any other `basePath` served an admin panel whose auth-provider
|
|
20
|
+
* discovery, storage browser, history panel, user picker and Studio tools
|
|
21
|
+
* all requested paths that did not exist.
|
|
22
|
+
*/
|
|
23
|
+
apiPath: string;
|
|
12
24
|
getAuthToken?: () => Promise<string | null>;
|
|
13
25
|
}
|
|
14
26
|
|
|
27
|
+
/** What the server uses when `basePath` is not configured. */
|
|
28
|
+
export const DEFAULT_API_PATH = "/api";
|
|
29
|
+
|
|
15
30
|
const ApiConfigContext = React.createContext<ApiConfig | undefined>(undefined);
|
|
16
31
|
|
|
17
32
|
/**
|
|
@@ -23,16 +38,43 @@ export function useApiConfig(): ApiConfig | undefined {
|
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
/**
|
|
26
|
-
*
|
|
41
|
+
* `apiUrl` and `apiPath` joined, with no trailing slash — the prefix to build a
|
|
42
|
+
* request URL from.
|
|
43
|
+
*
|
|
44
|
+
* Returns `undefined` when there is no provider, so a caller can keep its own
|
|
45
|
+
* fallback rather than silently requesting a relative path.
|
|
46
|
+
*/
|
|
47
|
+
export function useApiBase(): string | undefined {
|
|
48
|
+
const config = useApiConfig();
|
|
49
|
+
if (!config?.apiUrl) return undefined;
|
|
50
|
+
return config.apiUrl.replace(/\/+$/, "") + (config.apiPath || DEFAULT_API_PATH);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The same prefix, derived from a client rather than from context.
|
|
55
|
+
*
|
|
56
|
+
* For the code that holds a `RebaseClient` but sits outside (or above) an
|
|
57
|
+
* `ApiConfigProvider`. Returns `undefined` when the client has no base URL,
|
|
58
|
+
* which is the signal every caller already treats as "cannot build a request".
|
|
59
|
+
*/
|
|
60
|
+
export function apiBaseOf(client?: { baseUrl?: string; apiPath?: string }): string | undefined {
|
|
61
|
+
if (!client?.baseUrl) return undefined;
|
|
62
|
+
return client.baseUrl.replace(/\/+$/, "") + (client.apiPath || DEFAULT_API_PATH);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Provide API configuration (apiUrl, apiPath, getAuthToken) to the entire subtree.
|
|
27
67
|
* Typically rendered inside `<Rebase>` or at the app root.
|
|
28
68
|
*/
|
|
29
69
|
export function ApiConfigProvider({
|
|
30
70
|
apiUrl,
|
|
71
|
+
apiPath = DEFAULT_API_PATH,
|
|
31
72
|
getAuthToken,
|
|
32
73
|
children
|
|
33
|
-
}: ApiConfig & { children: React.ReactNode }) {
|
|
74
|
+
}: Omit<ApiConfig, "apiPath"> & { apiPath?: string; children: React.ReactNode }) {
|
|
34
75
|
const value = React.useMemo(() => ({ apiUrl,
|
|
35
|
-
|
|
76
|
+
apiPath,
|
|
77
|
+
getAuthToken }), [apiUrl, apiPath, getAuthToken]);
|
|
36
78
|
return (
|
|
37
79
|
<ApiConfigContext.Provider value={value}>
|
|
38
80
|
{children}
|
|
@@ -8,16 +8,42 @@ export function useAuthSubscription(authClient?: AuthClient): AuthController {
|
|
|
8
8
|
const currentSession = authClient?.getSession();
|
|
9
9
|
const [user, setUser] = useState<User | null>(currentSession?.user ?? null);
|
|
10
10
|
|
|
11
|
-
const [initialLoading, setInitialLoading] = useState(!currentSession);
|
|
12
11
|
const [authLoading, setAuthLoading] = useState(false);
|
|
13
12
|
const [authError, setAuthError] = useState<Error>();
|
|
14
13
|
const [loginSkipped, setLoginSkipped] = useState(false);
|
|
15
14
|
const [extra, setExtra] = useState<any>();
|
|
16
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Whether asking the server "who am I?" could possibly answer.
|
|
18
|
+
*
|
|
19
|
+
* A client configured with `persistSession: false` and JSON auth — the
|
|
20
|
+
* shape used when one client borrows another's credential, as the hosted
|
|
21
|
+
* Studio does — has no stored session and no auth cookie, so the probe
|
|
22
|
+
* below is guaranteed to 401. Sending it anyway put three failing
|
|
23
|
+
* `GET /auth/me` calls on the wire per mount, each one landing in the
|
|
24
|
+
* *customer's* request log as an authentication failure against their own
|
|
25
|
+
* backend.
|
|
26
|
+
*
|
|
27
|
+
* A client that does not implement the capability is treated as "might
|
|
28
|
+
* work" — the historical behaviour.
|
|
29
|
+
*/
|
|
30
|
+
const mayHaveRestorableSession = authClient?.canRestoreSession?.() ?? true;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `true` only while a probe is genuinely outstanding.
|
|
34
|
+
*
|
|
35
|
+
* It used to be seeded from `!currentSession` alone, which left a client
|
|
36
|
+
* that cannot restore a session — and therefore never runs the probe that
|
|
37
|
+
* clears the flag — reporting "still loading" forever. Anything gated on
|
|
38
|
+
* `initialLoading` (a spinner, a redirect to the login view) would never
|
|
39
|
+
* resolve.
|
|
40
|
+
*/
|
|
41
|
+
const [initialLoading, setInitialLoading] = useState(!currentSession && mayHaveRestorableSession);
|
|
42
|
+
|
|
17
43
|
useEffect(() => {
|
|
18
44
|
if (!authClient) return;
|
|
19
45
|
// If we don't have a session initially, try to get user which restores session if a persistent token exists
|
|
20
|
-
if (!currentSession) {
|
|
46
|
+
if (!currentSession && mayHaveRestorableSession) {
|
|
21
47
|
setInitialLoading(true);
|
|
22
48
|
authClient.getUser()
|
|
23
49
|
.then(user => {
|
|
@@ -32,7 +58,7 @@ export function useAuthSubscription(authClient?: AuthClient): AuthController {
|
|
|
32
58
|
setInitialLoading(false);
|
|
33
59
|
});
|
|
34
60
|
}
|
|
35
|
-
}, [authClient, currentSession]);
|
|
61
|
+
}, [authClient, currentSession, mayHaveRestorableSession]);
|
|
36
62
|
|
|
37
63
|
useEffect(() => {
|
|
38
64
|
if (!authClient) return;
|
|
@@ -10,12 +10,18 @@ import {
|
|
|
10
10
|
DownloadConfig,
|
|
11
11
|
StorageListResult
|
|
12
12
|
} from "@rebasepro/types";
|
|
13
|
+
import { DEFAULT_API_PATH } from "./ApiConfigContext";
|
|
13
14
|
|
|
14
15
|
export interface BackendStorageSourceProps {
|
|
15
16
|
/**
|
|
16
17
|
* Backend API URL (e.g., 'http://localhost:3001')
|
|
17
18
|
*/
|
|
18
19
|
apiUrl: string;
|
|
20
|
+
/**
|
|
21
|
+
* The path the backend mounts its API under. Defaults to the server's own
|
|
22
|
+
* default; pass the backend's `basePath` if it was configured otherwise.
|
|
23
|
+
*/
|
|
24
|
+
apiPath?: string;
|
|
19
25
|
/**
|
|
20
26
|
* Function to get the current auth token
|
|
21
27
|
*/
|
|
@@ -39,10 +45,11 @@ export interface BackendStorageSourceProps {
|
|
|
39
45
|
*/
|
|
40
46
|
export function useBackendStorageSource({
|
|
41
47
|
apiUrl,
|
|
48
|
+
apiPath = DEFAULT_API_PATH,
|
|
42
49
|
getAuthToken
|
|
43
50
|
}: BackendStorageSourceProps): StorageSource {
|
|
44
51
|
|
|
45
|
-
const storageBasePath = `${apiUrl}/
|
|
52
|
+
const storageBasePath = `${apiUrl.replace(/\/+$/, "")}${apiPath}/storage`;
|
|
46
53
|
|
|
47
54
|
// Cache for download URLs to avoid redundant API calls
|
|
48
55
|
const urlsCache = useMemo(() => new Map<string, DownloadConfig>(), []);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Blocker, BlockerFunction, useBlocker } from "react-router
|
|
2
|
+
import { Blocker, BlockerFunction, useBlocker } from "react-router";
|
|
3
3
|
|
|
4
4
|
const IDLE_BLOCKER: Blocker = {
|
|
5
5
|
state: "unblocked",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { SnackbarMessageType } from "@rebasepro/admin-types";
|
|
1
|
+
import type { SnackbarAction, SnackbarMessageType } from "@rebasepro/admin-types";
|
|
2
2
|
import React, { useCallback, useMemo } from "react";
|
|
3
3
|
import { useSnackbar } from "notistack";
|
|
4
|
-
;
|
|
4
|
+
import { Button } from "@rebasepro/ui";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Hook to retrieve the SnackbarContext.
|
|
@@ -24,11 +24,13 @@ export const useSnackbarController = () => {
|
|
|
24
24
|
title?: string;
|
|
25
25
|
message: React.ReactNode;
|
|
26
26
|
autoHideDuration?: number;
|
|
27
|
+
action?: SnackbarAction;
|
|
27
28
|
}) => {
|
|
28
29
|
const {
|
|
29
30
|
type,
|
|
30
31
|
message,
|
|
31
|
-
autoHideDuration
|
|
32
|
+
autoHideDuration,
|
|
33
|
+
action
|
|
32
34
|
} = props;
|
|
33
35
|
enqueueSnackbar({
|
|
34
36
|
message: props.title ? <div className={"flex flex-col"}>
|
|
@@ -36,13 +38,36 @@ export const useSnackbarController = () => {
|
|
|
36
38
|
{message}
|
|
37
39
|
</div> : message,
|
|
38
40
|
variant: type,
|
|
39
|
-
autoHideDuration
|
|
41
|
+
autoHideDuration,
|
|
42
|
+
// The provider sets `preventDuplicate`, which collapses two
|
|
43
|
+
// snackbars carrying the same message into one. That is right for a
|
|
44
|
+
// confirmation and wrong for an action: deleting two rows in a row
|
|
45
|
+
// produces the same "Deleted" text twice, and swallowing the second
|
|
46
|
+
// would leave the second deletion with no way to undo it. A key
|
|
47
|
+
// makes each one distinct.
|
|
48
|
+
...(action ? { key: nextActionKey() } : {}),
|
|
49
|
+
action: action
|
|
50
|
+
? (key) => (
|
|
51
|
+
<Button variant={"text"}
|
|
52
|
+
size={"small"}
|
|
53
|
+
className={"text-inherit"}
|
|
54
|
+
onClick={() => {
|
|
55
|
+
action.onClick();
|
|
56
|
+
// The offer has been taken; leaving it up
|
|
57
|
+
// invites a second click that undoes nothing,
|
|
58
|
+
// or worse, undoes something else.
|
|
59
|
+
closeSnackbar(key);
|
|
60
|
+
}}>
|
|
61
|
+
{action.label}
|
|
62
|
+
</Button>
|
|
63
|
+
)
|
|
64
|
+
: undefined
|
|
40
65
|
})
|
|
41
|
-
}, []);
|
|
66
|
+
}, [enqueueSnackbar, closeSnackbar]);
|
|
42
67
|
|
|
43
68
|
const close = useCallback(() => {
|
|
44
69
|
closeSnackbar();
|
|
45
|
-
}, []);
|
|
70
|
+
}, [closeSnackbar]);
|
|
46
71
|
|
|
47
72
|
return useMemo(() => ({
|
|
48
73
|
open,
|
|
@@ -50,3 +75,6 @@ export const useSnackbarController = () => {
|
|
|
50
75
|
}), [open, close]);
|
|
51
76
|
|
|
52
77
|
};
|
|
78
|
+
|
|
79
|
+
let actionKeySeq = 0;
|
|
80
|
+
const nextActionKey = () => `rebase-snackbar-action-${actionKeySeq++}`;
|
|
@@ -28,6 +28,32 @@ export interface StudioBridge {
|
|
|
28
28
|
urlController: UrlController;
|
|
29
29
|
navigationState: NavigationStateController;
|
|
30
30
|
breadcrumbs: BreadcrumbsController;
|
|
31
|
+
capabilities: StudioCapabilities;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What the *host* of these tools can do, as opposed to what the backend can.
|
|
36
|
+
*
|
|
37
|
+
* Studio is mounted in two very different places. In a project's own admin
|
|
38
|
+
* panel it runs next to the collection source files and can edit them through
|
|
39
|
+
* the schema-editor routes. In the hosted console it runs against somebody
|
|
40
|
+
* else's deployed container: there is no source to edit — the container is
|
|
41
|
+
* rebuilt from the customer's repository on every deploy — and the routes that
|
|
42
|
+
* would edit it are not mounted at all, because the framework switches the
|
|
43
|
+
* schema editor off under `NODE_ENV=production`.
|
|
44
|
+
*
|
|
45
|
+
* Tools that would otherwise offer a write into the codebase read this to
|
|
46
|
+
* decide whether that write is even meaningful.
|
|
47
|
+
*/
|
|
48
|
+
export interface StudioCapabilities {
|
|
49
|
+
/**
|
|
50
|
+
* Whether the host has the project's collection source at hand and can
|
|
51
|
+
* write to it.
|
|
52
|
+
*
|
|
53
|
+
* Defaults to `true`, which is what an admin panel running beside its own
|
|
54
|
+
* `collectionsDir` has always assumed.
|
|
55
|
+
*/
|
|
56
|
+
codebase: boolean;
|
|
31
57
|
}
|
|
32
58
|
|
|
33
59
|
// ─── Noop defaults ──────────────────────────────────────────────────
|
|
@@ -71,12 +97,17 @@ const NOOP_BREADCRUMBS: BreadcrumbsController = {
|
|
|
71
97
|
updateCount: () => {}
|
|
72
98
|
};
|
|
73
99
|
|
|
100
|
+
const DEFAULT_CAPABILITIES: StudioCapabilities = {
|
|
101
|
+
codebase: true
|
|
102
|
+
};
|
|
103
|
+
|
|
74
104
|
const NOOP_BRIDGE: StudioBridge = {
|
|
75
105
|
collectionRegistry: NOOP_COLLECTION_REGISTRY,
|
|
76
106
|
sidePanelController: NOOP_SIDE_PANEL,
|
|
77
107
|
urlController: NOOP_URL_CONTROLLER,
|
|
78
108
|
navigationState: NOOP_NAVIGATION_STATE,
|
|
79
|
-
breadcrumbs: NOOP_BREADCRUMBS
|
|
109
|
+
breadcrumbs: NOOP_BREADCRUMBS,
|
|
110
|
+
capabilities: DEFAULT_CAPABILITIES
|
|
80
111
|
};
|
|
81
112
|
|
|
82
113
|
// ─── Context & Provider ─────────────────────────────────────────────
|
|
@@ -107,10 +138,22 @@ export function StudioBridgeProvider({
|
|
|
107
138
|
value: Partial<StudioBridge>;
|
|
108
139
|
children: React.ReactNode;
|
|
109
140
|
}) {
|
|
141
|
+
// Keyed on the slices rather than on `value`: callers write the object
|
|
142
|
+
// inline (`value={{ collectionRegistry }}`), so a `[value]` dependency
|
|
143
|
+
// rebuilt the context on every render of the host and re-rendered every
|
|
144
|
+
// Studio consumer with it.
|
|
110
145
|
const merged = React.useMemo(
|
|
111
146
|
() => ({ ...NOOP_BRIDGE,
|
|
112
147
|
...value }),
|
|
113
|
-
|
|
148
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
149
|
+
[
|
|
150
|
+
value.collectionRegistry,
|
|
151
|
+
value.sidePanelController,
|
|
152
|
+
value.urlController,
|
|
153
|
+
value.navigationState,
|
|
154
|
+
value.breadcrumbs,
|
|
155
|
+
value.capabilities
|
|
156
|
+
]
|
|
114
157
|
);
|
|
115
158
|
return (
|
|
116
159
|
<StudioBridgeContext.Provider value={merged}>
|
|
@@ -146,6 +189,11 @@ export function useStudioBreadcrumbs(): BreadcrumbsController {
|
|
|
146
189
|
return useContext(StudioBridgeContext).breadcrumbs;
|
|
147
190
|
}
|
|
148
191
|
|
|
192
|
+
/** What the host can do — see {@link StudioCapabilities}. */
|
|
193
|
+
export function useStudioCapabilities(): StudioCapabilities {
|
|
194
|
+
return useContext(StudioBridgeContext).capabilities;
|
|
195
|
+
}
|
|
196
|
+
|
|
149
197
|
// ─── Self-Assembling Bridge Registry ────────────────────────────────
|
|
150
198
|
|
|
151
199
|
/**
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ export * from "./contexts";
|
|
|
9
9
|
// @rebasepro/admin and @rebasepro/studio consume them. Named explicitly
|
|
10
10
|
// (not `export *`) so this file is the single place that grows or shrinks
|
|
11
11
|
// the internal surface — see the JSDoc on each symbol for details.
|
|
12
|
-
export { CONTAINER_FULL_WIDTH,
|
|
12
|
+
export { CONTAINER_FULL_WIDTH, FORM_CONTAINER_WIDTH, SIDE_PANEL_DEFAULT_WIDTH } from "./internal/common";
|
|
13
13
|
export { useRestoreScroll } from "./internal/useRestoreScroll";
|
|
14
14
|
export { useUnsavedChangesDialog } from "./hooks/useUnsavedChangesDialog";
|
|
15
15
|
export { NavigationBlockerProvider, useNavigationBlocker } from "./hooks/useNavigationBlocker";
|
package/src/internal/common.tsx
CHANGED
|
@@ -9,8 +9,20 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export const CONTAINER_FULL_WIDTH = "100vw";
|
|
11
11
|
|
|
12
|
-
/** @internal See {@link CONTAINER_FULL_WIDTH}. */
|
|
13
|
-
export const ADDITIONAL_TAB_WIDTH = "55vw";
|
|
14
|
-
|
|
15
12
|
/** @internal See {@link CONTAINER_FULL_WIDTH}. */
|
|
16
13
|
export const FORM_CONTAINER_WIDTH = "768px";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How wide the side panel opens when neither the caller nor the collection
|
|
17
|
+
* says otherwise.
|
|
18
|
+
*
|
|
19
|
+
* Sized from what it has to hold rather than from a round number: the form's
|
|
20
|
+
* content column plus its metadata rail plus gutters, which is also the width
|
|
21
|
+
* at which the form stops being a single stack and picks up the same
|
|
22
|
+
* two-column layout it has in full screen. Capped against the viewport so a
|
|
23
|
+
* panel never becomes the whole window on a laptop — at that point you want
|
|
24
|
+
* full screen, and there is a button for it.
|
|
25
|
+
*
|
|
26
|
+
* @internal See {@link CONTAINER_FULL_WIDTH}.
|
|
27
|
+
*/
|
|
28
|
+
export const SIDE_PANEL_DEFAULT_WIDTH = "min(1160px, 88vw)";
|
package/src/locales/de.ts
CHANGED
|
@@ -41,6 +41,8 @@ export const de: RebaseTranslations = {
|
|
|
41
41
|
clear_filter: "Filter löschen",
|
|
42
42
|
clear_filter_sort: "Filter/Sortierung löschen",
|
|
43
43
|
clear_sort: "Sortierung löschen",
|
|
44
|
+
sort: "Sortieren",
|
|
45
|
+
sort_by: "Sortieren nach",
|
|
44
46
|
clear_all: "Alle löschen",
|
|
45
47
|
no_items: "Keine Elemente",
|
|
46
48
|
no_entries_found: "Keine Einträge gefunden",
|
|
@@ -74,6 +76,7 @@ export const de: RebaseTranslations = {
|
|
|
74
76
|
log_out: "Abmelden",
|
|
75
77
|
column_cannot_be_edited: "Diese Spalte kann nicht direkt bearbeitet werden",
|
|
76
78
|
close: "Schließen",
|
|
79
|
+
hide_list: "Liste ausblenden",
|
|
77
80
|
unsaved_local_changes: "Ungespeicherte lokale Änderungen",
|
|
78
81
|
unsaved_local_changes_description: "Dieses Dokument wurde lokal bearbeitet und weist ungespeicherte Änderungen auf. Diese lokalen Änderungen gehen verloren, wenn Sie sie nicht anwenden.",
|
|
79
82
|
preview_changes: "Änderungen in der Vorschau anzeigen",
|
|
@@ -902,6 +905,24 @@ no_filter: "No filter",
|
|
|
902
905
|
// ─── Markdown property ──────────────────────────────────────
|
|
903
906
|
markdown_paste_behavior: "Einfügeverhalten",
|
|
904
907
|
markdown_strip_html: "HTML beim Einfügen entfernen",
|
|
905
|
-
markdown_convert_pasted: "Eingefügten Text in Markdown umwandeln"
|
|
908
|
+
markdown_convert_pasted: "Eingefügten Text in Markdown umwandeln",
|
|
909
|
+
|
|
910
|
+
// ─── Entity identity bar and inspector ──────────────────────
|
|
911
|
+
entity_saved: "Gespeichert",
|
|
912
|
+
more_actions: "Weitere Aktionen",
|
|
913
|
+
record_history: "Verlauf",
|
|
914
|
+
inspect_record: "Datensatz inspizieren",
|
|
915
|
+
inspect: "Inspizieren",
|
|
916
|
+
close_inspector: "Inspektor schließen",
|
|
917
|
+
|
|
918
|
+
// ─── Revision history ───────────────────────────────────────
|
|
919
|
+
history_revert: "Auf diese Version zurücksetzen",
|
|
920
|
+
history_revert_title: "Daten auf diese Version zurücksetzen?",
|
|
921
|
+
history_reverted: "Auf die gewählte Version zurückgesetzt",
|
|
922
|
+
history_revert_error: "Fehler beim Zurücksetzen des Datensatzes",
|
|
923
|
+
history_revert_dirty: "Bitte speichere oder verwirf deine Änderungen vor dem Zurücksetzen",
|
|
924
|
+
history_empty: "Kein Verlauf vorhanden",
|
|
925
|
+
history_empty_description: "Beim Speichern eines Datensatzes wird eine neue Version erstellt und im Verlauf abgelegt.",
|
|
926
|
+
history_new_entity: "Der Verlauf ist nur für vorhandene Datensätze verfügbar",
|
|
927
|
+
history_no_more: "Kein weiterer Verlauf"
|
|
906
928
|
};
|
|
907
|
-
|
package/src/locales/en.ts
CHANGED
|
@@ -51,6 +51,8 @@ export const en: RebaseTranslations = {
|
|
|
51
51
|
clear_filter: "Clear filter",
|
|
52
52
|
clear_filter_sort: "Clear filter/sort",
|
|
53
53
|
clear_sort: "Clear sort",
|
|
54
|
+
sort: "Sort",
|
|
55
|
+
sort_by: "Sort by",
|
|
54
56
|
clear_all: "Clear all",
|
|
55
57
|
no_items: "No items",
|
|
56
58
|
no_entries_found: "No entries found",
|
|
@@ -84,6 +86,7 @@ export const en: RebaseTranslations = {
|
|
|
84
86
|
log_out: "Log out",
|
|
85
87
|
column_cannot_be_edited: "This column can't be edited directly",
|
|
86
88
|
close: "Close",
|
|
89
|
+
hide_list: "Hide list",
|
|
87
90
|
unsaved_local_changes: "Unsaved Local changes",
|
|
88
91
|
unsaved_local_changes_description: "This document was edited locally and has unsaved changes. These local changes will be lost if you don't apply them.",
|
|
89
92
|
preview_changes: "Preview Changes",
|
|
@@ -981,5 +984,24 @@ export const en: RebaseTranslations = {
|
|
|
981
984
|
// ─── Markdown property ──────────────────────────────────────
|
|
982
985
|
markdown_paste_behavior: "Paste behavior",
|
|
983
986
|
markdown_strip_html: "Strip HTML on paste",
|
|
984
|
-
markdown_convert_pasted: "Convert pasted text to markdown"
|
|
987
|
+
markdown_convert_pasted: "Convert pasted text to markdown",
|
|
988
|
+
|
|
989
|
+
// ─── Entity identity bar and inspector ──────────────────────
|
|
990
|
+
entity_saved: "Saved",
|
|
991
|
+
more_actions: "More actions",
|
|
992
|
+
record_history: "History",
|
|
993
|
+
inspect_record: "Inspect record",
|
|
994
|
+
inspect: "Inspect",
|
|
995
|
+
close_inspector: "Close inspector",
|
|
996
|
+
|
|
997
|
+
// ─── Revision history ───────────────────────────────────────
|
|
998
|
+
history_revert: "Revert to this version",
|
|
999
|
+
history_revert_title: "Revert data to this version?",
|
|
1000
|
+
history_reverted: "Reverted to selected version",
|
|
1001
|
+
history_revert_error: "Error reverting entity",
|
|
1002
|
+
history_revert_dirty: "Please save or discard your changes before reverting",
|
|
1003
|
+
history_empty: "No history available",
|
|
1004
|
+
history_empty_description: "When you save a record, a new version is created and stored in the history.",
|
|
1005
|
+
history_new_entity: "History is only available for existing records",
|
|
1006
|
+
history_no_more: "No more history"
|
|
985
1007
|
};
|
package/src/locales/es.ts
CHANGED
|
@@ -49,6 +49,8 @@ export const es: RebaseTranslations = {
|
|
|
49
49
|
clear_filter: "Borrar filtro",
|
|
50
50
|
clear_filter_sort: "Borrar filtro/orden",
|
|
51
51
|
clear_sort: "Borrar orden",
|
|
52
|
+
sort: "Ordenar",
|
|
53
|
+
sort_by: "Ordenar por",
|
|
52
54
|
clear_all: "Borrar todo",
|
|
53
55
|
no_items: "Sin elementos",
|
|
54
56
|
no_entries_found: "No se encontraron entradas",
|
|
@@ -82,6 +84,7 @@ export const es: RebaseTranslations = {
|
|
|
82
84
|
log_out: "Cerrar sesión",
|
|
83
85
|
column_cannot_be_edited: "Esta columna no se puede editar directamente",
|
|
84
86
|
close: "Cerrar",
|
|
87
|
+
hide_list: "Ocultar lista",
|
|
85
88
|
unsaved_local_changes: "Cambios locales sin guardar",
|
|
86
89
|
unsaved_local_changes_description: "Este documento ha sido editado localmente y tiene cambios sin guardar. Estos cambios locales se perderán si no los aplicas.",
|
|
87
90
|
preview_changes: "Previsualizar cambios",
|
|
@@ -923,5 +926,24 @@ no_filter: "No filter",
|
|
|
923
926
|
// ─── Markdown property ──────────────────────────────────────
|
|
924
927
|
markdown_paste_behavior: "Comportamiento al pegar",
|
|
925
928
|
markdown_strip_html: "Eliminar HTML al pegar",
|
|
926
|
-
markdown_convert_pasted: "Convertir texto pegado a markdown"
|
|
929
|
+
markdown_convert_pasted: "Convertir texto pegado a markdown",
|
|
930
|
+
|
|
931
|
+
// ─── Entity identity bar and inspector ──────────────────────
|
|
932
|
+
entity_saved: "Guardado",
|
|
933
|
+
more_actions: "Más acciones",
|
|
934
|
+
record_history: "Historial",
|
|
935
|
+
inspect_record: "Inspeccionar registro",
|
|
936
|
+
inspect: "Inspeccionar",
|
|
937
|
+
close_inspector: "Cerrar el inspector",
|
|
938
|
+
|
|
939
|
+
// ─── Revision history ───────────────────────────────────────
|
|
940
|
+
history_revert: "Volver a esta versión",
|
|
941
|
+
history_revert_title: "¿Volver a esta versión de los datos?",
|
|
942
|
+
history_reverted: "Se volvió a la versión seleccionada",
|
|
943
|
+
history_revert_error: "Error al revertir el registro",
|
|
944
|
+
history_revert_dirty: "Guarda o descarta tus cambios antes de revertir",
|
|
945
|
+
history_empty: "No hay historial disponible",
|
|
946
|
+
history_empty_description: "Al guardar un registro se crea una nueva versión y se almacena en el historial.",
|
|
947
|
+
history_new_entity: "El historial solo está disponible para registros existentes",
|
|
948
|
+
history_no_more: "No hay más historial"
|
|
927
949
|
};
|
package/src/locales/fr.ts
CHANGED
|
@@ -41,6 +41,8 @@ export const fr: RebaseTranslations = {
|
|
|
41
41
|
clear_filter: "Effacer le filtre",
|
|
42
42
|
clear_filter_sort: "Effacer le filtre/tri",
|
|
43
43
|
clear_sort: "Effacer le tri",
|
|
44
|
+
sort: "Trier",
|
|
45
|
+
sort_by: "Trier par",
|
|
44
46
|
clear_all: "Tout effacer",
|
|
45
47
|
no_items: "Aucun élément",
|
|
46
48
|
no_entries_found: "Aucune entrée trouvée",
|
|
@@ -74,6 +76,7 @@ export const fr: RebaseTranslations = {
|
|
|
74
76
|
log_out: "Se déconnecter",
|
|
75
77
|
column_cannot_be_edited: "Cette colonne ne peut pas être modifiée directement",
|
|
76
78
|
close: "Fermer",
|
|
79
|
+
hide_list: "Masquer la liste",
|
|
77
80
|
unsaved_local_changes: "Modifications locales non enregistrées",
|
|
78
81
|
unsaved_local_changes_description: "Ce document a été modifié localement et contient des modifications non enregistrées. Ces modifications locales seront perdues si vous ne les appliquez pas.",
|
|
79
82
|
preview_changes: "Aperçu des modifications",
|
|
@@ -902,5 +905,24 @@ no_filter: "No filter",
|
|
|
902
905
|
// ─── Markdown property ──────────────────────────────────────
|
|
903
906
|
markdown_paste_behavior: "Comportement au collage",
|
|
904
907
|
markdown_strip_html: "Supprimer le HTML au collage",
|
|
905
|
-
markdown_convert_pasted: "Convertir le texte collé en markdown"
|
|
908
|
+
markdown_convert_pasted: "Convertir le texte collé en markdown",
|
|
909
|
+
|
|
910
|
+
// ─── Entity identity bar and inspector ──────────────────────
|
|
911
|
+
entity_saved: "Enregistré",
|
|
912
|
+
more_actions: "Plus d'actions",
|
|
913
|
+
record_history: "Historique",
|
|
914
|
+
inspect_record: "Inspecter l'enregistrement",
|
|
915
|
+
inspect: "Inspecter",
|
|
916
|
+
close_inspector: "Fermer l'inspecteur",
|
|
917
|
+
|
|
918
|
+
// ─── Revision history ───────────────────────────────────────
|
|
919
|
+
history_revert: "Revenir à cette version",
|
|
920
|
+
history_revert_title: "Revenir à cette version des données ?",
|
|
921
|
+
history_reverted: "Retour à la version sélectionnée",
|
|
922
|
+
history_revert_error: "Erreur lors du retour en arrière",
|
|
923
|
+
history_revert_dirty: "Enregistrez ou annulez vos modifications avant de revenir en arrière",
|
|
924
|
+
history_empty: "Aucun historique disponible",
|
|
925
|
+
history_empty_description: "À chaque enregistrement, une nouvelle version est créée et conservée dans l'historique.",
|
|
926
|
+
history_new_entity: "L'historique n'est disponible que pour les enregistrements existants",
|
|
927
|
+
history_no_more: "Plus d'historique"
|
|
906
928
|
};
|