@reekon-tools/boldr-utils 1.20.2 → 1.21.0
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/annotation/canvas/AnnotationCanvasInner.d.ts +1 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.d.ts +1 -0
- package/dist/annotation/canvas/AnnotationCanvasInner.native.js +36 -1
- package/dist/exports.d.ts +1 -0
- package/dist/exports.js +3 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/dist/types/firestore.d.ts +64 -1
- package/dist/types/firestore.js +31 -0
- package/dist/utils/permissions.d.ts +63 -0
- package/dist/utils/permissions.js +131 -0
- package/package.json +1 -1
- package/dist/canvas/AnnotationCanvas.d.ts +0 -11
- package/dist/canvas/AnnotationCanvas.js +0 -10
- package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
- package/dist/canvas/AnnotationCanvas.native.js +0 -6
- package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
- package/dist/canvas/AnnotationCanvasInner.js +0 -219
- package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
- package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
- package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
- package/dist/canvas/AnnotationCanvasSkia.js +0 -20
- package/dist/canvas/Tool.d.ts +0 -38
- package/dist/canvas/Tool.js +0 -1
- package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
- package/dist/canvas/elements/BackgroundImageElement.js +0 -37
- package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
- package/dist/canvas/elements/MeasurementStampElement.js +0 -30
- package/dist/canvas/elements/ShapeElement.d.ts +0 -7
- package/dist/canvas/elements/ShapeElement.js +0 -62
- package/dist/canvas/elements/StrokeElement.d.ts +0 -7
- package/dist/canvas/elements/StrokeElement.js +0 -18
- package/dist/canvas/measurementPicker.d.ts +0 -10
- package/dist/canvas/measurementPicker.js +0 -1
- package/dist/canvas/measurementStampOverlay.d.ts +0 -11
- package/dist/canvas/measurementStampOverlay.js +0 -1
- package/dist/canvas/pointerAdapter.d.ts +0 -3
- package/dist/canvas/pointerAdapter.js +0 -19
- package/dist/canvas/stampLayout.d.ts +0 -5
- package/dist/canvas/stampLayout.js +0 -14
- package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
- package/dist/canvas/tools/measurementStampTool.js +0 -37
- package/dist/canvas/tools/panTool.d.ts +0 -5
- package/dist/canvas/tools/panTool.js +0 -25
- package/dist/canvas/tools/penTool.d.ts +0 -13
- package/dist/canvas/tools/penTool.js +0 -68
- package/dist/canvas/tools/selectTool.d.ts +0 -2
- package/dist/canvas/tools/selectTool.js +0 -182
- package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
- package/dist/canvas/useAnnotationCanvasState.js +0 -210
- package/dist/canvas/viewport.d.ts +0 -16
- package/dist/canvas/viewport.js +0 -54
- package/dist/data/AnnotationDataContext.d.ts +0 -8
- package/dist/data/AnnotationDataContext.js +0 -11
- package/dist/data/AnnotationDataProvider.d.ts +0 -65
- package/dist/data/AnnotationDataProvider.js +0 -4
- package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
- package/dist/data/InMemoryAnnotationProvider.js +0 -197
- package/dist/data/canvasPersistence.d.ts +0 -3
- package/dist/data/canvasPersistence.js +0 -26
- package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
- package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
- package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
- package/dist/data/hooks/useAnnotationDoc.js +0 -33
- package/dist/data/hooks/useAnnotationList.d.ts +0 -7
- package/dist/data/hooks/useAnnotationList.js +0 -26
- package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
- package/dist/data/hooks/useAnnotationMutations.js +0 -11
- package/dist/hooks/useParseMeasurement.d.ts +0 -4
- package/dist/hooks/useParseMeasurement.js +0 -14
- package/dist/utils/evaluateFormula.d.ts +0 -20
- package/dist/utils/evaluateFormula.js +0 -31
|
@@ -41,6 +41,7 @@ export interface AnnotationCanvasInnerProps {
|
|
|
41
41
|
initialViewport?: ViewportState;
|
|
42
42
|
style?: CSSProperties;
|
|
43
43
|
magnifierTopOffset?: number;
|
|
44
|
+
onInteractingChange?: (interacting: boolean) => void;
|
|
44
45
|
imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
|
|
45
46
|
snapshotRef?: MutableRefObject<(() => Uint8Array | null) | null>;
|
|
46
47
|
}
|
|
@@ -42,6 +42,7 @@ export interface AnnotationCanvasInnerProps {
|
|
|
42
42
|
initialViewport?: ViewportState;
|
|
43
43
|
style?: ViewStyle;
|
|
44
44
|
magnifierTopOffset?: number;
|
|
45
|
+
onInteractingChange?: (interacting: boolean) => void;
|
|
45
46
|
imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
|
|
46
47
|
snapshotRef?: MutableRefObject<(() => Uint8Array | null) | null>;
|
|
47
48
|
}
|
|
@@ -65,7 +65,7 @@ const LOUPE_CROSS_WIDTH_PX = 1.5;
|
|
|
65
65
|
// is only re-synced once each gesture ends. The tool/tap path still hops to
|
|
66
66
|
// JS (`runOnJS(true)`) since drawing/selection are React-state operations.
|
|
67
67
|
export const AnnotationCanvasInner = (props) => {
|
|
68
|
-
const { resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, snapToGeometry = false, renderBackground, } = props;
|
|
68
|
+
const { resolveImageUrl, deferBackgroundToResolver, hideLinkBadges, stampFontSource, stampValueFontSize = 14, width, height, style, magnifierTopOffset = 0, onInteractingChange, snapToGeometry = false, renderBackground, } = props;
|
|
69
69
|
const valueFont = useFont(stampFontSource, stampValueFontSize);
|
|
70
70
|
// The animated-viewport implementation needs the Reanimated shared values,
|
|
71
71
|
// which are created below AFTER the state hook — so the hook gets a stable
|
|
@@ -159,6 +159,13 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
159
159
|
const magTouchX = useSharedValue(0);
|
|
160
160
|
const magTouchY = useSharedValue(0);
|
|
161
161
|
const [magnifying, setMagnifying] = useState(false);
|
|
162
|
+
// Viewport pan/zoom in motion. Set from the viewport pan/pinch onChange
|
|
163
|
+
// worklets on the first frame that actually moves the viewport (never their
|
|
164
|
+
// onBegin, which fires for plain taps too) and cleared when the LAST
|
|
165
|
+
// viewport gesture finalizes — the two run simultaneously during a
|
|
166
|
+
// two-finger gesture, hence the endViewportGesture ref-count check rather
|
|
167
|
+
// than clearing per-gesture.
|
|
168
|
+
const [viewportMoving, setViewportMoving] = useState(false);
|
|
162
169
|
// Lens geometry (screen points): a fixed square in the top-left, pushed below
|
|
163
170
|
// any overlaid toolbar by magnifierTopOffset. Its CENTER maps to the live
|
|
164
171
|
// touch point, so the magnified slice is the canvas around the finger.
|
|
@@ -474,6 +481,22 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
474
481
|
// per-frame JS. The Skia line stays static (draggingId is NOT set), only the
|
|
475
482
|
// tile moves.
|
|
476
483
|
const [slidingId, setSlidingId] = useState(null);
|
|
484
|
+
// Consumer notification (onInteractingChange): `magnifying` covers every
|
|
485
|
+
// precision gesture (set on actual movement, cleared on every finalize
|
|
486
|
+
// path); whole-element moves and tile slides deliberately DON'T show the
|
|
487
|
+
// loupe, so their gating ids are OR'd in here — as is viewport motion.
|
|
488
|
+
// (The other sub-drag ids — endpoint/corner/crop — all set `magnifying`
|
|
489
|
+
// alongside their own id, so they're already covered.) Ref-guarded so a
|
|
490
|
+
// consumer that re-renders us doesn't get duplicate notifications, and so
|
|
491
|
+
// the mount render doesn't fire a spurious `false`.
|
|
492
|
+
const interacting = magnifying || viewportMoving || draggingId != null || slidingId != null;
|
|
493
|
+
const notifiedInteractingRef = useRef(false);
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
if (notifiedInteractingRef.current === interacting)
|
|
496
|
+
return;
|
|
497
|
+
notifiedInteractingRef.current = interacting;
|
|
498
|
+
onInteractingChange?.(interacting);
|
|
499
|
+
}, [interacting, onInteractingChange]);
|
|
477
500
|
const slideCtx = useSharedValue({ ax: 0, ay: 0, bx: 0, by: 0, t0: 0.5 });
|
|
478
501
|
const slideTargetRef = useRef(null);
|
|
479
502
|
// Endpoint drag (resize/rotate a measurement annotation's line). `epDragId`
|
|
@@ -1139,6 +1162,10 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1139
1162
|
};
|
|
1140
1163
|
const endViewportGesture = () => {
|
|
1141
1164
|
activeViewportGestures.current = Math.max(0, activeViewportGestures.current - 1);
|
|
1165
|
+
// Simultaneous pan+pinch: only the LAST finalize ends "viewport moving"
|
|
1166
|
+
// (a no-op setState when it never started — onChange never fired).
|
|
1167
|
+
if (activeViewportGestures.current === 0)
|
|
1168
|
+
setViewportMoving(false);
|
|
1142
1169
|
};
|
|
1143
1170
|
const toolPan = Gesture.Pan()
|
|
1144
1171
|
.minPointers(1)
|
|
@@ -1274,6 +1301,10 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1274
1301
|
'worklet';
|
|
1275
1302
|
panX.value -= e.changeX / zoom.value;
|
|
1276
1303
|
panY.value -= e.changeY / zoom.value;
|
|
1304
|
+
// First frame of actual movement flips "viewport moving" (gated on
|
|
1305
|
+
// the dirty bit so it's one JS hop per gesture, not per frame).
|
|
1306
|
+
if (!viewportDirty.value)
|
|
1307
|
+
runOnJS(setViewportMoving)(true);
|
|
1277
1308
|
viewportDirty.value = true;
|
|
1278
1309
|
})
|
|
1279
1310
|
// onFinalize, not onEnd: on Android this pan normally exits through
|
|
@@ -1344,6 +1375,10 @@ export const AnnotationCanvasInner = (props) => {
|
|
|
1344
1375
|
panX.value = focalWorldX - e.focalX / next;
|
|
1345
1376
|
panY.value = focalWorldY - e.focalY / next;
|
|
1346
1377
|
zoom.value = next;
|
|
1378
|
+
// Same first-movement flip as the pan (see there) — the early returns
|
|
1379
|
+
// above keep a no-op pinch from ever firing it.
|
|
1380
|
+
if (!viewportDirty.value)
|
|
1381
|
+
runOnJS(setViewportMoving)(true);
|
|
1347
1382
|
viewportDirty.value = true;
|
|
1348
1383
|
})
|
|
1349
1384
|
// onFinalize, not onEnd, for the same reason as the pan: an Android
|
package/dist/exports.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './types/annotation.js';
|
|
|
9
9
|
export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, type ToleranceThreshold, type ToleranceConfig, } from './utils/tolerance.js';
|
|
10
10
|
export { GeneralColors, SecondaryColors, FormColors, TileColors, FormulaColors, YellowColors, RedColors, GreenColors, tailwindColors, } from './theme/colors.js';
|
|
11
11
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
|
|
12
|
+
export { ROLE_RANK, EMPTY_GRANTS, normalizeRole, roleAtLeast, effectiveRole, canView, canEdit, canManage, canManageOrg, isGuest, groupGrants, resolveAccess, type MaybeRole, type GroupedGrants, type ResolvedAccess, } from './utils/permissions.js';
|
|
12
13
|
export { ALL_EXPORT_CATEGORIES, ALL_EXPORT_MEASUREMENT_DATA_FIELDS, ALL_EXPORT_OBJECT_TYPES, DEFAULT_EXPORT_CONFIG, EXPORT_EVERYTHING_CONFIG, normalizeExportConfig, type ExportCategory, type ExportConfig, type ExportImageFormat, type ExportMeasurementDataField, type ExportMeasurementFilter, type ExportObjectType, } from './export/config.js';
|
|
13
14
|
export { classifyFileForExport } from './export/objectKinds.js';
|
|
14
15
|
export { buildJobCsv, escapeCSVField, hyperlinkCell, type BuildJobCsvInput, type ExportObjectRow, type ExportScope, } from './export/buildJobCsv.js';
|
package/dist/exports.js
CHANGED
|
@@ -14,6 +14,9 @@ export { getToleranceColor, calculateDeviationPercentage, isWithinTolerance, gen
|
|
|
14
14
|
// Shared color palette (canonical source for both apps' brand colors).
|
|
15
15
|
export { GeneralColors, SecondaryColors, FormColors, TileColors, FormulaColors, YellowColors, RedColors, GreenColors, tailwindColors, } from './theme/colors.js';
|
|
16
16
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
|
|
17
|
+
// Three-level access (organization → project → job) role arithmetic. Keep in
|
|
18
|
+
// step with rock-pro-backend/firestore.rules.
|
|
19
|
+
export { ROLE_RANK, EMPTY_GRANTS, normalizeRole, roleAtLeast, effectiveRole, canView, canEdit, canManage, canManageOrg, isGuest, groupGrants, resolveAccess, } from './utils/permissions.js';
|
|
17
20
|
// Configurable job CSV export, shared by rock-pro-app and rock-desktop so
|
|
18
21
|
// both platforms emit identical files.
|
|
19
22
|
export { ALL_EXPORT_CATEGORIES, ALL_EXPORT_MEASUREMENT_DATA_FIELDS, ALL_EXPORT_OBJECT_TYPES, DEFAULT_EXPORT_CONFIG, EXPORT_EVERYTHING_CONFIG, normalizeExportConfig, } from './export/config.js';
|
package/dist/server.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './types/layout.js';
|
|
|
7
7
|
export * from './types/annotation.js';
|
|
8
8
|
export { getToleranceColor, getToleranceSecondaryColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, DEFAULT_TOLERANCE_SECONDARY_COLORS, type ToleranceThreshold, type ToleranceConfig, } from './utils/tolerance.js';
|
|
9
9
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
|
|
10
|
+
export { ROLE_RANK, EMPTY_GRANTS, normalizeRole, roleAtLeast, effectiveRole, canView, canEdit, canManage, canManageOrg, isGuest, groupGrants, resolveAccess, type MaybeRole, type GroupedGrants, type ResolvedAccess, } from './utils/permissions.js';
|
|
10
11
|
export { ALL_EXPORT_CATEGORIES, ALL_EXPORT_MEASUREMENT_DATA_FIELDS, ALL_EXPORT_OBJECT_TYPES, DEFAULT_EXPORT_CONFIG, EXPORT_EVERYTHING_CONFIG, normalizeExportConfig, type ExportCategory, type ExportConfig, type ExportImageFormat, type ExportMeasurementDataField, type ExportMeasurementFilter, type ExportObjectType, } from './export/config.js';
|
|
11
12
|
export { classifyFileForExport } from './export/objectKinds.js';
|
|
12
13
|
export { buildJobCsv, escapeCSVField, hyperlinkCell, type BuildJobCsvInput, type ExportObjectRow, type ExportScope, } from './export/buildJobCsv.js';
|
package/dist/server.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from './types/layout.js';
|
|
|
12
12
|
export * from './types/annotation.js';
|
|
13
13
|
export { getToleranceColor, getToleranceSecondaryColor, calculateDeviationPercentage, isWithinTolerance, generateToleranceGradient, createDefaultToleranceThresholds, DEFAULT_TOLERANCE_COLORS, DEFAULT_TOLERANCE_SECONDARY_COLORS, } from './utils/tolerance.js';
|
|
14
14
|
export { DEFAULT_GROUP_INDEX, isDefaultGroup, findDefaultGroup, isFormGroupCompleted, } from './utils/groups.js';
|
|
15
|
+
export { ROLE_RANK, EMPTY_GRANTS, normalizeRole, roleAtLeast, effectiveRole, canView, canEdit, canManage, canManageOrg, isGuest, groupGrants, resolveAccess, } from './utils/permissions.js';
|
|
15
16
|
export { ALL_EXPORT_CATEGORIES, ALL_EXPORT_MEASUREMENT_DATA_FIELDS, ALL_EXPORT_OBJECT_TYPES, DEFAULT_EXPORT_CONFIG, EXPORT_EVERYTHING_CONFIG, normalizeExportConfig, } from './export/config.js';
|
|
16
17
|
export { classifyFileForExport } from './export/objectKinds.js';
|
|
17
18
|
export { buildJobCsv, escapeCSVField, hyperlinkCell, } from './export/buildJobCsv.js';
|
|
@@ -21,7 +21,60 @@ export declare enum OrganizationRole {
|
|
|
21
21
|
Admin = "admin",
|
|
22
22
|
Full = "member",
|
|
23
23
|
Contributor = "contributor",
|
|
24
|
-
Viewer = "viewer"
|
|
24
|
+
Viewer = "viewer",
|
|
25
|
+
/**
|
|
26
|
+
* Synthetic org membership for users invited to a single project or job.
|
|
27
|
+
* Grants read of the org doc and its supporting library (templates, labels,
|
|
28
|
+
* formulas, miter variants, print logos) and nothing under `projects/`.
|
|
29
|
+
* Server-written only; never offered in an org role picker. The same word
|
|
30
|
+
* is used for the project-level marker an `AccessGrant` carries when a user
|
|
31
|
+
* has job access but no project access — "may read this container's doc,
|
|
32
|
+
* nothing inside it".
|
|
33
|
+
*/
|
|
34
|
+
Guest = "guest"
|
|
35
|
+
}
|
|
36
|
+
/** Roles an org admin may assign in Team settings. Excludes Guest. */
|
|
37
|
+
export declare const ASSIGNABLE_ORG_ROLES: readonly OrganizationRole[];
|
|
38
|
+
/**
|
|
39
|
+
* Roles grantable on a single project or job. Same wire values as the org
|
|
40
|
+
* roles minus Admin (admin is org management, not data access).
|
|
41
|
+
*/
|
|
42
|
+
export type ScopedRole = OrganizationRole.Full | OrganizationRole.Contributor | OrganizationRole.Viewer;
|
|
43
|
+
export declare const SCOPED_ROLES: readonly ScopedRole[];
|
|
44
|
+
/**
|
|
45
|
+
* What the sharing UIs offer. Contributor is deliberately absent (Figma
|
|
46
|
+
* 1028-47443 / 746-37130 show Full and Viewer); add it here to enable it —
|
|
47
|
+
* rules and types already accept it.
|
|
48
|
+
*/
|
|
49
|
+
export declare const ACCESS_ROLE_OPTIONS: readonly {
|
|
50
|
+
label: string;
|
|
51
|
+
value: ScopedRole;
|
|
52
|
+
}[];
|
|
53
|
+
/** Role stored on an `AccessGrant`: a scoped role, or the project-level guest marker. */
|
|
54
|
+
export type AccessRole = ScopedRole | OrganizationRole.Guest;
|
|
55
|
+
export type AccessScope = 'project' | 'job';
|
|
56
|
+
/**
|
|
57
|
+
* A per-user grant on a project or a job. Lives at
|
|
58
|
+
* organizations/{orgId}/projects/{projectId}/access/{uid} (scope 'project')
|
|
59
|
+
* organizations/{orgId}/projects/{projectId}/jobs/{jobId}/access/{uid} (scope 'job')
|
|
60
|
+
* Doc id == uid. Written only by Cloud Functions. Discovered by the client
|
|
61
|
+
* with `collectionGroup('access').where('userId', '==', uid)`.
|
|
62
|
+
*/
|
|
63
|
+
export interface AccessGrant extends FirestoreDoc {
|
|
64
|
+
userId: string;
|
|
65
|
+
organizationId: string;
|
|
66
|
+
projectId: string;
|
|
67
|
+
jobId?: string;
|
|
68
|
+
scope: AccessScope;
|
|
69
|
+
role: AccessRole;
|
|
70
|
+
email: string;
|
|
71
|
+
firstName: string;
|
|
72
|
+
lastName: string;
|
|
73
|
+
grantedBy: {
|
|
74
|
+
userId: string;
|
|
75
|
+
email: string;
|
|
76
|
+
};
|
|
77
|
+
grantedAt: Date;
|
|
25
78
|
}
|
|
26
79
|
export declare enum InvitationStatus {
|
|
27
80
|
Pending = "pending",
|
|
@@ -49,6 +102,16 @@ export interface Invitation extends FirestoreDoc, Timestamps {
|
|
|
49
102
|
organizationId: string;
|
|
50
103
|
role: OrganizationRole;
|
|
51
104
|
status: InvitationStatus;
|
|
105
|
+
/**
|
|
106
|
+
* Present on project/job invites (`kind === 'access'`); absent on org
|
|
107
|
+
* invites. `projectName` / `jobName` are denormalized for email and UI
|
|
108
|
+
* copy only — never authoritative.
|
|
109
|
+
*/
|
|
110
|
+
kind?: 'organization' | 'access';
|
|
111
|
+
projectId?: string;
|
|
112
|
+
jobId?: string;
|
|
113
|
+
projectName?: string;
|
|
114
|
+
jobName?: string;
|
|
52
115
|
}
|
|
53
116
|
export interface OrganizationMember extends FirestoreDoc {
|
|
54
117
|
email: string;
|
package/dist/types/firestore.js
CHANGED
|
@@ -4,7 +4,38 @@ export var OrganizationRole;
|
|
|
4
4
|
OrganizationRole["Full"] = "member";
|
|
5
5
|
OrganizationRole["Contributor"] = "contributor";
|
|
6
6
|
OrganizationRole["Viewer"] = "viewer";
|
|
7
|
+
/**
|
|
8
|
+
* Synthetic org membership for users invited to a single project or job.
|
|
9
|
+
* Grants read of the org doc and its supporting library (templates, labels,
|
|
10
|
+
* formulas, miter variants, print logos) and nothing under `projects/`.
|
|
11
|
+
* Server-written only; never offered in an org role picker. The same word
|
|
12
|
+
* is used for the project-level marker an `AccessGrant` carries when a user
|
|
13
|
+
* has job access but no project access — "may read this container's doc,
|
|
14
|
+
* nothing inside it".
|
|
15
|
+
*/
|
|
16
|
+
OrganizationRole["Guest"] = "guest";
|
|
7
17
|
})(OrganizationRole || (OrganizationRole = {}));
|
|
18
|
+
/** Roles an org admin may assign in Team settings. Excludes Guest. */
|
|
19
|
+
export const ASSIGNABLE_ORG_ROLES = [
|
|
20
|
+
OrganizationRole.Admin,
|
|
21
|
+
OrganizationRole.Full,
|
|
22
|
+
OrganizationRole.Contributor,
|
|
23
|
+
OrganizationRole.Viewer,
|
|
24
|
+
];
|
|
25
|
+
export const SCOPED_ROLES = [
|
|
26
|
+
OrganizationRole.Full,
|
|
27
|
+
OrganizationRole.Contributor,
|
|
28
|
+
OrganizationRole.Viewer,
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* What the sharing UIs offer. Contributor is deliberately absent (Figma
|
|
32
|
+
* 1028-47443 / 746-37130 show Full and Viewer); add it here to enable it —
|
|
33
|
+
* rules and types already accept it.
|
|
34
|
+
*/
|
|
35
|
+
export const ACCESS_ROLE_OPTIONS = [
|
|
36
|
+
{ label: 'Full', value: OrganizationRole.Full },
|
|
37
|
+
{ label: 'Viewer', value: OrganizationRole.Viewer },
|
|
38
|
+
];
|
|
8
39
|
export var InvitationStatus;
|
|
9
40
|
(function (InvitationStatus) {
|
|
10
41
|
InvitationStatus["Pending"] = "pending";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { OrganizationRole, type AccessGrant, type ScopedRole } from '../types/firestore.js';
|
|
2
|
+
/** A role as it may arrive from Firestore: possibly legacy, possibly missing. */
|
|
3
|
+
export type MaybeRole = OrganizationRole | string | null | undefined;
|
|
4
|
+
export declare const ROLE_RANK: Readonly<Record<OrganizationRole, number>>;
|
|
5
|
+
/**
|
|
6
|
+
* Maps a stored role string to the enum. Founder member docs written by early
|
|
7
|
+
* `cloneDemoOrganization` carry the legacy value `'creator'`, which the Cloud
|
|
8
|
+
* Functions treat as admin; normalise it the same way. Unknown values → null.
|
|
9
|
+
*/
|
|
10
|
+
export declare function normalizeRole(role: MaybeRole): OrganizationRole | null;
|
|
11
|
+
export declare function roleAtLeast(role: MaybeRole, min: OrganizationRole): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Highest role wins across the levels that enclose a document. Pass only the
|
|
14
|
+
* levels that apply: `effectiveRole(org)` for org-level resources,
|
|
15
|
+
* `effectiveRole(org, project)` for a project, `effectiveRole(org, project,
|
|
16
|
+
* job)` for anything under a job. Guest at any level is ignored. Returns null
|
|
17
|
+
* when the user holds no data-bearing role.
|
|
18
|
+
*/
|
|
19
|
+
export declare function effectiveRole(orgRole: MaybeRole, projectRole?: MaybeRole, jobRole?: MaybeRole): OrganizationRole | null;
|
|
20
|
+
/** Viewer or better. */
|
|
21
|
+
export declare const canView: (role: MaybeRole) => boolean;
|
|
22
|
+
/** Contributor or better — may update existing documents. */
|
|
23
|
+
export declare const canEdit: (role: MaybeRole) => boolean;
|
|
24
|
+
/** Full or better — may create/delete children and manage sharing. */
|
|
25
|
+
export declare const canManage: (role: MaybeRole) => boolean;
|
|
26
|
+
/** Org admin — team management, org settings, deleting the org. */
|
|
27
|
+
export declare const canManageOrg: (orgRole: MaybeRole) => boolean;
|
|
28
|
+
export declare const isGuest: (orgRole: MaybeRole) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The grants a user holds inside ONE organization, indexed for O(1) lookup.
|
|
31
|
+
* `guestProjectIds` are projects the user may read the doc of (because they
|
|
32
|
+
* hold a job grant inside) but has no project-level role on.
|
|
33
|
+
*/
|
|
34
|
+
export interface GroupedGrants {
|
|
35
|
+
projectRoles: Readonly<Record<string, ScopedRole>>;
|
|
36
|
+
jobRolesByProject: Readonly<Record<string, Readonly<Record<string, ScopedRole>>>>;
|
|
37
|
+
guestProjectIds: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
export declare const EMPTY_GRANTS: GroupedGrants;
|
|
40
|
+
/**
|
|
41
|
+
* Groups the raw docs from `collectionGroup('access')` for one org. Grants for
|
|
42
|
+
* other orgs are ignored, so callers may pass the whole snapshot. A project
|
|
43
|
+
* grant with role `guest` is a marker, recorded in `guestProjectIds` only.
|
|
44
|
+
*/
|
|
45
|
+
export declare function groupGrants(grants: readonly Pick<AccessGrant, 'organizationId' | 'projectId' | 'jobId' | 'scope' | 'role'>[], orgId: string): GroupedGrants;
|
|
46
|
+
export interface ResolvedAccess {
|
|
47
|
+
/** Effective (highest-wins) role, or null when the user has no access. */
|
|
48
|
+
role: OrganizationRole | null;
|
|
49
|
+
canView: boolean;
|
|
50
|
+
canEdit: boolean;
|
|
51
|
+
canManage: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* True when the user may read the project DOCUMENT itself (org viewer+,
|
|
54
|
+
* a project role, or the guest marker). Distinct from `canView`, which
|
|
55
|
+
* covers the project's contents.
|
|
56
|
+
*/
|
|
57
|
+
canReadProjectDoc: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves what a user may do at a project or a job. Mirrors the rules:
|
|
61
|
+
* canViewProject/Job, canContributeProject/Job, canEditProject/Job.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveAccess(orgRole: MaybeRole, grants: GroupedGrants, projectId: string, jobId?: string | null): ResolvedAccess;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Role arithmetic shared by both clients and the backend. Pure: no React, no
|
|
2
|
+
// Firebase, no Skia — this module is re-exported from `server.ts`.
|
|
3
|
+
//
|
|
4
|
+
// Three levels of access exist (organization → project → job). A user's
|
|
5
|
+
// effective role on any document is the HIGHEST of the roles they hold at the
|
|
6
|
+
// levels enclosing it; lower-level grants are additive and never restrict.
|
|
7
|
+
// `Guest` (org role, or the project-level marker on an AccessGrant) ranks
|
|
8
|
+
// below Viewer and contributes nothing to the effective role — it exists so
|
|
9
|
+
// the container document itself is readable.
|
|
10
|
+
//
|
|
11
|
+
// The Firestore rules in rock-pro-backend/firestore.rules encode the same
|
|
12
|
+
// tiers; keep the two in step:
|
|
13
|
+
// view = Viewer+ (rules: canView*)
|
|
14
|
+
// edit = Contributor+ (rules: canContribute*)
|
|
15
|
+
// manage = Full+ (rules: canEdit* / canManage*)
|
|
16
|
+
import { OrganizationRole, } from '../types/firestore.js';
|
|
17
|
+
export const ROLE_RANK = {
|
|
18
|
+
[OrganizationRole.Guest]: 0,
|
|
19
|
+
[OrganizationRole.Viewer]: 1,
|
|
20
|
+
[OrganizationRole.Contributor]: 2,
|
|
21
|
+
[OrganizationRole.Full]: 3,
|
|
22
|
+
[OrganizationRole.Admin]: 4,
|
|
23
|
+
};
|
|
24
|
+
const KNOWN_ROLES = new Set(Object.values(OrganizationRole));
|
|
25
|
+
/**
|
|
26
|
+
* Maps a stored role string to the enum. Founder member docs written by early
|
|
27
|
+
* `cloneDemoOrganization` carry the legacy value `'creator'`, which the Cloud
|
|
28
|
+
* Functions treat as admin; normalise it the same way. Unknown values → null.
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeRole(role) {
|
|
31
|
+
if (role == null)
|
|
32
|
+
return null;
|
|
33
|
+
if (role === 'creator')
|
|
34
|
+
return OrganizationRole.Admin;
|
|
35
|
+
return KNOWN_ROLES.has(role) ? role : null;
|
|
36
|
+
}
|
|
37
|
+
export function roleAtLeast(role, min) {
|
|
38
|
+
const r = normalizeRole(role);
|
|
39
|
+
return r != null && ROLE_RANK[r] >= ROLE_RANK[min];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Highest role wins across the levels that enclose a document. Pass only the
|
|
43
|
+
* levels that apply: `effectiveRole(org)` for org-level resources,
|
|
44
|
+
* `effectiveRole(org, project)` for a project, `effectiveRole(org, project,
|
|
45
|
+
* job)` for anything under a job. Guest at any level is ignored. Returns null
|
|
46
|
+
* when the user holds no data-bearing role.
|
|
47
|
+
*/
|
|
48
|
+
export function effectiveRole(orgRole, projectRole, jobRole) {
|
|
49
|
+
let best = null;
|
|
50
|
+
for (const raw of [orgRole, projectRole, jobRole]) {
|
|
51
|
+
const r = normalizeRole(raw);
|
|
52
|
+
if (r == null || r === OrganizationRole.Guest)
|
|
53
|
+
continue;
|
|
54
|
+
if (best == null || ROLE_RANK[r] > ROLE_RANK[best])
|
|
55
|
+
best = r;
|
|
56
|
+
}
|
|
57
|
+
return best;
|
|
58
|
+
}
|
|
59
|
+
/** Viewer or better. */
|
|
60
|
+
export const canView = (role) => roleAtLeast(role, OrganizationRole.Viewer);
|
|
61
|
+
/** Contributor or better — may update existing documents. */
|
|
62
|
+
export const canEdit = (role) => roleAtLeast(role, OrganizationRole.Contributor);
|
|
63
|
+
/** Full or better — may create/delete children and manage sharing. */
|
|
64
|
+
export const canManage = (role) => roleAtLeast(role, OrganizationRole.Full);
|
|
65
|
+
/** Org admin — team management, org settings, deleting the org. */
|
|
66
|
+
export const canManageOrg = (orgRole) => roleAtLeast(orgRole, OrganizationRole.Admin);
|
|
67
|
+
export const isGuest = (orgRole) => normalizeRole(orgRole) === OrganizationRole.Guest;
|
|
68
|
+
export const EMPTY_GRANTS = Object.freeze({
|
|
69
|
+
projectRoles: Object.freeze({}),
|
|
70
|
+
jobRolesByProject: Object.freeze({}),
|
|
71
|
+
guestProjectIds: Object.freeze([]),
|
|
72
|
+
});
|
|
73
|
+
function isScopedRole(role) {
|
|
74
|
+
const r = normalizeRole(role);
|
|
75
|
+
return (r === OrganizationRole.Full ||
|
|
76
|
+
r === OrganizationRole.Contributor ||
|
|
77
|
+
r === OrganizationRole.Viewer);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Groups the raw docs from `collectionGroup('access')` for one org. Grants for
|
|
81
|
+
* other orgs are ignored, so callers may pass the whole snapshot. A project
|
|
82
|
+
* grant with role `guest` is a marker, recorded in `guestProjectIds` only.
|
|
83
|
+
*/
|
|
84
|
+
export function groupGrants(grants, orgId) {
|
|
85
|
+
var _a;
|
|
86
|
+
const projectRoles = {};
|
|
87
|
+
const jobRolesByProject = {};
|
|
88
|
+
const guest = new Set();
|
|
89
|
+
for (const g of grants) {
|
|
90
|
+
if (g.organizationId !== orgId)
|
|
91
|
+
continue;
|
|
92
|
+
if (g.scope === 'job') {
|
|
93
|
+
if (!g.jobId || !isScopedRole(g.role))
|
|
94
|
+
continue;
|
|
95
|
+
(jobRolesByProject[_a = g.projectId] ?? (jobRolesByProject[_a] = {}))[g.jobId] = g.role;
|
|
96
|
+
}
|
|
97
|
+
else if (g.scope === 'project') {
|
|
98
|
+
if (isScopedRole(g.role))
|
|
99
|
+
projectRoles[g.projectId] = g.role;
|
|
100
|
+
else if (normalizeRole(g.role) === OrganizationRole.Guest)
|
|
101
|
+
guest.add(g.projectId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// A real project role supersedes a stale marker.
|
|
105
|
+
for (const id of Object.keys(projectRoles))
|
|
106
|
+
guest.delete(id);
|
|
107
|
+
return {
|
|
108
|
+
projectRoles,
|
|
109
|
+
jobRolesByProject,
|
|
110
|
+
guestProjectIds: [...guest].sort(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolves what a user may do at a project or a job. Mirrors the rules:
|
|
115
|
+
* canViewProject/Job, canContributeProject/Job, canEditProject/Job.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveAccess(orgRole, grants, projectId, jobId) {
|
|
118
|
+
const projectRole = grants.projectRoles[projectId];
|
|
119
|
+
const jobRole = jobId
|
|
120
|
+
? grants.jobRolesByProject[projectId]?.[jobId]
|
|
121
|
+
: undefined;
|
|
122
|
+
const role = effectiveRole(orgRole, projectRole, jobId ? jobRole : undefined);
|
|
123
|
+
return {
|
|
124
|
+
role,
|
|
125
|
+
canView: canView(role),
|
|
126
|
+
canEdit: canEdit(role),
|
|
127
|
+
canManage: canManage(role),
|
|
128
|
+
canReadProjectDoc: canView(effectiveRole(orgRole, projectRole)) ||
|
|
129
|
+
grants.guestProjectIds.includes(projectId),
|
|
130
|
+
};
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.js';
|
|
3
|
-
export type { AnnotationCanvasHandle, GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
|
|
4
|
-
export interface CanvasKitOpts {
|
|
5
|
-
locateFile?: (file: string) => string;
|
|
6
|
-
}
|
|
7
|
-
export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
|
|
8
|
-
fallback?: ReactNode;
|
|
9
|
-
canvasKitOpts?: CanvasKitOpts;
|
|
10
|
-
};
|
|
11
|
-
export declare const AnnotationCanvas: ({ fallback, canvasKitOpts, ...rest }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { WithSkiaWeb } from '@shopify/react-native-skia/lib/module/web/index.js';
|
|
3
|
-
// Web-only entry point: lazy-loads CanvasKit wasm via WithSkiaWeb. Imperative
|
|
4
|
-
// API (undo/redo/zoom) is exposed through the `imperativeRef` prop rather
|
|
5
|
-
// than React refs, since WithSkiaWeb can't forward refs through its lazy
|
|
6
|
-
// component boundary. The native build sits in `AnnotationCanvas.native.tsx`
|
|
7
|
-
// and skips WithSkiaWeb entirely (no wasm to load).
|
|
8
|
-
export const AnnotationCanvas = ({ fallback, canvasKitOpts, ...rest }) => (_jsx(WithSkiaWeb, { getComponent: () => import('./AnnotationCanvasInner.js').then((m) => ({
|
|
9
|
-
default: m.AnnotationCanvasInner,
|
|
10
|
-
})), fallback: fallback ?? null, componentProps: rest, opts: canvasKitOpts }));
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.native.js';
|
|
2
|
-
export type { AnnotationCanvasHandle, } from './useAnnotationCanvasState.js';
|
|
3
|
-
export type { GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
|
|
4
|
-
export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
|
|
5
|
-
fallback?: unknown;
|
|
6
|
-
canvasKitOpts?: unknown;
|
|
7
|
-
};
|
|
8
|
-
export declare const AnnotationCanvas: (props: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { AnnotationCanvasInner } from './AnnotationCanvasInner.native.js';
|
|
3
|
-
export const AnnotationCanvas = (props) => {
|
|
4
|
-
const { fallback: _f, canvasKitOpts: _o, ...rest } = props;
|
|
5
|
-
return _jsx(AnnotationCanvasInner, { ...rest });
|
|
6
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type CSSProperties, type MutableRefObject } from 'react';
|
|
2
|
-
import type { DecimalTolerance, FractionalTolerance, Measurement, Units } from '../types/firestore.js';
|
|
3
|
-
import type { AnnotationCanvasState, AnnotationDocumentPatch, Selection } from '../types/annotation.js';
|
|
4
|
-
import type { MeasurementRef } from './measurementPicker.js';
|
|
5
|
-
import type { Tool } from './Tool.js';
|
|
6
|
-
import { type AnnotationCanvasHandle } from './useAnnotationCanvasState.js';
|
|
7
|
-
import { type ViewportState } from './viewport.js';
|
|
8
|
-
import type { RenderMeasurementStamp } from './measurementStampOverlay.js';
|
|
9
|
-
export type { AnnotationCanvasHandle };
|
|
10
|
-
export interface AnnotationCanvasInnerProps {
|
|
11
|
-
canvas: AnnotationCanvasState;
|
|
12
|
-
onCommit(patch: AnnotationDocumentPatch): void;
|
|
13
|
-
tools: Tool[];
|
|
14
|
-
activeToolId: string;
|
|
15
|
-
selection: Selection | null;
|
|
16
|
-
onSelectionChange(selection: Selection | null): void;
|
|
17
|
-
measurements?: Measurement[];
|
|
18
|
-
fallbackUnit?: Units;
|
|
19
|
-
fractionalTolerance?: FractionalTolerance;
|
|
20
|
-
decimalTolerance?: DecimalTolerance;
|
|
21
|
-
resolveImageUrl?: (storagePath: string) => Promise<string>;
|
|
22
|
-
pickMeasurement?: () => Promise<MeasurementRef | null>;
|
|
23
|
-
renderMeasurementStamp?: RenderMeasurementStamp;
|
|
24
|
-
stampFontSource?: unknown;
|
|
25
|
-
stampValueFontSize?: number;
|
|
26
|
-
stampLabelFontSize?: number;
|
|
27
|
-
gestures?: GestureConfig;
|
|
28
|
-
width: number;
|
|
29
|
-
height: number;
|
|
30
|
-
initialViewport?: ViewportState;
|
|
31
|
-
style?: CSSProperties;
|
|
32
|
-
imperativeRef?: MutableRefObject<AnnotationCanvasHandle | null>;
|
|
33
|
-
}
|
|
34
|
-
export type PanTrigger = 'middleMouse' | 'rightMouse' | 'space';
|
|
35
|
-
export interface GestureConfig {
|
|
36
|
-
wheel?: 'zoom' | 'pan' | 'auto';
|
|
37
|
-
panTriggers?: PanTrigger[];
|
|
38
|
-
}
|
|
39
|
-
export declare const AnnotationCanvasInner: (props: AnnotationCanvasInnerProps) => import("react/jsx-runtime").JSX.Element;
|