@instructure/platform-modules 0.1.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/README.md +75 -0
- package/dist/adapters/canvasGraphql/index.d.ts +54 -0
- package/dist/adapters/canvasGraphql/index.d.ts.map +1 -0
- package/dist/adapters/canvasGraphql/normalize.d.ts +345 -0
- package/dist/adapters/canvasGraphql/normalize.d.ts.map +1 -0
- package/dist/adapters/canvasGraphql/queries.d.ts +69 -0
- package/dist/adapters/canvasGraphql/queries.d.ts.map +1 -0
- package/dist/adapters/inMemory.d.ts +63 -0
- package/dist/adapters/inMemory.d.ts.map +1 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/limits.d.ts +86 -0
- package/dist/adapters/limits.d.ts.map +1 -0
- package/dist/adapters/types.d.ts +87 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/invalidate.d.ts +52 -0
- package/dist/cache/invalidate.d.ts.map +1 -0
- package/dist/cache/keys.d.ts +86 -0
- package/dist/cache/keys.d.ts.map +1 -0
- package/dist/components/FilterChips.d.ts +19 -0
- package/dist/components/FilterChips.d.ts.map +1 -0
- package/dist/components/ModuleCard.d.ts +32 -0
- package/dist/components/ModuleCard.d.ts.map +1 -0
- package/dist/components/ModuleItemRow.d.ts +17 -0
- package/dist/components/ModuleItemRow.d.ts.map +1 -0
- package/dist/components/ModulesList.d.ts +69 -0
- package/dist/components/ModulesList.d.ts.map +1 -0
- package/dist/components/ModulesList.stories.d.ts +138 -0
- package/dist/components/ModulesList.stories.d.ts.map +1 -0
- package/dist/components/StatusPill.d.ts +18 -0
- package/dist/components/StatusPill.d.ts.map +1 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/labels.d.ts +89 -0
- package/dist/components/labels.d.ts.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/registry.d.ts +38 -0
- package/dist/config/registry.d.ts.map +1 -0
- package/dist/config/schemas.d.ts +16 -0
- package/dist/config/schemas.d.ts.map +1 -0
- package/dist/fixtures/generate.d.ts +48 -0
- package/dist/fixtures/generate.d.ts.map +1 -0
- package/dist/fixtures/index.d.ts +3 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/scenarios.d.ts +42 -0
- package/dist/fixtures/scenarios.d.ts.map +1 -0
- package/dist/hooks/constants.d.ts +49 -0
- package/dist/hooks/constants.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/queryOptions.d.ts +58 -0
- package/dist/hooks/queryOptions.d.ts.map +1 -0
- package/dist/hooks/useItemsMode.d.ts +114 -0
- package/dist/hooks/useItemsMode.d.ts.map +1 -0
- package/dist/hooks/useModuleItemQueries.d.ts +56 -0
- package/dist/hooks/useModuleItemQueries.d.ts.map +1 -0
- package/dist/hooks/useModuleItems.d.ts +41 -0
- package/dist/hooks/useModuleItems.d.ts.map +1 -0
- package/dist/hooks/useModules.d.ts +50 -0
- package/dist/hooks/useModules.d.ts.map +1 -0
- package/dist/hooks/useModulesPageData.d.ts +78 -0
- package/dist/hooks/useModulesPageData.d.ts.map +1 -0
- package/dist/i18n.d.ts +92 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1966 -0
- package/dist/types/domain.d.ts +147 -0
- package/dist/types/domain.d.ts.map +1 -0
- package/dist/types/enums.d.ts +93 -0
- package/dist/types/enums.d.ts.map +1 -0
- package/dist/types/extensions.d.ts +48 -0
- package/dist/types/extensions.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/schemas.d.ts +38 -0
- package/dist/types/schemas.d.ts.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transport limits both adapters are bound by, defined once.
|
|
3
|
+
*
|
|
4
|
+
* ## What "inlining" means
|
|
5
|
+
*
|
|
6
|
+
* A modules page needs two things: the list of modules, and the items inside each
|
|
7
|
+
* module. There are two ways to get them.
|
|
8
|
+
*
|
|
9
|
+
* **Inlined** — ask for the items *in the same request as the module list*, nested
|
|
10
|
+
* under each module. One round trip for the whole page. On REST that is
|
|
11
|
+
* `?include[]=items`; on GraphQL it is the nested `moduleItemsConnection`. Cheap
|
|
12
|
+
* in requests, and the response grows with the total number of items in the
|
|
13
|
+
* course.
|
|
14
|
+
*
|
|
15
|
+
* **On demand** — ask for the module list alone, then fetch a module's items in a
|
|
16
|
+
* separate request when the user expands it. The first paint costs one request
|
|
17
|
+
* whatever the course size, and each module the user opens costs one more.
|
|
18
|
+
*
|
|
19
|
+
* Neither is right in general: inlining wins on a small course, on-demand wins on
|
|
20
|
+
* a large one, which is why `itemsMode: 'auto'` measures the course and picks.
|
|
21
|
+
* "Inlined" always describes *where the items came from*, never whether they were
|
|
22
|
+
* fetched at all — a module with `items: undefined` was not inlined, and a module
|
|
23
|
+
* with `items: []` was inlined and is genuinely empty.
|
|
24
|
+
*
|
|
25
|
+
* ## Why the ceiling is 100
|
|
26
|
+
*
|
|
27
|
+
* Above this many items in a single module, inlining stops being worthwhile — and
|
|
28
|
+
* on one transport it stops being possible. The two differ, and conflating them is
|
|
29
|
+
* easy:
|
|
30
|
+
*
|
|
31
|
+
* - **REST refuses outright.** `lib/api/v1/context_module.rb` omits the `items`
|
|
32
|
+
* key entirely for any module with more than `Api::MAX_PER_PAGE` (100) items.
|
|
33
|
+
* Server-side; a client cannot opt out.
|
|
34
|
+
* - **GraphQL has no per-module rule.** `moduleItemsConnection` is an ordinary
|
|
35
|
+
* Relay connection, and nothing in `app/graphql/types/module_type.rb` caps it.
|
|
36
|
+
*
|
|
37
|
+
* It would be wrong to conclude GraphQL will inline anything you ask for, though.
|
|
38
|
+
* Canvas runs a query-complexity analyser, and it charges for the *shape of the
|
|
39
|
+
* request* rather than the size of the result: `first: 100, itemsFirst: 100`
|
|
40
|
+
* scores ~1.5M against a 375,000 ceiling and is rejected outright, even on a
|
|
41
|
+
* course holding thirteen items. So the ceiling is real on both transports — just
|
|
42
|
+
* enforced differently, and on different things. See `INLINE_QUERY_NODE_BUDGET`
|
|
43
|
+
* for the measurements.
|
|
44
|
+
*
|
|
45
|
+
* The GraphQL adapter therefore applies this same 100 as a deliberate *policy*,
|
|
46
|
+
* so both transports fall back to a per-module fetch at the same point and the
|
|
47
|
+
* hooks see one behaviour rather than two.
|
|
48
|
+
*/
|
|
49
|
+
export declare const INLINE_ITEM_LIMIT = 100;
|
|
50
|
+
/**
|
|
51
|
+
* Ceiling on `pageSize × inlineItemsFirst` for a single inline request.
|
|
52
|
+
*
|
|
53
|
+
* Canvas runs a GraphQL complexity analyser and rejects anything over 375,000.
|
|
54
|
+
* Complexity is charged on the **shape of the request, not the size of the
|
|
55
|
+
* result** — so a course with 13 items is rejected just as readily as one with
|
|
56
|
+
* 13,000 if the query asks for `first: 100, itemsFirst: 100`.
|
|
57
|
+
*
|
|
58
|
+
* Measured against a real Canvas (`/api/graphql`, this document, Aug 2026):
|
|
59
|
+
*
|
|
60
|
+
* first × itemsFirst complexity verdict
|
|
61
|
+
* 100 × 100 1,512,106 rejected
|
|
62
|
+
* 100 × 50 757,106 rejected
|
|
63
|
+
* 100 × 25 379,606 rejected, barely
|
|
64
|
+
* 100 × 20 — accepted
|
|
65
|
+
* 25 × 100 378,031 rejected, barely
|
|
66
|
+
* 10 × 100 — accepted
|
|
67
|
+
*
|
|
68
|
+
* That is ~151 per requested item node, so the budget is the largest round
|
|
69
|
+
* product comfortably under 375,000/151 ≈ 2,483. Kept as a product rather than
|
|
70
|
+
* two separate caps because the analyser multiplies: 100×20 and 10×100 both fit,
|
|
71
|
+
* 25×100 does not.
|
|
72
|
+
*
|
|
73
|
+
* If a page genuinely needs more than this, it is not an inline page — the
|
|
74
|
+
* request is split and the items fetched on demand, which is what
|
|
75
|
+
* `resolveItemsMode` is for.
|
|
76
|
+
*
|
|
77
|
+
* This is a GraphQL fact, and it lives here rather than in the GraphQL adapter
|
|
78
|
+
* for two reasons. It is the *default* the hooks assume when an adapter declines
|
|
79
|
+
* to declare its own (`ModulesDataAdapter.inlineNodeBudget`), and importing it
|
|
80
|
+
* from `adapters/canvasGraphql` was making the transport-agnostic `hooks/` layer
|
|
81
|
+
* depend on one specific transport — which also dragged `queries.ts` and its two
|
|
82
|
+
* module-scope `gql` parses into any bundle that only wanted the in-memory
|
|
83
|
+
* adapter.
|
|
84
|
+
*/
|
|
85
|
+
export declare const INLINE_QUERY_NODE_BUDGET = 2000;
|
|
86
|
+
//# sourceMappingURL=limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/adapters/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAA;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAA"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ModuleItemsPage, ModulesPage } from '../types/domain';
|
|
2
|
+
/**
|
|
3
|
+
* The seam between this package and a host's transport.
|
|
4
|
+
*
|
|
5
|
+
* An adapter owns exactly three things: how a request is made, what fields it
|
|
6
|
+
* asks for, and how the response becomes the domain model. It owns nothing
|
|
7
|
+
* about caching, pagination policy, or React — those live in the hooks, so
|
|
8
|
+
* that swapping GraphQL for REST changes one file and no behaviour.
|
|
9
|
+
*
|
|
10
|
+
* Contract every adapter must honour:
|
|
11
|
+
*
|
|
12
|
+
* - **Return a real `pageInfo`.** The GraphQL adapter passes through
|
|
13
|
+
* `endCursor`; the REST adapter derives it from the `Link` header. An
|
|
14
|
+
* adapter that hardcodes `hasNextPage: false` silently truncates, which is
|
|
15
|
+
* the live bug in Canvas Career's modules path today.
|
|
16
|
+
* - **Honour `signal`.** Collapsing a module aborts its in-flight request.
|
|
17
|
+
* - **Never invent a cursor.** Pass back what the server gave you; do not
|
|
18
|
+
* synthesise offsets client-side.
|
|
19
|
+
* - **Normalise fully.** Output must satisfy the Zod schemas. No transport
|
|
20
|
+
* detail (`_id`, `quiz_lti`, snake_case) may reach a caller.
|
|
21
|
+
*/
|
|
22
|
+
export interface ModulesDataAdapter {
|
|
23
|
+
listModules(args: ListModulesArgs): Promise<ModulesPage>;
|
|
24
|
+
listModuleItems(args: ListModuleItemsArgs): Promise<ModuleItemsPage>;
|
|
25
|
+
/**
|
|
26
|
+
* The largest `modules × itemsPerModule` product this transport will accept in
|
|
27
|
+
* one inline request, or `undefined` for "no transport limit of my own".
|
|
28
|
+
*
|
|
29
|
+
* The transport declares its own ceiling rather than the hooks assuming one.
|
|
30
|
+
* Canvas GraphQL has a complexity analyser that rejects the *shape* of a large
|
|
31
|
+
* inline query (@see INLINE_QUERY_NODE_BUDGET); the in-memory double has no
|
|
32
|
+
* such constraint; REST will have a different one again, bounded by
|
|
33
|
+
* `Api::MAX_PER_PAGE` and response size rather than by query complexity.
|
|
34
|
+
*
|
|
35
|
+
* `resolveItemsMode` reads this to decide whether an inline page is deliverable.
|
|
36
|
+
* When it is `undefined` the hook falls back to the GraphQL budget, which is
|
|
37
|
+
* the conservative choice: guessing too low costs one extra request per opened
|
|
38
|
+
* module, guessing too high gets the whole query rejected.
|
|
39
|
+
*/
|
|
40
|
+
readonly inlineNodeBudget?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface ListModulesArgs {
|
|
43
|
+
courseId: string;
|
|
44
|
+
/** `null`/omitted requests the first page. */
|
|
45
|
+
cursor?: string | null;
|
|
46
|
+
pageSize: number;
|
|
47
|
+
/**
|
|
48
|
+
* Ask the server to inline each module's items in the same round trip.
|
|
49
|
+
*
|
|
50
|
+
* A request, not a guarantee. Canvas REST drops the `items` key for any module
|
|
51
|
+
* over `Api::MAX_PER_PAGE` (100) items; the GraphQL path has no such server
|
|
52
|
+
* rule but this package applies the same ceiling on purpose, so both transports
|
|
53
|
+
* behave alike. Either way an adapter must report an accurate `itemCount` and
|
|
54
|
+
* leave `items` **undefined** for a module it did not inline, never `[]`.
|
|
55
|
+
*
|
|
56
|
+
* @see INLINE_ITEM_LIMIT in `adapters/limits.ts` for why the number is 100 and
|
|
57
|
+
* which half of it Canvas actually enforces.
|
|
58
|
+
*/
|
|
59
|
+
includeItems: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* How many items to request per module when `includeItems` is true.
|
|
62
|
+
*
|
|
63
|
+
* Omit to let the adapter use its own ceiling. Supply it when the caller has
|
|
64
|
+
* *measured* what the page needs — `useModulesPageData` does, from the `auto`
|
|
65
|
+
* probe — because a transport may charge for what the query asks for rather
|
|
66
|
+
* than for what exists. @see INLINE_QUERY_NODE_BUDGET
|
|
67
|
+
*/
|
|
68
|
+
inlineItemsFirst?: number;
|
|
69
|
+
/**
|
|
70
|
+
* How many modules to request when inlining.
|
|
71
|
+
*
|
|
72
|
+
* Omit to use `pageSize`. Supply it when the caller has measured that page 1 is
|
|
73
|
+
* the whole list, because the complexity budget is charged on
|
|
74
|
+
* `first x inlineItemsFirst` — asking for 100 modules when the course has 2 is
|
|
75
|
+
* what forces `inlineItemsFirst` to be clamped below what the page needs.
|
|
76
|
+
*/
|
|
77
|
+
inlineFirst?: number;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}
|
|
80
|
+
export interface ListModuleItemsArgs {
|
|
81
|
+
courseId: string;
|
|
82
|
+
moduleId: string;
|
|
83
|
+
cursor?: string | null;
|
|
84
|
+
pageSize: number;
|
|
85
|
+
signal?: AbortSignal;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAEnE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IACpE;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;;;;;;OAWG;IACH,YAAY,EAAE,OAAO,CAAA;IACrB;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { moduleItemsPrefix, modulesKeys, modulesListPrefix, type ModuleItemsKey, type ModuleItemsKeyOptions, type ModulesAllKey, type ModulesCourseKey, type ModulesKeyFactory, type ModulesListKey, type ModulesListKeyOptions, type ModulesView, } from './keys';
|
|
2
|
+
export { invalidateCourse, invalidateModuleItems, invalidateModuleList } from './invalidate';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,WAAW,GACjB,MAAM,QAAQ,CAAA;AACf,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { ModuleItemsKeyOptions, ModulesListKeyOptions } from './keys';
|
|
3
|
+
/**
|
|
4
|
+
* Invalidation helpers.
|
|
5
|
+
*
|
|
6
|
+
* ## What is deliberately missing
|
|
7
|
+
*
|
|
8
|
+
* There is no `invalidateOnExpand` / `invalidateOnCollapse`, and there must
|
|
9
|
+
* never be one. **Expanding or collapsing a module invalidates nothing.**
|
|
10
|
+
*
|
|
11
|
+
* Expansion is UI state. It says which rows are visible; it says nothing about
|
|
12
|
+
* whether the server's copy of the data changed. Canvas's modules page ties the
|
|
13
|
+
* two together, so every collapse invalidates the module list, every
|
|
14
|
+
* invalidation re-walks every page that has been loaded, and a course with
|
|
15
|
+
* thirty modules re-fetches the world each time a teacher tidies the screen.
|
|
16
|
+
* That is the refetch loop this package exists to not have.
|
|
17
|
+
*
|
|
18
|
+
* The equivalents here:
|
|
19
|
+
* - collapse → cancel that module's in-flight item request (an abort, not an
|
|
20
|
+
* invalidation — see `useModulesPageData`), and leave the cached pages alone
|
|
21
|
+
* - re-expand → a cache hit, no request
|
|
22
|
+
*
|
|
23
|
+
* Invalidate when the *data* changed: a mutation, a websocket nudge, an
|
|
24
|
+
* explicit user refresh. Not when a disclosure arrow moved.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* One module's items. Use after reordering, adding, or deleting items.
|
|
28
|
+
*
|
|
29
|
+
* Omit `opts` — the default — to hit every variant of that module's items, which
|
|
30
|
+
* is what a mutation wants: adding an item changes the module for the student
|
|
31
|
+
* view and the teacher view alike, at every page size any consumer rendered
|
|
32
|
+
* with. Pass `opts` only to target the exact query one hook instance holds.
|
|
33
|
+
*/
|
|
34
|
+
export declare function invalidateModuleItems(queryClient: QueryClient, courseId: string, moduleId: string, opts?: ModuleItemsKeyOptions): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* The module list for a course.
|
|
37
|
+
*
|
|
38
|
+
* Omit `opts` to hit every list variant (all page sizes, both views, inline and
|
|
39
|
+
* on-demand) — which is almost always what you want, since a mutation that adds
|
|
40
|
+
* or removes a module invalidates all of them. Pass `opts` only to target the
|
|
41
|
+
* exact query a specific hook instance is rendering.
|
|
42
|
+
*/
|
|
43
|
+
export declare function invalidateModuleList(queryClient: QueryClient, courseId: string, opts?: ModulesListKeyOptions): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Everything for a course: the list and every module's items.
|
|
46
|
+
*
|
|
47
|
+
* The blunt instrument. Reasonable after a bulk operation (publish all, import
|
|
48
|
+
* a course package); wasteful after a single-item edit, which should use
|
|
49
|
+
* `invalidateModuleItems`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function invalidateCourse(queryClient: QueryClient, courseId: string): Promise<void>;
|
|
52
|
+
//# sourceMappingURL=invalidate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invalidate.d.ts","sourceRoot":"","sources":["../../src/cache/invalidate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,QAAQ,CAAA;AAEf;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,qBAAqB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,qBAAqB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1F"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query keys for the modules page.
|
|
3
|
+
*
|
|
4
|
+
* One factory, one shape, one place to look when you need to know what a given
|
|
5
|
+
* invalidation will hit. Every key is a `readonly` tuple so TanStack Query keeps
|
|
6
|
+
* the key type narrow instead of widening it to `unknown[]`, and so a typo in a
|
|
7
|
+
* literal segment is a compile error rather than a silent cache miss.
|
|
8
|
+
*
|
|
9
|
+
* The hierarchy is deliberate — each level is a prefix of the next, which is
|
|
10
|
+
* what makes `invalidateCourse` able to reach every query for a course without
|
|
11
|
+
* enumerating them:
|
|
12
|
+
*
|
|
13
|
+
* ['platform-modules']
|
|
14
|
+
* ['platform-modules', courseId]
|
|
15
|
+
* ['platform-modules', courseId, 'list', opts]
|
|
16
|
+
* ['platform-modules', courseId, 'module', moduleId, 'items', opts]
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Which Canvas audience the page is rendered for.
|
|
20
|
+
*
|
|
21
|
+
* It is not passed to the adapter — a host configures its adapter for the
|
|
22
|
+
* current user already — but it *is* part of every key, because a teacher sees
|
|
23
|
+
* unpublished content a student does not. Two views must never share a cache
|
|
24
|
+
* entry.
|
|
25
|
+
*/
|
|
26
|
+
export type ModulesView = 'student' | 'teacher';
|
|
27
|
+
/**
|
|
28
|
+
* Everything that changes what a list request returns, and therefore has to
|
|
29
|
+
* partition the cache. `includeItems` is here because the inline and on-demand
|
|
30
|
+
* shapes of the same page are genuinely different payloads.
|
|
31
|
+
*/
|
|
32
|
+
export interface ModulesListKeyOptions {
|
|
33
|
+
view?: ModulesView;
|
|
34
|
+
pageSize?: number;
|
|
35
|
+
includeItems?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The items equivalent, and it exists for the same two reasons the list one does.
|
|
39
|
+
*
|
|
40
|
+
* `view` because `published` is a per-*item* field, so the teacher/student
|
|
41
|
+
* argument above applies to items verbatim: a teacher's page of items includes
|
|
42
|
+
* unpublished rows a student's does not.
|
|
43
|
+
*
|
|
44
|
+
* `pageSize` because these are infinite queries. The cache entry is an
|
|
45
|
+
* `InfiniteData` whose `pages` were each fetched at some size; two consumers
|
|
46
|
+
* rendering the same module with different `itemPageSize` would otherwise share
|
|
47
|
+
* one entry whose pages were fetched at mixed sizes, and neither could say what
|
|
48
|
+
* `pages[n]` contains.
|
|
49
|
+
*/
|
|
50
|
+
export interface ModuleItemsKeyOptions {
|
|
51
|
+
view?: ModulesView;
|
|
52
|
+
pageSize?: number;
|
|
53
|
+
}
|
|
54
|
+
export type ModulesAllKey = readonly ['platform-modules'];
|
|
55
|
+
export type ModulesCourseKey = readonly ['platform-modules', string];
|
|
56
|
+
export type ModulesListKey = readonly ['platform-modules', string, 'list', ModulesListKeyOptions];
|
|
57
|
+
export type ModuleItemsKey = readonly [
|
|
58
|
+
'platform-modules',
|
|
59
|
+
string,
|
|
60
|
+
'module',
|
|
61
|
+
string,
|
|
62
|
+
'items',
|
|
63
|
+
ModuleItemsKeyOptions
|
|
64
|
+
];
|
|
65
|
+
export interface ModulesKeyFactory {
|
|
66
|
+
readonly all: ModulesAllKey;
|
|
67
|
+
course(courseId: string): ModulesCourseKey;
|
|
68
|
+
list(courseId: string, opts?: ModulesListKeyOptions): ModulesListKey;
|
|
69
|
+
items(courseId: string, moduleId: string, opts?: ModuleItemsKeyOptions): ModuleItemsKey;
|
|
70
|
+
}
|
|
71
|
+
export declare const modulesKeys: ModulesKeyFactory;
|
|
72
|
+
/**
|
|
73
|
+
* The prefix shared by every list key for a course, regardless of `opts`.
|
|
74
|
+
* Used to invalidate "the module list" without knowing which page size or view
|
|
75
|
+
* the caller happened to render with.
|
|
76
|
+
*/
|
|
77
|
+
export declare function modulesListPrefix(courseId: string): readonly ['platform-modules', string, 'list'];
|
|
78
|
+
/**
|
|
79
|
+
* The prefix shared by every items key for one module, regardless of `opts`.
|
|
80
|
+
*
|
|
81
|
+
* This is the one to reach for when acting on "that module's items" without
|
|
82
|
+
* knowing which view or page size rendered them — invalidating after a mutation,
|
|
83
|
+
* or cancelling in flight requests on collapse.
|
|
84
|
+
*/
|
|
85
|
+
export declare function moduleItemsPrefix(courseId: string, moduleId: string): readonly ['platform-modules', string, 'module', string, 'items'];
|
|
86
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/cache/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,CAAA;AAE/C;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC,kBAAkB,CAAC,CAAA;AACzD,MAAM,MAAM,gBAAgB,GAAG,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAA;AACpE,MAAM,MAAM,cAAc,GAAG,SAAS,CAAC,kBAAkB,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,CAAC,CAAA;AACjG,MAAM,MAAM,cAAc,GAAG,SAAS;IACpC,kBAAkB;IAClB,MAAM;IACN,QAAQ;IACR,MAAM;IACN,OAAO;IACP,qBAAqB;CACtB,CAAA;AAID,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAA;IAC3B,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAAA;IAC1C,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,cAAc,CAAA;IACpE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,cAAc,CAAA;CACxF;AAED,eAAO,MAAM,WAAW,EAAE,iBAOzB,CAAA;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC,kBAAkB,EAAE,MAAM,EAAE,MAAM,CAAC,CAEjG;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,SAAS,CAAC,kBAAkB,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,CAElE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { ModulesFilterId } from './labels';
|
|
3
|
+
/**
|
|
4
|
+
* The filter row.
|
|
5
|
+
*
|
|
6
|
+
* InstUI has no chip/tag component available in this repo, so these are small
|
|
7
|
+
* pill-shaped Buttons carrying `aria-pressed` — a toggle group in behaviour,
|
|
8
|
+
* radio-ish in appearance, which is what the mock draws. Selection is single,
|
|
9
|
+
* not multi: the mock shows exactly one chip active.
|
|
10
|
+
*/
|
|
11
|
+
export interface FilterChipsProps {
|
|
12
|
+
value: ModulesFilterId;
|
|
13
|
+
onChange: (filter: ModulesFilterId) => void;
|
|
14
|
+
/** Per-chip match counts, rendered when supplied. Cheap: module-level only. */
|
|
15
|
+
counts?: Partial<Record<ModulesFilterId, number>>;
|
|
16
|
+
}
|
|
17
|
+
/** @see FilterChipsProps */
|
|
18
|
+
export declare function FilterChips({ value, onChange, counts }: FilterChipsProps): ReactElement;
|
|
19
|
+
//# sourceMappingURL=FilterChips.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FilterChips.d.ts","sourceRoot":"","sources":["../../src/components/FilterChips.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,YAAY,EAAS,MAAM,OAAO,CAAA;AAEhD,OAAO,EAAmC,KAAK,eAAe,EAAsB,MAAM,UAAU,CAAA;AAEpG;;;;;;;GAOG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,eAAe,CAAA;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAA;IAC3C,+EAA+E;IAC/E,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAA;CAClD;AAED,4BAA4B;AAC5B,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,gBAAgB,GAAG,YAAY,CAoDvF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NamedExoticComponent } from 'react';
|
|
2
|
+
import { ModuleItemsState } from '../hooks';
|
|
3
|
+
import { Module } from '../types';
|
|
4
|
+
export interface ModuleCardProps {
|
|
5
|
+
module: Module;
|
|
6
|
+
expanded: boolean;
|
|
7
|
+
onToggle: (moduleId: string) => void;
|
|
8
|
+
/** The hook's per-module item state. Only read when `expanded`. */
|
|
9
|
+
itemsState: ModuleItemsState;
|
|
10
|
+
today: Date;
|
|
11
|
+
/** Show the `inline` vs `onDemand` provenance line. Dev affordance, off by default. */
|
|
12
|
+
showDiagnostics?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Heading level for the module name. Defaults to `'h3'`, one below
|
|
15
|
+
* `ModulesList`'s default of `h2`.
|
|
16
|
+
*
|
|
17
|
+
* The name was bold `Text`, which meant a 120-module course offered a screen
|
|
18
|
+
* reader no heading list to navigate by — the only heading in the tree was the
|
|
19
|
+
* page title.
|
|
20
|
+
*/
|
|
21
|
+
headingLevel?: 'h2' | 'h3' | 'h4';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Memoised, which is only useful because its props are now stable.
|
|
25
|
+
*
|
|
26
|
+
* `itemsState` comes from `useModulesPageData`'s per-module cache and `onToggle`
|
|
27
|
+
* from a ref-backed `useCallback`, so a card re-renders when *its own* module
|
|
28
|
+
* changes and not when a sibling's items arrive. Both of those had to be fixed
|
|
29
|
+
* first — a `memo` over freshly-built props is pure overhead.
|
|
30
|
+
*/
|
|
31
|
+
export declare const ModuleCard: NamedExoticComponent<ModuleCardProps>;
|
|
32
|
+
//# sourceMappingURL=ModuleCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModuleCard.d.ts","sourceRoot":"","sources":["../../src/components/ModuleCard.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAQ,KAAK,oBAAoB,EAAwC,MAAM,OAAO,CAAA;AAC7F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AA+BtC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,mEAAmE;IACnE,UAAU,EAAE,gBAAgB,CAAA;IAC5B,KAAK,EAAE,IAAI,CAAA;IACX,uFAAuF;IACvF,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;CAClC;AAgVD;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,EAAE,oBAAoB,CAAC,eAAe,CAAwB,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NamedExoticComponent } from 'react';
|
|
2
|
+
import { ModuleItem } from '../types';
|
|
3
|
+
export interface ModuleItemRowProps {
|
|
4
|
+
item: ModuleItem;
|
|
5
|
+
/** Injected so relative dates ("yesterday") are deterministic. */
|
|
6
|
+
today: Date;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Memoised because it is the hottest component in the package.
|
|
10
|
+
*
|
|
11
|
+
* `item` is a stable reference out of the query cache and `today` is pinned per
|
|
12
|
+
* mount, so the default shallow comparison is enough. It is what stops one
|
|
13
|
+
* module's observer publishing from re-rendering every other module's rows —
|
|
14
|
+
* ~3000 of them on a 30-module course with everything expanded.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ModuleItemRow: NamedExoticComponent<ModuleItemRowProps>;
|
|
17
|
+
//# sourceMappingURL=ModuleItemRow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModuleItemRow.d.ts","sourceRoot":"","sources":["../../src/components/ModuleItemRow.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAQ,KAAK,oBAAoB,EAAqB,MAAM,OAAO,CAAA;AAC1E,OAAO,EAA+B,KAAK,UAAU,EAAuB,MAAM,UAAU,CAAA;AA6D5F,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,UAAU,CAAA;IAChB,kEAAkE;IAClE,KAAK,EAAE,IAAI,CAAA;CACZ;AA0GD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,oBAAoB,CAAC,kBAAkB,CAA2B,CAAA"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { UseModulesPageDataResult } from '../hooks';
|
|
3
|
+
import { ModulesFilterId } from './labels';
|
|
4
|
+
/**
|
|
5
|
+
* The student modules page, approximating the Modules Foundation design.
|
|
6
|
+
*
|
|
7
|
+
* What it is: course header, a module-name search, an expand/collapse menu, the
|
|
8
|
+
* module-level filter row, and a stack of module cards.
|
|
9
|
+
*
|
|
10
|
+
* What it deliberately is not:
|
|
11
|
+
*
|
|
12
|
+
* - **No progress strip.** The `01 02 03 …` paginated bar under the filters in
|
|
13
|
+
* the mock is out of scope; the user deferred it.
|
|
14
|
+
* - **No item-level search.** See `moduleMatchesSearch`: in `onDemand` mode the
|
|
15
|
+
* items of collapsed modules are not loaded, so searching them would silently
|
|
16
|
+
* under-report. The input filters module *names* and says so on its label.
|
|
17
|
+
* Everything renderable comes from `useModulesPageData`; this component owns
|
|
18
|
+
* only view state (search text, active filter), never data. Expanding a module
|
|
19
|
+
* still invalidates nothing.
|
|
20
|
+
*/
|
|
21
|
+
/** Requests served by the adapter so far, split by operation. */
|
|
22
|
+
export interface ModulesRequestCounts {
|
|
23
|
+
listModules: number;
|
|
24
|
+
listModuleItems: number;
|
|
25
|
+
}
|
|
26
|
+
export interface ModulesListProps {
|
|
27
|
+
/** The whole result of `useModulesPageData`, passed straight through. */
|
|
28
|
+
data: UseModulesPageDataResult;
|
|
29
|
+
/** Course title in the header. */
|
|
30
|
+
courseName?: string;
|
|
31
|
+
/** The crumb above the title, e.g. "Math". Purely decorative. */
|
|
32
|
+
courseContext?: string;
|
|
33
|
+
/**
|
|
34
|
+
* "Now", injected so relative dates and the "Due this week" filter are
|
|
35
|
+
* deterministic in stories and tests. The fixtures use a fixed 2025-01-06
|
|
36
|
+
* epoch, so a story that wants sensible dates should pin this.
|
|
37
|
+
*/
|
|
38
|
+
today?: Date;
|
|
39
|
+
/**
|
|
40
|
+
* Live counts from `createInMemoryAdapter`'s `onRequest`. When supplied, a
|
|
41
|
+
* dev banner renders them. It is the most useful thing on screen for
|
|
42
|
+
* reviewing this MVP — "does this scale" as a number rather than a devtools
|
|
43
|
+
* exercise — and a host with a real adapter simply omits the prop.
|
|
44
|
+
*/
|
|
45
|
+
requestCounts?: ModulesRequestCounts;
|
|
46
|
+
/** Also show items-mode and per-module item provenance. Defaults to `requestCounts !== undefined`. */
|
|
47
|
+
showDiagnostics?: boolean;
|
|
48
|
+
/** Which chip starts active. Uncontrolled after mount — the chips own it. */
|
|
49
|
+
initialFilter?: ModulesFilterId;
|
|
50
|
+
/**
|
|
51
|
+
* Element for the outer container. Defaults to `'div'`.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately not `'main'` by default: Canvas's own layout already ships
|
|
54
|
+
* `role="main"` (`application.html.erb:267`), so a Canvas host rendering this
|
|
55
|
+
* would get two main landmarks. A host that owns its whole page can opt in with
|
|
56
|
+
* `as="main"`.
|
|
57
|
+
*/
|
|
58
|
+
as?: 'div' | 'main' | 'section';
|
|
59
|
+
/**
|
|
60
|
+
* Heading level for the course title. Defaults to `'h2'`.
|
|
61
|
+
*
|
|
62
|
+
* Also not `h1` by default, and for the same reason: a host page usually already
|
|
63
|
+
* has one. `ModuleCard` renders one level below this.
|
|
64
|
+
*/
|
|
65
|
+
headingLevel?: 'h1' | 'h2' | 'h3';
|
|
66
|
+
}
|
|
67
|
+
/** @see ModulesListProps */
|
|
68
|
+
export declare function ModulesList({ data, courseName, courseContext, today, requestCounts, showDiagnostics, initialFilter, as, headingLevel, }: ModulesListProps): ReactElement;
|
|
69
|
+
//# sourceMappingURL=ModulesList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModulesList.d.ts","sourceRoot":"","sources":["../../src/components/ModulesList.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,YAAY,EAA+C,MAAM,OAAO,CAAA;AACtF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAA;AAIxD,OAAO,EAGL,KAAK,eAAe,EAErB,MAAM,UAAU,CAAA;AAGjB;;;;;;;;;;;;;;;;GAgBG;AAEH,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,IAAI,EAAE,wBAAwB,CAAA;IAC9B,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,KAAK,CAAC,EAAE,IAAI,CAAA;IACZ;;;;;OAKG;IACH,aAAa,CAAC,EAAE,oBAAoB,CAAA;IACpC,sGAAsG;IACtG,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,eAAe,CAAA;IAC/B;;;;;;;OAOG;IACH,EAAE,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;IAC/B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;CAClC;AA0DD,4BAA4B;AAC5B,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,UAAU,EACV,aAAa,EACb,KAAK,EACL,aAAa,EACb,eAA6C,EAC7C,aAAqB,EACrB,EAAU,EACV,YAAmB,GACpB,EAAE,gBAAgB,GAAG,YAAY,CAqSjC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ReactElement } from 'react';
|
|
3
|
+
import { InMemoryFixture, ItemsMode } from '../index';
|
|
4
|
+
import { ModulesFilterId } from './labels';
|
|
5
|
+
/**
|
|
6
|
+
* The Modules Foundation design over the real data layer.
|
|
7
|
+
*
|
|
8
|
+
* Harness: `createInMemoryAdapter` over a fixture, a
|
|
9
|
+
* `QueryClient` whose identity survives a re-render, and the adapter's
|
|
10
|
+
* `onRequest` feeding a live counter. Nothing is mocked — the adapter is a real
|
|
11
|
+
* implementation of the contract, including declining to inline a module above
|
|
12
|
+
* `INLINE_ITEM_LIMIT` as Canvas REST does — so the request count in the dev
|
|
13
|
+
* banner is the genuine cost of what is on screen.
|
|
14
|
+
*
|
|
15
|
+
* The counter is the most useful thing here. Switch `itemsMode` to `onDemand`,
|
|
16
|
+
* hit "Expand all" on the large course, and watch what the design costs.
|
|
17
|
+
*/
|
|
18
|
+
declare const FIXTURES: Record<string, () => InMemoryFixture>;
|
|
19
|
+
/**
|
|
20
|
+
* The four scales, as either audience.
|
|
21
|
+
*
|
|
22
|
+
* The teacher shape comes from `buildFixture`'s own `view` option rather than
|
|
23
|
+
* being derived here by stripping fields. That matters for what the story shows:
|
|
24
|
+
* a teacher sees unpublished modules and items — which is the whole reason the
|
|
25
|
+
* teacher view exists — and no progress, because Canvas resolves `progression`
|
|
26
|
+
* to null for a viewer with no progression record. Deriving it by deletion used
|
|
27
|
+
* to produce the one combination Canvas cannot: an unpublished module with a
|
|
28
|
+
* progress bar beside it.
|
|
29
|
+
*/
|
|
30
|
+
type ProgressMode = 'student' | 'teacher';
|
|
31
|
+
interface ModulesListStoryProps {
|
|
32
|
+
fixture: keyof typeof FIXTURES;
|
|
33
|
+
itemsMode: ItemsMode;
|
|
34
|
+
progressMode: ProgressMode;
|
|
35
|
+
initialFilter: ModulesFilterId;
|
|
36
|
+
/** Modules per `listModules` request. Canvas caps this at 100. */
|
|
37
|
+
pageSize: number;
|
|
38
|
+
/** Items per `listModuleItems` request. Canvas caps this at 100. */
|
|
39
|
+
itemPageSize: number;
|
|
40
|
+
itemConcurrency: number;
|
|
41
|
+
/** Expand these module ids on mount, so a story shows item rows without a click. */
|
|
42
|
+
initialExpandedModuleIds: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Which i18n bridge to render through.
|
|
45
|
+
*
|
|
46
|
+
* `none` is a host that has not wired i18n: the package interpolates the
|
|
47
|
+
* English source keys itself. `pseudo` is the standard pre-translation audit —
|
|
48
|
+
* every string is bracketed and padded, so an untranslated literal is visible
|
|
49
|
+
* on sight and a layout that cannot survive a longer language shows up before
|
|
50
|
+
* a translator is ever involved.
|
|
51
|
+
*/
|
|
52
|
+
i18n: 'none' | 'pseudo';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Its own `QueryClient`, deliberately — the global preview decorator builds one
|
|
56
|
+
* in its render body, and these stories are about cache behaviour.
|
|
57
|
+
*/
|
|
58
|
+
declare function ModulesListHarness(props: ModulesListStoryProps): ReactElement;
|
|
59
|
+
declare const meta: Meta<typeof ModulesListHarness>;
|
|
60
|
+
export default meta;
|
|
61
|
+
type Story = StoryObj<typeof meta>;
|
|
62
|
+
/**
|
|
63
|
+
* The design, near enough: 5 modules × 8 items with module 1 open.
|
|
64
|
+
*
|
|
65
|
+
* `auto` resolves to `inline`, so opening any other module costs zero extra
|
|
66
|
+
* requests — the counter in the dev banner stays put as you click around.
|
|
67
|
+
*/
|
|
68
|
+
export declare const Default: Story;
|
|
69
|
+
/**
|
|
70
|
+
* 120 modules × 25 items. `auto` resolves to `onDemand` and first paint is a
|
|
71
|
+
* single request no matter how big the course is.
|
|
72
|
+
*
|
|
73
|
+
* The point of this story is the button at the bottom: `hasNextPage` is true,
|
|
74
|
+
* it says so, and the remaining 20 modules are one click away rather than gone.
|
|
75
|
+
* Canvas Career shows 100 and says nothing.
|
|
76
|
+
*/
|
|
77
|
+
export declare const LargeCourse: Story;
|
|
78
|
+
/**
|
|
79
|
+
* 3 modules, the middle one 250 items, pinned to `inline` — the Canvas fallback
|
|
80
|
+
* in miniature. Modules 1 and 3 come back with items attached; module 2 is over
|
|
81
|
+
* `INLINE_ITEM_LIMIT` so the server omits its items and that *one module* falls
|
|
82
|
+
* back to a per-module fetch, then pages 100 at a time. "Load more items" stays
|
|
83
|
+
* on the card until all 250 are loaded.
|
|
84
|
+
*/
|
|
85
|
+
export declare const FatModuleInlineFallback: Story;
|
|
86
|
+
/** No modules. The empty state. */
|
|
87
|
+
export declare const EmptyCourse: Story;
|
|
88
|
+
/**
|
|
89
|
+
* Teacher / anonymous: `progress` is undefined on every module and `status` on
|
|
90
|
+
* every item.
|
|
91
|
+
*
|
|
92
|
+
* The card renders no progress bar and no status pill, and nowhere does it say
|
|
93
|
+
* "0%" — "we did not fetch progress" and "the student has done none of it" are
|
|
94
|
+
* different facts. Every filter chip except "All" correctly matches nothing,
|
|
95
|
+
* because a module-level filter has nothing to read.
|
|
96
|
+
*/
|
|
97
|
+
export declare const TeacherViewNoProgress: Story;
|
|
98
|
+
/** `progress.status === 'in_progress'`. */
|
|
99
|
+
export declare const FilterInProgress: Story;
|
|
100
|
+
/** `progress.status` is `all_submitted` or `all_graded`. */
|
|
101
|
+
export declare const FilterComplete: Story;
|
|
102
|
+
/**
|
|
103
|
+
* `progress.latestDueAt` within 7 days of the injected "today" (2025-01-08).
|
|
104
|
+
*
|
|
105
|
+
* On the small fixture rather than the large one, deliberately: `latestDueAt` is
|
|
106
|
+
* the *latest* due date in a module, and a 25-item module's latest date is
|
|
107
|
+
* always months out, so the large course legitimately has nothing in a 7-day
|
|
108
|
+
* window. A story that only ever showed the empty state would prove nothing
|
|
109
|
+
* about the chip.
|
|
110
|
+
*/
|
|
111
|
+
export declare const FilterDueThisWeek: Story;
|
|
112
|
+
/** `progress.missingCount > 0`. */
|
|
113
|
+
export declare const FilterMissing: Story;
|
|
114
|
+
/**
|
|
115
|
+
* Every module status and every item status on one screen.
|
|
116
|
+
*
|
|
117
|
+
* `courseSmall` is sized to `MODULE_STATUS_CYCLE.length` precisely so it can
|
|
118
|
+
* show all six `ModuleStatus` values; expanding every module then shows every
|
|
119
|
+
* `ModuleItemStatus` too, so a reviewer can check the whole pill mapping in one
|
|
120
|
+
* screenshot. Note the mock's single green "All items complete" pill is split
|
|
121
|
+
* here into "All items complete" (`all_submitted`) and "All items graded"
|
|
122
|
+
* (`all_graded`), because the domain distinguishes them and the view should not
|
|
123
|
+
* quietly collapse the two.
|
|
124
|
+
*/
|
|
125
|
+
export declare const StatusCoverage: Story;
|
|
126
|
+
/**
|
|
127
|
+
* Every user-visible string, bracketed and padded.
|
|
128
|
+
*
|
|
129
|
+
* This is the story that makes the i18n seam reviewable rather than asserted: a
|
|
130
|
+
* string rendering *without* `⟦ ⟧` never went through `t`. Padding to ~135%
|
|
131
|
+
* approximates German, so it also shows whether the card header, the chip row
|
|
132
|
+
* and the "Load more" button survive a longer language.
|
|
133
|
+
*
|
|
134
|
+
* The one deliberate exception is the dev diagnostics banner, which is developer
|
|
135
|
+
* text and is documented as untranslated in `ModuleCard`.
|
|
136
|
+
*/
|
|
137
|
+
export declare const PseudoLocalised: Story;
|
|
138
|
+
//# sourceMappingURL=ModulesList.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModulesList.stories.d.ts","sourceRoot":"","sources":["../../src/components/ModulesList.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAEtD,OAAO,EAAE,KAAK,YAAY,EAAqB,MAAM,OAAO,CAAA;AAC5D,OAAO,EAQL,KAAK,eAAe,EACpB,KAAK,SAAS,EAGf,MAAM,UAAU,CAAA;AACjB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAG/C;;;;;;;;;;;;GAYG;AAEH,QAAA,MAAM,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,eAAe,CAKnD,CAAA;AAmBD;;;;;;;;;;GAUG;AAIH,KAAK,YAAY,GAAG,SAAS,GAAG,SAAS,CAAA;AAEzC,UAAU,qBAAqB;IAC7B,OAAO,EAAE,MAAM,OAAO,QAAQ,CAAA;IAC9B,SAAS,EAAE,SAAS,CAAA;IACpB,YAAY,EAAE,YAAY,CAAA;IAC1B,aAAa,EAAE,eAAe,CAAA;IAC9B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB,oFAAoF;IACpF,wBAAwB,EAAE,MAAM,EAAE,CAAA;IAClC;;;;;;;;OAQG;IACH,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAA;CACxB;AAqED;;;GAGG;AACH,iBAAS,kBAAkB,CAAC,KAAK,EAAE,qBAAqB,GAAG,YAAY,CAetE;AAgBD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,kBAAkB,CA6BzC,CAAA;AAED,eAAe,IAAI,CAAA;AACnB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAA;AAElC;;;;;GAKG;AACH,eAAO,MAAM,OAAO,EAAE,KAErB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,KAEzB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,EAAE,KAErC,CAAA;AAED,mCAAmC;AACnC,eAAO,MAAM,WAAW,EAAE,KAEzB,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAEnC,CAAA;AASD,2CAA2C;AAC3C,eAAO,MAAM,gBAAgB,EAAE,KAE9B,CAAA;AAED,4DAA4D;AAC5D,eAAO,MAAM,cAAc,EAAE,KAE5B,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAE/B,CAAA;AAED,mCAAmC;AACnC,eAAO,MAAM,aAAa,EAAE,KAE3B,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,EAAE,KAM5B,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,EAAE,KAO7B,CAAA"}
|