diff --git a/.github/workflows/cypress-dev.yml b/.github/workflows/cypress-dev.yml index b010ab5519..da7eff10bf 100644 --- a/.github/workflows/cypress-dev.yml +++ b/.github/workflows/cypress-dev.yml @@ -31,4 +31,5 @@ jobs: env_name: dev cypress_config_key: CYPRESS_CONFIG_DEV base_url: ${{ inputs.base_url || '' }} + git_ref: ${{ github.event.workflow_run.head_sha || github.sha }} secrets: inherit diff --git a/.github/workflows/cypress-e2e-runner.yml b/.github/workflows/cypress-e2e-runner.yml index 6dfab257b4..762d55d7c8 100644 --- a/.github/workflows/cypress-e2e-runner.yml +++ b/.github/workflows/cypress-e2e-runner.yml @@ -33,6 +33,10 @@ on: required: false type: string default: "" + git_ref: + description: "Branch/tag/SHA of bcgov/Unity to check out — must be the triggering commit, not github.sha (workflow_run events set github.sha to the default branch, not the triggering branch)" + required: true + type: string permissions: contents: read @@ -69,7 +73,7 @@ jobs: JOB_REF=$(oc process unity-cypress-job \ -p ENV=${{ inputs.env_name }} \ -p CYPRESS_CONFIG_KEY=${{ inputs.cypress_config_key }} \ - -p GIT_REF=$GITHUB_SHA \ + -p GIT_REF="${{ inputs.git_ref }}" \ -p GIT_TOKEN=$GH_TOKEN \ -p BASE_URL="${{ inputs.base_url }}" \ -n $TOOLS_NAMESPACE \ diff --git a/.github/workflows/cypress-prod.yml b/.github/workflows/cypress-prod.yml index a9e9fcde95..117db6f7fd 100644 --- a/.github/workflows/cypress-prod.yml +++ b/.github/workflows/cypress-prod.yml @@ -16,4 +16,5 @@ jobs: with: env_name: prod cypress_config_key: CYPRESS_CONFIG_PROD + git_ref: ${{ github.event.workflow_run.head_sha || github.sha }} secrets: inherit diff --git a/.github/workflows/cypress-test.yml b/.github/workflows/cypress-test.yml index 9a907f33d9..f49d93145d 100644 --- a/.github/workflows/cypress-test.yml +++ b/.github/workflows/cypress-test.yml @@ -21,4 +21,5 @@ jobs: with: env_name: test cypress_config_key: CYPRESS_CONFIG_TEST + git_ref: ${{ github.event.workflow_run.head_sha || github.sha }} secrets: inherit diff --git a/.github/workflows/cypress-uat.yml b/.github/workflows/cypress-uat.yml index a33f814e2f..503f3e68c8 100644 --- a/.github/workflows/cypress-uat.yml +++ b/.github/workflows/cypress-uat.yml @@ -24,4 +24,5 @@ jobs: with: env_name: uat cypress_config_key: CYPRESS_CONFIG_UAT + git_ref: ${{ github.event.workflow_run.head_sha || github.sha }} secrets: inherit diff --git a/applications/Unity.AutoUI/.claude/skills/validate-cypress-selectors/SKILL.md b/applications/Unity.AutoUI/.claude/skills/validate-cypress-selectors/SKILL.md new file mode 100644 index 0000000000..250395347c --- /dev/null +++ b/applications/Unity.AutoUI/.claude/skills/validate-cypress-selectors/SKILL.md @@ -0,0 +1,59 @@ +--- +name: validate-cypress-selectors +description: Extract and audit Cypress selectors against Unity Grant Manager application markup, and detect when a branch's app-code changes have broken a selector that worked on main. Use when changing Razor/C#/JS markup, Cypress specs, page objects, shared Cypress commands, element IDs, or CSS selectors — or when investigating why a Cypress test started failing after unrelated app changes. +--- + +# Validate Cypress Selectors + +Maintain the selector contract between `applications/Unity.AutoUI` and `applications/Unity.GrantManager`. Run everything from `applications/Unity.AutoUI`. + +## Two modes + +**Full snapshot** — classify every selector in the repo right now: +```bash +npm run selectors:report +``` +Writes `cypress/selectors/registry.json`. Use this to get a baseline understanding, or before/after a change to eyeball the raw counts. + +**Baseline diff (the useful one for "did this branch break something")**: +```bash +npm run selectors:diff # report only +npm run selectors:fix # + dry-run patch preview for fixable regressions +npm run selectors:apply # + write the unambiguous fixes to disk +``` +Compares the current working tree against the **committed baseline** — `cypress/selectors/registry.json` as it exists at `origin/main` (override with `--base `, e.g. `npm run selectors:diff -- --base origin/develop`). It does **not** re-parse git diffs to guess what changed; it re-runs the full scanner on the current tree and compares the resulting per-selector `status` against the baseline's. Anything that got *worse* (`matched` → `missing`, `matched` → `unverified`, etc.) is a regression this branch likely introduced. + +## Interpreting a full-scan entry + +- `matched` — every identifying token (`id`, `data-cy`, `data-testid`) has static evidence in application source. +- `missing` — at least one identifying token has no static evidence. Investigate. +- `unverified` — a structural/class-based CSS selector that can't be proven reliably by static scanning (no identifying token to search for). +- `exempt` — matches a configured ownership rule (`selector-contract.config.json`): external identity-provider markup, framework-generated selectors (Bootstrap-select, Select2, DataTables, SweetAlert2), or Form.io/CHEFS dynamic fields. Outside the static contract by design. +- `syntaxKind` — `css` or `xpath`. Everything today is `css`; `xpath` exists to catch it early if `cy.xpath(...)`-style selectors are ever introduced (they're harder to keep in sync with markup and generally discouraged). + +`matched` only proves the token exists *somewhere* in source — it says nothing about visibility, permission-gating, or runtime reachability. It's a fast static sanity check, not a substitute for actually running the Cypress spec. + +## Diff-mode regression auto-fix — how it decides what's safe to touch + +Only `matched` (baseline) → `missing` (current) regressions are eligible for auto-fix — that's the one transition backed by concrete proof the selector worked before. Pre-existing `missing`/`unverified` entries are left alone; they aren't this branch's fault and guessing at them is out of scope. + +For each eligible regression: +1. Find where the token was matched at baseline (`applicationMatches` in the baseline entry) and the exact line via `git show :`. +2. In the *current* version of that file, score every line for similarity to the old line (word-overlap, with a small proximity bonus toward the original line number — an attribute rename overwhelmingly stays in place rather than the element relocating). +3. **Exactly one clear winner** → propose restoring the missing attribute onto it (inserted right after the tag name, alongside whatever else is there — never replaces existing attributes). +4. **Zero or ambiguous candidates** (e.g. several structurally-identical sibling elements) → reported as "NEEDS REVIEW" with the old-baseline context shown. Never guessed. + +`--apply` only ever writes the unambiguous fixes from step 3. It never touches an ambiguous case, never invents a token value it doesn't have baseline evidence for, and never commits or pushes — it stops at "working tree modified, go review and commit like any other change." Treat an applied fix the same as any other diff: read it, and actually run the relevant Cypress spec before trusting it, since restoring the identifying token proves the selector *resolves* again, not that the element behaves correctly. + +## Workflow + +1. Before editing UI selectors or markup: `npm run selectors:report` to capture a baseline mentally (or diff against origin/main if you want the machine to do it). +2. Prefer a unique `data-cy` attribute for application-owned interactive elements over relying on a plain `#id`. +3. Update application markup and the corresponding Cypress selector together. +4. `npm run selectors:diff` to see what changed relative to `origin/main`. If something regressed, `npm run selectors:fix` to preview a proposed patch, then `npm run selectors:apply` if it looks right — followed by actually running the affected Cypress spec. +5. For anything reported "NEEDS REVIEW," check the rendered page or Cypress scenario by hand — conditional, permission-gated, or JavaScript-generated elements won't resolve automatically. +6. Report unresolved findings; don't hide them by widening `ownershipRules` exemptions in `selector-contract.config.json` just to make a `missing` entry disappear. + +## Keeping the baseline current + +`cypress/selectors/registry.json` is a committed file that's only meaningful if `origin/main`'s copy is kept up to date — regenerate and commit it on `main` after selector-affecting changes land, otherwise `selectors:diff` will compare against a stale snapshot. diff --git a/applications/Unity.AutoUI/cypress.config.ts b/applications/Unity.AutoUI/cypress.config.ts index 4a8fb6e4eb..83619ae6ca 100644 --- a/applications/Unity.AutoUI/cypress.config.ts +++ b/applications/Unity.AutoUI/cypress.config.ts @@ -3,10 +3,25 @@ import FormData from "form-data"; import fs from "fs"; import path from "path"; -function loadLocalEnvironmentConfig(): Record { - const environmentName = ( - process.env.UNITY_CYPRESS_ENV || "dev" - ).toLowerCase(); +function loadLocalEnvironmentConfig(requestedEnvironment?: string): { + environmentName: string; + environmentSource: string; + environmentConfig: Record; +} { + let environmentName: string; + let environmentSource: string; + + if (requestedEnvironment) { + environmentName = requestedEnvironment.toLowerCase(); + environmentSource = "--env environment"; + } else if (process.env.UNITY_CYPRESS_ENV) { + environmentName = process.env.UNITY_CYPRESS_ENV.toLowerCase(); + environmentSource = "UNITY_CYPRESS_ENV"; + } else { + environmentName = "dev"; + environmentSource = "default"; + } + const environmentFilePath = path.resolve( "cypress", "config", @@ -15,9 +30,13 @@ function loadLocalEnvironmentConfig(): Record { try { const content = fs.readFileSync(environmentFilePath, "utf-8"); - return JSON.parse(content) as Record; + return { + environmentName, + environmentSource, + environmentConfig: JSON.parse(content) as Record, + }; } catch { - return {}; + return { environmentName, environmentSource, environmentConfig: {} }; } } @@ -25,7 +44,25 @@ function loadLocalEnvironmentConfig(): Record { export default defineConfig({ e2e: { setupNodeEvents(on, config) { - const environmentConfig = loadLocalEnvironmentConfig(); + // Supports selecting the environment either via Cypress's native + // `--env environment=` CLI flag or the UNITY_CYPRESS_ENV OS + // environment variable (the flag takes precedence). + const { environmentName, environmentSource, environmentConfig } = + loadLocalEnvironmentConfig( + config.env?.environment as string | undefined, + ); + + // eslint-disable-next-line no-console + console.log( + `\n[Cypress] Running against environment: ${environmentName} (source: ${environmentSource})\n`, + ); + + on("after:run", () => { + // eslint-disable-next-line no-console + console.log( + `\n[Cypress] Finished run against environment: ${environmentName} (source: ${environmentSource})\n`, + ); + }); on("task", { readJsonIfExists(filePath: string): Record | null { diff --git a/applications/Unity.AutoUI/cypress/selectors/README.md b/applications/Unity.AutoUI/cypress/selectors/README.md new file mode 100644 index 0000000000..670cebd4f9 --- /dev/null +++ b/applications/Unity.AutoUI/cypress/selectors/README.md @@ -0,0 +1,61 @@ +# Cypress Selector Contract + +Keeps every selector used by the Cypress suite (`applications/Unity.AutoUI/cypress`) cross-checked against the actual application markup (`applications/Unity.GrantManager/src` + `modules`), so a Razor/C#/JS change that quietly removes or renames an `id`/`data-cy`/`data-testid` a test depends on gets caught before it breaks a run. + +All commands below run from `applications/Unity.AutoUI`. + +## What's in this directory + +- **`registry.json`** — generated output, one entry per distinct selector string found in Cypress code. **This file is meant to be committed** on `main`; it's the baseline every branch's `npm run selectors:diff` compares itself against. Regenerate and commit it after selector-affecting changes land on `main`, or the baseline drifts stale. + +## The two tools + +| Command | What it does | +|---|---| +| `npm run selectors:report` | Full snapshot: re-scans everything, writes `registry.json`, prints a summary. No comparison to anything — just "here's the state of the world right now." | +| `npm run selectors:diff` | Compares the current working tree against `registry.json` as it exists on `origin/main`, and reports any selector whose status got *worse* (a regression this branch likely introduced). | +| `npm run selectors:fix` | Same as `selectors:diff`, plus a dry-run preview of the patch it would apply for any regression it can confidently fix. Writes nothing. | +| `npm run selectors:apply` | Same as `selectors:fix`, but actually writes the unambiguous fixes to disk. Never commits, never pushes, never touches an ambiguous case. | + +Pass `-- --base ` to any diff/fix/apply command to compare against something other than `origin/main`. + +## How the full scan works (`scripts/selector-contract.mjs`) + +1. Walks `cypressRoots` (from `selector-contract.config.json`) and parses every `.ts`/`.tsx`/`.js`/`.jsx` file with the TypeScript compiler API, pulling out every string literal passed as the first argument to a selector-shaped call (`cy.get`, `.find`, `.contains`, `.xpath`, etc.) that looks like a selector. +2. Walks `applicationRoots` for `.cshtml`/`.razor`/`.html`/`.cs`/`.js`/`.ts` source. +3. For each selector, extracts identifying tokens (`#id`, `[data-cy=...]`, `[data-testid=...]`) and checks whether that literal token appears anywhere in the app source (`id="..."`, `asp-for="@Model...."` with underscores mapped to dots, etc.). +4. Classifies each selector: + - **status**: `matched` (evidence found) / `missing` (identifying token, no evidence) / `unverified` (no identifying token to check — a structural CSS selector) / `exempt` (matches an `ownershipRules` pattern in `selector-contract.config.json`: identity-provider markup, framework-generated selectors like Bootstrap-select/Select2/DataTables/SweetAlert2, or Form.io/CHEFS dynamic fields). + - **syntaxKind**: `css` or `xpath`. Nothing in this repo uses XPath today — this exists to flag it immediately if it ever shows up, since XPath selectors are harder to keep in sync with markup than `id`/`data-cy`. + +`matched` proves the token exists somewhere in source. It does **not** prove the element is visible, enabled, permission-gated correctly, or on the route the test expects — it's a fast static check, not a substitute for running the spec. + +## How the diff/fix works (`scripts/selector-diff-report.mjs`) + +No git-diff parsing, no guessing at what changed. It just runs the same full scan against the current tree, fetches the baseline `registry.json` via `git show :.../registry.json`, and compares `status` per selector by exact string match. + +- **Regression** = a selector's status is worse now than at baseline. Only `matched → missing` regressions are eligible for auto-fix — that's the one case with concrete proof the selector used to work. +- **Fix candidate search**: for each missing token, look up which file(s) had it at baseline (`applicationMatches` in the baseline entry), pull the exact old line via `git show`, then score every line in the *current* version of that file by word-overlap similarity to the old line (with a small bonus for being near the original line number — renames overwhelmingly stay in place). One clear winner → propose restoring the attribute there. Zero or multiple close-scoring candidates → reported as "NEEDS REVIEW," nothing is touched. +- **`--apply`** only ever writes the unambiguous fixes. It inserts the missing attribute next to whatever's already on that element — it never removes or rewrites existing attributes, never invents a token value without baseline evidence, and never auto-commits. + +Treat an applied fix like any other code change: read the diff, and run the actual Cypress spec that uses the selector before trusting it — restoring the token proves the selector *resolves* again, not that the underlying behavior is correct. + +## As a Claude Code skill + +`applications/Unity.AutoUI/.claude/skills/validate-cypress-selectors/SKILL.md` documents this same system for on-demand use inside a Claude Code session — invoke it by name when working on Cypress selectors, page objects, or Razor/C#/JS markup that backs them. + +## Configuration + +- **`selector-contract.config.json`** (repo root of `Unity.AutoUI`) — `cypressRoots`, `applicationRoots`, and `ownershipRules` (regex patterns + reason strings for what's exempt from the static contract). + +## Files that need to be committed for this to work for everyone + +| File | Why | +|---|---| +| `scripts/selector-contract.mjs` | The scanner/classifier — also exports the functions `selector-diff-report.mjs` reuses. | +| `scripts/selector-diff-report.mjs` | The baseline-diff + fix logic. | +| `selector-contract.config.json` | Scan roots and ownership exemptions. | +| `package.json` | `selectors:report` / `selectors:diff` / `selectors:fix` / `selectors:apply` npm scripts. | +| `.claude/skills/validate-cypress-selectors/SKILL.md` | The on-demand skill definition. | +| `cypress/selectors/registry.json` | **The baseline itself.** Without this committed on `main`, `selectors:diff`/`fix`/`apply` have nothing to compare against and will just print a "no baseline found" message. | +| `cypress/selectors/README.md` | This file. | diff --git a/applications/Unity.AutoUI/cypress/selectors/registry.json b/applications/Unity.AutoUI/cypress/selectors/registry.json new file mode 100644 index 0000000000..f2d6b48741 --- /dev/null +++ b/applications/Unity.AutoUI/cypress/selectors/registry.json @@ -0,0 +1,5481 @@ +{ + "schemaVersion": 2, + "mode": "report-only", + "generatedAt": "2026-08-19T20:53:21.994Z", + "summary": { + "total": 360, + "matched": 146, + "missing": 42, + "unverified": 81, + "exempt": 91, + "syntax": { + "css": 360, + "xpath": 0 + } + }, + "entries": [ + { + "selector": ".application-details-breadcrumb .application-status", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:67" + ], + "applicationMatches": [] + }, + { + "selector": ".card-header", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:293", + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:303", + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:313" + ], + "applicationMatches": [] + }, + { + "selector": ".checkbox-select", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:58", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:225", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:60", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:440" + ], + "applicationMatches": [] + }, + { + "selector": ".checkbox-select.chkbox", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:81" + ], + "applicationMatches": [] + }, + { + "selector": ".choices__item--selectable", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:369" + ], + "applicationMatches": [] + }, + { + "selector": ".display-input", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:72", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:75", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:76", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:77", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:78", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:79", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:80", + "applications/Unity.AutoUI/cypress/pages/BasePage.ts:139" + ], + "applicationMatches": [] + }, + { + "selector": ".display-input-label", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:246", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:257" + ], + "applicationMatches": [] + }, + { + "selector": ".dt-scroll-body", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:47" + ], + "applicationMatches": [] + }, + { + "selector": ".dt-scroll-body tbody tr", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:48" + ], + "applicationMatches": [] + }, + { + "selector": ".dt-scroll-head", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:49" + ], + "applicationMatches": [] + }, + { + "selector": ".dt-scroll-head span.dt-column-title", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:50", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:893" + ], + "applicationMatches": [] + }, + { + "selector": ".login-pf-page", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:19" + ], + "applicationMatches": [] + }, + { + "selector": ".modal-backdrop", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:446", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:62", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:166", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:251", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:292" + ], + "applicationMatches": [] + }, + { + "selector": ".modal-content", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:418" + ], + "applicationMatches": [] + }, + { + "selector": ".modal-footer", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:438", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:454" + ], + "applicationMatches": [] + }, + { + "selector": ".modal-footer button", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:404" + ], + "applicationMatches": [] + }, + { + "selector": ".modal-title", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:419", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:376", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:409", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:445", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:471" + ], + "applicationMatches": [] + }, + { + "selector": ".modal.show", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:117", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:119", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:89", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:642", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:67", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:165", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:241", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:279", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:291" + ], + "applicationMatches": [] + }, + { + "selector": ".modal.show .btn-close", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:650", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:651" + ], + "applicationMatches": [] + }, + { + "selector": ".modal.show .btn-close, .modal.show [data-bs-dismiss='modal'], .modal.show button.close", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:284" + ], + "applicationMatches": [] + }, + { + "selector": ".modal.show .modal-content", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:248" + ], + "applicationMatches": [] + }, + { + "selector": ".modal.show .modal-content:contains('Confirm Action')", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:238" + ], + "applicationMatches": [] + }, + { + "selector": ".modal.show button", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:641" + ], + "applicationMatches": [] + }, + { + "selector": ".modal.show, .modal.fade.show", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:443" + ], + "applicationMatches": [] + }, + { + "selector": ".right-card", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:13" + ], + "applicationMatches": [] + }, + { + "selector": ".select-all-applications", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:80" + ], + "applicationMatches": [] + }, + { + "selector": ".select2-dropdown", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:17" + ], + "applicationMatches": [] + }, + { + "selector": ".submission-attachments-table, [id*='SubmissionAttachments']", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:93" + ], + "applicationMatches": [] + }, + { + "selector": ".summary-table", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:48" + ], + "applicationMatches": [] + }, + { + "selector": ".swal2-close", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:189", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:190" + ], + "applicationMatches": [] + }, + { + "selector": ".swal2-confirm", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:105", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:106", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:191", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:192", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:638" + ], + "applicationMatches": [] + }, + { + "selector": ".swal2-container", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:101", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:187", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:188", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:197", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:90", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:96", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:98", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:637", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:652", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:68", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:164", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:233", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:276" + ], + "applicationMatches": [] + }, + { + "selector": ".swal2-container .swal2-confirm", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:636", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:272", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:273" + ], + "applicationMatches": [] + }, + { + "selector": ".swal2-container .swal2-icon.swal2-error", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:630" + ], + "applicationMatches": [] + }, + { + "selector": ".swal2-container button", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:110" + ], + "applicationMatches": [] + }, + { + "selector": ".swal2-popup", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:236", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:39" + ], + "applicationMatches": [] + }, + { + "selector": ".swal2-popup .swal2-confirm", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:229", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:230" + ], + "applicationMatches": [] + }, + { + "selector": ".swal2-popup button", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:237", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:243" + ], + "applicationMatches": [] + }, + { + "selector": ".swal2-popup, .modal.show", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:46" + ], + "applicationMatches": [] + }, + { + "selector": ".tags-suggestion-container .tags-suggestion-element", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:391", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:394" + ], + "applicationMatches": [] + }, + { + "selector": ".unity-user-initials", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:73", + "applications/Unity.AutoUI/cypress/pages/LoginPage.ts:15", + "applications/Unity.AutoUI/cypress/pages/NavigationPage.ts:16", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:30", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:37" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"1\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:357" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"10\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:363" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"11\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:364" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"12\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:365" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"15\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:366" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"2\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:358" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"3\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:359" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"31\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:367" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"32\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:368" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"4\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:360" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"5\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:361" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"63\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:369" + ], + "applicationMatches": [] + }, + { + "selector": "[data-dt-column=\"9\"] .dt-column-order", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:362" + ], + "applicationMatches": [] + }, + { + "selector": "[role=\"combobox\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:22" + ], + "applicationMatches": [] + }, + { + "selector": "#${fieldId}", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:313" + ], + "applicationMatches": [] + }, + { + "selector": "#addLinkBtn", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:addLinkBtn" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:102" + ], + "applicationMatches": [] + }, + { + "selector": "#AdjudicationTeamLeadActionBar #CompleteButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:CompleteButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:63" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js" + ] + }, + { + "selector": "#app > div > main > div.v-container.v-locale--is-ltr.text-center.main > div > div:nth-child(2) > div > button", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/commands.ts:169", + "applications/Unity.AutoUI/cypress/support/commands.ts:173" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkApprovals/ApproveApplicationsModal.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAttachments/ApplicantAttachments.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantPayments/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/ListMerge.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js" + ] + }, + { + "selector": "#app > div > main > header > header > div > div.d-print-none", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/commands.ts:154", + "applications/Unity.AutoUI/cypress/support/commands.ts:157" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkApprovals/ApproveApplicationsModal.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAttachments/ApplicantAttachments.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantPayments/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/ListMerge.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js" + ] + }, + { + "selector": "#app_custom_buttons", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:38" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/PermissionRoleMatrix.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/PermissionUserMatrix.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentConfigurations/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentActionBar/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantPrograms/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Intakes/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/PaymentHistory/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/Default.cshtml" + ] + }, + { + "selector": "#ApplicantInfo_MailingAddressCity", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_MailingAddressCity" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:94" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantInfo_MailingAddressPostalCode", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_MailingAddressPostalCode" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:96" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantInfo_MailingAddressProvince", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_MailingAddressProvince" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:95" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantInfo_MailingAddressStreet", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_MailingAddressStreet" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:91" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantInfo_MailingAddressStreet2", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_MailingAddressStreet2" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:92" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantInfo_MailingAddressUnit", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_MailingAddressUnit" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:93" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantInfo_SigningAuthorityBusinessPhone", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_SigningAuthorityBusinessPhone" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:101" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantInfo_SigningAuthorityCellPhone", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_SigningAuthorityCellPhone" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:102" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantInfo_SigningAuthorityEmail", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_SigningAuthorityEmail" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:99" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantInfo_SigningAuthorityFullName", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_SigningAuthorityFullName" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:97" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantInfo_SigningAuthorityTitle", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantInfo_SigningAuthorityTitle" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:98" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_ApplicantName", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:117" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js" + ] + }, + { + "selector": "#ApplicantSummary_ContactBusinessPhone", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_ContactBusinessPhone" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:83" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_ContactCellPhone", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_ContactCellPhone" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:84" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_ContactEmail", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_ContactEmail" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:82" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_ContactFullName", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_ContactFullName" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:80" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_ContactTitle", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_ContactTitle" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:81" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_OrgName", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:78" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/SupplierInfo.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js" + ] + }, + { + "selector": "#ApplicantSummary_OrgNumber", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:79" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js" + ] + }, + { + "selector": "#ApplicantSummary_PhysicalAddressCity", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_PhysicalAddressCity" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:88" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_PhysicalAddressPostalCode", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_PhysicalAddressPostalCode" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:90" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_PhysicalAddressProvince", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_PhysicalAddressProvince" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:89" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_PhysicalAddressStreet", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_PhysicalAddressStreet" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:85" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_PhysicalAddressStreet2", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_PhysicalAddressStreet2" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:86" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_PhysicalAddressUnit", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicantSummary_PhysicalAddressUnit" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:87" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicantSummary_Sector", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:119", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:140" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js" + ] + }, + { + "selector": "#ApplicantSummary_SectorSubSectorIndustryDesc", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:123", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:104" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js" + ] + }, + { + "selector": "#ApplicantSummary_SubSector", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:120", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:169" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js" + ] + }, + { + "selector": "#Application_ApproveButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:Application_ApproveButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:29", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:59" + ], + "applicationMatches": [] + }, + { + "selector": "#application_attachment_count", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:39" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js" + ] + }, + { + "selector": "#Application_CloseButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:Application_CloseButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:31" + ], + "applicationMatches": [] + }, + { + "selector": "#application_comments_count", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:38" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CommentsWidget/Default.cshtml" + ] + }, + { + "selector": "#Application_CompleteAssessmentButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:Application_CompleteAssessmentButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:28", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:58" + ], + "applicationMatches": [] + }, + { + "selector": "#Application_CompleteReviewButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:Application_CompleteReviewButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:26", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:56" + ], + "applicationMatches": [] + }, + { + "selector": "#Application_DeferButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:Application_DeferButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:33" + ], + "applicationMatches": [] + }, + { + "selector": "#Application_DenyButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:Application_DenyButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:30" + ], + "applicationMatches": [] + }, + { + "selector": "#application_emails_count", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:37" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml" + ] + }, + { + "selector": "#application_links_count", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:40" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js" + ] + }, + { + "selector": "#Application_OnHoldButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:Application_OnHoldButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:34" + ], + "applicationMatches": [] + }, + { + "selector": "#Application_StartAssessmentButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:Application_StartAssessmentButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:27", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:57" + ], + "applicationMatches": [] + }, + { + "selector": "#Application_StartReviewButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:Application_StartReviewButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:25", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:55" + ], + "applicationMatches": [] + }, + { + "selector": "#application_upload", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:application_upload" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:95" + ], + "applicationMatches": [] + }, + { + "selector": "#application_upload_btn", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:application_upload_btn" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:94" + ], + "applicationMatches": [] + }, + { + "selector": "#Application_WithdrawButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:Application_WithdrawButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:32" + ], + "applicationMatches": [] + }, + { + "selector": "#ApplicationActionDropdown", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:22" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationActionWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationActionWidget/Default.js" + ] + }, + { + "selector": "#ApplicationActionDropdown .dropdown-menu", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:24", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:54" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationActionWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationActionWidget/Default.js" + ] + }, + { + "selector": "#ApplicationActionDropdown .dropdown-toggle", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:23", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:53" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationActionWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationActionWidget/Default.js" + ] + }, + { + "selector": "#applicationAssigneeChart > div > svg > g > text:nth-child(1)", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:11" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.js" + ] + }, + { + "selector": "#ApplicationAttachmentsTable", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:92" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationAttachments/ApplicationAttachments.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationAttachments/Default.cshtml" + ] + }, + { + "selector": "#ApplicationCount", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:456" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatusModal.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequests.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequestsModal.js" + ] + }, + { + "selector": "#ApplicationHistoryTable", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ApplicationHistoryTable" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:107" + ], + "applicationMatches": [] + }, + { + "selector": "#applicationLink", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:66", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:67", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:75", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:91" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js" + ] + }, + { + "selector": "#ApplicationLinksTable", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:101" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js" + ] + }, + { + "selector": "#applicationPaymentRequest", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:382", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:90" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js" + ] + }, + { + "selector": "#applicationStatusChart > div > svg > g > text:nth-child(1)", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:7" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.js" + ] + }, + { + "selector": "#applicationStatusChart text", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:95" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.js" + ] + }, + { + "selector": "#applicationStatusWidget", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:45" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js" + ] + }, + { + "selector": "#applicationTagsWidget", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:46" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js" + ] + }, + { + "selector": "#ApprovalView_ApprovedAmount", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:81", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:340" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentResults/Default.cshtml" + ] + }, + { + "selector": "#ApprovalView_FinalDecisionDate", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:82", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:346" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentResults/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentResults/Default.js" + ] + }, + { + "selector": "#approveApplications", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:88" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkApprovals/ApproveApplicationsModal.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js" + ] + }, + { + "selector": "#AssessmentId", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:54" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260721203242_Initial.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260721203242_Initial.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentResultAttachments/AssessmentResultAttachments.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/_Shared/Attachments.js" + ] + }, + { + "selector": "#assessmentMainView", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:87" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml" + ] + }, + { + "selector": "#assignApplication", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:87", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:370" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js" + ] + }, + { + "selector": "#AssigneeId", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:380" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260721203242_Initial.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260721203242_Initial.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AssigneeSelection/AssigneeSelectionModal.cshtml" + ] + }, + { + "selector": "#attachments", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:30", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:573" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/ApplicationAnalysisOperationInputDto.cs", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/ApplicationScoringOperationInputDto.cs", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/ApplicationAnalysisRequest.cs", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/ApplicationScoringRequest.cs", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/PromptDataPayloadBuilder.cs", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationManager.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAttachments/ApplicantAttachments.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationAttachments/ApplicationAttachments.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js" + ] + }, + { + "selector": "#attachments input[type='file']", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:546" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/ApplicationAnalysisOperationInputDto.cs", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Operations/ApplicationScoringOperationInputDto.cs", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/ApplicationAnalysisRequest.cs", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Requests/ApplicationScoringRequest.cs", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Runtime/Prompts/PromptDataPayloadBuilder.cs", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationManager.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAttachments/ApplicantAttachments.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationAttachments/ApplicationAttachments.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js" + ] + }, + { + "selector": "#attachments-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:20" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantAttachments/ApplicantAttachments.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationAttachments/ApplicationAttachments.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js" + ] + }, + { + "selector": "#bcc-input-row", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:288" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#bs-select-1[role=\"listbox\"]", + "kind": "id", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [ + "id:bs-select-1" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:18" + ], + "applicationMatches": [] + }, + { + "selector": "#btn-cancel-email", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:78" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#btn-confirm-send", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:127", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:140", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:91", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:79", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:161" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#btn-new-email", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:215", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:67", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:116" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#btn-save", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:76", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:135" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/Default.js", + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/FormConfiguration/Notifications.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Template/Template.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Notifications.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/js/formConfiguration/Notifications.js" + ] + }, + { + "selector": "#btn-save-top, #btn-save", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:316", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:340" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIConfiguration/Default.js", + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/Mapping.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/FormConfiguration/Notifications.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Template/Template.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/Notifications/Notifications.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/wwwroot/js/formConfiguration/Notifications.js" + ] + }, + { + "selector": "#btn-send", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:77", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:154" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#btn-send-top, #btn-send", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:339", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:346" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#btn-show-bcc", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:290" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#btn-toggle-filter", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:92" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Onboarding/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Onboarding/Index.js", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Reconciliation/Index.js", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js", + "applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/plugins/filterRow.js", + "applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/plugins/tableContextMenu.js", + "applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/table-utils.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.js" + ] + }, + { + "selector": "#btnSubmitPayment", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:466" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatusModal.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequests.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequestsModal.js" + ] + }, + { + "selector": "#cleanGrowth", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:58" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js" + ] + }, + { + "selector": "#closeSummaryCanvas", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:82", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:77" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml" + ] + }, + { + "selector": "#comments", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:29" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/ScoresheetFieldSchemaParserTests.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml" + ] + }, + { + "selector": "#comments .add-comment-cancel-button", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:86" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/ScoresheetFieldSchemaParserTests.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml" + ] + }, + { + "selector": "#comments .add-comment-save-button", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:85" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/ScoresheetFieldSchemaParserTests.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml" + ] + }, + { + "selector": "#comments .comment-input", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:84" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/ScoresheetFieldSchemaParserTests.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml" + ] + }, + { + "selector": "#comments .comments-container", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:87" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Flex/test/Unity.Flex.Application.Tests/Reporting/ScoresheetFieldSchemaParserTests.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml" + ] + }, + { + "selector": "#comments-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:19" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml" + ] + }, + { + "selector": "#communities", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:104", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:69" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js" + ] + }, + { + "selector": "#CompleteButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:CompleteButton" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:86" + ], + "applicationMatches": [] + }, + { + "selector": "#ContactInfo_Email", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:135" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml" + ] + }, + { + "selector": "#ContactInfo_Name", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:133" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml" + ] + }, + { + "selector": "#ContactInfo_Phone", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:ContactInfo_Phone" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:136" + ], + "applicationMatches": [] + }, + { + "selector": "#ContactInfo_Phone2", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:137" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml" + ] + }, + { + "selector": "#ContactInfo_Title", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:134" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml" + ] + }, + { + "selector": "#CreateButton", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:85", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:611", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:612" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentConfigurations/Index.js", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ConfigurationManagement/PaymentConfigurations.js" + ] + }, + { + "selector": "#details", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:27" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantsPortal/Messages/MessageAcknowledgment.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js" + ] + }, + { + "selector": "#details-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:17" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml" + ] + }, + { + "selector": "#detailsTabContent", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:14" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js" + ] + }, + { + "selector": "#dynamicButtonContainerId", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:39" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/PermissionRoleMatrix.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/PermissionRoleMatrix.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/PermissionUserMatrix.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/PermissionUserMatrix.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentConfigurations/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentConfigurations/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentActionBar/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js" + ] + }, + { + "selector": "#dynamicButtonContainerId .dt-buttons button, #dynamicButtonContainerId .dt-buttons button span", + "kind": "id", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:41", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:419", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:604" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/PermissionRoleMatrix.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/PermissionRoleMatrix.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/PermissionUserMatrix.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/PermissionUserMatrix.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentConfigurations/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentConfigurations/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentActionBar/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js" + ] + }, + { + "selector": "#dynamicButtonContainerId button.grp-savedStates", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:407" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/PermissionRoleMatrix.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/PermissionRoleMatrix.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/index.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/PermissionUserMatrix.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/PermissionUserMatrix.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/index.js", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentConfigurations/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentConfigurations/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentActionBar/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.js" + ] + }, + { + "selector": "#economicImpact", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:56" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js" + ] + }, + { + "selector": "#economicRegionChart > div > svg > g > text:nth-child(1)", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:9" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.js" + ] + }, + { + "selector": "#economicRegions", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:102", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:67" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js" + ] + }, + { + "selector": "#EmailBCC", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:294", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:299", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:336", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:72", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:132", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:151" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#EmailBody", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:252", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:75" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#EmailCC", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:275", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:280", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:335", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:71", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:131", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:150" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#EmailForm", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:332", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:68" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#EmailFrom", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:73" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#EmailHistoryTable", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:68", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:70" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailHistoryWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailHistoryWidget/Default.js" + ] + }, + { + "selector": "#EmailHistoryTable td", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:321" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailHistoryWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailHistoryWidget/Default.js" + ] + }, + { + "selector": "#emails", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:28", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:141" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailHistoryWidget/Default.js" + ] + }, + { + "selector": "#emails-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:203", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:207", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:18", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:107" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailHistoryWidget/Default.js" + ] + }, + { + "selector": "#EmailSubject", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:305", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:310", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:337", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:74", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:133", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:152" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#EmailTo", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:219", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:264", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:269", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:334", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:70", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:130", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:149" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#externalLink", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:85", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:235", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:76", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:86" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js" + ] + }, + { + "selector": "#financialAnalysis", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:55" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js" + ] + }, + { + "selector": "#formio", + "kind": "id", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:17" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js" + ] + }, + { + "selector": "#GrantApplicationsTable", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:79", + "applications/Unity.AutoUI/cypress/support/auth.ts:31" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js" + ] + }, + { + "selector": "#GrantApplicationsTable tbody", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:82" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js" + ] + }, + { + "selector": "#GrantApplicationsTable tbody a[href^=\"/GrantApplications/Details?ApplicationId=\"]", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:172", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:177" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js" + ] + }, + { + "selector": "#GrantApplicationsTable tbody a[href^=\"/GrantApplications/Details\"]", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:98" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js" + ] + }, + { + "selector": "#GrantApplicationsTable tbody tr", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:83" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js" + ] + }, + { + "selector": "#GrantApplicationsTable tbody tr td a", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:84" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js" + ] + }, + { + "selector": "#history", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:32" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/HistoryWidget/Default.js" + ] + }, + { + "selector": "#history-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:22" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/HistoryWidget/Default.js" + ] + }, + { + "selector": "#inclusiveGrowth", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:57" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js" + ] + }, + { + "selector": "#links", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:31" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FieldsProviders/WorksheetFieldsProvider.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicationLinksWidget/Default.js" + ] + }, + { + "selector": "#links-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:21" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml" + ] + }, + { + "selector": "#main-left", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:666" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js" + ] + }, + { + "selector": "#modal-content", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:158" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js", + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfigurationViewStatus/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Onboarding/Index.js", + "applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/json-editor.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantsActionBar/ListMerge.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#nav-funding-agreement-info-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:nav-funding-agreement-info-tab" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:16" + ], + "applicationMatches": [] + }, + { + "selector": "#nav-organization-info-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:15" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/SupplierInfo.js" + ] + }, + { + "selector": "#nav-payment-info-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:175", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:17", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:155" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js" + ] + }, + { + "selector": "#nav-project-info-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:nav-project-info-tab" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:93", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:14" + ], + "applicationMatches": [] + }, + { + "selector": "#nav-review-and-assessment", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:16" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js" + ] + }, + { + "selector": "#nav-review-and-assessment-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:88", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:13" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js" + ] + }, + { + "selector": "#nav-summery", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:15" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml" + ] + }, + { + "selector": "#nav-summery-tab", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "missing", + "missingTokens": [ + "id:nav-summery-tab" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:179", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:12" + ], + "applicationMatches": [] + }, + { + "selector": "#Note", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:458", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:464" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260721203242_Initial.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicantHistory/ReportsHistoryModalViewModel.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantHistory/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CustomFields/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/PaymentConfiguration/Default.js" + ] + }, + { + "selector": "#password", + "kind": "id", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/LoginPage.ts:13", + "applications/Unity.AutoUI/cypress/support/commands.ts:185", + "applications/Unity.AutoUI/cypress/support/commands.ts:24" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FormsIoMappingUtils.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/TokenModal.js" + ] + }, + { + "selector": "#password, input[name='password'], input[type='password']", + "kind": "id", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:45" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/FormsIoMappingUtils.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationForms/TokenModal.js" + ] + }, + { + "selector": "#payment-modal", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:61" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatusModal.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequests.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequestsModal.js" + ] + }, + { + "selector": "#payment-modal .modal-footer button", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:63" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatusModal.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequests.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequestsModal.js" + ] + }, + { + "selector": "#payment-modal input[id$='__Description']", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:820" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatusModal.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequests.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/CreatePaymentRequestsModal.js" + ] + }, + { + "selector": "#ProjectInfo_Acquisition", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:99", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:64" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml" + ] + }, + { + "selector": "#ProjectInfo_CommunityPopulation", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:105", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:70" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml" + ] + }, + { + "selector": "#ProjectInfo_ElectoralDistrict", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:106", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:71" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml" + ] + }, + { + "selector": "#ProjectInfo_Forestry", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:100", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:65" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml" + ] + }, + { + "selector": "#ProjectInfo_ForestryFocus", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:101", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:66" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml" + ] + }, + { + "selector": "#ProjectInfo_Place", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:107", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:72" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml" + ] + }, + { + "selector": "#ProjectInfo_ProjectEndDate", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:96", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:61" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml" + ] + }, + { + "selector": "#ProjectInfo_ProjectName", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:94", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:59" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml" + ] + }, + { + "selector": "#recommendation_reset_btn", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:62" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js" + ] + }, + { + "selector": "#recommendation_select", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:61" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js" + ] + }, + { + "selector": "#regionalDistricts", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:103", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:68" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js" + ] + }, + { + "selector": "#RequestedAmount", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:176" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Application/TenantManagement/OnboardingCoreFieldRegistry.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260721203242_Initial.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantSubmissions/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentResults/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js" + ] + }, + { + "selector": "#RequestedAmountInputAR", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:89" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentResults/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentResults/Default.js" + ] + }, + { + "selector": "#RequestedAmountInputPI", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:97", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:62" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js" + ] + }, + { + "selector": "#reviewDetails", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:53" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js" + ] + }, + { + "selector": "#saveAssessmentScoresBtn", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:60" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js" + ] + }, + { + "selector": "#savePaymentInfoBtn", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:347" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js" + ] + }, + { + "selector": "#search", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:33", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:45", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:46", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:50", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:211", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:46", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:877", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:89" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIPromptsWidget/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/DataGridWidget/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/PermissionRoleMatrix.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Roles/PermissionRoleMatrix.js", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/PermissionUserMatrix.cshtml", + "applications/Unity.GrantManager/modules/Unity.Identity.Web/src/Pages/Identity/Users/PermissionUserMatrix.js", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentConfigurations/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentConfigurations/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentActionBar/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/EndpointManagement/Endpoints/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Onboarding/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Onboarding/Index.js", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Reconciliation/Index.js", + "applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml" + ] + }, + { + "selector": "#search-grant-programs", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:75", + "applications/Unity.AutoUI/cypress/pages/NavigationPage.ts:79", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:55" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantPrograms/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantPrograms/Index.js" + ] + }, + { + "selector": "#select2-dashboardIntakeId-container", + "kind": "id", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [ + "id:select2-dashboardIntakeId-container" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:16" + ], + "applicationMatches": [] + }, + { + "selector": "#Site_PaymentGroup", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:428" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/SupplierInfo.js" + ] + }, + { + "selector": "#SiteInfoTable", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:664", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:665" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/SupplierInfo.js" + ] + }, + { + "selector": "#SiteInfoTable tbody tr", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:380", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:391", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:405", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:675" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/SupplierInfo.js" + ] + }, + { + "selector": "#social-azureidir", + "kind": "id", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [ + "id:social-azureidir" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:21", + "applications/Unity.AutoUI/cypress/support/auth.ts:96", + "applications/Unity.AutoUI/cypress/support/auth.ts:98" + ], + "applicationMatches": [] + }, + { + "selector": "#social-idir", + "kind": "id", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [ + "id:social-idir" + ], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:157", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:158", + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:64", + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:65", + "applications/Unity.AutoUI/cypress/support/auth.ts:101", + "applications/Unity.AutoUI/cypress/support/auth.ts:20", + "applications/Unity.AutoUI/cypress/support/auth.ts:99", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:89", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:90" + ], + "applicationMatches": [] + }, + { + "selector": "#startDate", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:95", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:60" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Intakes/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js" + ] + }, + { + "selector": "#subsectorRequestedAmountChart > div > svg > g > text:nth-child(1)", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:13" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.js" + ] + }, + { + "selector": "#subTotal", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:59" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentScoresWidget/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ReviewList/ReviewList.js" + ] + }, + { + "selector": "#summaryWidgetArea", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:47" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js" + ] + }, + { + "selector": "#SupplierNumber", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:327" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/SupplierInfo/SupplierInfo.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260721203242_Initial.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260805212847_AddGenerationReviews.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806201536_AB33799_HardenCheckboxGroupReportingViews.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260806202244_AB33799_FixCheckboxGroupEmptyValues.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260807224046_AB33996_AddApplicantFiscalYearEndRestricted.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260813164219_AB33234_ApplicantPortalExternalLinks.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/20260818211028_AB33234_ExternalLinksApplicantMessage.Designer.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.EntityFrameworkCore/Migrations/TenantMigrations/GrantTenantDbContextModelSnapshot.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ApplicantInfo/Default.js" + ] + }, + { + "selector": "#tagApplication", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:89" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationTags/ApplicationTags.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/DetailsActionBar/Default.js" + ] + }, + { + "selector": "#template", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:69", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:124" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Zones/ZoneManager.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Template/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "#TotalBudgetInputAR", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:90" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentResults/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/AssessmentResults/Default.js" + ] + }, + { + "selector": "#TotalBudgetInputPI", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:98", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:63" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ProjectInfo/Default.js" + ] + }, + { + "selector": "#UpdateTotalAmount", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:457" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatus.cshtml", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentApprovals/UpdatePaymentRequestStatusModal.js" + ] + }, + { + "selector": "#user", + "kind": "id", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/LoginPage.ts:12", + "applications/Unity.AutoUI/cypress/support/commands.ts:182", + "applications/Unity.AutoUI/cypress/support/commands.ts:184", + "applications/Unity.AutoUI/cypress/support/commands.ts:21", + "applications/Unity.AutoUI/cypress/support/commands.ts:23" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js", + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs", + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js", + "applications/Unity.GrantManager/modules/Unity.Reporting/test/Unity.Reporting.Application.Tests/Configuration/ColumnsMappingServiceTests.cs" + ] + }, + { + "selector": "#user-dropdown .btn-dropdown span", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/LoginPage.ts:16", + "applications/Unity.AutoUI/cypress/pages/NavigationPage.ts:17" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/layout.js" + ] + }, + { + "selector": "#user-dropdown a.dropdown-item", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:74", + "applications/Unity.AutoUI/cypress/pages/NavigationPage.ts:58", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:40" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/layout.js" + ] + }, + { + "selector": "#user-tags-input", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:384", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:400" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/AssigneeSelection/AssigneeSelection.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js" + ] + }, + { + "selector": "#user, input[name='user'], input[name='username']", + "kind": "id", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:44" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js", + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Application/Configuration/ReportMappingUtils.cs", + "applications/Unity.GrantManager/modules/Unity.Reporting/src/Unity.Reporting.Web/Views/Shared/Components/ReportingConfiguration/Default.js", + "applications/Unity.GrantManager/modules/Unity.Reporting/test/Unity.Reporting.Application.Tests/Configuration/ColumnsMappingServiceTests.cs" + ] + }, + { + "selector": "#UserGrantProgramsTable", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/NavigationPage.ts:85", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:60" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantPrograms/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantPrograms/Index.js" + ] + }, + { + "selector": "#UserGrantProgramsTable tbody tr", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:76" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantPrograms/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantPrograms/Index.js" + ] + }, + { + "selector": "a.dropdown-item", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:55", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:82" + ], + "applicationMatches": [] + }, + { + "selector": "a.dropdown-item[role=\"option\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:54", + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:83" + ], + "applicationMatches": [] + }, + { + "selector": "a.nav-link", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:110" + ], + "applicationMatches": [] + }, + { + "selector": "a[id^='social-']", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:106" + ], + "applicationMatches": [] + }, + { + "selector": "body", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:156", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:186", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:194", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:235", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:287", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:45", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:67", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:88", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:95", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:36", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:55", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:65", + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:63", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:339", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:442", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:520", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:526", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:628", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:513", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:477", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:493", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:495", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:499", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:510", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:569", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:673", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:684", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:695", + "applications/Unity.AutoUI/cypress/pages/BasePage.ts:87", + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:106", + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:31", + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:43", + "applications/Unity.AutoUI/cypress/pages/NavigationPage.ts:43", + "applications/Unity.AutoUI/cypress/pages/NavigationPage.ts:55", + "applications/Unity.AutoUI/cypress/pages/NavigationPage.ts:68", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:121", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:210", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:214", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:219", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:258", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:264", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:271", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:289", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:298", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:345", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:389", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:416", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:610", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:642", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:674", + "applications/Unity.AutoUI/cypress/support/auth.ts:117", + "applications/Unity.AutoUI/cypress/support/auth.ts:149", + "applications/Unity.AutoUI/cypress/support/auth.ts:175", + "applications/Unity.AutoUI/cypress/support/auth.ts:203", + "applications/Unity.AutoUI/cypress/support/auth.ts:218", + "applications/Unity.AutoUI/cypress/support/auth.ts:54", + "applications/Unity.AutoUI/cypress/support/auth.ts:94", + "applications/Unity.AutoUI/cypress/support/commands.ts:146", + "applications/Unity.AutoUI/cypress/support/commands.ts:163", + "applications/Unity.AutoUI/cypress/support/commands.ts:180", + "applications/Unity.AutoUI/cypress/support/commands.ts:19", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:27", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:39", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:47", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:88" + ], + "applicationMatches": [] + }, + { + "selector": "button", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:132", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:52", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:367", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:439", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:455", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:643", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:718", + "applications/Unity.AutoUI/cypress/pages/NavigationPage.ts:91", + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:415", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:244", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:442", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:825", + "applications/Unity.AutoUI/cypress/support/auth.ts:151", + "applications/Unity.AutoUI/cypress/support/auth.ts:178", + "applications/Unity.AutoUI/cypress/support/auth.ts:213", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:66" + ], + "applicationMatches": [] + }, + { + "selector": "button, a", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:407", + "applications/Unity.AutoUI/cypress/support/commands.ts:149", + "applications/Unity.AutoUI/cypress/support/commands.ts:166" + ], + "applicationMatches": [] + }, + { + "selector": "button, a, [role='button']", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/library/chefs.cy.ts:11", + "applications/Unity.AutoUI/cypress/e2e/library/chefs.cy.ts:12", + "applications/Unity.AutoUI/cypress/e2e/library/chefs.cy.ts:6" + ], + "applicationMatches": [] + }, + { + "selector": "button:contains('Add Attachments'), .add-attachments-btn, [id*='addAttachment']", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:96" + ], + "applicationMatches": [] + }, + { + "selector": "button:contains('IDIR'), a:contains('IDIR')", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/commands.ts:165" + ], + "applicationMatches": [] + }, + { + "selector": "button:contains('LOGIN'), a:contains('LOGIN')", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/commands.ts:148" + ], + "applicationMatches": [] + }, + { + "selector": "button:contains(\"Continue\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:150" + ], + "applicationMatches": [] + }, + { + "selector": "button:contains(\"LOGIN\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:39", + "applications/Unity.AutoUI/cypress/support/commands.ts:49" + ], + "applicationMatches": [] + }, + { + "selector": "button:contains(\"Save\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:83" + ], + "applicationMatches": [] + }, + { + "selector": "button:contains(\"VIEW APPLICATIONS\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:176", + "applications/Unity.AutoUI/cypress/support/auth.ts:30", + "applications/Unity.AutoUI/cypress/support/auth.ts:50" + ], + "applicationMatches": [] + }, + { + "selector": "button:not([disabled])", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:739" + ], + "applicationMatches": [] + }, + { + "selector": "button.grp-savedStates", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:42", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:81" + ], + "applicationMatches": [] + }, + { + "selector": "button.swal2-cancel", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:41" + ], + "applicationMatches": [] + }, + { + "selector": "button.swal2-confirm", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:40" + ], + "applicationMatches": [] + }, + { + "selector": "button[data-id=\"dashboardIntakeId\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:17" + ], + "applicationMatches": [] + }, + { + "selector": "button[type='submit']", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:154", + "applications/Unity.AutoUI/cypress/support/auth.ts:155" + ], + "applicationMatches": [] + }, + { + "selector": "div.display-input", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:73", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:74" + ], + "applicationMatches": [] + }, + { + "selector": "div.dt-button-background", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:56" + ], + "applicationMatches": [] + }, + { + "selector": "div.spinner-grow[role=\"status\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:22" + ], + "applicationMatches": [] + }, + { + "selector": "fieldset[name=\"Unity_GrantManager_ApplicationManagement_Applicant_Contact\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:129" + ], + "applicationMatches": [] + }, + { + "selector": "fieldset[name=\"Unity_GrantManager_ApplicationManagement_Applicant_Summary\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:113" + ], + "applicationMatches": [] + }, + { + "selector": "fieldset[name$=\"Applicant_Summary\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:77" + ], + "applicationMatches": [] + }, + { + "selector": "form", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:158" + ], + "applicationMatches": [] + }, + { + "selector": "h4", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:194", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:197", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:296" + ], + "applicationMatches": [] + }, + { + "selector": "h4.card-title", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:109", + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:146" + ], + "applicationMatches": [] + }, + { + "selector": "h4.card-title:contains(\"1. INTRODUCTION\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:22" + ], + "applicationMatches": [] + }, + { + "selector": "h4.card-title:contains(\"2. ELIGIBILITY\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:23" + ], + "applicationMatches": [] + }, + { + "selector": "h4.card-title:contains(\"3. APPLICANT INFORMATION\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:24" + ], + "applicationMatches": [] + }, + { + "selector": "h4.card-title:contains(\"4. PROJECT INFORMATION\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:25" + ], + "applicationMatches": [] + }, + { + "selector": "h4.card-title:contains(\"5. PROJECT TIMELINES\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:26" + ], + "applicationMatches": [] + }, + { + "selector": "h4.card-title:contains(\"6. PROJECT BUDGET\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:27" + ], + "applicationMatches": [] + }, + { + "selector": "h4.card-title:contains(\"7. ATTESTATION\")", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:28" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_ApplicantName]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:33" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_ContactEmail]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:49" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_ContactName]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:47" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_ContactPhoneNumberPrimary]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:50" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_ContactPhoneNumberSecondary]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:51" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_ContactTitle]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:48" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_dateExtractBusinessName]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:34" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_MailingAddressCity]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:59" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_MailingAddressPostalCode]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:61" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_MailingAddressStreet1]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:57" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_MailingAddressStreet2]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:58" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_MailingAddressUnit]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:56" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_OrganizationName]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:36" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_ProjectName]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:66" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_registeredBusinessNumber]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:35" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_RequestedAmount]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:75" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[_TotalProjectBudget]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:76" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"data[${fieldName}]\"]", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:349", + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:358" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"password\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/commands.ts:115" + ], + "applicationMatches": [] + }, + { + "selector": "input[name=\"username\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/commands.ts:109" + ], + "applicationMatches": [] + }, + { + "selector": "input[type='file']", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:531" + ], + "applicationMatches": [] + }, + { + "selector": "input[type='submit']", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/support/auth.ts:152", + "applications/Unity.AutoUI/cypress/support/auth.ts:153" + ], + "applicationMatches": [] + }, + { + "selector": "input[type=\"search\"][aria-controls=\"bs-select-1\"][aria-label=\"Search\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/DashboardPage.ts:20" + ], + "applicationMatches": [] + }, + { + "selector": "input#submittedFromDate", + "kind": "id", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:20" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml" + ] + }, + { + "selector": "input#submittedToDate", + "kind": "id", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:21" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml" + ] + }, + { + "selector": "label", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:304" + ], + "applicationMatches": [] + }, + { + "selector": "label:contains('${labelText}'):visible", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:299" + ], + "applicationMatches": [] + }, + { + "selector": "label.display-input-label[for=\"OrganizationName\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:73" + ], + "applicationMatches": [] + }, + { + "selector": "label.display-input-label[for=\"OrganizationNumber\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:74" + ], + "applicationMatches": [] + }, + { + "selector": "label.form-label", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:105", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:106" + ], + "applicationMatches": [] + }, + { + "selector": "label[for=\"${labelFor}\"]", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/BasePage.ts:138" + ], + "applicationMatches": [] + }, + { + "selector": "label[for=\"Category\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:72" + ], + "applicationMatches": [] + }, + { + "selector": "label[for=\"Community\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:77" + ], + "applicationMatches": [] + }, + { + "selector": "label[for=\"EconomicRegion\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:75" + ], + "applicationMatches": [] + }, + { + "selector": "label[for=\"ProjectBudget\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:79" + ], + "applicationMatches": [] + }, + { + "selector": "label[for=\"RegionalDistrict\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:76" + ], + "applicationMatches": [] + }, + { + "selector": "label[for=\"RequestedAmount\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:78" + ], + "applicationMatches": [] + }, + { + "selector": "label[for=\"Sector\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:80" + ], + "applicationMatches": [] + }, + { + "selector": "option", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:320" + ], + "applicationMatches": [] + }, + { + "selector": "select[name=\"data[_Community]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:70" + ], + "applicationMatches": [] + }, + { + "selector": "select[name=\"data[_EconomicRegion]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:68" + ], + "applicationMatches": [] + }, + { + "selector": "select[name=\"data[_MailingAddressProvince]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:60" + ], + "applicationMatches": [] + }, + { + "selector": "select[name=\"data[_OrganizationType]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:37" + ], + "applicationMatches": [] + }, + { + "selector": "select[name=\"data[_OrgBookStatus]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:38" + ], + "applicationMatches": [] + }, + { + "selector": "select[name=\"data[_RegionalDistrict]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:69" + ], + "applicationMatches": [] + }, + { + "selector": "select[name=\"data[_riskRanking]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:39" + ], + "applicationMatches": [] + }, + { + "selector": "select[name=\"data[${fieldName}]\"]", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:367" + ], + "applicationMatches": [] + }, + { + "selector": "select[name=\"data[sector]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:40" + ], + "applicationMatches": [] + }, + { + "selector": "select[name=\"data[subsector]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:41" + ], + "applicationMatches": [] + }, + { + "selector": "select[title=\"Select a template to apply\"], #template", + "kind": "id", + "syntaxKind": "css", + "ownership": "application", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:227" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/Default.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Zones/ZoneManager.cs", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Template/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js" + ] + }, + { + "selector": "select#dashboardIntakeId option:selected", + "kind": "id", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:44" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Dashboard/Index.js" + ] + }, + { + "selector": "select#quickDateRange", + "kind": "id", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "matched", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:37", + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:38", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:19" + ], + "applicationMatches": [ + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.cshtml", + "applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/Notifications/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js", + "applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentActionBar/Default.cshtml", + "applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/plugins/filterRow.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.cshtml", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/UnityAdmin/Index.js", + "applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml" + ] + }, + { + "selector": "tbody tr", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:80", + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:83", + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:86", + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:89", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:564", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:595", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsRightTabPage.ts:607", + "applications/Unity.AutoUI/cypress/pages/BasePage.ts:110", + "applications/Unity.AutoUI/cypress/pages/BasePage.ts:117", + "applications/Unity.AutoUI/cypress/pages/NavigationPage.ts:88", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:144", + "applications/Unity.AutoUI/cypress_manyEmails/manyEmails.cy.ts:63" + ], + "applicationMatches": [] + }, + { + "selector": "td", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:74", + "applications/Unity.AutoUI/cypress/e2e/basicEmail.cy.ts:81", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:394", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:395", + "applications/Unity.AutoUI/cypress/pages/ApplicationDetailsPage.ts:396", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:269", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:899" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.applicantId + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:345" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.applicantName + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:297", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:405", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:507", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:559" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.approvedAmount + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:329", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:439" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.assignee + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:317", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:474" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.category + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:305", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:529" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.community + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:333" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.projectName + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:313" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.requestedAmount + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:325", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:430", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:543" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.status + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:321", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:386", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:399", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:452", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:515" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.submissionDate + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:309" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.submissionNumber + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:301", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:409", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:511" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.subStatus + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:337", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:492" + ], + "applicationMatches": [] + }, + { + "selector": "td:nth-child(${this.columns.tags + 1})", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:341", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:483" + ], + "applicationMatches": [] + }, + { + "selector": "textarea.select2-search__field", + "kind": "css", + "syntaxKind": "css", + "ownership": "framework-generated", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/lists.cy.ts:30" + ], + "applicationMatches": [] + }, + { + "selector": "textarea[name=\"data[_OtherSubSector]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:42" + ], + "applicationMatches": [] + }, + { + "selector": "textarea[name=\"data[_ProjectDescription]\"]", + "kind": "css", + "syntaxKind": "css", + "ownership": "external", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/pages/ReviewAssessmentPage.ts:67" + ], + "applicationMatches": [] + }, + { + "selector": "th", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:895" + ], + "applicationMatches": [] + }, + { + "selector": "tr", + "kind": "css", + "syntaxKind": "css", + "ownership": "unclassified", + "status": "unverified", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:57", + "applications/Unity.AutoUI/cypress/pages/ApplicationsListPage.ts:223", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:53", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:60", + "applications/Unity.AutoUI/cypress/pages/ListPages.ts:66", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:438", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:801", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:882", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:898", + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:93" + ], + "applicationMatches": [] + }, + { + "selector": "tr:contains(\"${id}\")", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/e2e/chefsdata.cy.ts:56" + ], + "applicationMatches": [] + }, + { + "selector": "tr:contains(\"${submissionId}\")", + "kind": "dynamic", + "syntaxKind": "css", + "ownership": "dynamic", + "status": "exempt", + "missingTokens": [], + "usedBy": [ + "applications/Unity.AutoUI/cypress/regression/ApprovalFlow.cy.ts:122" + ], + "applicationMatches": [] + } + ] +} diff --git a/applications/Unity.AutoUI/package.json b/applications/Unity.AutoUI/package.json index 9ec62d2b54..bec021ee05 100644 --- a/applications/Unity.AutoUI/package.json +++ b/applications/Unity.AutoUI/package.json @@ -1,5 +1,9 @@ { "scripts": { + "selectors:report": "node scripts/selector-contract.mjs", + "selectors:diff": "node scripts/selector-diff-report.mjs", + "selectors:fix": "node scripts/selector-diff-report.mjs --fix", + "selectors:apply": "node scripts/selector-diff-report.mjs --apply", "test": "node ./scripts/run-cypress.js run --spec \"cypress/e2e/**/*.cy.ts\" --browser chrome", "test:e2e": "node ./scripts/run-cypress.js run --spec \"cypress/e2e/**/*.cy.ts\" --browser chrome", "test:regression-headed": "node ./scripts/run-cypress.js run --spec \"cypress/regression/**/*.cy.ts\" --headed --browser chrome", diff --git a/applications/Unity.AutoUI/scripts/selector-contract.mjs b/applications/Unity.AutoUI/scripts/selector-contract.mjs new file mode 100644 index 0000000000..95203c8c2c --- /dev/null +++ b/applications/Unity.AutoUI/scripts/selector-contract.mjs @@ -0,0 +1,237 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { execFileSync } from "node:child_process"; +import ts from "typescript"; + +export const autoUiRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +export const repoRoot = path.resolve(autoUiRoot, "../.."); +export const configPath = path.join(autoUiRoot, "selector-contract.config.json"); +export const registryPath = path.join(autoUiRoot, "cypress/selectors/registry.json"); +const excludedDirectories = new Set(["node_modules", "bin", "obj", ".git", "coverage"]); + +// XPath expressions start with a path axis (`/`, `//`, `./`) or use named +// axis syntax (`ancestor::`, `following-sibling::`, ...) or the `contains()` +// function form — none of which are valid CSS selector syntax. Selectors +// passed to `cy.xpath(...)` (the cypress-xpath plugin command) are also +// treated as XPath regardless of their text shape. +const XPATH_PATTERN = + /^\.{0,2}\/\/|^\/[a-zA-Z*@]|::(?:ancestor|descendant|following-sibling|preceding-sibling|parent|self|child)(?:-or-self)?\b|contains\(\s*(?:text\(\)|@)/; + +export function loadConfig() { + return JSON.parse(fs.readFileSync(configPath, "utf8")); +} + +export function walk(root, extensions) { + const results = []; + if (!fs.existsSync(root)) return results; + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + const fullPath = path.join(root, entry.name); + if (entry.isDirectory()) { + if (!excludedDirectories.has(entry.name) && !fullPath.includes(`${path.sep}wwwroot${path.sep}libs${path.sep}`)) { + results.push(...walk(fullPath, extensions)); + } + } else if (extensions.has(path.extname(entry.name).toLowerCase())) { + results.push(fullPath); + } + } + return results; +} + +// Only scan files git actually tracks — a raw filesystem walk would also +// pick up local scratch/untracked files (e.g. a spec someone is drafting +// but never committed), which would leak into usedBy/applicationMatches in +// the committed registry and be internally inconsistent for anyone who +// doesn't have those same untracked files sitting on disk. Falls back to +// the plain filesystem walk if git isn't available at all (e.g. a tarball +// checkout with no .git directory). +export function trackedFiles(root, extensions) { + const relRoot = path.relative(repoRoot, root); + try { + const output = execFileSync("git", ["ls-files", "--", relRoot], { + cwd: repoRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + return output + .split("\n") + .filter(Boolean) + .map((tracked) => path.join(repoRoot, tracked)) + .filter((fullPath) => extensions.has(path.extname(fullPath).toLowerCase())) + .filter((fullPath) => !fullPath.includes(`${path.sep}wwwroot${path.sep}libs${path.sep}`)) + .sort(); + } catch { + return walk(root, extensions); + } +} + +function selectorText(node, sourceFile) { + if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text.trim(); + if (ts.isTemplateExpression(node)) return node.getText(sourceFile).slice(1, -1).trim(); + return null; +} + +function looksLikeSelector(value) { + if (!value || value.length > 500 || /^(https?:|\/[^\s]+|\{.*\})/.test(value)) return false; + if (XPATH_PATTERN.test(value)) return true; + if (/^[#.\[]/.test(value)) return true; + if (/^(html|body|main|nav|form|label|input|select|option|button|a|table|thead|tbody|tr|td|th|div|span|h[1-6])(?:$|[.#[:\s>+~,])/.test(value)) return true; + return /^[a-z][a-z0-9-]*(?:\[[^\]]+\]|[#.][A-Za-z_-])/.test(value); +} + +function isSelectorPosition(node) { + const parent = node.parent; + if (ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent) || ts.isVariableDeclaration(parent)) return true; + if (!ts.isCallExpression(parent)) return false; + const expression = parent.expression.getText(); + return parent.arguments.indexOf(node) === 0 && /(?:^|\.)(?:get|find|contains|within|closest|filter|children|parents|next|select|getElement|getBySelector|xpath)$/.test(expression); +} + +function callName(node) { + const parent = node.parent; + if (!ts.isCallExpression(parent)) return null; + return parent.expression.getText(); +} + +export function syntaxKind(selector, usedViaXpathCall) { + if (usedViaXpathCall || XPATH_PATTERN.test(selector)) return "xpath"; + return "css"; +} + +export function extractSelectors(config) { + const files = config.cypressRoots + .map((root) => path.join(repoRoot, root)) + .flatMap((root) => trackedFiles(root, new Set([".ts", ".tsx", ".js", ".jsx"]))) + .sort(); + const selectors = new Map(); + for (const file of files) { + const sourceFile = ts.createSourceFile(file, fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true); + function visit(node) { + const value = selectorText(node, sourceFile); + if (value && isSelectorPosition(node) && looksLikeSelector(value)) { + const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); + const usage = `${path.relative(repoRoot, file).replaceAll(path.sep, "/")}:${position.line + 1}`; + const viaXpathCall = /(?:^|\.)xpath$/.test(callName(node) ?? ""); + if (!selectors.has(value)) { + selectors.set(value, { usages: new Set(), viaXpathCall: false }); + } + const entry = selectors.get(value); + entry.usages.add(usage); + entry.viaXpathCall = entry.viaXpathCall || viaXpathCall; + } + ts.forEachChild(node, visit); + } + visit(sourceFile); + } + return selectors; +} + +function classifyOwnership(selector, ownershipRules) { + const override = ownershipRules.find((rule) => rule.regex.test(selector)); + if (override) return override.ownership; + if (selector.includes("${")) return "dynamic"; + if (/\[data-(?:cy|testid)=/.test(selector) || /(^|[\s>+~,])#[A-Za-z_]/.test(selector)) return "application"; + return "unclassified"; +} + +export function identifyingTokens(selector) { + const tokens = []; + for (const match of selector.matchAll(/#([A-Za-z_][\w:-]*)/g)) tokens.push({ type: "id", value: match[1] }); + for (const match of selector.matchAll(/\[(data-(?:cy|testid))=["']?([^\]"']+)/g)) tokens.push({ type: match[1], value: match[2] }); + return tokens; +} + +// Shared with selector-diff-report.mjs so the "does this token exist in this +// source file" check and the "restore this token to this line" logic use +// the exact same string patterns validate() checks — a fix that satisfies +// buildNeedles() is guaranteed to flip the selector back to "matched". +export function buildNeedles(token) { + const razorModelPath = token.value.replaceAll("_", "."); + return token.type === "id" + ? [ + `id=\"${token.value}\"`, + `id='${token.value}'`, + `Id(\"${token.value}\")`, + `#${token.value}`, + `'${token.value}'`, + `\"${token.value}\"`, + `asp-for=\"@Model.${razorModelPath}\"`, + `asp-for='@Model.${razorModelPath}'` + ] + : [`${token.type}=\"${token.value}\"`, `${token.type}='${token.value}'`]; +} + +export function applicationIndex(config) { + return config.applicationRoots + .map((root) => path.join(repoRoot, root)) + .flatMap((root) => trackedFiles(root, new Set([".cshtml", ".razor", ".html", ".js", ".ts", ".tsx", ".cs"]))) + .sort() + .map((file) => ({ file: path.relative(repoRoot, file).replaceAll(path.sep, "/"), text: fs.readFileSync(file, "utf8") })); +} + +/** + * @param {Map, viaXpathCall: boolean}>} selectors + */ +export function validate(selectors, config, sources) { + const ownershipRules = config.ownershipRules.map((rule) => ({ ...rule, regex: new RegExp(rule.pattern) })); + return [...selectors.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([selector, { usages, viaXpathCall }]) => { + const ownership = classifyOwnership(selector, ownershipRules); + const tokens = identifyingTokens(selector); + const matches = new Set(); + const missingTokens = []; + for (const token of tokens) { + const needles = buildNeedles(token); + const tokenMatches = sources.filter((source) => needles.some((needle) => source.text.includes(needle))); + if (tokenMatches.length === 0) missingTokens.push(`${token.type}:${token.value}`); + tokenMatches.slice(0, 20).forEach((source) => matches.add(source.file)); + } + let status = "unverified"; + if (["external", "framework-generated", "dynamic"].includes(ownership)) status = "exempt"; + else if (tokens.length > 0) status = missingTokens.length === 0 ? "matched" : "missing"; + return { + selector, + kind: selector.includes("${") ? "dynamic" : tokens[0]?.type ?? "css", + syntaxKind: syntaxKind(selector, viaXpathCall), + ownership, + status, + missingTokens, + usedBy: [...usages].sort(), + applicationMatches: [...matches].sort() + }; + }); +} + +export function buildRegistry(config = loadConfig()) { + const entries = validate(extractSelectors(config), config, applicationIndex(config)); + const counts = Object.fromEntries(["matched", "missing", "unverified", "exempt"].map((status) => [status, entries.filter((entry) => entry.status === status).length])); + const syntaxCounts = Object.fromEntries(["css", "xpath"].map((k) => [k, entries.filter((entry) => entry.syntaxKind === k).length])); + return { + schemaVersion: 2, + mode: "report-only", + generatedAt: new Date().toISOString(), + summary: { total: entries.length, ...counts, syntax: syntaxCounts }, + entries + }; +} + +function runCli() { + const config = loadConfig(); + const registry = buildRegistry(config); + fs.mkdirSync(path.dirname(registryPath), { recursive: true }); + fs.writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`); + console.log(`Cypress selector contract: ${registry.summary.total} selectors`); + for (const status of ["matched", "missing", "unverified", "exempt"]) console.log(` ${status}: ${registry.summary[status]}`); + console.log(` syntax — css: ${registry.summary.syntax.css}, xpath: ${registry.summary.syntax.xpath}`); + for (const entry of registry.entries.filter((candidate) => candidate.status === "missing")) console.log(` MISSING ${entry.selector} (${entry.usedBy.join(", ")})`); + for (const entry of registry.entries.filter((candidate) => candidate.syntaxKind === "xpath")) console.log(` XPATH ${entry.selector} (${entry.usedBy.join(", ")})`); + console.log(`Registry written to ${path.relative(repoRoot, registryPath)}`); +} + +// Only run the CLI report when this file is executed directly +// (`node scripts/selector-contract.mjs`) — not when imported as a module by +// selector-diff-report.mjs. +if (import.meta.url === `file://${process.argv[1]}`) { + runCli(); +} diff --git a/applications/Unity.AutoUI/scripts/selector-diff-report.mjs b/applications/Unity.AutoUI/scripts/selector-diff-report.mjs new file mode 100644 index 0000000000..1412125526 --- /dev/null +++ b/applications/Unity.AutoUI/scripts/selector-diff-report.mjs @@ -0,0 +1,241 @@ +#!/usr/bin/env node + +// Compares the Cypress selector contract on the current branch/working tree +// against a committed baseline (cypress/selectors/registry.json as it +// exists at --base, default origin/main) to find selectors that *worked at +// the baseline but no longer do* — i.e. this branch likely broke them. +// +// Modes: +// node scripts/selector-diff-report.mjs report only +// node scripts/selector-diff-report.mjs --fix report + dry-run patch preview +// node scripts/selector-diff-report.mjs --apply report + write unambiguous fixes to disk +// node scripts/selector-diff-report.mjs --base compare against a different ref +// +// Never commits or pushes. --apply only ever restores a token this script +// has concrete baseline evidence for, and only when exactly one unambiguous +// candidate line is found in the current file — anything else is left for a +// human to resolve and is reported as "needs review". + +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { + repoRoot, + registryPath, + loadConfig, + extractSelectors, + applicationIndex, + validate, + identifyingTokens, + buildNeedles +} from "./selector-contract.mjs"; + +const args = process.argv.slice(2); +const mode = args.includes("--apply") ? "apply" : args.includes("--fix") ? "fix" : "report"; +const baseIndex = args.indexOf("--base"); +const baseRef = baseIndex !== -1 ? args[baseIndex + 1] : "origin/main"; + +const STATUS_RANK = { missing: 0, unverified: 1, exempt: 2, matched: 3 }; +const registryRelPath = path.relative(repoRoot, registryPath).replaceAll(path.sep, "/"); + +function git(argv) { + return execFileSync("git", argv, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); +} + +function readBaselineRegistry() { + try { + const raw = git(["show", `${baseRef}:${registryRelPath}`]); + return JSON.parse(raw); + } catch { + return null; + } +} + +function readBaselineFile(relPath) { + try { + return git(["show", `${baseRef}:${relPath}`]); + } catch { + return null; + } +} + +function findLineWithNeedle(text, needles) { + const lines = text.split("\n"); + for (let i = 0; i < lines.length; i += 1) { + if (needles.some((needle) => lines[i].includes(needle))) { + return { lineNumber: i + 1, text: lines[i] }; + } + } + return null; +} + +// Crude but dependency-free line-similarity score: fraction of shared +// "words" (tag names, attribute names/values) between two lines. Good +// enough to spot "this is clearly the same element, just missing the +// attribute" without pulling in a real HTML/Razor parser — consistent with +// how the rest of this tool already does plain-text scanning rather than +// AST-level analysis of the application source. +function similarity(lineA, lineB) { + const wordsOf = (line) => new Set(line.toLowerCase().match(/[a-z0-9_-]+/g) ?? []); + const a = wordsOf(lineA); + const b = wordsOf(lineB); + if (a.size === 0 || b.size === 0) return 0; + let shared = 0; + for (const word of a) if (b.has(word)) shared += 1; + return shared / Math.max(a.size, b.size); +} + +function findRestoreCandidate(oldLineText, oldLineNumber, currentFileText) { + const currentLines = currentFileText.split("\n"); + const scored = currentLines + .map((text, index) => { + const lineNumber = index + 1; + const score = similarity(oldLineText, text); + // An attribute rename/removal overwhelmingly happens in place — the + // element doesn't relocate elsewhere in the file. Use distance from + // the original line as a tiebreaker so two structurally-similar + // sibling lines (e.g. repeated `` + // rows) don't score as equally likely; it only ever nudges the + // ranking, never overrides a genuinely higher word-overlap score. + const proximityBonus = 1 / (1 + Math.abs(lineNumber - oldLineNumber)); + return { lineNumber, text, score, ranked: score + proximityBonus * 0.05 }; + }) + .filter((candidate) => candidate.score > 0) + .sort((a, b) => b.ranked - a.ranked); + + if (scored.length === 0 || scored[0].score < 0.6) return { candidate: null, reason: "no line above the similarity threshold" }; + if (scored.length > 1 && scored[1].ranked >= scored[0].ranked - 0.02) { + return { candidate: null, reason: `ambiguous — top two candidates score ${scored[0].score.toFixed(2)} (line ${scored[0].lineNumber}) and ${scored[1].score.toFixed(2)} (line ${scored[1].lineNumber})` }; + } + return { candidate: scored[0], reason: null }; +} + +function proposeInsertion(lineText, token) { + const attr = token.type === "id" ? "id" : token.type; + const tagMatch = lineText.match(/<([a-zA-Z][a-zA-Z0-9-]*)/); + if (!tagMatch) return null; + const insertAt = tagMatch.index + tagMatch[0].length; + return `${lineText.slice(0, insertAt)} ${attr}="${token.value}"${lineText.slice(insertAt)}`; +} + +function main() { + const config = loadConfig(); + const baseline = readBaselineRegistry(); + + if (!baseline) { + console.log(`No baseline registry found at ${baseRef}:${registryRelPath}.`); + console.log("This is expected before cypress/selectors/registry.json has been committed on the base branch."); + console.log("Run `npm run selectors:report` and commit the result on your base branch first, then re-run this."); + return; + } + + const currentEntries = validate(extractSelectors(config), config, applicationIndex(config)); + const baselineBySelector = new Map(baseline.entries.map((entry) => [entry.selector, entry])); + + const regressions = []; + const newlyBroken = []; + + for (const current of currentEntries) { + const before = baselineBySelector.get(current.selector); + if (!before) { + if (current.status === "missing") newlyBroken.push(current); + continue; + } + if (STATUS_RANK[current.status] < STATUS_RANK[before.status]) { + regressions.push({ before, current }); + } + } + + console.log(`Selector contract diff — base: ${baseRef}`); + console.log(` ${currentEntries.length} selectors on current tree, ${baseline.entries.length} at baseline`); + console.log(` regressions: ${regressions.length}, new-and-already-broken: ${newlyBroken.length}`); + console.log(""); + + if (regressions.length === 0 && newlyBroken.length === 0) { + console.log("No selector regressions found relative to the baseline."); + return; + } + + for (const { before, current } of regressions) { + console.log(`REGRESSED ${current.selector}`); + console.log(` status: ${before.status} -> ${current.status}`); + console.log(` used by: ${current.usedBy.join(", ")}`); + + if (before.status !== "matched" || current.status !== "missing") { + console.log(" (not auto-fixable — only matched -> missing regressions are attempted)\n"); + continue; + } + + const tokens = identifyingTokens(current.selector).filter((token) => + current.missingTokens.includes(`${token.type}:${token.value}`), + ); + + for (const token of tokens) { + const needles = buildNeedles(token); + let resolved = false; + + for (const relPath of before.applicationMatches) { + const oldFileText = readBaselineFile(relPath); + if (oldFileText === null) continue; + const oldLine = findLineWithNeedle(oldFileText, needles); + if (!oldLine) continue; + + const absPath = path.join(repoRoot, relPath); + if (!fs.existsSync(absPath)) { + console.log(` ${token.type}:${token.value} — baseline evidence in ${relPath}:${oldLine.lineNumber}, but that file no longer exists. NEEDS REVIEW.`); + resolved = true; + break; + } + + const currentFileText = fs.readFileSync(absPath, "utf8"); + const { candidate, reason } = findRestoreCandidate(oldLine.text, oldLine.lineNumber, currentFileText); + + if (!candidate) { + console.log(` ${token.type}:${token.value} — was in ${relPath}:${oldLine.lineNumber} (${oldLine.text.trim()}). NEEDS REVIEW: ${reason}.`); + resolved = true; + break; + } + + const fixedLine = proposeInsertion(candidate.text, token); + if (!fixedLine) { + console.log(` ${token.type}:${token.value} — matched ${relPath}:${candidate.lineNumber} but couldn't locate a tag to attach the attribute to. NEEDS REVIEW.`); + resolved = true; + break; + } + + console.log(` ${token.type}:${token.value} — proposed fix in ${relPath}:${candidate.lineNumber}`); + console.log(` - ${candidate.text.trim()}`); + console.log(` + ${fixedLine.trim()}`); + + if (mode === "apply") { + const lines = currentFileText.split("\n"); + lines[candidate.lineNumber - 1] = fixedLine; + fs.writeFileSync(absPath, lines.join("\n")); + console.log(` applied to ${relPath}`); + } + + resolved = true; + break; + } + + if (!resolved) { + console.log(` ${token.type}:${token.value} — no baseline evidence found to restore from. NEEDS REVIEW.`); + } + } + console.log(""); + } + + for (const entry of newlyBroken) { + console.log(`NEW, ALREADY MISSING ${entry.selector}`); + console.log(` used by: ${entry.usedBy.join(", ")}`); + console.log(` missing: ${entry.missingTokens.join(", ")}\n`); + } + + if (mode === "report") { + console.log("Run with --fix to preview proposed patches, or --apply to write unambiguous fixes to disk."); + } else if (mode === "fix") { + console.log("Dry run only — nothing was written. Re-run with --apply to write the unambiguous fixes above."); + } +} + +main(); diff --git a/applications/Unity.AutoUI/selector-contract.config.json b/applications/Unity.AutoUI/selector-contract.config.json new file mode 100644 index 0000000000..71f7e4463a --- /dev/null +++ b/applications/Unity.AutoUI/selector-contract.config.json @@ -0,0 +1,27 @@ +{ + "cypressRoots": [ + "applications/Unity.AutoUI/cypress", + "applications/Unity.AutoUI/cypress_manyEmails" + ], + "applicationRoots": [ + "applications/Unity.GrantManager/src", + "applications/Unity.GrantManager/modules" + ], + "ownershipRules": [ + { + "pattern": "#(?:user|password|loginButton|social-(?:idir|azureidir))(?:$|[\\s>+~.,:[#])", + "ownership": "external", + "reason": "Identity-provider markup is not owned by Unity Grant Manager." + }, + { + "pattern": "#(?:bs-select-|select2-)|\\.dt-|\\.modal-backdrop|\\.swal2-|\\.select2-", + "ownership": "framework-generated", + "reason": "Selector is generated by a UI library at runtime." + }, + { + "pattern": "#formio|name=[\"']data\\[", + "ownership": "external", + "reason": "Form.io/CHEFS renders this markup dynamically." + } + ] +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.js b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.js index a8b364e85b..eeb4712ccf 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/AIReporting/Index.js @@ -5,32 +5,7 @@ const showInitializationError = (container, message, error) => { const reportingAiUrl = globalThis.reportingAiUrl; const container = document.getElementById('container'); -const initializeAIReporting = async () => { - if (!container) { - return; - } - - if (!reportingAiUrl) { - showInitializationError(container, 'AI Reporting is not configured.'); - return; - } - - let reportingUrl; - try { - reportingUrl = new URL(reportingAiUrl); - } catch (error) { - showInitializationError(container, 'AI Reporting is not configured correctly.', error); - return; - } - - let token; - try { - token = await unity.grantManager.identity.jwtToken.generateJWTToken(); - } catch (error) { - showInitializationError(container, 'Failed to initialize AI Reporting. Please refresh the page and try again.', error); - return; - } - +const buildReportingIframe = (reportingUrl, token) => { const iframe = document.createElement('iframe'); iframe.style.width = '100%'; @@ -66,7 +41,32 @@ const initializeAIReporting = async () => { }; iframe.src = reportingUrl.href; - container.appendChild(iframe); + return iframe; }; -initializeAIReporting(); +if (container) { + if (!reportingAiUrl) { + showInitializationError(container, 'AI Reporting is not configured.'); + } else { + let reportingUrl; + try { + reportingUrl = new URL(reportingAiUrl); + } catch (error) { + reportingUrl = null; + showInitializationError(container, 'AI Reporting is not configured correctly.', error); + } + + if (reportingUrl) { + const initializeReporting = async () => { + try { + const token = await unity.grantManager.identity.jwtToken.generateJWTToken(); + container.appendChild(buildReportingIframe(reportingUrl, token)); + } catch (error) { + showInitializationError(container, 'Failed to initialize AI Reporting. Please refresh the page and try again.', error); + } + }; + + initializeReporting(); + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js index c670b00b18..1b6d712a05 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/Scoresheet/Scoresheet.js @@ -360,9 +360,12 @@ $(function () { return hash; } for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); + const char = str.codePointAt(i); hash = ((hash << 5) - hash) + char; hash |= 0; + if (char > 0xFFFF) { + i++; + } } return hash; } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissionDefinitionProvider.cs index ce368255c9..b5a9b3fbe2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissionDefinitionProvider.cs @@ -20,6 +20,10 @@ public override void Define(IPermissionDefinitionContext context) NotificationsPermissions.Email.Send, L($"Permission:{NotificationsPermissions.Email.Send}")); + notificationsPermissions.AddChild( + NotificationsPermissions.Email.SendBulk, + L($"Permission:{NotificationsPermissions.Email.SendBulk}")); + notificationsPermissions.AddChild( NotificationsPermissions.Email.DeleteDraft, L($"Permission:{NotificationsPermissions.Email.DeleteDraft}")); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissions.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissions.cs index 20bd2b4ec1..b3ef9af03b 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissions.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application.Contracts/Permissions/NotificationsPermissions.cs @@ -11,6 +11,7 @@ public static class Email { public const string Default = "Notifications.Email"; public const string Send = "Notifications.Email.Send"; + public const string SendBulk = "Notifications.Email.SendBulk"; public const string DeleteDraft = "Notifications.Email.DeleteDraft"; public const string CancelScheduled = "Notifications.Email.CancelScheduled"; public const string Schedule = "Notifications.Email.Schedule"; diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs index f5f7b3224d..f5e46f0976 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/EmailNotificaions/EmailNotificationService.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -33,7 +32,6 @@ public class EmailNotificationService( ISettingManager settingManager, IFeatureChecker featureChecker, IConfiguration configuration, - IWebHostEnvironment webHostEnvironment, IMarkdownRenderer markdownRenderer) : ApplicationService, IEmailNotificationService { @@ -284,7 +282,8 @@ private async Task RenderCommentNotificationTemplateAsync(string current } /// - /// Loads an email template from the Views/EmailTemplates directory. + /// Loads an email template from the Application assembly's embedded resources. + /// The resource name follows the format Unity.Notifications.EmailTemplates.{templateName}.cshtml. /// /// Template name without extension (e.g., "CommentNotification") /// Template content as a string @@ -292,32 +291,17 @@ private async Task LoadEmailTemplateAsync(string templateName) { try { - // Content root is at: .../Unity.GrantManager/src/Unity.GrantManager.Web - // We need to go up 2 levels to reach Unity.GrantManager, then into modules - var contentRoot = webHostEnvironment.ContentRootPath; - - var templatePath = Path.Combine( - contentRoot, - "..", - "..", - "modules", - "Unity.Notifications", - "src", - "Unity.Notifications.Web", - "Views", - "EmailTemplates", - $"{templateName}.cshtml"); - - // Normalize the path to remove .. references - templatePath = Path.GetFullPath(templatePath); - - if (!File.Exists(templatePath)) + var assembly = typeof(EmailNotificationService).Assembly; + var resourceName = $"Unity.Notifications.EmailTemplates.{templateName}.cshtml"; + await using var templateStream = assembly.GetManifestResourceStream(resourceName); + + if (templateStream == null) { - throw new FileNotFoundException($"Email template not found at: {templatePath}"); + throw new FileNotFoundException($"Embedded email template not found: {resourceName}"); } - var content = await File.ReadAllTextAsync(templatePath); - return content; + using var reader = new StreamReader(templateStream); + return await reader.ReadToEndAsync(); } catch (Exception ex) { diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Unity.Notifications.Application.csproj b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Unity.Notifications.Application.csproj index 7261bdbf57..5fea35f0e5 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Unity.Notifications.Application.csproj +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Unity.Notifications.Application.csproj @@ -31,6 +31,8 @@ + all diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain.Shared/Localization/Notifications/en.json b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain.Shared/Localization/Notifications/en.json index cb4dccc3d5..c6bd3f3e5f 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain.Shared/Localization/Notifications/en.json +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain.Shared/Localization/Notifications/en.json @@ -10,6 +10,7 @@ "Permission:Notifications": "Notifications", "Permission:Notifications.Email": "Email", "Permission:Notifications.Email.Send": "Send Email for Individual Application", + "Permission:Notifications.Email.SendBulk": "Send Bulk Email Notification", "Permission:Notifications.Email.DeleteDraft": "Delete Draft Email for Individual Application", "Permission:Notifications.Email.CancelScheduled": "Cancel Scheduled Email for Individual Application", "Permission:Notifications.Email.Schedule": "Schedule Email for Individual Application", diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogsRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogsRepository.cs index 8ba3f48bc8..af1128ae19 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogsRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Domain/Emails/IEmailLogsRepository.cs @@ -9,5 +9,6 @@ public interface IEmailLogsRepository : IRepository { Task GetByIdAsync(Guid id, bool includeDetails = false); Task> GetByApplicationIdAsync(Guid applicationId); + Task> GetByApplicationIdsAndStatusAsync(List applicationIds, string status); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogsRepository.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogsRepository.cs index ad5bafe3c1..a2f1181711 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogsRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.EntityFrameworkCore/Repositories/EmailLogsRepository.cs @@ -15,18 +15,24 @@ public class EmailLogsRepository : EfCoreRepository dbContextProvider) : base(dbContextProvider) { - } - + } + public async Task GetByIdAsync(Guid id, bool includeDetails = false) { var dbSet = await GetDbSetAsync(); return await dbSet.FirstOrDefaultAsync(s => s.Id == id); - } - - public async Task> GetByApplicationIdAsync(Guid applicationId) - { - var dbSet = await GetDbSetAsync(); - return await dbSet.Where(x => x.ApplicationId == applicationId).ToListAsync(); + } + + public async Task> GetByApplicationIdAsync(Guid applicationId) + { + var dbSet = await GetDbSetAsync(); + return await dbSet.Where(x => x.ApplicationId == applicationId).ToListAsync(); + } + + public async Task> GetByApplicationIdsAndStatusAsync(List applicationIds, string status) + { + var dbSet = await GetDbSetAsync(); + return await dbSet.Where(x => applicationIds.Contains(x.ApplicationId) && x.Status == status).ToListAsync(); } public override async Task> WithDetailsAsync() diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Bundling/NotificationsScriptBundleContributor.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Bundling/NotificationsScriptBundleContributor.cs index 2fb1de2a35..6e0dead0b9 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Bundling/NotificationsScriptBundleContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Bundling/NotificationsScriptBundleContributor.cs @@ -7,7 +7,7 @@ public class NotificationsScriptBundleContributor : BundleContributor { public override void ConfigureBundle(BundleConfigurationContext context) { - context.Files.AddIfNotContains("/libs/signalr/browser/signalr.js"); - context.Files.AddIfNotContains("/js/notifications-realtime-client.js"); + context.Files.AddIfNotContains("/libs/select2/dist/js/select2.full.js"); + context.Files.AddIfNotContains("/libs/signalr/browser/signalr.min.js"); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Bundling/NotificationsStyleBundleContributor.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Bundling/NotificationsStyleBundleContributor.cs index 965f9a2d00..41f587b9ef 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Bundling/NotificationsStyleBundleContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Bundling/NotificationsStyleBundleContributor.cs @@ -7,6 +7,8 @@ public class NotificationsStyleBundleContributor : BundleContributor { public override void ConfigureBundle(BundleConfigurationContext context) { + context.Files.AddIfNotContains("/libs/select2/dist/css/select2.css"); + context.Files.AddIfNotContains("/libs/select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme.css"); context.Files.AddIfNotContains("/libs/tinymce/skins/ui/oxide/content.css"); context.Files.AddIfNotContains("/libs/tinymce/skins/ui/oxide/skin.css"); } diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Controllers/UnityMessagingController.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Controllers/UnityMessagingController.cs index cb1a6d2409..db61d5ba1e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Controllers/UnityMessagingController.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Controllers/UnityMessagingController.cs @@ -54,6 +54,11 @@ public async Task MessageUserAsync([FromBody] DirectMessageReques return BadRequest("TargetUserId and Message are required."); } + if (request.Message.Length > NotificationHub.MaxDirectMessageLength) + { + return BadRequest($"Messages cannot exceed {NotificationHub.MaxDirectMessageLength} characters."); + } + var senderUserId = CurrentUser.Id?.ToString() ?? "unknown"; var senderName = DisplayNameHelper.Resolve(CurrentUser); @@ -98,6 +103,11 @@ public async Task MessageTenantAsync([FromBody] TenantMessageRequ return BadRequest("TargetTenantId and Message are required."); } + if (request.Message.Length > NotificationHub.MaxDirectMessageLength) + { + return BadRequest($"Messages cannot exceed {NotificationHub.MaxDirectMessageLength} characters."); + } + if (currentTenant.Id.HasValue && currentTenant.Id.Value != request.TargetTenantId) { return Forbid(); diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/UnityMessaging/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/UnityMessaging/Index.cshtml index d308632181..8f19d9215c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/UnityMessaging/Index.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/UnityMessaging/Index.cshtml @@ -16,7 +16,7 @@ } -
+

@L["RealtimeOps:Title"]

diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/UnityMessaging/Index.css b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/UnityMessaging/Index.css index 986b7104cc..12e8ae1cd0 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/UnityMessaging/Index.css +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Pages/UnityMessaging/Index.css @@ -1,3 +1,16 @@ +.messaging-page { + display: flex; + flex-direction: column; + height: calc(100vh - 70px); + height: calc(100dvh - 70px); + min-height: 0; +} + +.messaging-page > h3, +.messaging-page > .messaging-split-layout { + flex: 0 0 auto; +} + .messaging-split-layout { display: flex; align-items: stretch; @@ -37,10 +50,12 @@ } .messaging-activity-card { + flex: 1 1 auto; display: flex; flex-direction: column; - min-height: 320px; - height: calc(100vh - 390px); + min-height: 0; + height: auto; + margin-bottom: 0; overflow: hidden; } @@ -72,7 +87,8 @@ display: flex; flex: 1 1 auto; flex-direction: column; - overflow: hidden; + overflow: visible; + height: auto; } .messaging-activity-card #UnityMessagingActivityTable_wrapper .dt-layout-table, @@ -98,6 +114,10 @@ background: var(--bs-card-bg, #fff); } +.messaging-activity-card .dt-unity-footer { + margin-bottom: 1rem; +} + @media (max-width: 992px) { .messaging-split-layout { flex-direction: column; @@ -112,7 +132,13 @@ } .messaging-activity-card { + flex: 0 0 auto; height: auto; min-height: 420px; } + + .messaging-page { + height: auto; + min-height: 0; + } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Realtime/NotificationHub.cs b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Realtime/NotificationHub.cs index 3940e149df..625f5d0819 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Realtime/NotificationHub.cs +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Realtime/NotificationHub.cs @@ -31,6 +31,7 @@ public class NotificationHub( { public const string HubRoute = "/signalr/notifications"; public const string NotificationLogsOpsGroup = "ops:notification-logs"; + public const int MaxDirectMessageLength = 4000; public override async Task OnConnectedAsync() { @@ -93,7 +94,9 @@ public async Task GetUnreadMessagesAsync() return [.. messages.Select(message => new UnreadMessageInfo { Scope = message.UserId.HasValue ? "user" : "tenant", - TargetId = message.UserId.HasValue ? userId.ToString() : message.TenantId?.ToString(), + TargetId = message.UserId.HasValue + ? message.SenderUserId?.ToString() + : message.TenantId?.ToString(), SenderId = message.SenderUserId?.ToString(), SenderName = message.SenderDisplayName ?? message.SenderUserId?.ToString() ?? "unknown", Source = message.Source, @@ -102,6 +105,54 @@ public async Task GetUnreadMessagesAsync() })]; } + public async Task GetConversationHistoryAsync(string scope, string? targetId) + { + if (!Guid.TryParse(GetCurrentUserId(), out var userId)) + { + return []; + } + + var query = await notificationLogsRepository.GetQueryableAsync(); + var messageQuery = query.Where(x => + x.NotificationType == NotificationLogType.SignalRDirectMessage + && x.TenantId == currentTenant.Id); + + if (scope == "tenant") + { + messageQuery = messageQuery.Where(x => x.UserId == null); + } + else if (Guid.TryParse(targetId, out var peerUserId)) + { + messageQuery = messageQuery.Where(x => + (x.UserId == userId && x.SenderUserId == peerUserId) + || (x.UserId == peerUserId && x.SenderUserId == userId)); + } + else + { + return []; + } + + var messages = await messageQuery + .OrderByDescending(x => x.CreationTime) + .Take(100) + .ToListAsync(); + + return [.. messages + .OrderBy(x => x.CreationTime) + .Select(message => new UnreadMessageInfo + { + Scope = message.UserId.HasValue ? "user" : "tenant", + TargetId = message.UserId.HasValue + ? (message.SenderUserId == userId ? message.UserId : message.SenderUserId)?.ToString() + : message.TenantId?.ToString(), + SenderId = message.SenderUserId?.ToString(), + SenderName = message.SenderDisplayName ?? message.SenderUserId?.ToString() ?? "unknown", + Source = message.Source, + Message = message.Message, + Timestamp = message.CreationTime + })]; + } + public Task MarkMessagesReadAsync() { return Guid.TryParse(GetCurrentUserId(), out var userId) @@ -147,6 +198,11 @@ public async Task SendDirectMessageAsync(string targetUserId, string message) throw new HubException("Target user and message are required."); } + if (message.Length > MaxDirectMessageLength) + { + throw new HubException($"Messages cannot exceed {MaxDirectMessageLength} characters."); + } + var senderId = GetCurrentUserId() ?? string.Empty; var senderName = DisplayNameHelper.Resolve(Context.User); var timestamp = DateTime.UtcNow; diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js index 8092c7b82c..fa9e79cad3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/Views/Settings/NotificationsSettingGroup/InternalEmailGroups.js @@ -444,7 +444,7 @@ const emailGroupsManager = { 'createAddUserBtn', function(selectedUser) { // Add to selected users if not already there - if (!selectedUsers.find(u => u.userId === selectedUser.userId)) { + if (!selectedUsers.some(u => u.userId === selectedUser.userId)) { const newUser = { userId: selectedUser.userId, userName: selectedUser.userName, diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/wwwroot/css/notifications-realtime-widget.css b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/wwwroot/css/notifications-realtime-widget.css index 0ba3c107fe..d50a00aa33 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/wwwroot/css/notifications-realtime-widget.css +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/wwwroot/css/notifications-realtime-widget.css @@ -7,8 +7,8 @@ .rt-widget-bubble { position: relative; - width: 56px; - height: 56px; + width: 40px; + height: 40px; border-radius: 50%; border: none; background-color: #2e5dd7; @@ -19,6 +19,7 @@ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); cursor: pointer; padding: 0; + touch-action: none; } .rt-widget-bubble:hover { @@ -61,7 +62,7 @@ bottom: 0; width: 340px; max-width: 100vw; - max-height: 480px; + max-height: 80vh; background: #fff; border: 1px solid #dee2e6; border-bottom: none; @@ -70,7 +71,20 @@ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.08); display: none; flex-direction: column; - overflow: hidden; + overflow: visible; +} + +.rt-widget-resize-handle { + position: absolute; + right: 0; + bottom: 0; + z-index: 2; + width: 14px; + height: 16px; + cursor: nwse-resize; + touch-action: none; + clip-path: polygon(0% 100%, 100% 100%, 100% 0%); + background: repeating-linear-gradient(135deg, transparent 0 3px, rgba(108, 117, 125, .65) 4px 5px, transparent 3px 1px); } .rt-widget-panel-open { @@ -86,6 +100,12 @@ color: #fff; font-weight: 600; flex-shrink: 0; + cursor: grab; + touch-action: none; +} + +.rt-widget-panel-header:active { + cursor: grabbing; } .rt-widget-close { @@ -170,6 +190,8 @@ } .rt-widget-mode-tab { + display: flex; + align-items: center; flex: 1 1 50%; border: 1px solid #ced4da; background: #fff; @@ -180,6 +202,24 @@ cursor: pointer; } +.rt-widget-mode-count { + display: inline-block; + min-width: 16px; + margin-left: auto; + padding: 1px 4px; + border-radius: 8px; + background-color: #6c757d; + color: #fff; + font-size: 0.7rem; + line-height: 1.2; + text-align: center; + vertical-align: 1px; +} + +.rt-widget-mode-count.rt-widget-hidden { + display: none; +} + .rt-widget-mode-tab.active { border-color: #2e5dd7; background: #2e5dd7; @@ -208,9 +248,52 @@ width: 100%; min-width: 0; font-size: 0.85rem; - padding: 4px 6px 4px 25px; - border: 1px solid #ced4da; - border-radius: 4px; + padding-left: 25px; +} + +.rt-widget-target-control .select2-container { + width: 100% !important; +} + +.rt-widget-target-control .select2-container--bootstrap-5.select2-container--focus { + outline: 0 !important; +} + +.rt-widget-target-control .select2-container--bootstrap-5.select2-container--focus .select2-selection { + border-color: #dee2e6 !important; + box-shadow: none !important; +} + +.rt-widget-target-control .select2-container--open, +.rt-widget-target-control .select2-dropdown { + z-index: 1057; +} + +.rt-widget-target-control .select2-container--open .select2-selection { + box-shadow: none !important; +} + +.rt-widget-target-control .select2-selection--single { + height: 31px; + padding-left: 18px; +} + +.rt-widget-target-control .select2-selection__rendered { + font-size: 0.85rem; + line-height: 29px; +} + +.rt-widget-target-control .select2-results__option { + min-height: 32px; + font-size: 0.85rem; +} + +.rt-widget-target-option-content { + display: flex; + align-items: center; + gap: 4px; + min-height: 20px; + white-space: nowrap; } .rt-widget-compose-row { diff --git a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/wwwroot/js/notifications-realtime-client.js b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/wwwroot/js/notifications-realtime-client.js index e7f5e13375..c8e2e63d11 100644 --- a/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/wwwroot/js/notifications-realtime-client.js +++ b/applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Web/wwwroot/js/notifications-realtime-client.js @@ -1,4 +1,10 @@ (function () { + if (globalThis.__unityRealtimeWidgetInitialized) { + return; + } + + globalThis.__unityRealtimeWidgetInitialized = true; + whenReady(init); function whenReady(callback) { @@ -9,25 +15,49 @@ } } - function init() { - if (/\/(account\/login|login|splash)(?:\/|$)/i.test(window.location.pathname)) { - return; - } + function shouldInit() { + shouldInit.depth = (shouldInit.depth || 0) + 1; - if (window.location.pathname === '/') { - return; - } + try { + if (shouldInit.depth > 10) { + console.error('RECURSIVE shouldInit DETECTED'); + console.trace(); + return false; + } - if (abp.features && typeof abp.features.isEnabled === 'function' - && !abp.features.isEnabled('Unity.Notifications.DirectMessaging')) { - return; - } + const path = window.location.pathname.toLowerCase(); + const guardedPaths = ['/account/login', '/login', '/splash']; - if (typeof signalR === 'undefined') { - return; + for (const guardedPath of guardedPaths) { + if (path.includes(guardedPath)) { + return false; + } + } + + if (path === '/') { + return false; + } + + if (!window.abp) { + return false; + } + + if (!window.abp?.currentUser?.isAuthenticated) { + return false; + } + + if (!window.signalR) { + return false; + } + + return true; + } finally { + shouldInit.depth--; } + } - if (!window.abp?.currentUser?.isAuthenticated) { + function init() { + if (!shouldInit()) { return; } @@ -41,16 +71,42 @@ let peers = []; let activeMode = 'individual'; let currentTenant = null; + const selectedTargets = { individual: '', tenant: '' }; + const modeNotificationCounts = { individual: 0, tenant: 0 }; + let targetSelect2Open = false; + let targetOptionsRefreshPending = false; const histories = {}; const MAX_HISTORY_ITEMS = 100; const STATUS_GREEN_MS = 10 * 60 * 1000; const STATUS_ORANGE_MS = 30 * 60 * 1000; + const BUBBLE_POSITION_STORAGE_KEY = 'unity.notifications.realtime.bubble-position'; + const PANEL_SIZE_STORAGE_KEY = 'unity.notifications.realtime.panel-size'; + const PANEL_POSITION_STORAGE_KEY = 'unity.notifications.realtime.panel-position'; const widget = buildWidget(); + setupBubbleDragging(); + setupPanelResizing(); + setupPanelDragging(); + setupTargetSelect2(); const connection = new signalR.HubConnectionBuilder() .withUrl('/signalr/notifications') .withAutomaticReconnect() .build(); + let connectionStartTask = null; + + function startConnection() { + if (connection.state === signalR.HubConnectionState.Connected) { + return Promise.resolve(); + } + + if (!connectionStartTask) { + connectionStartTask = connection.start().finally(function () { + connectionStartTask = null; + }); + } + + return connectionStartTask; + } connection.on('directMessageReceived', function (eventData) { const scope = eventData?.scope || 'user'; @@ -69,16 +125,13 @@ addMessage(mode, targetId, sender, senderId, message, eventData?.timestamp); if (scope === 'user') { - activeMode = 'individual'; ensurePeerOption(senderId, sender); - } else { - activeMode = 'tenant'; } - widget.modeTabs.forEach(tab => tab.classList.toggle('active', tab.dataset.mode === activeMode)); - renderTargetOptions(); - widget.target.value = targetId; - renderConversation(); + if (senderId !== myUserId) { + modeNotificationCounts[mode] += 1; + updateModeTabCounts(); + } if (eventData.source === 'UnityMessagingController' && senderId && senderId !== myUserId) { showIncomingToast(sender, message); @@ -92,10 +145,10 @@ const HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000; const ACTIVITY_HEARTBEAT_THROTTLE_MS = 60 * 1000; - const ACTIVITY_EVENTS = ['click', 'keydown', 'mousemove', 'scroll']; + const ACTIVITY_EVENTS = ['click', 'keydown', 'scroll']; let lastHeartbeatSentAt = 0; - connection.start().then(function () { + startConnection().then(function () { sendHeartbeat(true); refreshPeers(); refreshTenant(); @@ -115,15 +168,27 @@ } connection.invoke('GetUnreadMessagesAsync').then(function (messages) { + let firstUnreadMode = null; + let firstUnreadTarget = null; + (Array.isArray(messages) ? messages : []).forEach(function (eventData) { const scope = eventData.scope === 'tenant' ? 'tenant' : 'user'; const senderId = eventData.senderId || null; - const targetId = scope === 'tenant' ? eventData.targetId : senderId; + const targetId = scope === 'tenant' + ? eventData.targetId + : eventData.targetId || senderId; if (!targetId) { return; } + modeNotificationCounts[scope === 'tenant' ? 'tenant' : 'individual'] += 1; + + if (!firstUnreadTarget) { + firstUnreadMode = scope === 'tenant' ? 'tenant' : 'individual'; + firstUnreadTarget = targetId; + } + addMessage( scope === 'tenant' ? 'tenant' : 'individual', targetId, @@ -141,6 +206,19 @@ showIncomingToast(eventData.senderName || senderId, eventData.message || ''); } }); + + if (firstUnreadTarget) { + activeMode = firstUnreadMode; + widget.modeTabs.forEach(function (tab) { + tab.classList.toggle('active', tab.dataset.mode === activeMode); + }); + renderTargetOptions(); + widget.target.value = firstUnreadTarget; + refreshTargetSelect2(); + renderConversation(); + } + + updateModeTabCounts(); }).catch(function () { // Unread history is a convenience; realtime delivery remains available. }); @@ -178,6 +256,12 @@ peers = (Array.isArray(result) ? result : []).filter(function (p) { return p?.userId && p.userId !== myUserId; }); + + if (targetSelect2Open) { + targetOptionsRefreshPending = true; + return; + } + renderTargetOptions(); } @@ -188,6 +272,12 @@ connection.invoke('GetCurrentTenantAsync').then(function (result) { currentTenant = result || null; + + if (targetSelect2Open) { + targetOptionsRefreshPending = true; + return; + } + renderTargetOptions(); }).catch(function () { currentTenant = null; @@ -195,7 +285,12 @@ } function renderTargetOptions() { - const currentValue = widget.target.value; + if (targetSelect2Open || (window.jQuery?.('.select2-container--open').length > 0)) { + targetOptionsRefreshPending = true; + return; + } + + const currentValue = selectedTargets[activeMode] || widget.target.value; if (activeMode === 'tenant') { widget.targetControl.style.display = 'none'; @@ -205,7 +300,9 @@ widget.target.disabled = !currentTenant; if (currentTenant) { widget.target.value = currentTenant.id; + selectedTargets.tenant = currentTenant.id; } + refreshTargetSelect2(); updateTargetStatusDot(); renderConversation(); return; @@ -216,6 +313,7 @@ if (peers.length === 0) { widget.target.innerHTML = ``; widget.target.disabled = true; + refreshTargetSelect2(); updateTargetStatusDot(); return; } @@ -223,19 +321,117 @@ widget.target.disabled = false; widget.target.innerHTML = `` + peers .map(function (p) { - const suffix = p.isOnline ? '' : ` (${l('RealtimeWidget:StatusOffline')})`; - return ``; + return ``; }) .join(''); if (currentValue && peers.some(function (p) { return p.userId === currentValue; })) { widget.target.value = currentValue; + selectedTargets.individual = currentValue; } + refreshTargetSelect2(); updateTargetStatusDot(); renderConversation(); } + function setupTargetSelect2() { + if (!window.jQuery || !window.jQuery.fn?.select2) { + return; + } + + window.jQuery(widget.target).select2({ + theme: 'bootstrap-5', + width: '100%', + placeholder: l('RealtimeWidget:To'), + allowClear: true, + dropdownParent: window.jQuery(widget.targetControl), + templateResult: renderTargetSelect2Option, + templateSelection: renderTargetSelect2Option, + escapeMarkup: function (markup) { return markup; } + }); + + window.jQuery(widget.target) + .on('select2:open', function () { + targetSelect2Open = true; + syncTargetSelect2DropdownSize(); + }) + .on('select2:close', function () { + targetSelect2Open = false; + if (targetOptionsRefreshPending) { + targetOptionsRefreshPending = false; + renderTargetOptions(); + } else { + refreshTargetSelect2(); + } + }); + + window.addEventListener('resize', syncTargetSelect2DropdownSize); + } + + function syncTargetSelect2DropdownSize() { + if (!window.jQuery?.fn?.select2 || !window.jQuery(widget.target).data('select2')) { + return; + } + + const dropdown = widget.targetControl.querySelector('.select2-dropdown'); + if (dropdown && window.jQuery?.('.select2-container--open').length > 0) { + dropdown.style.setProperty('width', `${widget.targetControl.getBoundingClientRect().width}px`, 'important'); + } + } + + function refreshTargetSelect2() { + const select2MenuOpen = targetSelect2Open + || (window.jQuery?.('.select2-container--open').length > 0); + + if (!select2MenuOpen + && window.jQuery + && window.jQuery.fn?.select2 + && window.jQuery(widget.target).data('select2')) { + window.jQuery(widget.target).trigger('change.select2'); + } + } + + function renderTargetSelect2Option(data) { + if (!data.id) { + return data.text; + } + + const peer = peers.find(function (item) { return item.userId === data.id; }); + const status = activeMode === 'tenant' ? null : getPeerStatus(peer); + const statusMarkup = status + ? `` + : ''; + + return `${statusMarkup}${escapeHtml(data.text)}`; + } + + function loadConversationHistory(mode, targetId) { + if (!targetId || connection.state !== signalR.HubConnectionState.Connected) { + return; + } + + connection.invoke('GetConversationHistoryAsync', mode, targetId).then(function (messages) { + if (activeMode !== mode || widget.target.value !== targetId) { + return; + } + + const key = `${mode}:${targetId}`; + histories[key] = (Array.isArray(messages) ? messages : []).map(function (eventData) { + return { + sender: eventData.senderName || eventData.senderId || 'unknown', + senderId: eventData.senderId || null, + message: eventData.message || '', + timestamp: eventData.timestamp, + mode + }; + }).slice(-MAX_HISTORY_ITEMS); + renderConversation(); + }).catch(function () { + // Conversation history is optional; realtime and unread messages remain available. + }); + } + function getPeerStatus(peer) { if (!peer?.isOnline) { return { className: 'offline', label: l('RealtimeWidget:StatusOffline') }; @@ -264,21 +460,343 @@ } const peer = peers.find(function (p) { return p.userId === widget.target.value; }); + if (!peer || !widget.target.value) { + widget.targetStatus.className = 'rt-status-dot rt-widget-target-status rt-status-hidden'; + widget.targetStatus.removeAttribute('title'); + return; + } + const status = getPeerStatus(peer); widget.targetStatus.className = `rt-status-dot rt-widget-target-status rt-status-${status.className}`; widget.targetStatus.title = status.label; } + function setupBubbleDragging() { + let dragState = null; + let suppressNextClick = false; + + restoreBubblePosition(); + + widget.bubble.addEventListener('pointerdown', function (event) { + if (event.button !== 0) { + return; + } + + const bounds = widget.container.getBoundingClientRect(); + dragState = { + startX: event.clientX, + startY: event.clientY, + left: bounds.left, + top: bounds.top, + moved: false + }; + widget.bubble.setPointerCapture(event.pointerId); + event.preventDefault(); + }); + + widget.bubble.addEventListener('pointermove', function (event) { + if (!dragState) { + return; + } + + const deltaX = event.clientX - dragState.startX; + const deltaY = event.clientY - dragState.startY; + dragState.moved = dragState.moved || Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3; + + if (!dragState.moved) { + return; + } + + const position = clampBubblePosition( + dragState.left + deltaX, + dragState.top + deltaY + ); + setBubblePosition(position.left, position.top); + event.preventDefault(); + }); + + widget.bubble.addEventListener('pointerup', finishDrag); + widget.bubble.addEventListener('pointercancel', finishDrag); + + widget.bubble.addEventListener('click', function (event) { + if (suppressNextClick) { + suppressNextClick = false; + event.preventDefault(); + event.stopImmediatePropagation(); + } + }, true); + + function finishDrag(event) { + if (!dragState) { + return; + } + + if (dragState.moved) { + const bounds = widget.container.getBoundingClientRect(); + saveBubblePosition(bounds.left, bounds.top); + suppressNextClick = true; + } + + if (widget.bubble.hasPointerCapture(event.pointerId)) { + widget.bubble.releasePointerCapture(event.pointerId); + } + dragState = null; + } + } + + function clampBubblePosition(left, top) { + const bounds = widget.bubble.getBoundingClientRect(); + const margin = 8; + const maxLeft = Math.max(margin, window.innerWidth - bounds.width - margin); + const maxTop = Math.max(margin, window.innerHeight - bounds.height - margin); + + return { + left: Math.min(Math.max(left, margin), maxLeft), + top: Math.min(Math.max(top, margin), maxTop) + }; + } + + function setBubblePosition(left, top) { + widget.container.style.left = `${left}px`; + widget.container.style.top = `${top}px`; + widget.container.style.right = 'auto'; + widget.container.style.bottom = 'auto'; + } + + function reportNonFatalError(message, error) { + console.warn(message, error); + } + + function saveBubblePosition(left, top) { + try { + localStorage.setItem(BUBBLE_POSITION_STORAGE_KEY, JSON.stringify({ left, top })); + } catch (error) { + reportNonFatalError('Unable to save bubble position.', error); + } + } + + function restoreBubblePosition() { + try { + const storedPosition = JSON.parse(localStorage.getItem(BUBBLE_POSITION_STORAGE_KEY)); + if (Number.isFinite(storedPosition?.left) && Number.isFinite(storedPosition?.top)) { + const position = clampBubblePosition(storedPosition.left, storedPosition.top); + setBubblePosition(position.left, position.top); + } + } catch (error) { + reportNonFatalError('Unable to restore bubble position.', error); + } + } + + function setupPanelResizing() { + let resizeState = null; + + restorePanelSize(); + + widget.resizeHandle.addEventListener('pointerdown', function (event) { + if (event.button !== 0) { + return; + } + + const bounds = widget.panel.getBoundingClientRect(); + resizeState = { + startX: event.clientX, + startY: event.clientY, + width: bounds.width, + height: bounds.height + }; + widget.resizeHandle.setPointerCapture(event.pointerId); + event.preventDefault(); + event.stopPropagation(); + }); + + widget.resizeHandle.addEventListener('pointermove', function (event) { + if (!resizeState) { + return; + } + + const size = clampPanelSize( + resizeState.width + event.clientX - resizeState.startX, + resizeState.height + event.clientY - resizeState.startY + ); + widget.panel.style.width = `${size.width}px`; + widget.panel.style.height = `${size.height}px`; + syncTargetSelect2DropdownSize(); + event.preventDefault(); + }); + + widget.resizeHandle.addEventListener('pointerup', finishResize); + widget.resizeHandle.addEventListener('pointercancel', finishResize); + + function finishResize(event) { + if (!resizeState) { + return; + } + + const bounds = widget.panel.getBoundingClientRect(); + savePanelSize(bounds.width, bounds.height); + + if (widget.resizeHandle.hasPointerCapture(event.pointerId)) { + widget.resizeHandle.releasePointerCapture(event.pointerId); + } + resizeState = null; + } + } + + function setupPanelDragging() { + let dragState = null; + + restorePanelPosition(); + + widget.header.addEventListener('pointerdown', function (event) { + if (event.button !== 0 || event.target.closest('button')) { + return; + } + + const bounds = widget.panel.getBoundingClientRect(); + setPanelPosition(bounds.left, bounds.top); + dragState = { + startX: event.clientX, + startY: event.clientY, + left: bounds.left, + top: bounds.top + }; + widget.header.setPointerCapture(event.pointerId); + event.preventDefault(); + }); + + widget.header.addEventListener('pointermove', function (event) { + if (!dragState) { + return; + } + + const position = clampPanelPosition( + dragState.left + event.clientX - dragState.startX, + dragState.top + event.clientY - dragState.startY + ); + setPanelPosition(position.left, position.top); + event.preventDefault(); + }); + + widget.header.addEventListener('pointerup', finishDrag); + widget.header.addEventListener('pointercancel', finishDrag); + + function finishDrag(event) { + if (!dragState) { + return; + } + + const bounds = widget.panel.getBoundingClientRect(); + savePanelPosition(bounds.left, bounds.top); + + if (widget.header.hasPointerCapture(event.pointerId)) { + widget.header.releasePointerCapture(event.pointerId); + } + dragState = null; + } + } + + function clampPanelPosition(left, top) { + const bounds = widget.panel.getBoundingClientRect(); + const margin = 8; + const maxLeft = Math.max(margin, window.innerWidth - bounds.width - margin); + const maxTop = Math.max(margin, window.innerHeight - bounds.height - margin); + + return { + left: Math.min(Math.max(left, margin), maxLeft), + top: Math.min(Math.max(top, margin), maxTop) + }; + } + + function setPanelPosition(left, top) { + widget.panel.style.left = `${left}px`; + widget.panel.style.top = `${top}px`; + widget.panel.style.right = 'auto'; + widget.panel.style.bottom = 'auto'; + } + + function savePanelPosition(left, top) { + try { + localStorage.setItem(PANEL_POSITION_STORAGE_KEY, JSON.stringify({ left, top })); + } catch (error) { + reportNonFatalError('Unable to save panel position.', error); + } + } + + function restorePanelPosition() { + try { + const storedPosition = JSON.parse(localStorage.getItem(PANEL_POSITION_STORAGE_KEY)); + if (Number.isFinite(storedPosition?.left) && Number.isFinite(storedPosition?.top)) { + setPanelPosition(storedPosition.left, storedPosition.top); + } + } catch (error) { + reportNonFatalError('Unable to restore panel position.', error); + } + } + + function clampPanelSize(width, height) { + const margin = 16; + const minWidth = 260; + const minHeight = 240; + const maxWidth = Math.max(minWidth, window.innerWidth - margin * 2); + const maxHeight = Math.max(minHeight, window.innerHeight - margin * 2); + + return { + width: Math.min(Math.max(width, minWidth), maxWidth), + height: Math.min(Math.max(height, minHeight), maxHeight) + }; + } + + function savePanelSize(width, height) { + try { + localStorage.setItem(PANEL_SIZE_STORAGE_KEY, JSON.stringify({ width, height })); + } catch (error) { + reportNonFatalError('Unable to save panel size.', error); + } + } + + function restorePanelSize() { + try { + const storedSize = JSON.parse(localStorage.getItem(PANEL_SIZE_STORAGE_KEY)); + if (Number.isFinite(storedSize?.width) && Number.isFinite(storedSize?.height)) { + const size = clampPanelSize(storedSize.width, storedSize.height); + widget.panel.style.width = `${size.width}px`; + widget.panel.style.height = `${size.height}px`; + } + } catch (error) { + reportNonFatalError('Unable to restore panel size.', error); + } + } + widget.composeSend.addEventListener('click', sendComposeMessage); widget.target.addEventListener('change', function () { + selectedTargets[activeMode] = widget.target.value; updateTargetStatusDot(); renderConversation(); }); + if (window.jQuery) { + window.jQuery(widget.target).on('select2:select select2:clear', function () { + selectedTargets[activeMode] = widget.target.value; + updateTargetStatusDot(); + renderConversation(); + + if (widget.target.value) { + loadConversationHistory(activeMode, widget.target.value); + } + }); + } widget.modeTabs.forEach(function (tab) { tab.addEventListener('click', function () { + selectedTargets[activeMode] = widget.target.value; activeMode = tab.dataset.mode; + modeNotificationCounts[activeMode] = 0; + updateModeTabCounts(); widget.modeTabs.forEach(item => item.classList.toggle('active', item === tab)); renderTargetOptions(); + renderConversation(); + + if (widget.target.value) { + loadConversationHistory(activeMode, widget.target.value); + } }); }); widget.composeInput.addEventListener('keydown', function (e) { @@ -289,6 +807,7 @@ }); function sendComposeMessage() { + const messageMode = activeMode; const targetId = widget.target.value; const message = widget.composeInput.value.trim(); @@ -296,18 +815,33 @@ return; } - if (connection.state !== signalR.HubConnectionState.Connected) { + if (message.length > 4000) { return; } - const sendTask = activeMode === 'tenant' - ? connection.invoke('SendTenantMessageAsync', message) - : connection.invoke('SendPeerMessageAsync', targetId, message); + const sendTask = startConnection().then(function () { + if (connection.state !== signalR.HubConnectionState.Connected) { + throw new Error('Realtime connection is unavailable.'); + } + + return activeMode === 'tenant' + ? connection.invoke('SendTenantMessageAsync', message) + : connection.invoke('SendPeerMessageAsync', targetId, message); + }); sendTask.then(function () { - if (activeMode !== 'tenant') { - addMessage(activeMode, targetId, l('RealtimeWidget:You'), null, message, new Date().toISOString()); + if (messageMode !== 'tenant') { + addMessage(messageMode, targetId, l('RealtimeWidget:You'), null, message, new Date().toISOString()); } + + activeMode = messageMode; + widget.modeTabs.forEach(function (tab) { + tab.classList.toggle('active', tab.dataset.mode === activeMode); + }); + renderTargetOptions(); + widget.target.value = targetId; + refreshTargetSelect2(); + renderConversation(); widget.composeInput.value = ''; }).catch(function () { // Recipient is invalid or outside the tenant; leave the draft in place. @@ -324,7 +858,7 @@ unreadCount += 1; updateBadge(); widget.bubble.classList.remove('rt-widget-bounce'); - widget.bubble.offsetWidth; + widget.bubble.getBoundingClientRect(); widget.bubble.classList.add('rt-widget-bounce'); } @@ -407,7 +941,7 @@ toast: true, position: 'top-end', icon: 'info', - title: sender, + titleText: String(sender || ''), text: message, showConfirmButton: false, timer: 5000, @@ -421,6 +955,20 @@ } } + function updateModeTabCounts() { + widget.modeTabs.forEach(function (tab) { + const count = modeNotificationCounts[tab.dataset.mode] || 0; + const countElement = tab.querySelector('.rt-widget-mode-count'); + + if (!countElement) { + return; + } + + countElement.textContent = count > 9 ? '9+' : String(count); + countElement.classList.toggle('rt-widget-hidden', count === 0); + }); + } + function togglePanel() { panelOpen = !panelOpen; widget.panel.classList.toggle('rt-widget-panel-open', panelOpen); @@ -454,6 +1002,12 @@ const panel = document.createElement('div'); panel.className = 'rt-widget-panel'; + const resizeHandle = document.createElement('span'); + resizeHandle.className = 'rt-widget-resize-handle'; + resizeHandle.setAttribute('role', 'presentation'); + resizeHandle.setAttribute('aria-hidden', 'true'); + panel.appendChild(resizeHandle); + const header = document.createElement('div'); header.className = 'rt-widget-panel-header'; @@ -489,12 +1043,14 @@ individualTab.className = 'rt-widget-mode-tab active'; individualTab.dataset.mode = 'individual'; individualTab.textContent = l('RealtimeWidget:Individual'); + appendModeCount(individualTab); const tenantTab = document.createElement('button'); tenantTab.type = 'button'; tenantTab.className = 'rt-widget-mode-tab'; tenantTab.dataset.mode = 'tenant'; tenantTab.textContent = l('RealtimeWidget:Tenant'); + appendModeCount(tenantTab); modeTabs.appendChild(individualTab); modeTabs.appendChild(tenantTab); @@ -556,7 +1112,13 @@ container.appendChild(bubble); document.body.appendChild(container); - return { container, panel, list, bubble, badge, modeTabs: [individualTab, tenantTab], targetControl, target, targetStatus, composeInput, composeSend }; + return { container, panel, list, bubble, badge, resizeHandle, header, modeTabs: [individualTab, tenantTab], targetControl, target, targetStatus, composeInput, composeSend }; + + function appendModeCount(tab) { + const count = document.createElement('span'); + count.className = 'rt-widget-mode-count rt-widget-hidden'; + tab.appendChild(count); + } } } })(); diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js index 532a7e48d4..3bffd103a3 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Pages/PaymentRequests/Index.js @@ -1042,7 +1042,7 @@ $(function () { } function formatName(userData) { - return typeof userData !== 'undefined' && userData !== null ? `${userData?.name} ${userData?.surname}` : ""; + return userData !== undefined && userData !== null ? `${userData?.name} ${userData?.surname}` : ""; } function getApprovalDateColumn(columnIndex, level) { diff --git a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js index 7e70f1d011..6de50c7259 100644 --- a/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js +++ b/applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Web/Views/Shared/Components/PaymentInfo/Default.js @@ -1,4 +1,4 @@ -$(function () { +$(function () { const l = abp.localization.getResource('Payments'); $('.unity-currency-input').maskMoney({}); $('.unity-currency-input').each(function () { @@ -61,10 +61,8 @@ inputElement.hasClass('unity-currency-input') || inputElement.hasClass('numeric-mask') ) { - paymentInfoObj[input.name.split('.')[1]] = input.value.replace( - /,/g, - '' - ); + const fieldName = input.name.split('.')[1]; + paymentInfoObj[fieldName] = input.value.replaceAll(',', ''); } else { paymentInfoObj[input.name.split('.')[1]] = input.value; } diff --git a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Reconciliation/Index.js b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Reconciliation/Index.js index e38cc30cb5..2018e6539e 100644 --- a/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Reconciliation/Index.js +++ b/applications/Unity.GrantManager/modules/Unity.TenantManagement/src/Unity.TenantManagement.Web/Pages/TenantManagement/Reconciliation/Index.js @@ -146,12 +146,12 @@ $(function () { setExternalSearchFilter(iDt); if ($('#btn-toggle-filter').length) { - if ($.fn.dataTable !== 'undefined' && typeof $.fn.dataTable.FilterRow !== 'undefined') { + if ($.fn.dataTable !== undefined && $.fn.dataTable.FilterRow !== undefined) { const filterRow = new $.fn.dataTable.FilterRow(iDt.settings()[0], { buttonId: 'btn-toggle-filter', buttonText: FilterDesc.Default, buttonTextActive: FilterDesc.With_Filter, - enablePopover: $.fn.popover !== 'undefined' + enablePopover: $.fn.popover !== undefined }); iDt.settings()[0]._filterRow = filterRow; diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalStyleContributor.cs b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalStyleContributor.cs index e66ee48005..82129c2826 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalStyleContributor.cs +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Bundling/UnityThemeUX2GlobalStyleContributor.cs @@ -23,12 +23,6 @@ public override void ConfigureBundle(BundleConfigurationContext context) context.Files.AddIfNotContains("/libs/datatables.net-staterestore-dt/css/stateRestore.dataTables.min.css"); context.Files.AddIfNotContains("/libs/tributejs/dist/tribute.css"); - // Add assets for "/themes/ux2/fonts/**/*" - context.Files.AddRange([ - "/themes/ux2/fonts/icons/Segoe-Fluent-Icons.ttf", - "/themes/ux2/fonts/icons/Segoe-MDL2-Assets.ttf", - ]); - // ABP's own FontAwesomeStyleContributor adds the v4 compatibility shims // alongside all.css. Every rule in that file is scoped ".fa.fa-x", and no // element in this app carries the bare "fa" class any more (AB#33942), so diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml index 45283ea594..ba0c57eb4c 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Components/Topbar/Default.cshtml @@ -34,6 +34,9 @@ bool isAuthorizedForTenantSwitch = false; if (CurrentUser.IsAuthenticated && CurrentUser.FindClaimValue("has_multiple_tenants") == "true") isAuthorizedForTenantSwitch = true; + + bool isItOperations = CurrentUser.IsInRole("ITOperations"); + bool isDirectMessagingEnabled = isItOperations && await FeatureChecker.IsEnabledAsync("Unity.Notifications.DirectMessaging"); } @@ -53,12 +56,15 @@ { Configuration Management } - @if (CurrentUser.IsInRole("ITOperations")) + @if (isItOperations) { Unity Admin - Unity Messaging Exception Logs } + @if (isDirectMessagingEnabled) + { + Unity Messaging + } Logout diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Application.cshtml b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Application.cshtml index 2374b9863c..a797d7d9f1 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Application.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Application.cshtml @@ -109,7 +109,8 @@ @await Component.InvokeLayoutHookAsync(LayoutHooks.PageContent.Last, StandardLayouts.Application)
- + + diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Empty.cshtml b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Empty.cshtml index cb0780c51b..9bf7c310f0 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Empty.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/Themes/UX2/Layouts/Empty.cshtml @@ -65,7 +65,6 @@
- diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/unity-styles.css b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/unity-styles.css index 870d90080c..a0bc3def3d 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/unity-styles.css +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/unity-styles.css @@ -675,9 +675,12 @@ div.dt-container div.dt-search { #app-version-id { position: absolute; + top: 0; right: 0; + z-index: 1; font-size: 0.7rem; opacity: 0.5; + white-space: nowrap; } .worksheet_section_label { diff --git a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js index 038076fa8d..ec88681375 100644 --- a/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js +++ b/applications/Unity.GrantManager/modules/Unity.Theme.UX2/src/Unity.Theme.UX2/wwwroot/themes/ux2/zone-extensions.js @@ -5,27 +5,9 @@ /** * Unflatten dot separated JSON objects into nested objects - */ - $.fn.unflattenObject = function(flatObj) { - const result = {}; - for (const flatKey in flatObj) { - const value = flatObj[flatKey]; - if (!flatKey) continue; - const keys = flatKey.split('.'); - let cur = result; - for (let i = 0; i < keys.length; i++) { - const k = keys[i]; - if (i === keys.length - 1) { - cur[k] = value; - } else { - cur[k] = cur[k] || {}; - cur = cur[k]; - } - } - } - return result; - } - + return str + .replace(/^[A-Z]/, match => match.toLowerCase()) + .replace(/\.[A-Z]/g, match => match.toLowerCase()); /** * @public * Handles zone fieldset serialization with DTO nesting @@ -113,27 +95,9 @@ * @returns */ let toCamelCaseInternal = function (str) { - let regexs = [ - /(^[A-Z])/, // first char of string - /((\.)[A-Z])/ // first char after a dot (.) - ]; - - regexs.forEach( - function (regex) { - let infLoopAvoider = 0; - - while (regex.test(str)) { - str = str - .replace(regex, function ($1) { return $1.toLowerCase(); }); - - if (infLoopAvoider++ > 1000) { - break; - } - } - } - ); - - return str; + return str + .replace(/^[A-Z]/, match => match.toLowerCase()) + .replace(/\.[A-Z]/g, match => match.toLowerCase()); } /** diff --git a/applications/Unity.GrantManager/scripts/Get-SonarIssues.ps1 b/applications/Unity.GrantManager/scripts/Get-SonarIssues.ps1 index 994a1ece4a..f773f33560 100644 --- a/applications/Unity.GrantManager/scripts/Get-SonarIssues.ps1 +++ b/applications/Unity.GrantManager/scripts/Get-SonarIssues.ps1 @@ -8,11 +8,10 @@ or attaching to a PR instead of screen-scraping the SonarQube UI. .PARAMETER ServerUrl - SonarQube server URL. Default: https://sonarqube.econ.gov.bc.ca/sonar. Set this to - https://sonarcloud.io when querying SonarCloud. + SonarQube server URL. Default: https://sonarcloud.io, matching sonar-project.properties. .PARAMETER ProjectKey - SonarQube project (component) key. Default: UnityScanKey. + SonarQube project (component) key. Default: bcgov_Unity. .PARAMETER Branch Branch name to query. If omitted, you'll be prompted to pick the current git branch, one of @@ -70,9 +69,9 @@ .\Get-SonarIssues.ps1 -Branch main -FixLevel Quick #> param( - [string]$ServerUrl = "https://sonarqube.econ.gov.bc.ca/sonar", + [string]$ServerUrl = "https://sonarcloud.io", - [string]$ProjectKey = "UnityScanKey", + [string]$ProjectKey = "bcgov_Unity", [string]$Branch = "", @@ -281,9 +280,9 @@ function Get-SonarIssuePage { if (-not $response.IsSuccessStatusCode) { $status = [int]$response.StatusCode if ($status -eq 401 -or $status -eq 403) { - throw "SonarCloud returned $status - pass -Token (or set `$env:SONAR_TOKEN) with access to '$ProjectKey'." + throw "Sonar server returned $status for '$ProjectKey' at '$ServerUrl'. Pass -Token (or set `$env:SONAR_TOKEN) with browse access to this project." } - throw "SonarCloud returned $status`: $text" + throw "Sonar server returned $status`: $text" } return $text | ConvertFrom-Json diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs new file mode 100644 index 0000000000..449f52463d --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationDto.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Unity.GrantManager.GrantApplications +{ + public class BulkEmailNotificationDto + { + public BulkEmailNotificationDto() + { + ValidationMessages = []; + ReferenceNo = string.Empty; + ApplicantName = string.Empty; + FormName = string.Empty; + ApplicationStatus = string.Empty; + } + + public List ValidationMessages { get; set; } + public bool IsValid { get; set; } + + public Guid ApplicationId { get; set; } + public Guid? EmailId { get; set; } + public string? EmailSubject { get; set; } + public string ReferenceNo { get; set; } + public string ApplicantName { get; set; } + public string FormName { get; set; } + public string ApplicationStatus { get; set; } + public decimal ApprovedAmount { get; set; } + public DateTime? DecisionDate { get; set; } + public string? CreatedByName { get; set; } + public DateTime? LastModified { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationResultDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationResultDto.cs new file mode 100644 index 0000000000..5f1e68a8f5 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/BulkEmailNotificationResultDto.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Unity.GrantManager.GrantApplications +{ + public class BulkEmailNotificationResultDto + { + public List Successes { get; set; } = []; + + [JsonProperty("failures")] + public List> Failures { get; set; } = []; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkEmailNotificationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkEmailNotificationAppService.cs new file mode 100644 index 0000000000..bb53c56ce2 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IBulkEmailNotificationAppService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Unity.GrantManager.GrantApplications +{ + public interface IBulkEmailNotificationAppService + { + Task SendBulkEmailNotifications(List batchApplicationsToEmail); + Task> GetApplicationsForBulkEmail(Guid[] applicationGuids); + Task RevalidateApplicationForBulkEmail(Guid applicationId); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs new file mode 100644 index 0000000000..5fcac05aa3 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/GrantApplications/BulkEmailNotificationAppService.cs @@ -0,0 +1,269 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Notifications.Email; +using Unity.Modules.Shared.Utils; +using Unity.Notifications.Emails; +using Unity.Notifications.Permissions; +using Volo.Abp; +using Volo.Abp.Users; + +namespace Unity.GrantManager.GrantApplications +{ + // The right-panel editor reuses EmailsWidget, whose edit fieldset/Save/attachment handling all require + // Notifications.Email.Send (a sibling permission, not a parent of SendBulk) — so both are required here + // too, not just SendBulk, or a user granted SendBulk alone could reach this API into a state the UI can't + // actually support. Two stacked [Authorize] attributes compose as AND per standard ASP.NET Core semantics. + [Authorize(NotificationsPermissions.Email.SendBulk)] + [Authorize(NotificationsPermissions.Email.Send)] + public class BulkEmailNotificationAppService( + IApplicationRepository applicationRepository, + IEmailLogsRepository emailLogsRepository, + IEmailLogAttachmentRepository emailLogAttachmentRepository, + IEmailAppService emailAppService, + IExternalUserLookupServiceProvider externalUserLookupServiceProvider, + IConfiguration configuration) : GrantManagerAppService, IBulkEmailNotificationAppService + { + /// + /// Get applications for bulk email with added draft validation information + /// + /// + /// + public async Task> GetApplicationsForBulkEmail(Guid[] applicationGuids) + { + var applications = await applicationRepository.GetListByIdsAsync(applicationGuids); + var draftEmails = await emailLogsRepository.GetByApplicationIdsAndStatusAsync([.. applicationGuids], EmailStatus.Draft); + var draftsByApplication = draftEmails.GroupBy(e => e.ApplicationId).ToDictionary(g => g.Key, g => g.ToList()); + var draftAuthorsById = await GetDraftAuthorsAsync(draftEmails); + + var applicationsForEmail = new List(); + foreach (var application in applications) + { + draftsByApplication.TryGetValue(application.Id, out var drafts); + applicationsForEmail.Add(MapBulkEmailNotification(application, drafts ?? [], draftAuthorsById)); + } + + return applicationsForEmail; + } + + /// + /// Re-validate a single application's draft state (e.g. after an in-panel Save) without re-fetching the whole batch + /// + /// + /// + public async Task RevalidateApplicationForBulkEmail(Guid applicationId) + { + var applications = await applicationRepository.GetListByIdsAsync([applicationId]); + var application = applications.Single(); + var drafts = await emailLogsRepository.GetByApplicationIdsAndStatusAsync([applicationId], EmailStatus.Draft); + var draftAuthorsById = await GetDraftAuthorsAsync(drafts); + + return MapBulkEmailNotification(application, drafts, draftAuthorsById); + } + + private async Task> GetDraftAuthorsAsync(List drafts) + { + var creatorIds = drafts + .Where(d => d.CreatorId.HasValue) + .Select(d => d.CreatorId!.Value) + .Distinct(); + + var draftAuthorsById = new Dictionary(); + foreach (var creatorId in creatorIds) + { + var userInfo = await externalUserLookupServiceProvider.FindByIdAsync(creatorId); + if (userInfo != null) + { + draftAuthorsById[creatorId] = userInfo; + } + } + + return draftAuthorsById; + } + + /// + /// Send bulk email notifications for the given batch of draft emails + /// + /// + /// + public async Task SendBulkEmailNotifications(List batchApplicationsToEmail) + { + var bulkEmailResult = new BulkEmailNotificationResultDto(); + + // Fail the whole batch up front if notifications are disabled, rather than reporting false successes + // for emails that SendAsync would silently drop (it always returns true after publishing the event). + if (!await FeatureChecker.IsEnabledAsync("Unity.Notifications")) + { + foreach (var applicationToEmail in batchApplicationsToEmail) + { + bulkEmailResult.Failures.Add(new KeyValuePair(applicationToEmail.ReferenceNo, "Email notifications are currently disabled.")); + } + return bulkEmailResult; + } + + // We send individually here so that a failure on one application does not block the rest of the batch + foreach (var applicationToEmail in batchApplicationsToEmail) + { + try + { + if (!applicationToEmail.EmailId.HasValue) + { + throw new UserFriendlyException("No draft email was found for this application."); + } + + // Re-fetch the draft fresh (defense-in-depth: it may have changed since the modal opened) + var draft = await emailLogsRepository.GetAsync(applicationToEmail.EmailId.Value); + if (draft.Status != EmailStatus.Draft) + { + throw new UserFriendlyException("This email is no longer a draft."); + } + + // The posted ApplicationId is client-controlled (hidden form field) — never trust it for + // authorization-relevant writes. Confirm it still matches the draft's real owning application + // and use the draft's own ApplicationId, not the posted one, when sending. + if (draft.ApplicationId != applicationToEmail.ApplicationId) + { + throw new UserFriendlyException("This draft no longer matches the selected application."); + } + + // Re-check the "exactly one draft" invariant fresh at send time: this endpoint can be reached + // directly (bypassing the modal's GetApplicationsForBulkEmail check), and another draft may + // have been created for this application after the modal was loaded. + var currentDrafts = await emailLogsRepository.GetByApplicationIdsAndStatusAsync([draft.ApplicationId], EmailStatus.Draft); + if (currentDrafts.Count != 1) + { + throw new UserFriendlyException("Multiple draft emails found for this application. Please retain only one draft before proceeding."); + } + + // A non-blank ToAddress can still parse to zero recipients (e.g. ";" or ",") — the same check + // the send pipeline itself uses. Catch that here instead of reporting a false success: SendAsync + // always returns true, but the handler silently drops emails with no parseable recipients. + if (draft.ToAddress.ParseEmailList() is not { Count: > 0 }) + { + throw new UserFriendlyException("Draft email is missing a To address. Please update the draft before proceeding."); + } + + // Neither the per-upload size gate nor the modal's own UI check can catch every path an + // attachment can arrive by (e.g. copying a template's attachments onto a draft applies no + // size check at all), and this endpoint can be reached directly regardless of what the UI + // showed. FileSize is recorded on upload/copy, not derived from S3, so this is a cheap + // in-database check, not a storage round-trip. + var attachments = await emailLogAttachmentRepository.GetByEmailLogIdAsync(draft.Id); + var totalAttachmentMb = attachments.Sum(a => a.FileSize) * 0.000001; + // Same TryParse-with-fallback as AttachmentController's own total-size check: a missing, + // empty, malformed, or non-positive config value falls back to 25 rather than throwing — + // double.Parse would otherwise fail every row in the batch on a bad config value alone. + if (!double.TryParse(configuration["S3:EmailAttachmentsTotalMaxFileSize"], out double maxAttachmentMb) || maxAttachmentMb <= 0) + { + maxAttachmentMb = 25; + } + if (totalAttachmentMb > maxAttachmentMb) + { + throw new UserFriendlyException($"The total size of all attachments ({totalAttachmentMb:F2} MB) exceeds the maximum allowed {maxAttachmentMb} MB. Please remove one or more attachments before proceeding."); + } + + await emailAppService.SendAsync(new CreateEmailDto + { + EmailId = draft.Id, + ApplicationId = draft.ApplicationId, + EmailTo = draft.ToAddress, + EmailFrom = draft.FromAddress, + EmailSubject = draft.Subject, + EmailBody = draft.Body, + EmailCC = draft.CC, + EmailBCC = draft.BCC, + EmailTemplateName = draft.TemplateName, + SendOnDateTime = draft.SendOnDateTime + }); + + bulkEmailResult.Successes.Add(applicationToEmail.ReferenceNo); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error sending bulk email notification for application with ID: {ApplicationId} and ReferenceNo: {ReferenceNo}", + applicationToEmail.ApplicationId, + applicationToEmail.ReferenceNo); + + bulkEmailResult.Failures.Add(new KeyValuePair(applicationToEmail.ReferenceNo, ex.Message)); + } + } + + return bulkEmailResult; + } + + /// + /// Map the application to a BulkEmailNotificationDto with validation messages based on its draft emails + /// + /// + /// + /// + private static BulkEmailNotificationDto MapBulkEmailNotification(Application application, List drafts, Dictionary draftAuthorsById) + { + var validationMessages = new List(); + Guid? emailId = null; + string? emailSubject = null; + string? createdByName = null; + DateTime? lastModified = null; + + if (drafts.Count == 0) + { + validationMessages.Add("NO_DRAFT_FOUND"); + } + else if (drafts.Count > 1) + { + validationMessages.Add("MULTIPLE_DRAFTS_FOUND"); + } + else + { + var draft = drafts[0]; + emailId = draft.Id; + emailSubject = draft.Subject; + lastModified = draft.LastModificationTime ?? draft.CreationTime; + + if (draft.CreatorId.HasValue && draftAuthorsById.TryGetValue(draft.CreatorId.Value, out var author)) + { + createdByName = $"{author.Name} {author.Surname}".Trim(); + } + + if (string.IsNullOrWhiteSpace(draft.Subject)) + { + validationMessages.Add("MISSING_SUBJECT"); + } + if (draft.ToAddress.ParseEmailList() is not { Count: > 0 }) + { + validationMessages.Add("MISSING_TO_ADDRESS"); + } + if (string.IsNullOrWhiteSpace(draft.FromAddress)) + { + validationMessages.Add("MISSING_FROM_ADDRESS"); + } + if (string.IsNullOrWhiteSpace(draft.Body)) + { + validationMessages.Add("MISSING_BODY"); + } + } + + return new BulkEmailNotificationDto() + { + ApplicationId = application.Id, + EmailId = emailId, + EmailSubject = emailSubject, + ReferenceNo = application.ReferenceNo, + ApplicantName = application.Applicant?.ApplicantName ?? string.Empty, + ApplicationStatus = application.ApplicationStatus.InternalStatus, + FormName = application.ApplicationForm?.ApplicationFormName ?? string.Empty, + ApprovedAmount = application.ApprovedAmount, + DecisionDate = application.FinalDecisionDate, + CreatedByName = createdByName, + LastModified = lastModified, + ValidationMessages = validationMessages, + IsValid = validationMessages.Count == 0 + }; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json index 4469751493..d4d82c3b51 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain.Shared/Localization/GrantManager/en.json @@ -91,6 +91,7 @@ "ApplicationList:StartAssessmentButton": "Start Assessment", "ApplicationList:CompleteAssessmentButton": "Complete Assessment", "ApplicationList:TagButton": "Tags", + "ApplicationList:SendEmailButton": "Send Drafted Email", "ApplicationList:ResyncSubmissionAttachmentsButton": "Resync", "ApplicationList:ManageTagButton": "Manage Tags", @@ -557,6 +558,17 @@ "ApplicationBatchApprovalRequest:InvalidApprovedAmount": "Invalid Approved Amount, it must be greater than 0.00", "ApplicationBatchApprovalRequest:InvalidRecommendedAmount": "Invalid Recommended Amount, it must be greater than 0.00", + "SendEmailNotificationRequest:Title": "Send Email Notification", + "SendEmailNotificationRequest:SubmitButtonText": "Send", + "SendEmailNotificationRequest:CancelButtonText": "Cancel", + "SendEmailNotificationRequest:MaxCountExceeded": "You have exceeded the maximum number of items for bulk email. Please reduce the number to {0} or fewer", + "SendEmailNotificationRequest:NoDraftFound": "No draft email found for this application. Please create a draft email before proceeding.", + "SendEmailNotificationRequest:MultipleDraftsFound": "Multiple draft emails found for this application. Please retain only one draft before proceeding.", + "SendEmailNotificationRequest:MissingSubject": "Draft email is missing a Subject. Please update the draft before proceeding.", + "SendEmailNotificationRequest:MissingToAddress": "Draft email is missing a To address. Please update the draft before proceeding.", + "SendEmailNotificationRequest:MissingFromAddress": "Draft email is missing a From address. Please update the draft before proceeding.", + "SendEmailNotificationRequest:MissingBody": "Draft email is missing a Body. Please update the draft before proceeding.", + "ApplicationBatchPublishRequest:MaxCountExceeded": "You have exceeded the maximum number of items for bulk status publishing. Please reduce the number to {0} or fewer", "ApplicationBatchPublishRequest:MinCountExceeded": "You have no items selected for bulk status publishing. Please close this prompt to continue", "ApplicationBatchPublishRequest:ConfirmationNote": "By confirming, the selected application statuses will be published and made visible to applicants in the portal.", @@ -642,7 +654,10 @@ "WrongTenantError:Title": "An Error Occurred", "WrongTenantError:ApplicationTenant": "This application is in Tenant: {0}", + "WrongTenantError:ApplicantTenant": "This applicant is in Tenant: {0}", "WrongTenantError:CurrentTenant": "You are currently in Tenant: {0}", - "WrongTenantError:Instructions": "Please click on the Profile menu at the top right of the corner, click on Switch Grant Programs, and select the correct Tenant before proceeding to view the link." + "WrongTenantError:Instructions": "Please click on the Profile menu at the top right of the corner, click on Switch Grant Programs, and select the correct Tenant before proceeding to view the link.", + "WrongTenantError:ApplicantNotFoundPossibleTenant": "This applicant was not found in the current Tenant and may exist in another Tenant.", + "WrongTenantError:ApplicationNotFoundPossibleTenant": "This application was not found in the current Tenant and may exist in another Tenant." } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs index 8a83f33587..a839abf533 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Domain/Permissions/PermissionGrantsDataSeeder.cs @@ -70,6 +70,7 @@ public PermissionGrantsDataSeeder(IPermissionDataSeeder permissionDataSeeder) public readonly List Notifications_CommonPermissions = [ NotificationsPermissions.Email.Default, NotificationsPermissions.Email.Send, + NotificationsPermissions.Email.SendBulk, NotificationsPermissions.Email.DeleteDraft ]; diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs index c130431e85..ed4a513a2f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/GrantManagerWebModule.cs @@ -481,6 +481,8 @@ private void ConfigureBundles() { Configure(options => { + options.Mode = BundlingMode.BundleAndMinify; + options.MinificationIgnoredFiles.Add("/js/notifications-realtime-client.js"); options .StyleBundles .Configure(UnityThemeUX2Bundles.Styles.Global, bundle => @@ -684,7 +686,6 @@ public override void OnApplicationInitialization(ApplicationInitializationContex app.UseStaticFiles(); app.UseMiddleware(); app.UseMiddleware(); - app.UseMiddleware(); app.UseRouting(); app.UseHttpMetrics(); app.UseAuthentication(); @@ -697,6 +698,7 @@ public override void OnApplicationInitialization(ApplicationInitializationContex app.UseUnitOfWork(); app.UseDynamicClaims(); app.UseAuthorization(); + app.UseMiddleware(); if (IsProfilingAllowed(env, configuration)) { app.UseMiniProfiler(); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ErrorCountingLoggerSink.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ErrorCountingLoggerSink.cs index cd9c75e401..8ebf663cd6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ErrorCountingLoggerSink.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Middleware/ErrorCountingLoggerSink.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Unity.GrantManager.Logs; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Users; using Volo.Abp.Uow; namespace Unity.GrantManager.Web.Middleware; @@ -73,77 +75,99 @@ public void Emit(LogEvent logEvent) _persistenceInFlight = true; } - _ = Task.Run(async () => + Guid? tenantId = null; + Guid? userId = null; + string? userName = null; + + try + { + using var metadataScope = scopeFactory.CreateScope(); + var currentTenant = metadataScope.ServiceProvider.GetRequiredService(); + var currentUser = metadataScope.ServiceProvider.GetRequiredService(); + tenantId = currentTenant.Id; + userId = currentUser.Id; + userName = AbpUserTenantAccessor.GetCurrentUserName(metadataScope.ServiceProvider); + } + catch { - IsPersistingExceptionLog.Value = true; + // Persistence is best-effort; continue with host/unknown metadata. + } - try + // Do not inherit the request's ambient ABP unit of work. The request may be + // disposing its DbContext while this fire-and-forget persistence is running. + using (ExecutionContext.SuppressFlow()) + { + _ = Task.Run(async () => { - await using var scope = scopeFactory.CreateAsyncScope(); - var exceptionLogs = scope.ServiceProvider.GetService(); + IsPersistingExceptionLog.Value = true; - if (exceptionLogs == null) + try { - return; + await using var scope = scopeFactory.CreateAsyncScope(); + var exceptionLogs = scope.ServiceProvider.GetService(); + + if (exceptionLogs == null) + { + return; + } + + using (scope.ServiceProvider.GetRequiredService().Change(tenantId)) + { + var frame = logEvent.Exception == null + ? null + : ExceptionNotificationHelpers.GetTopFrame(logEvent.Exception); + string? sourceFile = frame?.File == null + ? null + : ExceptionNotificationHelpers.NormalizeRepoPath(frame.Value.File); + + // A fresh unit of work owns the context used by the background write. + using var uow = scope.ServiceProvider.GetRequiredService() + .Begin(requiresNew: true, isTransactional: false); + + await exceptionLogs.CreateAsync(new CreateExceptionLogDto + { + UserId = userId, + UserName = userName, + TenantName = await AbpUserTenantAccessor.GetCurrentTenantNameAsync(scope.ServiceProvider), + NotificationType = logEvent.Exception == null + ? ExceptionLogType.PrometheusErrorCounterEvent + : ExceptionLogType.PrometheusExceptionCounterEvent, + Channel = ExceptionLogChannel.Prometheus, + Severity = logEvent.Level >= LogEventLevel.Fatal + ? ExceptionLogSeverity.Critical + : ExceptionLogSeverity.Error, + Title = "Prometheus Error Counter Event", + Message = logEvent.RenderMessage(), + Source = nameof(ErrorCountingLoggerSink), + IsDeliveredRealtime = false, + ExceptionType = logEvent.Exception?.GetType().FullName, + ExceptionMessage = logEvent.Exception?.Message, + StackExcerpt = logEvent.Exception?.StackTrace, + SourceFile = sourceFile, + SourceLine = frame?.Line, + Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") + }); + + await uow.CompleteAsync(); + } } - - var frame = logEvent.Exception == null - ? null - : ExceptionNotificationHelpers.GetTopFrame(logEvent.Exception); - string? sourceFile = frame?.File == null - ? null - : ExceptionNotificationHelpers.NormalizeRepoPath(frame.Value.File); - - // Isolate this from whatever ambient unit of work/DbContext happens to be flowing - // through the captured ExecutionContext (e.g. the request that triggered this log - // event may still be mid-operation on its own DbContext) - without requiresNew, - // ABP's implicit [UnitOfWork] on CreateAsync would join that ambient one instead of - // getting its own, causing "a second operation was started on this context instance". - using var uow = scope.ServiceProvider.GetRequiredService() - .Begin(requiresNew: true, isTransactional: false); - - await exceptionLogs.CreateAsync(new CreateExceptionLogDto - { - UserId = AbpUserTenantAccessor.GetCurrentUserId(scope.ServiceProvider), - UserName = AbpUserTenantAccessor.GetCurrentUserName(scope.ServiceProvider), - TenantName = await AbpUserTenantAccessor.GetCurrentTenantNameAsync(scope.ServiceProvider), - NotificationType = logEvent.Exception == null - ? ExceptionLogType.PrometheusErrorCounterEvent - : ExceptionLogType.PrometheusExceptionCounterEvent, - Channel = ExceptionLogChannel.Prometheus, - Severity = logEvent.Level >= LogEventLevel.Fatal - ? ExceptionLogSeverity.Critical - : ExceptionLogSeverity.Error, - Title = "Prometheus Error Counter Event", - Message = logEvent.RenderMessage(), - Source = nameof(ErrorCountingLoggerSink), - IsDeliveredRealtime = false, - ExceptionType = logEvent.Exception?.GetType().FullName, - ExceptionMessage = logEvent.Exception?.Message, - StackExcerpt = logEvent.Exception?.StackTrace, - SourceFile = sourceFile, - SourceLine = frame?.Line, - Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") - }); - - await uow.CompleteAsync(); - } - catch - { - lock (_persistenceGate) + catch { - _persistenceDisabledUntil = DateTimeOffset.UtcNow.Add(PersistenceBackoff); + lock (_persistenceGate) + { + _persistenceDisabledUntil = DateTimeOffset.UtcNow.Add(PersistenceBackoff); + } } - } - finally - { - IsPersistingExceptionLog.Value = false; - - lock (_persistenceGate) + finally { - _persistenceInFlight = false; + IsPersistingExceptionLog.Value = false; + + lock (_persistenceGate) + { + _persistenceInFlight = false; + } } - } - }); + }); + } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml.cs index c3e28bf1d6..9dd0b510fc 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Applicants/Details.cshtml.cs @@ -60,6 +60,7 @@ public async Task OnGetAsync() return RedirectToPage("/Error", new { httpStatusCode = 409, + entityType = "Applicant", applicationTenantName = applicationTenant?.Name ?? TenantId.Value.ToString(), currentTenantName = CurrentTenant.Name ?? "Host" }); @@ -75,7 +76,12 @@ public async Task OnGetAsync() } catch (Exception) { - return NotFound(); + return RedirectToPage("/Error", new + { + httpStatusCode = 404, + entityType = "Applicant", + currentTenantName = CurrentTenant.Name ?? "Host" + }); } } @@ -105,7 +111,12 @@ public async Task OnGetAsync() } catch (Exception) { - return NotFound(); + return RedirectToPage("/Error", new + { + httpStatusCode = 404, + entityType = "Applicant", + currentTenantName = CurrentTenant.Name ?? "Host" + }); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinks.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinks.js index 1ecdc59e1d..7425fdd0d3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinks.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/ApplicationLinks/ApplicationLinks.js @@ -88,7 +88,7 @@ $(function () { // Make sure input string have no error with the plugin LinksInput.prototype.anyErrors = function (string) { - if (!this.options.duplicate && this.arr.indexOf(string) != -1) { + if (!this.options.duplicate && this.arr.includes(string)) { console.log('duplicate found " ' + string + ' " ') return true; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml new file mode 100644 index 0000000000..bb12b65fbb --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml @@ -0,0 +1,164 @@ +@page +@model Unity.GrantManager.Web.Pages.BulkEmailNotifications.SendEmailNotificationModalModel +@using Microsoft.Extensions.Localization +@using Unity.GrantManager.Localization + +@inject IStringLocalizer L + +@{ + Layout = null; +} + + + + + @if (ViewData["Error"] != null) + { + + } + else + { +
+
+ @* This form wraps only the left panel — deliberately not the whole modal. The right panel + injects EmailsWidget dynamically, which renders its own
; nesting + that inside this form would be invalid HTML with unreliable browser parsing behavior. + The bottom Send button lives in the modal footer, outside this form, and submits it via + the form="bulkEmailNotificationForm" attribute instead of DOM nesting. *@ + + + + + + + @for (var i = 0; i < Model.BulkEmailNotifications?.Count; i++) + { +
+ + +
+
@Model.BulkEmailNotifications[i].ReferenceNo
+
@Model.BulkEmailNotifications[i].ApplicantName
+
@string.Format("({0})", @Model.BulkEmailNotifications[i].FormName)
+
@Model.BulkEmailNotifications[i].ApplicationStatus
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + @for (var j = 0; j < Model.BulkEmailNotifications[i].Notes?.Count; j++) + { + + + @if (Model.BulkEmailNotifications[i].Notes[j].IsError) + { + Error + } + else + { + Note + } + @Model.BulkEmailNotifications[i].Notes[j].Description + + + } + +
+ } +
+
+ +
+ Error @Model.MaxBatchCountExceededError +
+
+ + @* WAI-ARIA "Window Splitter" pattern (draggable/keyboard-resizable, not a decorative break) — role="separator" + tabindex="0" are intentional here, not an
. *@ + + +
+
+ Select an application to view its draft email. +
+ + + +
+
+ } +
+ + + + + + + +
+ + diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs new file mode 100644 index 0000000000..00a2dbde7e --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.cshtml.cs @@ -0,0 +1,200 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.Applications; +using Unity.GrantManager.GrantApplications; +using Unity.GrantManager.Web.Pages.BulkEmailNotifications.ViewModels; +using Unity.Modules.Shared.Utils; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.GrantManager.Web.Pages.BulkEmailNotifications; + +public class SendEmailNotificationModalModel(IBulkEmailNotificationAppService bulkEmailNotificationAppService, + ApplicationIdsCacheService cacheService) : AbpPageModel +{ + [BindProperty] + public List? BulkEmailNotifications { get; set; } + + [TempData] + public int ApplicationsCount { get; set; } + + [TempData] + public bool Invalid { get; set; } + + [TempData] + public int MaxBatchCount { get; set; } + + [TempData] + public string? MaxBatchCountExceededError { get; set; } + + [TempData] + public bool MaxBatchCountExceeded { get; set; } + + public async Task OnGetAsync(string cacheKey) + { + MaxBatchCount = BatchApprovalConsts.MaxBatchCount; + BulkEmailNotifications = []; + MaxBatchCountExceededError = L["SendEmailNotificationRequest:MaxCountExceeded", BatchApprovalConsts.MaxBatchCount.ToString()].Value; + + try + { + // Retrieve application IDs from distributed cache + var selectedApplicationIds = await cacheService.GetApplicationIdsAsync(cacheKey); + + if (selectedApplicationIds == null || selectedApplicationIds.Count == 0) + { + Logger.LogWarning("Cache key expired or invalid: {CacheKey}", cacheKey.SanitizeField()); + ViewData["Error"] = "The session has expired. Please try selecting applications again."; + Invalid = true; + return; + } + + Guid[] applicationGuids = selectedApplicationIds.ToArray(); + + // Clean up cache after retrieval (one-time use) + await cacheService.RemoveAsync(cacheKey); + + if (!ValidCount(applicationGuids)) + { + MaxBatchCountExceeded = true; + } + + if (applicationGuids.Length == 0) + { + return; + } + + var applications = await bulkEmailNotificationAppService.GetApplicationsForBulkEmail(applicationGuids); + + foreach (var application in applications) + { + var bulkEmailNotification = new BulkEmailNotificationViewModel + { + ApplicationId = application.ApplicationId, + EmailId = application.EmailId, + ReferenceNo = application.ReferenceNo, + ApplicantName = application.ApplicantName, + EmailSubject = application.EmailSubject, + ApplicationStatus = application.ApplicationStatus, + FormName = application.FormName, + ApprovedAmount = application.ApprovedAmount, + DecisionDate = application.DecisionDate, + CreatedByName = application.CreatedByName, + LastModified = application.LastModified, + IsValid = application.IsValid + }; + + SetNotes(application, bulkEmailNotification); + BulkEmailNotifications.Add(bulkEmailNotification); + } + + Invalid = applications.Exists(s => !s.IsValid) || MaxBatchCountExceeded; + ApplicationsCount = applications.Count; + + Logger.LogInformation("Successfully loaded bulk email modal for {Count} applications", selectedApplicationIds.Count); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error loading bulk email modal"); + ViewData["Error"] = "An error occurred while loading the email form. Please try again."; + Invalid = true; + } + } + + private void SetNotes(BulkEmailNotificationDto application, BulkEmailNotificationViewModel bulkEmailNotification) + { + List notes = EmailNotificationNoteViewModel.CreateNotesList(localizer: L); + + foreach (var validation in application.ValidationMessages) + { + var index = notes.FindIndex(note => note.Key == validation); + if (index != -1) + { + notes[index] = new EmailNotificationNoteViewModel(validation, true, notes[index].Description, notes[index].IsError); + } + } + + bulkEmailNotification.Notes = notes; + } + + /// + /// Re-validate a single application's draft state (called after a Save in the right-hand edit panel) + /// so the row's validity/notes/Created By/Last Modified can be refreshed without reloading the whole batch. + /// + public async Task OnGetRevalidateAsync(Guid applicationId) + { + try + { + var application = await bulkEmailNotificationAppService.RevalidateApplicationForBulkEmail(applicationId); + + var viewModel = new BulkEmailNotificationViewModel { ApplicationId = application.ApplicationId }; + SetNotes(application, viewModel); + + return new OkObjectResult(new + { + applicationId = application.ApplicationId, + emailId = application.EmailId, + isValid = application.IsValid, + createdByName = application.CreatedByName, + lastModified = application.LastModified, + notes = viewModel.Notes + }); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error revalidating application {ApplicationId} for bulk email", applicationId); + // A 2xx status (including 204 No Content) is treated as success by the client's $.ajax().done() + // handler, which immediately dereferences fields on the response body — an empty success response + // would throw client-side. Return a genuine error status so it lands in .fail() instead. + return StatusCode(StatusCodes.Status500InternalServerError); + } + } + + public async Task OnPostAsync() + { + try + { + if (BulkEmailNotifications == null) return NoContent(); + + var emailRequests = MapBulkEmailRequests(); + + var result = await bulkEmailNotificationAppService.SendBulkEmailNotifications(emailRequests); + + return new OkObjectResult(result); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error sending bulk email notifications"); + } + + return NoContent(); + } + + private List MapBulkEmailRequests() + { + var bulkEmailNotifications = new List(); + + foreach (var application in BulkEmailNotifications ?? []) + { + bulkEmailNotifications.Add(new BulkEmailNotificationDto() + { + ApplicationId = application.ApplicationId, + EmailId = application.EmailId, + ReferenceNo = application.ReferenceNo, + ApplicantName = application.ApplicantName ?? string.Empty, + ValidationMessages = [] + }); + } + + return bulkEmailNotifications; + } + + private static bool ValidCount(Guid[] applicationGuids) + { + return applicationGuids.Length <= BatchApprovalConsts.MaxBatchCount; + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css new file mode 100644 index 0000000000..b9cd2bc8fb --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.css @@ -0,0 +1,126 @@ +/* Row/summary styling (.batch-approval-container, .approval-details-header, .approval-note-prefix, etc.) + is shared with the Approve Applications modal via ApproveApplicationsModal.css, loaded in the same bundle. */ + +/* Wider Submission # column for the send-email summary table: the shared .col-20/.col-75 split from + ApproveApplicationsModal.css is too narrow for this table's smaller (default-size) modal. */ +.custom-table .col-35 { + width: 35%; +} + +.custom-table .col-60 { + width: 60%; +} + +/* ---- Two-column layout: compressed application list on the left, draft editor on the right ---- */ + +#sendEmailNotificationModal .modal-dialog { + max-width: 95vw; +} + +.bulk-email-modal-body { + display: flex; + align-items: stretch; +} + +.bulk-email-list-panel { + flex: 0 0 40%; + min-width: 0; +} + +.bulk-email-list-panel .batch-approval-card { + max-height: 75vh; +} + +.bulk-email-edit-panel { + flex: 1 1 60%; + min-width: 0; + max-height: 75vh; + overflow-y: auto; + padding-left: 1rem; +} + +/* Draggable divider between the two panels. The border that used to sit on .bulk-email-edit-panel now lives + here instead, since this element is the visual (and interactive) separator between them. */ +.bulk-email-panel-divider { + flex: 0 0 auto; + width: 13px; + margin: 0 -6px; + display: flex; + align-items: center; + justify-content: center; + cursor: col-resize; + position: relative; + z-index: 1; +} + +.bulk-email-panel-divider::before { + content: ''; + width: var(--bs-border-width, 1px); + height: 100%; + background-color: var(--bs-border-color); + transition: background-color 0.15s ease, width 0.15s ease; +} + +.bulk-email-panel-divider:hover::before, +.bulk-email-panel-divider.dragging::before, +.bulk-email-panel-divider:focus-visible::before { + width: 3px; + background-color: var(--bc-colors-blue-text-links); +} + +.bulk-email-panel-divider:focus-visible { + outline: none; +} + +/* Applied to for the duration of a drag so text selection and the resize cursor stay consistent even + if the pointer briefly moves outside the divider itself while dragging. */ +body.bulk-email-resizing { + cursor: col-resize; + user-select: none; +} + +/* Compressed row spacing for the left list, so it reads as a compact selectable index rather than + the full-width row layout used by the Approve Applications modal. */ +.bulk-email-row { + cursor: pointer; +} + +.bulk-email-row.row-selected { + border-color: var(--bc-colors-blue-text-links); + border-width: 2px; + background-color: var(--bc-colors-blue-light, #f0f6ff); +} + +.bulk-email-details-row { + padding: 0.25rem 0.5rem !important; +} + +.bulk-email-details-row abp-input, +.bulk-email-details-row .form-group { + margin-bottom: 0.25rem; +} + +/* ---- Right-panel empty/error/loading placeholders ---- */ + +.bulk-email-edit-empty, +.bulk-email-edit-loading { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + min-height: 20rem; + text-align: center; + color: var(--bc-type-color-secondary); + padding: 2rem; +} + +/* ---- EmailsWidget reuse: hide actions that don't apply inside the bulk-send edit panel. + Sending happens only via the bottom "Send" button once the whole batch is valid; Save/Discard + stay available so the user can fix and persist a draft in place. ---- */ +.bulk-email-edit-panel #btn-send-top, +.bulk-email-edit-panel #btn-send-dropdown, +.bulk-email-edit-panel #btn-new-email, +.bulk-email-edit-panel #btn-send-close-top, +.bulk-email-edit-panel #email-alert-readonly { + display: none !important; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.js new file mode 100644 index 0000000000..cf18b91635 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationModal.js @@ -0,0 +1,561 @@ +let selectedApplicationId = null; +let panelRequestId = 0; +let panelRefreshXhr = null; +let bulkEmailSendConfirmed = false; + +const EMAILS_WIDGET_REFRESH_URL = '/GrantApplications/Widgets/Emails/RefreshEmails'; + +// Bumps the version counter every async step in the right-panel load flow is checked against, and aborts +// whatever widget-refresh request is currently in flight. Called from every place that changes what the panel +// should show without necessarily going through loadDraftIntoPanel itself (the no-draft/error branch, resetting +// the panel, modal close) — loadDraftIntoPanel bumps/aborts on its own, but those other paths don't touch it at +// all otherwise, which would leave a stale in-flight request free to land later with nothing to stop it. +function invalidatePendingPanelRequest() { + panelRequestId++; + if (panelRefreshXhr) { + panelRefreshXhr.abort(); + panelRefreshXhr = null; + } +} + +function removeApplicationEmail(containerId) { + const $row = $('#' + containerId); + const referenceNo = $row.find('input[name$=".ReferenceNo"]').val(); + + abp.message.confirm( + referenceNo + ? 'Remove application ' + referenceNo + ' from this batch?' + : 'Remove this application from this batch?', + 'Remove Application', + function (confirmed) { + if (!confirmed) { + return; + } + + if (selectedApplicationId && containerId === selectedApplicationId + '_container') { + resetEditPanel(); + } + + $row.remove(); + let applicationsCount = $('#ApplicationsCount').val(); + $('#ApplicationsCount').val(applicationsCount - 1); + runValidations(); + } + ); +} + +function runValidations() { + let isValid = true; + let itemCount = 0; + + $('#bulkEmailNotificationForm input[name="BulkEmailNotifications.Index"]').each(function () { + itemCount++; + let index = $(this).val(); + let isValidField = $('#bulkEmailNotificationForm input[name="BulkEmailNotifications[' + index + '].IsValid"]').val(); + + if (isValidField.toLowerCase() !== 'true') { + isValid = false; + } + }); + + if (itemCount === 0) { + isValid = false; + } + + if (!validBatchCount()) { + isValid = false; + setMaxCountError(true); + } else { + setMaxCountError(false); + } + + if (isValid) { + enableBulkEmailSubmit(); + } else { + disableBulkEmailSubmit(); + } +} + +function setMaxCountError(visible) { + const summary = $('#batch-approval-summary'); + if (visible) { + summary.css('display', 'block'); + } else { + summary.css('display', 'none'); + } +} + +function validBatchCount() { + // .val() returns strings; comparing them with <= directly does a lexicographic (character-by-character) + // comparison, not a numeric one — e.g. "6" <= "50" is false, because '6' sorts after '5'. Parse both sides + // to numbers first so counts like 6-9 (and 60-69, 70-79, ...) aren't misreported as exceeding the max. + let applicationsCount = Number.parseInt($('#ApplicationsCount').val(), 10); + let maxBatchCount = Number.parseInt($('#MaxBatchCount').val(), 10); + return applicationsCount <= maxBatchCount; +} + +function enableBulkEmailSubmit() { + $("#sendEmailNotificationModal") + .find('#btnSubmitBulkEmail').prop("disabled", false); +} + +function disableBulkEmailSubmit() { + $("#sendEmailNotificationModal") + .find('#btnSubmitBulkEmail').prop("disabled", true); +} + +function closeEmailNotifications() { + $('#sendEmailNotificationModal').modal('hide'); +} + +// ---- Right-panel draft edit/select behavior ---- + +function selectApplicationRow(applicationId, event) { + if (event) { + event.stopPropagation(); + } + + if (selectedApplicationId === applicationId) { + return; + } + + if (hasUnsavedEditorChanges()) { + abp.message.confirm( + 'You have unsaved changes in the current draft. Switching applications will discard them. Continue?', + 'Unsaved Changes', + function (confirmed) { + if (confirmed) { + proceedWithRowSelection(applicationId); + } + } + ); + return; + } + + proceedWithRowSelection(applicationId); +} + +function hasUnsavedEditorChanges() { + // showErrorState()/showEmptyState()/showLoadingState() only hide #bulkEmailEditPanelWidget via CSS — they + // don't clear its content. If the user switches away from a dirty draft to a row with no valid draft (or + // an empty selection), the previous row's #EmailForm is still sitting in the DOM, hidden but with its + // Save button still enabled. Without the visibility check, that stale, no-longer-shown draft would keep + // reporting as "dirty" on every subsequent row switch and on the bottom Send button, even though there's + // nothing left on screen for the user to have unsaved changes in. + const $panel = $('#bulkEmailEditPanelWidget'); + if (!$panel.is(':visible')) { + return false; + } + + const $saveBtn = $panel.find('#btn-save-top'); + return $saveBtn.length > 0 && !$saveBtn.prop('disabled'); +} + +function proceedWithRowSelection(applicationId) { + selectedApplicationId = applicationId; + + $('.bulk-email-row').removeClass('row-selected'); + const $row = $('#' + applicationId + '_container'); + $row.addClass('row-selected'); + + showCorrectStateForSelection(); +} + +// Re-derives and shows whatever the right panel should currently display for selectedApplicationId, from the +// left panel's own row data (never touched by widget refreshes). +function showCorrectStateForSelection() { + const $row = $('#' + selectedApplicationId + '_container'); + const emailId = $row.find('input[name$=".EmailId"]').val(); + + if (!emailId) { + // Not going through loadDraftIntoPanel here, so it won't invalidate whatever request a *previous* + // selection left in flight on its own — do it explicitly, or a stale response for that previous + // application could still land on top of this row's error state once it resolves. + invalidatePendingPanelRequest(); + showErrorState(getRowErrorHtml($row)); + return; + } + + loadDraftIntoPanel(selectedApplicationId, emailId); +} + +function getRowErrorHtml($row) { + const $errorNote = $row.find('.bulk-approval-notes-column').filter(function () { + return $(this).css('display') !== 'none'; + }).first(); + + return $errorNote.length ? $errorNote.html() : 'This application has no editable draft.'; +} + +function resetEditPanel() { + selectedApplicationId = null; + invalidatePendingPanelRequest(); + showEmptyState(); +} + +function showEmptyState() { + $('#bulkEmailEditPanelEmpty').show(); + $('#bulkEmailEditPanelError').hide(); + $('#bulkEmailEditPanelLoading').hide(); + $('#bulkEmailEditPanelWidget').hide(); +} + +function showErrorState(html) { + $('#bulkEmailEditPanelEmpty').hide(); + $('#bulkEmailEditPanelError').html(html).show(); + $('#bulkEmailEditPanelLoading').hide(); + $('#bulkEmailEditPanelWidget').hide(); +} + +function showLoadingState() { + $('#bulkEmailEditPanelEmpty').hide(); + $('#bulkEmailEditPanelError').hide(); + $('#bulkEmailEditPanelLoading').show(); + $('#bulkEmailEditPanelWidget').hide(); +} + +function showWidgetState() { + $('#bulkEmailEditPanelEmpty').hide(); + $('#bulkEmailEditPanelError').hide(); + $('#bulkEmailEditPanelLoading').hide(); + $('#bulkEmailEditPanelWidget').show(); +} + +function loadDraftIntoPanel(applicationId, emailId) { + const requestId = ++panelRequestId; + + // Deliberately not using abp.WidgetManager for this: it shares one wrapper across every row selection, and + // its internal completion handling inserts whatever HTML a completed request returns into + // #bulkEmailEditPanelWidget unconditionally — there's no documented way to abort a previous in-flight + // request or gate the actual DOM write by a version/token. If row A is still refreshing when row B is + // selected and the user starts editing B's draft, A's response arriving late would silently overwrite the + // editor with A's blank markup — destroying whatever the user was typing, not just showing the wrong + // content (correcting it afterward can restore the right *saved* state, but it can't recover keystrokes + // that only ever existed in a DOM node that's already been replaced). + // + // Making the request ourselves gives an actual handle to abort the previous one. An aborted request's + // .done() is guaranteed by jQuery to never fire, so a stale response can't reach the DOM at all — this + // removes the race by construction instead of detecting and correcting it after the fact. The requestId + // check below covers the second leg of this flow (loadSelectedDraftData's own separate AJAX call), which + // has no abortable handle of its own — a version counter checked on arrival works there regardless. + if (panelRefreshXhr) { + panelRefreshXhr.abort(); + panelRefreshXhr = null; + } + + showLoadingState(); + + // EmailsWidget/Default.js reads its owning application from this ambient hidden field (normally + // rendered once by GrantApplications/Details.cshtml for a single fixed application). ActionBar/Default.cshtml + // renders an empty placeholder for it on page load; update it here before every refresh so the widget + // picks up whichever application is currently selected in this modal. + $('#DetailsViewApplicationId').val(applicationId); + + const currentUserId = decodeURIComponent($('#CurrentUserId').val()); + + panelRefreshXhr = $.ajax({ + url: EMAILS_WIDGET_REFRESH_URL, + type: 'GET', + data: { applicationId: applicationId, currentUserId: currentUserId } + }).done(function (html) { + panelRefreshXhr = null; + if (requestId !== panelRequestId) { + // Superseded by a newer row selection since this request was made — its own load already owns + // the panel now (or will shortly); this response is stale and must not touch the DOM. + return; + } + + $('#bulkEmailEditPanelWidget').html(html); + + // jquery-validation-unobtrusive only converts data-val-* attributes into jQuery Validate rules once, + // automatically, against whatever is already in the DOM when $(document).ready() fires. This markup + // arrives later over AJAX, so without re-parsing it here, EmailsWidget's own $(emailForm).valid() call + // (used for Subject/From/Body) would silently pass no matter what those fields contain — only the + // hand-validated "To" field would still be enforced client-side. + if ($.validator?.unobtrusive) { + $.validator.unobtrusive.parse('#bulkEmailEditPanelWidget'); + } + + // EmailsWidget/Default.js normally sets itself up exactly once, at page load, against markup that's + // already server-rendered on the page. Its edit fields/buttons/attachment handlers only just got + // inserted into the DOM by the refresh above, so its setup needs to run again against them — + // window.EmailsWidget.reinitialize() (added to Default.js for this reuse) does that. + window.EmailsWidget.reinitialize(); + loadSelectedDraftData(applicationId, emailId, requestId); + }).fail(function (jqXHR) { + panelRefreshXhr = null; + + if (jqXHR.statusText === 'abort' || requestId !== panelRequestId) { + // Either superseded (aborted by us) or, if it somehow still fired, superseded by a newer selection + // — either way that newer request owns what the panel shows next, not this failure. + return; + } + + console.warn('Failed to load draft editor for application:', applicationId, jqXHR); + showErrorState('Failed to load the draft email. Please try again.'); + }); +} + +function loadSelectedDraftData(applicationId, emailId, requestId) { + unity.notifications.emailNotifications.emailNotification.getHistoryByApplicationId(applicationId) + .then(function (history) { + if (requestId !== panelRequestId) { + // A newer row selection has started since this call began — that selection's own load is + // responsible for the panel now, so don't publish this stale draft's data over whatever the + // user may already be doing there. + return; + } + + const draft = (history || []).find(function (item) { return item.id === emailId; }); + + if (!draft) { + showErrorState('The selected draft could not be loaded. It may have changed — try re-selecting this application.'); + return; + } + + PubSub.publish('email_selected', draft); + showWidgetState(); + }) + .catch(function (e) { + if (requestId !== panelRequestId) { + return; + } + + console.warn('Failed to load draft email for bulk edit panel:', e); + showErrorState('Failed to load the draft email. Please try again.'); + }); +} + +function formatDateOnly(isoString) { + return isoString ? isoString.substring(0, 10) : ''; +} + +function applyRevalidationResult(applicationId, result) { + const $row = $('#' + applicationId + '_container'); + $row.find('input[name$=".IsValid"]').val(result.isValid ? 'true' : 'false'); + $row.find('input[name$=".EmailId"]').val(result.emailId || ''); + $row.find('input[name$=".CreatedByName"]').val(result.createdByName || ''); + $row.find('input[name$=".LastModified"]').val(formatDateOnly(result.lastModified)); + + (result.notes || []).forEach(function (note) { + $('#' + applicationId + '_container_' + note.key).css('display', note.active ? 'block' : 'none'); + }); + + runValidations(); +} + +function revalidateRow(applicationId) { + $.ajax({ + url: '/BulkEmailNotifications/SendEmailNotificationModal?handler=Revalidate', + type: 'GET', + data: { applicationId: applicationId } + }).done(function (result) { + // Defensive: a 2xx response with no/empty body (e.g. a proxy or future server-side change) would + // otherwise crash applyRevalidationResult on result.isValid. The server-side fix returns a real error + // status on failure, so this is a second line of defense, not the primary one. + if (!result) { + console.warn('Revalidation returned an empty response for application:', applicationId); + return; + } + + applyRevalidationResult(applicationId, result); + + // Only touch the visible panel if the user is still on the row that was actually just saved. Save + // disables #btn-save-top immediately (before its AJAX call even resolves), which is also what + // hasUnsavedEditorChanges() checks — so the user is free to switch to a different row while a save is + // still in flight, with no "unsaved changes" prompt in the way. If they did, reloading the panel here + // would yank away whatever they're now doing on the newly-selected row; the row that was actually + // saved still gets its IsValid/notes/Created By/Last Modified refreshed above regardless. + if (applicationId !== selectedApplicationId) { + return; + } + + const $row = $('#' + applicationId + '_container'); + const emailId = $row.find('input[name$=".EmailId"]').val(); + + if (emailId) { + loadDraftIntoPanel(applicationId, emailId); + } else { + resetEditPanel(); + } + }).fail(function (e) { + console.warn('Failed to revalidate application after save:', applicationId, e); + }); +} + +// ---- Draggable divider between the list and edit panels ---- + +const PANEL_DIVIDER_MIN_LIST_WIDTH = 280; +const PANEL_DIVIDER_MIN_EDIT_WIDTH = 400; +const PANEL_DIVIDER_KEYBOARD_STEP = 24; + +function getPanelDividerElements() { + return { + $container: $('.bulk-email-modal-body'), + $list: $('#bulkEmailListPanel'), + $divider: $('#bulkEmailPanelDivider') + }; +} + +// Sets the list panel to an explicit pixel width (clamped so neither panel can be dragged/keyed below a +// usable minimum — the edit panel in particular needs enough room for TinyMCE's toolbar to lay out sanely) +// and keeps the divider's aria-value* attributes in sync for screen reader users, whether or not they ever +// actually drag it. +function applyListPanelWidth(widthPx) { + const { $container, $list, $divider } = getPanelDividerElements(); + if (!$container.length || !$list.length) { + return; + } + + const containerWidth = $container.width(); + const dividerWidth = $divider.outerWidth() || 0; + const maxListWidth = Math.max(containerWidth - dividerWidth - PANEL_DIVIDER_MIN_EDIT_WIDTH, PANEL_DIVIDER_MIN_LIST_WIDTH); + const clampedWidth = Math.min(Math.max(widthPx, PANEL_DIVIDER_MIN_LIST_WIDTH), maxListWidth); + + $list.css('flex', '0 0 ' + clampedWidth + 'px'); + + $divider.attr({ + 'aria-valuenow': Math.round(clampedWidth), + 'aria-valuemin': PANEL_DIVIDER_MIN_LIST_WIDTH, + 'aria-valuemax': Math.round(maxListWidth) + }); +} + +function startPanelDividerDrag(event) { + event.preventDefault(); + + const { $list, $divider } = getPanelDividerElements(); + const startX = event.pageX; + const startWidth = $list.outerWidth(); + + $divider.addClass('dragging'); + $('body').addClass('bulk-email-resizing'); + + function onDragMove(moveEvent) { + applyListPanelWidth(startWidth + (moveEvent.pageX - startX)); + } + + function onDragEnd() { + $(document).off('mousemove.bulkEmailDivider', onDragMove); + $(document).off('mouseup.bulkEmailDivider', onDragEnd); + $divider.removeClass('dragging'); + $('body').removeClass('bulk-email-resizing'); + } + + $(document).on('mousemove.bulkEmailDivider', onDragMove); + $(document).on('mouseup.bulkEmailDivider', onDragEnd); +} + +function handlePanelDividerKeydown(event) { + if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') { + return; + } + + event.preventDefault(); + const { $list } = getPanelDividerElements(); + const step = event.key === 'ArrowLeft' ? -PANEL_DIVIDER_KEYBOARD_STEP : PANEL_DIVIDER_KEYBOARD_STEP; + applyListPanelWidth($list.outerWidth() + step); +} + +$(function () { + PubSub.subscribe('refresh_application_emails', function (_msg, data) { + // EmailsWidget publishes the applicationId that was actually saved/sent (see Default.js) — use that + // rather than assuming it's whatever row happens to be selected right now, since the user may have + // switched rows while the save was still in flight. Fall back to the current selection for any other + // publisher of this topic that doesn't pass one. + const savedApplicationId = data?.applicationId || selectedApplicationId; + if (!savedApplicationId) { + return; + } + revalidateRow(savedApplicationId); + }); + + // The bottom Send button only posts this modal's own form (ApplicationId/EmailId/IsValid hidden fields) — + // it has no idea what's currently typed into EmailsWidget's separate #EmailForm in the right panel, and the + // backend always sends whatever is last *saved* to the draft. Without this guard, unsaved edits would be + // silently dropped: the user would see "success" for content they never actually saved. + $(document).on('click', '#btnSubmitBulkEmail', function (event) { + if (bulkEmailSendConfirmed) { + bulkEmailSendConfirmed = false; + return; + } + + // EmailsWidget's own attachment-total-size check (checkTotalAttachmentSize in Default.js) only ever + // toggles a button inside its own markup, which this modal hides — it has no way to know about, and + // never disables, our own #btnSubmitBulkEmail. Reachable via applying a different email template + // while attachments already exist (copyTemplateAttachments bypasses the normal per-upload size gate). + // Unlike unsaved edits, there's no reasonable "send anyway" here — the total genuinely exceeds what's + // allowed — so this is a hard block, not a confirm-to-proceed prompt. is(':visible') is false whenever + // the panel itself is hidden (error/empty/loading state), so this can't fire for a stale, no-longer- + // shown row the same way hasUnsavedEditorChanges() could before its own visibility fix. + if ($('#email-attachment-size-error').is(':visible')) { + event.preventDefault(); + event.stopImmediatePropagation(); + abp.message.error( + 'The currently open draft has attachments exceeding the total size limit. Remove attachments before sending.', + 'Attachments Too Large' + ); + return; + } + + // Past this point the click is actually going to send the batch, so it always needs an explicit + // confirmation first — this button has no "are you sure" of its own otherwise, and an accidental click + // would send every drafted email in the list right away with nothing to stop it. + event.preventDefault(); + event.stopImmediatePropagation(); + + if (hasUnsavedEditorChanges()) { + abp.message.confirm( + 'The currently open draft has unsaved changes. They will not be included in this send unless you save first. Send anyway?', + 'Unsaved Changes', + function (confirmed) { + if (confirmed) { + bulkEmailSendConfirmed = true; + $('#btnSubmitBulkEmail').trigger('click'); + } + } + ); + return; + } + + abp.message.confirm( + 'Are you sure you want to send the drafted emails for all applications in this batch?', + 'Send Drafted Email', + function (confirmed) { + if (confirmed) { + bulkEmailSendConfirmed = true; + $('#btnSubmitBulkEmail').trigger('click'); + } + } + ); + }); + + // Delegated (rather than bound directly to #bulkEmailPanelDivider) because this script is bundled once at + // page load, while the divider itself is part of the modal's DOM, which ABP's ModalManager destroys and + // freshly re-renders on every open. + $(document).on('mousedown', '#bulkEmailPanelDivider', startPanelDividerDrag); + $(document).on('keydown', '#bulkEmailPanelDivider', handlePanelDividerKeydown); + + // Initializes the divider's aria-value* attributes against the panel's default CSS-defined width (40%), + // without changing anything visually — applyListPanelWidth just re-expresses the current rendered width + // as an explicit pixel flex-basis, which future drags/keypresses then adjust from. Needed so a keyboard + // user tabbing to the divider without ever dragging it still gets correct aria-valuenow/min/max. + $(document).on('shown.bs.modal', '#sendEmailNotificationModal', function () { + const $list = $('#bulkEmailListPanel'); + if ($list.length) { + applyListPanelWidth($list.outerWidth()); + } + }); + + // This script is bundled once per page load, but the modal's DOM (including #bulkEmailEditPanelWidget) + // is destroyed and freshly re-rendered by ABP's ModalManager on every open. Reset module state on close + // so a stale selectedApplicationId or an in-flight request from a previous open doesn't leak into the + // next one — invalidatePendingPanelRequest() bumps the version counter (so any still-pending response, + // for either async leg of loadDraftIntoPanel, gets ignored on arrival) and aborts the refresh request. + $(document).on('hidden.bs.modal', '#sendEmailNotificationModal', function () { + selectedApplicationId = null; + invalidatePendingPanelRequest(); + bulkEmailSendConfirmed = false; + $('#DetailsViewApplicationId').val(''); + }); +}); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml new file mode 100644 index 0000000000..f1bc54e27f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml @@ -0,0 +1,50 @@ +@page +@model Unity.GrantManager.Web.Pages.BulkEmailNotifications.SendEmailNotificationSummaryModalModel +@using Microsoft.Extensions.Localization +@using Unity.GrantManager.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@inject IStringLocalizer L + +@{ + Layout = null; +} + + + + Send Email Summary + + +
+ + + + + + + + @for (var i = 0; i < Model.BulkEmailNotificationResults?.Count; i++) + { + + + + + + } + +
Submission #:Status:
+ @if (Model.BulkEmailNotificationResults[i].IsSuccess) + { + + } + else + { + + } + + + + +
+
+
+
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs new file mode 100644 index 0000000000..5118e7b1ce --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/SendEmailNotificationSummaryModal.cshtml.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Unity.GrantManager.GrantApplications; + +namespace Unity.GrantManager.Web.Pages.BulkEmailNotifications +{ + public class SendEmailNotificationSummaryModalModel : PageModel + { + [BindProperty] + public List? BulkEmailNotificationResults { get; set; } + + public void OnGet(string summaryJson) + { + var items = new List(); + + var result = JsonSerializer.Deserialize(summaryJson); + + foreach (var item in result?.Successes ?? []) + { + items.Add(new BulkEmailNotificationItemResult + { + ReferenceNo = item, + Message = "Queued for delivery", + IsSuccess = true + }); + } + + foreach (var item in result?.Failures ?? []) + { + items.Add(new BulkEmailNotificationItemResult + { + ReferenceNo = item.Key, + Message = item.Value, + IsSuccess = false + }); + } + + BulkEmailNotificationResults = [.. items.OrderBy(s => s.ReferenceNo)]; + } + + public class BulkEmailNotificationItemResult + { + public string ReferenceNo { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + public bool IsSuccess { get; set; } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs new file mode 100644 index 0000000000..19fd620d02 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/BulkEmailNotificationViewModel.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Unity.GrantManager.Web.Pages.BulkEmailNotifications.ViewModels +{ + public class BulkEmailNotificationViewModel + { + public BulkEmailNotificationViewModel() + { + Notes = []; + } + + public Guid ApplicationId { get; set; } + public Guid? EmailId { get; set; } + public string ReferenceNo { get; set; } = string.Empty; + public string? ApplicantName { get; set; } = string.Empty; + public string FormName { get; set; } = string.Empty; + public string ApplicationStatus { get; set; } = string.Empty; + + [DisplayName("Approved Amount")] + public decimal ApprovedAmount { get; set; } + + [DisplayName("Decision Date")] + public DateTime? DecisionDate { get; set; } + + [DisplayName("Created By")] + public string? CreatedByName { get; set; } + + [DisplayName("Last Modified")] + public DateTime? LastModified { get; set; } + + public string? EmailSubject { get; set; } + public bool IsValid { get; set; } + public List Notes { get; set; } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/EmailNotificationNoteViewModel.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/EmailNotificationNoteViewModel.cs new file mode 100644 index 0000000000..b4e4967383 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/BulkEmailNotifications/ViewModels/EmailNotificationNoteViewModel.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Localization; +using System.Collections.Generic; + +namespace Unity.GrantManager.Web.Pages.BulkEmailNotifications.ViewModels +{ + public class EmailNotificationNoteViewModel + { + public EmailNotificationNoteViewModel(string key, bool active, string description, bool isError) + { + Key = key; + Active = active; + Description = description; + IsError = isError; + } + + public string Key { get; set; } + public bool Active { get; set; } + public string Description { get; set; } + public bool IsError { get; set; } + + public static List CreateNotesList(IStringLocalizer localizer) + { + return + [ + new("NO_DRAFT_FOUND", false, localizer.GetString("SendEmailNotificationRequest:NoDraftFound"), true), + new("MULTIPLE_DRAFTS_FOUND", false, localizer.GetString("SendEmailNotificationRequest:MultipleDraftsFound"), true), + new("MISSING_SUBJECT", false, localizer.GetString("SendEmailNotificationRequest:MissingSubject"), true), + new("MISSING_TO_ADDRESS", false, localizer.GetString("SendEmailNotificationRequest:MissingToAddress"), true), + new("MISSING_FROM_ADDRESS", false, localizer.GetString("SendEmailNotificationRequest:MissingFromAddress"), true), + new("MISSING_BODY", false, localizer.GetString("SendEmailNotificationRequest:MissingBody"), true) + ]; + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Error.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Error.cshtml index 952ad6afb9..17c22b8b7e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Error.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Error.cshtml @@ -7,6 +7,8 @@ @{ var code = Model.HttpStatusCode; var isWrongTenant = code == 409; + var isPossibleTenantMismatch = code == 404 && !string.IsNullOrEmpty(Model.EntityType); + var isApplicant = string.Equals(Model.EntityType, "Applicant", StringComparison.OrdinalIgnoreCase); string title; string message; @@ -14,8 +16,8 @@ switch (code) { case 404: - title = "Page Not Found"; - message = "The page you are looking for does not exist or may have been moved."; + title = isPossibleTenantMismatch ? L["WrongTenantError:Title"].Value : "Page Not Found"; + message = isPossibleTenantMismatch ? string.Empty : "The page you are looking for does not exist or may have been moved."; break; case 403: title = "Access Denied"; @@ -42,7 +44,15 @@ @if (isWrongTenant) {

- @L["WrongTenantError:ApplicationTenant", Model.ApplicationTenantName]
+ @(isApplicant ? L["WrongTenantError:ApplicantTenant", Model.ApplicationTenantName] : L["WrongTenantError:ApplicationTenant", Model.ApplicationTenantName])
+ @L["WrongTenantError:CurrentTenant", Model.CurrentTenantName] +

+

@L["WrongTenantError:Instructions"]

+ } + else if (isPossibleTenantMismatch) + { +

+ @(isApplicant ? L["WrongTenantError:ApplicantNotFoundPossibleTenant"] : L["WrongTenantError:ApplicationNotFoundPossibleTenant"])
@L["WrongTenantError:CurrentTenant", Model.CurrentTenantName]

@L["WrongTenantError:Instructions"]

diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Error.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Error.cshtml.cs index aefc0dae49..fa3dd32a6f 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Error.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/Error.cshtml.cs @@ -7,11 +7,13 @@ public class ErrorModel : PageModel public int HttpStatusCode { get; private set; } public string? ApplicationTenantName { get; private set; } public string? CurrentTenantName { get; private set; } + public string? EntityType { get; private set; } - public void OnGet(int httpStatusCode = 0, string? applicationTenantName = null, string? currentTenantName = null) + public void OnGet(int httpStatusCode = 0, string? applicationTenantName = null, string? currentTenantName = null, string? entityType = null) { HttpStatusCode = httpStatusCode;//HTTP Status Code ApplicationTenantName = applicationTenantName; CurrentTenantName = currentTenantName; + EntityType = entityType; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml index 40884fc686..b4e8413905 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml @@ -368,7 +368,7 @@ @if (notificationsFeatureEnabled && readEmailGranted) {
-
+
@await Component.InvokeAsync("EmailsWidget", new { applicationId = Model.ApplicationId, currentUserId = Model.CurrentUserId })
Email History
diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs index 223565f1e8..f29cb1774d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/Details.cshtml.cs @@ -20,6 +20,7 @@ using Unity.Modules.Shared.Correlation; using Unity.Modules.Shared.Specializations; using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; +using Volo.Abp.Domain.Entities; using Volo.Abp.Features; using Volo.Abp.TenantManagement; using Volo.Abp.Users; @@ -128,6 +129,7 @@ public async Task OnGetAsync() return RedirectToPage("/Error", new { httpStatusCode = 409, + entityType = "Application", applicationTenantName = applicationTenant?.Name ?? TenantId.Value.ToString(), currentTenantName = CurrentTenant.Name ?? "Host" }); @@ -138,7 +140,20 @@ public async Task OnGetAsync() ViewData["ActiveNavHref"] = "/TenantManagement/Onboarding"; } - ApplicationFormSubmission applicationFormSubmission = await _grantApplicationAppService.GetFormSubmissionByApplicationId(ApplicationId); + ApplicationFormSubmission applicationFormSubmission; + try + { + applicationFormSubmission = await _grantApplicationAppService.GetFormSubmissionByApplicationId(ApplicationId); + } + catch (EntityNotFoundException) + { + return RedirectToPage("/Error", new + { + httpStatusCode = 404, + entityType = "Application", + currentTenantName = CurrentTenant.Name ?? "Host" + }); + } ZoneStateSet = await _zoneManagementAppService.GetZoneStateSetAsync(applicationFormSubmission.ApplicationFormId); var formVersion = applicationFormSubmission.ApplicationFormVersionId.HasValue diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js index c8bb1d95bf..3867265346 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Pages/GrantApplications/ai-generation-button-state.js @@ -21,7 +21,7 @@ || error?.responseText || ''; - const match = String(message).match(/try again in\s+(\d+)\s+second/i); + const match = /try again in\s+(\d+)\s+second/i.exec(String(message)); return match ? Number(match[1]) : 0; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js index 46df2d8efc..5d313d3add 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Settings/TagManagement/TagManagement.js @@ -44,9 +44,7 @@ function defineTagSummaryColumnDefs() { title: "Count", name: 'totalCount', data: 'totalCount' - }); - - columnDefs.push({ + }, { title: "Actions", name: 'actions', data: 'tag.name', diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/ActionBar.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/ActionBar.cs index 18a1e5edfc..a8ed290238 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/ActionBar.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/ActionBar.cs @@ -28,6 +28,13 @@ public override void ConfigureBundle(BundleConfigurationContext context) .AddIfNotContains("/Pages/BulkApprovals/ApproveApplicationsModal.css"); context.Files .AddIfNotContains("/Pages/BulkActions/BulkPublishApplications.css"); + context.Files + .AddIfNotContains("/Pages/BulkEmailNotifications/SendEmailNotificationModal.css"); + // The bulk-send modal mounts EmailsWidget dynamically (via abp.WidgetManager) for the selected + // application, rather than server-rendering it as part of this page — so its own [Widget] bundle + // never gets a chance to auto-register here. Pull it in explicitly. + context.Files + .AddIfNotContains("/Views/Shared/Components/EmailsWidget/Default.css"); } } @@ -52,6 +59,14 @@ public override void ConfigureBundle(BundleConfigurationContext context) .AddIfNotContains("/Pages/BulkApprovals/ApproveApplicationsModal.js"); context.Files .AddIfNotContains("/Pages/BulkActions/BulkPublishApplications.js"); + context.Files + .AddIfNotContains("/Pages/BulkEmailNotifications/SendEmailNotificationModal.js"); + // Same reasoning as the style bundle above: EmailsWidget is only ever mounted dynamically inside + // the bulk-send modal on this page, so its script needs to be pulled in explicitly rather than + // relying on the widget's own [Widget(AutoInitialize = true)] bundle registration, which only + // fires for widgets that are actually server-rendered as part of a page's initial HTML. + context.Files + .AddIfNotContains("/Views/Shared/Components/EmailsWidget/Default.js"); } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml index 7843f10950..14f3087489 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.cshtml @@ -4,14 +4,19 @@ @using Unity.GrantManager.Permissions; @using Unity.GrantManager.Payments; @using Unity.Modules.Shared; +@using Unity.Notifications.Permissions; @using Unity.Payments.Permissions; @using Volo.Abp.Authorization.Permissions; @using Volo.Abp.Features; +@using Volo.Abp.Users; +@using Microsoft.Extensions.Configuration; @inject IStringLocalizer L @inject IAuthorizationService AuthorizationService @inject IFeatureChecker FeatureChecker @inject IPermissionChecker PermissionChecker +@inject ICurrentUser CurrentUser +@inject IConfiguration Configuration
@@ -97,6 +102,30 @@ button-type="Secondary" /> } + @* The bulk-send modal's right panel reuses EmailsWidget, whose edit fieldset/Save button/attachment + handling all require Notifications.Email.Send (see EmailsWidget/Default.cshtml and + EmailLogAttachmentAppService's class-level [Authorize]). SendBulk and Send are sibling permissions, + not nested, so a user could otherwise be granted SendBulk alone and open this feature into a + read-only, non-functional right panel. Require both. *@ + @if (await FeatureChecker.IsEnabledAsync("Unity.Notifications") + && await PermissionChecker.IsGrantedAsync(NotificationsPermissions.Email.SendBulk) + && await PermissionChecker.IsGrantedAsync(NotificationsPermissions.Email.Send)) + { + + @* EmailsWidget (reused inside the bulk-send modal's edit panel) expects these ambient hidden + fields to exist on the host page — normally provided by GrantApplications/Details.cshtml. + DetailsViewApplicationId's value is updated per row selection by SendEmailNotificationModal.js. *@ + + + + + + } + @if (await FeatureChecker.IsEnabledAsync(PaymentConsts.UnityPaymentsFeature) && await PermissionChecker.IsGrantedAsync(PaymentsPermissions.Payments.RequestPayment)) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js index f370959240..271af63eb1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ActionBar/Default.js @@ -16,6 +16,12 @@ $(function () { let approveApplicationsSummaryModal = new abp.ModalManager({ viewUrl: 'BulkApprovals/ApproveApplicationsSummaryModal' }); + let sendEmailNotificationModal = new abp.ModalManager({ + viewUrl: 'BulkEmailNotifications/SendEmailNotificationModal' + }); + let sendEmailNotificationSummaryModal = new abp.ModalManager({ + viewUrl: 'BulkEmailNotifications/SendEmailNotificationSummaryModal' + }); let tagApplicationModal = new abp.ModalManager({ viewUrl: 'ApplicationTags/ApplicationTagsSelectionModal', }); @@ -67,9 +73,9 @@ $(function () { let groupedValues = Object.values(groupedTags); if (groupedValues.length === 0) return []; - return groupedValues.reduce(function (prev, next) { + return groupedValues.slice(1).reduce(function (prev, next) { return prev.filter(p => hasMatchingTagId(p, next)); - }); + }, groupedValues[0]); } function filterUncommonTags(tagList, commonTags) { @@ -328,6 +334,48 @@ $(function () { }); //#endregion Batch Approval + //#region Send Email Notification + $('#sendEmailNotification').on("click", function () { + // Store application IDs in distributed cache to avoid URL length limits + unity.grantManager.applications.applicationBulkActions + .storeApplicationIds({ applicationIds: selectedApplicationIds }) + .then(function(response) { + // Open modal with cache key instead of application IDs array + sendEmailNotificationModal.open({ + cacheKey: response.cacheKey + }); + }) + .catch(function(error) { + abp.notify.error('Failed to prepare bulk email. Please try again.'); + console.error('Error storing application IDs:', error); + }); + }); + sendEmailNotificationModal.onResult(function (_, response) { + let transformedFailures = response.responseText.failures.map(failure => { + return { + Key: failure.key, + Value: failure.value + }; + }); + let successCount = response.responseText.successes.length; + if (successCount > 0) { + abp.notify.success( + successCount === 1 + ? 'The email has been successfully queued for delivery.' + : successCount + ' emails have been successfully queued for delivery.', + 'Send Email Notification' + ); + } + let summaryJson = JSON.stringify( + { + Successes: response.responseText.successes, + Failures: transformedFailures + }); + sendEmailNotificationSummaryModal.open({ summaryJson: summaryJson }); + PubSub.publish("refresh_application_list"); + }); + //#endregion Send Email Notification + //#region Selection Events PubSub.subscribe("select_application", (msg, data) => { selectedApplicationIds.push(data.id); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js index 1a935464ae..804bac1579 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/ChefsAttachments/ChefsAttachments.js @@ -525,7 +525,7 @@ function downloadChefsFile(event) { link.style.display = 'none'; document.body.appendChild(link); link.click(); - document.body.removeChild(link); + link.remove(); abp.notify.success('', 'The file has been downloaded successfully.'); }, error: function (error) { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CommentsWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CommentsWidget/Default.js index 84252eb367..dd5d81eaed 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CommentsWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/CommentsWidget/Default.js @@ -203,7 +203,7 @@ function initTribute(mentionData) { let tribute = new Tribute({ values: mentionData, selectTemplate: function (item) { - if (typeof item === 'undefined') return null; + if (item === undefined) return null; if (this.range.isContentEditable(this.current.element)) { return (`${item.original.value}`); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.css b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.css index b345ff71b3..de4d96230a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.css +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.css @@ -1,4 +1,4 @@ -#applicationEmailsWidget .single-email { +.emails-widget-scope .single-email { position: relative; border-radius: 4px; overflow: hidden; @@ -10,11 +10,11 @@ flex: 1; } -#applicationEmailsWidget .single-email.edit-mode { +.emails-widget-scope .single-email.edit-mode { width: 100%; } -#applicationEmailsWidget .email-input { +.emails-widget-scope .email-input { outline: 0; margin-right: 8px; margin-left: 10px; @@ -23,11 +23,11 @@ height: 36px; } -#applicationEmailsWidget input.form-control { +.emails-widget-scope input.form-control { width: 99%; } -#applicationEmailsWidget .label-column-aligned { +.emails-widget-scope .label-column-aligned { display: flex; align-items: flex-start; height: 100%; @@ -36,36 +36,36 @@ justify-content: flex-end; } -#applicationEmailsWidget .col-1 { +.emails-widget-scope .col-1 { width: 44px; } -#applicationEmailsWidget .col-auto { +.emails-widget-scope .col-auto { flex: 0 0 auto; width: auto; } /* Uniform label width for To, Cc, Bcc fields */ -#applicationEmailsWidget .row.align-items-start > .col-auto, -#applicationEmailsWidget .row.align-items-center > .col-auto { +.emails-widget-scope .row.align-items-start > .col-auto, +.emails-widget-scope .row.align-items-center > .col-auto { flex: 0 0 46px !important; width: 46px !important; } -#applicationEmailsWidget .col { +.emails-widget-scope .col { flex: 1 0 0%; width: 100%; } -#applicationEmailsWidget .email-to-container { +.emails-widget-scope .email-to-container { position: relative; } -#applicationEmailsWidget .email-to-container .email-input { +.emails-widget-scope .email-to-container .email-input { padding-right: 60px; } -#applicationEmailsWidget .email-bcc-button { +.emails-widget-scope .email-bcc-button { position: absolute; right: 8px; font-size: 0.75rem; @@ -74,28 +74,28 @@ z-index: 10; } -#applicationEmailsWidget #bcc-input-row { +.emails-widget-scope #bcc-input-row { display: none; } -#applicationEmailsWidget #bcc-input-row.show { +.emails-widget-scope #bcc-input-row.show { display: flex; } -#applicationEmailsWidget #scheduled-delay-section #send-on-display:empty { +.emails-widget-scope #scheduled-delay-section #send-on-display:empty { display: none; } -#applicationEmailsWidget #scheduled-delay-section #send-on-display:empty ~ #btn-clear-schedule { +.emails-widget-scope #scheduled-delay-section #send-on-display:empty ~ #btn-clear-schedule { display: none !important; } -#applicationEmailsWidget .email-bcc-button.hide { +.emails-widget-scope .email-bcc-button.hide { display: none; } -#applicationEmailsWidget .from-field-container { +.emails-widget-scope .from-field-container { align-items: center; display: flex; height: 36px; @@ -107,7 +107,7 @@ margin-top: 4px; } -#applicationEmailsWidget .from-field-container .btn-send-from { +.emails-widget-scope .from-field-container .btn-send-from { position: absolute; right: 0; top: 0; @@ -120,7 +120,7 @@ justify-content: center; } -#applicationEmailsWidget .from-label { +.emails-widget-scope .from-label { font-size: 1rem !important; font-weight: 500; white-space: nowrap; @@ -130,7 +130,7 @@ min-width: 40px; } -#applicationEmailsWidget .from-field-container .from-input { +.emails-widget-scope .from-field-container .from-input { min-width: 150px; width: 100%; height: 36px; @@ -142,15 +142,16 @@ margin: 0px !important; } -#applicationEmailsWidget .from-field-container .mb-3 { +.emails-widget-scope .from-field-container .mb-3 { display: inline-block; vertical-align: middle; margin-top: 9px; margin-bottom: 0px; flex-grow: 1; + position: relative; } -#applicationEmailsWidget .email-form label { +.emails-widget-scope .email-form label { color: var(--bc-colors-grey-text-300); white-space: nowrap; overflow: hidden; @@ -161,14 +162,14 @@ } /* Override margin-top for labels in row layouts (To, Cc, Bcc) */ -#applicationEmailsWidget .row.align-items-start label, -#applicationEmailsWidget .row.align-items-center label { +.emails-widget-scope .row.align-items-start label, +.emails-widget-scope .row.align-items-center label { margin-top: 15px; font-size: 14px; font-weight: 500; } -#applicationEmailsWidget .form-label { +.emails-widget-scope .form-label { display: inline-block; color: var(--bc-colors-grey-text-300, #666666); font-size: 1rem !important; @@ -179,53 +180,53 @@ } /* Specific alignment fix for labels in horizontal layouts */ -#applicationEmailsWidget #scheduled-label-container { +.emails-widget-scope #scheduled-label-container { display: none; padding-top: 8px; /* Adjust this value to perfectly center label with the first line of input */ } -#applicationEmailsWidget #scheduled-label-container.show { +.emails-widget-scope #scheduled-label-container.show { display: inline; } /* Ensure the parent row aligns to the top to prevent shifting when errors appear */ -#applicationEmailsWidget .row.align-items-start { +.emails-widget-scope .row.align-items-start { align-items: flex-start !important; } -#applicationEmailsWidget .email-input:hover { +.emails-widget-scope .email-input:hover { box-shadow: none !important; } -#applicationEmailsWidget .email-input.selected { +.emails-widget-scope .email-input.selected { border: 0 !important; outline: 0 !important; border-bottom: 0.5px solid #D6D6D6 !important; border-radius: 0px !important; } -#applicationEmailsWidget .email-button-container { +.emails-widget-scope .email-button-container { display: flex; align-items: flex-end; justify-content: right; gap: 8px; } -#applicationEmailsWidget .email-input-multiple:hover { +.emails-widget-scope .email-input-multiple:hover { box-shadow: none !important; border-bottom: 1.5px solid var(--bc-colors-grey-text-500) !important; } -#applicationEmailsWidget .email-input::placeholder { +.emails-widget-scope .email-input::placeholder { color: #b9b9b9 !important; } -#applicationEmailsWidget textarea::placeholder { +.emails-widget-scope textarea::placeholder { color: #b9b9b9 !important; } /* Email template select styling */ -#applicationEmailsWidget #EmailTemplate { +.emails-widget-scope #EmailTemplate { margin-right: 8px; margin-left: 8px; margin-top: 2px; @@ -233,23 +234,21 @@ } /* 1. Style the select itself when the empty value is selected */ -#applicationEmailsWidget #EmailTemplate:has(option[value=""]:checked) { +.emails-widget-scope #EmailTemplate:has(option[value=""]:checked) { color: #b9b9b9 !important; } /* 2. Style the placeholder option inside the dropdown list */ -#applicationEmailsWidget #EmailTemplate option[value=""] { +.emails-widget-scope #EmailTemplate option[value=""] { color: #b9b9b9 !important; } /* 3. Ensure valid selections return to your default text color (e.g., dark grey/black) */ -#applicationEmailsWidget #EmailTemplate option:not([value=""]) { +.emails-widget-scope #EmailTemplate option:not([value=""]) { color: #212529; } -#applicationEmailsWidget button { - margin-top: 0px; - margin-bottom: 0px; +.emails-widget-scope button { height: 36px; min-height: 36px; display: inline-flex; @@ -259,9 +258,10 @@ padding-top: 0px; padding-bottom: 0px; box-sizing: border-box; + margin: 2px; } -#user-dropdown > button { +.emails-widget-scope #user-dropdown > button { height: 36px; min-height: 36px; max-height: 36px; @@ -275,12 +275,12 @@ margin-bottom: 0px; } -#applicationEmailsWidget .email-buttons-group { +.emails-widget-scope .email-buttons-group { display: flex; align-items: center; } -#applicationEmailsWidget .email-btn { +.emails-widget-scope .email-btn { margin-top: 10px; padding-left: 14px; padding-right: 14px; @@ -292,17 +292,16 @@ transition: box-shadow .3s; } -#applicationEmailsWidget .email-btn:hover { +.emails-widget-scope .email-btn:hover { box-shadow: -3px 3px 11px -3px rgba(33, 33, 33, .2); } -#applicationEmailsWidget .btn-send-close { - margin-top: 0px; - margin-bottom: 0px; +.emails-widget-scope .btn-send-close { + margin: 4px; } /* Send dropdown button styling */ -#applicationEmailsWidget .btn-group .dropdown-toggle { +.emails-widget-scope .btn-group .dropdown-toggle { padding-left: 1px !important; padding-right: 1px !important; border-left: 1px solid rgba(255, 255, 255, 0.2) !important; @@ -312,46 +311,46 @@ } /* When dropdown is not visible/enabled, apply right border radius to send button */ -#applicationEmailsWidget .btn-group:not(:has(.dropdown-toggle)) #btn-send-top { +.emails-widget-scope .btn-group:not(:has(.dropdown-toggle)) #btn-send-top { border-top-right-radius: 3px !important; border-bottom-right-radius: 3px !important; } -#applicationEmailsWidget .form-select { +.emails-widget-scope .form-select { height: 36px; } -#applicationEmailsWidget .btn-group .dropdown-toggle.show { +.emails-widget-scope .btn-group .dropdown-toggle.show { background-color: inherit !important; color: inherit !important; border-color: inherit !important; } -#applicationEmailsWidget .btn-group .dropdown-toggle::after { +.emails-widget-scope .btn-group .dropdown-toggle::after { display: none; } -#applicationEmailsWidget .btn-group .dropdown-toggle i { +.emails-widget-scope .btn-group .dropdown-toggle i { font-size: 0.85rem; } /* Dropdown menu positioning */ -#applicationEmailsWidget .btn-group .dropdown-menu { +.emails-widget-scope .btn-group .dropdown-menu { left: 0 !important; right: auto !important; margin-top: 0.25rem !important; transform: translate(0px, 36px) !important; } -.dropdown-menu .btn-send-menu, -.dropdown-menu .btn-schedule-send-menu { +.emails-widget-scope .dropdown-menu .btn-send-menu, +.emails-widget-scope .dropdown-menu .btn-schedule-send-menu { display: block; width: 100%; text-align: left; } /* Email buttons row - sticky at top */ -#applicationEmailsWidget #EmailForm .row:has(.email-buttons-container) { +.emails-widget-scope #EmailForm .row:has(.email-buttons-container) { position: sticky; top: 0; background-color: white; @@ -359,12 +358,12 @@ padding-bottom: 0.8rem !important; } -#scheduled-display-container { +.emails-widget-scope #scheduled-display-container { padding-bottom: 10px; } /* Email buttons container */ -.email-buttons-container { +.emails-widget-scope .email-buttons-container { display: flex !important; align-items: center; gap: 0.5rem; @@ -372,22 +371,22 @@ overflow: visible; } -.email-buttons-container abp-modal-footer { +.emails-widget-scope .email-buttons-container abp-modal-footer { display: contents; } -.email-buttons-container .ms-auto { +.emails-widget-scope .email-buttons-container .ms-auto { display: flex; align-items: center; gap: 0.5rem; overflow: visible; } -.email-buttons-container .dropdown-menu { +.emails-widget-scope .email-buttons-container .dropdown-menu { z-index: 1070; } -#applicationEmailsWidget .email-lbl { +.emails-widget-scope .email-lbl { display: block; margin-top: 0.25rem; width: 99%; @@ -398,14 +397,14 @@ font-size: var(--bc-font-size); } -#applicationEmailsWidget .email-lbl-wrapper { +.emails-widget-scope .email-lbl-wrapper { color: #666666; font-size: 0.89rem; font-weight: 400; word-wrap: break-word; } -#applicationEmailsWidget .unity-email-block { +.emails-widget-scope .unity-email-block { background: var(--bc-surface--color--brand--gold--20); padding: 0.75rem 0.5rem 0.5rem 1rem; border-radius: 0.25rem; @@ -415,7 +414,7 @@ width: 90%; } -#applicationEmailsWidget .field-validation-error { +.emails-widget-scope .field-validation-error { display: inline-block; margin-left: 8px; white-space: nowrap; @@ -423,7 +422,7 @@ color: #dc3545; } -#applicationEmailsWidget .input-validation-error { +.emails-widget-scope .input-validation-error { border: 1px solid #dc3545 !important; border-style: solid !important; border-color: #dc3545 !important; @@ -431,23 +430,28 @@ box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 0.2rem rgba(220, 53, 69, 0.5) !important; } -#applicationEmailsWidget .input-validation-error:focus, -#applicationEmailsWidget input.input-validation-error:focus, -#applicationEmailsWidget textarea.input-validation-error:focus, -#applicationEmailsWidget select.input-validation-error:focus { +.emails-widget-scope .input-validation-error:focus, +.emails-widget-scope input.input-validation-error:focus, +.emails-widget-scope textarea.input-validation-error:focus, +.emails-widget-scope select.input-validation-error:focus { border: 1px solid #dc3545 !important; border-color: #dc3545 !important; box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 0.2rem rgba(220, 53, 69, 0.5) !important; } -#applicationEmailsWidget .from-field-container .field-validation-error { +.emails-widget-scope .from-field-container .field-validation-error { position: absolute; - top: 85%; + top: calc(100% + 0.25rem); + left: 0; margin-left: 0; display: inline-block; } -#applicationEmailsWidget .confirmation-label { +.emails-widget-scope #EmailForm .row:has(.from-field-container .field-validation-error) { + padding-bottom: 1.75rem !important; +} + +.emails-widget-scope .confirmation-label { font-size: 19px; font-weight: 600; text-align: center; @@ -460,7 +464,7 @@ color: inherit; } -#confirmation-modal { +.emails-widget-scope #confirmation-modal { z-index: 1; justify-content: center; margin: 1em 1.6em .3em; @@ -475,7 +479,7 @@ word-break: break-word; } -#btn-cancel-email { +.emails-widget-scope #btn-cancel-email { border: 0; border-radius: .25em; background: initial; @@ -485,11 +489,7 @@ margin: 4px; } -#applicationEmailsWidget .btn-send-close { - margin: 4px; -} - -#applicationEmailsWidget #modal-background { +.emails-widget-scope #modal-background { display: none; position: fixed; top: 0; @@ -504,19 +504,19 @@ z-index: 1000; } -#applicationEmailsWidget #modal-content { +.emails-widget-scope #modal-content { display: none; } -#applicationEmailsWidget #spinner-modal { +.emails-widget-scope #spinner-modal { display: none; } -#applicationEmailsWidget .modal-footer { +.emails-widget-scope .modal-footer { justify-content: center; } -#applicationEmailsWidget .modal-content { +.emails-widget-scope .modal-content { border-radius: 5px; -webkit-border-radius: 5px; -moz-border-radius: 5px; @@ -539,31 +539,31 @@ justify-content: center; } -#EmailForm { +.emails-widget-scope #EmailForm { display: none; } -#EmailForm.active { +.emails-widget-scope #EmailForm.active { display: block; } -#applicationEmailsWidget .hide { +.emails-widget-scope .hide { display: none; } -#applicationEmailsWidget .toast-top-center { +.emails-widget-scope .toast-top-center { top: 220px; margin: 0 auto; left: 50%; margin-left: -450px; } -#applicationEmailsWidget #modal-background.active, -#applicationEmailsWidget #modal-content.active { +.emails-widget-scope #modal-background.active, +.emails-widget-scope #modal-content.active { display: block; } -#applicationEmailsWidget #email-spinner { +.emails-widget-scope #email-spinner { width: 40px; height: 40px; position: absolute; @@ -577,28 +577,24 @@ animation: rotateSpinner 800ms linear infinite; } -#applicationEmailsWidget .email-spinner-text { +.emails-widget-scope .email-spinner-text { display: flex; padding-right: 100px; } -#applicationEmailsWidget button { - margin: 2px; -} - -#applicationEmailsWidget button:hover { +.emails-widget-scope button:hover { border: 1px solid var(--bc-colors-blue-primary); } -#applicationEmailsWidget .fl-cancel { +.emails-widget-scope .fl-cancel { color: var(--bc-colors-blue-primary); } -#applicationEmailsWidget .btn-delete-draft { +.emails-widget-scope .btn-delete-draft { background-color: white !important; } -#schedule-modal-backdrop { +.emails-widget-scope #schedule-modal-backdrop { display: none; position: fixed; inset: 0; @@ -606,7 +602,7 @@ z-index: 1060; } -#schedule-send-modal { +.emails-widget-scope #schedule-send-modal { display: none !important; position: fixed; top: 50%; @@ -624,12 +620,12 @@ overflow: auto; } -#schedule-send-modal.active { +.emails-widget-scope #schedule-send-modal.active { display: flex !important; } /* Header */ -.schedule-modal-header { +.emails-widget-scope .schedule-modal-header { display: flex; justify-content: space-between; align-items: center; @@ -638,14 +634,14 @@ border-bottom: 1px solid #dee2e6; } -.schedule-modal-title { +.emails-widget-scope .schedule-modal-title { font-size: 1.25rem; font-weight: 600; margin: 0; color: #212529; } -.btn-close-modal { +.emails-widget-scope .btn-close-modal { background: none; border: none; font-size: 1.5rem; @@ -656,12 +652,12 @@ height: auto; } -.btn-close-modal:hover { +.emails-widget-scope .btn-close-modal:hover { color: #212529; } /* Body Layout */ -.schedule-modal-body { +.emails-widget-scope .schedule-modal-body { display: flex; gap: 2rem; flex: 1; @@ -669,19 +665,19 @@ } /* Calendar Section */ -.schedule-calendar-section { +.emails-widget-scope .schedule-calendar-section { flex: 1; min-width: 300px; } -.calendar-nav { +.emails-widget-scope .calendar-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; } -.calendar-month-year { +.emails-widget-scope .calendar-month-year { font-size: 1.1rem; font-weight: 600; color: #212529; @@ -689,7 +685,7 @@ text-align: center; } -.calendar-nav button { +.emails-widget-scope .calendar-nav button { width: 32px; height: 32px; padding: 0; @@ -697,13 +693,13 @@ } /* Calendar Grid */ -.calendar-grid { +.emails-widget-scope .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; } -.calendar-day-header { +.emails-widget-scope .calendar-day-header { text-align: center; font-weight: 600; font-size: 0.85rem; @@ -714,7 +710,7 @@ grid-column: span 1; } -.calendar-day { +.emails-widget-scope .calendar-day { text-align: center; padding: 8px; cursor: pointer; @@ -724,37 +720,37 @@ border: 2px solid transparent; } -.calendar-day.past { +.emails-widget-scope .calendar-day.past { color: #adb5bd; cursor: not-allowed; background: transparent; } -.calendar-day.today { +.emails-widget-scope .calendar-day.today { background: #0d6efd; color: #fff; border-radius: 50%; font-weight: 600; } -.calendar-day.selected { +.emails-widget-scope .calendar-day.selected { border-color: #0d6efd; font-weight: 600; background: #e7f1ff; } -.calendar-day:not(.past):hover { +.emails-widget-scope .calendar-day:not(.past):hover { background: #f8f9fa; border-color: #0d6efd; } -.calendar-day.today.selected { +.emails-widget-scope .calendar-day.today.selected { border-color: #fff; background: #0d6efd; } /* Inputs Section */ -.schedule-inputs-section { +.emails-widget-scope .schedule-inputs-section { flex: 1; min-width: 240px; display: flex; @@ -762,108 +758,108 @@ gap: 1.5rem; } -#schedule-date-input { +.emails-widget-scope #schedule-date-input { width: 100%; box-sizing: border-box; } -.schedule-input-group { +.emails-widget-scope .schedule-input-group { display: flex; flex-direction: column; } -.schedule-input-group label { +.emails-widget-scope .schedule-input-group label { font-size: 0.95rem; font-weight: 500; margin-bottom: 0.5rem; color: #495057; } -#schedule-date-input, -#schedule-time-select { +.emails-widget-scope #schedule-date-input, +.emails-widget-scope #schedule-time-select { padding: 0.5rem 0.75rem; font-size: 0.95rem; border: 1px solid #ced4da; border-radius: 0.25rem; } -#schedule-date-input:focus, -#schedule-time-select:focus { +.emails-widget-scope #schedule-date-input:focus, +.emails-widget-scope #schedule-time-select:focus { border-color: #0d6efd; box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); outline: none; } -.input-group-text { +.emails-widget-scope .input-group-text { background: #f8f9fa; border-color: #ced4da; color: #495057; } /* Footer */ -.schedule-modal-footer { +.emails-widget-scope .schedule-modal-footer { margin-top: auto; padding-top: 16px; border-top: 1px solid #dee2e6; } -.schedule-modal-footer button { +.emails-widget-scope .schedule-modal-footer button { min-width: 100px; } -#schedule-modal-validation { +.emails-widget-scope #schedule-modal-validation { font-size: 0.85rem; } /* Responsive */ @media (max-width: 768px) { - #schedule-send-modal { + .emails-widget-scope #schedule-send-modal { width: 95vw; max-height: 95vh; padding: 16px; } - .schedule-modal-body { + .emails-widget-scope .schedule-modal-body { flex-direction: column; gap: 1rem; min-height: auto; } - .schedule-calendar-section, - .schedule-inputs-section { + .emails-widget-scope .schedule-calendar-section, + .emails-widget-scope .schedule-inputs-section { min-width: auto; width: 100%; } } -#btn-picker-ok { +.emails-widget-scope #btn-picker-ok { display: none; } -#send-on-display { +.emails-widget-scope #send-on-display { font-size: 0.9em; } -#btn-clear-schedule { +.emails-widget-scope #btn-clear-schedule { display: none; } -#delay-datetime-validation { +.emails-widget-scope #delay-datetime-validation { display: none; font-size: 0.85em; } -.send-date-hint { +.emails-widget-scope .send-date-hint { font-size: 0.85em; } -input#EmailFrom.input-validation-error { - margin-bottom: -20px !important; +.emails-widget-scope input#EmailFrom.input-validation-error { + margin-bottom: 0 !important; } -.mt-14 { +.emails-widget-scope .mt-14 { margin-top: 14px !important; } @@ -878,12 +874,12 @@ input#EmailFrom.input-validation-error { ============================================ */ /* Hide TinyMCE toolbar completely */ -.tox-tinymce:has(#EmailBody) .tox-toolbar__primary { +.emails-widget-scope .tox-tinymce:has(#EmailBody) .tox-toolbar__primary { display: none !important; } /* Container for template label and menu buttons */ -.tinymce-template-label-top-right { +.emails-widget-scope .tinymce-template-label-top-right { position: static !important; font-size: 13px; color: #444; @@ -898,7 +894,7 @@ input#EmailFrom.input-validation-error { /* Template label text */ -.template-label-text { +.emails-widget-scope .template-label-text { display: flex !important; align-items: center !important; justify-content: center !important; @@ -914,20 +910,20 @@ input#EmailFrom.input-validation-error { transition: all 0.15s ease-in-out; } -.template-label-text:hover { +.emails-widget-scope .template-label-text:hover { background-color: #6c757d; color: #fff !important; } -.template-label-text.no-permission { +.emails-widget-scope .template-label-text.no-permission { cursor: default; background: #f9f9f9; } /* TinyMCE-style toolbar group for templates button */ -.templates-toolbar-group { +.emails-widget-scope .templates-toolbar-group { display: flex; align-items: center; gap: 0; @@ -936,7 +932,7 @@ input#EmailFrom.input-validation-error { } /* TinyMCE-style menu button */ -.tinymce-menu-button { +.emails-widget-scope .tinymce-menu-button { display: inline-flex; align-items: center; padding: 0; @@ -953,28 +949,28 @@ input#EmailFrom.input-validation-error { transition: background-color 0.1s ease-in-out; } -.tinymce-menu-button:hover { +.emails-widget-scope .tinymce-menu-button:hover { background: #e0e0e0; } -.tinymce-menu-button:active { +.emails-widget-scope .tinymce-menu-button:active { background: #d0d0d0; } -.tinymce-menu-button.tox-tbtn--select { +.emails-widget-scope .tinymce-menu-button.tox-tbtn--select { justify-content: space-between; padding: 0 8px; gap: 4px; } -.tox-tbtn__select-label { +.emails-widget-scope .tox-tbtn__select-label { display: inline-block; font-weight: 500; color: #333; font-size: 13px; } -.tox-tbtn__select-chevron { +.emails-widget-scope .tox-tbtn__select-chevron { display: flex; align-items: center; justify-content: center; @@ -983,14 +979,14 @@ input#EmailFrom.input-validation-error { flex-shrink: 0; } -.tox-tbtn__select-chevron svg { +.emails-widget-scope .tox-tbtn__select-chevron svg { width: 10px; height: 10px; display: block; } /* Dropdown menu styling */ -.templates-dropdown-menu { +.emails-widget-scope .templates-dropdown-menu { position: fixed !important; background: #fff; border: 1px solid #ccc; @@ -1004,14 +1000,14 @@ input#EmailFrom.input-validation-error { } /* Custom dropdown menu styling (not Bootstrap) */ -.custom-dropdown-menu { +.emails-widget-scope .custom-dropdown-menu { list-style: none; padding: 0 !important; margin: 0 !important; display: block !important; } -.custom-dropdown-item { +.emails-widget-scope .custom-dropdown-item { padding: 8px 12px; cursor: pointer; border-bottom: 1px solid #eee; @@ -1022,23 +1018,23 @@ input#EmailFrom.input-validation-error { display: block !important; } -.custom-dropdown-item:hover { +.emails-widget-scope .custom-dropdown-item:hover { background: #f5f5f5; } -.custom-dropdown-item:last-child { +.emails-widget-scope .custom-dropdown-item:last-child { border-bottom: none !important; } /* Position Templates button in top right of toolbar */ -.tox:has(#EmailBody) .templates-button-container { +.emails-widget-scope .tox:has(#EmailBody) .templates-button-container { position: absolute !important; top: 5px !important; right: 10px !important; z-index: 10 !important; } -.tox *:not(svg):not(rect):not(.tox-tooltip):not(.tox-tooltip *):not(.tox-dialog):not(.tox-dialog *):not(.tox-menu):not(.tox-menu *):not(.tox-collection):not(.tox-collection *) { +.emails-widget-scope .tox *:not(svg):not(rect):not(.tox-tooltip):not(.tox-tooltip *):not(.tox-dialog):not(.tox-dialog *):not(.tox-menu):not(.tox-menu *):not(.tox-collection):not(.tox-collection *) { background-color: initial; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 14px !important; @@ -1048,7 +1044,7 @@ input#EmailFrom.input-validation-error { } /* Style the Templates select element */ -.templates-select { +.emails-widget-scope .templates-select { background: #f7f7f7 !important; padding: 4px 8px !important; border: 1px solid #ccc !important; @@ -1063,8 +1059,8 @@ input#EmailFrom.input-validation-error { -.templates-select:active, -.templates-select:focus { +.emails-widget-scope .templates-select:active, +.emails-widget-scope .templates-select:focus { outline: none !important; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js index a361aef984..7df1f8f1c5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/Default.js @@ -1,7 +1,15 @@ -$(document).ready(function () { +// Wrapped in a named, re-callable function (rather than a bare $(document).ready IIFE) so this widget can be +// mounted more than once per page — e.g. re-initialized against a freshly swapped-in DOM each time a different +// application's draft is selected in the bulk-send modal's edit panel. Every handler bound to a *persistent* +// target (document, a page-level scroll container, PubSub's own global subscriber registry) is explicitly +// unbound/unsubscribed before re-binding below, so repeated calls don't accumulate duplicate listeners. Handlers +// bound to elements that are part of this widget's own markup (e.g. #btn-save-top, the attachments table) don't +// need that treatment: each call captures fresh references to the newly-rendered DOM, and the previous DOM (with +// whatever was bound to it) is simply discarded along with the old closure. +function initializeEmailsWidget() { const BC_PERMANENT_DST_ZONE = 'UTC-7'; // Close dropdown menus when clicking outside - $(document).on('click', function (e) { + $(document).off('click.emailWidgetDropdown').on('click.emailWidgetDropdown', function (e) { if (!$(e.target).closest('.tinymce-menu-button, .custom-dropdown-menu').length) { $('.custom-dropdown-menu').remove(); } @@ -104,6 +112,17 @@ $('.btn-send-menu').off('click'); $('.btn-schedule-send-menu').off('click'); + // #EmailForm contains a type="submit" button (#btn-send-top). Clicking it is safely intercepted by + // handleSendEmail's preventDefault(), but pressing Enter in a single-line field (To/From/CC/BCC/Subject) + // triggers the browser's *implicit* form submission directly, bypassing that click handler entirely. + // Every real Save/Send path here is already JS/AJAX-driven, so a native submit of this form is never + // correct — block it outright. This matters more now that this form can end up nested inside another + //
(the bulk-send modal's own form) when this widget is reused there; nested forms are invalid + // HTML, and an unhandled native submit in that context would be especially disruptive. + UIElements.emailForm.off('submit.emailWidgetGuard').on('submit.emailWidgetGuard', function (e) { + e.preventDefault(); + }); + // Bind button handlers UIElements.btnNewEmail.on('click', function (e) { e.preventDefault(); @@ -186,7 +205,7 @@ bindDelayModeEvents(); - $('.details-scrollable').on('scroll.emailWidget', function () { + $('.details-scrollable').off('scroll.emailWidget').on('scroll.emailWidget', function () { $('.tox-toolbar__overflow').hide(); }); @@ -1482,7 +1501,10 @@ hideConfirmation(); handleCloseEmail(); abp.notify.success('Your email is being sent'); - PubSub.publish('refresh_application_emails'); + // Pass along which application this save/send actually belonged to — a listener elsewhere on the + // page (e.g. a multi-application context switching selection) can't otherwise tell which row this + // completion is for, since UIElements.applicationId isn't visible outside this closure. + PubSub.publish('refresh_application_emails', { applicationId: UIElements.applicationId }); }).fail(function () { hideConfirmation(); abp.notify.error('An error ocurred your email could not be sent.'); @@ -1553,7 +1575,8 @@ isNewEmailDraft = false; newDraftId = null; handleCloseEmail(); abp.notify.success('Your email has been saved.'); - PubSub.publish('refresh_application_emails'); + // See the matching comment in performSendEmail's success handler above. + PubSub.publish('refresh_application_emails', { applicationId: UIElements.applicationId }); }).fail(function () { UIElements.btnSave.prop('disabled', false); abp.notify.error('An error ocurred your email could not be saved.'); @@ -2067,6 +2090,15 @@ }); } + // This widget is the sole subscriber to each of these four topics anywhere in the app (confirmed by + // project-wide search) — unsubscribe before re-subscribing so repeated initializeEmailsWidget() calls + // don't leave stale handlers (bound to a previous, now-discarded DOM/closure) stacking up alongside the + // current one. + PubSub.unsubscribe('email_selected'); + PubSub.unsubscribe('applicant_info_updated'); + PubSub.unsubscribe('draft_email_deleted'); + PubSub.unsubscribe('reload_email_attachments_table'); + PubSub.subscribe('email_selected', (msg, data) => { console.log("EMAIL SELECTED EVENT FIRED", data); @@ -2351,10 +2383,10 @@ 'maximum allowed ' + totalMaxFileSize + ' MB. Please remove one or more attachments before sending.' ); $('#email-attachment-size-error').show(); - $('#btn-send').prop('disabled', true); + $('#btn-send-top').prop('disabled', true); } else { $('#email-attachment-size-error').hide(); - $('#btn-send').prop('disabled', false); + $('#btn-send-top').prop('disabled', false); } } @@ -2481,8 +2513,26 @@ } }); } +} + +$(document).ready(function () { + // This script is now also bundled on pages (e.g. GrantApplications' list page) that mount this widget's + // markup dynamically, later, only for users with the right permission/feature — never as part of the + // page's own initial HTML. Only auto-run setup if the widget's markup (and the ambient hidden fields it + // reads, like #DetailsViewApplicationId) is actually already present, as it always is on the page that + // server-renders this widget directly (GrantApplications/Details). Callers that mount this widget + // dynamically elsewhere call window.EmailsWidget.reinitialize() themselves once their markup exists. + if ($('#EmailForm').length) { + initializeEmailsWidget(); + } }); +// Exposed so callers that dynamically (re)mount this widget's markup elsewhere on the page — outside the +// normal single server-rendered-once-per-page usage on GrantApplications/Details — can re-run its setup +// against the freshly-inserted DOM. See SendEmailNotificationModal.js for the current caller. +window.EmailsWidget = window.EmailsWidget || {}; +window.EmailsWidget.reinitialize = initializeEmailsWidget; + /** * Returns TinyMCE editor toolbar options diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/EmailsWidgetController.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/EmailsWidgetController.cs index 3053ce8e5a..a6050b3d1c 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/EmailsWidgetController.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Web/Views/Shared/Components/EmailsWidget/EmailsWidgetController.cs @@ -14,14 +14,14 @@ public class EmailsWidgetController : AbpController [HttpGet] [Route("RefreshEmails")] - public IActionResult Emails(Guid ownerId, Guid currentUserId) - { + public IActionResult Emails(Guid applicationId, Guid currentUserId) + { if (!ModelState.IsValid) { logger.LogWarning("Invalid model state for EmailsWidgetController: RefreshEmails"); return ViewComponent("EmailsWidget"); } - return ViewComponent("EmailsWidget", new { ownerId, currentUserId }); + return ViewComponent("EmailsWidget", new { applicationId, currentUserId }); } } } diff --git a/applications/Unity.Tools/Unity.NginxData/nginx.conf b/applications/Unity.Tools/Unity.NginxData/nginx.conf index e395b4a9eb..1acafb0f3d 100644 --- a/applications/Unity.Tools/Unity.NginxData/nginx.conf +++ b/applications/Unity.Tools/Unity.NginxData/nginx.conf @@ -1,5 +1,5 @@ worker_processes auto; -error_log /var/log/nginx/error.log; +error_log /dev/stderr; pid /run/nginx.pid; # Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. @@ -12,9 +12,9 @@ events { http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + '"$http_user_agent" "$http_x_forwarded_for" "$host"'; - access_log /var/log/nginx/access.log main; + access_log /dev/stdout main; sendfile on; tcp_nopush on; @@ -33,7 +33,7 @@ http { # Enable gzip compression gzip on; - gzip_types text/plain application/json application/javascript text/css text/xml application/xml application/xml+rss text/javascript; + gzip_types text/plain application/json application/geo+json application/javascript text/css text/xml application/xml application/xml+rss text/javascript; # Load modular configuration files from the /etc/nginx/conf.d directory. # See http://nginx.org/en/docs/ngx_core_module.html#include @@ -71,7 +71,14 @@ http { add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; } - + + # Confirmed safe: CHEFS consumes /reference/ via client-session browser + # fetch/XHR/img, not window.open(), so COOP has no effect on this path. + add_header X-Content-Type-Options "nosniff" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + add_header Cross-Origin-Resource-Policy "cross-origin" always; + add_header Cache-Control "public, max-age=86400" always; + try_files $uri $uri/ =404; } @@ -83,11 +90,7 @@ http { } # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "no-referrer-when-downgrade" always; - add_header Content-Security-Policy "default-src 'self'; style-src 'self'; font-src 'self'; script-src 'self'; object-src 'none';" always; # Cross-origin isolation headers (scan findings) # COOP: restricts cross-origin window access add_header Cross-Origin-Opener-Policy "same-origin" always; @@ -105,9 +108,5 @@ http { error_page 500 502 503 504 /50x.html; location = /50x.html { } - - location /grants { - return 404; - } } }