Skip to content

feat(lists): GitHub-backed lists — repo picker, repo link, private tag, refresh, locked schema (#50) - #113

Merged
Adron merged 1 commit into
parity/queuefrom
issue/50-github-backed-lists
Sep 16, 2026
Merged

Adron merged 1 commit into
parity/queuefrom
issue/50-github-backed-lists

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Closes #50. Part of epic #49.

What changed

Repo picker on creation. The Lists FAB now opens a "New list" sheet with a Local / GitHub-backed
choice. The GitHub half picks a repository, optionally scoped by organisation, and sends
source: "github" + githubRepo + githubSource: "issues".

Which module owns the GitHub calls, and why. :feature:lists calls /api/github/… through its
own Retrofit interface and DTOs. :feature:integrations's IntegrationsApi/GitHubDto/
GitHubMapper are module-internal, and the repo convention is that no feature module depends on
another — a cross-module dependency would also have collided with the in-flight #40 work. The
duplication is ~100 lines of lenient DTOs, documented as deliberate in GithubDtos.kt.

Repository link + Private repo tag. GithubRepoLinkRow renders owner/repo issues under the
title, opening the repo's issues page. The tag copy is pinned by test and gets the distinction the
help text insists on: "This repository is private on GitHub… That is separate from who can see this
list"
, plus the sign-in / "not found" warning — and it never says "private list".
githubRepoPrivate: null (never synced) shows no tag — unknown is not rendered as public.

Refresh from GitHub appears in the top bar only for a GitHub-backed list, and the ViewModel also
refuses to spend the request on a local one (the endpoint 400s).

Row edits → issue operations. The row writes already used the correct { data } / no-If-Match
shape. What was missing was isReadOnly: SchemaMapper now parses it (plus the
propertyKey/propertyName/propertyType/validationRules spellings the synthetic GitHub schema
uses), so Issue #, Link, Created and Updated display but are never sent. The row form
names the operation ("Saving opens issue #42 in owner/repo") and delete reads "Close issue on
GitHub".

Schema editor locked to parent-only. Columns render read-only with an explanation; add / remove /
retype / save are refused in the ViewModel as well as hidden. A parent-list picker
(PUT /api/lists/{id} with parentId) is the one remaining edit.

GET /api/github/orgs drives the picker's organisation filter chips (?org=), and
next-issue-number drives the add-row hint. Both fit; neither was forced.

Unlinked case is not a broken picker: GithubLinkProblem distinguishes not linked from token
refused / missing Issues scope
, each with its own explanation and a route to the connected-accounts
screen. An empty-but-successful repo list is explained separately as an org OAuth-approval problem.
#39 (OAuth linking) was not attempted — it remains blocked.

Live-API facts established while working

  • POST /api/lists with source: "github" and no repo →
    400 {"error":"githubRepo is required for GitHub-backed lists (format: owner/repo)","code":"bad_request"},
    and the same for a repo with no slash. Client-side validation now matches the server exactly.
  • GET /api/github/repos and /api/github/orgs return bare arrays;
    next-issue-number returns {"nextNumber": n}.
  • githubSource is not validated before the GitHub call, so its accepted values could not be
    confirmed. "issues" is used because that is the only mapping documented — an informed
    assumption, not a verified one.
  • On the test account GET /api/github/repos and /api/github/orgs both return 200 [], so an
    end-to-end create could not be completed here.

Verification

./gradlew clean :app:assembleDebug testDebugUnitTest → BUILD SUCCESSFUL, 1079 tests, 0
failures
. New: GithubRepoLinkTest (copy), DefaultListsRepositoryGithubTest (create payload,
refresh route, synthetic schema, re-parent), DefaultGithubRepositoryTest (repos/orgs/
next-issue-number, unlinked/refused mapping), NewListViewModelTest, plus additions to
SchemaEditorViewModelTest and ListDetailViewModelTest. Compose tests compile, not executed (no
emulator) — which is why the tag's copy is also asserted by a JVM test that does run.

ListsDatabase v2 → v3, destructive migration already configured.

Risks / follow-ups

  • The parent picker cannot clear a parent, because explicitNulls = false drops a null from
    the body. The module has the same pre-existing limitation for folderId. Clearing needs an
    explicit-JsonNull request shape.
  • One behaviour change beyond the issue: ListsViewModel.createList was removed as dead code
    once NewListViewModel took over creation for both kinds. The FAB no longer creates an untitled
    "New list" instantly — it opens the sheet.

…ocked schema

A list can now be created from a GitHub repository's issues, and the detail
screen operates one honestly.

Create: the "New list" sheet gains a Local / GitHub-backed choice. The GitHub
half picks a repository (optionally scoped to an organisation via
`GET /api/github/orgs` → `GET /api/github/repos?org=`) and sends
`source: "github"` with `githubRepo` and `githubSource: "issues"`. The
`owner/repo` form is validated client-side because the server refuses anything
else with `400 bad_request` before it ever reaches GitHub.

Repo link: a GitHub-backed list links `owner/repo issues` under its title. When
the repository is private on GitHub the link carries a **Private repo** tag whose
copy says, explicitly, that it describes the *repository* and not the list, and
warns that an invited collaborator may still meet a GitHub sign-in or "not found"
page. An unrecorded visibility (`githubRepoPrivate: null`) shows no tag rather
than being presented as public. Visibility is re-read from each list payload, so
a sync picks up a repository that changed.

Refresh: `POST /api/lists/{id}/refresh` is surfaced as "Refresh from GitHub", and
only on a GitHub-backed list — the endpoint 400s on a local one.

Rows are issues: the schema mapper now reads `isReadOnly` (and the ListProperty
`propertyKey`/`propertyName`/`validationRules` spellings the synthetic GitHub
schema uses), so `Issue #`, `Link`, `Created` and `Updated` are shown but never
sent. The row form names the operation a save performs, using
`GET /api/github/repos/{owner}/{repo}/next-issue-number` to say which issue is
about to be opened, and delete is labelled as closing the issue.

Schema editor: locked to parent-only for these lists. The fixed columns render
read-only with an explanation, add/remove/retype and save are refused in the
ViewModel as well as hidden, and a parent-list picker (`PUT /api/lists/{id}`
with `parentId`) is the one edit that remains — silently allowing a schema edit
would only produce a confusing server error.

Unlinked GitHub is handled as a first-class state: an unlinked account (400
"GitHub account not linked") and a refused token (401 `github_error`) each get
their own explanation and a route to the existing connected-accounts screen,
rather than an empty picker. OAuth linking itself (#39) is not attempted.

`:feature:lists` calls `/api/github/…` through its own Retrofit interface and
DTOs rather than depending on `:feature:integrations`, since no feature module in
this repo depends on another.

Extends #54's `CreateListRequest` rather than duplicating it: `githubRepo` and
`githubSource` join the existing `parentId`/`folderId`/`messageId`/`initialRows`/
`metadata`/`source` options, all still omitted when absent.

Tests: create sends `githubRepo`/`githubSource` (and a local create still sends
neither); the schema editor is locked for a GitHub-backed list and unlocked for a
local one; refresh updates rows and is never spent on a local list; the Private
repo tag renders from `githubRepoPrivate` and its copy is pinned to name the
repository, not the list.

Closes #50
@Adron
Adron merged commit 48fa368 into parity/queue Sep 16, 2026
1 check passed
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