@agent-native/toolkit 0.198.3 → 0.198.4
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/app/agentkit/react/components.d.ts +4 -1
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +12 -4
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +43 -21
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
- package/dist/app/chat/AgentSidebar.js +2 -0
- package/dist/app/chat/AgentSidebar.js.map +1 -1
- package/dist/app/chat/FileStorageSetupPopover.js +2 -2
- package/dist/app/chat/FileStorageSetupPopover.js.map +1 -1
- package/dist/app/chat/MultiTabAssistantChat.js +1 -1
- package/dist/app/chat/agentkit-chat/connections.d.ts +1 -4
- package/dist/app/chat/agentkit-chat/connections.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/connections.js +1 -26
- package/dist/app/chat/agentkit-chat/connections.js.map +1 -1
- package/dist/app/chat/agentkit-chat/history.d.ts +4 -1
- package/dist/app/chat/agentkit-chat/history.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/history.js +17 -5
- package/dist/app/chat/agentkit-chat/history.js.map +1 -1
- package/dist/app/chat/chat/history-types.d.ts +1 -1
- package/dist/app/chat/chat/history-types.d.ts.map +1 -1
- package/dist/app/chat/chat-first/copy.d.ts +1 -2
- package/dist/app/chat/chat-first/copy.d.ts.map +1 -1
- package/dist/app/chat/chat-first/copy.js +1 -121
- package/dist/app/chat/chat-first/copy.js.map +1 -1
- package/dist/app/chat/chat-first-copy.d.ts +3 -0
- package/dist/app/chat/chat-first-copy.d.ts.map +1 -0
- package/dist/app/chat/chat-first-copy.js +122 -0
- package/dist/app/chat/chat-first-copy.js.map +1 -0
- package/dist/app/feature-flags/FeatureFlagsPanel.d.ts.map +1 -1
- package/dist/app/feature-flags/FeatureFlagsPanel.js +1 -0
- package/dist/app/feature-flags/FeatureFlagsPanel.js.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +2 -1
- 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 +2 -1
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +2 -1
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +2 -1
- 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 +2 -1
- 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 +2 -1
- 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 +2 -1
- 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 +2 -1
- 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 +2 -1
- 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 +2 -1
- 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 +2 -1
- 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 +2 -1
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/labs/LabsSettings.d.ts.map +1 -1
- package/dist/app/labs/LabsSettings.js +7 -3
- package/dist/app/labs/LabsSettings.js.map +1 -1
- package/dist/app/org/OrgGeneralSection.d.ts.map +1 -1
- package/dist/app/org/OrgGeneralSection.js +21 -18
- package/dist/app/org/OrgGeneralSection.js.map +1 -1
- package/dist/app/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/app/org/OrgSwitcher.js +3 -2
- package/dist/app/org/OrgSwitcher.js.map +1 -1
- package/dist/app/org/workspace-app-links.d.ts +1 -0
- package/dist/app/org/workspace-app-links.d.ts.map +1 -1
- package/dist/app/org/workspace-app-links.js +3 -0
- package/dist/app/org/workspace-app-links.js.map +1 -1
- package/dist/app/resources/McpAgentKitConnectionRequest.d.ts +7 -2
- package/dist/app/resources/McpAgentKitConnectionRequest.d.ts.map +1 -1
- package/dist/app/resources/McpAgentKitConnectionRequest.js +132 -13
- package/dist/app/resources/McpAgentKitConnectionRequest.js.map +1 -1
- package/dist/icons/ResourceIconPicker.d.ts +6 -1
- package/dist/icons/ResourceIconPicker.d.ts.map +1 -1
- package/dist/icons/ResourceIconPicker.js +45 -6
- package/dist/icons/ResourceIconPicker.js.map +1 -1
- package/package.json +6 -4
- package/src/app/agentkit/react/components.spec.tsx +3 -3
- package/src/app/agentkit/react/components.tsx +22 -4
- package/src/app/agentkit/react/lifecycle.spec.tsx +42 -0
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +114 -11
- package/src/app/chat/AgentKitAssistantChat.tsx +78 -17
- package/src/app/chat/AgentPanel.header.spec.ts +59 -22
- package/src/app/chat/AgentSidebar.tsx +2 -0
- package/src/app/chat/FileStorageSetupPopover.tsx +2 -2
- package/src/app/chat/MultiTabAssistantChat.messageCount.spec.tsx +3 -1
- package/src/app/chat/MultiTabAssistantChat.spec.tsx +8 -4
- package/src/app/chat/MultiTabAssistantChat.tsx +1 -1
- package/src/app/chat/agentkit-chat/action-widget.spec.tsx +5 -17
- package/src/app/chat/agentkit-chat/connections.spec.tsx +137 -0
- package/src/app/chat/agentkit-chat/connections.ts +2 -37
- package/src/app/chat/agentkit-chat/history.tsx +23 -8
- package/src/app/chat/chat/history-types.ts +1 -1
- package/src/app/chat/chat/tool-call-display.spec.tsx +2 -2
- package/src/app/chat/chat/widgets/record-change-widget.spec.tsx +20 -29
- package/src/app/chat/chat-first/copy.ts +1 -133
- package/src/app/chat/chat-first-copy.ts +133 -0
- package/src/app/chat/composer/use-agentkit-capabilities.hardening.spec.tsx +3 -2
- package/src/app/feature-flags/FeatureFlagsPanel.tsx +1 -0
- package/src/app/i18n/catalogs/ar-SA.ts +2 -1
- package/src/app/i18n/catalogs/de-DE.ts +3 -1
- package/src/app/i18n/catalogs/en-US.ts +2 -1
- package/src/app/i18n/catalogs/es-ES.ts +2 -1
- package/src/app/i18n/catalogs/fr-FR.ts +3 -1
- package/src/app/i18n/catalogs/hi-IN.ts +2 -1
- package/src/app/i18n/catalogs/ja-JP.ts +2 -1
- package/src/app/i18n/catalogs/ko-KR.ts +2 -1
- package/src/app/i18n/catalogs/pt-BR.ts +3 -1
- package/src/app/i18n/catalogs/zh-CN.ts +2 -1
- package/src/app/i18n/catalogs/zh-TW.ts +2 -1
- package/src/app/integrations/IntegrationDetailPage.spec.tsx +1 -1
- package/src/app/labs/LabsSettings.tsx +46 -10
- package/src/app/observability/ThumbsFeedback.spec.tsx +15 -0
- package/src/app/onboarding/FirstRunOnboarding.spec.tsx +4 -0
- package/src/app/org/OrgGeneralSection.tsx +32 -22
- package/src/app/org/OrgSwitcher.tsx +3 -2
- package/src/app/org/workspace-app-links.spec.ts +4 -0
- package/src/app/org/workspace-app-links.ts +8 -1
- package/src/app/resources/McpAgentKitConnectionRequest.spec.tsx +371 -2
- package/src/app/resources/McpAgentKitConnectionRequest.tsx +171 -11
- package/src/app/settings/SettingsTabsPage.spec.tsx +4 -2
- package/src/app/settings/shell/SettingsShell.spec.tsx +14 -8
- package/src/app/settings/shell/pages/app-group-pages.spec.tsx +35 -5
- package/src/app/settings/shell/pages/resource-pages.spec.tsx +3 -0
- package/src/app/settings/usage/UsagePage.spec.tsx +3 -3
- package/src/app/shared/lazy-chunk-error-boundary.spec.tsx +2 -0
- package/src/app-shell/sidebar-overflow.spec.tsx +4 -1
- package/src/design-tweaks/visual-style-controls.spec.tsx +38 -0
- package/src/editor/useCollabReconcile.concurrent.spec.ts +2 -1
- package/src/icons/ResourceIconPicker.spec.tsx +270 -0
- package/src/icons/ResourceIconPicker.tsx +72 -4
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ResourceIconPicker,
|
|
9
|
+
type ResourceIconPickerLabels,
|
|
10
|
+
} from "./ResourceIconPicker.js";
|
|
11
|
+
import type { ResourceIconImage, ResourceIconValue } from "./types.js";
|
|
12
|
+
|
|
13
|
+
vi.mock("./catalog.js", () => ({ loadTablerCatalog: async () => [] }));
|
|
14
|
+
vi.mock("./emoji-catalog.js", () => ({
|
|
15
|
+
EMOJI_CATEGORIES: [],
|
|
16
|
+
loadEmojiCatalog: async () => [],
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const labels: ResourceIconPickerLabels = {
|
|
20
|
+
trigger: "Choose icon",
|
|
21
|
+
iconsTab: "Icons",
|
|
22
|
+
emojiTab: "Emoji",
|
|
23
|
+
uploadTab: "Upload",
|
|
24
|
+
search: "Search",
|
|
25
|
+
noResults: "No results",
|
|
26
|
+
recents: "Recent",
|
|
27
|
+
colors: "Colors",
|
|
28
|
+
defaultColor: "Default",
|
|
29
|
+
remove: "Remove",
|
|
30
|
+
upload: "Choose image",
|
|
31
|
+
uploading: "Uploading",
|
|
32
|
+
saveError: "Could not save icon.",
|
|
33
|
+
uploadFailed: "Could not upload icon image. Try again.",
|
|
34
|
+
uploadTooLarge: "Icon image must be 5 MiB or smaller.",
|
|
35
|
+
uploadUnsupportedType: "Only PNG, JPEG, WebP, or SVG images can be uploaded.",
|
|
36
|
+
loadError: "Could not load uploaded icons.",
|
|
37
|
+
retry: "Retry",
|
|
38
|
+
};
|
|
39
|
+
const previousIcon: ResourceIconValue = {
|
|
40
|
+
version: 1,
|
|
41
|
+
kind: "emoji",
|
|
42
|
+
emoji: "🌿",
|
|
43
|
+
};
|
|
44
|
+
const uploadedIcon: ResourceIconImage = {
|
|
45
|
+
version: 1,
|
|
46
|
+
kind: "image",
|
|
47
|
+
authority: "url",
|
|
48
|
+
assetId: "/uploaded.svg",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe("ResourceIconPicker uploads", () => {
|
|
52
|
+
let container: HTMLDivElement;
|
|
53
|
+
let root: Root;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
57
|
+
vi.stubGlobal(
|
|
58
|
+
"ResizeObserver",
|
|
59
|
+
class {
|
|
60
|
+
observe() {}
|
|
61
|
+
unobserve() {}
|
|
62
|
+
disconnect() {}
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
container = document.createElement("div");
|
|
66
|
+
document.body.append(container);
|
|
67
|
+
root = createRoot(container);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
act(() => root.unmount());
|
|
72
|
+
container.remove();
|
|
73
|
+
vi.unstubAllGlobals();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
async function renderPicker(
|
|
77
|
+
onUpload: (file: File) => Promise<ResourceIconImage>,
|
|
78
|
+
onValueChange = vi.fn(),
|
|
79
|
+
formatUploadError?: (error: unknown) => string,
|
|
80
|
+
) {
|
|
81
|
+
await act(async () => {
|
|
82
|
+
root.render(
|
|
83
|
+
<ResourceIconPicker
|
|
84
|
+
value={previousIcon}
|
|
85
|
+
onValueChange={onValueChange}
|
|
86
|
+
onUpload={onUpload}
|
|
87
|
+
formatUploadError={formatUploadError}
|
|
88
|
+
labels={labels}
|
|
89
|
+
open
|
|
90
|
+
portalled={false}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
const uploadTab = Array.from(container.querySelectorAll("[role=tab]")).find(
|
|
95
|
+
(tab) => tab.textContent === labels.uploadTab,
|
|
96
|
+
);
|
|
97
|
+
expect(uploadTab).toBeDefined();
|
|
98
|
+
await act(async () => {
|
|
99
|
+
uploadTab!.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
100
|
+
uploadTab!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function choose(file: File) {
|
|
105
|
+
const input =
|
|
106
|
+
container.querySelector<HTMLInputElement>('input[type="file"]')!;
|
|
107
|
+
Object.defineProperty(input, "files", {
|
|
108
|
+
configurable: true,
|
|
109
|
+
value: [file],
|
|
110
|
+
});
|
|
111
|
+
await act(async () => {
|
|
112
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function drop(file: File) {
|
|
117
|
+
const target = Array.from(container.querySelectorAll("button")).find(
|
|
118
|
+
(button) => button.textContent?.includes(labels.upload),
|
|
119
|
+
)!;
|
|
120
|
+
const event = new Event("drop", { bubbles: true, cancelable: true });
|
|
121
|
+
Object.defineProperty(event, "dataTransfer", { value: { files: [file] } });
|
|
122
|
+
await act(async () => target.dispatchEvent(event));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function alertText() {
|
|
126
|
+
return container.querySelector('[role="alert"]')?.textContent;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
it("rejects unsupported types from input and drop without replacing the icon", async () => {
|
|
130
|
+
const onUpload = vi.fn().mockResolvedValue(uploadedIcon);
|
|
131
|
+
const onValueChange = vi.fn();
|
|
132
|
+
await renderPicker(onUpload, onValueChange);
|
|
133
|
+
|
|
134
|
+
await choose(new File(["gif"], "animation.gif", { type: "image/gif" }));
|
|
135
|
+
expect(alertText()).toBe(labels.uploadUnsupportedType);
|
|
136
|
+
|
|
137
|
+
await drop(new File(["text"], "notes.txt", { type: "text/plain" }));
|
|
138
|
+
expect(alertText()).toBe(labels.uploadUnsupportedType);
|
|
139
|
+
expect(onUpload).not.toHaveBeenCalled();
|
|
140
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("shows the size limit for a supported file from either path", async () => {
|
|
144
|
+
const onUpload = vi.fn().mockResolvedValue(uploadedIcon);
|
|
145
|
+
await renderPicker(onUpload);
|
|
146
|
+
|
|
147
|
+
const large = new File([new Uint8Array(5 * 1024 * 1024 + 1)], "large.png", {
|
|
148
|
+
type: "image/png",
|
|
149
|
+
});
|
|
150
|
+
await choose(large);
|
|
151
|
+
expect(alertText()).toBe(labels.uploadTooLarge);
|
|
152
|
+
await drop(large);
|
|
153
|
+
expect(alertText()).toBe(labels.uploadTooLarge);
|
|
154
|
+
await drop(
|
|
155
|
+
new File([new Uint8Array(5 * 1024 * 1024 + 1)], "large.txt", {
|
|
156
|
+
type: "text/plain",
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
expect(alertText()).toBe(labels.uploadTooLarge);
|
|
160
|
+
expect(onUpload).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("keeps a mapped failure visible and clears it after a successful retry", async () => {
|
|
164
|
+
const onUpload = vi
|
|
165
|
+
.fn()
|
|
166
|
+
.mockRejectedValueOnce(new Error("private server detail"))
|
|
167
|
+
.mockResolvedValueOnce(uploadedIcon);
|
|
168
|
+
const onValueChange = vi.fn();
|
|
169
|
+
await renderPicker(
|
|
170
|
+
onUpload,
|
|
171
|
+
onValueChange,
|
|
172
|
+
() => "Storage is unavailable.",
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await choose(new File(["first"], "first.png", { type: "image/png" }));
|
|
176
|
+
expect(alertText()).toBe("Storage is unavailable.");
|
|
177
|
+
expect(container.textContent).not.toContain("private server detail");
|
|
178
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
179
|
+
|
|
180
|
+
await drop(new File(["<svg />"], "retry.SVG"));
|
|
181
|
+
expect(onUpload).toHaveBeenCalledTimes(2);
|
|
182
|
+
expect(onUpload.mock.calls[1]?.[0].type).toBe("image/svg+xml");
|
|
183
|
+
expect(onValueChange).toHaveBeenCalledWith(uploadedIcon);
|
|
184
|
+
expect(alertText()).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("uses the localized upload failure when the caller has no safe detail", async () => {
|
|
188
|
+
const onUpload = vi
|
|
189
|
+
.fn()
|
|
190
|
+
.mockRejectedValue(new Error("private server detail"));
|
|
191
|
+
await renderPicker(onUpload);
|
|
192
|
+
|
|
193
|
+
await choose(new File(["first"], "first.webp", { type: "image/webp" }));
|
|
194
|
+
expect(alertText()).toBe(labels.uploadFailed);
|
|
195
|
+
expect(container.textContent).not.toContain("private server detail");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("makes an unavailable upload library visible and retryable", async () => {
|
|
199
|
+
const onRetry = vi.fn();
|
|
200
|
+
await act(async () => {
|
|
201
|
+
root.render(
|
|
202
|
+
<ResourceIconPicker
|
|
203
|
+
value={previousIcon}
|
|
204
|
+
onValueChange={vi.fn()}
|
|
205
|
+
onUpload={vi.fn().mockResolvedValue(uploadedIcon)}
|
|
206
|
+
uploadedImagesError
|
|
207
|
+
onUploadedImagesRetry={onRetry}
|
|
208
|
+
labels={labels}
|
|
209
|
+
open
|
|
210
|
+
portalled={false}
|
|
211
|
+
/>,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
const uploadTab = Array.from(container.querySelectorAll("[role=tab]")).find(
|
|
215
|
+
(tab) => tab.textContent === labels.uploadTab,
|
|
216
|
+
);
|
|
217
|
+
await act(async () => {
|
|
218
|
+
uploadTab!.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
219
|
+
uploadTab!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(alertText()).toContain(labels.loadError);
|
|
223
|
+
const retryButton = Array.from(container.querySelectorAll("button")).find(
|
|
224
|
+
(button) => button.textContent === labels.retry,
|
|
225
|
+
);
|
|
226
|
+
await act(async () => retryButton!.click());
|
|
227
|
+
expect(onRetry).toHaveBeenCalledOnce();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("does not offer private recent images outside the current library", async () => {
|
|
231
|
+
const personal: ResourceIconImage = {
|
|
232
|
+
version: 1,
|
|
233
|
+
kind: "image",
|
|
234
|
+
authority: "private-icon",
|
|
235
|
+
assetId: "personal-asset",
|
|
236
|
+
alt: "Personal logo",
|
|
237
|
+
};
|
|
238
|
+
const workspace: ResourceIconImage = {
|
|
239
|
+
...personal,
|
|
240
|
+
assetId: "workspace-asset",
|
|
241
|
+
alt: "Workspace logo",
|
|
242
|
+
};
|
|
243
|
+
await act(async () => {
|
|
244
|
+
root.render(
|
|
245
|
+
<ResourceIconPicker
|
|
246
|
+
value={previousIcon}
|
|
247
|
+
onValueChange={vi.fn()}
|
|
248
|
+
onUpload={vi.fn()}
|
|
249
|
+
uploadedImages={[workspace]}
|
|
250
|
+
recentValues={[personal, workspace]}
|
|
251
|
+
labels={labels}
|
|
252
|
+
open
|
|
253
|
+
portalled={false}
|
|
254
|
+
/>,
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
const uploadTab = Array.from(container.querySelectorAll("[role=tab]")).find(
|
|
258
|
+
(tab) => tab.textContent === labels.uploadTab,
|
|
259
|
+
);
|
|
260
|
+
await act(async () => {
|
|
261
|
+
uploadTab!.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
|
|
262
|
+
uploadTab!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(
|
|
266
|
+
container.querySelector('[aria-label="Workspace logo"]'),
|
|
267
|
+
).not.toBeNull();
|
|
268
|
+
expect(container.querySelector('[aria-label="Personal logo"]')).toBeNull();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -51,6 +51,32 @@ const colors: Array<ResourceIconColor | undefined> = [
|
|
|
51
51
|
const columns = 7;
|
|
52
52
|
const rowHeight = 36;
|
|
53
53
|
const MAX_ICON_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
54
|
+
const ICON_UPLOAD_MIME_TYPES = new Set([
|
|
55
|
+
"image/png",
|
|
56
|
+
"image/jpeg",
|
|
57
|
+
"image/webp",
|
|
58
|
+
"image/svg+xml",
|
|
59
|
+
]);
|
|
60
|
+
const ICON_UPLOAD_EXTENSION_TYPES: Record<string, string> = {
|
|
61
|
+
png: "image/png",
|
|
62
|
+
jpg: "image/jpeg",
|
|
63
|
+
jpeg: "image/jpeg",
|
|
64
|
+
webp: "image/webp",
|
|
65
|
+
svg: "image/svg+xml",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function supportedIconUpload(file: File): File | null {
|
|
69
|
+
const mimeType = file.type.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
70
|
+
if (ICON_UPLOAD_MIME_TYPES.has(mimeType)) return file;
|
|
71
|
+
if (mimeType && mimeType !== "application/octet-stream") return null;
|
|
72
|
+
const extension = file.name.split(".").pop()?.toLowerCase() ?? "";
|
|
73
|
+
const inferredType = ICON_UPLOAD_EXTENSION_TYPES[extension];
|
|
74
|
+
if (!inferredType) return null;
|
|
75
|
+
return new File([file], file.name, {
|
|
76
|
+
type: inferredType,
|
|
77
|
+
lastModified: file.lastModified,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
54
80
|
|
|
55
81
|
export interface ResourceIconPickerLabels {
|
|
56
82
|
trigger: string;
|
|
@@ -69,7 +95,9 @@ export interface ResourceIconPickerLabels {
|
|
|
69
95
|
saveError?: string;
|
|
70
96
|
retry?: string;
|
|
71
97
|
uploadHint?: string;
|
|
98
|
+
uploadFailed?: string;
|
|
72
99
|
uploadTooLarge?: string;
|
|
100
|
+
uploadUnsupportedType?: string;
|
|
73
101
|
allCategories?: string;
|
|
74
102
|
colorNames?: Partial<Record<ResourceIconColor, string>>;
|
|
75
103
|
categoryNames?: Record<string, string>;
|
|
@@ -83,8 +111,11 @@ export interface ResourceIconPickerProps {
|
|
|
83
111
|
recentValues?: readonly ResourceIconValue[];
|
|
84
112
|
onRecentsChange?: (recents: ResourceIconValue[]) => void;
|
|
85
113
|
uploadedImages?: readonly ResourceIconImage[];
|
|
114
|
+
uploadedImagesError?: boolean;
|
|
115
|
+
onUploadedImagesRetry?: () => void;
|
|
86
116
|
onUpload?: (file: File) => Promise<ResourceIconImage>;
|
|
87
117
|
onUploadError?: (error: unknown) => void;
|
|
118
|
+
formatUploadError?: (error: unknown) => string;
|
|
88
119
|
resolveImageUrl?: (
|
|
89
120
|
image: Extract<ResourceIconValue, { kind: "image" }>,
|
|
90
121
|
) => string | undefined;
|
|
@@ -323,8 +354,11 @@ export function ResourceIconPicker({
|
|
|
323
354
|
recentValues,
|
|
324
355
|
onRecentsChange,
|
|
325
356
|
uploadedImages = [],
|
|
357
|
+
uploadedImagesError = false,
|
|
358
|
+
onUploadedImagesRetry,
|
|
326
359
|
onUpload,
|
|
327
360
|
onUploadError,
|
|
361
|
+
formatUploadError,
|
|
328
362
|
resolveImageUrl,
|
|
329
363
|
disabled,
|
|
330
364
|
className,
|
|
@@ -475,13 +509,22 @@ export function ResourceIconPicker({
|
|
|
475
509
|
if (fileInput.current) fileInput.current.value = "";
|
|
476
510
|
return;
|
|
477
511
|
}
|
|
512
|
+
const supportedFile = supportedIconUpload(file);
|
|
513
|
+
if (!supportedFile) {
|
|
514
|
+
setSaveFailed(false);
|
|
515
|
+
setUploadError(labels.uploadUnsupportedType ?? labels.saveError);
|
|
516
|
+
if (fileInput.current) fileInput.current.value = "";
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
478
519
|
setUploading(true);
|
|
479
520
|
setSaveFailed(false);
|
|
480
521
|
setUploadError(undefined);
|
|
481
522
|
try {
|
|
482
|
-
await select(await onUpload(
|
|
523
|
+
await select(await onUpload(supportedFile));
|
|
483
524
|
} catch (error) {
|
|
484
|
-
|
|
525
|
+
setUploadError(
|
|
526
|
+
formatUploadError?.(error) || labels.uploadFailed || labels.saveError,
|
|
527
|
+
);
|
|
485
528
|
onUploadError?.(error);
|
|
486
529
|
} finally {
|
|
487
530
|
setUploading(false);
|
|
@@ -524,7 +567,14 @@ export function ResourceIconPicker({
|
|
|
524
567
|
const uploadChoices = [
|
|
525
568
|
...uploadedImages,
|
|
526
569
|
...effectiveRecents.filter(
|
|
527
|
-
(entry): entry is ResourceIconImage =>
|
|
570
|
+
(entry): entry is ResourceIconImage =>
|
|
571
|
+
entry.kind === "image" &&
|
|
572
|
+
(entry.authority !== "private-icon" ||
|
|
573
|
+
uploadedImages.some(
|
|
574
|
+
(uploaded) =>
|
|
575
|
+
uploaded.authority === entry.authority &&
|
|
576
|
+
uploaded.assetId === entry.assetId,
|
|
577
|
+
)),
|
|
528
578
|
),
|
|
529
579
|
].filter(
|
|
530
580
|
(entry, index, all) =>
|
|
@@ -720,7 +770,7 @@ export function ResourceIconPicker({
|
|
|
720
770
|
<input
|
|
721
771
|
ref={fileInput}
|
|
722
772
|
type="file"
|
|
723
|
-
accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
|
773
|
+
accept="image/png,image/jpeg,image/webp,image/svg+xml,.png,.jpg,.jpeg,.webp,.svg"
|
|
724
774
|
className="hidden"
|
|
725
775
|
disabled={uploading}
|
|
726
776
|
onChange={(event) => {
|
|
@@ -728,6 +778,24 @@ export function ResourceIconPicker({
|
|
|
728
778
|
if (file) void upload(file);
|
|
729
779
|
}}
|
|
730
780
|
/>
|
|
781
|
+
{uploadedImagesError && (
|
|
782
|
+
<div
|
|
783
|
+
role="alert"
|
|
784
|
+
className="flex items-center justify-between gap-2 text-sm text-destructive"
|
|
785
|
+
>
|
|
786
|
+
<span>{labels.loadError}</span>
|
|
787
|
+
{onUploadedImagesRetry && (
|
|
788
|
+
<Button
|
|
789
|
+
type="button"
|
|
790
|
+
variant="outline"
|
|
791
|
+
size="sm"
|
|
792
|
+
onClick={onUploadedImagesRetry}
|
|
793
|
+
>
|
|
794
|
+
{labels.retry}
|
|
795
|
+
</Button>
|
|
796
|
+
)}
|
|
797
|
+
</div>
|
|
798
|
+
)}
|
|
731
799
|
{uploadChoices.length > 0 && (
|
|
732
800
|
<div className="grid grid-cols-7 gap-1">
|
|
733
801
|
{uploadChoices.map((entry) => (
|