@agent-native/core 0.211.0 → 0.212.0-nightly-20261021010049
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/app-config/analytics.d.ts +1 -0
- package/dist/app-config/analytics.js +7 -0
- package/dist/app-config/schema.d.ts +1 -0
- package/dist/chat-threads/store.d.ts +28 -2
- package/dist/chat-threads/store.js +75 -13
- package/dist/cli/template-baseline.js +18 -5
- package/dist/cli/template-sync.js +100 -19
- package/dist/client/agent-chat/index.d.ts +2 -1
- package/dist/client/agent-chat/index.js +2 -1
- package/dist/client/chat/agentkit-protocol.js +21 -1
- package/dist/client/chat/run-outcome.d.ts +1 -0
- package/dist/client/chat/runtime.js +6 -0
- package/dist/client/sharing/index.d.ts +2 -1
- package/dist/client/sharing/index.js +2 -1
- package/dist/client/sharing/share-controller-helpers.d.ts +14 -1
- package/dist/client/sharing/share-controller-helpers.js +46 -1
- package/dist/client/sharing/useShareButtonController.d.ts +12 -0
- package/dist/client/sharing/useShareButtonController.js +76 -57
- package/dist/client/sharing/useShareDialogController.d.ts +3 -0
- package/dist/client/sharing/useShareDialogController.js +77 -49
- package/dist/client/sharing/useShareVisibilityTransition.d.ts +68 -0
- package/dist/client/sharing/useShareVisibilityTransition.js +224 -0
- package/dist/client/use-chat-thread-list.d.ts +40 -0
- package/dist/client/use-chat-thread-list.js +210 -0
- package/dist/client/use-chat-threads.d.ts +67 -0
- package/dist/client/use-chat-threads.js +141 -23
- package/dist/client/use-db-sync.d.ts +1 -0
- package/dist/client/use-db-sync.js +13 -1
- package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
- package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/bounded-task-bundle.d.ts +17 -0
- package/dist/collab/bounded-task-bundle.js +39 -0
- package/dist/collab/bounded-task-source.d.ts +14 -0
- package/dist/collab/bounded-task-source.js +8 -0
- package/dist/collab/bounded-tasks.d.ts +67 -0
- package/dist/collab/bounded-tasks.js +439 -0
- package/dist/collab/client.js +2 -1
- package/dist/collab/index.d.ts +5 -2
- package/dist/collab/index.js +5 -2
- package/dist/collab/live-copy-tasks.d.ts +80 -0
- package/dist/collab/live-copy-tasks.js +330 -0
- package/dist/collab/live-copy.d.ts +188 -0
- package/dist/collab/live-copy.js +415 -0
- package/dist/collab/routes.d.ts +5 -0
- package/dist/collab/routes.js +24 -8
- package/dist/collab/storage.d.ts +53 -1
- package/dist/collab/storage.js +93 -5
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/collab/ydoc-manager.d.ts +92 -3
- package/dist/collab/ydoc-manager.js +447 -38
- package/dist/db/client.d.ts +6 -0
- package/dist/db/client.js +25 -5
- package/dist/deploy/build.js +2 -0
- package/dist/extensions/actions.js +1 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/mcp/agent-connected.js +5 -1
- package/dist/mcp/build-server.js +39 -10
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/org/index.d.ts +2 -2
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
- package/dist/provider-api/actions/provider-api.d.ts +10 -10
- package/dist/server/agent-chat-plugin.js +80 -17
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/analytics.d.ts +1 -0
- package/dist/server/analytics.js +18 -1
- package/dist/server/collab-plugin.d.ts +10 -1
- package/dist/server/collab-plugin.js +89 -20
- package/dist/server/index.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/optional-node-builtins.d.ts +1 -0
- package/dist/shared/optional-node-builtins.js +3 -0
- package/dist/sharing/access.d.ts +1 -0
- package/dist/sharing/access.js +113 -95
- package/dist/tracking/providers.d.ts +1 -0
- package/dist/tracking/providers.js +2 -1
- package/dist/triggers/actions/manage-automation.d.ts +2 -2
- package/dist/vite/bounded-task-plugin.d.ts +22 -0
- package/dist/vite/bounded-task-plugin.js +44 -0
- package/dist/vite/client.js +2 -0
- package/dist/vitest-config.js +3 -0
- package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +1 -1
- package/docs/content/deployment-environment-variables.mdx +1 -0
- package/docs/content/environment-variables.mdx +1 -0
- package/docs/content/sharing.mdx +36 -0
- package/docs/content/toolkit-sharing.mdx +98 -12
- package/package.json +5 -3
|
@@ -44,9 +44,22 @@ export declare function useShareMutations(): {
|
|
|
44
44
|
share: import("@tanstack/react-query").UseMutationResult<any, Error, Record<string, any>, unknown>;
|
|
45
45
|
unshare: import("@tanstack/react-query").UseMutationResult<any, Error, Record<string, any>, unknown>;
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Chat history lists are cached outside the share query, so a saved grant or
|
|
49
|
+
* visibility change must tell them to refetch. Other resource types have no
|
|
50
|
+
* such list to refresh.
|
|
51
|
+
*/
|
|
52
|
+
export declare function notifyShareChanged(resourceType: string): void;
|
|
47
53
|
export declare function optimisticallyUpdateShareCache<TData>(queryClient: QueryClient, queryKey: ShareQueryKey, updater: (previous: TData | undefined) => TData | undefined): TData | undefined;
|
|
48
54
|
export declare function rollbackShareCache<TData>(queryClient: QueryClient, queryKey: ShareQueryKey, previous: TData | undefined): void;
|
|
49
|
-
export declare function
|
|
55
|
+
export declare function optimisticallyUpdateShareVisibility<TData extends {
|
|
56
|
+
visibility: string | null;
|
|
57
|
+
}>(queryClient: QueryClient, queryKey: ShareQueryKey, visibility: TData["visibility"]): {
|
|
58
|
+
isCurrent: () => boolean;
|
|
59
|
+
commit: (next: TData["visibility"]) => void;
|
|
60
|
+
rollback: () => void;
|
|
61
|
+
};
|
|
62
|
+
export declare function useShareMutationGuard(scope?: string): {
|
|
50
63
|
begin: () => number;
|
|
51
64
|
isLatest: (requestId: number) => boolean;
|
|
52
65
|
};
|
|
@@ -2,6 +2,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { agentNativePath } from "../api-path.js";
|
|
4
4
|
import { useActionMutation, useActionQuery } from "../use-action.js";
|
|
5
|
+
import { notifyChatThreadsUpdated } from "../use-chat-threads.js";
|
|
5
6
|
export const DEFAULT_MEMBER_SUGGESTION_LIMIT = 25;
|
|
6
7
|
export const DEFAULT_MEMBER_SEARCH_DEBOUNCE_MS = 140;
|
|
7
8
|
export function createShareQueryParams(resourceType, resourceId) {
|
|
@@ -27,6 +28,15 @@ export function useShareMutations() {
|
|
|
27
28
|
unshare: useActionMutation("unshare-resource"),
|
|
28
29
|
};
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Chat history lists are cached outside the share query, so a saved grant or
|
|
33
|
+
* visibility change must tell them to refetch. Other resource types have no
|
|
34
|
+
* such list to refresh.
|
|
35
|
+
*/
|
|
36
|
+
export function notifyShareChanged(resourceType) {
|
|
37
|
+
if (resourceType === "chat_thread")
|
|
38
|
+
notifyChatThreadsUpdated();
|
|
39
|
+
}
|
|
30
40
|
export function optimisticallyUpdateShareCache(queryClient, queryKey, updater) {
|
|
31
41
|
const previous = queryClient.getQueryData(queryKey);
|
|
32
42
|
const next = updater(previous);
|
|
@@ -45,8 +55,43 @@ export function rollbackShareCache(queryClient, queryKey, previous) {
|
|
|
45
55
|
}
|
|
46
56
|
queryClient.setQueryData(queryKey, previous);
|
|
47
57
|
}
|
|
48
|
-
|
|
58
|
+
// Ownership follows the cache entry, not the mounted surface. Weak keys also
|
|
59
|
+
// retire it when a query is removed, without retaining resource ids forever.
|
|
60
|
+
const visibilityCacheOwners = new WeakMap();
|
|
61
|
+
export function optimisticallyUpdateShareVisibility(queryClient, queryKey, visibility) {
|
|
62
|
+
const cacheKey = queryKey;
|
|
63
|
+
const query = queryClient
|
|
64
|
+
.getQueryCache()
|
|
65
|
+
.build(queryClient, { queryKey: cacheKey });
|
|
66
|
+
const previous = queryClient.getQueryData(queryKey);
|
|
67
|
+
const owner = {};
|
|
68
|
+
visibilityCacheOwners.set(query, owner);
|
|
69
|
+
const isCurrent = () => queryClient.getQueryCache().find({ queryKey, exact: true }) === query &&
|
|
70
|
+
visibilityCacheOwners.get(query) === owner;
|
|
71
|
+
const commit = (next) => {
|
|
72
|
+
if (!isCurrent())
|
|
73
|
+
return;
|
|
74
|
+
queryClient.setQueryData(queryKey, (cached) => cached ? { ...cached, visibility: next } : cached);
|
|
75
|
+
};
|
|
76
|
+
commit(visibility);
|
|
77
|
+
return {
|
|
78
|
+
isCurrent,
|
|
79
|
+
commit,
|
|
80
|
+
rollback: () => {
|
|
81
|
+
if (previous &&
|
|
82
|
+
queryClient.getQueryData(queryKey)?.visibility === visibility) {
|
|
83
|
+
commit(previous.visibility);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function useShareMutationGuard(scope) {
|
|
49
89
|
const latestRequest = useRef(0);
|
|
90
|
+
const currentScope = useRef(scope);
|
|
91
|
+
if (currentScope.current !== scope) {
|
|
92
|
+
currentScope.current = scope;
|
|
93
|
+
latestRequest.current += 1;
|
|
94
|
+
}
|
|
50
95
|
const begin = useCallback(() => {
|
|
51
96
|
latestRequest.current += 1;
|
|
52
97
|
return latestRequest.current;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useActionQuery } from "../use-action.js";
|
|
2
|
+
import { type ShareVisibilityTransition } from "./useShareVisibilityTransition.js";
|
|
2
3
|
export type ShareButtonVisibility = "private" | "org" | "public";
|
|
3
4
|
export type ShareButtonRole = "viewer" | "commenter" | "editor" | "admin";
|
|
4
5
|
export interface ShareButtonShare {
|
|
@@ -68,7 +69,18 @@ export interface ShareButtonController {
|
|
|
68
69
|
setInviteEmail: (email: string) => void;
|
|
69
70
|
sharesQuery: ReturnType<typeof useActionQuery<ShareButtonSharesResponse>>;
|
|
70
71
|
visibilityOverride: ShareButtonVisibility | null;
|
|
72
|
+
/**
|
|
73
|
+
* @deprecated Use `visibilityTransition.request`, which this calls. It
|
|
74
|
+
* resolves only once the change is persisted. A direct change rejects when
|
|
75
|
+
* its save fails. A private-to-Organization change stays pending through
|
|
76
|
+
* `visibilityTransition.confirm()` failures (retry is possible) and rejects
|
|
77
|
+
* with `ShareVisibilityNotSavedError` once it is cancelled or invalidated.
|
|
78
|
+
* Without manage rights it
|
|
79
|
+
* sets `shareError` and resolves, as it always has.
|
|
80
|
+
*/
|
|
71
81
|
handleVisibilityChange: (visibility: ShareButtonVisibility) => Promise<void>;
|
|
82
|
+
/** Request, confirm and cancel a visibility change that needs confirming. */
|
|
83
|
+
visibilityTransition: ShareVisibilityTransition;
|
|
72
84
|
data: ShareButtonSharesResponse | undefined;
|
|
73
85
|
policy: {
|
|
74
86
|
allowPublic: boolean;
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useActionQuery } from "../use-action.js";
|
|
3
|
-
import { DEFAULT_MEMBER_SEARCH_DEBOUNCE_MS, DEFAULT_MEMBER_SUGGESTION_LIMIT, extractShareErrorMessage, optimisticallyUpdateShareCache, rollbackShareCache, useShareOrgMemberSearch, useShareMutationGuard, useShareMutations, useShareQuery, } from "./share-controller-helpers.js";
|
|
3
|
+
import { DEFAULT_MEMBER_SEARCH_DEBOUNCE_MS, DEFAULT_MEMBER_SUGGESTION_LIMIT, extractShareErrorMessage, notifyShareChanged, optimisticallyUpdateShareCache, optimisticallyUpdateShareVisibility, rollbackShareCache, useShareOrgMemberSearch, useShareMutationGuard, useShareMutations, useShareQuery, } from "./share-controller-helpers.js";
|
|
4
|
+
import { useShareVisibilityTransition, } from "./useShareVisibilityTransition.js";
|
|
4
5
|
export function useShareButtonController(options) {
|
|
5
6
|
const [open, setOpen] = useState(false);
|
|
6
7
|
const [inviteEmail, setInviteEmail] = useState("");
|
|
7
8
|
const [selectedGroup, setSelectedGroup] = useState(null);
|
|
8
9
|
const shareTabDefaultValue = options.shareTabs?.defaultValue ?? "share";
|
|
9
10
|
const [activeShareTab, setActiveShareTab] = useState(shareTabDefaultValue);
|
|
10
|
-
const
|
|
11
|
-
|
|
11
|
+
const resourceKey = `${options.resourceType}:${options.resourceId}`;
|
|
12
|
+
// Tied to the resource it was made for: a click that lands on a new resource
|
|
13
|
+
// before any effect runs must not see the previous resource's value.
|
|
14
|
+
const [visibilityOverrideState, setVisibilityOverrideState] = useState(null);
|
|
15
|
+
const visibilityOverride = visibilityOverrideState?.resourceKey === resourceKey
|
|
16
|
+
? visibilityOverrideState.value
|
|
17
|
+
: null;
|
|
18
|
+
const clearVisibilityOverride = useCallback((key, token) => setVisibilityOverrideState((state) => state?.resourceKey === key && state.token === token ? null : state), []);
|
|
12
19
|
const appliedDefaultOpenRef = useRef(false);
|
|
13
20
|
const { queryKey: shareQueryKey, query: sharesQuery, queryClient, } = useShareQuery(options.resourceType, options.resourceId, open, 0);
|
|
14
21
|
const { setVisibility, share, unshare } = useShareMutations();
|
|
15
|
-
const visibilityGuard = useShareMutationGuard();
|
|
22
|
+
const visibilityGuard = useShareMutationGuard(resourceKey);
|
|
16
23
|
const data = sharesQuery.data;
|
|
17
24
|
const canManage = data?.role === "owner" || data?.role === "admin";
|
|
18
25
|
const [shareError, setShareError] = useState(null);
|
|
@@ -31,7 +38,6 @@ export function useShareButtonController(options) {
|
|
|
31
38
|
setSelectedGroup(null);
|
|
32
39
|
setShareMessage("");
|
|
33
40
|
setMessageOpen(false);
|
|
34
|
-
visibilitySequenceStartRef.current = null;
|
|
35
41
|
}, [options.resourceId, options.resourceType]);
|
|
36
42
|
useEffect(() => {
|
|
37
43
|
if (!options.defaultOpen || appliedDefaultOpenRef.current)
|
|
@@ -43,62 +49,52 @@ export function useShareButtonController(options) {
|
|
|
43
49
|
setActiveShareTab(value);
|
|
44
50
|
options.shareTabs?.onValueChange?.(value);
|
|
45
51
|
}, [options.shareTabs]);
|
|
46
|
-
const
|
|
52
|
+
const applyVisibility = useCallback(async (next) => {
|
|
47
53
|
if (!canManage) {
|
|
48
54
|
setShareError("Only owners and admins can change access.");
|
|
49
|
-
return
|
|
55
|
+
return;
|
|
50
56
|
}
|
|
51
57
|
const requestId = visibilityGuard.begin();
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const token = {};
|
|
59
|
+
const mutation = optimisticallyUpdateShareVisibility(queryClient, shareQueryKey, next);
|
|
60
|
+
setVisibilityOverrideState({ resourceKey, token, value: next });
|
|
61
|
+
let result;
|
|
62
|
+
try {
|
|
63
|
+
// Settles for this call alone: per-call mutate callbacks are skipped
|
|
64
|
+
// once a newer call supersedes it or the observer unmounts.
|
|
65
|
+
result = await setVisibility.mutateAsync({
|
|
57
66
|
resourceType: options.resourceType,
|
|
58
67
|
resourceId: options.resourceId,
|
|
59
68
|
visibility: next,
|
|
60
|
-
}, {
|
|
61
|
-
onSuccess: (result) => {
|
|
62
|
-
const resultVisibility = typeof result === "object" &&
|
|
63
|
-
result !== null &&
|
|
64
|
-
"visibility" in result &&
|
|
65
|
-
result.visibility;
|
|
66
|
-
if (visibilityGuard.isLatest(requestId)) {
|
|
67
|
-
if (visibilitySequenceStartRef.current?.visibility ===
|
|
68
|
-
"private" &&
|
|
69
|
-
(resultVisibility === "org" || resultVisibility === "public")) {
|
|
70
|
-
options.onShareSuccess?.();
|
|
71
|
-
}
|
|
72
|
-
optimisticallyUpdateShareCache(queryClient, shareQueryKey, (prev) => prev
|
|
73
|
-
? {
|
|
74
|
-
...prev,
|
|
75
|
-
visibility: resultVisibility ?? next,
|
|
76
|
-
}
|
|
77
|
-
: prev);
|
|
78
|
-
}
|
|
79
|
-
sharesQuery
|
|
80
|
-
.refetch()
|
|
81
|
-
.then(() => resolve())
|
|
82
|
-
.catch(reject)
|
|
83
|
-
.finally(() => {
|
|
84
|
-
if (visibilityGuard.isLatest(requestId)) {
|
|
85
|
-
setVisibilityOverride(null);
|
|
86
|
-
visibilitySequenceStartRef.current = null;
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
},
|
|
90
|
-
onError: (error) => {
|
|
91
|
-
if (visibilityGuard.isLatest(requestId)) {
|
|
92
|
-
const sequenceStart = visibilitySequenceStartRef.current;
|
|
93
|
-
setVisibilityOverride(null);
|
|
94
|
-
visibilitySequenceStartRef.current = null;
|
|
95
|
-
rollbackShareCache(queryClient, shareQueryKey, sequenceStart ? sequenceStart.shares : previous);
|
|
96
|
-
void sharesQuery.refetch();
|
|
97
|
-
}
|
|
98
|
-
reject(error);
|
|
99
|
-
},
|
|
100
69
|
});
|
|
101
|
-
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
mutation.rollback();
|
|
73
|
+
clearVisibilityOverride(resourceKey, token);
|
|
74
|
+
if (visibilityGuard.isLatest(requestId)) {
|
|
75
|
+
void sharesQuery.refetch();
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
notifyShareChanged(options.resourceType);
|
|
80
|
+
const resultVisibility = typeof result === "object" &&
|
|
81
|
+
result !== null &&
|
|
82
|
+
"visibility" in result &&
|
|
83
|
+
result.visibility;
|
|
84
|
+
mutation.commit(resultVisibility ?? next);
|
|
85
|
+
if (visibilityGuard.isLatest(requestId)) {
|
|
86
|
+
if (visibility === "private" &&
|
|
87
|
+
(resultVisibility === "org" || resultVisibility === "public")) {
|
|
88
|
+
options.onShareSuccess?.();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
if (visibilityGuard.isLatest(requestId))
|
|
93
|
+
await sharesQuery.refetch();
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
clearVisibilityOverride(resourceKey, token);
|
|
97
|
+
}
|
|
102
98
|
}, [
|
|
103
99
|
options.resourceId,
|
|
104
100
|
options.resourceType,
|
|
@@ -110,6 +106,8 @@ export function useShareButtonController(options) {
|
|
|
110
106
|
visibilityGuard,
|
|
111
107
|
options.onShareSuccess,
|
|
112
108
|
visibility,
|
|
109
|
+
resourceKey,
|
|
110
|
+
clearVisibilityOverride,
|
|
113
111
|
]);
|
|
114
112
|
const policy = data?.policy ?? {
|
|
115
113
|
allowPublic: true,
|
|
@@ -187,6 +185,17 @@ export function useShareButtonController(options) {
|
|
|
187
185
|
setSelectedGroup(group);
|
|
188
186
|
setInviteEmail(group.name);
|
|
189
187
|
}, []);
|
|
188
|
+
const visibilityTransition = useShareVisibilityTransition({
|
|
189
|
+
resourceType: options.resourceType,
|
|
190
|
+
resourceId: options.resourceId,
|
|
191
|
+
current: visibility,
|
|
192
|
+
orgId: data?.orgId ?? null,
|
|
193
|
+
canManage,
|
|
194
|
+
active: open,
|
|
195
|
+
apply: applyVisibility,
|
|
196
|
+
onDirectError: (error) => setShareError(extractShareErrorMessage(error)),
|
|
197
|
+
});
|
|
198
|
+
const requestVisibility = visibilityTransition.request;
|
|
190
199
|
const handleVisibility = useCallback((next) => {
|
|
191
200
|
if (next === visibility)
|
|
192
201
|
return;
|
|
@@ -195,10 +204,16 @@ export function useShareButtonController(options) {
|
|
|
195
204
|
return;
|
|
196
205
|
}
|
|
197
206
|
setShareError(null);
|
|
198
|
-
void
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
void requestVisibility(next);
|
|
208
|
+
}, [canManage, requestVisibility, visibility]);
|
|
209
|
+
const handleVisibilityChange = useCallback((next) => {
|
|
210
|
+
if (!canManage) {
|
|
211
|
+
setShareError("Only owners and admins can change access.");
|
|
212
|
+
return Promise.resolve();
|
|
213
|
+
}
|
|
214
|
+
setShareError(null);
|
|
215
|
+
return requestVisibility(next);
|
|
216
|
+
}, [canManage, requestVisibility]);
|
|
202
217
|
const handleHideInSearch = useCallback(() => {
|
|
203
218
|
const control = options.hideInSearchControl;
|
|
204
219
|
if (!control || control.pending || !canManage)
|
|
@@ -248,6 +263,7 @@ export function useShareButtonController(options) {
|
|
|
248
263
|
...(message ? { message } : {}),
|
|
249
264
|
}, {
|
|
250
265
|
onSuccess: (result) => {
|
|
266
|
+
notifyShareChanged(options.resourceType);
|
|
251
267
|
if (!(typeof result === "object" &&
|
|
252
268
|
result !== null &&
|
|
253
269
|
"updated" in result &&
|
|
@@ -315,6 +331,7 @@ export function useShareButtonController(options) {
|
|
|
315
331
|
notify: false,
|
|
316
332
|
}, {
|
|
317
333
|
onSuccess: () => {
|
|
334
|
+
notifyShareChanged(options.resourceType);
|
|
318
335
|
void sharesQuery.refetch().then(() => {
|
|
319
336
|
setRoleOverrides((previous) => {
|
|
320
337
|
const { [key]: _removed, ...rest } = previous;
|
|
@@ -367,6 +384,7 @@ export function useShareButtonController(options) {
|
|
|
367
384
|
principalId: currentShare.principalId,
|
|
368
385
|
}, {
|
|
369
386
|
onSuccess: () => {
|
|
387
|
+
notifyShareChanged(options.resourceType);
|
|
370
388
|
void sharesQuery.refetch().then(() => {
|
|
371
389
|
setPendingRemoves((previous) => {
|
|
372
390
|
const next = new Set(previous);
|
|
@@ -408,6 +426,7 @@ export function useShareButtonController(options) {
|
|
|
408
426
|
sharesQuery,
|
|
409
427
|
visibilityOverride,
|
|
410
428
|
handleVisibilityChange,
|
|
429
|
+
visibilityTransition,
|
|
411
430
|
data,
|
|
412
431
|
policy,
|
|
413
432
|
visibility,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ShareVisibilityTransition } from "./useShareVisibilityTransition.js";
|
|
1
2
|
export type ShareVisibility = "private" | "org" | "public";
|
|
2
3
|
export type ShareRole = "viewer" | "commenter" | "editor" | "admin";
|
|
3
4
|
export type ShareDialogTab = "link" | "invite" | "embed";
|
|
@@ -82,7 +83,9 @@ export interface ShareDialogController {
|
|
|
82
83
|
options: Array<ShareOption<ShareVisibility>>;
|
|
83
84
|
disabled: boolean;
|
|
84
85
|
pending: boolean;
|
|
86
|
+
/** Saves at once, or holds a broadening change for `transition`. */
|
|
85
87
|
set: (visibility: ShareVisibility) => void;
|
|
88
|
+
transition: ShareVisibilityTransition;
|
|
86
89
|
};
|
|
87
90
|
invite: {
|
|
88
91
|
email: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { trackEvent } from "../analytics.js";
|
|
3
3
|
import { useT } from "../i18n.js";
|
|
4
|
-
import { extractShareErrorMessage, optimisticallyUpdateShareCache, rollbackShareCache, useShareMutationGuard, useShareMutations, useShareOrgMemberSearch, useShareQuery, } from "./share-controller-helpers.js";
|
|
4
|
+
import { extractShareErrorMessage, notifyShareChanged, optimisticallyUpdateShareCache, optimisticallyUpdateShareVisibility, rollbackShareCache, useShareMutationGuard, useShareMutations, useShareOrgMemberSearch, useShareQuery, } from "./share-controller-helpers.js";
|
|
5
|
+
import { useShareVisibilityTransition, } from "./useShareVisibilityTransition.js";
|
|
5
6
|
async function writeBrowserClipboardText(text) {
|
|
6
7
|
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
|
|
7
8
|
return false;
|
|
@@ -34,9 +35,16 @@ export function useShareDialogController({ open, onClose, resourceType, resource
|
|
|
34
35
|
const [message, setMessage] = useState("");
|
|
35
36
|
const [messageOpen, setMessageOpen] = useState(false);
|
|
36
37
|
const [copiedField, setCopiedField] = useState(null);
|
|
37
|
-
const
|
|
38
|
+
const resourceKey = `${resourceType}:${resourceId}`;
|
|
39
|
+
// Tied to the resource it was made for: a click that lands on a new resource
|
|
40
|
+
// before any effect runs must not see the previous resource's value.
|
|
41
|
+
const [visibilityOverrideState, setVisibilityOverrideState] = useState(null);
|
|
42
|
+
const visibilityOverride = visibilityOverrideState?.resourceKey === resourceKey
|
|
43
|
+
? visibilityOverrideState.value
|
|
44
|
+
: null;
|
|
45
|
+
const clearVisibilityOverride = useCallback((key, token) => setVisibilityOverrideState((state) => state?.resourceKey === key && state.token === token ? null : state), []);
|
|
38
46
|
const [mutationError, setMutationError] = useState(null);
|
|
39
|
-
const visibilityGuard = useShareMutationGuard();
|
|
47
|
+
const visibilityGuard = useShareMutationGuard(resourceKey);
|
|
40
48
|
const copyResetTimer = useRef(null);
|
|
41
49
|
useEffect(() => {
|
|
42
50
|
if (open)
|
|
@@ -56,61 +64,76 @@ export function useShareDialogController({ open, onClose, resourceType, resource
|
|
|
56
64
|
.map((value) => visibilityOption(value, t)), [data?.policy?.allowPublic, t, visibility]);
|
|
57
65
|
const roleOptions = useMemo(() => ["viewer", "commenter", "editor", "admin"].map((value) => roleOption(value, t)), [t]);
|
|
58
66
|
const refetch = useCallback(() => sharesQuery.refetch(), [sharesQuery]);
|
|
59
|
-
const
|
|
60
|
-
if (!canManage || next === visibility)
|
|
61
|
-
return;
|
|
67
|
+
const applyVisibility = useCallback(async (next) => {
|
|
62
68
|
const requestId = visibilityGuard.begin();
|
|
63
|
-
const
|
|
69
|
+
const token = {};
|
|
70
|
+
const mutation = optimisticallyUpdateShareVisibility(queryClient, shareQueryKey, next);
|
|
64
71
|
setMutationError(null);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
72
|
+
setVisibilityOverrideState({ resourceKey, token, value: next });
|
|
73
|
+
let result;
|
|
74
|
+
try {
|
|
75
|
+
// Settles for this call alone: per-call mutate callbacks are skipped
|
|
76
|
+
// once a newer call supersedes it or the observer unmounts.
|
|
77
|
+
result = await visibilityMutation.mutateAsync({
|
|
78
|
+
resourceType,
|
|
79
|
+
resourceId,
|
|
80
|
+
visibility: next,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
mutation.rollback();
|
|
85
|
+
clearVisibilityOverride(resourceKey, token);
|
|
86
|
+
if (visibilityGuard.isLatest(requestId) && mutation.isCurrent()) {
|
|
87
|
+
void queryClient.refetchQueries({ queryKey: shareQueryKey, exact: true, type: "active" }, { throwOnError: false });
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
notifyShareChanged(resourceType);
|
|
92
|
+
const resultVisibility = typeof result === "object" &&
|
|
93
|
+
result !== null &&
|
|
94
|
+
"visibility" in result &&
|
|
95
|
+
result.visibility;
|
|
96
|
+
mutation.commit(resultVisibility ?? next);
|
|
97
|
+
if (!visibilityGuard.isLatest(requestId)) {
|
|
98
|
+
clearVisibilityOverride(resourceKey, token);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
void Promise.resolve(refetch())
|
|
102
|
+
.catch((error) => {
|
|
103
|
+
if (visibilityGuard.isLatest(requestId)) {
|
|
104
|
+
setMutationError(extractShareErrorMessage(error));
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
.finally(() => {
|
|
108
|
+
clearVisibilityOverride(resourceKey, token);
|
|
102
109
|
});
|
|
103
110
|
}, [
|
|
104
|
-
canManage,
|
|
105
111
|
refetch,
|
|
106
112
|
resourceId,
|
|
107
113
|
resourceType,
|
|
114
|
+
resourceKey,
|
|
115
|
+
clearVisibilityOverride,
|
|
108
116
|
queryClient,
|
|
109
117
|
shareQueryKey,
|
|
110
|
-
visibility,
|
|
111
118
|
visibilityGuard,
|
|
112
119
|
visibilityMutation,
|
|
113
120
|
]);
|
|
121
|
+
const visibilityTransition = useShareVisibilityTransition({
|
|
122
|
+
resourceType,
|
|
123
|
+
resourceId,
|
|
124
|
+
current: visibility,
|
|
125
|
+
orgId: data?.orgId ?? null,
|
|
126
|
+
canManage,
|
|
127
|
+
active: open,
|
|
128
|
+
apply: applyVisibility,
|
|
129
|
+
onDirectError: (error) => setMutationError(extractShareErrorMessage(error)),
|
|
130
|
+
});
|
|
131
|
+
const requestVisibility = visibilityTransition.request;
|
|
132
|
+
// A void setter: callers that return it from a handler must not end up
|
|
133
|
+
// awaiting a change that may sit waiting for confirmation.
|
|
134
|
+
const setVisibility = useCallback((next) => {
|
|
135
|
+
void requestVisibility(next);
|
|
136
|
+
}, [requestVisibility]);
|
|
114
137
|
const submitInvite = useCallback(() => {
|
|
115
138
|
const principalId = email.trim();
|
|
116
139
|
if (!canManage || !principalId)
|
|
@@ -135,6 +158,7 @@ export function useShareDialogController({ open, onClose, resourceType, resource
|
|
|
135
158
|
...(notificationMessage ? { message: notificationMessage } : {}),
|
|
136
159
|
}, {
|
|
137
160
|
onSuccess: () => {
|
|
161
|
+
notifyShareChanged(resourceType);
|
|
138
162
|
setEmail("");
|
|
139
163
|
setMessage("");
|
|
140
164
|
setMessageOpen(false);
|
|
@@ -176,7 +200,10 @@ export function useShareDialogController({ open, onClose, resourceType, resource
|
|
|
176
200
|
principalType: share.principalType,
|
|
177
201
|
principalId: share.principalId,
|
|
178
202
|
}, {
|
|
179
|
-
onSuccess:
|
|
203
|
+
onSuccess: () => {
|
|
204
|
+
notifyShareChanged(resourceType);
|
|
205
|
+
return refetch();
|
|
206
|
+
},
|
|
180
207
|
onError: (error) => {
|
|
181
208
|
rollbackShareCache(queryClient, shareQueryKey, previous);
|
|
182
209
|
setMutationError(extractShareErrorMessage(error));
|
|
@@ -260,9 +287,10 @@ export function useShareDialogController({ open, onClose, resourceType, resource
|
|
|
260
287
|
label: currentVisibility.label,
|
|
261
288
|
description: currentVisibility.description,
|
|
262
289
|
options: visibilityOptions,
|
|
263
|
-
disabled: !canManage,
|
|
290
|
+
disabled: !canManage || visibilityTransition.submitting,
|
|
264
291
|
pending: visibilityMutation.isPending,
|
|
265
292
|
set: setVisibility,
|
|
293
|
+
transition: visibilityTransition,
|
|
266
294
|
},
|
|
267
295
|
invite: {
|
|
268
296
|
email,
|
|
@@ -290,7 +318,7 @@ export function useShareDialogController({ open, onClose, resourceType, resource
|
|
|
290
318
|
copiedField,
|
|
291
319
|
copy,
|
|
292
320
|
loading: sharesQuery.isLoading,
|
|
293
|
-
error: sharesQuery.error
|
|
321
|
+
error: mutationError ?? sharesQuery.error,
|
|
294
322
|
refetch,
|
|
295
323
|
canManage,
|
|
296
324
|
agentReadable: data?.agentReadable === true,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ShareVisibility } from "./useShareDialogController.js";
|
|
2
|
+
/**
|
|
3
|
+
* Whether moving to `to` shows more of the resource to more people than `from`
|
|
4
|
+
* does and so needs an explicit confirmation. Public -> organization narrows
|
|
5
|
+
* access and Public stays confirmation-free, so only private -> organization
|
|
6
|
+
* asks.
|
|
7
|
+
*/
|
|
8
|
+
export declare function visibilityNeedsConfirmation(from: ShareVisibility, to: ShareVisibility): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Why a requested visibility change was never saved: `ignored` when the
|
|
11
|
+
* request was refused up front (closed surface, no manage rights, a save
|
|
12
|
+
* already running) and `dropped` when it was held for confirmation and then
|
|
13
|
+
* cancelled or invalidated.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ShareVisibilityNotSavedError extends Error {
|
|
16
|
+
readonly reason: "ignored" | "dropped";
|
|
17
|
+
constructor(reason: "ignored" | "dropped");
|
|
18
|
+
}
|
|
19
|
+
export interface ShareVisibilityTransitionOptions {
|
|
20
|
+
resourceType: string;
|
|
21
|
+
resourceId: string;
|
|
22
|
+
/** The visibility shown right now, including an in-flight optimistic one. */
|
|
23
|
+
current: ShareVisibility;
|
|
24
|
+
/** The organization the resource belongs to, from the share listing. */
|
|
25
|
+
orgId: string | null;
|
|
26
|
+
canManage: boolean;
|
|
27
|
+
/** False while the share surface is closed; a pending request is dropped. */
|
|
28
|
+
active: boolean;
|
|
29
|
+
/** Saves the visibility. Rejects on failure so the caller can show it. */
|
|
30
|
+
apply: (next: ShareVisibility) => Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Receives a failure of a change that needed no confirmation. Required so a
|
|
33
|
+
* failed save is never left looking like a successful one.
|
|
34
|
+
*/
|
|
35
|
+
onDirectError: (error: unknown) => void;
|
|
36
|
+
}
|
|
37
|
+
export interface ShareVisibilityTransitionRequest {
|
|
38
|
+
from: ShareVisibility;
|
|
39
|
+
to: ShareVisibility;
|
|
40
|
+
}
|
|
41
|
+
export interface ShareVisibilityTransition {
|
|
42
|
+
/** The change waiting for confirmation, or null. Nothing is saved yet. */
|
|
43
|
+
pending: ShareVisibilityTransitionRequest | null;
|
|
44
|
+
/**
|
|
45
|
+
* The resource's organization name, only when it was read for that exact
|
|
46
|
+
* organization. Null means "say your organization" instead of a name.
|
|
47
|
+
*/
|
|
48
|
+
organizationName: string | null;
|
|
49
|
+
submitting: boolean;
|
|
50
|
+
/** Why the last confirmation failed; the request stays pending to retry. */
|
|
51
|
+
error: string | null;
|
|
52
|
+
/**
|
|
53
|
+
* Saves `next` at once, or holds it for confirmation when it broadens.
|
|
54
|
+
* The promise resolves only once the change is persisted. A change that
|
|
55
|
+
* needs no confirmation rejects with the save's failure. A held change that
|
|
56
|
+
* fails when confirmed keeps its `error` and stays pending so the user can
|
|
57
|
+
* retry, and its promise stays pending too: it resolves after a successful
|
|
58
|
+
* retry and rejects with `ShareVisibilityNotSavedError` once the change is
|
|
59
|
+
* cancelled or invalidated (resource change, closed surface, lost manage
|
|
60
|
+
* rights). A request that is refused up front rejects the same way.
|
|
61
|
+
* While a save runs, the same target joins it and a different target is
|
|
62
|
+
* refused with reason `ignored` until the write settles.
|
|
63
|
+
*/
|
|
64
|
+
request: (next: ShareVisibility) => Promise<void>;
|
|
65
|
+
confirm: () => Promise<void>;
|
|
66
|
+
cancel: () => void;
|
|
67
|
+
}
|
|
68
|
+
export declare function useShareVisibilityTransition(options: ShareVisibilityTransitionOptions): ShareVisibilityTransition;
|