Skip to content

Add a Read-Only Redmine Tickets View to the Tickets Page - #536

Open
SarkarShubhdeep wants to merge 4 commits into
redmine-integrationfrom
redmine/redmine-tickets-view
Open

SarkarShubhdeep wants to merge 4 commits into
redmine-integrationfrom
redmine/redmine-tickets-view

Conversation

@SarkarShubhdeep

@SarkarShubhdeep SarkarShubhdeep commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

TL;DR

Adds a "Redmine Tickets" view to the tickets page — a read-only list of the signed-in user's Redmine issues, switched from the existing "Tickets v1" list via a heading dropdown. Issues flow Redmine → TimeHuddle only; nothing is written to Redmine. This is Milestone 2 of huddle_redmine_clock.md.

Note: the base branch redmine-integration does not yet contain Milestone 1, so this PR also carries the two M1 commits (connect a personal Redmine account). The M2 work is the single commit feat(redmine): read-only Redmine Tickets view (Milestone 2).

"Redmine Tickets" view on Tickets page

CleanShot.2026-09-10.at.15.58.47.mp4

Syncing a New Assigned ticket on Huddle

CleanShot.2026-09-10.at.16.04.24.mp4

Overview

A user should not have to open Redmine to see what is assigned to them. Milestone 1 let a user link their personal Redmine API key in Settings. Milestone 2 uses that key to fetch and render their issues inside TimeHuddle, read-only, with a scope toggle, search, and click-through links back to Redmine.

Current State

  • Users can connect/disconnect a personal Redmine account in Settings (M1), and the encrypted API key is stored in redmine_links.
  • redmine-client.js only implements getCurrentUser — there is no way to read issues.
  • The tickets page renders a single hard-coded view with a static h1 supplied by the route registry.
  • Redmine issues are invisible inside TimeHuddle; users must open Redmine to see their assignments.

Proposed Changes

1. Backend — read Redmine issues

  • redmine-client.listIssues(apiKey, { scope, limit, offset })GET /issues.json, adding assigned_to_id=me for scope: 'mine'. Pagination bounded by limit/offset (Redmine caps limit at 100). Returns the raw issues array.
  • redmine-issues.js — pure, Meteor-free shaping into a minimal client-safe DTO: id, subject, project, status, assignedTo. Tracker, priority, due date, custom fields, etc. are intentionally dropped.
  • Meteor method redmine.issues.list({ scope }) — validates scope, decrypts the caller's own key, calls Redmine, returns { connected, baseUrl, issues }.
    • No link → { connected: false, issues: [] } so the view renders its not-connected state without a second round-trip.
    • 401/403invalid-key; anything else → unreachable. The API key is never returned to the client.
  • Wormhole.expose('redmine.issues.list', …) in main.js for REST access.

2. Frontend — data layer

  • redmineApi.issues.list(scope) plus RedmineIssue, RedmineNamed, RedmineIssueList, and RedmineScope types in src/lib/api.ts.
  • Deliberately a separate, thin data layer — Redmine issues are not adapted into the internal Ticket shape, and no generic "ticket source" abstraction is introduced (YAGNI/KISS).

3. Frontend — RedmineTicketsView

  • Built entirely from @mieweb/ui primitives (Card, Table, Badge, Button, Input, Select, Spinner, Text, Tooltip).
  • Scope toggle "Assigned to me" / "All" drives the server fetch; the header assignee filter refines what was already loaded, with its options derived from the assignees present in the fetched issues (no Redmine users API needed).
  • Client-side search across issue id, subject, and project.
  • Rows link out to {baseUrl}/issues/{id} in a new tab.
  • States: loading, empty, error, and not connected (card linking to Settings) — this closes the M1 item deferred into M2.
  • "New Ticket" is disabled with a "Read only" tooltip in this view.
  • Caching: fetch-on-view + manual "Refresh" + a module-level per-scope session cache so toggling views does not refetch. A request sequence guard prevents a slow response for an older scope from overwriting a newer one. No server-side cache in v1.

4. Tickets page view switcher

  • The page h1 is replaced by a Dropdown toggling Tickets v1 / Redmine Tickets.
  • TicketsPage stays always-mounted (its state is preserved); the v1 subtree is hidden rather than unmounted when the Redmine view is active.
  • AppLayout now always withholds the registry PageTitleContext value from the tickets instance, since the feature owns its own heading — this avoids a duplicate h1.

Acceptance Criteria

  • A connected user can switch to "Redmine Tickets" and see their assigned Redmine issues.
  • The scope toggle switches between "Assigned to me" and everything the key can see.
  • Search filters by issue id, subject, and project.
  • The assignee filter is populated from the fetched issues.
  • Each row links out to the correct Redmine issue URL in a new tab.
  • Loading, empty, error, and not-connected states all render correctly.
  • "New Ticket" is disabled with a "Read only" tooltip in the Redmine view.
  • Switching back to "Tickets v1" leaves that view completely unchanged.
  • Only the minimal DTO fields cross the wire; the API key never reaches the client.
  • npm run typecheck passes; redmine-issues unit tests pass.

Out of Scope (for Now)

  • Creating, editing, or deleting Redmine issues from TimeHuddle — issues are read-only.
  • Starting a timer against a Redmine issue (Milestone 3) and the source-aware WorkItem.
  • Pushing time entries back to Redmine (Milestone 5).
  • Server-side caching or background refresh of Redmine issues.
  • Server-side pagination UI — the first page (limit=100) is fetched.
  • A Redmine issue detail page inside TimeHuddle — rows link out instead.
  • Redmine issues in the Huddle composer / TicketPicker.

Testing

  • npm run typecheck — clean.
  • meteor-backend/tests/redmine-issues.test.ts — unit tests for DTO shaping (pure, no server needed).
  • meteor-backend/tests/redmine.test.ts — method-level tests; these require a running Meteor test server on :3101 and skip without it.
  • Manual: connect a key in Settings → switch to Redmine Tickets → toggle scope → search → click through to Redmine; then disconnect and confirm the not-connected state.

Notes for Reviewers

  • docker-compose.override.yml is intentionally not committed — it is a local-only dev override.
  • huddle_redmine_clock.md is included so the milestone plan and the decisions made along the way (separate data layer, cache strategy, scope-toggle semantics) are reviewable alongside the code.

Add a second view to the tickets page, switched via a heading dropdown, that
lists the connected user's Redmine issues read-only.

- redmine-client.listIssues() -> GET /issues.json (assigned_to_id=me | all)
- redmine-issues.js shapes a minimal, client-safe DTO (id, subject, project,
  status, assignedTo); everything else Redmine returns is dropped
- Meteor method redmine.issues.list({ scope }) + Wormhole.expose for REST
- redmineApi.issues.list() + RedmineIssue types on the frontend
- RedmineTicketsView: scope toggle, client-side search, assignee filter derived
  from fetched issues, per-scope session cache, refresh, external row links,
  loading / empty / not-connected states
- New Ticket is disabled with a Read only tooltip in the Redmine view
- Tickets page owns its heading, so AppLayout withholds the registry title
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

🚀 Preview Deployment Ready

Service URL
App https://mieweb-timehuddle-redmine-redmine-tickets-view.os.mieweb.org
API (Meteor) https://mieweb-timehuddle-redmine-redmine-tickets-view-api.os.mieweb.org

Preview auto-deletes when this PR is closed.

@SarkarShubhdeep SarkarShubhdeep self-assigned this Sep 10, 2026
@SarkarShubhdeep
SarkarShubhdeep marked this pull request as ready for review September 10, 2026 20:09
Copilot AI balanced review requested due to automatic review settings September 10, 2026 20:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a read-only Redmine ticket view backed by each user’s linked Redmine account.

Changes:

  • Adds Redmine issue fetching, DTO shaping, and REST exposure.
  • Adds filtering, caching, refresh, and external issue links.
  • Adds a tickets view switcher and supporting tests/documentation.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/ui/AppLayout.tsx Suppresses the static tickets title.
src/lib/api.ts Adds Redmine issue types and API client.
src/features/tickets/TicketsPage.tsx Adds the tickets view switcher.
src/features/tickets/RedmineTicketsView.tsx Implements the Redmine issue interface.
meteor-backend/tests/redmine.test.ts Tests method authentication and validation.
meteor-backend/tests/redmine-issues.test.ts Tests issue DTO shaping.
meteor-backend/server/redmine.js Adds the issue-list Meteor method.
meteor-backend/server/redmine-issues.js Shapes Redmine responses.
meteor-backend/server/redmine-client.js Adds Redmine issue retrieval.
meteor-backend/server/main.js Exposes the method through Wormhole.
huddle_redmine_clock.md Documents the integration milestones.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/features/tickets/RedmineTicketsView.tsx Outdated
Comment thread src/features/tickets/RedmineTicketsView.tsx Outdated
Comment thread src/features/tickets/RedmineTicketsView.tsx
Comment thread src/features/tickets/RedmineTicketsView.tsx
Comment thread src/features/tickets/TicketsPage.tsx Outdated
- Reset the assignee filter on scope change; a name present in "All" can be
  absent from "Assigned to me", which left the controlled select on a value
  with no matching option and every issue filtered out.
- Key the issue cache by user id as well as scope. Sign-out does not reload the
  page, so a second user signing in could be served the first user's issues.
- Only cache connected responses, so linking an account in Settings and coming
  back refetches instead of replaying a stale "not connected" state.
- Bump the request sequence before the cache check so a cache hit also
  supersedes an in-flight request for a previously selected scope.
- Gate the pull-to-refresh handler on the tickets route, matching the v1 view;
  TicketsPage stays mounted behind other routes.
- Render the view switcher inside an h1 using the @mieweb/ui Button primitive,
  restoring the page heading semantics AppLayout no longer supplies.
Copilot AI review requested due to automatic review settings September 10, 2026 20:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

meteor-backend/server/redmine-client.js:70

  • Redmine defaults GET /issues.json to open issues. Since this request omits status_id=*, both scopes exclude closed issues, so the documented “All = everything the key can see” contract is not met. Add status_id=*, or explicitly redefine the API and UI as open issues only.
    src/features/tickets/RedmineTicketsView.tsx:248
  • The issue count changes as search, scope, and assignee filters change, but it is not exposed as a live status. Screen-reader users receive no result-count feedback after filtering; mark this text as a polite status region.

Comment thread src/features/tickets/RedmineTicketsView.tsx
Comment thread src/features/tickets/RedmineTicketsView.tsx
Comment thread src/features/tickets/TicketsPage.tsx Outdated
- Refetch the Redmine view when it becomes visible again. TicketsPage stays
  mounted behind other routes, so linking or unlinking an account in Settings
  previously left the stale connection state on screen after navigating back.
- Gate the v1 pull-to-refresh handler on the active view as well as the route;
  RefreshContext runs every registered handler, so the hidden v1 list was
  refetching alongside the visible Redmine view.
- Include the active view in the switcher's accessible name so the h1 announces
  "Tickets v1" / "Redmine Tickets" instead of only the control's purpose.
Copilot AI review requested due to automatic review settings September 10, 2026 20:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment on lines +66 to +68
const listCache = new Map<string, RedmineIssueList>();

const cacheKey = (userId: string, scope: RedmineScope) => `${userId}:${scope}`;
Comment on lines +111 to +113
} catch (err: unknown) {
if (seq !== requestSeq.current) return;
setError(err instanceof Error ? err.message : 'Failed to load Redmine issues.');
Co-authored-by: SarkarShubhdeep <30292255+SarkarShubhdeep@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

meteor-backend/server/redmine-client.js:70

  • The all branch does not actually request all visible issues: Redmine defaults an omitted status_id filter to open issues, so closed issues are silently excluded even though this scope is documented as “everything the key can see.” Add status_id=* for the all scope (and keep the assignee filter only for mine).
    src/features/tickets/RedmineTicketsView.tsx:135
  • This return-from-Settings refresh only works while RedmineTicketsView is mounted. TicketsPage unmounts it whenever the v1 view is selected, so after issues have been cached a user can switch to v1, connect/disconnect in Settings, return, and reopen Redmine; the initializer replays the stale connected cache and wasVisible starts as true, so no forced fetch occurs. Invalidate this user's cache on link mutations, or keep the view mounted and gate it by an active flag.

Comment thread .github/workflows/pr-preview.yml
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.

3 participants