Skip to content

Feature ETP-4520: Window access rendering + capability field visibility#67

Open
Gremiger wants to merge 23 commits into
epic/ETP-3504from
feature/ETP-4520
Open

Feature ETP-4520: Window access rendering + capability field visibility#67
Gremiger wants to merge 23 commits into
epic/ETP-3504from
feature/ETP-4520

Conversation

@Gremiger

Copy link
Copy Markdown
Contributor

Summary

Part A — generic window-level access rendering:

  • AuthContext/AuthProvider gain windowAccess/capabilities state, populated via a fetchWindowAccess callback fired from selectRole()
  • New useWindowAccess(windowId) (→ none/read-only/full) and useHasCapability(key) hooks, both fail-closed
  • WindowAccessGuard component + generate-frontend.js wiring: route guard for "none", forces readOnly for "read-only" — generic to every generated window with a window.id

Part B — capability-gated field visibility:

  • New decisions.json field property visibleWhenCapability: string
  • Threaded through resolve-curated.jsgenerate-contract.jsgenerate-frontend.js, onto grid columns and status-pill/extraBadges entries
  • Consumers omit the field entirely (not disable) when the capability resolves false

Version: manual minor bump signaled via packages/schema-forge-core/package.json (lockstep resolver's dedicated signal file) → 0.4.0.

Part of ETP-4520 (Roles & Users Phase 5) — companion PRs in com.etendoerp.go and etendo_schema_forge. Full cross-repo design: docs/plans/ETP-4520-cross-domain.md in etendo_schema_forge.

Jira: ETP-4520

Known follow-up (non-blocking)

Reviewed and confirmed: selectRole()'s fetchWindowAccess call has no stale-response guard — rapid role-switching before the first fetch resolves could let a slower response overwrite the map for the wrong role. Not currently exploitable (no caller does rapid switching yet), but should be hardened before a role-picker UI ships.

Test plan

  • packages/app-shell-core auth suite: 24/24 pass (AuthContext, useWindowAccess, useHasCapability, WindowAccessGuard, AppShellRuntime wiring)
  • cli suite: 2523/2523 pass, including 4 new dedicated test files for the generator wiring
  • Sonar Quality Gate clean (fixed one cognitive-complexity violation via extraction, commit 4c0bca195)

@Gremiger

Gremiger commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ Automated QA verdict — not an independent human review. This comment was posted by an autonomous AI agent as part of a fully automated DEV → REVIEW → QA pipeline run for this ticket. The same automation authored, reviewed, and QA'd this change end-to-end, under this GitHub account — there was no independent human checkpoint at any stage. Treat everything below as a documented automated check to inform a human reviewer's decision, not as a substitute for one.


VERDICT: APPROVE

TEST RESULTS (this repo's portion):

  • etendo_schema_forge root npm test: 4308/4309 passed (1 skipped), 0 failures.
  • tools/app-shell vitest: 9549/9550 passed (1 skipped), 0 failures, 513/513 files.
  • sf-validate-pipeline --scope=sales-invoice,purchase-invoice: 0 violations.

Cross-repo (schema_forge_core): CLI npm test 2523/2528 pass (5 skipped, 0 fail); app-shell-core vitest 605/622 pass, 17 pre-existing failures in 6 unrelated files (date-range-popover, distinct-values-filter, money-amount, DataProvider, queryCache, add-line-button) — confirmed identical on epic/ETP-3504 baseline, not a regression.

Cross-repo (com.etendoerp.go): full monorepo ./gradlew test not run to completion (descoped — re-runs unrelated compliance modules, out of scope for this ticket's QA). ETP-4520-specific classes green: SFWindowAccessMapTest 13/13, SFListMenuTest 32/32, NeoAccessHelperTest 50/50.

SCENARIO VERIFICATION (see full report to coordinator for detail/confidence level per item):

  1. TC-37 posted capability gate — code + automated test verified (DataTable.jsx/DetailView.jsx genuinely omit, not disable; contract.json carries visibleWhenCapability on both windows).
  2. TC-38/39 AD_Role.EM_ETGO_Show_Acct_Fields — DB-verified: column exists, queryable, correct IDs/default.
  3. TC-40 re-fetch on role reselection — code-verified: AuthContext.selectRole() → fetchWindowAccess → state update.
  4. TC-41 admin bypass — code + JUnit-verified (SFWindowAccessMap.java + NeoAccessHelper.isAdminOrClientAdmin(), backed by passing tests).
  5. TC-42 FA "Cuentas contables" tab — automated-test-verified (EditAccountModal.vitest.jsx asserts not.toBeInTheDocument()).
  6. TC-43 ETP-4529/4531 untouched — diff-verified: zero file overlap with dimension/accounting-date code paths.

No live browser/E2E session or live webhook call was performed (no reachable running deploy in the time available) — all confidence above is code-level/automated-test-level, not a live manual click-through. No Critical/High bugs found.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end support for runtime, per-role window access enforcement and capability-gated field visibility across Schema Forge’s generator pipeline and the app-shell auth runtime.

Changes:

  • Extend AuthProvider/AuthContext with windowAccess and capabilities, populated via an injected fetchWindowAccess hook on selectRole(), plus fail-closed hooks (useWindowAccess, useHasCapability) and a reusable WindowAccessGuard.
  • Wire window-access tier checks into generated window pages (route guard for "none", readOnly enforcement for "read-only"), keyed by window.id.
  • Add visibleWhenCapability as an opt-in decisions/contract/frontend hint so consumers can omit gated columns/status pills at runtime.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/app-shell-core/src/runtime/AppShellRuntime.jsx Forwards auth.fetchWindowAccess down to AuthProvider.
packages/app-shell-core/src/runtime/tests/AppShellRuntime.test.jsx Verifies fetchWindowAccess is forwarded and role selection triggers access map population.
packages/app-shell-core/src/auth/AuthContext.jsx Adds windowAccess/capabilities state and fetchWindowAccess integration at role selection time.
packages/app-shell-core/src/auth/useWindowAccess.js Introduces fail-closed useWindowAccess and useHasCapability hooks.
packages/app-shell-core/src/auth/WindowAccessGuard.jsx Adds a generic “no access” guard UI for window routes.
packages/app-shell-core/src/auth/index.js Exposes the new hooks and guard from the auth package entrypoint.
packages/app-shell-core/src/auth/tests/AuthContext.test.jsx Covers fetchWindowAccess invocation, defaults, failure behavior, and clearing behavior.
packages/app-shell-core/src/auth/tests/useWindowAccess.test.jsx Covers fail-closed semantics for access tiers and capability flags.
packages/app-shell-core/src/auth/tests/WindowAccessGuard.test.jsx Verifies denied rendering behavior and allow-through for "full"/"read-only".
cli/src/resolve-curated.js Copies visibleWhenCapability from decisions into curated schema fields.
cli/src/generate-contract.js Passes visibleWhenCapability through to the frontend contract field definition.
cli/src/generate-frontend.js Emits visibleWhenCapability on columns/status pills and adds window-access tier wiring/guarding when window.id exists.
cli/test/resolve-curated-visible-when-capability.test.js Tests curated-schema passthrough for visibleWhenCapability.
cli/test/generate-contract-visible-when-capability.test.js Tests contract passthrough for visibleWhenCapability.
cli/test/generate-frontend-visible-when-capability.test.js Tests frontend emission for columns and status pills.
cli/test/generate-frontend-window-access.test.js Tests page generation for access-tier routing/readonly behavior.
cli/test/generate-frontend.test.js Makes existing import assertions resilient to added useMemo under window-access wiring.
docs/decisions-reference.md Documents runtime window-access gating and capability-gated visibility.
docs/neo-headless-extensibility.md Adds SFWindowAccessMap to the webhook catalog and notes its consumption path.

Comment on lines +56 to +60
if (typeof fetchWindowAccess !== 'function') return;
Promise.resolve(fetchWindowAccess(nextSession))
.then((result) => {
if (result?.windowAccess) setWindowAccess(result.windowAccess);
if (result?.capabilities) setCapabilities(result.capabilities);
Comment thread docs/decisions-reference.md Outdated

`window.id` (the AD_Window_ID, already threaded through `resolve-curated.js`/`generate-contract.js` unconditionally — not a decisions.json key, extracted straight from the raw AD Window) now doubles as the lookup key for a **runtime, per-role access tier**. Whenever a contract carries a real `window.id`, `generatePageComponent` emits:

- `useWindowAccess(windowId)` (from `@etendosoftware/app-shell-core/auth`), resolving `"none" | "read-only" | "full"` from the `windowAccess` map fetched once, at role-selection time, from `GET /webhooks/SFWindowAccessMap`.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Comment on lines +20 to +24
export function useWindowAccess(windowId) {
const { windowAccess } = useAuth();
if (!windowId) return 'none';
return windowAccess?.[windowId] ?? 'none';
}
Comment thread packages/app-shell-core/src/auth/useWindowAccess.js
Comment on lines +1 to +6
import { test, expect } from 'vitest';
import { renderToStaticMarkup } from 'react-dom/server';
import { render, screen, act } from '@testing-library/react';
import { useLocation } from 'react-router-dom';
import { AppShellRuntime } from '../AppShellRuntime.jsx';
import { useAuth } from '../../auth/index.js';

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

docs/neo-headless-extensibility.md:416

  • This doc references modules/com.etendoerp.go/docs/neo-headless.md, but that path does not exist in this repository, so the reference reads like a broken relative link. Consider clarifying that it lives in the Etendo Go repository (or linking to the correct location) so readers don’t chase a non-existent file here.
All webhooks are invoked via HTTP (see `push-to-neo.js` for programmatic usage from Schema Forge). `SFWindowAccessMap` is the one exception consumed directly by the generated frontend (`AuthContext`'s `fetchWindowAccess`) rather than by Schema Forge tooling — see `docs/decisions-reference.md` (window-access gating) and `modules/com.etendoerp.go/docs/neo-headless.md` §8b for the response shape and resolution order.

---

## Related Documentation

- **API Reference:** `modules/com.etendoerp.go/docs/neo-headless.md`

Comment on lines +68 to +73
Promise.resolve()
.then(() => fetchWindowAccess(nextSession))
.then((result) => {
setWindowAccess(result?.windowAccess ?? {});
setCapabilities(result?.capabilities ?? {});
})

Both hooks/the guard fail **closed**: an unloaded `windowAccess` map or a missing `windowId` both resolve `"none"`, never `"full"`. Artifacts without a `window.id` (pre-ETP-4520 extractions, hand-built windows with no backing AD Window) silently keep today's behavior — the feature only activates once `window.id` is present.

This is not a decisions.json opt-in; it applies automatically to every generated window that has an `window.id`. The one companion piece it still needs from whoever wires up the host app: the `GET /webhooks/SFWindowAccessMap` fetcher must be passed to `AuthProvider` as the `fetchWindowAccess` prop, invoked at the same lifecycle moment `selectedRole.orgList` is populated (see `AuthContext.jsx`'s `selectRole()`).
Comment on lines 1933 to 1941
const windowConfig = contract?.frontendContract?.window ?? {};
// ETP-4520 — AD_Window_ID (threaded through resolve-curated.js/generate-contract.js
// already, see schema.window.id). Baked as a literal so useWindowAccess() doesn't
// depend on the host injecting a matching `window.id` prop at runtime. Gates the whole
// feature: artifacts extracted before window.id was tracked (or hand-built windows with
// no backing AD_Window) fall back to today's unrestricted behavior rather than emitting
// a check against an id we don't have.
const windowAccessId = windowConfig.id ?? null;
const documentPreview = windowConfig.documentPreview ?? null;

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

docs/decisions-reference.md:79

  • Grammar: “has an window.id” should be “has a window.id”.
This is not a decisions.json opt-in; it applies automatically to every generated window that has an `window.id`. The one companion piece it still needs from whoever wires up the host app: the `GET /webhooks/SFWindowAccessMap` fetcher must be passed to `AuthProvider` as the `fetchWindowAccess` prop, invoked at the same lifecycle moment `selectedRole.orgList` is populated (see `AuthContext.jsx`'s `selectRole()`).

Comment on lines 39 to 43
const logout = useCallback(() => {
const clearedSession = normalizeAuthSession();
setSessionState(clearedSession);
authStorage.clear();
onSessionChange?.(clearedSession);
Comment thread docs/neo-headless-extensibility.md Outdated
| `SFWindowAccessMap` | Per-role window access tier + capability flags (ETP-4520) | -- |

All webhooks are invoked via HTTP (see `push-to-neo.js` for programmatic usage from Schema Forge).
All webhooks are invoked via HTTP (see `push-to-neo.js` for programmatic usage from Schema Forge). `SFWindowAccessMap` is the one exception consumed directly by the generated frontend (`AuthContext`'s `fetchWindowAccess`) rather than by Schema Forge tooling — see `docs/decisions-reference.md` (window-access gating) and `modules/com.etendoerp.go/docs/neo-headless.md` §8b for the response shape and resolution order.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

screen.getByTestId('select-role').click();
});

expect(await screen.findByTestId('window-access')).toHaveTextContent('{"147":"full"}');
Comment on lines +6 to +10
// DATA annotation onto column/statusPill entries here; the actual runtime
// omission (calling useHasCapability and skipping the render) happens inside
// DataTable.jsx / DetailView.jsx, which live in the functional repo's
// tools/app-shell/src/components/contract-ui — NOT in this repo. See the
// delivery report for the companion change those components still need.
Comment on lines +20 to +24
// ETP-4520 — request-sequencing guard against a stale-response race: if
// role A is selected then role B before A's fetchWindowAccess resolves, A's
// slower response can land AFTER B's and overwrite B's correct maps with
// A's stale ones. Every selectRole() call increments this ref immediately;
// only the response whose captured id still matches the ref's CURRENT value

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

packages/app-shell-core/src/runtime/tests/AppShellRuntime.test.jsx:102

  • This assertion is likely flaky: findByTestId('window-access') resolves immediately because the element is rendered from the start, but windowAccess updates asynchronously (fire-and-forget fetch). This can assert before the text content changes.

Wait for the updated text instead (e.g., findByText on the expected JSON string, or waitFor around toHaveTextContent).

  expect(await screen.findByTestId('window-access')).toHaveTextContent('{"147":"full"}');

docs/decisions-reference.md:79

  • Grammar: use “a window.id” (not “an window.id”).
This is not a decisions.json opt-in; it applies automatically to every generated window that has an `window.id`. The one companion piece it still needs from whoever wires up the host app: the `GET /webhooks/SFWindowAccessMap` fetcher must be passed to `AuthProvider` as the `fetchWindowAccess` prop, invoked at the same lifecycle moment `selectedRole.orgList` is populated (see `AuthContext.jsx`'s `selectRole()`).

Comment on lines +8 to +10
// Core vitest runs without `globals: true` (see vitest.config.js) — do
// explicit cleanup so mounted providers don't bleed between tests.
afterEach(cleanup);
Comment on lines +7 to +9
// Core vitest runs without `globals: true` (see vitest.config.js) — do
// explicit cleanup so mounted providers don't bleed between tests.
afterEach(cleanup);
Gremiger added 16 commits July 24, 2026 12:35
The functional repo's DataTable/DetailView consumption (via the new
capabilityVisibility.js helper, commit c6e2ac1e3) closed the gap this
note previously flagged as pending; corrects the hook name too.
generatePageComponent tripped Sonar S3776 (18 > 15 allowed) after the
windowAccess route-guard/readOnly wiring was added inline. Extracted
the four windowAccessId ternaries into buildWindowAccessWiring(), a
pure helper with no behavior change.
Clears windowAccess/capabilities before the new role's fetch starts so
a role switch never briefly shows the previous role's access map, and
defers the fetchWindowAccess() call into the promise chain so a
synchronous throw is caught the same way a rejected promise is.
@sonarscanetendo

Copy link
Copy Markdown

Passed Quality Gate passed

Issues

Measures

Project ID: etendosoftware_schema_forge_core_976a1e0b-4b24-4757-a2f4-8afa9a5e0289

View in SonarQube

@github-actions

Copy link
Copy Markdown
Contributor

📦 Preview packages published — dist-tag alpha

0.3.17-preview.feature-ETP-4520.20260724123958.4bccf1f

To exercise the published-package path (no LOCAL_CORE), bump all
core pins to this preview in the functional repo and reinstall:

make bump-core-version VERSION=0.3.17-preview.feature-ETP-4520.20260724123958.4bccf1f

Re-posted on each push to this branch; supersedes older previews.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants