@agent-native/toolkit 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/agent-native.eject.json +1 -0
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +23 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +23 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +23 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +23 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +23 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +23 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +23 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +23 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +23 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +23 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +23 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +23 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/sharing/OwnershipFilter.d.ts +8 -0
- package/dist/app/sharing/OwnershipFilter.d.ts.map +1 -0
- package/dist/app/sharing/OwnershipFilter.js +20 -0
- package/dist/app/sharing/OwnershipFilter.js.map +1 -0
- package/dist/app/sharing/ShareButton.d.ts +43 -1
- package/dist/app/sharing/ShareButton.d.ts.map +1 -1
- package/dist/app/sharing/ShareButton.js +102 -63
- package/dist/app/sharing/ShareButton.js.map +1 -1
- package/dist/app/sharing/ShareDialog.d.ts +3 -1
- package/dist/app/sharing/ShareDialog.d.ts.map +1 -1
- package/dist/app/sharing/ShareDialog.js +18 -12
- package/dist/app/sharing/ShareDialog.js.map +1 -1
- package/dist/app/sharing/ShareVisibilityConfirmDialog.d.ts +20 -0
- package/dist/app/sharing/ShareVisibilityConfirmDialog.d.ts.map +1 -0
- package/dist/app/sharing/ShareVisibilityConfirmDialog.js +57 -0
- package/dist/app/sharing/ShareVisibilityConfirmDialog.js.map +1 -0
- package/dist/app/sharing/VisibilityBadge.d.ts +8 -1
- package/dist/app/sharing/VisibilityBadge.d.ts.map +1 -1
- package/dist/app/sharing/VisibilityBadge.js +16 -1
- package/dist/app/sharing/VisibilityBadge.js.map +1 -1
- package/dist/app/sharing/index.d.ts +2 -0
- package/dist/app/sharing/index.d.ts.map +1 -1
- package/dist/app/sharing/index.js +2 -0
- package/dist/app/sharing/index.js.map +1 -1
- package/dist/chat-history/ChatHistoryList.d.ts +19 -0
- package/dist/chat-history/ChatHistoryList.d.ts.map +1 -1
- package/dist/chat-history/ChatHistoryList.js +19 -7
- package/dist/chat-history/ChatHistoryList.js.map +1 -1
- package/dist/chat-history/ChatHistoryRail.d.ts +55 -8
- package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -1
- package/dist/chat-history/ChatHistoryRail.js +33 -7
- package/dist/chat-history/ChatHistoryRail.js.map +1 -1
- package/dist/chat-history/index.d.ts +3 -3
- package/dist/chat-history/index.d.ts.map +1 -1
- package/dist/chat-history/index.js +1 -1
- package/dist/chat-history/index.js.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.d.ts +44 -0
- package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.js +46 -0
- package/dist/chat-history/useChatHistoryRailController.js.map +1 -1
- package/dist/chat-history.css +29 -0
- package/dist/composer/TiptapComposer.d.ts +7 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +15 -2
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/sharing/OwnershipFilter.d.ts +21 -0
- package/dist/sharing/OwnershipFilter.d.ts.map +1 -0
- package/dist/sharing/OwnershipFilter.js +30 -0
- package/dist/sharing/OwnershipFilter.js.map +1 -0
- package/dist/sharing/VisibilityBadge.d.ts +12 -2
- package/dist/sharing/VisibilityBadge.d.ts.map +1 -1
- package/dist/sharing/VisibilityBadge.js +13 -4
- package/dist/sharing/VisibilityBadge.js.map +1 -1
- package/dist/sharing/index.d.ts +2 -1
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js +2 -1
- package/dist/sharing/index.js.map +1 -1
- package/dist/ui/alert-dialog.d.ts +2 -0
- package/dist/ui/alert-dialog.d.ts.map +1 -1
- package/dist/ui/alert-dialog.js +1 -1
- package/dist/ui/alert-dialog.js.map +1 -1
- package/dist/ui/tooltip.d.ts +6 -1
- package/dist/ui/tooltip.d.ts.map +1 -1
- package/dist/ui/tooltip.js +13 -1
- package/dist/ui/tooltip.js.map +1 -1
- package/package.json +4 -4
- package/src/app/i18n/catalogs/ar-SA.ts +26 -0
- package/src/app/i18n/catalogs/de-DE.ts +25 -0
- package/src/app/i18n/catalogs/en-US.ts +25 -0
- package/src/app/i18n/catalogs/es-ES.ts +25 -0
- package/src/app/i18n/catalogs/fr-FR.ts +25 -0
- package/src/app/i18n/catalogs/hi-IN.ts +25 -0
- package/src/app/i18n/catalogs/ja-JP.ts +25 -0
- package/src/app/i18n/catalogs/ko-KR.ts +25 -0
- package/src/app/i18n/catalogs/pt-BR.ts +25 -0
- package/src/app/i18n/catalogs/zh-CN.ts +25 -0
- package/src/app/i18n/catalogs/zh-TW.ts +25 -0
- package/src/app/sharing/OwnershipFilter.tsx +35 -0
- package/src/app/sharing/ShareButton.tsx +384 -259
- package/src/app/sharing/ShareDialog.spec.tsx +50 -0
- package/src/app/sharing/ShareDialog.tsx +51 -15
- package/src/app/sharing/ShareVisibilityConfirmDialog.spec.tsx +138 -0
- package/src/app/sharing/ShareVisibilityConfirmDialog.tsx +124 -0
- package/src/app/sharing/ShareVisibilityKeyboard.spec.tsx +244 -0
- package/src/app/sharing/VisibilityBadge.spec.tsx +65 -0
- package/src/app/sharing/VisibilityBadge.tsx +31 -3
- package/src/app/sharing/index.ts +2 -0
- package/src/app/sharing/share-visibility-mutation-lifecycle.spec.tsx +1107 -0
- package/src/app/sharing/useShareButtonController.spec.tsx +226 -37
- package/src/app/sharing/useShareDialogController.spec.tsx +357 -9
- package/src/chat-history/ChatHistoryList.spec.tsx +157 -0
- package/src/chat-history/ChatHistoryList.tsx +104 -10
- package/src/chat-history/ChatHistoryRail.spec.tsx +50 -1
- package/src/chat-history/ChatHistoryRail.tsx +177 -26
- package/src/chat-history/ChatHistoryRailSections.spec.tsx +410 -0
- package/src/chat-history/index.ts +8 -0
- package/src/chat-history/useChatHistoryRailController.ts +94 -0
- package/src/chat-history/useChatHistoryRailSectionsController.spec.tsx +133 -0
- package/src/chat-history.css +29 -0
- package/src/composer/TiptapComposer.spec.ts +19 -0
- package/src/composer/TiptapComposer.tsx +26 -5
- package/src/sharing/OwnershipFilter.spec.tsx +108 -0
- package/src/sharing/OwnershipFilter.tsx +103 -0
- package/src/sharing/VisibilityBadge.spec.ts +53 -35
- package/src/sharing/VisibilityBadge.tsx +23 -3
- package/src/sharing/index.ts +11 -0
- package/src/ui/alert-dialog.tsx +7 -2
- package/src/ui/tooltip.tsx +22 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ShareDialogController,
|
|
7
7
|
type ShareDialogControllerOptions,
|
|
8
8
|
} from "@agent-native/core/client/sharing/useShareDialogController";
|
|
9
|
+
import { ShareVisibilityNotSavedError } from "@agent-native/core/client/sharing/useShareVisibilityTransition";
|
|
9
10
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
10
11
|
import React, { act } from "react";
|
|
11
12
|
import { createRoot, type Root } from "react-dom/client";
|
|
@@ -20,10 +21,22 @@ const mocks = vi.hoisted(() => ({
|
|
|
20
21
|
},
|
|
21
22
|
share: { mutate: vi.fn(), isPending: false },
|
|
22
23
|
unshare: { mutate: vi.fn(), isPending: false },
|
|
23
|
-
visibility: { mutate: vi.fn(), isPending: false },
|
|
24
|
+
visibility: { mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false },
|
|
24
25
|
writeClipboardText: vi.fn(),
|
|
25
26
|
}));
|
|
26
27
|
|
|
28
|
+
// The controllers settle each save through mutateAsync; the tests drive it
|
|
29
|
+
// through the callbacks recorded on `mutate`.
|
|
30
|
+
mocks.visibility.mutateAsync.mockImplementation(
|
|
31
|
+
(variables: unknown) =>
|
|
32
|
+
new Promise((resolve, reject) => {
|
|
33
|
+
mocks.visibility.mutate(variables, {
|
|
34
|
+
onSuccess: resolve,
|
|
35
|
+
onError: reject,
|
|
36
|
+
});
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
27
40
|
vi.mock("@agent-native/core/client/use-action", () => ({
|
|
28
41
|
useActionQuery: vi.fn(() => mocks.query),
|
|
29
42
|
useActionMutation: vi.fn((name: string) => {
|
|
@@ -111,7 +124,12 @@ describe("useShareDialogController", () => {
|
|
|
111
124
|
});
|
|
112
125
|
});
|
|
113
126
|
|
|
114
|
-
afterEach(() => {
|
|
127
|
+
afterEach(async () => {
|
|
128
|
+
await act(async () => {
|
|
129
|
+
for (const [variables, callbacks] of mocks.visibility.mutate.mock.calls) {
|
|
130
|
+
callbacks?.onSuccess?.({ visibility: variables.visibility });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
115
133
|
act(() => root.unmount());
|
|
116
134
|
container.remove();
|
|
117
135
|
queryClient.clear();
|
|
@@ -238,6 +256,12 @@ describe("useShareDialogController", () => {
|
|
|
238
256
|
const memberShare = mocks.query.data?.shares[0];
|
|
239
257
|
|
|
240
258
|
act(() => result.visibility.set("org"));
|
|
259
|
+
expect(mocks.visibility.mutate).not.toHaveBeenCalled();
|
|
260
|
+
act(() => {
|
|
261
|
+
void (
|
|
262
|
+
controller as ShareDialogController
|
|
263
|
+
).visibility.transition.confirm();
|
|
264
|
+
});
|
|
241
265
|
expect(mocks.visibility.mutate).toHaveBeenCalledWith(
|
|
242
266
|
{ resourceType: "document", resourceId: "doc-1", visibility: "org" },
|
|
243
267
|
expect.objectContaining({ onSuccess: expect.any(Function) }),
|
|
@@ -266,18 +290,19 @@ describe("useShareDialogController", () => {
|
|
|
266
290
|
expect(mocks.share.mutate).not.toHaveBeenCalled();
|
|
267
291
|
});
|
|
268
292
|
|
|
269
|
-
it("
|
|
293
|
+
it("refetches the actual visibility save after refusing a different target", async () => {
|
|
270
294
|
let result = await render();
|
|
271
295
|
|
|
272
|
-
act(() => result.visibility.set("org"));
|
|
273
|
-
result = controller as ShareDialogController;
|
|
274
296
|
act(() => result.visibility.set("public"));
|
|
297
|
+
result = controller as ShareDialogController;
|
|
298
|
+
act(() => result.visibility.set("org"));
|
|
275
299
|
|
|
276
|
-
|
|
300
|
+
expect(mocks.visibility.mutate).toHaveBeenCalledOnce();
|
|
301
|
+
expect(result.visibility.value).toBe("public");
|
|
277
302
|
expect(mocks.query.refetch).not.toHaveBeenCalled();
|
|
278
303
|
|
|
279
304
|
await act(async () => {
|
|
280
|
-
mocks.visibility.mutate.mock.calls[
|
|
305
|
+
mocks.visibility.mutate.mock.calls[0]?.[1]?.onSuccess();
|
|
281
306
|
await Promise.resolve();
|
|
282
307
|
});
|
|
283
308
|
expect(mocks.query.refetch).toHaveBeenCalledOnce();
|
|
@@ -309,13 +334,23 @@ describe("useShareDialogController", () => {
|
|
|
309
334
|
{ enabled: false },
|
|
310
335
|
);
|
|
311
336
|
|
|
312
|
-
await render(options);
|
|
313
|
-
|
|
337
|
+
const opened = await render(options);
|
|
338
|
+
const fetched = vi.mocked(fetch).mock.calls.map(([url]) => String(url));
|
|
339
|
+
expect(fetched.filter((url) => url.includes("/org/members"))).toHaveLength(
|
|
340
|
+
1,
|
|
341
|
+
);
|
|
342
|
+
expect(fetched.filter((url) => url.endsWith("/org/me"))).toHaveLength(0);
|
|
314
343
|
expect(useActionQuery).toHaveBeenLastCalledWith(
|
|
315
344
|
"list-resource-shares",
|
|
316
345
|
expect.anything(),
|
|
317
346
|
{ enabled: true },
|
|
318
347
|
);
|
|
348
|
+
await act(async () => opened.visibility.set("org"));
|
|
349
|
+
expect(
|
|
350
|
+
vi
|
|
351
|
+
.mocked(fetch)
|
|
352
|
+
.mock.calls.filter(([url]) => String(url).endsWith("/org/me")),
|
|
353
|
+
).toHaveLength(1);
|
|
319
354
|
});
|
|
320
355
|
|
|
321
356
|
it("resets tab state when opened and delegates close transitions", async () => {
|
|
@@ -382,4 +417,317 @@ describe("useShareDialogController", () => {
|
|
|
382
417
|
]),
|
|
383
418
|
).toEqual(initial);
|
|
384
419
|
});
|
|
420
|
+
|
|
421
|
+
describe("organization confirmation", () => {
|
|
422
|
+
const sharesKey = [
|
|
423
|
+
"action",
|
|
424
|
+
"list-resource-shares",
|
|
425
|
+
{ resourceType: "document", resourceId: "doc-1" },
|
|
426
|
+
] as const;
|
|
427
|
+
|
|
428
|
+
const transition = () =>
|
|
429
|
+
(controller as ShareDialogController).visibility.transition;
|
|
430
|
+
|
|
431
|
+
it("holds a private-to-organization change until it is confirmed", async () => {
|
|
432
|
+
const initial = { ...mocks.query.data! };
|
|
433
|
+
queryClient.setQueryData(sharesKey, initial);
|
|
434
|
+
const result = await render();
|
|
435
|
+
|
|
436
|
+
act(() => result.visibility.set("org"));
|
|
437
|
+
|
|
438
|
+
expect(transition().pending).toEqual({ from: "private", to: "org" });
|
|
439
|
+
expect(mocks.visibility.mutate).not.toHaveBeenCalled();
|
|
440
|
+
expect(queryClient.getQueryData(sharesKey)).toEqual(initial);
|
|
441
|
+
expect((controller as ShareDialogController).visibility.value).toBe(
|
|
442
|
+
"private",
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("leaves the saved access and cache alone on cancel", async () => {
|
|
447
|
+
const initial = { ...mocks.query.data! };
|
|
448
|
+
queryClient.setQueryData(sharesKey, initial);
|
|
449
|
+
const result = await render();
|
|
450
|
+
act(() => result.visibility.set("org"));
|
|
451
|
+
|
|
452
|
+
act(() => transition().cancel());
|
|
453
|
+
|
|
454
|
+
expect(transition().pending).toBeNull();
|
|
455
|
+
expect(mocks.visibility.mutate).not.toHaveBeenCalled();
|
|
456
|
+
expect(queryClient.getQueryData(sharesKey)).toEqual(initial);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("saves once on confirm and closes after success", async () => {
|
|
460
|
+
const result = await render();
|
|
461
|
+
act(() => result.visibility.set("org"));
|
|
462
|
+
|
|
463
|
+
await act(async () => {
|
|
464
|
+
void transition().confirm();
|
|
465
|
+
void transition().confirm();
|
|
466
|
+
});
|
|
467
|
+
expect(mocks.visibility.mutate).toHaveBeenCalledOnce();
|
|
468
|
+
expect(transition().submitting).toBe(true);
|
|
469
|
+
|
|
470
|
+
await act(async () => {
|
|
471
|
+
mocks.visibility.mutate.mock.calls[0]?.[1]?.onSuccess({
|
|
472
|
+
visibility: "org",
|
|
473
|
+
});
|
|
474
|
+
await Promise.resolve();
|
|
475
|
+
});
|
|
476
|
+
expect(transition().pending).toBeNull();
|
|
477
|
+
expect(transition().submitting).toBe(false);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("keeps the confirmation open with the error and retries after a failure", async () => {
|
|
481
|
+
const initial = { ...mocks.query.data! };
|
|
482
|
+
queryClient.setQueryData(sharesKey, initial);
|
|
483
|
+
const result = await render();
|
|
484
|
+
act(() => result.visibility.set("org"));
|
|
485
|
+
await act(async () => {
|
|
486
|
+
void transition().confirm();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await act(async () => {
|
|
490
|
+
mocks.visibility.mutate.mock.calls[0]?.[1]?.onError(
|
|
491
|
+
new Error("visibility failed"),
|
|
492
|
+
);
|
|
493
|
+
await Promise.resolve();
|
|
494
|
+
await Promise.resolve();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
expect(transition().pending).toEqual({ from: "private", to: "org" });
|
|
498
|
+
expect(transition().error).toBe("visibility failed");
|
|
499
|
+
expect(transition().submitting).toBe(false);
|
|
500
|
+
expect(queryClient.getQueryData(sharesKey)).toEqual(initial);
|
|
501
|
+
expect((controller as ShareDialogController).error).toBeNull();
|
|
502
|
+
|
|
503
|
+
await act(async () => {
|
|
504
|
+
void transition().confirm();
|
|
505
|
+
});
|
|
506
|
+
expect(mocks.visibility.mutate).toHaveBeenCalledTimes(2);
|
|
507
|
+
expect(transition().error).toBeNull();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("reports the actual write failure after refusing a different target", async () => {
|
|
511
|
+
mocks.query.data = { ...mocks.query.data!, visibility: "public" };
|
|
512
|
+
const result = await render();
|
|
513
|
+
let older!: Promise<void>;
|
|
514
|
+
act(() => {
|
|
515
|
+
older = result.visibility.transition.request("org");
|
|
516
|
+
});
|
|
517
|
+
act(() =>
|
|
518
|
+
(controller as ShareDialogController).visibility.set("private"),
|
|
519
|
+
);
|
|
520
|
+
expect(mocks.visibility.mutate).toHaveBeenCalledOnce();
|
|
521
|
+
expect((controller as ShareDialogController).visibility.value).toBe(
|
|
522
|
+
"org",
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
await act(async () => {
|
|
526
|
+
mocks.visibility.mutate.mock.calls[0]?.[1]?.onError(
|
|
527
|
+
new Error("stale write failed"),
|
|
528
|
+
);
|
|
529
|
+
await expect(older).rejects.toThrow("stale write failed");
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
expect((controller as ShareDialogController).error).toBe(
|
|
533
|
+
"stale write failed",
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("drops the request when the resource changes or access is lost", async () => {
|
|
538
|
+
const result = await render();
|
|
539
|
+
act(() => result.visibility.set("org"));
|
|
540
|
+
expect(transition().pending).not.toBeNull();
|
|
541
|
+
|
|
542
|
+
await render({ ...options, resourceId: "doc-2" });
|
|
543
|
+
expect(transition().pending).toBeNull();
|
|
544
|
+
|
|
545
|
+
act(() => (controller as ShareDialogController).visibility.set("org"));
|
|
546
|
+
expect(transition().pending).not.toBeNull();
|
|
547
|
+
mocks.query.data = { ...mocks.query.data!, role: "viewer" };
|
|
548
|
+
await render({ ...options, resourceId: "doc-2" });
|
|
549
|
+
expect(transition().pending).toBeNull();
|
|
550
|
+
await act(async () => {
|
|
551
|
+
await transition().confirm();
|
|
552
|
+
});
|
|
553
|
+
expect(mocks.visibility.mutate).not.toHaveBeenCalled();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("does not confirm after the saved value moved or the dialog closed", async () => {
|
|
557
|
+
const result = await render();
|
|
558
|
+
act(() => result.visibility.set("org"));
|
|
559
|
+
const staleConfirm = transition().confirm;
|
|
560
|
+
|
|
561
|
+
mocks.query.data = { ...mocks.query.data!, visibility: "org" };
|
|
562
|
+
await render();
|
|
563
|
+
await act(async () => {
|
|
564
|
+
await staleConfirm();
|
|
565
|
+
});
|
|
566
|
+
expect(mocks.visibility.mutate).not.toHaveBeenCalled();
|
|
567
|
+
|
|
568
|
+
mocks.query.data = { ...mocks.query.data!, visibility: "private" };
|
|
569
|
+
await render();
|
|
570
|
+
act(() => (controller as ShareDialogController).visibility.set("org"));
|
|
571
|
+
const closedConfirm = transition().confirm;
|
|
572
|
+
await render({ ...options, open: false });
|
|
573
|
+
await act(async () => {
|
|
574
|
+
await closedConfirm();
|
|
575
|
+
});
|
|
576
|
+
expect(mocks.visibility.mutate).not.toHaveBeenCalled();
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("does not carry a pending change from one resource into the next", async () => {
|
|
580
|
+
await render();
|
|
581
|
+
act(() => (controller as ShareDialogController).visibility.set("public"));
|
|
582
|
+
expect(mocks.visibility.mutate).toHaveBeenCalledTimes(1);
|
|
583
|
+
expect((controller as ShareDialogController).visibility.value).toBe(
|
|
584
|
+
"public",
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
await render({ ...options, resourceId: "doc-2" });
|
|
588
|
+
expect((controller as ShareDialogController).visibility.value).toBe(
|
|
589
|
+
"private",
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
act(() => (controller as ShareDialogController).visibility.set("org"));
|
|
593
|
+
expect(transition().pending).toEqual({ from: "private", to: "org" });
|
|
594
|
+
expect(mocks.visibility.mutate).toHaveBeenCalledTimes(1);
|
|
595
|
+
|
|
596
|
+
await act(async () => {
|
|
597
|
+
mocks.visibility.mutate.mock.calls[0]?.[1]?.onSuccess({
|
|
598
|
+
visibility: "public",
|
|
599
|
+
});
|
|
600
|
+
await Promise.resolve();
|
|
601
|
+
});
|
|
602
|
+
expect(transition().pending).toEqual({ from: "private", to: "org" });
|
|
603
|
+
expect((controller as ShareDialogController).visibility.value).toBe(
|
|
604
|
+
"private",
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
await act(async () => {
|
|
608
|
+
void transition().confirm();
|
|
609
|
+
});
|
|
610
|
+
expect(mocks.visibility.mutate).toHaveBeenCalledTimes(2);
|
|
611
|
+
expect(mocks.visibility.mutate.mock.calls[1]?.[0]).toMatchObject({
|
|
612
|
+
resourceId: "doc-2",
|
|
613
|
+
visibility: "org",
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("settles a request only when it is persisted, rejects it otherwise", async () => {
|
|
618
|
+
await render();
|
|
619
|
+
let saved!: Promise<void>;
|
|
620
|
+
act(() => {
|
|
621
|
+
saved = transition().request("public");
|
|
622
|
+
});
|
|
623
|
+
await act(async () => {
|
|
624
|
+
mocks.visibility.mutate.mock.calls[0]?.[1]?.onError(
|
|
625
|
+
new Error("visibility failed"),
|
|
626
|
+
);
|
|
627
|
+
await expect(saved).rejects.toThrow("visibility failed");
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
mocks.visibility.mutate.mockClear();
|
|
631
|
+
let held!: Promise<void>;
|
|
632
|
+
act(() => {
|
|
633
|
+
held = transition().request("org");
|
|
634
|
+
});
|
|
635
|
+
act(() => transition().cancel());
|
|
636
|
+
await expect(held).rejects.toBeInstanceOf(ShareVisibilityNotSavedError);
|
|
637
|
+
expect(mocks.visibility.mutate).not.toHaveBeenCalled();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("keeps a held request pending through a failed confirmation until a retry saves", async () => {
|
|
641
|
+
await render();
|
|
642
|
+
let settled: "pending" | "saved" | "rejected" = "pending";
|
|
643
|
+
act(() => {
|
|
644
|
+
void transition()
|
|
645
|
+
.request("org")
|
|
646
|
+
.then(
|
|
647
|
+
() => {
|
|
648
|
+
settled = "saved";
|
|
649
|
+
},
|
|
650
|
+
() => {
|
|
651
|
+
settled = "rejected";
|
|
652
|
+
},
|
|
653
|
+
);
|
|
654
|
+
});
|
|
655
|
+
await act(async () => {
|
|
656
|
+
void transition().confirm();
|
|
657
|
+
});
|
|
658
|
+
await act(async () => {
|
|
659
|
+
mocks.visibility.mutate.mock.calls[0]?.[1]?.onError(new Error("nope"));
|
|
660
|
+
await Promise.resolve();
|
|
661
|
+
await Promise.resolve();
|
|
662
|
+
});
|
|
663
|
+
expect(transition().error).toBe("nope");
|
|
664
|
+
expect(settled).toBe("pending");
|
|
665
|
+
|
|
666
|
+
await act(async () => {
|
|
667
|
+
void transition().confirm();
|
|
668
|
+
});
|
|
669
|
+
await act(async () => {
|
|
670
|
+
mocks.visibility.mutate.mock.calls[1]?.[1]?.onSuccess({
|
|
671
|
+
visibility: "org",
|
|
672
|
+
});
|
|
673
|
+
await Promise.resolve();
|
|
674
|
+
await Promise.resolve();
|
|
675
|
+
});
|
|
676
|
+
expect(settled).toBe("saved");
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("joins a repeated request for a value that is still being saved", async () => {
|
|
680
|
+
await render();
|
|
681
|
+
let first!: Promise<void>;
|
|
682
|
+
act(() => {
|
|
683
|
+
first = transition().request("public");
|
|
684
|
+
});
|
|
685
|
+
let second!: Promise<void>;
|
|
686
|
+
act(() => {
|
|
687
|
+
second = (
|
|
688
|
+
controller as ShareDialogController
|
|
689
|
+
).visibility.transition.request("public");
|
|
690
|
+
});
|
|
691
|
+
expect(mocks.visibility.mutate).toHaveBeenCalledTimes(1);
|
|
692
|
+
|
|
693
|
+
await act(async () => {
|
|
694
|
+
mocks.visibility.mutate.mock.calls[0]?.[1]?.onError(
|
|
695
|
+
new Error("visibility failed"),
|
|
696
|
+
);
|
|
697
|
+
await expect(first).rejects.toThrow("visibility failed");
|
|
698
|
+
await expect(second).rejects.toThrow("visibility failed");
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("ignores a visibility request from a closed dialog", async () => {
|
|
703
|
+
await render({ ...options, open: false });
|
|
704
|
+
|
|
705
|
+
act(() => (controller as ShareDialogController).visibility.set("public"));
|
|
706
|
+
act(() => (controller as ShareDialogController).visibility.set("org"));
|
|
707
|
+
|
|
708
|
+
expect(mocks.visibility.mutate).not.toHaveBeenCalled();
|
|
709
|
+
expect(transition().pending).toBeNull();
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it("does not ask for narrowing or for private-to-public changes", async () => {
|
|
713
|
+
mocks.query.data = { ...mocks.query.data!, visibility: "public" };
|
|
714
|
+
const result = await render();
|
|
715
|
+
act(() => result.visibility.set("org"));
|
|
716
|
+
expect(transition().pending).toBeNull();
|
|
717
|
+
expect(mocks.visibility.mutate).toHaveBeenCalledTimes(1);
|
|
718
|
+
|
|
719
|
+
await act(async () => {
|
|
720
|
+
mocks.visibility.mutate.mock.calls[0]?.[1]?.onSuccess({
|
|
721
|
+
visibility: "org",
|
|
722
|
+
});
|
|
723
|
+
await Promise.resolve();
|
|
724
|
+
});
|
|
725
|
+
mocks.visibility.mutate.mockClear();
|
|
726
|
+
mocks.query.data = { ...mocks.query.data!, visibility: "private" };
|
|
727
|
+
const priv = await render();
|
|
728
|
+
act(() => priv.visibility.set("public"));
|
|
729
|
+
expect(transition().pending).toBeNull();
|
|
730
|
+
expect(mocks.visibility.mutate).toHaveBeenCalledTimes(1);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
385
733
|
});
|
|
@@ -73,6 +73,100 @@ describe("ChatHistoryList", () => {
|
|
|
73
73
|
vi.unstubAllGlobals();
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
it("shows the search empty state instead of section empty labels when every section has no matches", () => {
|
|
77
|
+
act(() =>
|
|
78
|
+
root.render(
|
|
79
|
+
<ChatHistoryList
|
|
80
|
+
sections={[
|
|
81
|
+
{
|
|
82
|
+
id: "mine",
|
|
83
|
+
label: "My chats",
|
|
84
|
+
items: [],
|
|
85
|
+
emptyLabel: "No chats yet",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "shared",
|
|
89
|
+
label: "Shared with you",
|
|
90
|
+
items: [],
|
|
91
|
+
emptyLabel: "No shared chats",
|
|
92
|
+
},
|
|
93
|
+
]}
|
|
94
|
+
searchValue="missing"
|
|
95
|
+
onSelect={() => {}}
|
|
96
|
+
/>,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
expect(container.textContent).toBe("No matching chats");
|
|
100
|
+
expect(
|
|
101
|
+
container.querySelectorAll(".an-chat-history__section-label"),
|
|
102
|
+
).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("keeps matching sections and their headings without empty labels in the other section", () => {
|
|
106
|
+
act(() =>
|
|
107
|
+
root.render(
|
|
108
|
+
<ChatHistoryList
|
|
109
|
+
sections={[
|
|
110
|
+
{ id: "mine", label: "My chats", items: [item({ id: "match" })] },
|
|
111
|
+
{
|
|
112
|
+
id: "shared",
|
|
113
|
+
label: "Shared with you",
|
|
114
|
+
items: [],
|
|
115
|
+
emptyLabel: "No shared chats",
|
|
116
|
+
},
|
|
117
|
+
]}
|
|
118
|
+
searchValue="match"
|
|
119
|
+
onSelect={() => {}}
|
|
120
|
+
/>,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
expect(container.textContent).toBe("My chatsmatch");
|
|
124
|
+
expect(
|
|
125
|
+
container.querySelectorAll(".an-chat-history__section-label"),
|
|
126
|
+
).toHaveLength(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it.each(["loading", "error", "footer"] as const)(
|
|
130
|
+
"preserves a search section's %s state and heading",
|
|
131
|
+
(state) => {
|
|
132
|
+
act(() =>
|
|
133
|
+
root.render(
|
|
134
|
+
<ChatHistoryList
|
|
135
|
+
sections={[
|
|
136
|
+
{
|
|
137
|
+
id: "shared",
|
|
138
|
+
label: "Shared",
|
|
139
|
+
items: [],
|
|
140
|
+
emptyLabel: "No shared chats",
|
|
141
|
+
loading: state === "loading",
|
|
142
|
+
error: state === "error" ? "Search failed" : undefined,
|
|
143
|
+
footer:
|
|
144
|
+
state === "footer" ? <button>Load more</button> : undefined,
|
|
145
|
+
},
|
|
146
|
+
]}
|
|
147
|
+
searchValue="missing"
|
|
148
|
+
onSelect={() => {}}
|
|
149
|
+
/>,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
expect(
|
|
153
|
+
container.querySelector(".an-chat-history__section-label")?.textContent,
|
|
154
|
+
).toBe("Shared");
|
|
155
|
+
expect(container.textContent).not.toContain("No shared chats");
|
|
156
|
+
expect(container.textContent).not.toContain("No matching chats");
|
|
157
|
+
if (state === "loading")
|
|
158
|
+
expect(container.querySelector('[aria-busy="true"]')).not.toBeNull();
|
|
159
|
+
if (state === "error")
|
|
160
|
+
expect(container.querySelector('[role="alert"]')?.textContent).toBe(
|
|
161
|
+
"Search failed",
|
|
162
|
+
);
|
|
163
|
+
if (state === "footer")
|
|
164
|
+
expect(container.querySelector("button")?.textContent).toBe(
|
|
165
|
+
"Load more",
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
76
170
|
it("renders a flat item list and calls onSelect when a row is clicked", () => {
|
|
77
171
|
const onSelect = vi.fn();
|
|
78
172
|
const items: ChatHistoryItem[] = [
|
|
@@ -478,4 +572,67 @@ describe("ChatHistoryList", () => {
|
|
|
478
572
|
});
|
|
479
573
|
expect(container.textContent).toContain("Load older chats");
|
|
480
574
|
});
|
|
575
|
+
|
|
576
|
+
it("hides actions per row while the row stays selectable and other rows keep theirs", async () => {
|
|
577
|
+
const onSelect = vi.fn();
|
|
578
|
+
act(() => {
|
|
579
|
+
root.render(
|
|
580
|
+
<ChatHistoryList
|
|
581
|
+
items={[
|
|
582
|
+
item({ id: "mine" }),
|
|
583
|
+
item({ id: "shared", hideActions: true, pinned: true }),
|
|
584
|
+
]}
|
|
585
|
+
onSelect={onSelect}
|
|
586
|
+
onRename={() => {}}
|
|
587
|
+
onDelete={() => {}}
|
|
588
|
+
onTogglePin={() => {}}
|
|
589
|
+
renderAdditionalRowActions={() => <span>extra</span>}
|
|
590
|
+
/>,
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const rows = container.querySelectorAll<HTMLElement>(
|
|
595
|
+
".an-chat-history-row",
|
|
596
|
+
);
|
|
597
|
+
expect(
|
|
598
|
+
rows[0]?.querySelector(".an-chat-history-row__menu-trigger"),
|
|
599
|
+
).not.toBeNull();
|
|
600
|
+
expect(rows[1]?.querySelector(".an-chat-history-row__menu")).toBeNull();
|
|
601
|
+
expect(rows[1]?.className).not.toContain("an-chat-history-row--has-menu");
|
|
602
|
+
|
|
603
|
+
await act(async () => {
|
|
604
|
+
rows[1]?.dispatchEvent(
|
|
605
|
+
new MouseEvent("contextmenu", { bubbles: true, cancelable: true }),
|
|
606
|
+
);
|
|
607
|
+
await Promise.resolve();
|
|
608
|
+
});
|
|
609
|
+
expect(
|
|
610
|
+
document.body.querySelector(".an-chat-history-row__menu-content"),
|
|
611
|
+
).toBeNull();
|
|
612
|
+
|
|
613
|
+
act(() => rows[1]?.querySelector("button")?.click());
|
|
614
|
+
expect(onSelect).toHaveBeenCalledWith("shared");
|
|
615
|
+
|
|
616
|
+
await openMenu(
|
|
617
|
+
rows[0]!.querySelector<HTMLButtonElement>(
|
|
618
|
+
".an-chat-history-row__menu-trigger",
|
|
619
|
+
)!,
|
|
620
|
+
);
|
|
621
|
+
expect(getMenuItems().map((element) => element.textContent)).toContain(
|
|
622
|
+
"Delete",
|
|
623
|
+
);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it("hides the menu even when renderRowActions would return nothing", () => {
|
|
627
|
+
act(() => {
|
|
628
|
+
root.render(
|
|
629
|
+
<ChatHistoryList
|
|
630
|
+
items={[item({ id: "shared", hideActions: true })]}
|
|
631
|
+
onSelect={() => {}}
|
|
632
|
+
renderRowActions={() => null}
|
|
633
|
+
/>,
|
|
634
|
+
);
|
|
635
|
+
});
|
|
636
|
+
expect(container.querySelector(".an-chat-history-row__menu")).toBeNull();
|
|
637
|
+
});
|
|
481
638
|
});
|