Artifact Registry: Add the repository edit flow
What does this MR do and why?
This change is behind the feature flag :artifact_registry_ui
Part 3 of 3, splitting !248123 (closed) into reviewable pieces. Delivers monolith/S07 Step 7 — the Vue edit flow, plus the slice of Step 5 that makes it reachable.
| MR | Targets | |
|---|---|---|
| 1 | Shared form extraction | 602638-repositories-list-step-4 |
| 2 | Create form followups | MR 1 |
| 3 | The edit flow — this MR | MR 2 |
This one is ~1,150 lines and that was a deliberate call. The read, the mutation, the view and the entry point were each small enough to split further, but separating them means shipping either a query nothing calls or a menu linking to a route that does not exist. Kept together, the whole flow is reviewable and testable in one place. Roughly 470 of the total is spec.
Changes
Local state. A single-repository read and an update mutation, backed by local Apollo resolvers like create, until the Artifact Registry GraphQL surface exists. The update input carries slug and name as identity and visibility/description as the only writable fields — name and format are immutable (ADR-009, ADR-022), so neither is writable and format is not in the input at all. The query is getArtifactRegistryRepositoryByName because the unsuffixed name already belongs to the Google Artifact Registry slice and @graphql-eslint/unique-operation-name is an error.
The view. A full page at :id/edit, not a modal, wrapping the shared form in its immutable-identity shape. States come from the prefill the way the list's do: skeleton while resolving, the not-found component alone when the repository does not resolve, the service-unavailable alert with the heading intact when the read fails. The form is not rendered until the prefill lands, because it seeds its own state from the repository once. The route param is the identity throughout, so nothing can drift between what was read and what is written. The heading carries the format's logo — the format is the one thing edit cannot change (prototype items 4 and 5).
The entry point. Nothing linked to a write flow before this, so the per-row actions menu S07 owns arrives with it. Its item carries a route location rather than a click handler, so it renders as a real link and opens in a new tab. Adding the column exposed cells aligning to the top of a row as tall as the menu, so every cell now centers vertically, and the header stays visible, right-aligned over the menu it names, per the additional-actions guidance in https://design.gitlab.com/components/table#additional-actions (prototype items 2 and 3).
The breadcrumb trail. The routes nest, because the trail is built from $route.matched and that holds ancestors only: a root node carries the Repositories crumb, a :id node carries the repository, and edit sits under it, so the trail reads Repositories / <repository-name> / Edit. It renders through an artifact-registry-owned component rather than the shared SpaBreadcrumbs, which cannot resolve a dynamic crumb under Vue 3. Both the route structure and that component are taken from !248296 (merged) so the two changes converge. A crumb can be shorter than the page it names, so meta.title is where a route names itself in full for the document title.
Cache: patched in place, no update callback. The payload selects name, which cache_config.js keys the type on, so Apollo normalizes the result onto the entry the list already holds. Evicting would drop a valid page to no purpose.
Screenshots or screen recordings
| List with the row actions menu | Edit |
|---|---|
![]() |
![]() |
Compare with the edit prototype.
How to set up and validate locally
- Enable the flag:
echo "Feature.enable(:artifact_registry_ui)" \| bundle exec rails c - Go to
/o/<your-org>/-/artifact_registry/acme/repositories/new/hostedand create a repository. You land on the list and the row now carries an Actions kebab. - Kebab → Edit. The breadcrumb trail reads Repositories / api-gateway / Edit; the form prefills; Name is read-only but still focusable; Format is absent; the heading carries the format logo.
- Change the description → Save changes → toast → back on the list.
- Kebab → Edit again. The new description is prefilled — this is the cache-patch proof.
- Direct-URL
:name/editfor a name that does not exist: the in-SPA not-found state renders, with no heading.
description and visibility has a single value; the toast and re-opening the form are the confirmation. And a full page reload wipes the mock store, because it is a module-level array, so a deep link after a reload always renders not-found. Keep step 2 onwards in one page session.
Agent Notes
Implemented by an AI coding agent, reviewed by @zcuddy before submission.
namerendersreadonly, per review. A disabled input leaves the tab order and some screen readers skip it, so the name a viewer cannot change would also be a name they cannot reach;readonlykeeps it focusable with near-identical styling.- The no-
update-callback decision was verified, not assumed — an@client-only mutation does normalize its payload onto the keyed entity; checked directly against the cache rather than inferred from the docs. - A spec detail worth a look: the edit spec backs its resolvers with a small store rather than fixed values. The read is an
@clientfield, so Apollo re-runs it on every cache broadcast, and a resolver that always answered the pre-edit repository would silently undo the patch the spec is asserting. - Delete is Step 8. The menu is shaped so a danger action item drops in beside Edit without restructuring.
- 203/203 green under both Vue 2 and Vue 3.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

