Paginate the user references tab against the new object-dependencies endpoint - #4008
Conversation
…endpoint
The user detail screen's "References" tab rendered the entire
user.objectDependencies.dependencies array embedded in the GET /users/{id}
response, reproducing the same unbounded-hydration OOM risk as
admin-ui-classic-bundle#1106 on the frontend.
Regenerate the OpenAPI client (docs.jsonopenapi.json + user-api-slice.gen.ts)
against the new paginated GET /user/{id}/object-dependencies endpoint added
in studio-backend-bundle, add a userGetObjectDependencies cache-tag wrapper,
and rework references-container.tsx to page through it with local page/
pageSize state and the shared Pagination control, following the same
pattern as the generic element Dependencies tab's required-by-panel.
Refs: pimcore/platform-version#308
studio-backend-bundle's GET /users/{id} now returns objectDependencies
again as a bounded 20-item preview (ObjectDependenciesPreview) instead
of the previously removed field. Regenerate docs.jsonopenapi.json and
user-api-slice.gen.ts so the User type matches; nothing in the UI reads
this field yet, so no other frontend changes are needed.
Refs: pimcore/platform-version#308
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a paginated “object dependencies” data source to the User References tab and wires it to a new RTK Query endpoint.
Changes:
- Introduces
userGetObjectDependenciesRTK Query endpoint + types. - Updates References tab to fetch dependencies server-side and render pagination controls.
- Adds a dedicated Pagination component for the References tab.
Reviewed changes
Copilot reviewed 4 out of 6 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| assets/js/src/core/modules/user/user-api-slice-enhanced.ts | Adds cache tags for the new userGetObjectDependencies endpoint. |
| assets/js/src/core/modules/user/management/detail/tabs/references/references-container.tsx | Switches from draft-based dependencies to paginated API query + pagination UI. |
| assets/js/src/core/modules/user/management/detail/tabs/references/components/pagination/pagination.tsx | New pagination UI wrapper with loading/empty states. |
| assets/js/src/core/modules/auth/user/user-api-slice.gen.ts | Adds generated endpoint + response/arg types; updates user dependency preview typing. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Use useTranslation() instead of importing t from i18next so the pagination component re-renders on language change - Extract the default page size into a shared constant to avoid divergence between the container's query state and the pagination default - Give userGetObjectDependencies its own USER_OBJECT_DEPENDENCIES cache tag instead of USER_DETAIL, avoiding unnecessary refetches when unrelated user detail fields change Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The success-response description for GET /user/{id}/object-dependencies
copied this bundle's standard "with total count as header param"
phrasing, but PaginatedResponseTrait::getPaginatedCollection() also
serializes totalItems in the JSON body via the Collection DTO - true for
every endpoint using that trait, not just this one. Only fixing the one
description this PR introduced; the same incomplete phrasing predates
this change on ~15 other endpoints across the bundle and is out of scope
here.
Refs: pimcore/platform-version#308, review feedback on
pimcore/studio-ui-bundle#4008
studio-backend-bundle's OpenAPI description for GET /user/{id}/object-
dependencies now states the total count is in both the response header
and the totalItems body field, resolving the remaining open Copilot
review comment on this PR (the others were already fixed in a prior
commit). Also picks up the MAX_PAGE_SIZE=100 schema constraint from that
same backend PR's earlier review-fix commit, which hadn't been synced
to this repo's generated client yet.
Refs: pimcore/platform-version#308
…ange studio-backend-bundle restored the original ObjectDependencies name and its dependencies/hasHidden fields, adding totalItems alongside them instead of replacing the shape. Regenerate the client so the User type matches (UserObjectDependenciesPreview -> UserObjectDependencies, with hasHidden back and totalItems added). Nothing in the UI reads this field, so no other frontend changes are needed. Refs: pimcore/platform-version#308
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
assets/js/src/core/modules/user/management/detail/tabs/references/references-container.tsx:51
- After the first response, RTK Query keeps
isLoadingfalse during page and page-size requests and reports those throughisFetching. The grid therefore continues to present the previous page as settled while the next page is loading. CaptureisFetchingfrom the hook and use it for the table's loading state (the pagination can keep the initial-load flag if it must preserve its internal page-size state).
isLoading={ isLoading }
assets/js/src/core/modules/user/management/detail/tabs/references/references-container.tsx:50
- A failed dependencies request also produces
data === undefined, so this fallback presents the failure as an empty references list and the API error is never reported. Captureerrorfrom the query and pass it totrackError(new ApiError(error)), following the user-management error-handling pattern.
data={ data?.items ?? [] }
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
assets/js/src/core/modules/user/management/detail/tabs/references/references-container.tsx:28
- The new RTK Query request does not handle failures. A 403/404/network error is therefore presented as an empty references table (and a zero-item pager) without being reported through the application's error handler. Capture
isError/errorand calltrackError(new ApiError(error)), as other user-management requests do.
const { id } = useUserManagementContext()
const [page, setPage] = useState<number>(1)
const [pageSize, setPageSize] = useState<number>(DEFAULT_PAGE_SIZE)
const { data, isLoading } = useUserGetObjectDependenciesQuery({ id, page, pageSize })
assets/js/src/core/modules/user/management/detail/tabs/references/references-container.tsx:51
- After the first successful request, RTK Query keeps
isLoadingfalse while a new page is fetched and exposes the previousdatauntil that request completes. The pager therefore advances immediately while this table still displays rows from the old page with no loading state. ReadisFetchingfrom the query and pass it to the table (while keeping the pagination's initial-load handling unchanged) so stale rows are visibly pending.
<Table
data={ data?.items ?? [] }
isLoading={ isLoading }
- Track query errors via trackError(new ApiError(error)) on isError, matching the established pattern used elsewhere (use-user-management- draft.tsx, use-zip-download.tsx, custom-metadata-container.tsx, etc.) - previously a 403/404/network error just showed an empty table with no indication anything went wrong. - Pass isFetching (not isLoading) to the Table so page/pageSize changes show a pending state instead of silently displaying stale rows from the previous page while the new one loads. Pagination keeps isLoading for its own initial-load skeleton, unchanged. Note: the sibling required-by-panel.tsx (generic element Dependencies tab) this was modeled on has the same isLoading-only gap and no error handling either - left untouched since it's a separate, pre-existing component out of scope for this PR. Refs: pimcore/platform-version#308
|
Addressed both suppressed Copilot comments in f999c57 (rebased to 7cea893 after the auto-build bot):
Note: 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (1)
assets/js/src/core/modules/user/management/detail/tabs/references/references-container.tsx:59
- RTK Query keeps
datafrom the last fulfilled argument while a newly selected page is pending or rejected. If page 1 loaded and the page 2 request fails,pageremains 2 andisFetchingbecomes false, but this table renders page 1's rows as though they belonged to page 2. Use the hook'scurrentDatafor the rows (while retainingdataseparately if the pagination total should remain visible) so a failed page cannot display stale results.
<Table
data={ data?.items ?? [] }
isLoading={ isFetching }
/>
RTK Query's data retains the last fulfilled result across arg changes; currentData is only populated for the current query args. If page 1 loaded and switching to page 2 fails, data still held page 1's items while isFetching settled back to false - the table would render page 1's rows as if they belonged to page 2, with no visual indication. Use currentData for the table's rows (undefined during a pending or failed fetch, so the isFetching-driven loading state covers it correctly) while keeping data for the Pagination spread, so the last known totalItems stays visible instead of resetting during a transient failure. Verified against the actual installed RTK Query type definitions, not just Copilot's description of the behavior. Refs: pimcore/platform-version#308
|
Confirmed and fixed in b97d50f (rebased to f90e04c after the auto-build bot). Verified the claim directly against the installed RTK Query type definitions rather than taking it on faith: The table now reads 🤖 Generated with Claude Code |
studio-backend-bundle dropped totalItems from ObjectDependencies' OpenAPI required list specifically to avoid a TypeScript source break for SDK consumers: UserObjectDependencies is re-exported through this package's public SDK entry point (sdk/api/user/index.ts), and this package is actively published to npm, so any existing consumer code constructing or mocking a value of that type without totalItems would otherwise fail to compile against the new type. Verified the actual generated output, not just the OpenAPI spec: totalItems?: number. Refs: pimcore/platform-version#308
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (1)
assets/js/src/core/modules/user/management/detail/tabs/references/references-container.tsx:29
- The dependent endpoint counts all matching objects before applying each object's
listpermission, then filters unauthorized objects out of the selected page. As a result, this tab can show short or completely empty pages while the paginator reports a larger total (and can expose the exact count of hidden references). The backend should paginate and count the authorized result set, or return separate visible/hidden pagination metadata that this UI can represent accurately.
const { data, currentData, isLoading, isFetching, isError, error } = useUserGetObjectDependenciesQuery({ id, page, pageSize })
|
Per discussion, documented (not code-fixed) in pimcore/studio-backend-bundle@34d21edc: totalItems is a raw match count that never subtracts permission-denied objects, since accurately excluding them would require hydrating and checking every matching object across the whole dataset - exactly the unbounded cost this endpoint exists to avoid. That tradeoff is now stated plainly in the schema description, the totalItems property description, the endpoint's operation description, and the upgrade note: a page (or the embedded preview) can legitimately come back shorter than requested, or empty, while totalItems stays the same. No frontend changes needed - this is purely a documentation-text change on the backend side. 🤖 Generated with Claude Code |
ValeriaMaltseva
left a comment
There was a problem hiding this comment.
@jcPimcore
LGTM :)
As soon as pimcore/studio-backend-bundle#2011 is
merged, we can merge this one.
P.S. Please double-check that composer.json and docs.jsonopenapi.json are
updated properly.
* Add paginated object-dependencies endpoint for users, drop embedded array
ObjectDependenciesService::getDependenciesForUser() and the resolver it
called (DataObject\Service::getObjectsReferencingUser()) unconditionally
hydrate every DataObject referencing a user via a User-type field, and
the result was embedded in every GET /users/{id} response. This has the
same unbounded-hydration OOM risk as admin-ui-classic-bundle#1106, just
not yet reported for Studio.
Add ObjectDependenciesRepository, which reproduces the same class/field
discovery but counts matches per class first and only ever hydrates the
slice of objects that falls inside the requested page's offset/limit
window, regardless of total match count. Expose it via a new paginated
GET /user/{id}/object-dependencies endpoint (GetObjectDependenciesController),
modeled on the generic element Dependencies tab's CollectionController.
Drop the objectDependencies field from the main user payload/schema and
UserHydrator, since it's no longer embedded.
Refs: pimcore/platform-version#308
* Document the object-dependencies breaking change in upgrade notes
Note the removal of the embedded objectDependencies field from GET
/users/{id} and point at the new paginated GET /user/{id}/object-
dependencies endpoint, following the existing per-version bullet
convention in this file.
Refs: pimcore/platform-version#308
* Restore a bounded objectDependencies preview on GET /users/{id}
Avoid the BC break of dropping objectDependencies entirely: keep it on
the User schema, but as a small, deliberately-capped 20-item preview
(ObjectDependenciesPreview: totalItems + dependencies) built by calling
the same paginated ObjectDependenciesService added earlier with
page=1/pageSize=20, instead of the old unbounded hydration. Consumers
that need more than the preview use GET /user/{id}/object-dependencies.
Update the upgrade note accordingly: this is a schema shape change
(hasHidden removed, totalItems added, capped at 20), not a removal.
Refs: pimcore/platform-version#308
* Address Copilot review feedback on the object-dependencies pagination
- ObjectDependenciesRepository: join multi-field User conditions with OR,
not AND - an object referencing the user via any one of several
User-type fields was previously excluded from both the page and
totalItems unless every field matched. Also add a deterministic
ORDER BY id, since OFFSET/LIMIT paging without one isn't guaranteed
stable across requests (verified against real data: two adjacent
pages now have zero overlap and no gap).
- Extract the cross-class windowing math into a pure resolveClassWindow()
method and add a focused unit test for it (class-boundary pages, deep
offsets, exhausted budget, exact boundaries) - the surrounding
class-discovery/listing code still has no unit fixtures in this
bundle's suite, but the arithmetic that determines pagination
correctness now does.
- GetObjectDependenciesController: enforce a MAX_PAGE_SIZE of 100
(matching the largest selectable page size in the Studio UI) before
offset/limit are calculated, since CollectionParameters has no upper
bound and an unbounded pageSize would reintroduce the OOM this
endpoint exists to prevent. Also resolve and authorize the target
user (NotFoundException / admin-only-viewable-by-admins, mirroring
UserService::getUserById()) before querying dependencies, instead of
querying an arbitrary/nonexistent id and silently returning 200 with
an empty collection.
- PageSizeParameter gains an optional maxSize to surface the cap in the
OpenAPI schema; existing call sites are unaffected (defaults to no
maximum).
Refs: pimcore/platform-version#308
* Clarify that object-dependencies total count is in both header and body
The success-response description for GET /user/{id}/object-dependencies
copied this bundle's standard "with total count as header param"
phrasing, but PaginatedResponseTrait::getPaginatedCollection() also
serializes totalItems in the JSON body via the Collection DTO - true for
every endpoint using that trait, not just this one. Only fixing the one
description this PR introduced; the same incomplete phrasing predates
this change on ~15 other endpoints across the bundle and is out of scope
here.
Refs: pimcore/platform-version#308, review feedback on
pimcore/studio-ui-bundle#4008
* Eliminate the objectDependencies breaking change entirely
The bounded preview (previous commit) still replaced ObjectDependencies
(dependencies + hasHidden) with a differently-shaped ObjectDependenciesPreview
(totalItems + dependencies), dropping hasHidden. That was never necessary -
adding totalItems doesn't require removing hasHidden.
Restore the original ObjectDependencies class name and its dependencies/
hasHidden fields exactly as they were, and add totalItems alongside them
as a new, additive property. GET /users/{id} keeps returning the same
shape it always has, just with dependencies now capped at 20 (down from
unbounded) and one new field. hasHidden's scope narrows to the visible
preview window rather than scanning every referencing object, which is
an unavoidable, documented consequence of no longer scanning everything -
but the schema itself has zero breaking changes: nothing was removed or
renamed, nothing changed type.
Add ObjectDependenciesServiceInterface::getPreviewForUser(), used only by
UserHydrator, alongside the existing getPaginatedDependenciesForUser()
used by the new paginated endpoint - keeps the preview's original return
type separate from the generic Collection type the real pagination uses.
Refs: pimcore/platform-version#308
* Restore UserHydrator's original constructor parameter order
Purely cosmetic: ObjectDependenciesServiceInterface had moved from its
original 3rd position to last. Autowiring resolves by type so this
never affected anything functionally, but restoring the exact original
order removes even a theoretical positional-instantiation concern.
Refs: pimcore/platform-version#308
* Fix PHPStan return-type error on getObjectsReferencingUser()
Listing::load() is typed to return the broader DataObject[], not
Concrete[], even though a per-class dynamic Listing (e.g.
Issue1106\Listing) only ever loads Concrete instances of that class.
Narrow it with an instanceof filter so the inferred return type matches
the interface's declared array{items: Concrete[], totalItems: int} -
a no-op at runtime (verified: still returns the same 50/50000 against
the real repro data, and every item is genuinely Concrete already).
Verified with a real PHPStan run (installed standalone, not part of
this bundle's own composer deps) against this file and the rest of
src/User at this bundle's configured level 6: no errors.
Refs: pimcore/platform-version#308
* Fix CI test failure: Dependency is final and can't be mocked
CI failed with PHPUnit's ClassIsFinalException: Dependency is a
`final readonly class`, so makeEmpty()/mock generators can't create a
test double for it. It's a trivial constructor-only DTO anyway -
instantiate a real one instead of doubling it.
Verified the fixed test logic directly (same code path, real classes,
not just reasoning): items=1, totalItems=2 as expected.
Refs: pimcore/platform-version#308
* Address Copilot review: fix route typo, be precise about compat impact
- The upgrade note said GET /users/{id} (plural); the actual route is
GET /user/{id} (singular, GetUserController).
- "Not a breaking change" was an overclaim. The objectDependencies
*schema* genuinely has no breaking change (dependencies/hasHidden
keep their names, types, meaning; totalItems is purely additive),
but dependencies previously contained the complete list and now
caps at 20 - a real behavioral compatibility impact for any consumer
that assumed completeness rather than checking totalItems. State
that plainly instead of a blanket "not breaking" claim.
Refs: pimcore/platform-version#308
* Make totalItems optional in the schema to avoid an SDK source break
studio-ui-bundle is a real, actively-published npm package
(@pimcore/studio-ui-bundle), and UserObjectDependencies is re-exported
through its public SDK entry point (sdk/api/user/index.ts). Marking
totalItems as OpenAPI-required generated it as a non-optional TS field,
which is source-breaking for any SDK consumer constructing or mocking
a value of that type without it.
Drop totalItems from the schema's required list. The PHP property
itself is unaffected and always set - this only changes the generated
contract to totalItems?: number, which is backward compatible with
existing consumer code while still giving new consumers the real value
in every actual response.
Verified against real data: hasHidden=false, dependencies=20,
totalItems=50000 - unchanged at runtime, only the schema's required
list changed.
Refs: pimcore/platform-version#308
* Document that totalItems counts permission-hidden objects too
Per Copilot review feedback: totalItems is a raw match count that
never subtracts permission-denied objects, since accurately excluding
them would require hydrating and checking every matching object
across the whole dataset - exactly the unbounded cost this endpoint
exists to avoid. That's a legitimate tradeoff, but was previously
undocumented: a page (or the embedded preview) can come back shorter
than requested, or empty, while totalItems stays the same.
Documented in the ObjectDependencies schema description, the
totalItems property description, the paginated endpoint's operation
description, and the upgrade note.
Refs: pimcore/platform-version#308
* Address martineiber's review on object-dependencies pagination
Move pageSize validation and the admin-view permission check out of
GetObjectDependenciesController into ObjectDependenciesService, matching
the pattern already used in UserUpdateService. Split an overlong line in
ObjectDependenciesRepository (Sonar php:S103), simplify its Concrete filter
closure per suggestion, and trim the ObjectDependencies schema description
to stay under 100 chars.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Add repository-level coverage for the multi-field User condition
Extract the OR-joined User-field condition building out of
createListingForClass() into a pure buildUserFieldCondition() method,
mirroring the earlier resolveClassWindow() extraction, and unit test it
directly. Addresses the remaining part of Copilot's test-coverage
comment that's actually testable without real DB-backed class fixtures
(class-boundary pages and deep offsets were already covered by the
existing resolveClassWindow() tests).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
|



Changes in this pull request
Resolves: pimcore/platform-version#308 Studio UI
Resolves: pimcore/admin-ui-classic-bundle#1106 Studio UI
The user detail screen's "References" tab rendered the entire
user.objectDependencies.dependenciesarray embedded in theGET /users/{id}response, reproducing the same unbounded-hydration OOM risk as pimcore/admin-ui-classic-bundle#1106 on the frontend.docs.jsonopenapi.json+user-api-slice.gen.ts) against the new paginatedGET /user/{id}/object-dependenciesendpoint added in pimcore/studio-backend-bundle.references-container.tsxto page through the new endpoint with localpage/pageSizestate and a newcomponents/pagination/pagination.tsx, following the same pattern already used by the generic element Dependencies tab'srequired-by-panel. UsesuseTranslation()(not a directi18nextimport) so translated text updates on language change, and a sharedDEFAULT_PAGE_SIZEconstant instead of a duplicated magic number.USER_OBJECT_DEPENDENCIEScache tag inuser-api-slice-enhanced.ts(instead of reusingUSER_DETAIL), so editing/deleting a user doesn't force an unnecessary refetch of their object-dependencies pagination, mirroring the existingELEMENT_DEPENDENCIEStag pattern.objectDependencieson the generatedUsertype keeps its originaldependencies/hasHiddenfields exactly as they were; it only gains an additivetotalItemsfield. Nothing in this repo's UI reads that field at all (the References tab always calls the new paginated endpoint directly), so this is purely a type-sync change with zero runtime UI impact.Additional info
Full project
tsc --noEmitandeslintboth pass clean. The build was re-run locally multiple times after backend changes and verified byte-for-byte (checked for zero-byte/corrupted chunks, not just a clean exit code) to actually contain the current endpoint path/hook, not just the source.All 6 Copilot review comments on this PR were addressed with real code/doc changes (translation-key description clarified on the backend side for the one that originated there).
Depends on pimcore/studio-backend-bundle's
fix/308-paginated-user-object-dependencies.🤖 Generated with Claude Code