Skip to content

feat(database): match desktop's relation creation dialog - #482

Merged
appflowy merged 7 commits into
mainfrom
feat/relation-dialog-desktop-parity
Aug 18, 2026
Merged

feat(database): match desktop's relation creation dialog#482
appflowy merged 7 commits into
mainfrom
feat/relation-dialog-desktop-parity

Conversation

@appflowy

@appflowy appflowy commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

What

Brings the Add relation property flow in line with the Flutter client
(relation_creation_dialog.dart). Behaviour, layout, tokens and copy were all
diffed against desktop rather than approximated.

Behaviour changes

Before (web) Now (desktop parity)
Property name on target pick stayed "Relation" named after the target database
Reciprocal name filled on two-way toggle, from source filled on target pick, from source
Self-relation shown as an ordinary database "This database", icon suppressed, Related X / Related back to X copy, "Inverse related property name" label
Target list always-visible inline list + search closed dropdown, no search
Property icon icon picker, as desktop
Summary panel rendered removed (desktop has none)

On the dropdown having no search: AFDropDownMenu.updateFilteredItems
short-circuits to the full list whenever isInput is false, and this dialog
constructs it without that flag — so desktop genuinely has no search here.

Styling

The appflowy_ui tokens map 1:1 onto the web scale — tailwind.config.cjs
uses the same 100–600 ramp (4/6/8/12/16/20) as AppFlowySpacing /
AppFlowyBorderRadius, and tailwind/new-colors.cjs is generated from the
same token JSON as the Flutter theme. So textColorScheme.primary
text-text-primary, fillColorScheme.contentHoverbg-fill-content-hover,
shadow.smallshadow-popover, and so on.

Ported: 420px width, left-aligned heading4.prominent title, section labels
as body.standard on text-primary (not muted captions), AFTextField(size: m) metrics, the _DropdownMenuButton 7px/5px border inset, the AFPopover
layer-01 + shadow panel, and hover-only item backgrounds — DatabaseMetaItem
and LimitOptionItem supply custom builders, so neither tints the selected
row; the limit list marks selection with a tick instead.

Two icons had no web equivalent and were ported, converted to currentColor:
two_way_relation.svg and count.svg.

Localization

The dialog was effectively English-only — every string came from an inline
defaultValue, so no locale ever translated it. Added the 14
grid.relation.* keys that already ship translated on desktop and dropped
the literals.

This changes visible copy where web had invented its own wording:

key was (web) now (desktop)
limitOnePage One page 1 page only

Shared modal footer

Second commit swaps NormalModal's MUI buttons for the token-based Button,
whose variants are already exact ports of the AF button specs; only padding
and weight were off. This touches all 49 modal call sites, so it was checked
first — none pass MUI-specific props, caller classNames are merged rather
than overwritten (so HIDDEN_BUTTON_PROPS still works), and type="button"
is set explicitly since MUI defaulted to it and the design-system button does
not.

Testing

  • tsc clean; 319 suites / 3038 tests pass (the one excluded failure,
    invitation.integration, needs a live backend and is unrelated).
  • Unit tests rewritten for the dropdown flow, now covering closed-by-default,
    auto-naming, self-relation copy and limit selection. The i18n mock resolves
    against the real en.json so a missing key fails the test.
  • DatabaseTemplateButton asserted the MUI class MuiButton-containedError
    to prove the delete confirm is destructive; now asserts the token
    equivalent, same intent.
  • Both Playwright helpers updated for the dropdown.

⚠️ Not verified in a browser — everything above is unit-level. Worth an
E2E run before merge, since the two updated Playwright helpers are the path
that would catch a real integration problem.

🤖 Generated with Claude Code

Summary by Sourcery

Match relation creation and editing flows to the desktop client while standardizing modal actions and related UI behavior.

New Features:

  • Align the relation creation dialog with the desktop experience, including database and limit dropdowns, property icon selection, self-relation handling, and desktop-compatible naming behavior.
  • Add desktop-parity relation property controls for selecting related databases, limits, and reciprocal names from the property menu.

Bug Fixes:

  • Use localized relation dialog copy instead of inline English fallbacks.
  • Preserve consistent avatar fallback colors by hashing the full display name rather than only the initial.

Enhancements:

  • Update shared modal footers to use the token-based design-system buttons while preserving destructive actions and caller customization.
  • Improve relation database candidate resolution with database container names and hierarchical paths.

Tests:

  • Update relation dialog unit coverage for dropdown behavior, automatic naming, self-relations, localization, and limit selection.
  • Update Playwright relation helpers and modal button assertions for the revised controls and styling.

Chores:

  • Add the relation and two-way relation icons required by the desktop-parity UI.

appflowy and others added 2 commits August 18, 2026 17:29
Ports `relation_creation_dialog.dart` so adding a Relation property behaves
and reads the same on both clients.

Behaviour:
- Picking a target database now names the property after it and the
  reciprocal after the source, mirroring `_RelatedToSection.onSelect`.
- Self-relations are first class: the trigger reads "This database" with its
  icon suppressed, the name fields use the "Related X" / "Related back to X"
  copy, and the reciprocal label becomes "Inverse related property name".
- The target list moved behind a dropdown trigger, matching `AFDropDownMenu`
  in its non-input mode (which has no search — `updateFilteredItems`
  short-circuits to the full list when `isInput` is false).
- Added the property icon picker desktop has, wired to the existing
  `useUpdatePropertyIconDispatch`.
- Dropped the summary panel; desktop's dialog has no such section.

Styling follows the appflowy_ui tokens, which map 1:1 onto the web scale
(spacing 4/6/8/12/16/20, radius m = 8, body 14/400, heading4 16/700). Two
icons with no web equivalent were ported and converted to currentColor.

The dialog was previously English-only: every string came from an inline
`defaultValue`. Added the 14 `grid.relation.*` keys that already ship
translated on desktop and removed the literals. This changes visible copy
where web had invented its own — notably "One page" becomes "1 page only".

Both Playwright helpers clicked candidates directly, so they now open the
dropdown first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NormalModal still used MUI buttons while the rest of the app moved to the
token-based `Button`, whose `default`/`outline` variants are already exact
ports of `AFFilledTextButton.primary` / `AFOutlinedTextButton.normal`. Only
the metrics were off, so the footer now also applies desktop's
`AFButtonSize.m` pairing (px-4, font-medium) instead of the tighter default.

This touches every modal in the app, so the swap was checked first: no
caller passes MUI-specific props to `okButtonProps`/`cancelButtonProps` —
only `disabled`, `className` and `data-testid`. Caller classNames are merged
rather than overwritten so `HIDDEN_BUTTON_PROPS` still hides footers, and
`type="button"` is set explicitly because MUI defaulted to it and the
design-system button does not (a bare button inside a form would submit).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Aligns the web Relation Creation dialog and shared modal footer with the Flutter/desktop implementation: introduces a new AF-style dropdown UI for database/limit selection, updates naming and self-relation behavior, wires in icon selection and full i18n keys, and replaces MUI buttons in NormalModal with design-system Button components, along with updated tests and Playwright helpers.

Sequence diagram for the updated relation creation flow

sequenceDiagram
    actor User
    participant RelationCreationDialog
    participant AFDropdown as AFDropdown
    participant NormalModal as NormalModal
    participant IconDispatch as useUpdatePropertyIconDispatch
    participant OnCreate as onCreate

    User->>RelationCreationDialog: open (open=true, fieldId, initialFieldName)
    RelationCreationDialog->>IconDispatch: useUpdatePropertyIconDispatch(fieldId)

    User->>RelationCreationDialog: click relation-field-icon-trigger
    RelationCreationDialog->>IconDispatch: updateIcon(iconValue)

    User->>AFDropdown: click relation-database-trigger
    AFDropdown-->>RelationCreationDialog: show candidates

    User->>AFDropdown: click relation-candidate-databaseId
    AFDropdown-->>RelationCreationDialog: handleSelectCandidate(candidate)
    RelationCreationDialog->>RelationCreationDialog: setSelectedDatabaseId
    RelationCreationDialog->>RelationCreationDialog: setFieldName / setReciprocalFieldName
    AFDropdown-->>User: close dropdown

    User->>RelationCreationDialog: toggle relation-two-way-switch
    RelationCreationDialog->>RelationCreationDialog: setIsTwoWay(true)
    RelationCreationDialog->>RelationCreationDialog: show reciprocalFieldName input

    User->>NormalModal: click modal-ok-button
    NormalModal->>RelationCreationDialog: onOk()
    RelationCreationDialog->>OnCreate: onCreate(RelationCreationResult)
    RelationCreationDialog->>RelationCreationDialog: reset local state
    RelationCreationDialog-->>User: dialog closed via onOpenChange(false)
Loading

File-Level Changes

Change Details Files
Rebuilt the RelationCreationDialog UI to match desktop behavior and styling, including AF-style dropdowns, auto-naming logic, self-relation handling, and icon selection.
  • Reduced modal width to 420px and changed the title to a left-aligned, styled ReactNode while switching the primary button label to the shared button.add key.
  • Introduced SectionLabel, AFDropdown, and AFDropdownItem components to mimic AFDropDownMenu with a non-searching, popover-based candidate and limit selector.
  • Removed the inline search input and always-open candidate list; the database list now appears only after clicking the new dropdown trigger.
  • Implemented handleSelectCandidate to auto-name the relation property and reciprocal field based on the selected database, including special "Related X" / "Related back to X" copy for self-relations.
  • Hooked up property icon editing via CustomIconPopover, useUpdatePropertyIconDispatch, and FieldCustomIcon, and added new SVG icons (two_way_relation, count, alt_arrow_down) plus PageIcon integration.
  • Changed limit selection from two buttons to a dropdown backed by RelationLimit, using localized grid.relation.limit* labels without inline defaults.
  • Updated the two-way relation section to a bordered card with a switch, desktop copy, and a reciprocal name input whose label changes for self-relations; removed the old textual summary panel.
  • Made RelationCreationDialog accept a required fieldId and updated PropertyMenu to pass it so icon edits can be scoped.
src/components/database/components/property/relation/RelationCreationDialog.tsx
src/components/database/components/property/PropertyMenu.tsx
src/assets/icons/count.svg
src/assets/icons/two_way_relation.svg
Reworked RelationCreationDialog tests around the new dropdown-based flow, desktop naming behavior, and real translation bundle, and updated Playwright helpers to open the dropdown before selecting candidates.
  • Replaced the mock i18n implementation with a translate helper that reads from the real en.json so tests assert actual copy and fail on missing keys.
  • Added shared fixtures (currentGrid/currentContainer/relatedGrid/relatedContainer) and helpers to mock views, database context, and view meta loading via getViews and loadViewMeta.
  • Introduced utility helpers (renderDialog, openDatabaseDropdown, fieldNameValue, reciprocalNameValue) to simplify repeated interactions.
  • Updated tests to assert: closed-by-default candidate list, per-database naming in the dropdown, auto-naming of relation and reciprocal fields (including self-relations), limit dropdown options and default, and fallback behavior when batch getViews fails.
  • Adjusted Playwright database/relation helpers to click the new relation-database-trigger test id before expecting relation candidates to exist.
src/components/database/components/property/relation/RelationCreationDialog.test.tsx
playwright/support/database-ui-helpers.ts
playwright/support/relation-test-helpers.ts
src/@types/translations/en.json
Swapped NormalModal from MUI Button to the design-system Button, aligning footer behavior and styles with AF button specs and updating dependent tests.
  • Imported the design-system Button and cn utility, defined ModalButtonProps, and added a FOOTER_BUTTON_CLASS to enforce AF-like padding and font weight.
  • Changed okButtonProps and cancelButtonProps to use ModalButtonProps, and wired cn-based className merging so caller classes still apply (e.g., HIDDEN_BUTTON_PROPS).
  • Updated the cancel and ok buttons to use type="button", design-system variants (outline/default/destructive), and kept loading/disabled behavior with CircularProgress.
  • Updated DatabaseTemplateButton.test to assert the destructive confirm button via its bg-fill-error-thick class instead of the previous MUI MuiButton-containedError class.
src/components/_shared/modal/NormalModal.tsx
src/components/database/components/template/__tests__/DatabaseTemplateButton.test.tsx
Aligned RelationPropertyMenuContent strings and limit labels with shared localized keys instead of inline defaults, reusing the same i18n keys as desktop.
  • Simplified relationLimitLabel to use grid.relation.limitOnePage and grid.relation.limitNoLimit without defaultValue fallbacks.
  • Ensured menu labels for limit and two-way relation use localized grid.relation.limit, grid.relation.twoWayRelation, and dialog copy (grid.relation.disableTwoWayRelation*, button.cancel, button.confirm) instead of inline English strings.
src/components/database/components/property/relation/RelationPropertyMenuContent.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

🥷 Ninja i18n – 🛎️ Translations need to be updated

Project /project.inlang

lint rule new reports level link
Missing translation 504 warning contribute (via Fink 🐦)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The new grid.relation.selectADatabse i18n key appears to be misspelled; consider renaming it (and its usages) to something like grid.relation.selectADatabase before this typo propagates across clients.
  • In RelationCreationDialog.test.tsx the mock for CustomIconPopover types children as ReactNode but does not import ReactNode; adding the import (or dropping the explicit type) would avoid type/check-time issues.
  • The custom AFDropdown implementation currently only closes on mouse clicks and lacks Escape/keyboard navigation handling; if you want to keep parity with other menus and improve accessibility, consider adding basic keyboard support.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `grid.relation.selectADatabse` i18n key appears to be misspelled; consider renaming it (and its usages) to something like `grid.relation.selectADatabase` before this typo propagates across clients.
- In `RelationCreationDialog.test.tsx` the mock for `CustomIconPopover` types `children` as `ReactNode` but does not import `ReactNode`; adding the import (or dropping the explicit type) would avoid type/check-time issues.
- The custom `AFDropdown` implementation currently only closes on mouse clicks and lacks Escape/keyboard navigation handling; if you want to keep parity with other menus and improve accessibility, consider adding basic keyboard support.

## Individual Comments

### Comment 1
<location path="src/components/database/components/property/relation/RelationCreationDialog.tsx" line_range="513" />
<code_context>
+                  </span>
+                </>
+              ) : (
+                <span className='min-w-0 flex-1 truncate text-text-tertiary'>{t('grid.relation.selectADatabse')}</span>
+              )
+            }
</code_context>
<issue_to_address>
**issue (typo):** Fix the typo in the `selectADatabse` i18n key to avoid inconsistent naming

The `grid.relation.selectADatabse` key is misspelled, which makes it harder to discover and reuse (e.g., via search). Please rename it to something like `grid.relation.selectADatabase` and update the translation bundle accordingly.

Suggested implementation:

```typescript
                <span className='min-w-0 flex-1 truncate text-text-tertiary'>{t('grid.relation.selectADatabase')}</span>

```

You will also need to update your i18n translation bundle(s), for example:
- In the relevant locale JSON/YAML (or TS) file(s), rename the key `grid.relation.selectADatabse` to `grid.relation.selectADatabase`, or add `grid.relation.selectADatabase` with the same value and remove the old one.
- If there are any other usages of `grid.relation.selectADatabse` elsewhere in the codebase, update them to `grid.relation.selectADatabase` for consistency.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

</span>
</>
) : (
<span className='min-w-0 flex-1 truncate text-text-tertiary'>{t('grid.relation.selectADatabse')}</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (typo): Fix the typo in the selectADatabse i18n key to avoid inconsistent naming

The grid.relation.selectADatabse key is misspelled, which makes it harder to discover and reuse (e.g., via search). Please rename it to something like grid.relation.selectADatabase and update the translation bundle accordingly.

Suggested implementation:

                <span className='min-w-0 flex-1 truncate text-text-tertiary'>{t('grid.relation.selectADatabase')}</span>

You will also need to update your i18n translation bundle(s), for example:

  • In the relevant locale JSON/YAML (or TS) file(s), rename the key grid.relation.selectADatabse to grid.relation.selectADatabase, or add grid.relation.selectADatabase with the same value and remove the old one.
  • If there are any other usages of grid.relation.selectADatabse elsewhere in the codebase, update them to grid.relation.selectADatabase for consistency.

appflowy and others added 5 commits August 18, 2026 19:52
The creation dialog was ported but the property menu still diverged from
`type_option_editor/relation.dart`. Rebuilt it row for row:

- Every row now carries its leading icon in `iconColorScheme.primary`:
  `database_s` for the data source, `count_s` for the limit, and the two-way
  glyph for the reciprocal toggle.
- The database row gained its "Data source" title; the database name moved
  into the trailing slot as `caption.standard` in `textColorScheme.secondary`,
  preceded by the target's icon.
- Two-way relation is a submenu showing On/Off, not an inline switch. The
  submenu is desktop's `_TwoWayRelationPopoverContent`: an "Enable" toggle, a
  divider, then the reciprocal name field, disabled while two-way is off and
  debounced on write (empty input is ignored, as on desktop).
- The reciprocal name now seeds from the relation field's own name rather
  than the source database name, matching `initState`/`onTwoWayToggle`.
- Dropped the disable-two-way confirmation dialog; desktop writes
  `isTwoWay: false` directly, and the submenu is already a two-step
  interaction.
- The section header is `caption.standard` (12px/400), not 12px/500.

Also fixes the Relation field-type icon: `relation.svg` held desktop's
*two-way* glyph (two crossing arrows), so every relation column header and
the type row rendered the wrong symbol. Replaced with desktop's one-way
`relation.svg` geometry — a single diagonal arrow.

Verified in the browser against the running stack, not just in unit tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The data-source picker listed the *registered* database views, which are the
inner views and are almost always named "Grid" — so nearly every entry read
"Grid" instead of the database's name. `useRelationData` already resolved the
container name, but only for the currently selected view, never for the list.

Resolve candidates once from the folder outline the hook already loads:

- `databaseCandidates` pairs each `(databaseId, viewId)` with the container
  that owns the user-facing name, and with the registered view's ancestors.
- The picker renders desktop's `_DatabaseListItem`: icon, name, and the
  ancestor path as a two-line subtitle, elided to `first / ... / last` when
  deeper than two, with the tick on the selected row.
- Added the search field from `_DatabaseList`, filtering on the database name,
  falling back to "No results".

Resolution reuses the cached outline via `findAncestors`, so no extra fetches
are needed — unlike the creation dialog, which has no outline to hand and
batches its own view lookups.

Verified against the running stack: the list now reads "employees /
General / employees", "5000_employees / Big Database / 5000_employees", and
search narrows it correctly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two issues from a pass with the Vercel React performance rules:

`js-index-maps` — `findAncestors` restarts from the outline root on every
call, so resolving N databases re-walked the whole tree N times. Collect the
registered view ids into a Set up front and record ancestry during a single
traversal, copying the path only for the handful of matching nodes.

`rerender-derived-state-no-effect` — the reciprocal name was mirrored from
the type option into state inside an effect, costing an extra commit per
change and needing an `eslint-disable` that hid `fieldName` from the
dependencies. Adjust during render instead: keep a nullable draft for the
in-flight edit and fall back to the persisted value, so an external change
(our own debounced write, or a remote edit) is picked up without an effect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`AvatarFallback` derived its colour from `children` whenever that was a
string, and only fell back to the `name` prop otherwise. Several call sites
render just the initial, so the colour came from hashing one character:
"Nathan Foo" hashes to badge 7, "N" to badge 19, meaning the same person
appeared olive in a grid cell and a different colour in the person filter
menu. Call sites that passed `name` explicitly were silently overridden too —
MembersPanel supplied `name={m.name}` but rendered the initial as its child,
so the prop never took effect.

Make `name` authoritative, matching desktop's `AFAvatar`, where the colour
always comes from `colorHash ?? name` and the initial is only ever the
rendered glyph. Then pass the full name at the three sites that had none:
the person filter menu, the advanced filter panel, and the row-comment
member avatar.

The hash itself already matches desktop byte for byte, including the
workspace icon, which runs the same `randomColorFromDesignSystem` over the
workspace name and lands on the same badge slot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@appflowy
appflowy merged commit 2d8037a into main Aug 18, 2026
15 of 16 checks passed
@appflowy
appflowy deleted the feat/relation-dialog-desktop-parity branch August 18, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant