@burdenoff/microfe-buildmyiq 2026.1031.1 → 2026.1031.2

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.
@@ -0,0 +1,3 @@
1
+ import { QueryClient, QueryKey } from '@tanstack/react-query';
2
+ export declare const invalidateAfterWrite: (queryClient: QueryClient, queryKey: QueryKey) => Promise<void>;
3
+ //# sourceMappingURL=invalidateAfterWrite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalidateAfterWrite.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/invalidateAfterWrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEnE,eAAO,MAAM,oBAAoB,GAC/B,aAAa,WAAW,EACxB,UAAU,QAAQ,KACjB,OAAO,CAAC,IAAI,CAMd,CAAC"}
@@ -0,0 +1,11 @@
1
+ //#region src/buildmyiq/hooks/invalidateAfterWrite.ts
2
+ var e = async (e, t) => {
3
+ await e.cancelQueries({
4
+ queryKey: t,
5
+ predicate: (e) => e.state.fetchStatus === "fetching" && e.state.data === void 0
6
+ }), await e.invalidateQueries({ queryKey: t });
7
+ };
8
+ //#endregion
9
+ export { e as invalidateAfterWrite };
10
+
11
+ //# sourceMappingURL=invalidateAfterWrite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalidateAfterWrite.js","names":[],"sources":["../../../src/buildmyiq/hooks/invalidateAfterWrite.ts"],"sourcesContent":["/**\n * Refresh the React Query cache after a BuildMyIQ write.\n *\n * `queryClient.invalidateQueries` alone is not enough when a query matching the\n * key is still on its FIRST load. React Query only cancels and restarts an\n * in-flight fetch when the query already holds data; a query with no data yet\n * has the refetch joined onto the request that is already on the wire. That\n * request was sent before the write committed, so it resolves with the\n * pre-write result and no second read follows. In practice: open a course's\n * Curriculum tab and add a module before the empty module list has loaded, and\n * the tab keeps showing \"No modules yet\" even though the module exists.\n *\n * So first cancel any matching query that is still loading without data (React\n * Query reverts it to its pre-fetch state, so nothing errors), then invalidate,\n * which issues a fresh read for every active query under the key.\n */\nimport type { QueryClient, QueryKey } from '@tanstack/react-query';\n\nexport const invalidateAfterWrite = async (\n queryClient: QueryClient,\n queryKey: QueryKey\n): Promise<void> => {\n await queryClient.cancelQueries({\n queryKey,\n predicate: (query) => query.state.fetchStatus === 'fetching' && query.state.data === undefined,\n });\n await queryClient.invalidateQueries({ queryKey });\n};\n"],"mappings":";AAkBA,IAAa,IAAuB,OAClC,GACA,MACkB;CAKlB,AAJA,MAAM,EAAY,cAAc;EAC9B;EACA,YAAY,MAAU,EAAM,MAAM,gBAAgB,cAAc,EAAM,MAAM,SAAS,KAAA;CACvF,CAAC,GACD,MAAM,EAAY,kBAAkB,EAAE,YAAS,CAAC;AAClD"}
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiComments.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiComments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACzB,CAAC;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,uEAAuE;AACvE,eAAO,MAAM,cAAc,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,GAAG,SAAS;;;UAwC9E,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM;QAKN,MAAM;0BAmBvE,CAAC"}
1
+ {"version":3,"file":"useBuiComments.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiComments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACzB,CAAC;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,uEAAuE;AACvE,eAAO,MAAM,cAAc,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,GAAG,SAAS;;;UAwC9E,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM;QAKN,MAAM;0BAmBvE,CAAC"}
@@ -1,45 +1,46 @@
1
1
  import { useBuildmyiq as e } from "../context/BuildmyiqContext.js";
2
2
  import "../context/index.js";
3
- import { wspaceFetch as t } from "./useBuiFiles.js";
4
- import { useMutation as n, useQuery as r, useQueryClient as i } from "@tanstack/react-query";
3
+ import { invalidateAfterWrite as t } from "./invalidateAfterWrite.js";
4
+ import { wspaceFetch as n } from "./useBuiFiles.js";
5
+ import { useMutation as r, useQuery as i, useQueryClient as a } from "@tanstack/react-query";
5
6
  //#region src/buildmyiq/hooks/useBuiComments.ts
6
- var a = (e, t) => [
7
+ var o = (e, t) => [
7
8
  "buildmyiq",
8
9
  "comments",
9
10
  e,
10
11
  t
11
- ], o = (n, i) => {
12
- let { apiGatewayUrl: o } = e();
13
- return r({
14
- queryKey: a(n, i ?? ""),
15
- enabled: !!i,
12
+ ], s = (t, r) => {
13
+ let { apiGatewayUrl: a } = e();
14
+ return i({
15
+ queryKey: o(t, r ?? ""),
16
+ enabled: !!r,
16
17
  queryFn: async () => {
17
- let e = await t("query BuiGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n content { text markdown }\n createdAt\n edited\n parentCommentId\n replyCount\n }\n }\n }", { input: {
18
- entityType: n,
19
- entityId: i,
18
+ let e = await n("query BuiGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n content { text markdown }\n createdAt\n edited\n parentCommentId\n replyCount\n }\n }\n }", { input: {
19
+ entityType: t,
20
+ entityId: r,
20
21
  take: 100,
21
22
  parentCommentId: null
22
- } }, o), r = e.getEntityComments.items.filter((e) => !e.parentCommentId);
23
- return r.sort((e, t) => new Date(e.createdAt).getTime() - new Date(t.createdAt).getTime()), {
24
- items: r,
23
+ } }, a), i = e.getEntityComments.items.filter((e) => !e.parentCommentId);
24
+ return i.sort((e, t) => new Date(e.createdAt).getTime() - new Date(t.createdAt).getTime()), {
25
+ items: i,
25
26
  total: e.getEntityComments.total
26
27
  };
27
28
  }
28
29
  });
29
- }, s = (r, o) => {
30
- let { apiGatewayUrl: s } = e(), c = i();
31
- return n({
32
- mutationFn: async (e) => (await t("mutation BuiCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }", { input: {
33
- entityType: r,
34
- entityId: o,
30
+ }, c = (i, s) => {
31
+ let { apiGatewayUrl: c } = e(), l = a();
32
+ return r({
33
+ mutationFn: async (e) => (await n("mutation BuiCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }", { input: {
34
+ entityType: i,
35
+ entityId: s,
35
36
  content: { text: e }
36
- } }, s)).createEntityComment,
37
+ } }, c)).createEntityComment,
37
38
  onSuccess: () => {
38
- c.invalidateQueries({ queryKey: a(r, o) });
39
+ t(l, o(i, s));
39
40
  }
40
41
  });
41
42
  };
42
43
  //#endregion
43
- export { o as useBuiComments, s as useEcoCreateComment };
44
+ export { s as useBuiComments, c as useEcoCreateComment };
44
45
 
45
46
  //# sourceMappingURL=useBuiComments.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiComments.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiComments.ts"],"sourcesContent":["/**\n * Buildmyiq — wspace-conversations-svc EntityComment REUSE hooks (Integration B).\n * @module buildmyiq/hooks/useBuiComments\n *\n * Adds a discussion surface to Buildmyiq entities WITHOUT any buildmyiq-svc change by\n * reusing the generic EntityComment API composed into the WORKSPACE supergraph.\n * Comments attach to any entity by `(entityType, entityId)`:\n * - `getEntityComments(input): EntityCommentConnection!`\n * - `createEntityComment(input): EntityComment!`\n *\n * Transport reuses `wspaceFetch` from `useBuiFiles` (workspace gateway +\n * workspace token). Lives in a separate file; `useBuildmyiqApi.ts` is\n * untouched.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useBuildmyiq } from '../context';\nimport { wspaceFetch } from './useBuiFiles';\n\nexport interface BuiComment {\n id: string;\n authorId: string;\n authorType: string;\n content: {\n text: string | null;\n markdown: string | null;\n };\n createdAt: string;\n edited: boolean;\n parentCommentId: string | null;\n replyCount: number;\n}\n\nconst commentsKey = (entityType: string, entityId: string) =>\n ['buildmyiq', 'comments', entityType, entityId] as const;\n\n/** Top-level (root) comments for an Buildmyiq entity, oldest first. */\nexport const useBuiComments = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: commentsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{\n getEntityComments: { items: BuiComment[]; total: number };\n }>(\n `query BuiGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n content { text markdown }\n createdAt\n edited\n parentCommentId\n replyCount\n }\n }\n }`,\n {\n input: {\n entityType,\n entityId,\n take: 100,\n parentCommentId: null,\n },\n },\n apiGatewayUrl\n );\n // Render root comments oldest-first (server order is not guaranteed).\n const roots = data.getEntityComments.items.filter((c) => !c.parentCommentId);\n roots.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());\n return { items: roots, total: data.getEntityComments.total };\n },\n });\n};\n\nexport const useEcoCreateComment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (text: string) => {\n const data = await wspaceFetch<{ createEntityComment: { id: string } }>(\n `mutation BuiCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }`,\n {\n input: {\n entityType,\n entityId,\n content: { text },\n },\n },\n apiGatewayUrl\n );\n return data.createEntityComment;\n },\n onSuccess: () => {\n qc.invalidateQueries({ queryKey: commentsKey(entityType, entityId) });\n },\n });\n};\n"],"mappings":";;;;;AAkCA,IAAM,KAAe,GAAoB,MACvC;CAAC;CAAa;CAAY;CAAY;AAAQ,GAGnC,KAAkB,GAAoB,MAAiC;CAClF,IAAM,EAAE,qBAAkB,EAAa;CACvC,OAAO,EAAS;EACd,UAAU,EAAY,GAAY,KAAY,EAAE;EAChD,SAAS,EAAQ;EACjB,SAAS,YAAY;GACnB,IAAM,IAAO,MAAM,EAGjB,6YAeA,EACE,OAAO;IACL;IACA;IACA,MAAM;IACN,iBAAiB;GACnB,EACF,GACA,CACF,GAEM,IAAQ,EAAK,kBAAkB,MAAM,QAAQ,MAAM,CAAC,EAAE,eAAe;GAE3E,OADA,EAAM,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC,GAC/E;IAAE,OAAO;IAAO,OAAO,EAAK,kBAAkB;GAAM;EAC7D;CACF,CAAC;AACH,GAEa,KAAuB,GAAoB,MAAqB;CAC3E,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAcV,MAbY,EACjB,wIAGA,EACE,OAAO;GACL;GACA;GACA,SAAS,EAAE,QAAK;EAClB,EACF,GACA,CACF,GACY;EAEd,iBAAiB;GACf,EAAG,kBAAkB,EAAE,UAAU,EAAY,GAAY,CAAQ,EAAE,CAAC;EACtE;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"useBuiComments.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiComments.ts"],"sourcesContent":["/**\n * Buildmyiq — wspace-conversations-svc EntityComment REUSE hooks (Integration B).\n * @module buildmyiq/hooks/useBuiComments\n *\n * Adds a discussion surface to Buildmyiq entities WITHOUT any buildmyiq-svc change by\n * reusing the generic EntityComment API composed into the WORKSPACE supergraph.\n * Comments attach to any entity by `(entityType, entityId)`:\n * - `getEntityComments(input): EntityCommentConnection!`\n * - `createEntityComment(input): EntityComment!`\n *\n * Transport reuses `wspaceFetch` from `useBuiFiles` (workspace gateway +\n * workspace token). Lives in a separate file; `useBuildmyiqApi.ts` is\n * untouched.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { invalidateAfterWrite } from './invalidateAfterWrite';\n\nimport { useBuildmyiq } from '../context';\nimport { wspaceFetch } from './useBuiFiles';\n\nexport interface BuiComment {\n id: string;\n authorId: string;\n authorType: string;\n content: {\n text: string | null;\n markdown: string | null;\n };\n createdAt: string;\n edited: boolean;\n parentCommentId: string | null;\n replyCount: number;\n}\n\nconst commentsKey = (entityType: string, entityId: string) =>\n ['buildmyiq', 'comments', entityType, entityId] as const;\n\n/** Top-level (root) comments for an Buildmyiq entity, oldest first. */\nexport const useBuiComments = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: commentsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{\n getEntityComments: { items: BuiComment[]; total: number };\n }>(\n `query BuiGetEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n content { text markdown }\n createdAt\n edited\n parentCommentId\n replyCount\n }\n }\n }`,\n {\n input: {\n entityType,\n entityId,\n take: 100,\n parentCommentId: null,\n },\n },\n apiGatewayUrl\n );\n // Render root comments oldest-first (server order is not guaranteed).\n const roots = data.getEntityComments.items.filter((c) => !c.parentCommentId);\n roots.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());\n return { items: roots, total: data.getEntityComments.total };\n },\n });\n};\n\nexport const useEcoCreateComment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (text: string) => {\n const data = await wspaceFetch<{ createEntityComment: { id: string } }>(\n `mutation BuiCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) { id }\n }`,\n {\n input: {\n entityType,\n entityId,\n content: { text },\n },\n },\n apiGatewayUrl\n );\n return data.createEntityComment;\n },\n onSuccess: () => {\n invalidateAfterWrite(qc, commentsKey(entityType, entityId));\n },\n });\n};\n"],"mappings":";;;;;;AAmCA,IAAM,KAAe,GAAoB,MACvC;CAAC;CAAa;CAAY;CAAY;AAAQ,GAGnC,KAAkB,GAAoB,MAAiC;CAClF,IAAM,EAAE,qBAAkB,EAAa;CACvC,OAAO,EAAS;EACd,UAAU,EAAY,GAAY,KAAY,EAAE;EAChD,SAAS,EAAQ;EACjB,SAAS,YAAY;GACnB,IAAM,IAAO,MAAM,EAGjB,6YAeA,EACE,OAAO;IACL;IACA;IACA,MAAM;IACN,iBAAiB;GACnB,EACF,GACA,CACF,GAEM,IAAQ,EAAK,kBAAkB,MAAM,QAAQ,MAAM,CAAC,EAAE,eAAe;GAE3E,OADA,EAAM,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC,GAC/E;IAAE,OAAO;IAAO,OAAO,EAAK,kBAAkB;GAAM;EAC7D;CACF,CAAC;AACH,GAEa,KAAuB,GAAoB,MAAqB;CAC3E,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAcV,MAbY,EACjB,wIAGA,EACE,OAAO;GACL;GACA;GACA,SAAS,EAAE,QAAK;EAClB,EACF,GACA,CACF,GACY;EAEd,iBAAiB;GACf,EAAqB,GAAI,EAAY,GAAY,CAAQ,CAAC;EAC5D;CACF,CAAC;AACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiProctoring.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiProctoring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EACV,aAAa,EACb,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,+BAA+B,EAC/B,gCAAgC,EACjC,MAAM,UAAU,CAAC;AAkClB;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,MAAM,GAAG,SAAS,EAAE,iBAAc,gGAwBzF,CAAC;AAEF,eAAO,MAAM,4BAA4B,uGAkBxC,CAAC;AAEF,eAAO,MAAM,8BAA8B;QAIR,MAAM;WAAS,gCAAgC;WAejF,CAAC;AAEF,6FAA6F;AAC7F,eAAO,MAAM,+BAA+B,uGAkB3C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,6BAA6B;QAIP,MAAM;aAAW,wBAAwB,EAAE;WAc7E,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,8BAA8B,GAAI,WAAW,MAAM,GAAG,SAAS,EAAE,iBAAc,4JA2B3F,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,wBAAwB,GACnC,MAAM;IAAE,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7E,iBAAc,wGAuBf,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GAAI,WAAW,MAAM,GAAG,SAAS,EAAE,iBAAc,qGAuBzF,CAAC;AAEF,eAAO,MAAM,6BAA6B;QAIP,MAAM;WAAS,+BAA+B;WAYhF,CAAC"}
1
+ {"version":3,"file":"useBuiProctoring.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiProctoring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,OAAO,KAAK,EACV,aAAa,EACb,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,+BAA+B,EAC/B,gCAAgC,EACjC,MAAM,UAAU,CAAC;AAkClB;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,cAAc,MAAM,GAAG,SAAS,EAAE,iBAAc,gGAwBzF,CAAC;AAEF,eAAO,MAAM,4BAA4B,uGAkBxC,CAAC;AAEF,eAAO,MAAM,8BAA8B;QAIR,MAAM;WAAS,gCAAgC;WAejF,CAAC;AAEF,6FAA6F;AAC7F,eAAO,MAAM,+BAA+B,uGAkB3C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,6BAA6B;QAIP,MAAM;aAAW,wBAAwB,EAAE;WAc7E,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,8BAA8B,GAAI,WAAW,MAAM,GAAG,SAAS,EAAE,iBAAc,4JA2B3F,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,wBAAwB,GACnC,MAAM;IAAE,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7E,iBAAc,wGAuBf,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GAAI,WAAW,MAAM,GAAG,SAAS,EAAE,iBAAc,qGAuBzF,CAAC;AAEF,eAAO,MAAM,6BAA6B;QAIP,MAAM;WAAS,+BAA+B;WAYhF,CAAC"}
@@ -1,9 +1,10 @@
1
1
  import { useBuildmyiq as e } from "../context/BuildmyiqContext.js";
2
2
  import "../context/index.js";
3
- import { wspaceFetch as t } from "./useBuiFiles.js";
4
- import { useInfiniteQuery as n, useMutation as r, useQuery as i, useQueryClient as a } from "@tanstack/react-query";
3
+ import { invalidateAfterWrite as t } from "./invalidateAfterWrite.js";
4
+ import { wspaceFetch as n } from "./useBuiFiles.js";
5
+ import { useInfiniteQuery as r, useMutation as i, useQuery as a, useQueryClient as o } from "@tanstack/react-query";
5
6
  //#region src/buildmyiq/hooks/useBuiProctoring.ts
6
- var o = "\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt violationCount\n aiMonitoringConsentAt signalCount reviewedById reviewedAt reviewNotes expiresAt createdAt updatedAt\n identityFile { id }\n", s = {
7
+ var s = "\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt violationCount\n aiMonitoringConsentAt signalCount reviewedById reviewedAt reviewNotes expiresAt createdAt updatedAt\n identityFile { id }\n", c = {
7
8
  mine: (e) => [
8
9
  "buildmyiq",
9
10
  "proctoring",
@@ -27,121 +28,121 @@ var o = "\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt
27
28
  "signals",
28
29
  e
29
30
  ]
30
- }, c = 4 * 6e4, l = "\n id sessionId type startedAt durationMs confidence faceCount peakLevelDb createdAt\n", u = 15e3, d = (n, r = !0) => {
31
- let { apiGatewayUrl: a } = e();
32
- return i({
33
- queryKey: n ? s.mine(n) : [
31
+ }, l = 4 * 6e4, u = "\n id sessionId type startedAt durationMs confidence faceCount peakLevelDb createdAt\n", d = 15e3, f = (t, r = !0) => {
32
+ let { apiGatewayUrl: i } = e();
33
+ return a({
34
+ queryKey: t ? c.mine(t) : [
34
35
  "buildmyiq",
35
36
  "proctoring",
36
37
  "mine",
37
38
  "nil"
38
39
  ],
39
- enabled: !!n && r,
40
- queryFn: async () => (await t(`query BuiMyProctoringSession($assessmentId: ID!) {
41
- buiMyProctoringSession(assessmentId: $assessmentId) { ${o} }
42
- }`, { assessmentId: n }, a)).buiMyProctoringSession,
40
+ enabled: !!t && r,
41
+ queryFn: async () => (await n(`query BuiMyProctoringSession($assessmentId: ID!) {
42
+ buiMyProctoringSession(assessmentId: $assessmentId) { ${s} }
43
+ }`, { assessmentId: t }, i)).buiMyProctoringSession,
43
44
  refetchInterval: (e) => {
44
45
  let t = e.state.data?.status;
45
- return t === "UNDER_REVIEW" || t === "IDENTITY_SUBMITTED" ? u : !1;
46
+ return t === "UNDER_REVIEW" || t === "IDENTITY_SUBMITTED" ? d : !1;
46
47
  }
47
48
  });
48
- }, f = () => {
49
- let { apiGatewayUrl: n } = e(), i = a();
50
- return r({
51
- mutationFn: async (e) => (await t(`mutation BuiStartProctoringSession($assessmentId: ID!) {
52
- buiStartProctoringSession(assessmentId: $assessmentId) { ${o} }
53
- }`, { assessmentId: e }, n)).buiStartProctoringSession,
49
+ }, p = () => {
50
+ let { apiGatewayUrl: t } = e(), r = o();
51
+ return i({
52
+ mutationFn: async (e) => (await n(`mutation BuiStartProctoringSession($assessmentId: ID!) {
53
+ buiStartProctoringSession(assessmentId: $assessmentId) { ${s} }
54
+ }`, { assessmentId: e }, t)).buiStartProctoringSession,
54
55
  onSuccess: (e) => {
55
- i.setQueryData(s.mine(e.assessmentId), e);
56
+ r.setQueryData(c.mine(e.assessmentId), e);
56
57
  }
57
58
  });
58
- }, p = () => {
59
- let { apiGatewayUrl: n } = e(), i = a();
60
- return r({
61
- mutationFn: async (e) => (await t(`mutation BuiSubmitProctoringIdentity($id: ID!, $input: BuiSubmitProctoringIdentityInput!) {
62
- buiSubmitProctoringIdentity(id: $id, input: $input) { ${o} }
63
- }`, e, n)).buiSubmitProctoringIdentity,
59
+ }, m = () => {
60
+ let { apiGatewayUrl: r } = e(), a = o();
61
+ return i({
62
+ mutationFn: async (e) => (await n(`mutation BuiSubmitProctoringIdentity($id: ID!, $input: BuiSubmitProctoringIdentityInput!) {
63
+ buiSubmitProctoringIdentity(id: $id, input: $input) { ${s} }
64
+ }`, e, r)).buiSubmitProctoringIdentity,
64
65
  onSuccess: (e) => {
65
- i.setQueryData(s.mine(e.assessmentId), e), i.invalidateQueries({ queryKey: s.queue });
66
+ a.setQueryData(c.mine(e.assessmentId), e), t(a, c.queue);
66
67
  }
67
68
  });
68
- }, m = () => {
69
- let { apiGatewayUrl: n } = e(), i = a();
70
- return r({
71
- mutationFn: async (e) => (await t(`mutation BuiRecordProctoringSignals($id: ID!, $signals: [BuiProctoringSignalInput!]!) {
72
- buiRecordProctoringSignals(id: $id, signals: $signals) { ${o} }
73
- }`, e, n)).buiRecordProctoringSignals,
69
+ }, h = () => {
70
+ let { apiGatewayUrl: t } = e(), r = o();
71
+ return i({
72
+ mutationFn: async (e) => (await n(`mutation BuiRecordProctoringSignals($id: ID!, $signals: [BuiProctoringSignalInput!]!) {
73
+ buiRecordProctoringSignals(id: $id, signals: $signals) { ${s} }
74
+ }`, e, t)).buiRecordProctoringSignals,
74
75
  onSuccess: (e) => {
75
- i.setQueryData(s.mine(e.assessmentId), e);
76
+ r.setQueryData(c.mine(e.assessmentId), e);
76
77
  }
77
78
  });
78
- }, h = (r, i = !0) => {
79
+ }, g = (t, i = !0) => {
79
80
  let { apiGatewayUrl: a } = e();
80
- return n({
81
- queryKey: r ? s.signals(r) : [
81
+ return r({
82
+ queryKey: t ? c.signals(t) : [
82
83
  "buildmyiq",
83
84
  "proctoring",
84
85
  "signals",
85
86
  "nil"
86
87
  ],
87
- enabled: !!r && i,
88
+ enabled: !!t && i,
88
89
  initialPageParam: void 0,
89
- queryFn: async ({ pageParam: e }) => (await t(`query BuiProctoringSessionSignals($sessionId: ID!, $limit: Int, $after: ID) {
90
+ queryFn: async ({ pageParam: e }) => (await n(`query BuiProctoringSessionSignals($sessionId: ID!, $limit: Int, $after: ID) {
90
91
  buiProctoringSessionSignals(sessionId: $sessionId, limit: $limit, after: $after) {
91
- items { ${l} }
92
+ items { ${u} }
92
93
  pageInfo { cursor hasMore }
93
94
  }
94
95
  }`, e ? {
95
- sessionId: r,
96
+ sessionId: t,
96
97
  limit: 100,
97
98
  after: e
98
99
  } : {
99
- sessionId: r,
100
+ sessionId: t,
100
101
  limit: 100
101
102
  }, a)).buiProctoringSessionSignals,
102
103
  getNextPageParam: (e) => e.pageInfo.hasMore ? e.pageInfo.cursor ?? void 0 : void 0
103
104
  });
104
- }, g = (n, r = !0) => {
105
- let { apiGatewayUrl: a } = e(), { status: c, assessmentId: l, limit: u = 50 } = n;
106
- return i({
105
+ }, _ = (t, r = !0) => {
106
+ let { apiGatewayUrl: i } = e(), { status: o, assessmentId: l, limit: u = 50 } = t;
107
+ return a({
107
108
  queryKey: [
108
- ...s.queue,
109
- c ?? "ALL",
109
+ ...c.queue,
110
+ o ?? "ALL",
110
111
  l ?? "ALL",
111
112
  u
112
113
  ],
113
114
  enabled: r,
114
- queryFn: async () => (await t(`query BuiListProctoringSessions($assessmentId: ID, $status: BuiProctoringStatus, $pagination: PaginationInput) {
115
+ queryFn: async () => (await n(`query BuiListProctoringSessions($assessmentId: ID, $status: BuiProctoringStatus, $pagination: PaginationInput) {
115
116
  buiListProctoringSessions(assessmentId: $assessmentId, status: $status, pagination: $pagination) {
116
- items { ${o} }
117
+ items { ${s} }
117
118
  pageInfo { cursor hasMore }
118
119
  }
119
120
  }`, {
120
121
  assessmentId: l,
121
- status: c,
122
+ status: o,
122
123
  pagination: { limit: u }
123
- }, a)).buiListProctoringSessions
124
+ }, i)).buiListProctoringSessions
124
125
  });
125
- }, _ = (n, r = !0) => {
126
- let { apiGatewayUrl: a } = e();
127
- return i({
128
- queryKey: s.identityFile(n ?? "nil"),
129
- enabled: !!n && r,
130
- staleTime: c,
131
- refetchInterval: c,
126
+ }, v = (t, r = !0) => {
127
+ let { apiGatewayUrl: i } = e();
128
+ return a({
129
+ queryKey: c.identityFile(t ?? "nil"),
130
+ enabled: !!t && r,
131
+ staleTime: l,
132
+ refetchInterval: l,
132
133
  retry: !1,
133
- queryFn: async () => (await t("query BuiProctoringIdentityFile($sessionId: ID!) {\n buiProctoringIdentityFile(sessionId: $sessionId) {\n fileId name mimeType size downloadUrl expiresAt\n }\n }", { sessionId: n }, a)).buiProctoringIdentityFile
134
+ queryFn: async () => (await n("query BuiProctoringIdentityFile($sessionId: ID!) {\n buiProctoringIdentityFile(sessionId: $sessionId) {\n fileId name mimeType size downloadUrl expiresAt\n }\n }", { sessionId: t }, i)).buiProctoringIdentityFile
134
135
  });
135
- }, v = () => {
136
- let { apiGatewayUrl: n } = e(), i = a();
137
- return r({
138
- mutationFn: async (e) => (await t(`mutation BuiReviewProctoringSession($id: ID!, $input: BuiReviewProctoringSessionInput!) {
139
- buiReviewProctoringSession(id: $id, input: $input) { ${o} }
140
- }`, e, n)).buiReviewProctoringSession,
141
- onSuccess: () => i.invalidateQueries({ queryKey: s.queue })
136
+ }, y = () => {
137
+ let { apiGatewayUrl: r } = e(), a = o();
138
+ return i({
139
+ mutationFn: async (e) => (await n(`mutation BuiReviewProctoringSession($id: ID!, $input: BuiReviewProctoringSessionInput!) {
140
+ buiReviewProctoringSession(id: $id, input: $input) { ${s} }
141
+ }`, e, r)).buiReviewProctoringSession,
142
+ onSuccess: () => t(a, c.queue)
142
143
  });
143
144
  };
144
145
  //#endregion
145
- export { d as useBuiMyProctoringSession, _ as useBuiProctoringIdentityFile, h as useBuiProctoringSessionSignals, g as useBuiProctoringSessions, m as useBuiRecordProctoringSignals, v as useBuiReviewProctoringSession, f as useBuiStartProctoringSession, p as useBuiSubmitProctoringIdentity };
146
+ export { f as useBuiMyProctoringSession, v as useBuiProctoringIdentityFile, g as useBuiProctoringSessionSignals, _ as useBuiProctoringSessions, h as useBuiRecordProctoringSignals, y as useBuiReviewProctoringSession, p as useBuiStartProctoringSession, m as useBuiSubmitProctoringIdentity };
146
147
 
147
148
  //# sourceMappingURL=useBuiProctoring.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiProctoring.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiProctoring.ts"],"sourcesContent":["/**\n * Buildmyiq — high-stakes exam proctoring hooks (BOFF-4431).\n * @module buildmyiq/hooks/useBuiProctoring\n *\n * Wired to the `wspace-buildmyiq-svc` proctoring contract:\n * - learner (self-scoped): `buiMyProctoringSession`, `buiStartProctoringSession`,\n * `buiSubmitProctoringIdentity`, `buiReportProctoringViolation`\n * - reviewer (workspace-scoped): `buiListProctoringSessions`,\n * `buiReviewProctoringSession`, `buiProctoringIdentityFile`\n * - BOFF-9690 signals: `buiRecordProctoringSignals` (learner, own session)\n * and the reviewer timeline `buiProctoringSessionSignals`\n *\n * The backend is the gate: `buiSubmitAssessment` refuses a `requiresProctoring`\n * assessment unless the learner's latest session is APPROVED and unexpired.\n * These hooks only drive the UI through that lifecycle.\n */\n\nimport { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useBuildmyiq } from '../context';\nimport { wspaceFetch } from './useBuiFiles';\nimport type {\n BuiConnection,\n BuiProctoringIdentityFile,\n BuiProctoringSession,\n BuiProctoringSignal,\n BuiProctoringSignalInput,\n BuiProctoringStatus,\n BuiReviewProctoringSessionInput,\n BuiSubmitProctoringIdentityInput,\n} from '../types';\n\n// `identityFile` selects ONLY `id`: wspace-files-svc's File entity has no\n// __resolveReference, so any other field on a federated File stub fails the\n// whole operation. The learner's photo ID is PRIVATE in the file store\n// (BOFF-9691), so the reviewer page opens it through `buiProctoringIdentityFile`\n// (useBuiProctoringIdentityFile), never through the files graph.\nconst PROCTORING_SESSION_FIELDS = `\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt violationCount\n aiMonitoringConsentAt signalCount reviewedById reviewedAt reviewNotes expiresAt createdAt updatedAt\n identityFile { id }\n`;\n\nconst PROCTORING_KEY = {\n mine: (assessmentId: string) => ['buildmyiq', 'proctoring', 'mine', assessmentId] as const,\n queue: ['buildmyiq', 'proctoring', 'queue'] as const,\n identityFile: (sessionId: string) =>\n ['buildmyiq', 'proctoring', 'identity-file', sessionId] as const,\n signals: (sessionId: string) => ['buildmyiq', 'proctoring', 'signals', sessionId] as const,\n};\n\n/**\n * The backend presigns the identity URL for about five minutes; re-fetch well\n * before that so an open review page never holds a dead link.\n */\nconst IDENTITY_URL_REFRESH_MS = 4 * 60_000;\n\nconst PROCTORING_SIGNAL_FIELDS = `\n id sessionId type startedAt durationMs confidence faceCount peakLevelDb createdAt\n`;\n\n/** How often the learner's waiting screen re-checks for a reviewer decision. */\nconst AWAITING_REVIEW_POLL_MS = 15_000;\n\n/**\n * The calling learner's most recent proctoring session for an assessment\n * (null when they have never started one). Polls while a reviewer decision\n * is pending so the exam unlocks without a manual refresh.\n */\nexport const useBuiMyProctoringSession = (assessmentId: string | undefined, enabled = true) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: assessmentId\n ? PROCTORING_KEY.mine(assessmentId)\n : ['buildmyiq', 'proctoring', 'mine', 'nil'],\n enabled: Boolean(assessmentId) && enabled,\n queryFn: async () => {\n const data = await wspaceFetch<{ buiMyProctoringSession: BuiProctoringSession | null }>(\n `query BuiMyProctoringSession($assessmentId: ID!) {\n buiMyProctoringSession(assessmentId: $assessmentId) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { assessmentId },\n apiGatewayUrl\n );\n return data.buiMyProctoringSession;\n },\n refetchInterval: (query) => {\n const status = query.state.data?.status;\n return status === 'UNDER_REVIEW' || status === 'IDENTITY_SUBMITTED'\n ? AWAITING_REVIEW_POLL_MS\n : false;\n },\n });\n};\n\nexport const useBuiStartProctoringSession = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (assessmentId: string) => {\n const data = await wspaceFetch<{ buiStartProctoringSession: BuiProctoringSession }>(\n `mutation BuiStartProctoringSession($assessmentId: ID!) {\n buiStartProctoringSession(assessmentId: $assessmentId) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { assessmentId },\n apiGatewayUrl\n );\n return data.buiStartProctoringSession;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n },\n });\n};\n\nexport const useBuiSubmitProctoringIdentity = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiSubmitProctoringIdentityInput }) => {\n const data = await wspaceFetch<{ buiSubmitProctoringIdentity: BuiProctoringSession }>(\n `mutation BuiSubmitProctoringIdentity($id: ID!, $input: BuiSubmitProctoringIdentityInput!) {\n buiSubmitProctoringIdentity(id: $id, input: $input) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n args,\n apiGatewayUrl\n );\n return data.buiSubmitProctoringIdentity;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n void qc.invalidateQueries({ queryKey: PROCTORING_KEY.queue });\n },\n });\n};\n\n/** Records one lockdown violation (tab switch, window blur, fullscreen exit, copy/paste). */\nexport const useBuiReportProctoringViolation = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await wspaceFetch<{ buiReportProctoringViolation: BuiProctoringSession }>(\n `mutation BuiReportProctoringViolation($id: ID!) {\n buiReportProctoringViolation(id: $id) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.buiReportProctoringViolation;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n },\n });\n};\n\n/**\n * BOFF-9690: records a batch (max 50) of signal episodes on the learner's own\n * approved session — lockdown events and, when the assessment opted in and\n * the learner consented, in-browser AI signals.\n */\nexport const useBuiRecordProctoringSignals = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; signals: BuiProctoringSignalInput[] }) => {\n const data = await wspaceFetch<{ buiRecordProctoringSignals: BuiProctoringSession }>(\n `mutation BuiRecordProctoringSignals($id: ID!, $signals: [BuiProctoringSignalInput!]!) {\n buiRecordProctoringSignals(id: $id, signals: $signals) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n args,\n apiGatewayUrl\n );\n return data.buiRecordProctoringSignals;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n },\n });\n};\n\n/** BOFF-9690: reviewer timeline of one session's signals, oldest first, 100 per page. */\nexport const useBuiProctoringSessionSignals = (sessionId: string | undefined, enabled = true) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useInfiniteQuery({\n queryKey: sessionId\n ? PROCTORING_KEY.signals(sessionId)\n : ['buildmyiq', 'proctoring', 'signals', 'nil'],\n enabled: Boolean(sessionId) && enabled,\n initialPageParam: undefined as string | undefined,\n queryFn: async ({ pageParam }) => {\n const data = await wspaceFetch<{\n buiProctoringSessionSignals: BuiConnection<BuiProctoringSignal>;\n }>(\n `query BuiProctoringSessionSignals($sessionId: ID!, $limit: Int, $after: ID) {\n buiProctoringSessionSignals(sessionId: $sessionId, limit: $limit, after: $after) {\n items { ${PROCTORING_SIGNAL_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n // Explicit limit/after: the composed PaginationInput carries only `limit`.\n pageParam ? { sessionId, limit: 100, after: pageParam } : { sessionId, limit: 100 },\n apiGatewayUrl\n );\n return data.buiProctoringSessionSignals;\n },\n getNextPageParam: (last) =>\n last.pageInfo.hasMore ? (last.pageInfo.cursor ?? undefined) : undefined,\n });\n};\n\n/** Reviewer queue: proctoring sessions in the workspace, optionally filtered by status. */\nexport const useBuiProctoringSessions = (\n args: { status?: BuiProctoringStatus; assessmentId?: string; limit?: number },\n enabled = true\n) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const { status, assessmentId, limit = 50 } = args;\n return useQuery({\n queryKey: [...PROCTORING_KEY.queue, status ?? 'ALL', assessmentId ?? 'ALL', limit],\n enabled,\n queryFn: async () => {\n const data = await wspaceFetch<{\n buiListProctoringSessions: BuiConnection<BuiProctoringSession>;\n }>(\n `query BuiListProctoringSessions($assessmentId: ID, $status: BuiProctoringStatus, $pagination: PaginationInput) {\n buiListProctoringSessions(assessmentId: $assessmentId, status: $status, pagination: $pagination) {\n items { ${PROCTORING_SESSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { assessmentId, status, pagination: { limit } },\n apiGatewayUrl\n );\n return data.buiListProctoringSessions;\n },\n });\n};\n\n/**\n * Reviewer-only: the learner's identity photo/ID for one session, with a\n * short-lived download URL (BOFF-9691). Null when the session has none.\n */\nexport const useBuiProctoringIdentityFile = (sessionId: string | undefined, enabled = true) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: PROCTORING_KEY.identityFile(sessionId ?? 'nil'),\n enabled: Boolean(sessionId) && enabled,\n staleTime: IDENTITY_URL_REFRESH_MS,\n refetchInterval: IDENTITY_URL_REFRESH_MS,\n retry: false,\n queryFn: async () => {\n const data = await wspaceFetch<{\n buiProctoringIdentityFile: BuiProctoringIdentityFile | null;\n }>(\n `query BuiProctoringIdentityFile($sessionId: ID!) {\n buiProctoringIdentityFile(sessionId: $sessionId) {\n fileId name mimeType size downloadUrl expiresAt\n }\n }`,\n { sessionId },\n apiGatewayUrl\n );\n return data.buiProctoringIdentityFile;\n },\n });\n};\n\nexport const useBuiReviewProctoringSession = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiReviewProctoringSessionInput }) => {\n const data = await wspaceFetch<{ buiReviewProctoringSession: BuiProctoringSession }>(\n `mutation BuiReviewProctoringSession($id: ID!, $input: BuiReviewProctoringSessionInput!) {\n buiReviewProctoringSession(id: $id, input: $input) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n args,\n apiGatewayUrl\n );\n return data.buiReviewProctoringSession;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: PROCTORING_KEY.queue }),\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useBuiProctoring.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiProctoring.ts"],"sourcesContent":["/**\n * Buildmyiq — high-stakes exam proctoring hooks (BOFF-4431).\n * @module buildmyiq/hooks/useBuiProctoring\n *\n * Wired to the `wspace-buildmyiq-svc` proctoring contract:\n * - learner (self-scoped): `buiMyProctoringSession`, `buiStartProctoringSession`,\n * `buiSubmitProctoringIdentity`, `buiReportProctoringViolation`\n * - reviewer (workspace-scoped): `buiListProctoringSessions`,\n * `buiReviewProctoringSession`, `buiProctoringIdentityFile`\n * - BOFF-9690 signals: `buiRecordProctoringSignals` (learner, own session)\n * and the reviewer timeline `buiProctoringSessionSignals`\n *\n * The backend is the gate: `buiSubmitAssessment` refuses a `requiresProctoring`\n * assessment unless the learner's latest session is APPROVED and unexpired.\n * These hooks only drive the UI through that lifecycle.\n */\n\nimport { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { invalidateAfterWrite } from './invalidateAfterWrite';\n\nimport { useBuildmyiq } from '../context';\nimport { wspaceFetch } from './useBuiFiles';\nimport type {\n BuiConnection,\n BuiProctoringIdentityFile,\n BuiProctoringSession,\n BuiProctoringSignal,\n BuiProctoringSignalInput,\n BuiProctoringStatus,\n BuiReviewProctoringSessionInput,\n BuiSubmitProctoringIdentityInput,\n} from '../types';\n\n// `identityFile` selects ONLY `id`: wspace-files-svc's File entity has no\n// __resolveReference, so any other field on a federated File stub fails the\n// whole operation. The learner's photo ID is PRIVATE in the file store\n// (BOFF-9691), so the reviewer page opens it through `buiProctoringIdentityFile`\n// (useBuiProctoringIdentityFile), never through the files graph.\nconst PROCTORING_SESSION_FIELDS = `\n id workspaceId assessmentId learnerId status lockdownAcknowledgedAt violationCount\n aiMonitoringConsentAt signalCount reviewedById reviewedAt reviewNotes expiresAt createdAt updatedAt\n identityFile { id }\n`;\n\nconst PROCTORING_KEY = {\n mine: (assessmentId: string) => ['buildmyiq', 'proctoring', 'mine', assessmentId] as const,\n queue: ['buildmyiq', 'proctoring', 'queue'] as const,\n identityFile: (sessionId: string) =>\n ['buildmyiq', 'proctoring', 'identity-file', sessionId] as const,\n signals: (sessionId: string) => ['buildmyiq', 'proctoring', 'signals', sessionId] as const,\n};\n\n/**\n * The backend presigns the identity URL for about five minutes; re-fetch well\n * before that so an open review page never holds a dead link.\n */\nconst IDENTITY_URL_REFRESH_MS = 4 * 60_000;\n\nconst PROCTORING_SIGNAL_FIELDS = `\n id sessionId type startedAt durationMs confidence faceCount peakLevelDb createdAt\n`;\n\n/** How often the learner's waiting screen re-checks for a reviewer decision. */\nconst AWAITING_REVIEW_POLL_MS = 15_000;\n\n/**\n * The calling learner's most recent proctoring session for an assessment\n * (null when they have never started one). Polls while a reviewer decision\n * is pending so the exam unlocks without a manual refresh.\n */\nexport const useBuiMyProctoringSession = (assessmentId: string | undefined, enabled = true) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: assessmentId\n ? PROCTORING_KEY.mine(assessmentId)\n : ['buildmyiq', 'proctoring', 'mine', 'nil'],\n enabled: Boolean(assessmentId) && enabled,\n queryFn: async () => {\n const data = await wspaceFetch<{ buiMyProctoringSession: BuiProctoringSession | null }>(\n `query BuiMyProctoringSession($assessmentId: ID!) {\n buiMyProctoringSession(assessmentId: $assessmentId) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { assessmentId },\n apiGatewayUrl\n );\n return data.buiMyProctoringSession;\n },\n refetchInterval: (query) => {\n const status = query.state.data?.status;\n return status === 'UNDER_REVIEW' || status === 'IDENTITY_SUBMITTED'\n ? AWAITING_REVIEW_POLL_MS\n : false;\n },\n });\n};\n\nexport const useBuiStartProctoringSession = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (assessmentId: string) => {\n const data = await wspaceFetch<{ buiStartProctoringSession: BuiProctoringSession }>(\n `mutation BuiStartProctoringSession($assessmentId: ID!) {\n buiStartProctoringSession(assessmentId: $assessmentId) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { assessmentId },\n apiGatewayUrl\n );\n return data.buiStartProctoringSession;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n },\n });\n};\n\nexport const useBuiSubmitProctoringIdentity = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiSubmitProctoringIdentityInput }) => {\n const data = await wspaceFetch<{ buiSubmitProctoringIdentity: BuiProctoringSession }>(\n `mutation BuiSubmitProctoringIdentity($id: ID!, $input: BuiSubmitProctoringIdentityInput!) {\n buiSubmitProctoringIdentity(id: $id, input: $input) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n args,\n apiGatewayUrl\n );\n return data.buiSubmitProctoringIdentity;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n void invalidateAfterWrite(qc, PROCTORING_KEY.queue);\n },\n });\n};\n\n/** Records one lockdown violation (tab switch, window blur, fullscreen exit, copy/paste). */\nexport const useBuiReportProctoringViolation = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await wspaceFetch<{ buiReportProctoringViolation: BuiProctoringSession }>(\n `mutation BuiReportProctoringViolation($id: ID!) {\n buiReportProctoringViolation(id: $id) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n { id },\n apiGatewayUrl\n );\n return data.buiReportProctoringViolation;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n },\n });\n};\n\n/**\n * BOFF-9690: records a batch (max 50) of signal episodes on the learner's own\n * approved session — lockdown events and, when the assessment opted in and\n * the learner consented, in-browser AI signals.\n */\nexport const useBuiRecordProctoringSignals = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; signals: BuiProctoringSignalInput[] }) => {\n const data = await wspaceFetch<{ buiRecordProctoringSignals: BuiProctoringSession }>(\n `mutation BuiRecordProctoringSignals($id: ID!, $signals: [BuiProctoringSignalInput!]!) {\n buiRecordProctoringSignals(id: $id, signals: $signals) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n args,\n apiGatewayUrl\n );\n return data.buiRecordProctoringSignals;\n },\n onSuccess: (session) => {\n qc.setQueryData(PROCTORING_KEY.mine(session.assessmentId), session);\n },\n });\n};\n\n/** BOFF-9690: reviewer timeline of one session's signals, oldest first, 100 per page. */\nexport const useBuiProctoringSessionSignals = (sessionId: string | undefined, enabled = true) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useInfiniteQuery({\n queryKey: sessionId\n ? PROCTORING_KEY.signals(sessionId)\n : ['buildmyiq', 'proctoring', 'signals', 'nil'],\n enabled: Boolean(sessionId) && enabled,\n initialPageParam: undefined as string | undefined,\n queryFn: async ({ pageParam }) => {\n const data = await wspaceFetch<{\n buiProctoringSessionSignals: BuiConnection<BuiProctoringSignal>;\n }>(\n `query BuiProctoringSessionSignals($sessionId: ID!, $limit: Int, $after: ID) {\n buiProctoringSessionSignals(sessionId: $sessionId, limit: $limit, after: $after) {\n items { ${PROCTORING_SIGNAL_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n // Explicit limit/after: the composed PaginationInput carries only `limit`.\n pageParam ? { sessionId, limit: 100, after: pageParam } : { sessionId, limit: 100 },\n apiGatewayUrl\n );\n return data.buiProctoringSessionSignals;\n },\n getNextPageParam: (last) =>\n last.pageInfo.hasMore ? (last.pageInfo.cursor ?? undefined) : undefined,\n });\n};\n\n/** Reviewer queue: proctoring sessions in the workspace, optionally filtered by status. */\nexport const useBuiProctoringSessions = (\n args: { status?: BuiProctoringStatus; assessmentId?: string; limit?: number },\n enabled = true\n) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const { status, assessmentId, limit = 50 } = args;\n return useQuery({\n queryKey: [...PROCTORING_KEY.queue, status ?? 'ALL', assessmentId ?? 'ALL', limit],\n enabled,\n queryFn: async () => {\n const data = await wspaceFetch<{\n buiListProctoringSessions: BuiConnection<BuiProctoringSession>;\n }>(\n `query BuiListProctoringSessions($assessmentId: ID, $status: BuiProctoringStatus, $pagination: PaginationInput) {\n buiListProctoringSessions(assessmentId: $assessmentId, status: $status, pagination: $pagination) {\n items { ${PROCTORING_SESSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { assessmentId, status, pagination: { limit } },\n apiGatewayUrl\n );\n return data.buiListProctoringSessions;\n },\n });\n};\n\n/**\n * Reviewer-only: the learner's identity photo/ID for one session, with a\n * short-lived download URL (BOFF-9691). Null when the session has none.\n */\nexport const useBuiProctoringIdentityFile = (sessionId: string | undefined, enabled = true) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: PROCTORING_KEY.identityFile(sessionId ?? 'nil'),\n enabled: Boolean(sessionId) && enabled,\n staleTime: IDENTITY_URL_REFRESH_MS,\n refetchInterval: IDENTITY_URL_REFRESH_MS,\n retry: false,\n queryFn: async () => {\n const data = await wspaceFetch<{\n buiProctoringIdentityFile: BuiProctoringIdentityFile | null;\n }>(\n `query BuiProctoringIdentityFile($sessionId: ID!) {\n buiProctoringIdentityFile(sessionId: $sessionId) {\n fileId name mimeType size downloadUrl expiresAt\n }\n }`,\n { sessionId },\n apiGatewayUrl\n );\n return data.buiProctoringIdentityFile;\n },\n });\n};\n\nexport const useBuiReviewProctoringSession = () => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiReviewProctoringSessionInput }) => {\n const data = await wspaceFetch<{ buiReviewProctoringSession: BuiProctoringSession }>(\n `mutation BuiReviewProctoringSession($id: ID!, $input: BuiReviewProctoringSessionInput!) {\n buiReviewProctoringSession(id: $id, input: $input) { ${PROCTORING_SESSION_FIELDS} }\n }`,\n args,\n apiGatewayUrl\n );\n return data.buiReviewProctoringSession;\n },\n onSuccess: () => invalidateAfterWrite(qc, PROCTORING_KEY.queue),\n });\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiTags.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiTags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAOH,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;CACb;AAiBD,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,GAAG,SAAS,uFAmBhF,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,6EAiB5C,CAAC;AAEF,4CAA4C;AAC5C,eAAO,MAAM,eAAe,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM;QAKZ,MAAM;0BAW7D,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,yBAAyB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,uFAgB7E,CAAC;AA+BF,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM;QAalB,MAAM;0BAkBhE,CAAC"}
1
+ {"version":3,"file":"useBuiTags.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/hooks/useBuiTags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAQH,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;CACb;AAiBD,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,GAAG,SAAS,uFAmBhF,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,6EAiB5C,CAAC;AAEF,4CAA4C;AAC5C,eAAO,MAAM,eAAe,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM;QAKZ,MAAM;0BAW7D,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,yBAAyB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,uFAgB7E,CAAC;AA+BF,2EAA2E;AAC3E,eAAO,MAAM,wBAAwB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM;QAalB,MAAM;0BAkBhE,CAAC"}
@@ -1,89 +1,90 @@
1
1
  import { useBuildmyiq as e } from "../context/BuildmyiqContext.js";
2
2
  import "../context/index.js";
3
- import { wspaceFetch as t } from "./useBuiFiles.js";
4
- import { useMutation as n, useQuery as r, useQueryClient as i } from "@tanstack/react-query";
3
+ import { invalidateAfterWrite as t } from "./invalidateAfterWrite.js";
4
+ import { wspaceFetch as n } from "./useBuiFiles.js";
5
+ import { useMutation as r, useQuery as i, useQueryClient as a } from "@tanstack/react-query";
5
6
  //#region src/buildmyiq/hooks/useBuiTags.ts
6
- var a = "buildmyiq", o = (e, t) => [
7
+ var o = "buildmyiq", s = (e, t) => [
7
8
  "buildmyiq",
8
9
  "entity-tags",
9
10
  e,
10
11
  t
11
- ], s = "id label color key", c = (e) => e.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "tag", l = (n, i) => {
12
+ ], c = "id label color key", l = (e) => e.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "tag", u = (t, r) => {
12
13
  let { apiGatewayUrl: a } = e();
13
- return r({
14
- queryKey: o(n, i ?? ""),
15
- enabled: !!i,
16
- queryFn: async () => (await t(`query BuiGetEntityTags($entityType: String!, $entityId: ID!) {
14
+ return i({
15
+ queryKey: s(t, r ?? ""),
16
+ enabled: !!r,
17
+ queryFn: async () => (await n(`query BuiGetEntityTags($entityType: String!, $entityId: ID!) {
17
18
  getEntityTags(entityType: $entityType, entityId: $entityId) {
18
19
  id
19
- tag { ${s} }
20
+ tag { ${c} }
20
21
  }
21
22
  }`, {
22
- entityType: n,
23
- entityId: i
23
+ entityType: t,
24
+ entityId: r
24
25
  }, a)).getEntityTags
25
26
  });
26
- }, u = (n) => {
27
- let { apiGatewayUrl: i } = e(), a = n.trim();
28
- return r({
27
+ }, d = (t) => {
28
+ let { apiGatewayUrl: r } = e(), a = t.trim();
29
+ return i({
29
30
  queryKey: [
30
31
  "buildmyiq",
31
32
  "tag-search",
32
33
  a
33
34
  ],
34
35
  enabled: a.length > 0,
35
- queryFn: async () => (await t(`query BuiSearchTags($query: String!, $limit: Int) {
36
- searchTags(query: $query, limit: $limit) { ${s} }
36
+ queryFn: async () => (await n(`query BuiSearchTags($query: String!, $limit: Int) {
37
+ searchTags(query: $query, limit: $limit) { ${c} }
37
38
  }`, {
38
39
  query: a,
39
40
  limit: 8
40
- }, i)).searchTags
41
+ }, r)).searchTags
41
42
  });
42
- }, d = (r, a) => {
43
- let { apiGatewayUrl: s } = e(), c = i();
44
- return n({
45
- mutationFn: async (e) => (await t("mutation BuiAssignTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }", { input: {
46
- entityType: r,
47
- entityId: a,
43
+ }, f = (i, o) => {
44
+ let { apiGatewayUrl: c } = e(), l = a();
45
+ return r({
46
+ mutationFn: async (e) => (await n("mutation BuiAssignTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }", { input: {
47
+ entityType: i,
48
+ entityId: o,
48
49
  tagId: e
49
- } }, s)).assignTag,
50
- onSuccess: () => c.invalidateQueries({ queryKey: o(r, a) })
50
+ } }, c)).assignTag,
51
+ onSuccess: () => t(l, s(i, o))
51
52
  });
52
- }, f = (r, a) => {
53
- let { apiGatewayUrl: s } = e(), c = i();
54
- return n({
55
- mutationFn: async (e) => (await t("mutation BuiRemoveTagAssignment($id: ID!) {\n removeTagAssignment(id: $id)\n }", { id: e }, s)).removeTagAssignment,
56
- onSuccess: () => c.invalidateQueries({ queryKey: o(r, a) })
53
+ }, p = (i, o) => {
54
+ let { apiGatewayUrl: c } = e(), l = a();
55
+ return r({
56
+ mutationFn: async (e) => (await n("mutation BuiRemoveTagAssignment($id: ID!) {\n removeTagAssignment(id: $id)\n }", { id: e }, c)).removeTagAssignment,
57
+ onSuccess: () => t(l, s(i, o))
57
58
  });
58
- }, p = async (e) => {
59
- let n = await t("query BuiGetVocabulary($key: String!) {\n getVocabularyByKey(key: $key) { id }\n }", { key: a }, e);
60
- return n.getVocabularyByKey?.id ? n.getVocabularyByKey.id : (await t("mutation BuiCreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) { id }\n }", { input: {
61
- key: a,
59
+ }, m = async (e) => {
60
+ let t = await n("query BuiGetVocabulary($key: String!) {\n getVocabularyByKey(key: $key) { id }\n }", { key: o }, e);
61
+ return t.getVocabularyByKey?.id ? t.getVocabularyByKey.id : (await n("mutation BuiCreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) { id }\n }", { input: {
62
+ key: o,
62
63
  name: "Buildmyiq",
63
64
  description: "Tags for Buildmyiq sites, species and other records.",
64
65
  allowCustomTags: !0
65
66
  } }, e)).createVocabulary.id;
66
- }, m = (r, a) => {
67
- let { apiGatewayUrl: s } = e(), l = i();
68
- return n({
67
+ }, h = (i, o) => {
68
+ let { apiGatewayUrl: c } = e(), u = a();
69
+ return r({
69
70
  mutationFn: async (e) => {
70
- let n = e.trim();
71
- if (!n) throw Error("A tag label is required.");
72
- let i = await t("mutation BuiCreateTag($input: CreateTagInput!) {\n createTag(input: $input) { id }\n }", { input: {
73
- vocabularyId: await p(s),
74
- key: `${c(n)}-${Math.random().toString(36).slice(2, 6)}`,
75
- label: n
76
- } }, s);
77
- return await t("mutation BuiAssignCreatedTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }", { input: {
78
- entityType: r,
79
- entityId: a,
80
- tagId: i.createTag.id
81
- } }, s), i.createTag;
71
+ let t = e.trim();
72
+ if (!t) throw Error("A tag label is required.");
73
+ let r = await n("mutation BuiCreateTag($input: CreateTagInput!) {\n createTag(input: $input) { id }\n }", { input: {
74
+ vocabularyId: await m(c),
75
+ key: `${l(t)}-${Math.random().toString(36).slice(2, 6)}`,
76
+ label: t
77
+ } }, c);
78
+ return await n("mutation BuiAssignCreatedTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }", { input: {
79
+ entityType: i,
80
+ entityId: o,
81
+ tagId: r.createTag.id
82
+ } }, c), r.createTag;
82
83
  },
83
- onSuccess: () => l.invalidateQueries({ queryKey: o(r, a) })
84
+ onSuccess: () => t(u, s(i, o))
84
85
  });
85
86
  };
86
87
  //#endregion
87
- export { d as useEcoAssignTag, m as useEcoCreateAndAssignTag, l as useEcoEntityTags, f as useEcoRemoveTagAssignment, u as useEcoTagSearch };
88
+ export { f as useEcoAssignTag, h as useEcoCreateAndAssignTag, u as useEcoEntityTags, p as useEcoRemoveTagAssignment, d as useEcoTagSearch };
88
89
 
89
90
  //# sourceMappingURL=useBuiTags.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useBuiTags.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiTags.ts"],"sourcesContent":["/**\n * Buildmyiq — wspace-tags-svc UniversalTag REUSE hooks (Integration C).\n * @module buildmyiq/hooks/useBuiTags\n *\n * The buildmyiq svc does NOT expose a tags field, so we drive the platform tag\n * assignment store directly (approach (b) in the reuse audit) — no buildmyiq-svc\n * change. All operations are composed into the WORKSPACE supergraph:\n * - `getEntityTags(entityType, entityId)` → current assignments (display)\n * - `searchTags(query)` → pick an existing tag\n * - `createTag(input)` → create a new tag (needs a vocabulary)\n * - `assignTag(input)` → link tag ↔ entity\n * - `removeTagAssignment(id)` → unlink\n *\n * `createTag` requires a `vocabularyId`; we resolve a stable, workspace-scoped\n * \"buildmyiq\" vocabulary (`getVocabularyByKey`, creating it on first use).\n *\n * Transport reuses `wspaceFetch` from `useBuiFiles`. Lives in a separate file;\n * `useBuildmyiqApi.ts` is untouched.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useBuildmyiq } from '../context';\nimport { wspaceFetch } from './useBuiFiles';\n\nexport interface BuiTag {\n id: string;\n label: string;\n color: string | null;\n key: string;\n}\n\nexport interface BuiTagAssignment {\n id: string;\n tag: BuiTag;\n}\n\nconst BUI_VOCABULARY_KEY = 'buildmyiq';\n\nconst entityTagsKey = (entityType: string, entityId: string) =>\n ['buildmyiq', 'entity-tags', entityType, entityId] as const;\n\nconst TAG_FIELDS = 'id label color key';\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 48) || 'tag';\n\n/** Current tag assignments for an Buildmyiq entity. */\nexport const useEcoEntityTags = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: entityTagsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{ getEntityTags: BuiTagAssignment[] }>(\n `query BuiGetEntityTags($entityType: String!, $entityId: ID!) {\n getEntityTags(entityType: $entityType, entityId: $entityId) {\n id\n tag { ${TAG_FIELDS} }\n }\n }`,\n { entityType, entityId },\n apiGatewayUrl\n );\n return data.getEntityTags;\n },\n });\n};\n\n/** Search existing tags (enabled only for a non-empty query). */\nexport const useEcoTagSearch = (query: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const trimmed = query.trim();\n return useQuery({\n queryKey: ['buildmyiq', 'tag-search', trimmed],\n enabled: trimmed.length > 0,\n queryFn: async () => {\n const data = await wspaceFetch<{ searchTags: BuiTag[] }>(\n `query BuiSearchTags($query: String!, $limit: Int) {\n searchTags(query: $query, limit: $limit) { ${TAG_FIELDS} }\n }`,\n { query: trimmed, limit: 8 },\n apiGatewayUrl\n );\n return data.searchTags;\n },\n });\n};\n\n/** Assign an existing tag to the entity. */\nexport const useEcoAssignTag = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (tagId: string) => {\n const data = await wspaceFetch<{ assignTag: { id: string } }>(\n `mutation BuiAssignTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }`,\n { input: { entityType, entityId, tagId } },\n apiGatewayUrl\n );\n return data.assignTag;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: entityTagsKey(entityType, entityId) }),\n });\n};\n\n/** Remove a tag assignment by its assignment id. */\nexport const useEcoRemoveTagAssignment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (assignmentId: string) => {\n const data = await wspaceFetch<{ removeTagAssignment: boolean }>(\n `mutation BuiRemoveTagAssignment($id: ID!) {\n removeTagAssignment(id: $id)\n }`,\n { id: assignmentId },\n apiGatewayUrl\n );\n return data.removeTagAssignment;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: entityTagsKey(entityType, entityId) }),\n });\n};\n\n/** Resolve (creating on first use) the workspace-scoped \"buildmyiq\" vocabulary. */\nconst resolveEcoVocabularyId = async (baseUrl?: string): Promise<string> => {\n const existing = await wspaceFetch<{ getVocabularyByKey: { id: string } | null }>(\n `query BuiGetVocabulary($key: String!) {\n getVocabularyByKey(key: $key) { id }\n }`,\n { key: BUI_VOCABULARY_KEY },\n baseUrl\n );\n if (existing.getVocabularyByKey?.id) {\n return existing.getVocabularyByKey.id;\n }\n const created = await wspaceFetch<{ createVocabulary: { id: string } }>(\n `mutation BuiCreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) { id }\n }`,\n {\n input: {\n key: BUI_VOCABULARY_KEY,\n name: 'Buildmyiq',\n description: 'Tags for Buildmyiq sites, species and other records.',\n allowCustomTags: true,\n },\n },\n baseUrl\n );\n return created.createVocabulary.id;\n};\n\n/** Create a new tag from a free-text label and assign it to the entity. */\nexport const useEcoCreateAndAssignTag = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (label: string) => {\n const trimmed = label.trim();\n if (!trimmed) {\n throw new Error('A tag label is required.');\n }\n const vocabularyId = await resolveEcoVocabularyId(apiGatewayUrl);\n // A unique key avoids collisions with existing tags in the vocabulary;\n // the (non-unique) label is what users see.\n const key = `${slugify(trimmed)}-${Math.random().toString(36).slice(2, 6)}`;\n const created = await wspaceFetch<{ createTag: { id: string } }>(\n `mutation BuiCreateTag($input: CreateTagInput!) {\n createTag(input: $input) { id }\n }`,\n { input: { vocabularyId, key, label: trimmed } },\n apiGatewayUrl\n );\n await wspaceFetch<{ assignTag: { id: string } }>(\n `mutation BuiAssignCreatedTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }`,\n { input: { entityType, entityId, tagId: created.createTag.id } },\n apiGatewayUrl\n );\n return created.createTag;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: entityTagsKey(entityType, entityId) }),\n });\n};\n"],"mappings":";;;;;AAqCA,IAAM,IAAqB,aAErB,KAAiB,GAAoB,MACzC;CAAC;CAAa;CAAe;CAAY;AAAQ,GAE7C,IAAa,sBAEb,KAAW,MACf,EACG,YAAY,EACZ,KAAK,EACL,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE,KAAK,OAGR,KAAoB,GAAoB,MAAiC;CACpF,IAAM,EAAE,qBAAkB,EAAa;CACvC,OAAO,EAAS;EACd,UAAU,EAAc,GAAY,KAAY,EAAE;EAClD,SAAS,EAAQ;EACjB,SAAS,aAWA,MAVY,EACjB;;;oBAGY,EAAW;;YAGvB;GAAE;GAAY;EAAS,GACvB,CACF,GACY;CAEhB,CAAC;AACH,GAGa,KAAmB,MAAkB;CAChD,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAU,EAAM,KAAK;CAC3B,OAAO,EAAS;EACd,UAAU;GAAC;GAAa;GAAc;EAAO;EAC7C,SAAS,EAAQ,SAAS;EAC1B,SAAS,aAQA,MAPY,EACjB;uDAC+C,EAAW;YAE1D;GAAE,OAAO;GAAS,OAAO;EAAE,GAC3B,CACF,GACY;CAEhB,CAAC;AACH,GAGa,KAAmB,GAAoB,MAAqB;CACvE,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB,0GAGA,EAAE,OAAO;GAAE;GAAY;GAAU;EAAM,EAAE,GACzC,CACF,GACY;EAEd,iBAAiB,EAAG,kBAAkB,EAAE,UAAU,EAAc,GAAY,CAAQ,EAAE,CAAC;CACzF,CAAC;AACH,GAGa,KAA6B,GAAoB,MAAqB;CACjF,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB,kGAGA,EAAE,IAAI,EAAa,GACnB,CACF,GACY;EAEd,iBAAiB,EAAG,kBAAkB,EAAE,UAAU,EAAc,GAAY,CAAQ,EAAE,CAAC;CACzF,CAAC;AACH,GAGM,IAAyB,OAAO,MAAsC;CAC1E,IAAM,IAAW,MAAM,EACrB,8FAGA,EAAE,KAAK,EAAmB,GAC1B,CACF;CAkBA,OAjBI,EAAS,oBAAoB,KACxB,EAAS,mBAAmB,MAgB9B,MAde,EACpB,uHAGA,EACE,OAAO;EACL,KAAK;EACL,MAAM;EACN,aAAa;EACb,iBAAiB;CACnB,EACF,GACA,CACF,GACe,iBAAiB;AAClC,GAGa,KAA4B,GAAoB,MAAqB;CAChF,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,MAAkB;GACnC,IAAM,IAAU,EAAM,KAAK;GAC3B,IAAI,CAAC,GACH,MAAU,MAAM,0BAA0B;GAM5C,IAAM,IAAU,MAAM,EACpB,0GAGA,EAAE,OAAO;IAAE,cAAA,MARc,EAAuB,CAAa;IAQpC,KAAA,GALZ,EAAQ,CAAO,EAAE,GAAG,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC;IAKxC,OAAO;GAAQ,EAAE,GAC/C,CACF;GAQA,OAPA,MAAM,EACJ,iHAGA,EAAE,OAAO;IAAE;IAAY;IAAU,OAAO,EAAQ,UAAU;GAAG,EAAE,GAC/D,CACF,GACO,EAAQ;EACjB;EACA,iBAAiB,EAAG,kBAAkB,EAAE,UAAU,EAAc,GAAY,CAAQ,EAAE,CAAC;CACzF,CAAC;AACH"}
1
+ {"version":3,"file":"useBuiTags.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuiTags.ts"],"sourcesContent":["/**\n * Buildmyiq — wspace-tags-svc UniversalTag REUSE hooks (Integration C).\n * @module buildmyiq/hooks/useBuiTags\n *\n * The buildmyiq svc does NOT expose a tags field, so we drive the platform tag\n * assignment store directly (approach (b) in the reuse audit) — no buildmyiq-svc\n * change. All operations are composed into the WORKSPACE supergraph:\n * - `getEntityTags(entityType, entityId)` → current assignments (display)\n * - `searchTags(query)` → pick an existing tag\n * - `createTag(input)` → create a new tag (needs a vocabulary)\n * - `assignTag(input)` → link tag ↔ entity\n * - `removeTagAssignment(id)` → unlink\n *\n * `createTag` requires a `vocabularyId`; we resolve a stable, workspace-scoped\n * \"buildmyiq\" vocabulary (`getVocabularyByKey`, creating it on first use).\n *\n * Transport reuses `wspaceFetch` from `useBuiFiles`. Lives in a separate file;\n * `useBuildmyiqApi.ts` is untouched.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { invalidateAfterWrite } from './invalidateAfterWrite';\n\nimport { useBuildmyiq } from '../context';\nimport { wspaceFetch } from './useBuiFiles';\n\nexport interface BuiTag {\n id: string;\n label: string;\n color: string | null;\n key: string;\n}\n\nexport interface BuiTagAssignment {\n id: string;\n tag: BuiTag;\n}\n\nconst BUI_VOCABULARY_KEY = 'buildmyiq';\n\nconst entityTagsKey = (entityType: string, entityId: string) =>\n ['buildmyiq', 'entity-tags', entityType, entityId] as const;\n\nconst TAG_FIELDS = 'id label color key';\n\nconst slugify = (value: string): string =>\n value\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 48) || 'tag';\n\n/** Current tag assignments for an Buildmyiq entity. */\nexport const useEcoEntityTags = (entityType: string, entityId: string | undefined) => {\n const { apiGatewayUrl } = useBuildmyiq();\n return useQuery({\n queryKey: entityTagsKey(entityType, entityId ?? ''),\n enabled: Boolean(entityId),\n queryFn: async () => {\n const data = await wspaceFetch<{ getEntityTags: BuiTagAssignment[] }>(\n `query BuiGetEntityTags($entityType: String!, $entityId: ID!) {\n getEntityTags(entityType: $entityType, entityId: $entityId) {\n id\n tag { ${TAG_FIELDS} }\n }\n }`,\n { entityType, entityId },\n apiGatewayUrl\n );\n return data.getEntityTags;\n },\n });\n};\n\n/** Search existing tags (enabled only for a non-empty query). */\nexport const useEcoTagSearch = (query: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const trimmed = query.trim();\n return useQuery({\n queryKey: ['buildmyiq', 'tag-search', trimmed],\n enabled: trimmed.length > 0,\n queryFn: async () => {\n const data = await wspaceFetch<{ searchTags: BuiTag[] }>(\n `query BuiSearchTags($query: String!, $limit: Int) {\n searchTags(query: $query, limit: $limit) { ${TAG_FIELDS} }\n }`,\n { query: trimmed, limit: 8 },\n apiGatewayUrl\n );\n return data.searchTags;\n },\n });\n};\n\n/** Assign an existing tag to the entity. */\nexport const useEcoAssignTag = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (tagId: string) => {\n const data = await wspaceFetch<{ assignTag: { id: string } }>(\n `mutation BuiAssignTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }`,\n { input: { entityType, entityId, tagId } },\n apiGatewayUrl\n );\n return data.assignTag;\n },\n onSuccess: () => invalidateAfterWrite(qc, entityTagsKey(entityType, entityId)),\n });\n};\n\n/** Remove a tag assignment by its assignment id. */\nexport const useEcoRemoveTagAssignment = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (assignmentId: string) => {\n const data = await wspaceFetch<{ removeTagAssignment: boolean }>(\n `mutation BuiRemoveTagAssignment($id: ID!) {\n removeTagAssignment(id: $id)\n }`,\n { id: assignmentId },\n apiGatewayUrl\n );\n return data.removeTagAssignment;\n },\n onSuccess: () => invalidateAfterWrite(qc, entityTagsKey(entityType, entityId)),\n });\n};\n\n/** Resolve (creating on first use) the workspace-scoped \"buildmyiq\" vocabulary. */\nconst resolveEcoVocabularyId = async (baseUrl?: string): Promise<string> => {\n const existing = await wspaceFetch<{ getVocabularyByKey: { id: string } | null }>(\n `query BuiGetVocabulary($key: String!) {\n getVocabularyByKey(key: $key) { id }\n }`,\n { key: BUI_VOCABULARY_KEY },\n baseUrl\n );\n if (existing.getVocabularyByKey?.id) {\n return existing.getVocabularyByKey.id;\n }\n const created = await wspaceFetch<{ createVocabulary: { id: string } }>(\n `mutation BuiCreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) { id }\n }`,\n {\n input: {\n key: BUI_VOCABULARY_KEY,\n name: 'Buildmyiq',\n description: 'Tags for Buildmyiq sites, species and other records.',\n allowCustomTags: true,\n },\n },\n baseUrl\n );\n return created.createVocabulary.id;\n};\n\n/** Create a new tag from a free-text label and assign it to the entity. */\nexport const useEcoCreateAndAssignTag = (entityType: string, entityId: string) => {\n const { apiGatewayUrl } = useBuildmyiq();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (label: string) => {\n const trimmed = label.trim();\n if (!trimmed) {\n throw new Error('A tag label is required.');\n }\n const vocabularyId = await resolveEcoVocabularyId(apiGatewayUrl);\n // A unique key avoids collisions with existing tags in the vocabulary;\n // the (non-unique) label is what users see.\n const key = `${slugify(trimmed)}-${Math.random().toString(36).slice(2, 6)}`;\n const created = await wspaceFetch<{ createTag: { id: string } }>(\n `mutation BuiCreateTag($input: CreateTagInput!) {\n createTag(input: $input) { id }\n }`,\n { input: { vocabularyId, key, label: trimmed } },\n apiGatewayUrl\n );\n await wspaceFetch<{ assignTag: { id: string } }>(\n `mutation BuiAssignCreatedTag($input: AssignTagInput!) {\n assignTag(input: $input) { id }\n }`,\n { input: { entityType, entityId, tagId: created.createTag.id } },\n apiGatewayUrl\n );\n return created.createTag;\n },\n onSuccess: () => invalidateAfterWrite(qc, entityTagsKey(entityType, entityId)),\n });\n};\n"],"mappings":";;;;;;AAsCA,IAAM,IAAqB,aAErB,KAAiB,GAAoB,MACzC;CAAC;CAAa;CAAe;CAAY;AAAQ,GAE7C,IAAa,sBAEb,KAAW,MACf,EACG,YAAY,EACZ,KAAK,EACL,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE,KAAK,OAGR,KAAoB,GAAoB,MAAiC;CACpF,IAAM,EAAE,qBAAkB,EAAa;CACvC,OAAO,EAAS;EACd,UAAU,EAAc,GAAY,KAAY,EAAE;EAClD,SAAS,EAAQ;EACjB,SAAS,aAWA,MAVY,EACjB;;;oBAGY,EAAW;;YAGvB;GAAE;GAAY;EAAS,GACvB,CACF,GACY;CAEhB,CAAC;AACH,GAGa,KAAmB,MAAkB;CAChD,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAU,EAAM,KAAK;CAC3B,OAAO,EAAS;EACd,UAAU;GAAC;GAAa;GAAc;EAAO;EAC7C,SAAS,EAAQ,SAAS;EAC1B,SAAS,aAQA,MAPY,EACjB;uDAC+C,EAAW;YAE1D;GAAE,OAAO;GAAS,OAAO;EAAE,GAC3B,CACF,GACY;CAEhB,CAAC;AACH,GAGa,KAAmB,GAAoB,MAAqB;CACvE,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB,0GAGA,EAAE,OAAO;GAAE;GAAY;GAAU;EAAM,EAAE,GACzC,CACF,GACY;EAEd,iBAAiB,EAAqB,GAAI,EAAc,GAAY,CAAQ,CAAC;CAC/E,CAAC;AACH,GAGa,KAA6B,GAAoB,MAAqB;CACjF,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,OAQV,MAPY,EACjB,kGAGA,EAAE,IAAI,EAAa,GACnB,CACF,GACY;EAEd,iBAAiB,EAAqB,GAAI,EAAc,GAAY,CAAQ,CAAC;CAC/E,CAAC;AACH,GAGM,IAAyB,OAAO,MAAsC;CAC1E,IAAM,IAAW,MAAM,EACrB,8FAGA,EAAE,KAAK,EAAmB,GAC1B,CACF;CAkBA,OAjBI,EAAS,oBAAoB,KACxB,EAAS,mBAAmB,MAgB9B,MAde,EACpB,uHAGA,EACE,OAAO;EACL,KAAK;EACL,MAAM;EACN,aAAa;EACb,iBAAiB;CACnB,EACF,GACA,CACF,GACe,iBAAiB;AAClC,GAGa,KAA4B,GAAoB,MAAqB;CAChF,IAAM,EAAE,qBAAkB,EAAa,GACjC,IAAK,EAAe;CAC1B,OAAO,EAAY;EACjB,YAAY,OAAO,MAAkB;GACnC,IAAM,IAAU,EAAM,KAAK;GAC3B,IAAI,CAAC,GACH,MAAU,MAAM,0BAA0B;GAM5C,IAAM,IAAU,MAAM,EACpB,0GAGA,EAAE,OAAO;IAAE,cAAA,MARc,EAAuB,CAAa;IAQpC,KAAA,GALZ,EAAQ,CAAO,EAAE,GAAG,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC;IAKxC,OAAO;GAAQ,EAAE,GAC/C,CACF;GAQA,OAPA,MAAM,EACJ,iHAGA,EAAE,OAAO;IAAE;IAAY;IAAU,OAAO,EAAQ,UAAU;GAAG,EAAE,GAC/D,CACF,GACO,EAAQ;EACjB;EACA,iBAAiB,EAAqB,GAAI,EAAc,GAAY,CAAQ,CAAC;CAC/E,CAAC;AACH"}