@rebasepro/app 0.23.0 → 0.24.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/dist/auth/api.d.ts +2 -2
- package/dist/auth/types.d.ts +57 -4
- package/dist/collections/filter-operator-resolution.d.ts +18 -0
- package/dist/collections/navigation_utils.d.ts +8 -2
- package/dist/collections/property_presentation.d.ts +12 -15
- package/dist/components/ConnectionLostBanner.d.ts +31 -0
- package/dist/components/LoginView/email-link.d.ts +14 -5
- package/dist/components/LoginView/index.d.ts +2 -0
- package/dist/components/UserSelectPopover.d.ts +14 -1
- package/dist/components/email-change-message.d.ts +6 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/contexts/CollectionResolverContext.d.ts +11 -1
- package/dist/debug.js +47 -40
- package/dist/hooks/data/collectionWindow.d.ts +133 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/index.es.js +3332 -279
- package/dist/index.es.js.map +1 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/realtime.d.ts +18 -0
- package/dist/vitePlugin.js +121 -12
- package/package.json +7 -7
package/dist/auth/api.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* here is the unauthenticated `/api/auth/config` probe used to detect
|
|
9
9
|
* bootstrap mode and enabled providers before a session exists.
|
|
10
10
|
*/
|
|
11
|
-
import type {
|
|
11
|
+
import type { AuthConfigDocument } from "@rebasepro/types";
|
|
12
12
|
/**
|
|
13
13
|
* What `GET /api/auth/config` answers.
|
|
14
14
|
*
|
|
@@ -17,7 +17,7 @@ import type { AuthAdapterCapabilities } from "@rebasepro/types";
|
|
|
17
17
|
* this one listed an `emailServiceEnabled` flag no backend sends, and marked as
|
|
18
18
|
* optional fields every backend always returns.
|
|
19
19
|
*/
|
|
20
|
-
export type AuthConfigResponse =
|
|
20
|
+
export type AuthConfigResponse = AuthConfigDocument;
|
|
21
21
|
/**
|
|
22
22
|
* Cache container for `fetchAuthConfig` — holds both the inflight promise
|
|
23
23
|
* (to deduplicate concurrent calls) and the cached result.
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -21,7 +21,14 @@ export type RebaseAuthController = AuthController & {
|
|
|
21
21
|
/** Login with email and password */
|
|
22
22
|
emailPasswordLogin: (email: string, password: string) => Promise<void>;
|
|
23
23
|
/** Register a new user */
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Create an account. `confirmationRequired` when the backend registers
|
|
26
|
+
* confirm-first (`requireEmailVerification`): nobody is signed in until
|
|
27
|
+
* the address is confirmed.
|
|
28
|
+
*/
|
|
29
|
+
register: (email: string, password: string, displayName?: string) => Promise<{
|
|
30
|
+
confirmationRequired: boolean;
|
|
31
|
+
}>;
|
|
25
32
|
/** Skip login (for anonymous access if enabled) */
|
|
26
33
|
skipLogin: () => void;
|
|
27
34
|
/** Whether login was skipped */
|
|
@@ -36,10 +43,33 @@ export type RebaseAuthController = AuthController & {
|
|
|
36
43
|
forgotPassword: (email: string) => Promise<void>;
|
|
37
44
|
/** Reset password using token from email */
|
|
38
45
|
resetPassword: (token: string, password: string) => Promise<void>;
|
|
39
|
-
/**
|
|
40
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Confirm an email address using the token from a verification email.
|
|
48
|
+
* Refused with `PROOF_REQUIRED` when the account holds a password this
|
|
49
|
+
* call does not prove: pass `password` to keep it (and sign in), or
|
|
50
|
+
* `removeUnproven` to verify without it.
|
|
51
|
+
*/
|
|
52
|
+
verifyEmail: (token: string, options?: {
|
|
53
|
+
password?: string;
|
|
54
|
+
removeUnproven?: boolean;
|
|
55
|
+
}) => Promise<{
|
|
56
|
+
passwordRemoved: boolean;
|
|
57
|
+
signedIn: boolean;
|
|
58
|
+
}>;
|
|
59
|
+
/** Sign in with a magic link's token. Absent when the client cannot. */
|
|
60
|
+
magicLinkLogin?: (token: string) => Promise<void>;
|
|
41
61
|
/** Change password for authenticated user */
|
|
42
62
|
changePassword: (oldPassword: string, newPassword: string) => Promise<void>;
|
|
63
|
+
/** Ask to move the signed-in account to another address. Absent when the client cannot. */
|
|
64
|
+
changeEmail?: (newEmail: string) => Promise<{
|
|
65
|
+
pendingEmail: string;
|
|
66
|
+
expiresAt: string;
|
|
67
|
+
}>;
|
|
68
|
+
/** Follow an address-change link. Absent when the client cannot. */
|
|
69
|
+
confirmEmailChange?: (token: string) => Promise<{
|
|
70
|
+
email: string;
|
|
71
|
+
removedProviders: string[];
|
|
72
|
+
}>;
|
|
43
73
|
/** Update user profile */
|
|
44
74
|
updateProfile: (displayName?: string, photoURL?: string) => Promise<User>;
|
|
45
75
|
/** Fetch active sessions */
|
|
@@ -64,6 +94,8 @@ export type RebaseAuthController = AuthController & {
|
|
|
64
94
|
* the client has no MFA support.
|
|
65
95
|
*/
|
|
66
96
|
verifyMfaChallenge?: (mfaToken: string, challengeId: string, code: string) => Promise<void>;
|
|
97
|
+
/** The scopes the signed-in user holds; `undefined` until known. */
|
|
98
|
+
heldScopes?: string[];
|
|
67
99
|
};
|
|
68
100
|
/**
|
|
69
101
|
* Structural type for the subset of `client.auth` (from `@rebasepro/client`)
|
|
@@ -91,8 +123,23 @@ export interface ClientAuth {
|
|
|
91
123
|
resetPasswordForEmail(email: string): Promise<unknown>;
|
|
92
124
|
resetPassword(token: string, password: string): Promise<unknown>;
|
|
93
125
|
/** Optional so a hand-built auth client need not implement it. */
|
|
94
|
-
verifyEmail?(token: string
|
|
126
|
+
verifyEmail?(token: string, options?: {
|
|
127
|
+
password?: string;
|
|
128
|
+
removeUnproven?: boolean;
|
|
129
|
+
}): Promise<unknown>;
|
|
95
130
|
changePassword(oldPassword: string, newPassword: string): Promise<unknown>;
|
|
131
|
+
/** Optional so a hand-built auth client need not implement it. */
|
|
132
|
+
verifyMagicLink?(token: string): Promise<unknown>;
|
|
133
|
+
/** Optional so a hand-built auth client need not implement it. */
|
|
134
|
+
changeEmail?(newEmail: string): Promise<{
|
|
135
|
+
pendingEmail: string;
|
|
136
|
+
expiresAt: string;
|
|
137
|
+
}>;
|
|
138
|
+
/** Optional so a hand-built auth client need not implement it. */
|
|
139
|
+
confirmEmailChange?(token: string): Promise<{
|
|
140
|
+
email: string;
|
|
141
|
+
removedProviders: string[];
|
|
142
|
+
}>;
|
|
96
143
|
updateUser(updates: {
|
|
97
144
|
displayName?: string;
|
|
98
145
|
photoURL?: string;
|
|
@@ -131,6 +178,12 @@ export interface RebaseAuthControllerProps {
|
|
|
131
178
|
setAuthTokenGetter: (getter: () => Promise<string | null>) => void;
|
|
132
179
|
};
|
|
133
180
|
auth?: ClientAuth;
|
|
181
|
+
/** Read for the scopes the signed-in user holds (`GET /auth/scopes`). */
|
|
182
|
+
personalKeys?: {
|
|
183
|
+
listScopes(): Promise<{
|
|
184
|
+
held: string[];
|
|
185
|
+
}>;
|
|
186
|
+
};
|
|
134
187
|
};
|
|
135
188
|
/** Google OAuth client ID (optional, enables Google login) */
|
|
136
189
|
googleClientId?: string;
|
|
@@ -55,3 +55,21 @@ export interface ResolveFilterOperatorsParams {
|
|
|
55
55
|
* @group Models
|
|
56
56
|
*/
|
|
57
57
|
export declare function resolveFilterOperators({ property, isArray, engine }: ResolveFilterOperatorsParams): WhereFilterOp[];
|
|
58
|
+
/**
|
|
59
|
+
* Whether a property can be filtered at all — the one answer the table header
|
|
60
|
+
* and the filters dialog both give.
|
|
61
|
+
*
|
|
62
|
+
* They used to answer it with a predicate each, and disagreed: the header
|
|
63
|
+
* allowed an array only of strings, numbers, dates, references and relations;
|
|
64
|
+
* the dialog allowed an array of anything, and listed an array of maps with an
|
|
65
|
+
* empty control. The header, for its part, ignored both the developer's
|
|
66
|
+
* `filterOperators: []` and a custom `admin.Filter`.
|
|
67
|
+
*
|
|
68
|
+
* A property with a custom filter field is filterable: the developer owns that
|
|
69
|
+
* field. Otherwise it is filterable when at least one operator survives
|
|
70
|
+
* {@link resolveFilterOperators} — for an array, on its item type. A tuple
|
|
71
|
+
* array (`of: [a, b]`) has no single item type to filter by.
|
|
72
|
+
*
|
|
73
|
+
* @group Models
|
|
74
|
+
*/
|
|
75
|
+
export declare function isPropertyFilterable(property: Property, engine?: string): boolean;
|
|
@@ -13,8 +13,14 @@ export declare function resolveCollectionPathIds(path: string, allCollections: A
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function getCollectionBySlugWithin(slugOrPath: string, collections: AdminCollection[]): AdminCollection | undefined;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Every leading run of whole segments of a path, longest first: the
|
|
17
|
+
* candidates for the collection the path starts with.
|
|
18
|
+
* "sites/es/locales" => ["sites/es/locales", "sites/es", "sites"]
|
|
19
|
+
*
|
|
20
|
+
* Every length, not only the odd ones. A slug may contain slashes, so
|
|
21
|
+
* `content/podcasts/abc123/episodes` starts with the two-segment collection
|
|
22
|
+
* `content/podcasts`, and a path's own segment count says nothing about where
|
|
23
|
+
* its first collection ends.
|
|
18
24
|
* @param subpaths
|
|
19
25
|
*/
|
|
20
26
|
export declare function getCollectionPathsCombinations(subpaths: string[]): string[];
|
|
@@ -3,23 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* These read a property's admin block — `readOnly`, `disabled.hidden`, the
|
|
5
5
|
* declarative `conditions` — and lived in `@rebasepro/common`, which is on the
|
|
6
|
-
* backend's dependency path. Nothing in core ever called them: `isReadOnly
|
|
7
|
-
* `isHidden`
|
|
8
|
-
* production caller at all.
|
|
6
|
+
* backend's dependency path. Nothing in core ever called them: `isReadOnly`,
|
|
7
|
+
* `isHidden` and `applyPropertyConditions` are the panel's alone.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* `applyPropertyConditions` is called by the record form, through
|
|
10
|
+
* `resolveFormProperty`, both where a field is laid out and where the form is
|
|
11
|
+
* judged on save — so a rule the collection editor writes hides, disables and
|
|
12
|
+
* requires what it says it does, and a field it hides is not held against the
|
|
13
|
+
* save. Until that was wired, the evaluator was reached only from its own tests.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* deliberate, not an oversight: it is the difference between a condition that
|
|
22
|
-
* needs an entity to be evaluated against and one that does not.
|
|
15
|
+
* A condition may also be a literal: `hidden`/`readOnly`/`disabled` stated as a
|
|
16
|
+
* plain boolean rather than as a rule. A literal needs no context, so
|
|
17
|
+
* `isHidden`/`isReadOnly`/`isDisabled` answer it directly, and those three
|
|
18
|
+
* gates are consulted everywhere a field is laid out — including places with
|
|
19
|
+
* no entity to evaluate a rule against.
|
|
23
20
|
*/
|
|
24
21
|
import type { ConditionContext, Property } from "@rebasepro/types";
|
|
25
22
|
export declare function isReadOnly(property: Property): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { RealtimeConnectionState } from "@rebasepro/types";
|
|
3
|
+
/**
|
|
4
|
+
* The realtime connection's state, re-rendering on every change.
|
|
5
|
+
*
|
|
6
|
+
* `undefined` when there is no client, or it was built without realtime —
|
|
7
|
+
* there is no connection to describe.
|
|
8
|
+
*
|
|
9
|
+
* @group Hooks and utilities
|
|
10
|
+
*/
|
|
11
|
+
export declare function useRealtimeConnectionState(): RealtimeConnectionState | undefined;
|
|
12
|
+
export interface ConnectionLostBannerProps {
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Says that live updates have stopped, while they have.
|
|
17
|
+
*
|
|
18
|
+
* Shown once the realtime client reports the connection `disconnected`: the
|
|
19
|
+
* socket has been down for longer than a blip, so tables, boards and open
|
|
20
|
+
* records are showing what they last received and nothing else will arrive
|
|
21
|
+
* until it is back. They keep their data rather than turning into errors — this
|
|
22
|
+
* banner is how the user learns it may be out of date. It disappears by itself
|
|
23
|
+
* when the connection returns, and every live view then refreshes.
|
|
24
|
+
*
|
|
25
|
+
* A brief drop (`reconnecting`) shows nothing: a deploy blips every socket for
|
|
26
|
+
* a second or two, and a banner that flashes on each one teaches people to
|
|
27
|
+
* ignore it.
|
|
28
|
+
*
|
|
29
|
+
* @group Components
|
|
30
|
+
*/
|
|
31
|
+
export declare function ConnectionLostBanner({ className }: ConnectionLostBannerProps): React.JSX.Element | null;
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The server builds them from the configured frontend URL:
|
|
5
5
|
* `<frontend>/reset-password?token=…` for a forgotten password, an admin's
|
|
6
|
-
* "send reset email" and every invitation,
|
|
7
|
-
* to confirm an address
|
|
8
|
-
*
|
|
6
|
+
* "send reset email" and every invitation, `<frontend>/verify-email?token=…`
|
|
7
|
+
* to confirm an address, `<frontend>/confirm-email-change?token=…` to move an
|
|
8
|
+
* account to a new one, and `<frontend>/auth/magic-link?token=…` to sign in.
|
|
9
|
+
* The frontend URL may carry a base path (`/admin`), so the action is read off
|
|
10
|
+
* the *end* of the path, not the whole path.
|
|
9
11
|
*/
|
|
10
12
|
export type EmailLinkAction = {
|
|
11
13
|
kind: "reset-password";
|
|
@@ -13,14 +15,21 @@ export type EmailLinkAction = {
|
|
|
13
15
|
} | {
|
|
14
16
|
kind: "verify-email";
|
|
15
17
|
token: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "confirm-email-change";
|
|
20
|
+
token: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "magic-link";
|
|
23
|
+
token: string;
|
|
16
24
|
};
|
|
17
25
|
export declare function readEmailLinkAction(location: {
|
|
18
26
|
pathname: string;
|
|
19
27
|
search: string;
|
|
20
28
|
}): EmailLinkAction | null;
|
|
21
29
|
/**
|
|
22
|
-
* Where the app lives: the link's own address without the action
|
|
23
|
-
* its token. `/admin/reset-password?token=…` becomes `/admin
|
|
30
|
+
* Where the app lives: the link's own address without the action's path and
|
|
31
|
+
* its token. `/admin/reset-password?token=…` becomes `/admin/`, and
|
|
32
|
+
* `/admin/auth/magic-link?token=…` too.
|
|
24
33
|
*/
|
|
25
34
|
export declare function appAddressOfEmailLink(location: {
|
|
26
35
|
origin: string;
|
|
@@ -28,6 +28,19 @@ export interface UserSelectPopoverProps {
|
|
|
28
28
|
* Whether users are currently loading.
|
|
29
29
|
*/
|
|
30
30
|
loading?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Called with the search text each time it changes (debounced), for a
|
|
33
|
+
* parent that searches its users on a server rather than passing them all
|
|
34
|
+
* in `users`. The list is still filtered here by the same text.
|
|
35
|
+
*/
|
|
36
|
+
onSearchTextChange?: (searchText: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* For a parent that lists only the first page of a server search: how many
|
|
39
|
+
* users that search matched in all. Leave it out when `users` holds every
|
|
40
|
+
* match. When set, the header counts the listed users against it and a
|
|
41
|
+
* notice says the others are not listed.
|
|
42
|
+
*/
|
|
43
|
+
totalCount?: number;
|
|
31
44
|
/**
|
|
32
45
|
* The current user (displayed as the "self" option at the top).
|
|
33
46
|
*/
|
|
@@ -60,4 +73,4 @@ export interface UserSelectPopoverProps {
|
|
|
60
73
|
*
|
|
61
74
|
* @group Components
|
|
62
75
|
*/
|
|
63
|
-
export declare function UserSelectPopover({ selectedUser, onUserSelected, users, loading, currentUser, defaultLabel, renderLimit, className, size }: UserSelectPopoverProps): React.JSX.Element;
|
|
76
|
+
export declare function UserSelectPopover({ selectedUser, onUserSelected, users, loading, onSearchTextChange, totalCount, currentUser, defaultLabel, renderLimit, className, size }: UserSelectPopoverProps): React.JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the settings screen and the link's screen say about a refused address
|
|
3
|
+
* change: the codes a person can act on get the panel's own, translated words,
|
|
4
|
+
* and anything else keeps the server's message.
|
|
5
|
+
*/
|
|
6
|
+
export declare function emailChangeErrorMessage(error: unknown, t: (key: string) => string): string;
|
|
@@ -5,6 +5,12 @@ import React from "react";
|
|
|
5
5
|
export type CollectionResolver = (slug: string) => {
|
|
6
6
|
properties?: Record<string, unknown>;
|
|
7
7
|
} | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Translate the path a collection is addressed by into the path its driver
|
|
10
|
+
* stores it under — `fs_diagnosis` into `diagnosis`, for a Firestore collection
|
|
11
|
+
* that declares `path: "diagnosis"`. See `EntityDataOptions.resolveDataPath`.
|
|
12
|
+
*/
|
|
13
|
+
export type DataPathResolver = (path: string) => string | undefined;
|
|
8
14
|
/**
|
|
9
15
|
* Lets the layer that owns the collections hand a resolver *up* to the data
|
|
10
16
|
* layer, which needs a collection's primary keys to give its rows an address.
|
|
@@ -15,9 +21,13 @@ export type CollectionResolver = (slug: string) => {
|
|
|
15
21
|
* prop without giving every headless BaaS app a collections argument it has no
|
|
16
22
|
* use for.
|
|
17
23
|
*
|
|
24
|
+
* The same layer knows where each collection is stored, so it hands up the
|
|
25
|
+
* data-path translation alongside: a direct driver has to be given a
|
|
26
|
+
* collection's declared `path`, and only the registry knows it.
|
|
27
|
+
*
|
|
18
28
|
* Registration is a ref write, so it is safe to call during render — and it has
|
|
19
29
|
* to be. The views that fetch rows are *below* the registrar, and child effects
|
|
20
30
|
* run before parent effects, so registering in an effect would bind after the
|
|
21
31
|
* first page of rows had already been converted.
|
|
22
32
|
*/
|
|
23
|
-
export declare const CollectionResolverRegistrationContext: React.Context<(resolver: CollectionResolver | undefined) => void>;
|
|
33
|
+
export declare const CollectionResolverRegistrationContext: React.Context<(resolver: CollectionResolver | undefined, resolveDataPath?: DataPathResolver) => void>;
|
package/dist/debug.js
CHANGED
|
@@ -77,11 +77,11 @@ var UIStyleGuide = () => {
|
|
|
77
77
|
/* @__PURE__ */ jsx(Typography, { variant: "h4", gutterBottom: true, className: "border-b pb-2 mb-6", children: "Icon Buttons" }),
|
|
78
78
|
/* @__PURE__ */ jsx(Paper, { className: "p-6", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-8 items-center", children: [
|
|
79
79
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2", children: [
|
|
80
|
-
/* @__PURE__ */ jsx(IconButton, {
|
|
80
|
+
/* @__PURE__ */ jsx(IconButton, { children: /* @__PURE__ */ jsx(IconExample, {}) }),
|
|
81
81
|
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-text-disabled font-mono", children: "primary" })
|
|
82
82
|
] }),
|
|
83
83
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2", children: [
|
|
84
|
-
/* @__PURE__ */ jsx(IconButton, {
|
|
84
|
+
/* @__PURE__ */ jsx(IconButton, { children: /* @__PURE__ */ jsx(IconExample, {}) }),
|
|
85
85
|
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-text-disabled font-mono", children: "secondary" })
|
|
86
86
|
] }),
|
|
87
87
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2", children: [
|
|
@@ -93,7 +93,7 @@ var UIStyleGuide = () => {
|
|
|
93
93
|
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-text-disabled font-mono", children: "disabled" })
|
|
94
94
|
] }),
|
|
95
95
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2", children: [
|
|
96
|
-
/* @__PURE__ */ jsx(IconButton, { size: "small",
|
|
96
|
+
/* @__PURE__ */ jsx(IconButton, { size: "small", children: /* @__PURE__ */ jsx(Trash2Icon, {}) }),
|
|
97
97
|
/* @__PURE__ */ jsx("span", { className: "text-[10px] text-text-disabled font-mono", children: "delete" })
|
|
98
98
|
] })
|
|
99
99
|
] }) })
|
|
@@ -397,7 +397,7 @@ function PipelineOverview({
|
|
|
397
397
|
}
|
|
398
398
|
)
|
|
399
399
|
] }),
|
|
400
|
-
/* @__PURE__ */
|
|
400
|
+
/* @__PURE__ */ jsxs4(
|
|
401
401
|
Button2,
|
|
402
402
|
{
|
|
403
403
|
variant: "text",
|
|
@@ -405,8 +405,10 @@ function PipelineOverview({
|
|
|
405
405
|
onClick: () => {
|
|
406
406
|
onNavigate("/c/engagements");
|
|
407
407
|
},
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
children: [
|
|
409
|
+
"View All",
|
|
410
|
+
/* @__PURE__ */ jsx4(ChevronRightIcon, { className: "h-4 w-4" })
|
|
411
|
+
]
|
|
410
412
|
}
|
|
411
413
|
)
|
|
412
414
|
] }),
|
|
@@ -1131,7 +1133,7 @@ function CompletedSection({
|
|
|
1131
1133
|
if (willExpand) onExpand();
|
|
1132
1134
|
};
|
|
1133
1135
|
return /* @__PURE__ */ jsxs7("div", { children: [
|
|
1134
|
-
/* @__PURE__ */
|
|
1136
|
+
/* @__PURE__ */ jsxs7(
|
|
1135
1137
|
Button5,
|
|
1136
1138
|
{
|
|
1137
1139
|
variant: "text",
|
|
@@ -1139,34 +1141,36 @@ function CompletedSection({
|
|
|
1139
1141
|
color: "neutral",
|
|
1140
1142
|
className: "w-full justify-between hover:bg-surface-hover px-3 py-1.5 h-auto text-left",
|
|
1141
1143
|
onClick: handleToggle,
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1144
|
+
children: [
|
|
1145
|
+
/* @__PURE__ */ jsxs7(
|
|
1146
|
+
Typography6,
|
|
1147
|
+
{
|
|
1148
|
+
variant: "caption",
|
|
1149
|
+
color: "secondary",
|
|
1150
|
+
className: "font-semibold uppercase tracking-wider text-[10px]",
|
|
1151
|
+
children: [
|
|
1152
|
+
"Completed",
|
|
1153
|
+
tasks.length > 0 ? ` (${tasks.length}${hasMore ? "+" : ""})` : ""
|
|
1154
|
+
]
|
|
1155
|
+
}
|
|
1156
|
+
),
|
|
1157
|
+
/* @__PURE__ */ jsx7(
|
|
1158
|
+
"svg",
|
|
1159
|
+
{
|
|
1160
|
+
className: cls4(
|
|
1161
|
+
"h-3 w-3 text-text-secondary dark:text-text-secondary-dark transition-transform duration-150",
|
|
1162
|
+
expanded && "rotate-90"
|
|
1163
|
+
),
|
|
1164
|
+
viewBox: "0 0 24 24",
|
|
1165
|
+
fill: "none",
|
|
1166
|
+
stroke: "currentColor",
|
|
1167
|
+
strokeWidth: 2,
|
|
1168
|
+
strokeLinecap: "round",
|
|
1169
|
+
strokeLinejoin: "round",
|
|
1170
|
+
children: /* @__PURE__ */ jsx7("polyline", { points: "9 18 15 12 9 6" })
|
|
1171
|
+
}
|
|
1172
|
+
)
|
|
1173
|
+
]
|
|
1170
1174
|
}
|
|
1171
1175
|
),
|
|
1172
1176
|
expanded && /* @__PURE__ */ jsx7("div", { className: "mt-1 space-y-0.5", children: loading && tasks.length === 0 ? /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 px-3 py-3", children: [
|
|
@@ -1537,7 +1541,10 @@ function CalendarWidget({ loading, tasks, onOpenTask }) {
|
|
|
1537
1541
|
{
|
|
1538
1542
|
onClick: () => setSelectedDay(isSelected ? null : day.dateKey),
|
|
1539
1543
|
className: cls5(
|
|
1540
|
-
|
|
1544
|
+
// The today and selected rings are utilities, which outrank the kit's
|
|
1545
|
+
// base :focus-visible ring — so the focus ring is said here too, or a
|
|
1546
|
+
// keyboard user on today's cell sees nothing change.
|
|
1547
|
+
"flex flex-col items-center justify-center py-1.5 rounded-md transition-colors duration-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary",
|
|
1541
1548
|
day.isCurrentMonth ? "text-text-primary dark:text-text-primary-dark" : "text-surface-300 dark:text-surface-600",
|
|
1542
1549
|
day.isToday && !isSelected && "ring-1 ring-primary/50",
|
|
1543
1550
|
isSelected ? "bg-primary/10 dark:bg-primary/15 ring-1 ring-primary/60" : "hover:bg-surface-hover",
|
|
@@ -2661,8 +2668,8 @@ function CollectionTableDemo() {
|
|
|
2661
2668
|
/* @__PURE__ */ jsx11(TableCell, { children: /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1", children: project.tags.map((tag) => /* @__PURE__ */ jsx11(Chip4, { size: "smallest", colorScheme: "cyan", children: tag }, tag)) }) }),
|
|
2662
2669
|
/* @__PURE__ */ jsx11(TableCell, { children: /* @__PURE__ */ jsx11(Typography9, { variant: "body2", className: "font-mono text-xs", children: project.dueDate }) }),
|
|
2663
2670
|
/* @__PURE__ */ jsx11(TableCell, { style: { width: "80px" }, children: /* @__PURE__ */ jsxs11("div", { className: "flex gap-1", children: [
|
|
2664
|
-
/* @__PURE__ */ jsx11(Tooltip2, { title: "Edit", asChild: true, children: /* @__PURE__ */ jsx11(IconButton3, { size: "smallest", children: /* @__PURE__ */ jsx11(PencilIcon, { size: iconSize.smallest }) }) }),
|
|
2665
|
-
/* @__PURE__ */ jsx11(Tooltip2, { title: "Delete", asChild: true, children: /* @__PURE__ */ jsx11(IconButton3, { size: "smallest", children: /* @__PURE__ */ jsx11(Trash2Icon2, { size: iconSize.smallest }) }) })
|
|
2671
|
+
/* @__PURE__ */ jsx11(Tooltip2, { title: "Edit", asChild: true, children: /* @__PURE__ */ jsx11(IconButton3, { "aria-label": "Edit", size: "smallest", children: /* @__PURE__ */ jsx11(PencilIcon, { size: iconSize.smallest }) }) }),
|
|
2672
|
+
/* @__PURE__ */ jsx11(Tooltip2, { title: "Delete", asChild: true, children: /* @__PURE__ */ jsx11(IconButton3, { "aria-label": "Delete", size: "smallest", children: /* @__PURE__ */ jsx11(Trash2Icon2, { size: iconSize.smallest }) }) })
|
|
2666
2673
|
] }) })
|
|
2667
2674
|
] }, project.id)) })
|
|
2668
2675
|
] }),
|
|
@@ -3569,7 +3576,7 @@ function UIReferenceView() {
|
|
|
3569
3576
|
/* @__PURE__ */ jsx16("button", { className: cls12("px-3 py-1 text-xs font-medium rounded-md transition-all", "bg-surface-lifted shadow-sm text-text-primary dark:text-text-primary-dark"), children: "Content" }),
|
|
3570
3577
|
/* @__PURE__ */ jsx16("button", { className: cls12("px-3 py-1 text-xs font-medium rounded-md transition-all", "text-surface-500 hover:text-surface-900 dark:hover:text-white"), children: "Studio" })
|
|
3571
3578
|
] }),
|
|
3572
|
-
/* @__PURE__ */ jsxs16(Menu, { trigger: /* @__PURE__ */ jsx16(IconButton5, {
|
|
3579
|
+
/* @__PURE__ */ jsxs16(Menu, { trigger: /* @__PURE__ */ jsx16(IconButton5, { children: /* @__PURE__ */ jsx16(MoonIcon, {}) }), children: [
|
|
3573
3580
|
/* @__PURE__ */ jsxs16(MenuItem, { children: [
|
|
3574
3581
|
/* @__PURE__ */ jsx16(MoonIcon, { size: iconSize3.smallest }),
|
|
3575
3582
|
" Dark"
|
|
@@ -4092,7 +4099,7 @@ function UIReferenceView() {
|
|
|
4092
4099
|
roles: []
|
|
4093
4100
|
}
|
|
4094
4101
|
].map((user) => /* @__PURE__ */ jsxs16(TableRow2, { children: [
|
|
4095
|
-
/* @__PURE__ */ jsx16(TableCell2, { style: { width: "64px" }, children: /* @__PURE__ */ jsx16(Tooltip3, { asChild: true, title: "Delete this user", children: /* @__PURE__ */ jsx16(IconButton5, { size: "small", children: /* @__PURE__ */ jsx16(Trash2Icon3, {}) }) }) }),
|
|
4102
|
+
/* @__PURE__ */ jsx16(TableCell2, { style: { width: "64px" }, children: /* @__PURE__ */ jsx16(Tooltip3, { asChild: true, title: "Delete this user", children: /* @__PURE__ */ jsx16(IconButton5, { "aria-label": "Delete this user", size: "small", children: /* @__PURE__ */ jsx16(Trash2Icon3, {}) }) }) }),
|
|
4096
4103
|
/* @__PURE__ */ jsx16(TableCell2, { children: user.email }),
|
|
4097
4104
|
/* @__PURE__ */ jsx16(TableCell2, { className: "font-medium", children: user.displayName }),
|
|
4098
4105
|
/* @__PURE__ */ jsx16(TableCell2, { children: /* @__PURE__ */ jsx16("div", { className: "flex flex-wrap gap-2", children: user.roles.map((role) => /* @__PURE__ */ jsx16(Chip8, { colorScheme: role.isAdmin ? "purple" : "blue", size: "small", children: role.name }, role.id)) }) })
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { CollectionAccessor, Entity, FindParams } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* The part of a read that says *which* rows, without saying how many or from
|
|
4
|
+
* where. The window adds `limit` and `offset` itself, page by page.
|
|
5
|
+
*/
|
|
6
|
+
export type CollectionWindowQuery<M extends Record<string, unknown>> = Omit<FindParams<M>, "limit" | "offset" | "page" | "after">;
|
|
7
|
+
/**
|
|
8
|
+
* What a {@link CollectionWindow} reports on every change.
|
|
9
|
+
*/
|
|
10
|
+
export interface CollectionWindowState<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
11
|
+
/**
|
|
12
|
+
* The rows in the window, in order, without duplicates.
|
|
13
|
+
*
|
|
14
|
+
* `undefined` until the window first covers what it was opened for — the
|
|
15
|
+
* first page, or every row a restored scroll position had on screen — so a
|
|
16
|
+
* view that is still showing its previous rows keeps them instead of
|
|
17
|
+
* flashing a partial list.
|
|
18
|
+
*/
|
|
19
|
+
rows: Entity<M>[] | undefined;
|
|
20
|
+
/** Rows not yet on screen are being read: the first page, or the next one. */
|
|
21
|
+
loading: boolean;
|
|
22
|
+
error: Error | undefined;
|
|
23
|
+
/** A page came back short, so there is nothing past `rows`. */
|
|
24
|
+
complete: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface CollectionWindowOptions<M extends Record<string, unknown>> {
|
|
27
|
+
accessor: Pick<CollectionAccessor<M>, "find" | "listen">;
|
|
28
|
+
query: CollectionWindowQuery<M>;
|
|
29
|
+
/** Rows in the first page, which is the one kept live. */
|
|
30
|
+
pageSize: number;
|
|
31
|
+
/** How many rows the window should hold. `Infinity` reads them all. */
|
|
32
|
+
target: number;
|
|
33
|
+
/** Subscribe to the first page when the accessor can. Defaults to `true`. */
|
|
34
|
+
live?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Applied to every page as it arrives, before it is merged — the panel's
|
|
37
|
+
* own `afterRead`, for instance.
|
|
38
|
+
*/
|
|
39
|
+
process?: (rows: Entity<M>[]) => Promise<Entity<M>[]> | Entity<M>[];
|
|
40
|
+
/**
|
|
41
|
+
* When the subscription fails, read the first page once instead of
|
|
42
|
+
* reporting the failure (which is only reported if that read fails too).
|
|
43
|
+
*/
|
|
44
|
+
fallbackToFind?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Read the first page once if the subscription has not answered by then.
|
|
47
|
+
* The subscription still wins whenever it does answer.
|
|
48
|
+
*/
|
|
49
|
+
firstPaintTimeoutMs?: number;
|
|
50
|
+
onChange: (state: CollectionWindowState<M>) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A scrolled collection view's rows, read one page at a time.
|
|
54
|
+
*
|
|
55
|
+
* ## Why this exists
|
|
56
|
+
*
|
|
57
|
+
* The table, list, card and board views used to grow a single read: the next
|
|
58
|
+
* page was the same query with `limit` raised by a page, so page *k* re-read
|
|
59
|
+
* the *k − 1* pages before it, and the read that reached row 1,001 asked for a
|
|
60
|
+
* `limit` the API refuses (`MAX_LIST_LIMIT`). Every row past 1,000 was out of
|
|
61
|
+
* reach, behind a developer error. The board clamped at the ceiling instead,
|
|
62
|
+
* and its columns then said they were finished while their own header counted
|
|
63
|
+
* thousands more.
|
|
64
|
+
*
|
|
65
|
+
* ## How it reads
|
|
66
|
+
*
|
|
67
|
+
* - The **first page** is a subscription when the accessor can listen, else a
|
|
68
|
+
* one-shot read. It is the only live query the window holds.
|
|
69
|
+
* - Every **later page** is a one-shot read by `offset`, of only the rows past
|
|
70
|
+
* the ones already held, never more than `MAX_LIST_LIMIT` at a time. Offset
|
|
71
|
+
* rather than the keyset cursor because it is the one paging every sort a
|
|
72
|
+
* view offers can express — a relevance-ordered search has no cursor — and
|
|
73
|
+
* because the server breaks ties on the id, so pages neither repeat nor skip
|
|
74
|
+
* rows under a sort with duplicates.
|
|
75
|
+
* - **Realtime** covers every row held, not only the first page. The server
|
|
76
|
+
* re-runs a subscription on every write to its collection, so each push after
|
|
77
|
+
* the first says "something changed". The rows past the first page are then
|
|
78
|
+
* read again, in reads of at most `MAX_LIST_LIMIT`, and replaced in one step.
|
|
79
|
+
* Pushes that arrive while that re-read runs are folded into one more.
|
|
80
|
+
*
|
|
81
|
+
* Rows are de-duplicated by id: between a push moving the first page and the
|
|
82
|
+
* re-read of the rest, a row can briefly belong to both.
|
|
83
|
+
*/
|
|
84
|
+
export declare class CollectionWindow<M extends Record<string, unknown>> {
|
|
85
|
+
private readonly accessor;
|
|
86
|
+
private readonly query;
|
|
87
|
+
private readonly pageSize;
|
|
88
|
+
private readonly options;
|
|
89
|
+
/** Rows the window must hold before it first shows any. */
|
|
90
|
+
private readonly holdUntil;
|
|
91
|
+
private target;
|
|
92
|
+
private head;
|
|
93
|
+
/** The first page came back full, so there may be more past it. */
|
|
94
|
+
private headFull;
|
|
95
|
+
/** Rows past the first page, from offset `pageSize` on. */
|
|
96
|
+
private tail;
|
|
97
|
+
/** The last read past the first page came back short. */
|
|
98
|
+
private tailEnded;
|
|
99
|
+
/** The first page changed after it first arrived: re-read the rest. */
|
|
100
|
+
private stale;
|
|
101
|
+
private pumping;
|
|
102
|
+
private loadingMore;
|
|
103
|
+
private headError;
|
|
104
|
+
private tailError;
|
|
105
|
+
private settled;
|
|
106
|
+
private disposed;
|
|
107
|
+
private headSequence;
|
|
108
|
+
private liveAnswered;
|
|
109
|
+
private unsubscribe;
|
|
110
|
+
private firstPaintTimer;
|
|
111
|
+
constructor(options: CollectionWindowOptions<M>);
|
|
112
|
+
start(): void;
|
|
113
|
+
/** Hold this many rows. Fewer than are held drops the rows past it. */
|
|
114
|
+
setTarget(target: number): void;
|
|
115
|
+
dispose(): void;
|
|
116
|
+
private readHeadOnce;
|
|
117
|
+
private receiveLiveError;
|
|
118
|
+
private receiveHead;
|
|
119
|
+
/** Rows still wanted past the ones held. */
|
|
120
|
+
private wanted;
|
|
121
|
+
/**
|
|
122
|
+
* The one loop that reads past the first page, so a re-read and a next page
|
|
123
|
+
* never run at the same time and interleave their offsets.
|
|
124
|
+
*/
|
|
125
|
+
private pump;
|
|
126
|
+
private readNext;
|
|
127
|
+
/** Read every held row past the first page again, and swap them in at once. */
|
|
128
|
+
private reread;
|
|
129
|
+
private process;
|
|
130
|
+
private complete;
|
|
131
|
+
private emit;
|
|
132
|
+
private rows;
|
|
133
|
+
}
|
package/dist/hooks/index.d.ts
CHANGED