Skip to content

Commit 92d4cc1

Browse files
dbernsteintdilauro
andauthored
Add patron blocking rules help modal with template variables (PP-3866) (#207)
## Description Adds a patron blocking rules help modal to the SIP2 patron auth service editor (PP-3866). The modal surfaces template variable names and sample values fetched live from the ILS, a reference of allowed functions, and a syntax quick-reference. Several refactors accompany the feature to keep the codebase clean. **Features:** - New `PatronBlockingRulesHelpModal` component with three sections: live template variables (fetched from the ILS), allowed functions reference (synced from `circulation/docs/FUNCTIONS.md`), and a Jinja2 syntax quick-reference - `?` help button placed directly beside the "Patron Blocking Rules" label - Live patron data fields fetched via the existing validation endpoint and cached with `@tanstack/react-query` via a new `useAvailableFields` custom hook **Refactors & fixes:** - Restored `shiftEntries<T>` helper; `removeRule` was duplicating the same index-shifting logic inline for `clientErrors` and `serverErrors` - Replaced manual `useState`/`useEffect` prefetch with `useQuery`; blur-validation results update the query cache via `queryClient.setQueryData` - Added a pending-validation sentinel in `serverErrors` so the Save button stays blocked while an edited rule expression awaits re-validation - Simplified `syncPatronBlockingDocs.js` from ~200 to ~40 lines by removing a custom Markdown fallback converter and relying directly on `marked` (added as an explicit devDependency) - Added a stale-copy warning comment to `src/content/patronBlockingFunctions.md` - Added a `Modal.Footer` Close button to `PatronBlockingRulesHelpModal` **Tests:** - New `PatronBlockingRulesHelpModal.test.tsx` with 12 isolated unit tests - `PatronBlockingRulesEditor.test.tsx` wrapped with `QueryClientProvider`; significantly expanded test coverage - `PatronAuthServiceEditForm.test.tsx` wrapped with `QueryClientProvider` to accommodate `useAvailableFields` - `patronBlockingRules.test.ts` updated for the new `ValidationResult` return type; added `availableFields` coverage ## Motivation and Context PP-3866: Library staff configuring SIP2 patron blocking rules had no in-app reference for which patron data fields are available or what syntax is valid. This change adds contextual help that shows live data pulled from their own ILS, reducing trial-and-error configuration. ## How Has This Been Tested? - All 227 Jest tests pass (`tests/jest/`) - TypeScript lint clean (`tslint`) - Manually verified modal opens/closes, live fields load, and the functions reference renders correctly ## Checklist: - [x] I have updated the documentation accordingly. - [x] All new and existing tests passed. --------- Co-authored-by: Tim DiLauro <tdilauro@users.noreply.github.com>
1 parent 5c4d823 commit 92d4cc1

18 files changed

Lines changed: 1193 additions & 156 deletions

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,48 @@ There are end-to-end tests that run on Nightwatch. This selenium-based test runn
138138

139139
To set up credentials and run the tests, check out the [README](/tests/README.md) in `/tests/.
140140

141+
## Patron Blocking Rules — Keeping Documentation in Sync
142+
143+
The help modal displayed alongside the patron blocking rules editor renders
144+
documentation sourced from
145+
[`circulation/docs/FUNCTIONS.md`](../circulation/docs/FUNCTIONS.md).
146+
Because `circulation-admin` is a separate package, this content is
147+
pre-processed and committed as a generated TypeScript file:
148+
149+
```
150+
src/content/patronBlockingFunctionsHtml.ts ← auto-generated, do not edit by hand
151+
```
152+
153+
### When to run the sync script
154+
155+
Run the sync script any time `circulation/docs/FUNCTIONS.md` is updated:
156+
157+
```bash
158+
npm run sync-patron-blocking-docs
159+
```
160+
161+
The script (`scripts/syncPatronBlockingDocs.js`) reads the markdown from the
162+
sibling `circulation` repository, converts it to HTML, and overwrites
163+
`src/content/patronBlockingFunctionsHtml.ts`. Commit the regenerated file
164+
alongside any other changes.
165+
166+
### Expected repository layout
167+
168+
The script assumes both repositories are checked out as siblings:
169+
170+
```
171+
/path/to/
172+
├── circulation/ ← Palace Project Circulation Manager
173+
│ └── docs/
174+
│ └── FUNCTIONS.md ← source of truth for allowed functions
175+
└── circulation-admin/ ← this repository
176+
└── src/content/
177+
└── patronBlockingFunctionsHtml.ts ← generated output
178+
```
179+
180+
If your layout differs, update the source path at the top of
181+
`scripts/syncPatronBlockingDocs.js`.
182+
141183
## Debugging
142184

143185
The [Redux DevTools browser extension](https://github.com/reduxjs/redux-devtools/tree/main/extension) may be used to easily inspect app states and state transitions.

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = {
44
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
55
"<rootDir>/tests/__mocks__/fileMock.js",
66
"\\.(css|less)$": "<rootDir>/tests/__mocks__/styleMock.js",
7+
"\\.md$": "<rootDir>/tests/__mocks__/fileMock.js",
78
},
89
preset: "ts-jest",
910
testEnvironment: "jest-fixed-jsdom",

package-lock.json

Lines changed: 42 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"dev-server": "dotenv -c -- webpack serve --progress --hot --config webpack.dev-server.config",
3333
"dev-test-axe": "TEST_AXE=true npm run dev",
3434
"prod": "webpack --progress --config webpack.prod.config",
35-
"build-docs": "typedoc --tsconfig tsconfig.json src"
35+
"build-docs": "typedoc --tsconfig tsconfig.json src",
36+
"sync-patron-blocking-docs": "node scripts/syncPatronBlockingDocs.js"
3637
},
3738
"dependencies": {
3839
"@nypl/dgx-svg-icons": "0.3.4",
@@ -113,6 +114,7 @@
113114
"jsdom": "^20.0.3",
114115
"json-loader": "^0.5.4",
115116
"lint-staged": "^10.4.0",
117+
"marked": "^17.0.6",
116118
"mini-css-extract-plugin": "1.6.0",
117119
"mocha": "^10.2.0",
118120
"msw": "^2.7.3",

scripts/syncPatronBlockingDocs.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env node
2+
/* eslint-disable @typescript-eslint/no-var-requires */
3+
/**
4+
* Reads ../circulation/docs/FUNCTIONS.md, converts it to HTML, and writes
5+
* src/content/patronBlockingFunctionsHtml.ts — a TypeScript module that
6+
* exports the HTML as a string constant.
7+
*
8+
* Run via: npm run sync-patron-blocking-docs
9+
*/
10+
11+
"use strict";
12+
13+
const fs = require("fs");
14+
const path = require("path");
15+
const { marked } = require("marked");
16+
17+
// ---------------------------------------------------------------------------
18+
// Main
19+
// ---------------------------------------------------------------------------
20+
const srcMd = path.resolve(__dirname, "../../circulation/docs/FUNCTIONS.md");
21+
const destTs = path.resolve(
22+
__dirname,
23+
"../src/content/patronBlockingFunctionsHtml.ts"
24+
);
25+
26+
if (!fs.existsSync(srcMd)) {
27+
console.error(`Source not found: ${srcMd}`);
28+
process.exit(1);
29+
}
30+
31+
const markdown = fs.readFileSync(srcMd, "utf8");
32+
const html = marked(markdown);
33+
34+
const tsContent = `// AUTO-GENERATED — do not edit by hand.
35+
// Run \`npm run sync-patron-blocking-docs\` to regenerate from
36+
// circulation/docs/FUNCTIONS.md.
37+
const patronBlockingFunctionsHtml = ${JSON.stringify(html)};
38+
export default patronBlockingFunctionsHtml;
39+
`;
40+
41+
fs.mkdirSync(path.dirname(destTs), { recursive: true });
42+
fs.writeFileSync(destTs, tsContent, "utf8");
43+
console.log(`Written: ${destTs}`);

src/api/patronBlockingRules.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,29 @@ import { PatronBlockingRule } from "../interfaces";
22

33
const VALIDATE_URL = "/admin/patron_auth_service_validate_patron_blocking_rule";
44

5+
export type ValidationResult = {
6+
/** Non-null when the server reports a validation error; null on success. */
7+
error: string | null;
8+
/** The patron data dictionary returned by the SIP call on success; null on error or parse failure. */
9+
availableFields: Record<string, unknown> | null;
10+
};
11+
512
/**
613
* Validate a patron blocking rule expression against live ILS data on the server.
714
*
815
* The server loads the saved PatronAuthService by serviceId, makes a live
916
* authentication call using its configured test credentials, and evaluates
10-
* the rule expression against the real patron data returned. Only parse/eval
11-
* success or failure is reported — the boolean result is discarded.
17+
* the rule expression against the real patron data returned.
1218
*
13-
* Returns null on success, or an error message string on failure.
19+
* On success, returns the available_fields dictionary (patron data from the SIP
20+
* call) so callers can display template variable names and sample values to the
21+
* user. On failure, returns the error message string.
1422
*/
1523
export const validatePatronBlockingRuleExpression = async (
1624
serviceId: number | undefined,
1725
rule: PatronBlockingRule,
1826
csrfToken: string | undefined
19-
): Promise<string | null> => {
27+
): Promise<ValidationResult> => {
2028
const formData = new FormData();
2129
if (serviceId !== undefined) {
2230
formData.append("service_id", String(serviceId));
@@ -37,13 +45,28 @@ export const validatePatronBlockingRuleExpression = async (
3745
});
3846

3947
if (res.ok) {
40-
return null;
48+
try {
49+
const data = await res.json();
50+
const fields = data.available_fields;
51+
return {
52+
error: null,
53+
availableFields:
54+
fields && typeof fields === "object" && !Array.isArray(fields)
55+
? (fields as Record<string, unknown>)
56+
: null,
57+
};
58+
} catch {
59+
return { error: null, availableFields: null };
60+
}
4161
}
4262

4363
try {
4464
const data = await res.json();
45-
return data.detail || "Rule validation failed.";
65+
return {
66+
error: data.detail || "Rule validation failed.",
67+
availableFields: null,
68+
};
4669
} catch {
47-
return "Rule validation failed.";
70+
return { error: "Rule validation failed.", availableFields: null };
4871
}
4972
};

0 commit comments

Comments
 (0)