@forge/react 11.4.0 → 11.5.0-next.1
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/CHANGELOG.md +18 -0
- package/out/hooks/__test__/usePermissions.test.d.ts +2 -0
- package/out/hooks/__test__/usePermissions.test.d.ts.map +1 -0
- package/out/hooks/__test__/usePermissions.test.js +413 -0
- package/out/hooks/usePermissions.d.ts +89 -0
- package/out/hooks/usePermissions.d.ts.map +1 -0
- package/out/hooks/usePermissions.js +198 -0
- package/out/index.d.ts +1 -0
- package/out/index.d.ts.map +1 -1
- package/out/index.js +3 -1
- package/package.json +3 -2
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @forge/react
|
|
2
2
|
|
|
3
|
+
## 11.5.0-next.1
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 01521ac: add usePermissions hook
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [01521ac]
|
|
12
|
+
- @forge/bridge@5.6.0-next.1
|
|
13
|
+
|
|
14
|
+
## 11.4.1-next.0
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [58a20e7]
|
|
19
|
+
- @forge/bridge@5.6.0-next.0
|
|
20
|
+
|
|
3
21
|
## 11.4.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePermissions.test.d.ts","sourceRoot":"","sources":["../../../src/hooks/__test__/usePermissions.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const react_hooks_1 = require("@testing-library/react-hooks");
|
|
4
|
+
const usePermissions_1 = require("../usePermissions");
|
|
5
|
+
const testUtils_1 = require("../../__test__/testUtils");
|
|
6
|
+
// Mock @forge/bridge
|
|
7
|
+
jest.mock('@forge/bridge', () => ({
|
|
8
|
+
view: {
|
|
9
|
+
getContext: jest.fn()
|
|
10
|
+
}
|
|
11
|
+
}));
|
|
12
|
+
const mockGetContext = jest.fn();
|
|
13
|
+
describe('usePermissions', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
(0, testUtils_1.setupBridge)();
|
|
17
|
+
// Set up the mock to use our mockGetContext function
|
|
18
|
+
const { view } = require('@forge/bridge');
|
|
19
|
+
view.getContext.mockImplementation(mockGetContext);
|
|
20
|
+
});
|
|
21
|
+
describe('Interface inheritance', () => {
|
|
22
|
+
it('should allow PermissionRequirements to be used as Permissions', () => {
|
|
23
|
+
const requiredPermissions = {
|
|
24
|
+
scopes: ['read:confluence-content']
|
|
25
|
+
};
|
|
26
|
+
// This should compile without errors, demonstrating inheritance
|
|
27
|
+
const permissions = requiredPermissions;
|
|
28
|
+
expect(permissions.scopes).toEqual(['read:confluence-content']);
|
|
29
|
+
});
|
|
30
|
+
it('should allow MissingPermissions to be used as Permissions', () => {
|
|
31
|
+
const missingPermissions = {
|
|
32
|
+
scopes: ['write:jira-work']
|
|
33
|
+
};
|
|
34
|
+
// This should compile without errors, demonstrating inheritance
|
|
35
|
+
const permissions = missingPermissions;
|
|
36
|
+
expect(permissions.scopes).toEqual(['write:jira-work']);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('Loading state', () => {
|
|
40
|
+
it('should start with loading true', () => {
|
|
41
|
+
const requiredPermissions = {
|
|
42
|
+
scopes: ['read:confluence-content']
|
|
43
|
+
};
|
|
44
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
45
|
+
expect(result.current.isLoading).toBe(true);
|
|
46
|
+
expect(result.current.hasPermission).toBe(false);
|
|
47
|
+
expect(result.current.missingPermissions).toBe(null);
|
|
48
|
+
expect(result.current.error).toBe(null);
|
|
49
|
+
});
|
|
50
|
+
it('should set loading to false after context loads', async () => {
|
|
51
|
+
const mockContext = {
|
|
52
|
+
permissions: {
|
|
53
|
+
scopes: ['read:confluence-content']
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
57
|
+
const requiredPermissions = {
|
|
58
|
+
scopes: ['read:confluence-content']
|
|
59
|
+
};
|
|
60
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
61
|
+
await (0, react_hooks_1.act)(async () => {
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
63
|
+
});
|
|
64
|
+
expect(result.current.hasPermission).toBe(true);
|
|
65
|
+
expect(result.current.missingPermissions).toBe(null);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('Error handling', () => {
|
|
69
|
+
it('should handle context loading errors', async () => {
|
|
70
|
+
const error = new Error('Failed to load context');
|
|
71
|
+
mockGetContext.mockRejectedValue(error);
|
|
72
|
+
const requiredPermissions = {
|
|
73
|
+
scopes: ['read:confluence-content']
|
|
74
|
+
};
|
|
75
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
76
|
+
await (0, react_hooks_1.act)(async () => {
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
78
|
+
});
|
|
79
|
+
expect(result.current.isLoading).toBe(false);
|
|
80
|
+
expect(result.current.error).toEqual(error);
|
|
81
|
+
expect(result.current.hasPermission).toBe(false);
|
|
82
|
+
expect(result.current.missingPermissions).toBe(null);
|
|
83
|
+
});
|
|
84
|
+
it('should handle non-Error objects in catch block', async () => {
|
|
85
|
+
mockGetContext.mockRejectedValue('String error');
|
|
86
|
+
const requiredPermissions = {
|
|
87
|
+
scopes: ['read:confluence-content']
|
|
88
|
+
};
|
|
89
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
90
|
+
await (0, react_hooks_1.act)(async () => {
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
92
|
+
});
|
|
93
|
+
expect(result.current.error).toEqual(new Error('Failed to load context'));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('Scope permissions', () => {
|
|
97
|
+
it('should grant permission when all required scopes are present', async () => {
|
|
98
|
+
const mockContext = {
|
|
99
|
+
permissions: {
|
|
100
|
+
scopes: ['read:confluence-content', 'write:confluence-content']
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
104
|
+
const requiredPermissions = {
|
|
105
|
+
scopes: ['read:confluence-content']
|
|
106
|
+
};
|
|
107
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
108
|
+
await (0, react_hooks_1.act)(async () => {
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
110
|
+
});
|
|
111
|
+
expect(result.current.isLoading).toBe(false);
|
|
112
|
+
expect(result.current.hasPermission).toBe(true);
|
|
113
|
+
expect(result.current.missingPermissions).toBe(null);
|
|
114
|
+
});
|
|
115
|
+
it('should deny permission when required scopes are missing', async () => {
|
|
116
|
+
const mockContext = {
|
|
117
|
+
permissions: {
|
|
118
|
+
scopes: ['read:confluence-content']
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
122
|
+
const requiredPermissions = {
|
|
123
|
+
scopes: ['read:confluence-content', 'write:jira-work']
|
|
124
|
+
};
|
|
125
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
126
|
+
await (0, react_hooks_1.act)(async () => {
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
128
|
+
});
|
|
129
|
+
expect(result.current.isLoading).toBe(false);
|
|
130
|
+
expect(result.current.hasPermission).toBe(false);
|
|
131
|
+
expect(result.current.missingPermissions).toEqual({
|
|
132
|
+
scopes: ['write:jira-work']
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
it('should handle scopes as object format', async () => {
|
|
136
|
+
const mockContext = {
|
|
137
|
+
permissions: {
|
|
138
|
+
scopes: {
|
|
139
|
+
'read:confluence-content': { allowImpersonation: false },
|
|
140
|
+
'write:confluence-content': { allowImpersonation: true }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
145
|
+
const requiredPermissions = {
|
|
146
|
+
scopes: ['read:confluence-content']
|
|
147
|
+
};
|
|
148
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
149
|
+
await (0, react_hooks_1.act)(async () => {
|
|
150
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
151
|
+
});
|
|
152
|
+
expect(result.current.isLoading).toBe(false);
|
|
153
|
+
expect(result.current.hasPermission).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
it('should handle empty scopes array', async () => {
|
|
156
|
+
const mockContext = {
|
|
157
|
+
permissions: {
|
|
158
|
+
scopes: []
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
162
|
+
const requiredPermissions = {
|
|
163
|
+
scopes: []
|
|
164
|
+
};
|
|
165
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
166
|
+
await (0, react_hooks_1.act)(async () => {
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
168
|
+
});
|
|
169
|
+
expect(result.current.isLoading).toBe(false);
|
|
170
|
+
expect(result.current.hasPermission).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe('External permissions', () => {
|
|
174
|
+
it('should grant permission for allowed fetch URLs', async () => {
|
|
175
|
+
const mockContext = {
|
|
176
|
+
permissions: {
|
|
177
|
+
external: {
|
|
178
|
+
fetch: {
|
|
179
|
+
backend: ['https://api.example.com', 'https://api.test.com'],
|
|
180
|
+
client: ['https://cdn.example.com']
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
186
|
+
const requiredPermissions = {
|
|
187
|
+
external: {
|
|
188
|
+
fetch: {
|
|
189
|
+
backend: ['https://api.example.com'],
|
|
190
|
+
client: ['https://cdn.example.com']
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
195
|
+
await (0, react_hooks_1.act)(async () => {
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
197
|
+
});
|
|
198
|
+
expect(result.current.isLoading).toBe(false);
|
|
199
|
+
expect(result.current.hasPermission).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
it('should deny permission for disallowed fetch URLs', async () => {
|
|
202
|
+
const mockContext = {
|
|
203
|
+
permissions: {
|
|
204
|
+
external: {
|
|
205
|
+
fetch: {
|
|
206
|
+
backend: ['https://api.example.com']
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
212
|
+
const requiredPermissions = {
|
|
213
|
+
external: {
|
|
214
|
+
fetch: {
|
|
215
|
+
backend: ['https://api.example.com', 'https://api.unauthorized.com']
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
220
|
+
await (0, react_hooks_1.act)(async () => {
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
222
|
+
});
|
|
223
|
+
expect(result.current.isLoading).toBe(false);
|
|
224
|
+
expect(result.current.hasPermission).toBe(false);
|
|
225
|
+
expect(result.current.missingPermissions).toEqual({
|
|
226
|
+
external: {
|
|
227
|
+
fetch: {
|
|
228
|
+
backend: ['https://api.unauthorized.com']
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
it('should handle wildcard URL patterns', async () => {
|
|
234
|
+
const mockContext = {
|
|
235
|
+
permissions: {
|
|
236
|
+
external: {
|
|
237
|
+
fetch: {
|
|
238
|
+
backend: ['https://api.example.com/*', 'https://*.test.com/**']
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
244
|
+
const requiredPermissions = {
|
|
245
|
+
external: {
|
|
246
|
+
fetch: {
|
|
247
|
+
backend: ['https://api.example.com/users', 'https://subdomain.test.com/api']
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
252
|
+
await (0, react_hooks_1.act)(async () => {
|
|
253
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
254
|
+
});
|
|
255
|
+
expect(result.current.isLoading).toBe(false);
|
|
256
|
+
expect(result.current.hasPermission).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
it('should handle resource permissions (fonts, images, etc.)', async () => {
|
|
259
|
+
const mockContext = {
|
|
260
|
+
permissions: {
|
|
261
|
+
external: {
|
|
262
|
+
fonts: ['https://fonts.googleapis.com'],
|
|
263
|
+
images: ['https://images.example.com'],
|
|
264
|
+
scripts: ['https://scripts.example.com']
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
269
|
+
const requiredPermissions = {
|
|
270
|
+
external: {
|
|
271
|
+
fonts: ['https://fonts.googleapis.com'],
|
|
272
|
+
images: ['https://images.example.com']
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
276
|
+
await (0, react_hooks_1.act)(async () => {
|
|
277
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
278
|
+
});
|
|
279
|
+
expect(result.current.isLoading).toBe(false);
|
|
280
|
+
expect(result.current.hasPermission).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
describe('Complex permission scenarios', () => {
|
|
284
|
+
it('should handle mixed permission types', async () => {
|
|
285
|
+
const mockContext = {
|
|
286
|
+
permissions: {
|
|
287
|
+
scopes: ['read:confluence-content', 'write:confluence-content', 'read:jira-work'],
|
|
288
|
+
external: {
|
|
289
|
+
fetch: {
|
|
290
|
+
backend: ['https://api.example.com', 'https://api.test.com'],
|
|
291
|
+
client: ['https://cdn.example.com', 'https://cdn.test.com']
|
|
292
|
+
},
|
|
293
|
+
images: ['https://images.example.com', 'https://images.test.com'],
|
|
294
|
+
fonts: ['https://fonts.googleapis.com', 'https://fonts.gstatic.com'],
|
|
295
|
+
scripts: ['https://scripts.example.com']
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
300
|
+
const requiredPermissions = {
|
|
301
|
+
scopes: ['read:confluence-content', 'write:confluence-content'],
|
|
302
|
+
external: {
|
|
303
|
+
fetch: {
|
|
304
|
+
backend: ['https://api.example.com'],
|
|
305
|
+
client: ['https://cdn.example.com']
|
|
306
|
+
},
|
|
307
|
+
images: ['https://images.example.com'],
|
|
308
|
+
fonts: ['https://fonts.googleapis.com'],
|
|
309
|
+
scripts: ['https://scripts.example.com']
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
313
|
+
await (0, react_hooks_1.act)(async () => {
|
|
314
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
315
|
+
});
|
|
316
|
+
expect(result.current.isLoading).toBe(false);
|
|
317
|
+
expect(result.current.hasPermission).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
it('should identify missing permissions across different types', async () => {
|
|
320
|
+
const mockContext = {
|
|
321
|
+
permissions: {
|
|
322
|
+
scopes: ['read:confluence-content', 'read:jira-work'],
|
|
323
|
+
external: {
|
|
324
|
+
fetch: {
|
|
325
|
+
backend: ['https://api.example.com', 'https://api.authorized.com'],
|
|
326
|
+
client: ['https://cdn.authorized.com']
|
|
327
|
+
},
|
|
328
|
+
images: ['https://images.authorized.com'],
|
|
329
|
+
fonts: ['https://fonts.authorized.com']
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
334
|
+
const requiredPermissions = {
|
|
335
|
+
scopes: ['read:confluence-content', 'write:jira-work', 'admin:confluence'],
|
|
336
|
+
external: {
|
|
337
|
+
fetch: {
|
|
338
|
+
backend: ['https://api.example.com', 'https://api.unauthorized.com', 'https://api.missing.com'],
|
|
339
|
+
client: ['https://cdn.example.com', 'https://cdn.unauthorized.com']
|
|
340
|
+
},
|
|
341
|
+
images: ['https://images.example.com', 'https://images.unauthorized.com'],
|
|
342
|
+
fonts: ['https://fonts.example.com'],
|
|
343
|
+
scripts: ['https://scripts.example.com', 'https://scripts.unauthorized.com']
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
347
|
+
await (0, react_hooks_1.act)(async () => {
|
|
348
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
349
|
+
});
|
|
350
|
+
expect(result.current.isLoading).toBe(false);
|
|
351
|
+
expect(result.current.hasPermission).toBe(false);
|
|
352
|
+
expect(result.current.missingPermissions).toEqual({
|
|
353
|
+
scopes: ['write:jira-work', 'admin:confluence'],
|
|
354
|
+
external: {
|
|
355
|
+
fetch: {
|
|
356
|
+
backend: ['https://api.unauthorized.com', 'https://api.missing.com'],
|
|
357
|
+
client: ['https://cdn.example.com', 'https://cdn.unauthorized.com']
|
|
358
|
+
},
|
|
359
|
+
images: ['https://images.example.com', 'https://images.unauthorized.com'],
|
|
360
|
+
fonts: ['https://fonts.example.com'],
|
|
361
|
+
scripts: ['https://scripts.example.com', 'https://scripts.unauthorized.com']
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
describe('Edge cases', () => {
|
|
367
|
+
it('should handle empty required permissions', async () => {
|
|
368
|
+
const mockContext = {
|
|
369
|
+
permissions: {
|
|
370
|
+
scopes: ['read:confluence-content']
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
374
|
+
const requiredPermissions = {};
|
|
375
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
376
|
+
await (0, react_hooks_1.act)(async () => {
|
|
377
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
378
|
+
});
|
|
379
|
+
expect(result.current.isLoading).toBe(false);
|
|
380
|
+
expect(result.current.hasPermission).toBe(true);
|
|
381
|
+
expect(result.current.missingPermissions).toBe(null);
|
|
382
|
+
});
|
|
383
|
+
it('should handle complex external permission objects', async () => {
|
|
384
|
+
const mockContext = {
|
|
385
|
+
permissions: {
|
|
386
|
+
external: {
|
|
387
|
+
fetch: {
|
|
388
|
+
backend: [
|
|
389
|
+
'https://api.example.com',
|
|
390
|
+
{ address: 'https://api.address.com' },
|
|
391
|
+
{ remote: 'https://api.remote.com' }
|
|
392
|
+
]
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
mockGetContext.mockResolvedValue(mockContext);
|
|
398
|
+
const requiredPermissions = {
|
|
399
|
+
external: {
|
|
400
|
+
fetch: {
|
|
401
|
+
backend: ['https://api.example.com', 'https://api.address.com']
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const { result } = (0, react_hooks_1.renderHook)(() => (0, usePermissions_1.usePermissions)(requiredPermissions));
|
|
406
|
+
await (0, react_hooks_1.act)(async () => {
|
|
407
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
408
|
+
});
|
|
409
|
+
expect(result.current.isLoading).toBe(false);
|
|
410
|
+
expect(result.current.hasPermission).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* https://ecosystem-platform.atlassian.net/browse/DEPLOY-1411
|
|
3
|
+
* reuse logic from @forge/api
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Resource types that can be loaded externally
|
|
7
|
+
*/
|
|
8
|
+
declare const RESOURCE_TYPES: readonly ["fonts", "styles", "frames", "images", "media", "scripts"];
|
|
9
|
+
export declare type ResourceType = (typeof RESOURCE_TYPES)[number];
|
|
10
|
+
/**
|
|
11
|
+
* Fetch types for external requests
|
|
12
|
+
*/
|
|
13
|
+
declare const FETCH_TYPES: readonly ["backend", "client"];
|
|
14
|
+
export declare type FetchType = (typeof FETCH_TYPES)[number];
|
|
15
|
+
export interface Permissions {
|
|
16
|
+
scopes?: string[];
|
|
17
|
+
external?: {
|
|
18
|
+
fetch?: {
|
|
19
|
+
backend?: string[];
|
|
20
|
+
client?: string[];
|
|
21
|
+
};
|
|
22
|
+
fonts?: string[];
|
|
23
|
+
styles?: string[];
|
|
24
|
+
frames?: string[];
|
|
25
|
+
images?: string[];
|
|
26
|
+
media?: string[];
|
|
27
|
+
scripts?: string[];
|
|
28
|
+
};
|
|
29
|
+
content?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Required permissions for a component
|
|
33
|
+
*/
|
|
34
|
+
export declare type PermissionRequirements = Permissions;
|
|
35
|
+
/**
|
|
36
|
+
* Missing permissions information
|
|
37
|
+
*/
|
|
38
|
+
export declare type MissingPermissions = Permissions;
|
|
39
|
+
/**
|
|
40
|
+
* Permission check result
|
|
41
|
+
*/
|
|
42
|
+
export interface PermissionCheckResult {
|
|
43
|
+
granted: boolean;
|
|
44
|
+
missing: MissingPermissions | null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Hook for checking permissions in Forge apps
|
|
48
|
+
*
|
|
49
|
+
* @param requiredPermissions - The permissions required for the component
|
|
50
|
+
* @returns Object containing permission state, loading status, and error information
|
|
51
|
+
* @returns hasPermission - Whether all required permissions are granted
|
|
52
|
+
* @returns isLoading - Whether the permission check is still in progress
|
|
53
|
+
* @returns missingPermissions - Details about which permissions are missing (null if all granted)
|
|
54
|
+
* @returns error - Any error that occurred during permission checking (null if no error)
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* const MyComponent: React.FC = () => {
|
|
59
|
+
* const { hasPermission, isLoading, missingPermissions, error } = usePermissions({
|
|
60
|
+
* scopes: ['write:confluence-content'],
|
|
61
|
+
* external: {
|
|
62
|
+
* fetch: {
|
|
63
|
+
* backend: ['https://api.example.com']
|
|
64
|
+
* }
|
|
65
|
+
* }
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* if (isLoading) return <LoadingSpinner />;
|
|
69
|
+
*
|
|
70
|
+
* if (error) {
|
|
71
|
+
* return <ErrorMessage error={error} />;
|
|
72
|
+
* }
|
|
73
|
+
*
|
|
74
|
+
* if (!hasPermission) {
|
|
75
|
+
* return <PermissionDenied missingPermissions={missingPermissions} />;
|
|
76
|
+
* }
|
|
77
|
+
*
|
|
78
|
+
* return <ProtectedFeature />;
|
|
79
|
+
* };
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare const usePermissions: (requiredPermissions: PermissionRequirements) => {
|
|
83
|
+
hasPermission: boolean;
|
|
84
|
+
isLoading: boolean;
|
|
85
|
+
missingPermissions: Permissions | null;
|
|
86
|
+
error: Error | null;
|
|
87
|
+
};
|
|
88
|
+
export {};
|
|
89
|
+
//# sourceMappingURL=usePermissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePermissions.d.ts","sourceRoot":"","sources":["../../src/hooks/usePermissions.ts"],"names":[],"mappings":"AAIA;;;GAGG;AAEH;;GAEG;AACH,QAAA,MAAM,cAAc,sEAAuE,CAAC;AAC5F,oBAAY,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D;;GAEG;AACH,QAAA,MAAM,WAAW,gCAAiC,CAAC;AACnD,oBAAY,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE;YACN,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;YACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;SACnB,CAAC;QACF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;IACF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,oBAAY,sBAAsB,GAAG,WAAW,CAAC;AAEjD;;GAEG;AACH,oBAAY,kBAAkB,GAAG,WAAW,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACpC;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,cAAc,wBAAyB,sBAAsB;;;;;CA2JzE,CAAC"}
|