mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
main
1215
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
aca0e40c37 |
Combine Policies and Pipelines pages (#7681)
# Description of Changes Combine the Policies and Pipelines pages into one, so we have the new concept of Policies as Pipelines that always run which the user cannot disable. What used to be Policies are now referred to as Templates, and they allow you to create a new Pipeline more easily with the simple UI. There's followup work to be done here to improve the template UIs because they've not been touched in a long time, but I've considered that beyond the scope of this merge. The only real changes I've made to them in this PR is that they have a toggle for whether they're policies, they now have a "Customise" button to kick you into the full Pipeline editor, and I've removed the source selection. Previously, they supported selecting as many sources as you liked, but that feature never worked and is incompatible with the backend as it stands now, which only allows for one source. Because of that, I've made it so that they can only run in editor unless you open them in the custom pipeline editor, where you can switch out which source it will use. There's also another bit of followup to rename and remove all the previous Policies code. Now that they've been combined into one, we don't need a lot of the Policies code anymore, but also there's about 300 files in the frontend referencing policies in text/comments which need to be updated to say pipelines. This is way more work than is reasonable to do in this PR so I'll just do it in a new PR. ## Limitations This PR is about the merging of the old Policies and Pipelines and I'm considering enforcing the new definition of a Policy where it's only modifiable by admins beyond the scope of this PR. <img width="756" height="395" alt="image" src="https://github.com/user-attachments/assets/d31be5ce-f1c9-46b3-8e8d-866e63f89a81" /> <img width="1507" height="793" alt="image" src="https://github.com/user-attachments/assets/9ba8875f-8be5-4881-91cf-40e0bc1076dc" /> <img width="1508" height="787" alt="image" src="https://github.com/user-attachments/assets/3e1da77b-a0c0-4262-aad3-16650098db81" /> --------- Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> |
||
|
|
1b2a3118a6 |
Disk-mounted folders on desktop and improved folder management (#7502)
Description of Changes Adds folder kinds so the file manager can work with real directories on disk. Desktop - New folder is now a menu with two options: "Add local folder" and "New folder on the server". - Add local folder opens the native picker and mounts a directory. Files are listed straight from disk, nothing is copied in. - Subfolders show inside a mount and open like any folder. New folder inside a mount creates a real directory on disk. - Moving, dropping or uploading files into a mount writes them to the directory. The app copy is only removed after the write succeeds. Name clashes get a " (2)" suffix. - Mounted files get thumbnails. - Adding the same directory twice just returns the existing mount. - Removing a mount never touches the disk. - The server option is disabled in local mode with a sign in message. Web + desktop - Uploading or dropping files while inside a folder puts them in that folder instead of Local. - Files can be dragged onto folders in the grid and the tree to move them. - Folders show an origin badge (cloud or local). - The Local view now means files that are not in any folder. Follow ups for a future pr - Mount listing cap: large directories currently show the 500 most recent files with no notice. Will be removed as part of the virtualisation/performance PR. - Folders within folders need to be supported - Symlinks in mounts: currently not listed. Behaviour to be decided alongside the wider folder work. |
||
|
|
3056e5ff44 |
Reset the PAYG free grant each billing period (#7709)
Needs the schema half: Stirling-Tools/Stirling-PDF-SaaS#327 ## Current state The PAYG free allowance is a one-time lifetime pool. `pricing_policy.free_tier_units` is copied into `payg_team_extensions.free_units_remaining` once, at team creation (V14 trigger, updated in V19), and the charge pipeline decrements it until it reaches zero. Nothing ever puts it back. ## Problem The product promises a monthly allowance the billing model does not grant. - The account-link connect dialog advertises "500 free per month". That has **merged to main** (#7415), so the claim is live and unhonoured until this lands. - The wallet meter already read "Process 500 PDFs free, then $X/PDF", which reads as an allowance-then-meter model. - `SignupRequiredBootstrap`'s own doc comment described a "free 500-op/month allowance" while its copy said only "500 free operations". Three separate comments asserted the opposite in code (`billing/types.ts`, `WalletSnapshotResponse`, `TeamBillingContext`), so the two halves of the repo disagreed about what a customer is owed. ## Solution The grant now recurs each billing period, **for every team**. Paying does not cost you the allowance: a subscribed team draws its grant first each period and meters only the excess, which is what the meter's copy always described. That also matches how the grant already worked at charge time, where it reduced metered units regardless of subscription. ### The reset is lazy, with no scheduler `payg_team_extensions` gains `free_units_period_start`: the period `free_units_remaining` was last written for. - A stamp older than the current period start, or absent as on every existing row, means the reset is owed. `TeamBillingService.remainingForPeriod` projects it to a full grant, so the entitlement gate and the wallet both show it the instant the period turns. - `JobChargeService.consumeFreeGrant` persists it on the next charge, under the pessimistic row lock that already makes the per-job free/paid split exact. One rule, both callers, so display and enforcement cannot drift onto separate schedules. A team that runs nothing for a month has nothing to write, and no job is needed to hand out the grant. ### One period definition "Per period" is `TeamBillingContext.periodStart`: the Stripe subscription's current period when subscribed, the calendar month otherwise. It was already the only period notion in the system, so the grant joined it rather than inventing its own: - `InstanceEntitlement.periodCapUnits` is enforced over the same window. - `localUsageService.currentPeriodUnsynced` already buckets a linked instance's local usage by the `periodStart` it reads from the same snapshot, and resets its counters on that boundary. For an un-subscribed team, the only kind the grant gates, that window is the calendar month, which is what the copy promises. The period rule stays in Java by choice, not necessity: SQL could reach the Stripe period through the sync engine, but restating the rule there would give it a second home to drift from. Hence a nullable column and no backfill in the migration — NULL already means "stale", so every existing team reads as owed the current period's grant. ### Refunds A refund landing after the period turned would have stacked last period's units on top of the fresh grant. `JobChargeService.restoreFreeGrant` now clamps the restore to one period's grant, taking the same row lock, and the bulk-increment `restoreFreeUnits` query is gone. Removing it also removed a `@Query` string that no test would have parsed before application startup. ### Copy and comments Every comment and user-facing string that asserted the lifetime model is corrected. The strings that changed (code defaults and `en-US` TOML updated together): | Key | Now reads | | --- | --- | | `portal.billing.walletMeter.title` / `titleWithRate` | "500 free credits every month, then $X per PDF" | | `portal.billing.walletMeter.capSuffix` / `barAria` | "of 500 free credits left this month" / "Free credits remaining" | | `payg.free.hero.capSuffix` | "of 500 free PDFs left this month" | | `plan.freeLimit.message` | "...this month. ... It resets next month, or keep the momentum going now..." | | `payg.signupRequired.body` | "500 free operations a month" | Main rewrote these keys to "500 free credits to start" while this branch was open. The merge keeps main's credits vocabulary and drops "to start", which asserts the one-time grant this branch removes and which main's own connect dialog already contradicts. Also fixed in passing: `testing/compose/payg/saas-seed.sql` still inserted `free_tier_units_per_cycle`, the pre-V19 column name, so that INSERT had been failing since the rename. ## How to test Backend: ```bash STIRLING_FLAVOR=saas ./gradlew :saas:test spotlessCheck ``` Frontend: ```bash task frontend:typecheck && task frontend:lint && task frontend:format:check ``` New coverage, 10 tests: - `TeamBillingServiceMoreTest` — a past-period stamp reads as a fresh grant, a current stamp reads the stored balance, an unstamped row reads as a fresh grant, the grant follows the Stripe window rather than the calendar month, plus the `remainingForPeriod` rule itself including a future stamp and null/negative balances. - `JobChargeServiceTest` — the first charge of a new period resets and re-stamps, an unstamped row resets, a zero-grant policy still advances the stamp, and a refund crossing a period boundary does not exceed the grant. Manually, against a team whose grant is spent: set `free_units_period_start` back a month (or leave it NULL) and the wallet, the sidebar meter and the entitlement gate should all show a full grant before any job runs. The first billable job should then draw from it and write the reset. Three tests fail on a local Windows run and pass in CI, on files this branch does not touch: `workbenchSession.test.ts`, `notificationActions.test.tsx`, and `:proprietary` `FolderIdentitiesTest.identityAgreesAcrossASymlinkedAliasOfTheDirectory`. Nothing to do here — noted so a local run does not look like a regression. ## Merge order The migration is additive, and Hibernate `ddl-auto=update` will add the column in a dev environment, so either order works locally. Beyond that the schema goes first: Stirling-Tools/Stirling-PDF-SaaS#327 targets `v3` (staging), so it needs to reach an environment before this lands there. |
||
|
|
798ba57f0b |
Reply to chat in the user's UI language (#7766)
# Description of Changes Pass a user browser lang ID to engine <img width="1400" height="900" alt="image" src="https://github.com/user-attachments/assets/7e8fc5c2-8881-4a74-b718-7f5cd350d457" /> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] Every comment I added says something the code does not ([guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/CODE_COMMENTS.md)) - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. |
||
|
|
42bdce155c |
Fix mobile scanner upload flow and fit it to one screen (#7684)
file mobile phone scanner UI issues when on http and scaling UI issues Ensuring that smaller screens dont cut off UI elements better handling of batch photos <img width="2104" height="8800" alt="montage_mobile-scanner" src="https://github.com/user-attachments/assets/b4dd114b-c54d-4101-8700-7307dbb0eee9" /> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. |
||
|
|
2cf355c5cd |
feat(editor): move admin settings onto TanStack Query (#7437)
# Description of Changes
## The problem
`useAdminSettings` backs all 18 admin config sections. Each section
fetched its own copy of its settings block, held it in hand-rolled
loading/saving state, and refetched manually after every save.
Three consequences:
- **Duplicate fetching.** Four AI tabs all read the `aiEngine` block.
Nothing was shared, so each open refetched it.
- **Duplicated wiring.** All 18 sections carried the same effect to
trigger the fetch, each one depending on a `fetchSettings` callback that
would have refetched on every render had it ever become unstable.
- **Console noise.** The hook made 11 `console.*` calls, four of them
`JSON.stringify(settings, null, 2)` on **every fetch and every save** —
admin configuration serialised into the console of every admin session.
Every save also ended with a hand-written `await fetchSettings()`.
Forget it in a new section and its pending badges silently go stale.
## The fix
The hook uses TanStack Query, keyed on `sectionName`, so sections
reading the same block share one fetch and one cache entry.
The fetch gate moved into the hook. Sections used to write:
```ts
const { settings, fetchSettings } = useAdminSettings({ sectionName: "legal" });
useEffect(() => {
if (loginEnabled) fetchSettings();
}, [loginEnabled, fetchSettings]);
```
and now write:
```ts
const { settings } = useAdminSettings({
sectionName: "legal",
enabled: loginEnabled,
});
```
Saving is a mutation that invalidates the section on success, so the
refetch is structural rather than something each section remembers.
The delta computation and the save transformer are unchanged — that is
domain logic, not fetching. `settings` is still an editable draft seeded
from the server response, so forms behave exactly as before.
## Why it is better
Measured against the previous implementation across identical scenarios.
`commits` counts committed renders.
| Scenario | Before | After |
|---|---|---|
| Open one section | 2 commits, 1 request | 2 commits, 1 request |
| Browse the four AI tabs | 8 commits, 4 requests | **5 commits, 1
request** |
| Edit and save | 4 commits, 2 requests | 4 commits, 2 requests |
Committed renders are equal or better everywhere; browsing the AI tabs
costs a quarter of the requests.
The diff reads +449 / −303, but that includes a test file for a hook
that had no tests:
| | Added | Removed | Net |
|---|---|---|---|
| Production code (21 files) | 154 | 303 | **−149** |
| Tests (1 file) | 295 | 0 | +295 |
The 18 section files account for −133 of that: each drops an effect, a
destructure and usually an import, and gains one `enabled:` line. The
hook itself goes from 234 to 180 lines. `console.*` calls go from 11 to
0.
## Caching
Settings inherit the client's 30s stale window rather than refetching on
every mount, which is where the request saving comes from.
Nothing inside a cached block is server-observed — the only live reads
in these sections, `/api/v1/ai/health` and the tessdata language list,
are separate calls outside this query. A block therefore only changes
when another admin writes it.
Two things bound the staleness:
- Sections already held a single snapshot for as long as the modal
stayed open, with no refetch on focus. 30s is shorter than that window,
not longer.
- `computeDelta` only emits fields whose draft differs from the baseline
it was seeded from, so a stale baseline cannot produce a collateral
write. The only race is two admins editing the same field, which is
unchanged. Saving invalidates, so acting refreshes to current values.
The blocks where a stale read would matter most — `security`, `premium`,
`database` — are set once at deployment and effectively never edited
concurrently. The block with the most cache reuse, `aiEngine`, is the
least consequential.
**Convention:** config blocks cache; observed state does not. A section
that displays live server state inside its settings block should
override `staleTime` locally.
## Testing
14 tests, covering the shared fetch, cache reuse across tab reopens, key
separation between blocks, the `enabled` gate, delta-only saves, the
empty-delta short circuit, post-save invalidation, pending-value
display, and draft reseeding.
Each was checked by breaking the implementation and confirming the suite
fails: per-consumer query keys, sending the whole draft instead of the
delta, dropping the post-save invalidate, reporting loaded while
disabled, skipping the empty-delta short circuit, and reverting the
stale window to zero.
`task frontend:check` green. Two unrelated tests fail on this branch —
`workbenchSession.test.ts` and `notificationActions.test.tsx` — and fail
identically on `main`.
## Follow-ups
The sections that fetch through services rather than this hook — Teams,
TeamDetails, People, roughly 2,600 lines — are unchanged. Between them
they share two reads (`getTeams` and `getUsers`, both used by all three)
and carry ten distinct write operations, with no test coverage today.
---
## Primer: mutations
`useQuery` is for reads. It caches, dedupes, and re-renders when data
arrives. `useMutation` is for writes, where none of that applies — a
write happens once, when the user asks.
```ts
const save = useMutation({
mutationFn: (body) => putAdminSection("legal", body),
onSuccess: () => queryClient.invalidateQueries({ queryKey }),
});
save.mutate(body); // fire and forget
await save.mutateAsync(body); // or await it
save.isPending; // disable the button
save.error; // show the failure
```
`isPending` and `error` replace the `useState` flag and
`try/catch/finally` you would otherwise write around every save.
After a write the cache holds stale data. Two ways to fix it:
| | What it does | Use when |
|---|---|---|
| `invalidateQueries` | Marks the data stale so it refetches | The
server may transform, queue or reject part of what you sent |
| `setQueryData` | Writes your value into the cache, no request | The
response tells you exactly what the server now holds |
**Invalidate by default. Use `setQueryData` only when the response is
authoritative.**
This hook has to invalidate: the server can queue a settings change
rather than applying it, returning it in a `_pending` block that the
form renders as a badge. Writing the local draft into the cache would
show a queued change as applied.
Most mutations are not like that. A "rename a team" write, where the
response is the new team, is a `setQueryData` case.
One gotcha: `mutate` does not throw, `mutateAsync` does. An awaited
`mutateAsync` without a `try/catch` is an unhandled rejection.
|
||
|
|
c57a2a45de |
Add v2 client-side PDF text editor (#6500)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. |
||
|
|
d30faf246b |
fix(billing): the paid tier is Team, and it is not unlimited users (#7730)
Copy only. No behaviour, no lookup keys, no licence semantics, no backend. ## Current state Every surface that sells the paid self-hosted tier offers **"unlimited seats"** for **"$99/server/mo"**, and the portal's free plan badges **"Unlimited users"** and **"SSO included"** as free-tier facts. ## Problem Both claims are now enforceably false. [#7492](https://github.com/Stirling-Tools/Stirling-PDF/pull/7492) makes the licence carry a real user cap, and [Stirling-PDF-SaaS#325](https://github.com/Stirling-Tools/Stirling-PDF-SaaS/pull/325) sells capacity in blocks of 100 users. An admin reading "unlimited seats" and then hitting a 409 at the invite screen is the worst version of this. The demo has already dropped both claims; ours were the last ones standing. ## Solution | Surface | Was | Now | |---|---|---| | Onboarding licence slide | "Stirling Server plan, **unlimited seats** … $99/server/mo" | "Stirling Team plan, **100 users** … $99/mo" | | Plan comparison table | `unlimitedUsers` = "Unlimited users" | `usersIncluded` = "100 users included" | | Plan card highlights | "Unlimited users" | "100 users included" | | Static plan section | `name: "Server"`, `maxUsers: "Unlimited users"` | `plan.team.name`, `plan.team.maxUsers` | | Upgrade banner | "Upgrade to Server Plan" / "unlimited users" | "Upgrade to the Team plan" / "100 users, SSO" | | Portal free plan | "Editor" + "SSO included" + "Unlimited users" | "Editor" + "Every PDF tool" + "Web, desktop & self-hosted" | The i18n keys are **renamed** (`unlimitedUsers` to `usersIncluded`) rather than just revalued, so the key name cannot outlive the claim. Also drops "per server" from `plan.licenseWarning` — we price a block of 100 users and count the provisioned roster, never nodes. And deletes the orphaned `[settings.planBilling.tier]` block: zero source references, and it described a retired model (50 credits/mo free, 500 included plus overage billing). ## Deliberately unchanged **"Processor" stays the name of the product surface.** The demo names each plan for its price tier (Editor = $0, Team = $99/mo, Credits = 1¢ each) while keeping Processor as the surface a plan unlocks. Renaming the surface here would conflate the two, so the plan-name split is left for the explicit plan catalogue. The free plan also gains no "500 free credits monthly" badge yet: that is true in the demo but not in our backend, which still grants a one-time lifetime pool. ## How to test Self-hosted, as an admin over the free user limit: Settings → Plan should offer the Team plan at "100 users included", and the onboarding licence slide should no longer promise unlimited seats. On the portal billing page, the free plan should read "Free" with no SSO or unlimited-users badge. Green locally: 4/4 i18n audits (missing, unused, structure, translation), 876 tests across 110 files, oxlint, prettier, and all four typecheck variants (core, proprietary, saas, portal). |
||
|
|
f6661a8f87 |
Failure action slots, resolve transition, and the bell that renders them (Review Flow PR 5a) (#7761)
Review Flow PR 5a — the first half of #7479, which stays open for reference until both halves land. This PR is the ranking and the bookkeeping; #7762 adds the retry handlers. Merging both reproduces #7479's diff byte-for-byte. ## What's added **The action slot model (backend).** `FailureActionSlot` ranks each of a kind's offers as its `RESOLUTION`, `SECONDARY` or `OVERFLOW`. `FailureKind` now declares placement per offer — the password-protected kind names `DECRYPT_AND_RETRY` as its resolution, `UNKNOWN` leads with a plain `RETRY` — and `FailureActionId` gains those two ids. The declarations are data; their client handlers arrive in the follow-up, so this build withholds them with a reason rather than rendering unwired buttons (the same forward-compatibility #7478 relied on). **A resolve transition.** `POST /api/v1/notifications/{id}/resolved` lets a client report a failure fixed. `NotificationSource.parse` turns a qualified notification id back into the source that owns it, and `FileRunEventService` folds the resolution into the incident rather than deleting it. **`viewerReviewsTeam` on the list response.** A member sees only rows whose document this browser holds — they can neither open nor fix anything else — while a team reviewer keeps every row. **The bell renders the ranking** (`promoteActions`): one primary button, at most one secondary, the rest in an overflow menu beside **Copy log**. The row's body is the kind's own sentence; the raw failure message moves into the menu. **Read state is a timestamp, not a row id.** `readThroughAt` replaces `lastSeenId`: when a resolved or dismissed row leaves the list, the rows below it stay read instead of re-lighting the badge. ## How to test Needs a proprietary or SaaS build with login enabled (`task dev:all`, sign in). 1. **Create a failure.** Add a password-protected PDF to the editor and choose **Skip for now**; the upload's policy run fails on it. 2. **Open the bell.** The row reads the kind's sentence, not a stack trace. Its primary button is **View file** — the server offers Decrypt and retry as the resolution, but this build withholds it (handler lands in the follow-up), so the best renderable offer is promoted instead. 3. **Open the row's ⋯ menu.** View in processor and Dismiss sit there, along with **Copy log**, which copies the raw message. 4. **Check the read marker survives a departure.** With two failures, open the bell (badge clears), dismiss the newer row, and refresh: the badge stays dark. On main, the marker held the departed row's id and the older row re-read as unread. 5. **Member visibility.** As a plain member, a failure recorded from another browser does not appear in the bell; as a team reviewer it does. 6. **Resolve endpoint.** `POST /api/v1/notifications/failure-{eventId}/resolved` as the owner removes the row on the next poll; `NotificationResolveTest` pins refusal for a non-owner, an unknown id, and a foreign prefix. ## Migration None. |
||
|
|
ceeec53df4 |
Let a pipeline run on the editor, on upload or export (#7581)
Redesigns the policies system so that the backend has an understanding of policies running over the Editor. The Editor is not set up as a source for the backend because the backend can't actively get files from it, they come in via the frontend sending them to the backend, so instead pipelines have a specific editor key in them to encode whether the pipeline is triggered on file upload/export in the editor. Also make a big effort in the frontend code towards genericising policy running. Previously, there was specific support in the main policy executor for each policy that it had to run, which was not going to be appropriate long-term, especially when users can run any pipeline in the editor. There's more work needed here for me to really be happy with it but this PR is plenty large on its own and moves it in the right direction. All of the above was required to allow arbitrary user pipelines to run in the editor. This PR makes it so that the user can select Editor as a source in the pipeline creator, along with whether it should run on upload or export. <img width="1437" height="506" alt="image" src="https://github.com/user-attachments/assets/b2d176a1-185c-480b-9916-abdd1447d8e1" /> --------- Co-authored-by: James Brunton <james@stirlingpdf.com> |
||
|
|
31d52d4c32 |
Connect flow for self-hosted account linking, and the triggers that drive it (#7415)
Replaces the bare account-link login box with a guided Connect flow, and wires up the triggers that actually put it in front of someone. ## Top bar <img width="1580" height="422" alt="image" src="https://github.com/user-attachments/assets/719e12fc-121a-4caa-bc72-124c5167b011" /> ## The modal Three steps on the portal's own `FlowModal` + `StepModalHeader`, the shells procurement and prepay already wear: 1. **What you unlock** — six benefits as a plain list. <img width="817" height="503" alt="image" src="https://github.com/user-attachments/assets/4644ddd2-6181-44e1-9be9-7a961972195d" /> 2. **Sign in** — the existing `SupabaseLoginForm`, reseated. <img width="880" height="930" alt="image" src="https://github.com/user-attachments/assets/fc66cbbb-9f98-40a4-9daa-4f2447713f39" /> 3. **Connected** — confirms, then deep links into Users, Pipelines and Policies. <img width="876" height="752" alt="image" src="https://github.com/user-attachments/assets/28358e4d-a44f-4118-a8ae-8275984ebd00" /> Re-auth stays a single step with no pitch and no success screen. ## The triggers **`LinkGate` stops being dead code.** It was built as the drop-anywhere "link to unlock" wrapper and was imported by nothing. It is now a blocking empty state that replaces the feature it guards, wired into Pipelines, Policies, Users, Sources and Integrations. **Scoped to creating and editing, never viewing.** Existing pipelines, policies, sources and connections keep listing and running, so upgrading an unlinked instance cannot take away something that already works. The clicks that would open a builder or a create modal ask for the connection first, which is the moment an admin has already declared intent. ## Capability signal `accountLinkAvailable` on `/api/v1/config/app-config`. Gating needs two facts: whether the instance is linked (`LinkContext`) and whether it *could* be (this flag). The account-link endpoints 404 when the feature flag is off, which the client cannot distinguish from "not linked yet" — so gating on link state alone would lock all five views on every default install with no way out. `useConnectGate` holds that decision in one place and shares the app-config query key, so it costs no extra request. Read from the environment rather than `AccountLinkProperties` because `:core` cannot depend on `:proprietary`. |
||
|
|
d55d8acbfa |
fix(portal): keep the processor's cache across a trip to the editor (#7729)
# Description of Changes
## The problem
The portal's query client was created per mount:
```ts
const [queryClient] = useState(createPortalQueryClient);
```
The portal is a route (`/processor/*`, a lazy element), and the switch
to the editor is a client-side `navigate()`. So leaving the processor
unmounts `PortalApp`, the client goes with the component, and the cache
goes with the client. Coming back refetches everything, whether or not
anything changed: four requests for the Users page alone (roster,
grants, teams, auth config), and 21 `useQuery` sites across the portal.
The editor's client sits above the router in `AppProviders` and survives
the same trip. The round trip only ever cost in one direction.
## The fix
The module already kept the instance in a module-level slot so
`tryGetPortalQueryClient()` could find it. It just replaced it on every
mount instead of reusing it, so the change is to create it lazily and
hand out the same one:
```ts
export function getPortalQueryClient(): QueryClient {
current ??= new QueryClient({ defaultOptions: { queries: baseQueryOptions } });
return current;
}
```
Still a separate instance from the editor's. The two namespace their
keys apart (`["portal", ...]` against `["editor", ...]`) and invalidate
independently, which this does not change.
## What this does not do
`gcTime` is 5 minutes, from the shared `baseQueryOptions`. An entry with
no observer is still collected on that timer, so this warms a quick trip
to the editor and back, not a return after a long editing session.
Raising the portal's `gcTime` is a separate decision and is not made
here.
## Why it is safe
**Signing out.** A cache that outlives a mount must not outlive a
session, because the portal's holds the admin roster, emails and roles.
Logout goes through `window.location.assign`, a full page load, so the
whole JS context is discarded and no cache can survive it. Nothing in
the codebase calls `queryClient.clear()` on sign-out, and nothing needs
to. If logout ever becomes a client-side navigation, this needs an
explicit reset, and `resetPortalQueryClient()` is the hook for it.
**The one caller of the null check.** `resolveTeam` in
`saas/portal/usersBackend.ts` uses `tryGetPortalQueryClient()` and falls
back to a direct fetch when there is no client, which its comment
describes as the unit-test path; the cache path is preferred because it
honours both `staleTime` and invalidation. A longer-lived client means
the preferred path is taken more often, not less.
## Testing
Three tests in `queryClient.test.tsx`, and the first two fail if the
client goes back to being created per call:
| | |
|---|---|
| A remount is served from cache rather than refetching | the behaviour
this changes |
| Every caller gets the same instance | the mechanism |
| No client is reported until the portal first mounts | the contract
`resolveTeam` reads |
The three existing portal caching suites called the factory expecting a
fresh client per case. They now call `resetPortalQueryClient()` in a
`beforeEach`, which is what keeps `sharing.test.tsx`'s "a later screen
refetches nothing" case honest rather than passing on a leaked cache.
`task frontend:check` passes typecheck, lint and oxfmt, and 2402 of 2404
editor tests. The two failures, `workbenchSession.test.ts` and
`notificationActions.test.tsx`, are untouched here and fail the same way
on `main`.
|
||
|
|
96207a7304 |
build(deps): bump @tanstack/react-query from 5.101.4 to 5.102.0 in /frontend in the tanstack group across 1 directory (#7749)
Signed-off-by: dependabot[bot] <support@github.com> |
||
|
|
d93049db9f |
build(deps): bump log from 0.4.33 to 0.4.34 in /frontend/editor/src-tauri (#7747)
Signed-off-by: dependabot[bot] <support@github.com> |
||
|
|
1bb6961414 |
Update Frontend 3rd Party Licenses (#7738)
Auto-generated by stirlingbot[bot] This PR updates the frontend license report based on changes to package.json dependencies. Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com> Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> |
||
|
|
0b7b4e02c2 |
chore(crop): Remove invalid crop area message and related validation logic (#7160)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> |
||
|
|
74be5bf0ad |
fix(forms): Fix checkbox export values and wide dropdown options (#7288)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> |
||
|
|
c5da4177c4 |
refactor(ui): improve button layouts and modal sizing of formFill (#7509)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> |
||
|
|
8bdd00b2fa |
build(deps): bump @tanstack/react-virtual from 3.13.23 to 3.14.10 in /frontend in the tanstack group across 1 directory (#7605)
Bumps the tanstack group with 1 update in the /frontend directory: [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual). Updates `@tanstack/react-virtual` from 3.13.23 to 3.14.10 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/TanStack/virtual/releases">@tanstack/react-virtual's releases</a>.</em></p> <blockquote> <h2><code>@tanstack/react-virtual</code><a href="https://github.com/3"><code>@3</code></a>.14.10</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/a0a411e06f7334a063422de35d59b12b264b3573"><code>a0a411e</code></a>, <a href="https://github.com/TanStack/virtual/commit/d2cf98beea1696c7187c06b57c9e724d1957963c"><code>d2cf98b</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.8</li> </ul> </li> </ul> <h2><code>@tanstack/react-virtual</code><a href="https://github.com/3"><code>@3</code></a>.14.9</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/a5417b4b0d3c82876747bb9635db7239c28d3e44"><code>a5417b4</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.7</li> </ul> </li> </ul> <h2><code>@tanstack/react-virtual</code><a href="https://github.com/3"><code>@3</code></a>.14.8</h2> <h3>Patch Changes</h3> <ul> <li> <p><a href="https://redirect.github.com/TanStack/virtual/pull/1237">#1237</a> <a href="https://github.com/TanStack/virtual/commit/aa536e7746a88d9f55ca8a4b50d2f548a888fea6"><code>aa536e7</code></a> - Fix a gap at the top of the list after an end-anchored prepend in <code>directDomUpdates</code> mode. The prepend grows the total size and bumps <code>scrollOffset</code> to the new bottom in the same pass, but the size container's height was written <em>after</em> <code>_willUpdate</code> synced the scroll position — so the browser clamped the <code>scrollTop</code> write to the stale (shorter) <code>scrollHeight</code>, leaving whitespace at the top until the next scroll. The container is now grown before the scroll sync. Only affected <code>directDomUpdates</code> mode (React-rendered sizers receive their height during render).</p> </li> <li> <p>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/7ae32b55887fd044a48c788546cd940279b338e0"><code>7ae32b5</code></a>]:</p> <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.6</li> </ul> </li> </ul> <h2><code>@tanstack/react-virtual</code><a href="https://github.com/3"><code>@3</code></a>.14.7</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/1e3b908705e04e45be2615f2277580cb09f5cdef"><code>1e3b908</code></a>, <a href="https://github.com/TanStack/virtual/commit/7dcfc07b877479697124157d3124c09537b87a75"><code>7dcfc07</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.5</li> </ul> </li> </ul> <h2><code>@tanstack/react-virtual</code><a href="https://github.com/3"><code>@3</code></a>.14.6</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/6cbecd887df56faaee3b6a81a1aae8049de0671e"><code>6cbecd8</code></a>, <a href="https://github.com/TanStack/virtual/commit/d49cc526fe248be7b5ad97ec6ac814db8271b0d0"><code>d49cc52</code></a>, <a href="https://github.com/TanStack/virtual/commit/cf7834daade953fea5dfd2ab5685c15771ca300a"><code>cf7834d</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.4</li> </ul> </li> </ul> <h2><code>@tanstack/react-virtual</code><a href="https://github.com/3"><code>@3</code></a>.14.5</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/767ead46e4fab761fd6e15bcf281486042723152"><code>767ead4</code></a>, <a href="https://github.com/TanStack/virtual/commit/bc8643b7579e10e512654f58269de13d98b48781"><code>bc8643b</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.3</li> </ul> </li> </ul> <h2><code>@tanstack/react-virtual</code><a href="https://github.com/3"><code>@3</code></a>.14.4</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/b04f9ee48f0812e89156c1dac1fa58277cc32464"><code>b04f9ee</code></a>, <a href="https://github.com/TanStack/virtual/commit/37be28427ba52399ce8884e0006933e83f2645e9"><code>37be284</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.2</li> </ul> </li> </ul> <h2><code>@tanstack/react-virtual</code><a href="https://github.com/3"><code>@3</code></a>.14.3</h2> <h3>Patch Changes</h3> <ul> <li> <p><a href="https://redirect.github.com/TanStack/virtual/pull/1201">#1201</a> <a href="https://github.com/TanStack/virtual/commit/2ba5eb60f108f4ba9b2bd9570bbd41f9ce618438"><code>2ba5eb6</code></a> - Make <code>directDomUpdates</code> a no-op for direct DOM writes when <code>containerRef</code> is omitted. Previously the virtualizer still wrote item positions while never sizing the container (a broken half-state). Now omitting <code>containerRef</code> skips all direct writes while still skipping re-renders, letting consumers own the DOM updates themselves (e.g. in <code>onChange</code>).</p> </li> <li> <p>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/ef69ea31738caa2819142e922efa03d3c408e25c"><code>ef69ea3</code></a>]:</p> </li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md">@tanstack/react-virtual's changelog</a>.</em></p> <blockquote> <h2>3.14.10</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/a0a411e06f7334a063422de35d59b12b264b3573"><code>a0a411e</code></a>, <a href="https://github.com/TanStack/virtual/commit/d2cf98beea1696c7187c06b57c9e724d1957963c"><code>d2cf98b</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.8</li> </ul> </li> </ul> <h2>3.14.9</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/a5417b4b0d3c82876747bb9635db7239c28d3e44"><code>a5417b4</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.7</li> </ul> </li> </ul> <h2>3.14.8</h2> <h3>Patch Changes</h3> <ul> <li> <p><a href="https://redirect.github.com/TanStack/virtual/pull/1237">#1237</a> <a href="https://github.com/TanStack/virtual/commit/aa536e7746a88d9f55ca8a4b50d2f548a888fea6"><code>aa536e7</code></a> - Fix a gap at the top of the list after an end-anchored prepend in <code>directDomUpdates</code> mode. The prepend grows the total size and bumps <code>scrollOffset</code> to the new bottom in the same pass, but the size container's height was written <em>after</em> <code>_willUpdate</code> synced the scroll position — so the browser clamped the <code>scrollTop</code> write to the stale (shorter) <code>scrollHeight</code>, leaving whitespace at the top until the next scroll. The container is now grown before the scroll sync. Only affected <code>directDomUpdates</code> mode (React-rendered sizers receive their height during render).</p> </li> <li> <p>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/7ae32b55887fd044a48c788546cd940279b338e0"><code>7ae32b5</code></a>]:</p> <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.6</li> </ul> </li> </ul> <h2>3.14.7</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/1e3b908705e04e45be2615f2277580cb09f5cdef"><code>1e3b908</code></a>, <a href="https://github.com/TanStack/virtual/commit/7dcfc07b877479697124157d3124c09537b87a75"><code>7dcfc07</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.5</li> </ul> </li> </ul> <h2>3.14.6</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/6cbecd887df56faaee3b6a81a1aae8049de0671e"><code>6cbecd8</code></a>, <a href="https://github.com/TanStack/virtual/commit/d49cc526fe248be7b5ad97ec6ac814db8271b0d0"><code>d49cc52</code></a>, <a href="https://github.com/TanStack/virtual/commit/cf7834daade953fea5dfd2ab5685c15771ca300a"><code>cf7834d</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.4</li> </ul> </li> </ul> <h2>3.14.5</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/767ead46e4fab761fd6e15bcf281486042723152"><code>767ead4</code></a>, <a href="https://github.com/TanStack/virtual/commit/bc8643b7579e10e512654f58269de13d98b48781"><code>bc8643b</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.3</li> </ul> </li> </ul> <h2>3.14.4</h2> <h3>Patch Changes</h3> <ul> <li>Updated dependencies [<a href="https://github.com/TanStack/virtual/commit/b04f9ee48f0812e89156c1dac1fa58277cc32464"><code>b04f9ee</code></a>, <a href="https://github.com/TanStack/virtual/commit/37be28427ba52399ce8884e0006933e83f2645e9"><code>37be284</code></a>]: <ul> <li><code>@tanstack/virtual-core</code><a href="https://github.com/3"><code>@3</code></a>.17.2</li> </ul> </li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/TanStack/virtual/commit/e9874f033c74afd3251eeb9f3e60b2530cc7ae88"><code>e9874f0</code></a> ci: Version Packages (<a href="https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual/issues/1247">#1247</a>)</li> <li><a href="https://github.com/TanStack/virtual/commit/b4a76cac25ef7e334c180ceb8c0d859b7c91ab09"><code>b4a76ca</code></a> fix(marko-virtual): consolidate Marko e2e into one in-package app, fix test (...</li> <li><a href="https://github.com/TanStack/virtual/commit/deca524a9b2ed29a8a23001389580de10f8002db"><code>deca524</code></a> ci: Version Packages (<a href="https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual/issues/1240">#1240</a>)</li> <li><a href="https://github.com/TanStack/virtual/commit/32b2f2b412739015a47da1463fe2749456cdc4e9"><code>32b2f2b</code></a> ci: Version Packages (<a href="https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual/issues/1238">#1238</a>)</li> <li><a href="https://github.com/TanStack/virtual/commit/aa536e7746a88d9f55ca8a4b50d2f548a888fea6"><code>aa536e7</code></a> fix(react-virtual): grow size container before scroll sync on end-anchored pr...</li> <li><a href="https://github.com/TanStack/virtual/commit/87f689a5c67ee1ed8db1e6754021a6b6b41c8550"><code>87f689a</code></a> ci: Version Packages (<a href="https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual/issues/1231">#1231</a>)</li> <li><a href="https://github.com/TanStack/virtual/commit/ba5c47a93f597f8370bc9e0119d505551c962a09"><code>ba5c47a</code></a> feat(angular-virtual): add chat example and require Angular 20 (<a href="https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual/issues/1228">#1228</a>)</li> <li><a href="https://github.com/TanStack/virtual/commit/e2cb096862f5b74aa586957eae207b39999cb654"><code>e2cb096</code></a> ci: Version Packages (<a href="https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual/issues/1225">#1225</a>)</li> <li><a href="https://github.com/TanStack/virtual/commit/d49cc526fe248be7b5ad97ec6ac814db8271b0d0"><code>d49cc52</code></a> fix(virtual-core): invalidate measurements when gap option changes (<a href="https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual/issues/1223">#1223</a>)</li> <li><a href="https://github.com/TanStack/virtual/commit/151e9f47abd4ef2d3b11936c04be8908e6bd0607"><code>151e9f4</code></a> ci: Version Packages (<a href="https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual/issues/1213">#1213</a>)</li> <li>Additional commits viewable in <a href="https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.14.10/packages/react-virtual">compare view</a></li> </ul> </details> <br /> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
3718af45ff |
build(deps): bump the mui group across 1 directory with 2 updates (#7602)
Bumps the mui group with 1 update in the /frontend directory: [@mui/icons-material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material). Updates `@mui/icons-material` from 9.2.0 to 9.3.1 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/mui/material-ui/releases">@mui/icons-material's releases</a>.</em></p> <blockquote> <h2>v9.3.1</h2> <p>A big thanks to the 4 contributors who made this release possible.</p> <h3><code>@mui/material@9.3.1</code></h3> <ul> <li>[transitions] Prevent exit transitions from getting stuck (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48881">#48881</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> </ul> <h3><code>@mui/codemod@9.3.1</code></h3> <ul> <li>Include transforms in published package (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48934">#48934</a>) <a href="https://github.com/brijeshb42"><code>@brijeshb42</code></a></li> </ul> <h3>Core</h3> <ul> <li>[blog] Clarify early bird renewal discount scope (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48906">#48906</a>) <a href="https://github.com/DanailH"><code>@DanailH</code></a></li> <li>[test][pagination] Add more unit tests (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48927">#48927</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <p>All contributors of this release in alphabetical order: <a href="https://github.com/brijeshb42"><code>@brijeshb42</code></a>, <a href="https://github.com/DanailH"><code>@DanailH</code></a>, <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a>, <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></p> <h2>v9.3.0</h2> <p>A big thanks to the 18 contributors who made this release possible. Here are some highlights ✨:</p> <ul> <li>♿️ Keyboard navigation in the <a href="https://mui.com/material-ui/react-toggle-button/">Toggle Button Group</a> now follows the roving tabindex pattern.</li> <li>♿️ The <a href="https://mui.com/material-ui/react-autocomplete/">Autocomplete</a> announces its loading and no options messages through a new <code>status</code> slot.</li> </ul> <h3><code>@mui/material@9.3.0</code></h3> <ul> <li>[autocomplete] Wrap the no results and loading messages in an aria live region (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48690">#48690</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[buttongroup] Respect global disableRipple / disableFocusRipple in grouped buttons (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48762">#48762</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[checkbox][radio] Respect global disableRipple from MuiButtonBase defaultProps (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48795">#48795</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[formcontrollabel] Add missing <code>labelPlacementEnd</code> class (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48843">#48843</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[listitembutton] Fix typos in component code (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48868">#48868</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[menuitem] Add <code>aria-checked</code> for checkbox and radio menu items (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48651">#48651</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[modal] Replace custom findIndexOf with findIndex (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48827">#48827</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[modal][dialog] Fix scrollbar compensation in Shadow DOM (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48826">#48826</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[select] Fix endAdornment overlapping the open indicator (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48723">#48723</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[tablepagination] Add focus style to default InputBase used in Select (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48871">#48871</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[togglebuttongroup] Add roving tabindex keyboard navigation (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48849">#48849</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <h3><code>@mui/system@9.3.0</code></h3> <ul> <li>Prevent prototype pollution in cssVarsParser (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48822">#48822</a>) <a href="https://github.com/Janpot"><code>@Janpot</code></a></li> </ul> <h3><code>@mui/codemod@9.3.0</code></h3> <ul> <li>Don't leak state between files in v5.0.0/path-imports (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48797">#48797</a>) <a href="https://github.com/manbearwiz"><code>@manbearwiz</code></a></li> <li>Remove use of eval() (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48701">#48701</a>) <a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a></li> <li>Transform all style exports in <code>v5.0.0/path-imports</code> codemod (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48800">#48800</a>) <a href="https://github.com/manbearwiz"><code>@manbearwiz</code></a></li> </ul> <h3>Docs</h3> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/mui/material-ui/blob/master/CHANGELOG.md">@mui/icons-material's changelog</a>.</em></p> <blockquote> <h2>9.3.1</h2> <!-- raw HTML omitted --> <p><em>Aug 6, 2026</em></p> <p>A big thanks to the 4 contributors who made this release possible.</p> <h3><code>@mui/material@9.3.1</code></h3> <ul> <li>[transitions] Prevent exit transitions from getting stuck (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48881">#48881</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> </ul> <h3><code>@mui/codemod@9.3.1</code></h3> <ul> <li>Include transforms in published package (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48934">#48934</a>) <a href="https://github.com/brijeshb42"><code>@brijeshb42</code></a></li> </ul> <h3>Core</h3> <ul> <li>[blog] Clarify early bird renewal discount scope (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48906">#48906</a>) <a href="https://github.com/DanailH"><code>@DanailH</code></a></li> <li>[test][pagination] Add more unit tests (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48927">#48927</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <p>All contributors of this release in alphabetical order: <a href="https://github.com/brijeshb42"><code>@brijeshb42</code></a>, <a href="https://github.com/DanailH"><code>@DanailH</code></a>, <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a>, <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></p> <h2>9.3.0</h2> <!-- raw HTML omitted --> <p><em>Aug 4, 2026</em></p> <p>A big thanks to the 18 contributors who made this release possible. Here are some highlights ✨:</p> <ul> <li>♿️ Keyboard navigation in the <a href="https://mui.com/material-ui/react-toggle-button/">Toggle Button Group</a> now follows the roving tabindex pattern.</li> <li>♿️ The <a href="https://mui.com/material-ui/react-autocomplete/">Autocomplete</a> announces its loading and no options messages through a new <code>status</code> slot.</li> </ul> <h3><code>@mui/material@9.3.0</code></h3> <ul> <li>[autocomplete] Wrap the no results and loading messages in an aria live region (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48690">#48690</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[buttongroup] Respect global disableRipple / disableFocusRipple in grouped buttons (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48762">#48762</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[checkbox][radio] Respect global disableRipple from MuiButtonBase defaultProps (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48795">#48795</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[formcontrollabel] Add missing <code>labelPlacementEnd</code> class (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48843">#48843</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[listitembutton] Fix typos in component code (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48868">#48868</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[menuitem] Add <code>aria-checked</code> for checkbox and radio menu items (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48651">#48651</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[modal] Replace custom findIndexOf with findIndex (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48827">#48827</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[modal][dialog] Fix scrollbar compensation in Shadow DOM (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48826">#48826</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[select] Fix endAdornment overlapping the open indicator (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48723">#48723</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[tablepagination] Add focus style to default InputBase used in Select (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48871">#48871</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[togglebuttongroup] Add roving tabindex keyboard navigation (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48849">#48849</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <h3><code>@mui/system@9.3.0</code></h3> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/mui/material-ui/commit/5b91ac75008dbd43286a20ef87847042cc7a44ca"><code>5b91ac7</code></a> [release] v9.3.1 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48935">#48935</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/da37c088786eead9c7ddaffe0798ec692ece0a11"><code>da37c08</code></a> v9.3.0 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48909">#48909</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/2e38eb7f8f77152f4bb4047169cce332da420cb9"><code>2e38eb7</code></a> Bump chalk to 6.0.0 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48902">#48902</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/20fe2b6aa86965e3f1e2e5b0a82e2ed38f753ffd"><code>20fe2b6</code></a> Bump code-infra:devDependencies (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48891">#48891</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/a900cd7d7e66e37248ec0ae8da44d588d0577aa3"><code>a900cd7</code></a> Bump react monorepo to 19.2.8 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48858">#48858</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/8cbc3ce36fb59cd6f4e3a3e925fb247b6c4b971b"><code>8cbc3ce</code></a> Bump code-infra:devDependencies (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48830">#48830</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/a5faab53e647b92b5efb8ea26ce1ae758778736e"><code>a5faab5</code></a> Bump react monorepo (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48770">#48770</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/ca10194ad116e97fa4ecfc95ca09421bbbb6e2a7"><code>ca10194</code></a> Bump code-infra:devDependencies (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48767">#48767</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/620c9e95e8e57d99d91524f6f60de00d194184f1"><code>620c9e9</code></a> Bump babel monorepo to ^7.29.7 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-icons-material/issues/48766">#48766</a>)</li> <li>See full diff in <a href="https://github.com/mui/material-ui/commits/v9.3.1/packages/mui-icons-material">compare view</a></li> </ul> </details> <br /> Updates `@mui/material` from 9.2.0 to 9.3.1 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/mui/material-ui/releases">@mui/material's releases</a>.</em></p> <blockquote> <h2>v9.3.1</h2> <p>A big thanks to the 4 contributors who made this release possible.</p> <h3><code>@mui/material@9.3.1</code></h3> <ul> <li>[transitions] Prevent exit transitions from getting stuck (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48881">#48881</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> </ul> <h3><code>@mui/codemod@9.3.1</code></h3> <ul> <li>Include transforms in published package (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48934">#48934</a>) <a href="https://github.com/brijeshb42"><code>@brijeshb42</code></a></li> </ul> <h3>Core</h3> <ul> <li>[blog] Clarify early bird renewal discount scope (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48906">#48906</a>) <a href="https://github.com/DanailH"><code>@DanailH</code></a></li> <li>[test][pagination] Add more unit tests (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48927">#48927</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <p>All contributors of this release in alphabetical order: <a href="https://github.com/brijeshb42"><code>@brijeshb42</code></a>, <a href="https://github.com/DanailH"><code>@DanailH</code></a>, <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a>, <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></p> <h2>v9.3.0</h2> <p>A big thanks to the 18 contributors who made this release possible. Here are some highlights ✨:</p> <ul> <li>♿️ Keyboard navigation in the <a href="https://mui.com/material-ui/react-toggle-button/">Toggle Button Group</a> now follows the roving tabindex pattern.</li> <li>♿️ The <a href="https://mui.com/material-ui/react-autocomplete/">Autocomplete</a> announces its loading and no options messages through a new <code>status</code> slot.</li> </ul> <h3><code>@mui/material@9.3.0</code></h3> <ul> <li>[autocomplete] Wrap the no results and loading messages in an aria live region (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48690">#48690</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[buttongroup] Respect global disableRipple / disableFocusRipple in grouped buttons (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48762">#48762</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[checkbox][radio] Respect global disableRipple from MuiButtonBase defaultProps (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48795">#48795</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[formcontrollabel] Add missing <code>labelPlacementEnd</code> class (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48843">#48843</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[listitembutton] Fix typos in component code (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48868">#48868</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[menuitem] Add <code>aria-checked</code> for checkbox and radio menu items (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48651">#48651</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[modal] Replace custom findIndexOf with findIndex (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48827">#48827</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[modal][dialog] Fix scrollbar compensation in Shadow DOM (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48826">#48826</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[select] Fix endAdornment overlapping the open indicator (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48723">#48723</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[tablepagination] Add focus style to default InputBase used in Select (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48871">#48871</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[togglebuttongroup] Add roving tabindex keyboard navigation (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48849">#48849</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <h3><code>@mui/system@9.3.0</code></h3> <ul> <li>Prevent prototype pollution in cssVarsParser (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48822">#48822</a>) <a href="https://github.com/Janpot"><code>@Janpot</code></a></li> </ul> <h3><code>@mui/codemod@9.3.0</code></h3> <ul> <li>Don't leak state between files in v5.0.0/path-imports (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48797">#48797</a>) <a href="https://github.com/manbearwiz"><code>@manbearwiz</code></a></li> <li>Remove use of eval() (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48701">#48701</a>) <a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a></li> <li>Transform all style exports in <code>v5.0.0/path-imports</code> codemod (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48800">#48800</a>) <a href="https://github.com/manbearwiz"><code>@manbearwiz</code></a></li> </ul> <h3>Docs</h3> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/mui/material-ui/blob/master/CHANGELOG.md">@mui/material's changelog</a>.</em></p> <blockquote> <h2>9.3.1</h2> <!-- raw HTML omitted --> <p><em>Aug 6, 2026</em></p> <p>A big thanks to the 4 contributors who made this release possible.</p> <h3><code>@mui/material@9.3.1</code></h3> <ul> <li>[transitions] Prevent exit transitions from getting stuck (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48881">#48881</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> </ul> <h3><code>@mui/codemod@9.3.1</code></h3> <ul> <li>Include transforms in published package (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48934">#48934</a>) <a href="https://github.com/brijeshb42"><code>@brijeshb42</code></a></li> </ul> <h3>Core</h3> <ul> <li>[blog] Clarify early bird renewal discount scope (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48906">#48906</a>) <a href="https://github.com/DanailH"><code>@DanailH</code></a></li> <li>[test][pagination] Add more unit tests (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48927">#48927</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <p>All contributors of this release in alphabetical order: <a href="https://github.com/brijeshb42"><code>@brijeshb42</code></a>, <a href="https://github.com/DanailH"><code>@DanailH</code></a>, <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a>, <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></p> <h2>9.3.0</h2> <!-- raw HTML omitted --> <p><em>Aug 4, 2026</em></p> <p>A big thanks to the 18 contributors who made this release possible. Here are some highlights ✨:</p> <ul> <li>♿️ Keyboard navigation in the <a href="https://mui.com/material-ui/react-toggle-button/">Toggle Button Group</a> now follows the roving tabindex pattern.</li> <li>♿️ The <a href="https://mui.com/material-ui/react-autocomplete/">Autocomplete</a> announces its loading and no options messages through a new <code>status</code> slot.</li> </ul> <h3><code>@mui/material@9.3.0</code></h3> <ul> <li>[autocomplete] Wrap the no results and loading messages in an aria live region (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48690">#48690</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[buttongroup] Respect global disableRipple / disableFocusRipple in grouped buttons (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48762">#48762</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[checkbox][radio] Respect global disableRipple from MuiButtonBase defaultProps (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48795">#48795</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[formcontrollabel] Add missing <code>labelPlacementEnd</code> class (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48843">#48843</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[listitembutton] Fix typos in component code (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48868">#48868</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[menuitem] Add <code>aria-checked</code> for checkbox and radio menu items (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48651">#48651</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[modal] Replace custom findIndexOf with findIndex (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48827">#48827</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[modal][dialog] Fix scrollbar compensation in Shadow DOM (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48826">#48826</a>) <a href="https://github.com/ZeeshanTamboli"><code>@ZeeshanTamboli</code></a></li> <li>[select] Fix endAdornment overlapping the open indicator (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48723">#48723</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[tablepagination] Add focus style to default InputBase used in Select (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48871">#48871</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[togglebuttongroup] Add roving tabindex keyboard navigation (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48849">#48849</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <h3><code>@mui/system@9.3.0</code></h3> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/mui/material-ui/commit/5b91ac75008dbd43286a20ef87847042cc7a44ca"><code>5b91ac7</code></a> [release] v9.3.1 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48935">#48935</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/a13824f0bae9214534d2a35740802d15d711a373"><code>a13824f</code></a> [transitions] Prevent exit transitions from getting stuck (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48881">#48881</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/54e1993311bbc3e61fe1684dfcf3fed784bfdcd8"><code>54e1993</code></a> [test][pagination] Add more unit tests (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48927">#48927</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/da37c088786eead9c7ddaffe0798ec692ece0a11"><code>da37c08</code></a> v9.3.0 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48909">#48909</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/0bb025974d5eca56f626121cd14a21b77e71e982"><code>0bb0259</code></a> [tablepagination] Add focus style to default InputBase used in Select (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48871">#48871</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/20fe2b6aa86965e3f1e2e5b0a82e2ed38f753ffd"><code>20fe2b6</code></a> Bump code-infra:devDependencies (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48891">#48891</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/7fb01101f45fb72fdbeb3d826984030583e71ea9"><code>7fb0110</code></a> [togglebuttongroup] Add roving tabindex keyboard navigation (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48849">#48849</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/3dfeb20bb65e598f90200ef1fc1429d02fa8c4b7"><code>3dfeb20</code></a> [internal] Fix typos in ListItemButton component code (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48868">#48868</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/27f46fa1acabd6d70898b40544f20000bea0149d"><code>27f46fa</code></a> Bump <code>@types/sinon</code> to 22.0.0 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48865">#48865</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/a900cd7d7e66e37248ec0ae8da44d588d0577aa3"><code>a900cd7</code></a> Bump react monorepo to 19.2.8 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48858">#48858</a>)</li> <li>Additional commits viewable in <a href="https://github.com/mui/material-ui/commits/v9.3.1/packages/mui-material">compare view</a></li> </ul> </details> <br /> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
993adaa3cd |
build(deps-dev): bump @iconify-json/material-symbols from 1.2.83 to 1.2.89 in /frontend in the iconify group across 1 directory (#7641)
Bumps the iconify group with 1 update in the /frontend directory: [@iconify-json/material-symbols](https://github.com/iconify/icon-sets). Updates `@iconify-json/material-symbols` from 1.2.83 to 1.2.89 <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/iconify/icon-sets/commits">compare view</a></li> </ul> </details> <br /> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
ead8a536d2 |
feat(editor): move signing sessions onto TanStack Query (#7436)
# Description of Changes Step 4 of the TanStack Query rollout, and the first of the polling hooks. Follows #7264, #7283, #7285. ## The problem `useSigningSessions` hand-rolled its own fetch, loading state and `setInterval`. Two consequences: - **A raw `setInterval` keeps polling a hidden tab.** Browsers throttle background timers, they do not stop them, so a backgrounded editor with Shared Sign open keeps hitting both endpoints for as long as it is open. - **No tests.** The hook had none, and its quietest behaviour (below) is the easiest thing to break without noticing. ## End state One query behind `qk.signingSessions()`, with the polling lifecycle handed to the library: - Polling stops while the tab is hidden, and refetches on return rather than leaving data up to a full interval stale. - Mounts render from cache while they revalidate, so moving between the tool picker and the signing tool no longer flashes an empty list. - 12 tests where there were none. Same return shape, so no consumer files change. ### What this is not This is not a deduplication win. The three consumers are never mounted at the same time: `ToolPanel` renders the tool picker or the active tool and never both, so the badge cannot be on screen with either of the others, and `SharedSigningLauncher` and `useSigningSessionController` sit inside two different tools. The shared key earns its keep on cache reuse across those transitions, not on concurrent fetches. ## The bit worth reviewing The hand-rolled `{ silent: true }` flag encoded three states, and no single Query flag reproduces them: | | Spinner | Toast on failure | |---|---|---| | First load | yes | yes | | Background poll | no | no | | Explicit refetch | **yes** | **yes** | `isLoading` is false during an explicit refetch when data is already on screen; `isFetching` is true during a background poll. Neither matches, so the user-initiated case is tracked with a small flag and the failure toast is gated on `isLoadingError` plus the explicit path. ## Testing Twelve tests. Rather than trust them, each claim was checked by breaking the implementation and confirming the relevant test fails: | Mutation | Caught by | |---|---| | `refetchIntervalInBackground: true` | hidden-tab test | | Drop `refetchOnWindowFocus` | returns-to-view test | | Drop the user-initiated spinner flag | manual-refresh test | | Toast on every error | background-failure-is-silent test | | Give each observer its own key | dedupe test | Three things worth knowing for the next conversion: - **`waitFor` flushes renders.** Recording an index *after* `waitFor(callCount === 2)` skips past the in-flight render, so a "did the spinner flip on" assertion passes vacuously. The marker has to go before the poll. - **Fake timers hide in-flight state.** The fetch settles inside the same `act()`, so the intermediate render never happens. That test uses real timers and a held-open promise. - **`visibilitychange` has to bubble.** query-core listens for it on `window`, and the real event bubbles from `document`. A test helper dispatching a non-bubbling event never reaches the focus manager, and the pause behaviour still appears to work because `refetchInterval` reads `document.visibilityState` directly at tick time rather than through the event. **One claim is deliberately unguarded.** `isLoading` vs `isFetching` for a background poll produces no re-render at all, so there is nothing observable for a test to assert and no user-visible difference to protect. ## Pre-existing failures `task frontend:check` passes typecheck, lint and oxfmt, and 2363 of 2365 editor tests. The two failures, `workbenchSession.test.ts` and `notificationActions.test.tsx`, fail identically with this branch's changes reverted and are untouched by it. ## Scope This is one of five pollers. The remaining four, `useLocalFolderPoller`, `WatchedFolderWorkbenchView`, `SessionDetailPanel` and cloud `TeamSection`, are separate files with their own consumers and follow separately, now that the silent-refresh pattern has a worked example. --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> |
||
|
|
c22d9ecf58 |
feat(editor): move the admin directory onto TanStack Query (#7726)
# Description of Changes Step 5 of the TanStack Query rollout, covering the admin People, Teams and Team details screens. Follows #7264, #7283, #7285. ## The problem Two separate ones, in the same three files. **Reads.** Each section fetched and held its own copy of the same resources: People read the roster and the team list, Teams read the team list plus the roster again when its add-member modal opened, Team details read all three. Cost scaled with how many screens you visited rather than with how much data exists. **Writes.** Thirteen handlers each did the same five things by hand: set a processing flag, call the service, toast the outcome, dig a message out of an axios error, and reload their own slice. Refreshing was a convention, not a mechanism, and one handler had already forgotten it. ## The fix Three shared query keys (`adminUsers`, `teams`, `teamDetails`), and one `useAdminMutation` helper that every write is declared against: ```ts const createTeam = useAdminMutation({ write: (name: string) => teamService.createTeam(name), invalidates: ["teams"], success: t("workspace.teams.createTeam.success"), errorFallback: t("workspace.teams.createTeam.error"), onDone: () => { setNewTeamName(""); setCreateModalOpened(false); }, }); ``` Each write names the slices it disturbs, which is the part that only works when reads and writes are designed together: `createTeam` invalidates the team list, while a membership move invalidates the list, both teams' detail rows and the roster, because it genuinely changes all three. Invalidation refetches only mounted queries, so this costs nothing extra. The blanket "invalidate everything" helper survives in exactly one role: child components (invite, password change, seat update) that write through their own services, where the affected scopes are not visible from the call site. ## Why it is better, measured Request counts come from one harness driving `teams -> team details -> back -> people`, run against the branch point and against this branch. The assertion is committed, so it cannot silently regress. | | Before | After | |---|---|---| | Requests | 7 | **3** | | `getTeams` | 4 | **1** | | `getUsers` | 2 | **1** | | `getTeamDetails` | 1 | 1 | | Committed renders | 17 | **15** | Three is one per distinct resource, the floor for that sequence. The four `getTeams` were the Teams table, Team details fetching the same list for its "move to team" dropdown, the explicit refresh on the back button, and People. Renders barely move, which is expected: this changes where data lives, not how often React draws. It is reported because a caching change can quietly cost renders, and this one does not. On the code itself, across the three sections: | | | |---|---| | Net lines | **-216** | | `useState`/`useEffect` removed | **11**, none added | | Duplicated `isAxiosError` blocks | 13 to **1** | | `setProcessing` calls | 19 to **0** | `isAxiosError` is no longer imported by any of the three files. ## Bug fixed `disableMfaByAdmin` showed a success toast and never refreshed. The menu item renders only when `user.mfaEnabled` is true, so an admin disabled MFA, was told it worked, and watched the option stay on screen until a manual reload. It is covered by a test that fails if the invalidation is removed. ## Behaviour worth checking in review - A write no longer blocks its handler before closing the modal. The dialog closes when the write succeeds and the table updates when the refetch lands, rather than the button spinning through both. - Modal submit buttons now track their own mutation rather than one shared flag. Team details still derives a single busy flag, now from its five mutations rather than a `useState`, so its row actions disable together as before. - The per-handler `console.error` is kept, once, in the shared error path. ## Testing Five tests, each verified by breaking the implementation and confirming that one test, and only that one, fails: | Mutation | Caught by | |---|---| | Drop the shared stale window (`staleTime: 0`) | request-count test | | Make invalidation a no-op | write-visibility test | | Ignore the login-enabled gate | login-disabled test | | Stop invalidating after the MFA write | MFA-refresh test | | Fall back to the generic error message | server-message test | The write tests drive the real flows through their modals and menus rather than calling hooks directly. `task frontend:check` passes typecheck, lint and oxfmt, and 2383 of 2385 editor tests. The two failures, `workbenchSession.test.ts` and `notificationActions.test.tsx`, are untouched here and fail identically with this branch's changes reverted. ## Scope The three services keep their current shape; nothing outside these three sections and the new hook module changes. Child modals that write through their own services still refresh via the blanket helper, and converting those is separate work. |
||
|
|
d3708c1e63 | Highlight the rail entry whose tool is open (#7723) | ||
|
|
1c055f3d18 |
Centre modals in the viewport instead of pinning them near the top (#7715)
## What Every dialog in the processor is the shared `.sui-modal` shell, and its backdrop was top-aligning the panel: ```css align-items: flex-start; padding: 5rem 1.5rem 1.5rem; /* 80px above, 24px below */ ``` On a 900px-tall viewport that started every dialog at `y=80` with ~350px of dead space beneath it. Phones already had an `align-items: center` override; desktop never got one. ## Change `frontend/editor/src/core/ui/Modal.css` only: - Symmetric block inset, `align-items: center`. - The inset is published as `--modal-inset-block`, and `.sui-modal`'s `max-height` derives from it. That coupling is the point: if the two drift apart, a tall modal overflows a centre-aligned backdrop and loses its header off the top of the screen, unreachable. - The phone breakpoint now only moves the variable. Measured at 375x812 it resolves to exactly the previous values (`16px 12px`, `max-height: 780px`), so mobile behaviour is unchanged. One shared file, so this covers flow modals, source / user / pipeline / API-key modals, billing and procurement. ## Before / After <img width="2104" height="2284" alt="image" src="https://github.com/user-attachments/assets/bcb50145-f75e-449e-92c6-a0b085cc091c" /> ## Testing - `task frontend:check` passes (lint + typecheck + 2356 tests). - Phone breakpoint measured directly in the browser, values match the previous behaviour. |
||
|
|
4ab2505a6c |
Comment-quality standard, and the gate that enforces it (#7663)
## The problem AI PRs write comments that restate the line below them, mark sections with box drawing, and narrate the diff. Nothing in the repo said not to, and nothing checked. `AGENTS.md` had one line about comments and it was buried in the Python section. Banners and `Step N:` narration have zero occurrences in the 15 months before Aug 2025, so this is new. ## The fix A written standard, plus a linter that enforces the mechanical part of it on added lines only. - [devGuide/CODE_COMMENTS.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/devGuide/CODE_COMMENTS.md) holds the reasoning and worked examples; a section in [AGENTS.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/AGENTS.md) holds the operative rules, kept short so they stay in an agent's context. The two are split by kind rather than duplicated, because the same prose in two places drifts. - Rules in [comment-rules.mjs](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs), shared by both engines. - Two engines. `.ts` / `.tsx` / `.mjs` go to an [oxlint JS plugin](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-lint-oxlint-plugin.mjs) so comments come from the parser rather than a line scan; `.java` / `.py` go to a [line scanner](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-lint.mjs). Neither reads the other's files, so they cannot disagree about one file. - Between them they read every comment form the repo writes: `//` and `/* */`, Javadoc and JSDoc, JSX comments, `#`, and Python docstrings. - Runs in `task pre-commit`, so the git hook and the `pre_commit.yml` CI job both get it, and as a Claude Code [`Stop` hook](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-lint-hook.mjs) so an agent fixes the comment inside the turn that wrote it. ## The rules The part worth arguing about. **Every rule blocks.** A rule that only warns is a rule nobody acts on, so a finding you believe is wrong is a bug in the rule: narrow it, or mark the line and say why. | | Fires on | | --- | --- | | [CMT001](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L71) | Every word in the comment already appears in the code below it. Max 6 words, skipped for prose punctuation and for a bare Arrange/Act/Assert marker | | [CMT002](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L92) | 4+ rule or box-drawing characters, or a bare section label from [a fixed list](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L84) (`Types`, `Helpers`, `State`, `Handlers`, ...) | | [CMT003](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L110) | `Step N:` with a separator, or `Then,` / `Next,` / `Finally,`. Suppressed in test files | | [CMT004](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L129) | A comment about the code's own past: `this used to`, `renamed from`, `was previously called`. Suppressed in test files | | [CMT005](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L154) | 3+ consecutive comment lines where 2/3 [parse as code](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L143) | | [CMT006](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L31) | A run of implementation comment over 12 lines, outside the first 5 lines of a file. Doc blocks are exempt, because the standard asks for thorough contracts | | [CMT007](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L180) | A parameter or return description that adds no word its name lacks. Reads Javadoc/JSDoc `@param`, Sphinx `:param name:` and Google `name: description` | | [CMT008](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L239) | An allow directive naming a rule that does not exist, or one that silenced nothing | | [CMT009](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L219) | A `TODO` / `FIXME` / `HACK` naming no issue or link. An owner is not accepted: a username goes stale, an issue outlives it | Each rule carries the readings it deliberately excludes, next to the rule. Those exclusions came from running the rules over this repo, not from taste: `CMT004` does not match a bare "no longer needed" because that is as often about runtime lifecycle as about history, and `CMT003` needs a separator after the number so a wrapped line beginning "step 2 unmounts + remounts the panel" reads as the prose it is. A comment sharing a line with code is judged by the rules that do not depend on the code below it, so a trailing `// TODO fix this` or `/* this used to run before the flush */` still reports, while `50L * 1024 * 1024 // 50 MB` does not. `CMT001` would have been wrong about six in seven trailing comments here, so it stays out of them. If a finding is wrong, `// comment-lint-allow: CMT002` on the line above. Rule-specific, [no blanket disable](https://github.com/Stirling-Tools/Stirling-PDF/blob/claude/ai-pr-comment-quality-dd970e/scripts/lint/comment-rules.mjs#L229). A directive naming a rule that does not exist, or silencing nothing, is itself a `CMT008` failure, so a typo cannot quietly disable a rule and a stale one gets deleted rather than accumulating. No native linter covers `CMT007`. `eslint-plugin-jsdoc`'s `require-param-description`, Checkstyle's `NonEmptyAtclauseDescription` and ruff's D-rules all check that a description exists, not whether it says anything. ## Scoping Added comment **text** only, not lines git calls new. Reindenting a file or moving a block makes git mark untouched comments as added; findings are matched against the comment text at the base, so only genuinely new content reports. The whole file is read and every comment in it evaluated. Only the *reporting* is filtered, so a rule still sees the code a comment introduces, the full run it belongs to, and the base version of the file. Existing tree is untouched. `task pre-commit:comment-lint:all` reports it and always exits 0: | | java | ts/js | py | | --- | --- | --- | --- | | findings | 1,218 | 741 | 204 | 2,163 across 542 files, mostly `CMT002` banners (1,482) and `CMT001` restatements (456). Clearing it is separate work, by directory. Not in this PR: an advisory LLM review layer for the things no pattern can judge. ## Verification Run against [#7494](https://github.com/Stirling-Tools/Stirling-PDF/pull/7494) as CI would, in a throwaway worktree: **two findings on a 78 file, +4,512 line change, both genuine banners, in 952ms**. A whole-file scan of those same files gives 11; the other 9 were withheld because that PR's author did not write them, and they are the `@param teamId the team ID` shape this standard exists to stop. Both scanners blank string and character literals before looking for comment markers, because a partial lex desynchronises everything after it: one apostrophe in a Java comment, or one Python template whose closing quotes start a line, is enough to read dozens of lines of code as a single comment. Two fixtures carry canaries that stop being reported if either engine ever desynchronises again. The [fixture corpus](https://github.com/Stirling-Tools/Stirling-PDF/tree/claude/ai-pr-comment-quality-dd970e/scripts/lint/fixtures) pins all 9 rules against both engines, and `--selftest` fails if the two disagree about the same file. ## Two things reviewers should know **The oxlint JS plugin API is alpha.** oxlint itself is stable and already this repo's frontend linter; the plugin API is the new dependency. Its documented failure mode ([oxc#25203](https://github.com/oxc-project/oxc/issues/25203)) is being skipped silently while oxlint still reports success. That affects the standalone release binary rather than the npm package this invokes, but the class of failure reads exactly like clean code, so the run asserts `number_of_rules >= 1` from oxlint's own report and a broken engine exits 2 rather than passing. If the API ever breaks, the fallback is folding these rules into the line scanner, which already implements all nine for Java and Python. **`.claude/settings.json` is now committed**, carrying the hook and nothing else: 19 lines, no `permissions`, nothing machine-specific. That partly reverts `c35546a212` ("Ignore claude dir"), which existed because this file had twice been committed by accident with a personal `permissions` allowlist, once with absolute machine paths. Personal config still belongs in `.claude/settings.local.json`, which the new pattern keeps ignored, and hook entries merge across the two so nobody's own hooks are lost. If you already hand-wrote a `.claude/settings.json`, copy it somewhere first: that path used to be git-ignored, and git overwrites an ignored file without warning when a commit starts tracking it. Across 19 local checkouts here, 13 have `settings.local.json` and none has a hand-written `settings.json`. To turn the hook off, `{ "env": { "COMMENT_LINT_HOOK": "0" } }` in local settings. Claude Code can only disable all hooks at once, hence the switch. The commit-time gate still applies. ## How to test ```bash task pre-commit:comment-lint:ci ``` The fixture corpus, then the diff. The corpus checks the rules themselves rather than the code under review, so it runs on CI and before a rule change, not on every local commit. ```bash task comment-lint:branch ``` `clean (34 files in scope)`. `task comment-lint` is the same thing scoped to uncommitted work, which is what the git hook and CI run. To watch it bite, add `// Is banner` above `export function isBanner` in `scripts/lint/comment-rules.mjs` and run `task comment-lint`: one `CMT001`, exit 1. The gate covers its own source, which is why these scripts have no section dividers. ```bash task pre-commit:comment-lint:all ``` The standing backlog, report-only. Verified on the pinned oxlint 1.77.0, not only the 1.79 the plugin was prototyped against. |
||
|
|
658aa54c20 |
Update tool models to fix main (#7725)
# Description of Changes When #6697 merged, the CI didn't run for some reason so it was never caught that the tool models were out of date. This PR updates them to the correct state. |
||
|
|
0a3f0c1814 |
Fix redirect bugs in SaaS (#7721)
# Description of Changes Fixes various bugs that affected SaaS (and some self-hosted): - Refreshing on Editor caused the user to be redirected to Processor - User was unable to access Processor in SaaS - Deep link hijacking fixes - Fix double prefix `/app/app` issue - Fix going from tool -> editor -> processor -> editor putting you back into tool --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> |
||
|
|
849d616451 | Fix refreshing causing you to go to the Processor (#7694) | ||
|
|
f71b0247da | Quick access bar and old school sidebars (#7695) | ||
|
|
51835a7b5e |
fix: clean up Add Stamp image preview blob URLs (#6779)
Co-authored-by: James Brunton <jbrunton96@gmail.com> |
||
|
|
5b5e922069 |
Make the upgrade banner neutral instead of gradient purple (#7696)
## What The `promo` banner tone was a full-bleed `indigo-500 → purple-500` gradient with white text and a black drop-shadow on the CTA. It was the only saturated fill in the app, and against the warm neutral palette it read as a foreign object above the workbench. The bar is now app chrome: | | Before | After | |---|---|---| | Background | 135° indigo→purple gradient | `--c-bg-raised` | | Border | `transparent` | `--c-border-subtle` hairline | | Icon | white glyph, no container | neutral glyph in a `--c-surface-sunken` chip | | Text | forced white | `--c-text` / `--c-text-muted` | | CTA | `premium` accent (violet gradient) | `default` accent (same primary button as the rest of the app) | Before <img width="1504" height="739" alt="Screenshot 2026-08-27 at 4 49 00 PM" src="https://github.com/user-attachments/assets/a912d9e9-9590-4e1d-8202-1abb22a00f23" /> After <img width="1061" height="665" alt="Screenshot 2026-08-27 at 4 48 30 PM" src="https://github.com/user-attachments/assets/3be14aec-ccfe-4448-93b3-339a57be4937" /> Only caller is the friendly variant of `UpgradeBanner` (self-hosted, under the free-tier user limit). ## Notes - **No new theme tokens.** Every value is an existing `--c-*` semantic token, so light and dark both follow automatically with no per-theme overrides. - The `premium` accent itself is untouched, so the upgrade CTAs in `OfflineActivationCard` and `PairingPanel` are unaffected. - `--c-hue-indigo` / `--c-hue-purple` are still used by `SaaSOnboardingSlides`, `PaygFree` and `UpgradeModal`, so no tokens are orphaned. - Deleted comments describe rules that no longer exist (the gradient, the white-on-gradient text overrides, the CTA shadow). No new comments added. ## Verification - `task frontend:check:all` passes (typecheck, oxlint, all four theme linters, stylelint, format, tests, build, storybook build). - `task frontend:storybook:a11y:changed` passes light and dark: 7 AppBanner stories, 0 violations. Both a11y baselines are empty, so this is zero known violations rather than a baselined pass. - Checked in Storybook under **Shared / AppBanner → All Top Bars**, which renders every top bar the app can show side by side, in both themes. |
||
|
|
a215c30068 |
Add the missing en-US translations for classification labels (#7692)
Our classification labels were rendering their hardcoded English names because the en-US locale file had no `classification` section at all, so this adds the missing keys (labels and category names). Also wires the category names through i18n, since those had no `t()` call, and adds a test so a new label can't ship without its key. |
||
|
|
897c72e9d9 |
Fix tool panel scrolling so the action button stays reachable (#7688)
# Description of Changes
After ui rework all scrolling in all tool panels stopped working
This fixes this to allow tool panels to be scrollabe again
## What was wrong
PDF/UA is the only convert target whose settings panel overflows the
tool rail. Measured at 1920×1080: overflow was 0px for pdfa, pdfx, png,
docx, epub, and 158px for pdfua. Its action button sat at bottom: 1220
in a 1080px viewport — 140px below the fold — and the info alert was
clipped mid-sentence. The panel could be scrolled, but nothing said so
(Mantine's scrollbar auto-hides).
Normally the app would scroll the button into view for you. It didn't,
because both mechanisms built to do that were dead
## Cause:
Two separate mechanisms, both broken since the same commit (
|
||
|
|
732ef18ae5 |
feat(account-link): redirect-based connect handshake for self-hosted linking (#7494)
Links a self-hosted instance to a SaaS team over an ordinary redirect, and leaves the admin's browser holding a Stirling session at the same time. ## The problem A self-hosted server needs a device credential bound to a SaaS team, and the admin's Supabase JWT must never reach the instance backend. Three things ruled out the obvious approaches: - **A customer hostname can never be in Supabase's redirect allow-list**, so the sign-in cannot happen on the instance's own origin. That is why SSO and sign-up did not work for linking at all. - **A device credential identifies a server, not a person.** Every attended portal read (Usage, Billing, Documents, Infrastructure) goes through `getPortalSaasToken()` and needs a *user* session, so a credential-only link left all of them asking for a second sign-in. - **The previous design relayed a JWT** from the browser into the instance, which is the thing we wanted to avoid. That path is deleted here. ## The solution Redirect and nonce, modelled on desktop's `authService.loginWithSelfHostedOAuth`: mint a nonce, hand the browser off, accept only a callback carrying that nonce back. Desktop has the OS route the reply; self-hosted has no OS hop, so our own approval page performs it. That is the point — the human half happens on an origin we control. ``` instance SaaS admin's browser | POST connect/request | | | (name, callback, nonce, | | | claim-secret hash) | | |-------------------------->| | | <- requestId + authorizeUrl | | | GET /link?request=... | | |<-------------------------------| | | sign in (SSO works here), | | | see ACCOUNT + ORIGIN, approve | | |------------------------------->| | | 302 callback#nonce+session | | POST connect/claim | | | (requestId, claim secret)| | |-------------------------->| | | <- device credential | | ``` Four properties carry the safety, and each is stated in the code because each is easy to lose in a refactor: - **The redirect target is never caller-supplied.** Validated once at creation, then read back from the stored row, so nothing in the approval page's URL can steer the token elsewhere. - **Approval and minting are separate.** Approval records the team and hands out nothing usable; the credential is minted only on claim, authenticated by a secret that never entered a browser. - **A re-authentication cannot move a server between teams.** The team is pinned at creation from the credential only that instance holds, so an approver from another team gets `WRONG_TEAM` instead of a rebind. - **The approver has to confirm what they are binding.** The page shows the address and the signed-in account, with a way to switch, and a checkbox naming the address gates the approve button. The name the server reports is deliberately not shown: the requester picks it on an unauthenticated endpoint, and its honest value is the hostname already in the address. The session rides the URL fragment, so it stays out of access logs and `Referer`, and is stripped before anything awaits. The claim is row-locked, so one approval mints once. A request lives 30 minutes; a settled one is not offered again, since approving it fails server-side. Signing in mid-flow no longer loses the request. The id is kept on the SaaS origin and resumed after any sign-in, which is what makes creating an account work: the confirmation email opens a new tab, where the `next` parameter is gone. Reading it does not consume it — the request may be open in two tabs — and only a recorded decision retires it. The result lands as a modal over the portal the admin started from, and the portal re-reads its link status so the page behind agrees with the modal. Plaintext `http://` callbacks are accepted rather than refused, because many self-hosted instances legitimately run plain HTTP on a private network; the address carries a warning icon explaining the risk, derived server-side so a requester cannot suppress it. Hard-refusing `http://` to a public IP literal is a reasonable follow-up; a bare hostname can't be classified without a DNS lookup, so the warning stays the general mechanism. ## Configuration Four surfaces. Placeholders below, not values. **SaaS backend** | Setting | Needed | Why | |---|---|---| | `stirling.billing.account-link.enabled` | Yes, `true` | The connect controller and service are `@ConditionalOnProperty` with no default, so without it the endpoints do not exist. | | `system.frontendUrl` | Only when the approval page is not on the API's own origin | Where the approver is sent. Must include the app's base path if it is served under one, or the redirect misses `/link`. | **SaaS frontend** | Setting | Needed | Why | |---|---|---| | `VITE_SUPABASE_URL`, `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY` | Yes | Its own sign-in. Must be the project the SaaS backend validates tokens against. | | `RUN_SUBPATH` | Only if served under a subpath | Moves the approval page to `<base>/<subpath>/link`, so `system.frontendUrl` has to agree. | **Self-hosted backend** | Setting | Needed | Why | |---|---|---| | `stirling.billing.account-link.enabled` | Yes, `true` | Defaults to `false`. | | `stirling.billing.account-link.saas-base-url` | Yes | Origin of the SaaS API it links to. Not the SaaS frontend. | | `system.frontendUrl` | Optional | Externally reachable base URL for the callback. Otherwise derived from the request's `Origin`, which is right for ordinary deployments and wrong behind a rewriting proxy. | **Self-hosted frontend** | Setting | Needed | Why | |---|---|---| | `VITE_SUPABASE_URL`, `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY` | Yes | Accepts the session handed over in the callback fragment. | | `VITE_SAAS_API_URL` | For Usage and Billing | Attended reads go to the SaaS API with the admin's token. Absent, those surfaces stay on the mock. | | `VITE_INCLUDE_PORTAL` | Production builds | Dev builds include the portal automatically; without it there is no link UI and no callback route. | Two things worth stating because neither fails loudly: - **Both frontends must use the URL *and* key of the same Supabase project**, and the same one the SaaS backend validates against. A key from one project with a URL from another is accepted by the browser and rejected by Supabase, which surfaces much later as "session expired" on Usage rather than as an error at hand-over. - **The Supabase redirect allow-list must contain the SaaS app's `/auth/callback`**, since a confirmation email returns through it. Entries are matched exactly. - **`system.frontendUrl` is the existing setting for this**, not a new one, so each side reads its own value and there is nothing extra to configure. It also gates share links, so on a stack with storage and sharing already on, setting it here turns those on too. The self-hosted side deliberately does **not** configure where the approval page lives — SaaS answers that in the connect-request reply, being the only party that knows. Also here, because testing this needs two stacks side by side: `linked:staging` / `linked:dev` (which derive `system.frontendUrl` and `RUN_SUBPATH` themselves), the missing `frontend:staging:saas`, and a per-mode vite `cacheDir` — two dev servers in different modes otherwise re-optimise over one shared dep cache. ## How to test Automated and green: `task frontend:check:all` plus both backend modules. `ConnectRequestServiceTest` covers callback validation, the per-IP cap, single-use approval, claim outcomes, expiry, `WRONG_TEAM` and reauth confirming without minting; `ConnectServiceTest` covers callback-resolution precedence including a foreign-origin callback being discarded; `ConnectControllerTest` covers the authorize URL, including the forwarded-header path and only the first hop being trusted; `ConnectCallback.test.tsx` covers the fragment being stripped synchronously and malformed fragments refused; `LinkAccountModal.test.tsx` covers link and reauth hitting different endpoints. Manual walkthrough: 1. `task linked:staging` — added here; brings up a SaaS stack and a self-hosted instance pointed at it, on discovered ports, and prints the four addresses. 2. Open the link-account modal in the self-hosted portal and continue. Expect the SaaS approval page at `/link?request=<id>`. 3. Sign in as a team leader, or create an account and confirm the email. Either way you should come back to the approval page. 4. Tick the acknowledgement and approve. Expect the fragment gone from the address bar immediately, a result modal over the portal, the portal showing linked without a reload, and attended reads (Usage, Billing) working without a second sign-in. 5. Repeat, approving as a member of a different team. Expect a refusal, not a rebind. ## Outstanding - #7415 to be reworked against this design once this lands. - **No SaaS-side UI to disconnect a server.** `GET /account-link/instances` and `POST /account-link/instances/{id}/revoke` are already team-scoped and leader-gated, and the portal has a panel that uses them, but `portal-saas/components/settings/accountLinkSettings.tsx` exports `null` on the reasoning that "SaaS has no account-link concept". That held when linking was a self-hosted admin managing their own instance; here a leader approves a server they may not administer, and has no way to withdraw it. The seam to fill is that one file. Expected to land with the CTA work in #7415. --------- Co-authored-by: James Brunton <jbrunton96@gmail.com> |
||
|
|
f7a2c626c9 |
Persist the workbench session across the editor/processor switch (#7654)
## What Switching editor -> processor (or reloading) unmounts every editor provider, which emptied the workbench. This PR mirrors the workbench into per-tab sessionStorage and refills an empty one from that record on the next mount: - **Files, selection, view and active document survive** the shell switch and reloads. Each recorded file is resolved to its *current leaf* version on restore, so a file versioned by a policy or another tab comes back at its latest state. - **The switch back lands where the user left**: the processor sidebar's "editor" button consumes a one-shot return path saved at switch time. - **The app switch respects unsaved changes**: `useOtherAppSwitch` (proprietary + saas) now routes through `requestNavigation`, so the same warning guards it as any other navigation. - Desktop shadows `WorkbenchSessionPersistence` with a stub (OS-launched files own boot there). ## How to test I've run through each of these manually: - Upload several PDFs in the editor, select a couple, and switch to the Active Files grid. Click "Open PDF Processor" in the sidebar footer, then switch back to the editor. The same files, selection and view should return, and you should land on the editor page you left. - Open a document in the viewer, then reload the tab. The workbench should refill and come back on the viewer with the same document active. - With unsaved changes in a tool, click the processor switch. The unsaved-changes warning should appear, and the switch should only proceed if you confirm. - Open a second browser tab with different files. Each tab should restore its own workbench independently (the record is per-tab sessionStorage). - While in the processor, delete one of the open files from storage, then switch back. The remaining files should restore and a warning toast should report "Restored X of Y files". --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
caeca0b88a |
Fix classification escalation: the local pass was claiming the server dispatch key (#7667)
Follow-up to #7580: the escalation it added could never fire. ## What's broken The auto-run skips a policy that has already run on a file, keyed on `(categoryId, fileId)`. `recordRunStart` claims that key — and #7580 has the **browser-side first pass** record its own run under `categoryId: "classification"` for the uploaded file. So the local heuristic ticks the very key the server escalation checks, and the AI is never asked, at any confidence. Trigger is the default seeded setup: **Classification as the only on-upload policy**, and a local verdict below `high`. Any other on-upload policy masks it, because classification then targets that policy's output — a new file id whose key was never claimed. That's why this went unnoticed. Two smaller faults in the same path: - A chained output carried no `classificationConfidence`, so `shouldDispatchToAi` waited for a verdict that could never arrive (a tool-derived file gets no local pass). - Browser-local runs were polled against the server: 3 × 404 per file, after which `MAX_NOT_FOUND` marked a local run that had actually **succeeded** as `FAILED`. ## The fix - `PolicyRunRecord.browserLocal`; `recordRunStart` skips the dispatch claim for such a run. It is the first pass, not the policy's run. - The local pass meters under `classification:local-meter` instead of the category id, so metering dedupe survives without suppressing dispatch. - The poll effect skips browser-local runs. - `CONSUME_FILES` inherits `classificationConfidence` alongside the labels, so the verdict survives a version bump. ## How to test Download [`low-confidence-classification.pdf`](https://github.com/Stirling-Tools/Stirling-PDF/raw/fix/chained-classification-confidence/frontend/editor/src/proprietary/services/heuristic/fixtures/low-confidence-classification.pdf) (checked in as a fixture, verdict pinned by a test). With **Classification as the only on-upload policy**, upload it and watch the Network tab: - **Before:** no `POST /api/v1/policies/{id}/run` for classification, ever. Console shows `local-classification-*` 404s. - **After:** exactly one, and the engine receives `POST /api/v1/documents/classify`. Judge it on that request, not on the resulting label — the model's answer varies, so a label comparison can pass or fail for the wrong reason. Headless equivalent: ``` npx vitest run --project proprietary src/proprietary/components/policies/usePolicyAutoRun.escalation.test.tsx ``` Passes here, fails on `main` on "asks the AI about an unsure verdict even though the local pass already ran". Its other two cases pass on both, so the guards still hold: a confident verdict still costs nothing, and a file with no verdict yet still waits rather than racing the free pass. New tests drive the **real** run store — mocking it is what let this through. `task frontend:check`: 255 files / 2202 tests. |
||
|
|
c93feb5dfc |
Remove a bunch of unnecessary casts from the frontend (#7662)
# Description of Changes Originally, I wanted to re-enable typed linting on our repo but using Oxlint this time to avoid the memory and speed issues that ESLint was causing. Unfortunately, it's not stable enough yet to actually use on our repo (although it is close, I suspect it'll be stable enough fairly soon). I was able to remove many of the unnecessary casts that it found though, so even though this won't be enforced, it's still worth cleaning up what I've found. |
||
|
|
f945cc7dc6 |
Regenerate expired test certificates and guard against future expiry (#7682)
The bundled signing test certificates expired at **07:41:10 UTC on 2026-08-26**. They were issued exactly one year earlier, so they went from fine to fatal mid-morning with no warning, and they take down `main` and every open branch, not just one PR. First casualty was the `docker-compose-tests` job on #6802, which started at 07:45: ``` java.security.cert.CertificateExpiredException: NotAfter: Wed Aug 26 07:41:10 UTC 2026 at CreateSignatureBase.checkValidity(CreateSignatureBase.java:159) at CertSignControllerTest.testSignPdfWithPkcs12(CertSignControllerTest.java:205) ``` ``` $ openssl x509 -in app/core/src/test/resources/certs/test-cert.pem -noout -dates notBefore=Aug 26 07:41:10 2025 GMT notAfter =Aug 26 07:41:10 2026 GMT ``` ## What was broken `CertSignControllerTest` (7 tests) and `PdfSigningServiceImplTest` (2) fail outright. `ValidateSignatureControllerMoreTest` and `CertificateValidationServiceMoreTest` read the same fixtures. Auditing the rest of the repo turned up three more time bombs that had not gone off yet: | Fixture | Was | Problem | |---|---|---| | `app/core/.../certs/test-cert.*` + `test-key.*` | expired 2026-08-26 | **already breaking every branch** | | `test-certs/valid-test.p12`, `valid-test.jks` (proprietary + frontend copies) | expire 2027-03-25 | same failure, seven months out | | `test-certs/not-yet-valid-test.p12` | valid **from** 2027-03-25 | becomes valid, so its test silently stops proving anything, on the same day | ## What this does **Regenerates every fixture** with the identical subject DN, alias, password, key size and signature algorithm as before, changing only the validity window. Nothing that any test asserts on has moved. - valid fixtures: `2025-01-01` to `2125-01-01` - `not-yet-valid-test.p12`: `2125-01-01` to `2126-01-01`, so it stays in the future - `expired-test.p12`: pinned to its permanently-past 2024 window **Adds `scripts/generate-test-certs.sh`** as the source of truth, so the next regeneration is one command instead of archaeology. It documents every DN, alias and password, pins the validity windows, and runs on Linux, macOS and Git Bash. **Adds two guard tests** that fail with an actionable message, naming the script, while there is still a year of runway: - `BundledTestCertificateExpiryTest` (app/core) checks all seven formats parse, are in their validity window, and have more than 365 days left - `BundledWorkflowCertificateExpiryTest` (proprietary) does the same for the valid pair, and additionally asserts the expired fixture is still expired and the not-yet-valid one is still in the future That last pair matters: those two fixtures exist to test a validity outcome, and each one silently stops testing anything once the clock passes its window. ## Verification Run locally against the regenerated bytes, on the exact content committed here: ``` ./gradlew :stirling-pdf:test --tests '*CertSignControllerTest*' --tests '*BundledTestCertificateExpiryTest*' \ --tests '*PdfSigningServiceImplTest*' --tests '*ValidateSignatureControllerMoreTest*' \ --tests '*CertificateValidationServiceMoreTest*' BUILD SUCCESSFUL ./gradlew :proprietary:test --tests '*BundledWorkflowCertificateExpiryTest*' --tests '*CertificateValidationIntegrationTest*' \ --tests '*SigningFinalizationServiceMoreTest*' --tests '*ServerCertificateServiceTest*' \ --tests '*CertificateSubmissionValidatorTest*' --tests '*WorkflowSessionServiceTest*' BUILD SUCCESSFUL ``` `spotlessCheck` passes on both modules. |
||
|
|
72b7892312 |
Translations + com.squareup.okhttp3:okhttp-bom from 5.3.2 to 5.4.0 (#7599)
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Signed-off-by: dependabot[bot] <support@github.com> |
||
|
|
353df7a647 |
Improve modals in Sources page in Processor (#7664)
# Description of Changes Various changes throughout to try and convert the bulk of the dev UI sources modals to production quality. Changes include: - Fixing inconsistencies between different modals - Hide things users will rarely need to change behind advanced - Removed clutter in the UI - Renaming settings in terms that the user will understand and care about <img width="2360" height="3068" alt="image" src="https://github.com/user-attachments/assets/2c637e8f-bc1b-4c7e-98cb-d836ae626ba5" /> <img width="2360" height="3008" alt="image" src="https://github.com/user-attachments/assets/d894aadc-7c83-41d1-b614-881637a6bd34" /> |
||
|
|
0d75715af2 |
Fix flaky e2e tests (#7595)
# Description of Changes e2e Playwright tests are currently failing intermittently on all platforms for different reasons, most notably WebKit, which seems to fail much more often than the others. This PR attempts to fix the issues. I've ran the e2e tests a few times now and they don't seem to be inconsistent any more, but it's difficult to tell if all the issues are genuinely fixed due to the inconsistent nature. As far as I can tell, I've not broken anything though. |
||
|
|
49c1e75ced |
Surface recorded failures in a notification bell (Review Flow PR 4) (#7478)
Review Flow PR 4. Stacked on #7477. Recorded failures appear in a notification bell, showing each reader the failures they are allowed to see and the actions they can actually take. Scope is deliberately viewing and routing only. Resolving a failure — retry, decrypt-and-retry — is #7479, which also brings the write path for it; nothing resolution-shaped ships here, not even dark. ## What's added **A notification bell** in the editor and the processor shell. Polls `GET /api/v1/notifications` every 30 seconds, shows an unread badge, and lists open failures newest first. Each row shows the failure's title, its message with **Copy error** and **Show full message** chips, an occurrence count, and its available actions. **A notification API** (`stirling.software.proprietary.notification`), derived from failures on read rather than stored in its own table: | Route | Purpose | |---|---| | `GET /api/v1/notifications` | the caller's open failures, newest first | Read-only by design: every action the bell offers is one the client runs on its own device, so there is nothing to post back. Every id is prefixed (`failure:<uuid>`), so the bell never holds a raw failure id it could hand to a failure endpoint. **Per-reader actions.** A `FailureKind` declares each action with an audience (`OWNER`, `TEAM_REVIEWER`, `ANYONE_WHO_SEES`). The server resolves that against the reader and derives `Ownership` (`MINE` / `THEIRS` / `UNOWNED`) from the row's actor, so an admin reviewing someone else's failure is not offered a document their browser does not hold. Adding a failure kind requires no frontend change. **Server-run and client-run actions are distinguished.** `FailureActionId` carries an `Execution` facet; the registry requires a bean only for server actions, and dispatching a client action on the failure surface returns 400. The notification projection goes further: it carries only client-run offers, so the bell cannot be sent a button it would refuse to draw. **Actions in the bell:** at most two. The owner of the document gets **View file** (opens it in the editor); a team reviewer gets **View in processor** (dev builds only). Dismiss stays on the failure queue in `/processor/documents` — deciding a failure's fate belongs to the review surface, not the panel that announces it. An action id the build has not wired is skipped rather than rendered dead, so the server can ship new kinds ahead of the clients that understand them. **Attended policy runs record their document.** `POST /api/v1/policies/{id}/run` accepts an optional opaque `fileId`, recorded when the run carries exactly one primary document. This is what lets a repeat fold onto one incident instead of opening a new one per upload, lets deleting the file clear its failure, and lets the owner open the document from the row. ## Behaviour changes - **The bell re-reads as soon as a failure you caused is recorded**, rather than leaving you to wait out a poll interval for news of your own upload. Applies to a failed tool run and to a policy run reaching `FAILED`. Other people's failures still arrive on the poll, which is what it is for. - **An action the reader cannot use is not rendered.** Where the server gave a reason for withholding it, that reason appears as the row's one-line note. An action that was never offered to that reader produces no note. - **Deleting a document closes every incident about it that the deleter caused**, including a failed policy run on their own upload, so a user's own errors leave the bell with the file rather than lingering with a dead button. - **The failures list in `/processor/documents` stays behind `import.meta.env.DEV`**, and View in processor is gated to match so it cannot navigate to a section that is not mounted. Both lift when failures get their own review screen. - **One poll for all bells.** The bell is mounted in three places; the list, document lookups and read marker are shared, so mounting more than one does not multiply requests. - `ACKNOWLEDGE` is no longer offered by any kind. The id, bean and status remain so existing rows stay readable. ## Known limits - The poll does not pause when the tab is hidden. - No retention or per-team cap on `file_run_events`. ## How to test Needs a proprietary or SaaS build with login enabled. `task dev:all`, then sign in. 1. **Create a failure.** Add a password-protected PDF to the editor and choose **Skip for now** when it asks to unlock. The upload starts a policy run that fails on it. 2. **Watch the bell.** The badge should appear within a second or two, not after 30 — this is the refresh-on-failure path. Open it: a row titled "Password-protected document" with the error message and the two chips. 3. **The buttons should be View file and View in processor, nothing else.** No Dismiss and no retries: dispositions live on the review surface, resolutions in #7479. 4. **View file** closes the panel and selects that document in the editor. 5. **Dismiss from the queue instead.** Open `/processor/documents` (dev build), find the row in the failures list and dismiss it there; the bell drops it on its next read. 6. **Confirm the local-document probe.** Create a second failure, then delete that file from the editor and reload. Its incident closes with it; a row whose document is still present keeps **View file**. 7. **Confirm attribution end to end.** Sign in as a plain member, run a shared policy on your own upload so it fails. The member sees their own row in the bell. Sign in as the team leader: they see it too, but with **View in processor** instead of **View file**, because the document is not in their browser. 8. **Confirm folding.** Add the same locked PDF again and skip again. The existing row's occurrence count increases rather than a second row appearing. 9. **Confirm one poll for many bells.** Open the editor and the processor in two tabs. Each tab issues its own poll, but within a tab the several mounted bells share one — the Network tab should show one `GET /api/v1/notifications` per 30s per tab, not three. ## Migration None. No new column and no new value in any CHECK-constrained enum; `CheckConstrainedEnumsTest` fails if that changes. |
||
|
|
826e487f00 |
Update Frontend 3rd Party Licenses (#7650)
Auto-generated by stirlingbot[bot] This PR updates the frontend license report based on changes to package.json dependencies. Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com> Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> |
||
|
|
79686a3a09 |
form field editing (#6655)
# Description of Changes Building ontop of a users draft PR for form creation tools **Fill Form** becomes a full **Form Editor**: fill, create, modify and delete AcroForm fields visually. Builds on the community form-creation draft, plus a UX/UI rework pass. - **Backend**: `/api/v1/form` endpoints — `fields-with-coordinates`, `add/modify/delete-fields`, combined `edit-fields` (one round-trip), `fill`, `extract-csv/xlsx`; supports text (multiline, comb), checkbox, dropdown, list box, radio, button actions (reset/print/URL/submit) and signature placeholders - **Create**: type palette, click-or-drag placement with snap guides, inline property editor, batch "Add N fields" - **Modify**: move/resize on the page, arrow-nudge + Delete key, X/Y/W/H inputs, staged edits/deletes with chips, discard - **Fill**: live progress + required tracking, flatten toggle, Export menu (JSON/CSV/XLSX), Ctrl/Cmd+S - **Safety**: confirm dialog before discarding staged work; empty required fields warn with "Save anyway" instead of blocking - **UI**: consistent panel skeleton (fixed header / scrolling list / pinned actions), empty states that link into Create, full i18n with plural keys [walkthrough.html](https://github.com/user-attachments/files/30508976/walkthrough.html) --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Co-authored-by: Denys Vitali <denys@denv.it> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
158187ac46 |
Fix Documents tab in Processor (#7569)
# Description of Changes The Documents tab in the Processor is supposed to be available to all Processor users, but because the API is built on top of the Audit data, which is only for enterprise users, the API call always fails with 403. This means that it never fills the query cache, so every time you go back to the tab it has to reload all the data for a couple of seconds (and will fail again). This fixes the API so that it's available to any Processor user instead of just enterprise users. Also, the documents data was only being written to the log on an enterprise license, so I've changed it so that data is always tracked in the audit log because otherwise the Documents tab would still be useless to non-enterprise users. The Audit Log tab was also available to all Processor users, but would have the same issue where the table would never load because the API would 403 as well. I've just made the Audit Log tab disabled for non-enterprise users now. We might want to do something to signpost it a bit more that it's an enterprise-specific feature, but it's better than nothing for now. |
||
|
|
e50c3de0a9 |
Run classification locally first and only escalate an unsure verdict to the AI (#7580)
Split out of #7574 — this is the classification half, which is independent of the editor-source work and can land on its own. ## What this does - **Runs the local heuristic first and only escalates an unsure verdict to the AI.** A high-confidence local answer stands; anything less (or a file the heuristic hasn't reached yet) goes to the engine. A wrong label costs more than an engine call, so the bar is deliberately strict. - **Makes `classify` an authorable pipeline task**, so it can be used as a step like any other tool, and skips files that are already classified. - **Leaves the seeded Classification policy unowned** rather than naming a `system` placeholder that was never a real user; existing seeds are repaired on boot. ## Review feedback applied From @jbrunton96 on #7574: - **The generic runner no longer names classification.** Everything classification-specific moved into `proprietary/data/classificationPolicy.ts`, and `usePolicyAutoRun` now asks capability questions instead: `policyRewritesDocument`, `policyDeliversOutputFiles`, `policyRequiresAiEngine`, `shouldDispatchToAi`. There is no `id === "classification"` left in the runner. - **Ordering is no longer a name in the runner.** `pinClassificationLast` is gone; the runner sorts annotating policies after rewriting ones. The constraint is real: an annotating policy is non-blocking, so a rewriting one running after it forks from the pre-annotation version and drops the labels. To be straight about what this is and isn't - see "Still open" below - `policyRewritesDocument` is still keyed on the category id, not on a property each policy declares. The check moved out of the runner; it did not stop being a check on one id. - **Confidence is typed.** New `ClassificationConfidence` union in `core/types/fileContext.ts`, reused by `fileStorage`, `HeuristicConfidence`, and the trusted-verdict constant instead of being respelled at each site. - **Comments trimmed** to the repo's 2-line guideline, and a stale seeder javadoc that still claimed an internal-user owner was corrected. ## Still open, deliberately `classificationPolicy.ts` answers its capability questions with `categoryId === "classification"`. That is the same check relocated, not removed, and the module doc now says so outright. Deliberate, for two reasons: - **The concept it would be declared against is going away.** Policies are becoming pipelines with labels behind a separate enforcement layer, which removes the category the flag would live on. A capability system built on `categoryId` today gets migrated twice. - **Classification is genuinely privileged, not accidentally special.** It is the only policy with a browser-side implementation, so it can answer without the server. That is a product decision, and a local-only mode for set scenarios is planned - the flag for it should be designed with that feature, not guessed at now. The end state for the rest: an in-place output mode retires the ordering rule and `policyDeliversOutputFiles`, and a run result that can carry findings as well as files retires the remainder. Both touch the import path, which is the most delicate code in `usePolicyAutoRun` - not something to bolt on to a PR that has already been split once. Nothing is broken by leaving it. A user-built classify pipeline still gets its labels: the generic import path reads them off the returned PDF. It versions the file instead of labelling in place, and it misses the local-heuristic shortcut, so it always bills the engine. ## Testing - `classificationPolicy.test.ts` — 12 cases covering each capability and the escalation rule - Full frontend `proprietary` project: 39 files / 442 tests - `:proprietary:test` for `DefaultClassificationPolicySeederTest` + `ClassifyLabelControllerTest` - `tsc --noEmit` on core, proprietary, portal, saas, desktop, cloud --------- Co-authored-by: James Brunton <jbrunton96@gmail.com> |
||
|
|
dbd60d4765 |
Replace Prettier with Oxfmt (#7422)
# Description of Changes Prettier takes about 10 seconds to run over our frontend folder, but [Oxfmt](https://oxc.rs/docs/guide/usage/formatter.html) does an (almost) identical job in 0.2 seconds. This PR converts our Prettier integration to an equivalent Oxfmt integration. There's exactly 2 files in the frontend folder that Oxfmt formats differently to Prettier so it'll barely cause any disruption to the source. I've removed the `--check` option from the frontend tool models generator as part of this because we can do the same thing with Task easily enough and Oxfmt isn't directly importable like Prettier since it's a Rust binary instead of a JS library. Originally I was shelling out to Oxfmt on single-file mode to keep it all in memory but it just seemed more likely that there'd be config mismatches between that script and the task so I think it's better this way. |
||
|
|
629f501e9c |
Fix any type usages in frontend (#7617)
# Description of Changes Follow-on from #7334. Fix more `any` type usages and ban them in the linter. We're starting to get down to only difficult folders left now, so some of these fixes replace an excluded folder with a couple of individual files to reduce scope to manageable levels. There are two real behaviour changes in this PR because of bugs that were never caught due to the lack of proper typing: - In the Google Drive service, `lastModified` was always `undefined` because it should have been read via `lastModifiedUtc`, which it now is. This means that files being read from Google Drive should now accurately retain their last modified date from Drive. - In the error toasts, there was translation logic to try and make friendlier error messages, but it'd never actually fire since it relied on `i18n` being written to `globalThis`, which it never was. It now imports the singleton instead so that translation should start working. I also had to tweak the way that FitText works because it was relying on `any` typing to mix refs between different places where they weren't technically compatible but I've changed it to go via a function and the behaviour doesn't change. |
||
|
|
1df372764f |
Mobile follow-ups to #7518: tool-list search, and drop the empty overflow menu (#7660)
# Description of Changes Follow-up to #7518, picking up two mobile rough edges found while going over that branch. Two changes, one commit each. ## 1. Tool search back in the tool list (mobile) Tool search lives in the workbench bar's super search, which on mobile sits on the Workspace slide. So searching for a tool meant swiping off the tool list, typing, then swiping back. This puts a filter at the head of the tool panel on mobile. Reuses the existing `ToolSearch` component in `mode="filter"`, the same one the desktop fullscreen picker uses. Drives `setSearchQuery` on `ToolWorkflowContext`, so the query, filtering and grouped results are all existing paths. `ToolPanel` takes a new `showSearch` prop; `RightSidebar` passes `showSearch={isMobile}`. Desktop renders exactly as before. **To test:** - Open the editor at a phone-width viewport (under 1024px). - A "Search tools..." field should sit above Favourites / Recommended in the Tools pane. - Typing filters into grouped results. Clearing goes back to the compact list. - It hides once a tool is open, and comes back on the way out. - On desktop the field should not appear at all. ## 2. The mobile overflow menu opened with nothing in it `WorkbenchBarMobileActions` rendered its kebab trigger unconditionally. But every item inside is gated on `currentView === "viewer"` or `!isCustomView`. In a `custom:*` workbench both are false, so the dropdown was empty. `WorkbenchBarDesktopActions` renders nothing in that case, so this only showed on phones. Now returns `null` when neither group applies, with the two conditions named so the trigger and the items can't drift apart again. **To test:** - Phone-width viewport, load a PDF. - Open a tool with its own workbench view: Compare, Get Info report, Show JS, Validate Signature, Edit Table of Contents, or PDF Text Editor. - The kebab at the right of the workbench bar should be gone entirely, rather than opening an empty menu. - Back in the viewer or page editor it should still be there, with Print / Download / Save As / Close. |