@forge/react 11.4.1-next.0 → 11.5.0-next.1-experimental-75d036a
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 +27 -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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.usePermissions = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const bridge_1 = require("@forge/bridge");
|
|
6
|
+
const minimatch_1 = require("minimatch");
|
|
7
|
+
/**
|
|
8
|
+
* https://ecosystem-platform.atlassian.net/browse/DEPLOY-1411
|
|
9
|
+
* reuse logic from @forge/api
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Resource types that can be loaded externally
|
|
13
|
+
*/
|
|
14
|
+
const RESOURCE_TYPES = ['fonts', 'styles', 'frames', 'images', 'media', 'scripts'];
|
|
15
|
+
/**
|
|
16
|
+
* Fetch types for external requests
|
|
17
|
+
*/
|
|
18
|
+
const FETCH_TYPES = ['backend', 'client'];
|
|
19
|
+
/**
|
|
20
|
+
* Helper function to check if a URL matches any of the allowed patterns
|
|
21
|
+
* Uses minimatch for robust pattern matching with wildcards
|
|
22
|
+
*/
|
|
23
|
+
const matchesAllowedUrl = (url, allowedUrls) => {
|
|
24
|
+
return allowedUrls.some((allowedUrl) => {
|
|
25
|
+
// Use minimatch for pattern matching
|
|
26
|
+
return (0, minimatch_1.minimatch)(url, allowedUrl);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Hook for checking permissions in Forge apps
|
|
31
|
+
*
|
|
32
|
+
* @param requiredPermissions - The permissions required for the component
|
|
33
|
+
* @returns Object containing permission state, loading status, and error information
|
|
34
|
+
* @returns hasPermission - Whether all required permissions are granted
|
|
35
|
+
* @returns isLoading - Whether the permission check is still in progress
|
|
36
|
+
* @returns missingPermissions - Details about which permissions are missing (null if all granted)
|
|
37
|
+
* @returns error - Any error that occurred during permission checking (null if no error)
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const MyComponent: React.FC = () => {
|
|
42
|
+
* const { hasPermission, isLoading, missingPermissions, error } = usePermissions({
|
|
43
|
+
* scopes: ['write:confluence-content'],
|
|
44
|
+
* external: {
|
|
45
|
+
* fetch: {
|
|
46
|
+
* backend: ['https://api.example.com']
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* if (isLoading) return <LoadingSpinner />;
|
|
52
|
+
*
|
|
53
|
+
* if (error) {
|
|
54
|
+
* return <ErrorMessage error={error} />;
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* if (!hasPermission) {
|
|
58
|
+
* return <PermissionDenied missingPermissions={missingPermissions} />;
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* return <ProtectedFeature />;
|
|
62
|
+
* };
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
const usePermissions = (requiredPermissions) => {
|
|
66
|
+
const [context, setContext] = (0, react_1.useState)();
|
|
67
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
|
|
68
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
69
|
+
// Load context on mount
|
|
70
|
+
(0, react_1.useEffect)(() => {
|
|
71
|
+
const loadContext = async () => {
|
|
72
|
+
try {
|
|
73
|
+
setIsLoading(true);
|
|
74
|
+
setError(null);
|
|
75
|
+
const contextData = await bridge_1.view.getContext();
|
|
76
|
+
setContext(contextData);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
setError(err instanceof Error ? err : new Error('Failed to load context'));
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
setIsLoading(false);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
void loadContext();
|
|
86
|
+
}, []);
|
|
87
|
+
// Permission checking utilities
|
|
88
|
+
const permissionUtils = (0, react_1.useMemo)(() => {
|
|
89
|
+
if (!context?.permissions)
|
|
90
|
+
return null;
|
|
91
|
+
const { scopes, external = {} } = context.permissions;
|
|
92
|
+
const scopeArray = Array.isArray(scopes) ? scopes : Object.keys(scopes || {});
|
|
93
|
+
return {
|
|
94
|
+
hasScope: (scope) => scopeArray.includes(scope),
|
|
95
|
+
canFetchFrom: (type, url) => {
|
|
96
|
+
const fetchUrls = external.fetch?.[type];
|
|
97
|
+
if (!fetchUrls?.length)
|
|
98
|
+
return false;
|
|
99
|
+
// Extract string URLs from fetch URLs array
|
|
100
|
+
const allowedUrls = fetchUrls
|
|
101
|
+
.map((item) => {
|
|
102
|
+
// If item is already a string, use it directly
|
|
103
|
+
if (typeof item === 'string') {
|
|
104
|
+
return item;
|
|
105
|
+
}
|
|
106
|
+
// If item has an address property, use that
|
|
107
|
+
if ('address' in item && item.address) {
|
|
108
|
+
return item.address;
|
|
109
|
+
}
|
|
110
|
+
// Otherwise, use the remote property (if it exists)
|
|
111
|
+
return item.remote;
|
|
112
|
+
})
|
|
113
|
+
.filter((url) => typeof url === 'string');
|
|
114
|
+
return matchesAllowedUrl(url, allowedUrls);
|
|
115
|
+
},
|
|
116
|
+
canLoadResource: (type, url) => {
|
|
117
|
+
const resourceUrls = external[type];
|
|
118
|
+
if (!resourceUrls?.length)
|
|
119
|
+
return false;
|
|
120
|
+
const stringUrls = resourceUrls.filter((item) => typeof item === 'string');
|
|
121
|
+
return matchesAllowedUrl(url, stringUrls);
|
|
122
|
+
},
|
|
123
|
+
getScopes: () => scopeArray,
|
|
124
|
+
getExternalPermissions: () => external,
|
|
125
|
+
hasAnyPermissions: () => scopeArray.length > 0 || Object.keys(external).length > 0
|
|
126
|
+
};
|
|
127
|
+
}, [context?.permissions]);
|
|
128
|
+
// Check permissions
|
|
129
|
+
const permissionResult = (0, react_1.useMemo)(() => {
|
|
130
|
+
if (!requiredPermissions) {
|
|
131
|
+
return { granted: false, missing: null };
|
|
132
|
+
}
|
|
133
|
+
if (!permissionUtils) {
|
|
134
|
+
// If still loading or there's an error, return null for missing permissions
|
|
135
|
+
if (isLoading || error) {
|
|
136
|
+
return { granted: false, missing: null };
|
|
137
|
+
}
|
|
138
|
+
throw new Error('This feature is not available yet');
|
|
139
|
+
}
|
|
140
|
+
const missing = {};
|
|
141
|
+
let hasAllRequiredPermissions = true;
|
|
142
|
+
// Check scopes
|
|
143
|
+
if (requiredPermissions.scopes?.length) {
|
|
144
|
+
const missingScopes = requiredPermissions.scopes.filter((scope) => !permissionUtils.hasScope(scope));
|
|
145
|
+
if (missingScopes.length > 0) {
|
|
146
|
+
missing.scopes = missingScopes;
|
|
147
|
+
hasAllRequiredPermissions = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Check external permissions
|
|
151
|
+
if (requiredPermissions.external) {
|
|
152
|
+
const missingExternal = {};
|
|
153
|
+
// Check fetch permissions
|
|
154
|
+
if (requiredPermissions.external.fetch) {
|
|
155
|
+
const missingFetch = {};
|
|
156
|
+
FETCH_TYPES.forEach((type) => {
|
|
157
|
+
const requiredUrls = requiredPermissions.external?.fetch?.[type];
|
|
158
|
+
if (requiredUrls?.length) {
|
|
159
|
+
const missingUrls = requiredUrls.filter((url) => !permissionUtils.canFetchFrom(type, url));
|
|
160
|
+
if (missingUrls.length > 0) {
|
|
161
|
+
missingFetch[type] = missingUrls;
|
|
162
|
+
hasAllRequiredPermissions = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
if (Object.keys(missingFetch).length > 0) {
|
|
167
|
+
missingExternal.fetch = missingFetch;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Check resource permissions
|
|
171
|
+
RESOURCE_TYPES.forEach((type) => {
|
|
172
|
+
const requiredUrls = requiredPermissions.external?.[type];
|
|
173
|
+
if (requiredUrls?.length) {
|
|
174
|
+
const missingUrls = requiredUrls.filter((url) => !permissionUtils.canLoadResource(type, url));
|
|
175
|
+
if (missingUrls.length > 0) {
|
|
176
|
+
missingExternal[type] = missingUrls;
|
|
177
|
+
hasAllRequiredPermissions = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
if (Object.keys(missingExternal).length > 0) {
|
|
182
|
+
missing.external = missingExternal;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Note: Content permissions are not supported in the current RuntimePermissions type
|
|
186
|
+
return {
|
|
187
|
+
granted: hasAllRequiredPermissions,
|
|
188
|
+
missing: hasAllRequiredPermissions ? null : missing
|
|
189
|
+
};
|
|
190
|
+
}, [permissionUtils, requiredPermissions]);
|
|
191
|
+
return {
|
|
192
|
+
hasPermission: permissionResult.granted,
|
|
193
|
+
isLoading,
|
|
194
|
+
missingPermissions: permissionResult.missing,
|
|
195
|
+
error
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
exports.usePermissions = usePermissions;
|
package/out/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { useProductContext } from './hooks/useProductContext';
|
|
2
2
|
export { useConfig } from './hooks/useConfig';
|
|
3
|
+
export { usePermissions, type Permissions, type PermissionRequirements, type MissingPermissions, type PermissionCheckResult } from './hooks/usePermissions';
|
|
3
4
|
export { ForgeReconciler as default } from './reconciler';
|
|
4
5
|
export * from './components';
|
|
5
6
|
export { useContentProperty } from './hooks/useContentProperty';
|
package/out/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EACL,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,eAAe,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAE1D,cAAc,cAAc,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAE,+BAA+B,EAAE,MAAM,oDAAoD,CAAC;AACrG,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,cAAc,iBAAiB,CAAC"}
|
package/out/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useForm = exports.replaceUnsupportedDocumentNodes = exports.I18nProvider = exports.useTranslation = exports.useIssueProperty = exports.useSpaceProperty = exports.useContentProperty = exports.default = exports.useConfig = exports.useProductContext = void 0;
|
|
3
|
+
exports.useForm = exports.replaceUnsupportedDocumentNodes = exports.I18nProvider = exports.useTranslation = exports.useIssueProperty = exports.useSpaceProperty = exports.useContentProperty = exports.default = exports.usePermissions = exports.useConfig = exports.useProductContext = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
var useProductContext_1 = require("./hooks/useProductContext");
|
|
6
6
|
Object.defineProperty(exports, "useProductContext", { enumerable: true, get: function () { return useProductContext_1.useProductContext; } });
|
|
7
7
|
var useConfig_1 = require("./hooks/useConfig");
|
|
8
8
|
Object.defineProperty(exports, "useConfig", { enumerable: true, get: function () { return useConfig_1.useConfig; } });
|
|
9
|
+
var usePermissions_1 = require("./hooks/usePermissions");
|
|
10
|
+
Object.defineProperty(exports, "usePermissions", { enumerable: true, get: function () { return usePermissions_1.usePermissions; } });
|
|
9
11
|
var reconciler_1 = require("./reconciler");
|
|
10
12
|
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return reconciler_1.ForgeReconciler; } });
|
|
11
13
|
tslib_1.__exportStar(require("./components"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forge/react",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.5.0-next.1-experimental-75d036a",
|
|
4
4
|
"description": "Forge React reconciler",
|
|
5
5
|
"author": "Atlassian",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
"@atlaskit/adf-schema": "^48.0.0",
|
|
29
29
|
"@atlaskit/adf-utils": "^19.19.0",
|
|
30
30
|
"@atlaskit/forge-react-types": "^0.42.10",
|
|
31
|
-
"@forge/bridge": "^5.6.0-next.
|
|
31
|
+
"@forge/bridge": "^5.6.0-next.4-experimental-75d036a",
|
|
32
32
|
"@forge/i18n": "0.0.7",
|
|
33
33
|
"@types/react": "^18.2.64",
|
|
34
34
|
"@types/react-reconciler": "^0.28.8",
|
|
35
35
|
"lodash": "^4.17.21",
|
|
36
|
+
"minimatch": "^9.0.5",
|
|
36
37
|
"react": "^18.2.0",
|
|
37
38
|
"react-hook-form": "^7.50.1",
|
|
38
39
|
"react-reconciler": "^0.29.0",
|