mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
refactor: group Jira and Linear task-page modules by domain (#42009)
* refactor: group Jira and Linear task-page modules by domain * refactor: remove orphaned Linear refresh test entry
This commit is contained in:
@@ -156,18 +156,18 @@
|
||||
1 src/renderer/src/components/tab-bar/windows-shell-menu-visibility.ts
|
||||
3 src/renderer/src/components/tab-group/useTabGroupCreationCommands.ts
|
||||
11 src/renderer/src/components/task-page/TaskPage.tsx
|
||||
2 src/renderer/src/components/task-page/jira/IssueAssigneeField.tsx
|
||||
1 src/renderer/src/components/task-page/jira/task-page-jira-status-order.ts
|
||||
4 src/renderer/src/components/task-page/jira/use-task-page-jira-creation-metadata.ts
|
||||
4 src/renderer/src/components/task-page/jira/use-task-page-jira-issue-creation.ts
|
||||
1 src/renderer/src/components/task-page/jira/use-task-page-jira-list-effects.ts
|
||||
2 src/renderer/src/components/task-page/jira/creation/IssueAssigneeField.tsx
|
||||
1 src/renderer/src/components/task-page/jira/listing/task-page-jira-status-order.ts
|
||||
4 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-creation-metadata.ts
|
||||
4 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-issue-creation.ts
|
||||
1 src/renderer/src/components/task-page/jira/listing/use-task-page-jira-list-effects.ts
|
||||
1 src/renderer/src/components/task-page/linear/Filters.tsx
|
||||
4 src/renderer/src/components/task-page/linear/task-page-linear-issue-model.tsx
|
||||
8 src/renderer/src/components/task-page/linear/use-task-page-linear-board.ts
|
||||
4 src/renderer/src/components/task-page/linear/issues/task-page-linear-state-cell.tsx
|
||||
8 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-board.ts
|
||||
4 src/renderer/src/components/task-page/linear/use-task-page-linear-context-state.ts
|
||||
6 src/renderer/src/components/task-page/linear/use-task-page-linear-creation-state.ts
|
||||
4 src/renderer/src/components/task-page/linear/use-task-page-linear-issue-creation.ts
|
||||
2 src/renderer/src/components/task-page/linear/use-task-page-linear-project-creation.ts
|
||||
6 src/renderer/src/components/task-page/linear/creation/use-task-page-linear-creation-state.ts
|
||||
4 src/renderer/src/components/task-page/linear/creation/use-task-page-linear-issue-creation.ts
|
||||
2 src/renderer/src/components/task-page/linear/creation/use-task-page-linear-project-creation.ts
|
||||
5 src/renderer/src/components/task-page/linear/use-task-page-linear-view-state.ts
|
||||
3 src/renderer/src/components/task-page/use-task-page-detail-routing.ts
|
||||
2 src/renderer/src/components/task-page/use-task-page-provider-metadata.ts
|
||||
|
||||
@@ -592,26 +592,26 @@
|
||||
3 src/renderer/src/components/task-page/github/listing/use-task-page-github-search-pagination.ts
|
||||
4 src/renderer/src/components/task-page/github/cache/use-task-page-work-items-cache-selection.ts
|
||||
1 src/renderer/src/components/task-page/gitlab/use-task-page-gitlab-loading.ts
|
||||
2 src/renderer/src/components/task-page/jira/task-page-jira-board-view.tsx
|
||||
1 src/renderer/src/components/task-page/jira/use-task-page-jira-creation-projects.ts
|
||||
33 src/renderer/src/components/task-page/jira/use-task-page-jira-creation-state.ts
|
||||
2 src/renderer/src/components/task-page/jira/use-task-page-jira-favourite-filters.ts
|
||||
2 src/renderer/src/components/task-page/jira/use-task-page-jira-issue-creation.ts
|
||||
1 src/renderer/src/components/task-page/jira/use-task-page-jira-list-effects.ts
|
||||
2 src/renderer/src/components/task-page/jira/use-task-page-jira-quick-filters.ts
|
||||
2 src/renderer/src/components/task-page/jira/use-task-page-jira-saved-filters.ts
|
||||
2 src/renderer/src/components/task-page/jira/use-task-page-jira-summary-generation.ts
|
||||
1 src/renderer/src/components/task-page/linear/IssueTable.tsx
|
||||
7 src/renderer/src/components/task-page/linear/use-task-page-linear-board.ts
|
||||
2 src/renderer/src/components/task-page/jira/boards/task-page-jira-board-view.tsx
|
||||
1 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-creation-projects.ts
|
||||
33 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-creation-state.ts
|
||||
2 src/renderer/src/components/task-page/jira/filters/use-task-page-jira-favourite-filters.ts
|
||||
2 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-issue-creation.ts
|
||||
1 src/renderer/src/components/task-page/jira/listing/use-task-page-jira-list-effects.ts
|
||||
2 src/renderer/src/components/task-page/jira/filters/use-task-page-jira-quick-filters.ts
|
||||
2 src/renderer/src/components/task-page/jira/filters/use-task-page-jira-saved-filters.ts
|
||||
2 src/renderer/src/components/task-page/jira/creation/use-task-page-jira-summary-generation.ts
|
||||
1 src/renderer/src/components/task-page/linear/issues/IssueTable.tsx
|
||||
7 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-board.ts
|
||||
3 src/renderer/src/components/task-page/linear/use-task-page-linear-context-actions.ts
|
||||
1 src/renderer/src/components/task-page/linear/use-task-page-linear-context-state.ts
|
||||
4 src/renderer/src/components/task-page/linear/use-task-page-linear-filter-selection.ts
|
||||
1 src/renderer/src/components/task-page/linear/use-task-page-linear-issue-creation.ts
|
||||
1 src/renderer/src/components/task-page/linear/use-task-page-linear-list-effects.ts
|
||||
6 src/renderer/src/components/task-page/linear/use-task-page-linear-list-presentation.ts
|
||||
13 src/renderer/src/components/task-page/linear/use-task-page-linear-list-projection.ts
|
||||
20 src/renderer/src/components/task-page/linear/use-task-page-linear-list-selection.ts
|
||||
2 src/renderer/src/components/task-page/linear/use-task-page-linear-project-creation.ts
|
||||
4 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-filter-selection.ts
|
||||
1 src/renderer/src/components/task-page/linear/creation/use-task-page-linear-issue-creation.ts
|
||||
1 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-list-effects.ts
|
||||
6 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-list-presentation.ts
|
||||
13 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-list-projection.ts
|
||||
20 src/renderer/src/components/task-page/linear/issues/use-task-page-linear-list-selection.ts
|
||||
2 src/renderer/src/components/task-page/linear/creation/use-task-page-linear-project-creation.ts
|
||||
1 src/renderer/src/components/task-page/linear/use-task-page-linear-view-state.ts
|
||||
10 src/renderer/src/components/task-page/use-task-page-composer-actions.ts
|
||||
3 src/renderer/src/components/task-page/use-task-page-detail-routing.ts
|
||||
|
||||
@@ -13,7 +13,7 @@ src/renderer/src/components/settings/skills/linear/LinearAgentSkillPane.tsx
|
||||
src/renderer/src/components/settings/tasks/use-task-source-provider-readiness.ts
|
||||
src/renderer/src/components/sidebar/workspace-card/details/use-worktree-card-review-details.ts
|
||||
src/renderer/src/components/task-page/jira/Content.tsx
|
||||
src/renderer/src/components/task-page/jira/use-task-page-jira-list-projection.ts
|
||||
src/renderer/src/components/task-page/jira/listing/use-task-page-jira-list-projection.ts
|
||||
src/renderer/src/components/task-page/jira/use-task-page-jira-list-state.ts
|
||||
src/renderer/src/hooks/use-integration-provider-status-refresh.ts
|
||||
|
||||
|
||||
@@ -5092,26 +5092,16 @@
|
||||
"src/renderer/src/components/task-page/github/use-task-page-github-issue-creation.test.tsx": 1769,
|
||||
"src/renderer/src/components/task-page/gitlab/use-task-page-gitlab-loading.test.tsx": 1504,
|
||||
"src/renderer/src/components/task-page/jira/Content.board-owner.test.tsx": 2582,
|
||||
"src/renderer/src/components/task-page/jira/IssueAssigneeField.test.tsx": 1122,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-board-detail-integration.test.tsx": 2192,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-board-transitions.test.ts": 69,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-board-view.test.tsx": 2287,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-favourite-selection.test.tsx": 3326,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-grouping.test.ts": 2324,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-linked-issues.test.ts": 204,
|
||||
"src/renderer/src/components/task-page/jira/task-page-jira-quick-copy.test.tsx": 3457,
|
||||
"src/renderer/src/components/task-page/jira/use-task-page-jira-project-scope.test.tsx": 2642,
|
||||
"src/renderer/src/components/task-page/jira/use-task-page-jira-quick-filters.test.tsx": 1466,
|
||||
"src/renderer/src/components/task-page/jira/use-task-page-jira-saved-filters.test.tsx": 1501,
|
||||
"src/renderer/src/components/task-page/jira/use-task-page-jira-summary-generation.test.tsx": 571,
|
||||
"src/renderer/src/components/task-page/linear/IssueTable.test.tsx": 11230,
|
||||
"src/renderer/src/components/task-page/linear/task-page-linear-committed-publication.test.tsx": 1978,
|
||||
"src/renderer/src/components/task-page/jira/filters/use-task-page-jira-project-scope.test.tsx": 2642,
|
||||
"src/renderer/src/components/task-page/jira/filters/use-task-page-jira-quick-filters.test.tsx": 1466,
|
||||
"src/renderer/src/components/task-page/jira/filters/use-task-page-jira-saved-filters.test.tsx": 1501,
|
||||
"src/renderer/src/components/task-page/jira/creation/use-task-page-jira-summary-generation.test.tsx": 571,
|
||||
"src/renderer/src/components/task-page/linear/task-page-linear-creation-lifetime.test.tsx": 3315,
|
||||
"src/renderer/src/components/task-page/linear/task-page-linear-in-orca-issues.test.ts": 430,
|
||||
"src/renderer/src/components/task-page/linear/task-page-linear-refresh-entry.test.tsx": 2217,
|
||||
"src/renderer/src/components/task-page/linear/task-page-linear-refresh-lifecycle.test.tsx": 1814,
|
||||
"src/renderer/src/components/task-page/linear/task-page-linear-refresh-writers.test.tsx": 2810,
|
||||
"src/renderer/src/components/task-page/linear/task-page-linear-selection-decisions.test.tsx": 1018,
|
||||
"src/renderer/src/components/task-page/linear/issues/task-page-linear-in-orca-issues.test.ts": 430,
|
||||
"src/renderer/src/components/task-page/linear/issue-refresh/task-page-linear-refresh-lifecycle.test.tsx": 1814,
|
||||
"src/renderer/src/components/task-page/task-page-cache-selectors.test.ts": 108,
|
||||
"src/renderer/src/components/task-page/task-page-initial-selection-scaling.test.tsx": 1953,
|
||||
"src/renderer/src/components/task-page/task-page-work-item-pagination.test.ts": 63,
|
||||
@@ -6940,7 +6930,6 @@
|
||||
"tests/e2e/structured-attention-read-retirement.unit.test.ts": 6109,
|
||||
"tests/e2e/structured-chat-tab-lifetime.unit.test.ts": 5616,
|
||||
"tests/e2e/sustained-agent-typing-load-scripts.unit.test.ts": 2169,
|
||||
"tests/e2e/task-page-linear-refresh-authority.unit.test.ts": 5548,
|
||||
"tests/e2e/terminal-perf-presentation.unit.test.ts": 60,
|
||||
"tests/e2e/worktree-status-warning.unit.test.ts": 2331,
|
||||
"tests/tools/omp-close-lifecycle.test.mjs": 403,
|
||||
|
||||
@@ -77,7 +77,7 @@ const SUPPRESSED_REACT_DOCTOR_DIAGNOSTICS = new Map([
|
||||
'react-doctor(no-adjust-state-on-prop-change)',
|
||||
new Set([
|
||||
'src/renderer/src/components/task-page/github/issue-creation/use-task-page-github-issue-draft.ts',
|
||||
'src/renderer/src/components/task-page/jira/use-task-page-jira-creation-state.ts'
|
||||
'src/renderer/src/components/task-page/jira/creation/use-task-page-jira-creation-state.ts'
|
||||
])
|
||||
],
|
||||
[
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig.web.json",
|
||||
"exclude": [
|
||||
"../src/renderer/src/components/task-page/linear/task-page-linear-detail-query.test.tsx",
|
||||
"../src/renderer/src/runtime/provider-accounts/observations/provider-account-observation.test-fixture.ts",
|
||||
"../src/shared/**/*.test.ts",
|
||||
"../src/shared/**/*.test.tsx",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"extends": "./tsconfig.web.json",
|
||||
"include": [
|
||||
"../src/main/runtime/orchestration/terminal-authority/structured-sessions/structured-chat-coordinator-mail.test.ts",
|
||||
"../src/renderer/src/components/task-page/linear/task-page-linear-detail-query.test.tsx",
|
||||
"../src/renderer/src/runtime/observed-provider-accounts.test.tsx",
|
||||
"../src/renderer/src/components/status-bar/ClaudeSwitcherMenu.observation.test.tsx",
|
||||
"../src/renderer/src/runtime/provider-accounts/observations/provider-account-observation.test-fixture.ts",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"../src/shared/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"../src/renderer/src/components/task-page/linear/task-page-linear-detail-query.test.tsx",
|
||||
"../src/renderer/src/runtime/provider-accounts/observations/provider-account-observation.test-fixture.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
|
||||
+1
-1
@@ -441,7 +441,7 @@
|
||||
{
|
||||
"files": [
|
||||
"src/renderer/src/components/task-page/github/issue-creation/use-task-page-github-issue-draft.ts",
|
||||
"src/renderer/src/components/task-page/jira/use-task-page-jira-creation-state.ts"
|
||||
"src/renderer/src/components/task-page/jira/creation/use-task-page-jira-creation-state.ts"
|
||||
],
|
||||
"rules": [
|
||||
"react-doctor/no-adjust-state-on-prop-change"
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { vi, type Mock } from 'vitest'
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { JiraIssue } from '../../../../shared/jira/jira-types'
|
||||
import type { JiraSavedFilterState } from '../../../../shared/jira/jira-saved-filters'
|
||||
import type { TaskPageLinearViewStateModel } from '../task-page/linear/use-task-page-linear-view-state'
|
||||
import type { TaskPageLinearCollectionEffectsModel } from '../task-page/linear/use-task-page-linear-collection-effects'
|
||||
import { useTaskPageJiraListState } from '../task-page/jira/use-task-page-jira-list-state'
|
||||
import { useTaskPageJiraListEffects } from '../task-page/jira/use-task-page-jira-list-effects'
|
||||
|
||||
const favouriteTransport: {
|
||||
catalogue: Mock
|
||||
readSaved: Mock
|
||||
mutateSaved: Mock
|
||||
readQuick: Mock
|
||||
list: Mock
|
||||
search: Mock
|
||||
resume: Mock
|
||||
} = vi.hoisted(() => ({
|
||||
catalogue: vi.fn(),
|
||||
readSaved: vi.fn(),
|
||||
mutateSaved: vi.fn(),
|
||||
readQuick: vi.fn(),
|
||||
list: vi.fn(),
|
||||
search: vi.fn(),
|
||||
resume: vi.fn()
|
||||
}))
|
||||
export { favouriteTransport }
|
||||
vi.mock('@/runtime/jira/runtime-jira-favourite-filters-client', () => ({
|
||||
jiraListFavouriteFiltersForTarget: favouriteTransport.catalogue
|
||||
}))
|
||||
vi.mock('@/runtime/jira/runtime-jira-saved-filter-state', () => ({
|
||||
readJiraSavedFilterState: favouriteTransport.readSaved,
|
||||
mutateJiraSavedFilters: favouriteTransport.mutateSaved
|
||||
}))
|
||||
vi.mock('@/runtime/jira/runtime-jira-quick-filter-state', () => ({
|
||||
readJiraQuickFilterState: favouriteTransport.readQuick
|
||||
}))
|
||||
vi.mock('@/runtime/jira/runtime-jira-detail-client', () => ({
|
||||
jiraListPrioritiesForTarget: vi.fn()
|
||||
}))
|
||||
vi.mock('@/runtime/jira/runtime-jira-client', () => ({
|
||||
jiraGetProjectStatusOrder: vi.fn()
|
||||
}))
|
||||
|
||||
export const favourite = {
|
||||
id: 'fav-1',
|
||||
name: 'My Jira favourite',
|
||||
jql: 'filter = 12345 ORDER BY updated DESC',
|
||||
siteId: 'site-a'
|
||||
}
|
||||
export const named = { id: 'named', name: 'Custom named filter', jql: 'project = CUSTOM' }
|
||||
export const emptySaved: JiraSavedFilterState = { filters: [named], activeFilterId: null }
|
||||
const emptyIssues: JiraIssue[] = []
|
||||
const noop = () => {}
|
||||
|
||||
export function resetFavouriteTransport() {
|
||||
vi.resetAllMocks()
|
||||
favouriteTransport.catalogue.mockImplementation(async (_target, siteId: string) => ({
|
||||
supported: true,
|
||||
filters: [{ ...favourite, siteId }],
|
||||
truncated: false
|
||||
}))
|
||||
favouriteTransport.readSaved.mockResolvedValue(emptySaved)
|
||||
favouriteTransport.mutateSaved.mockImplementation(async (_target, mutation) => ({
|
||||
...emptySaved,
|
||||
activeFilterId: mutation.kind === 'select' ? mutation.id : null
|
||||
}))
|
||||
favouriteTransport.readQuick.mockResolvedValue({ statuses: [] })
|
||||
favouriteTransport.list.mockResolvedValue(emptyIssues)
|
||||
favouriteTransport.search.mockResolvedValue(emptyIssues)
|
||||
}
|
||||
|
||||
export type FavouriteFixtureProps = { hostId?: ExecutionHostId; siteId?: string; enabled?: boolean }
|
||||
export function useFavouriteTaskPage({
|
||||
hostId = 'runtime:owner-a',
|
||||
siteId = 'site-a',
|
||||
enabled = true
|
||||
}: FavouriteFixtureProps = {}) {
|
||||
const sourceContext = useMemo(
|
||||
() => ({
|
||||
kind: 'task-source' as const,
|
||||
provider: 'jira' as const,
|
||||
projectId: 'folder-project',
|
||||
hostId,
|
||||
providerIdentity: { provider: 'jira' as const, siteId: siteId === 'all' ? null : siteId }
|
||||
}),
|
||||
[hostId, siteId]
|
||||
)
|
||||
const model = {
|
||||
accountBackedTaskSourceHostId: hostId,
|
||||
jiraTaskSourceContext: sourceContext,
|
||||
jiraTaskSourceScopeKey: JSON.stringify([hostId, siteId]),
|
||||
taskSource: enabled ? 'jira' : 'github',
|
||||
taskResumeApplied: true,
|
||||
jiraConnected: true,
|
||||
selectedJiraSiteId: siteId,
|
||||
repos: [],
|
||||
selectedRepos: [],
|
||||
pageData: {},
|
||||
settings: { activeRuntimeEnvironmentId: 'different-focused-server' },
|
||||
jiraSearchPersistReadyRef: useRef(true),
|
||||
selectedJiraIssueKey: null,
|
||||
selectedJiraIssueFallback: null,
|
||||
setSelectedJiraIssueKey: noop,
|
||||
setSelectedJiraIssueFallback: noop,
|
||||
setTaskResumeState: favouriteTransport.resume,
|
||||
linkedJiraIssueRefs: [],
|
||||
linkedJiraIssueRefsSignature: '[]',
|
||||
listJiraIssues: favouriteTransport.list,
|
||||
searchJiraIssues: favouriteTransport.search
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The actual Jira hooks only read the supplied fields from the preceding provider pipeline.
|
||||
const state = useTaskPageJiraListState(model as unknown as TaskPageLinearViewStateModel)
|
||||
const effects = { ...state, displayedJiraIssues: state.jiraIssues }
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: All fields read by the actual Jira list effect are supplied; unrelated provider stages are omitted.
|
||||
useTaskPageJiraListEffects(effects as unknown as TaskPageLinearCollectionEffectsModel)
|
||||
return state
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
buildJiraIssueKeyJql,
|
||||
buildJiraTextMatchJql
|
||||
} from '../../../shared/jira/jira-search-input-jql'
|
||||
import { getJiraBadRequestReason } from './task-page/jira/task-page-jira-load-state'
|
||||
import { getJiraBadRequestReason } from './task-page/jira/listing/task-page-jira-load-state'
|
||||
|
||||
export async function searchJiraIssuesByKeyOrText(
|
||||
input: string,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { TaskPageComposerActionsModel } from './use-task-page-composer-actions'
|
||||
import { TaskPageFrame } from './Frame'
|
||||
import { TaskPageGitHubIssueDialog } from './github/issue-creation/IssueDialog'
|
||||
import { TaskPageLinearProjectDialog } from './linear/ProjectDialog'
|
||||
import { TaskPageLinearIssueDialog } from './linear/IssueDialog'
|
||||
import { TaskPageJiraIssueDialog } from './jira/IssueDialog'
|
||||
import { TaskPageLinearProjectDialog } from './linear/creation/ProjectDialog'
|
||||
import { TaskPageLinearIssueDialog } from './linear/creation/IssueDialog'
|
||||
import { TaskPageJiraIssueDialog } from './jira/creation/IssueDialog'
|
||||
import { TaskPageGitLabDialog } from './gitlab/Dialog'
|
||||
import { TaskPageLinearConnectDialog } from './linear/ConnectDialog'
|
||||
import { TaskPageJiraConnectDialog } from './jira/ConnectDialog'
|
||||
|
||||
@@ -14,14 +14,14 @@ import { useTaskPageJiraListState } from './jira/use-task-page-jira-list-state'
|
||||
import { useTaskPageResumeRestoration } from './use-task-page-resume-restoration'
|
||||
import { useTaskPageProviderMetadata } from './use-task-page-provider-metadata'
|
||||
import { useTaskPageGitLabLoading } from './gitlab/use-task-page-gitlab-loading'
|
||||
import { useTaskPageLinearListSelection } from './linear/use-task-page-linear-list-selection'
|
||||
import { useTaskPageLinearListProjection } from './linear/use-task-page-linear-list-projection'
|
||||
import { useTaskPageLinearBoard } from './linear/use-task-page-linear-board'
|
||||
import { useTaskPageJiraListProjection } from './jira/use-task-page-jira-list-projection'
|
||||
import { useTaskPageLinearCreationState } from './linear/use-task-page-linear-creation-state'
|
||||
import { useTaskPageLinearListSelection } from './linear/issues/use-task-page-linear-list-selection'
|
||||
import { useTaskPageLinearListProjection } from './linear/issues/use-task-page-linear-list-projection'
|
||||
import { useTaskPageLinearBoard } from './linear/issues/use-task-page-linear-board'
|
||||
import { useTaskPageJiraListProjection } from './jira/listing/use-task-page-jira-list-projection'
|
||||
import { useTaskPageLinearCreationState } from './linear/creation/use-task-page-linear-creation-state'
|
||||
import { useTaskPageGitHubMutationState } from './github/mutations/use-task-page-github-mutation-state'
|
||||
import { useTaskPageJiraCreationState } from './jira/use-task-page-jira-creation-state'
|
||||
import { useTaskPageJiraCreationMetadata } from './jira/use-task-page-jira-creation-metadata'
|
||||
import { useTaskPageJiraCreationState } from './jira/creation/use-task-page-jira-creation-state'
|
||||
import { useTaskPageJiraCreationMetadata } from './jira/creation/use-task-page-jira-creation-metadata'
|
||||
import { useTaskPageGitHubListProjection } from './github/listing/use-task-page-github-list-projection'
|
||||
import { useTaskPageGitHubSearchPagination } from './github/listing/use-task-page-github-search-pagination'
|
||||
import { useTaskPageGitHubLandingRefresh } from './github/refresh/use-task-page-github-landing-refresh'
|
||||
@@ -29,15 +29,15 @@ import { useTaskPageGitHubQuietRefresh } from './github/refresh/use-task-page-gi
|
||||
import { useTaskPageSearchActions } from './use-task-page-search-actions'
|
||||
import { useTaskPageWorkspaceActions } from './use-task-page-workspace-actions'
|
||||
import { useTaskPageGitHubIssueCreation } from './github/issue-creation/use-task-page-github-issue-creation'
|
||||
import { useTaskPageLinearProjectCreation } from './linear/use-task-page-linear-project-creation'
|
||||
import { useTaskPageLinearIssueCreation } from './linear/use-task-page-linear-issue-creation'
|
||||
import { useTaskPageJiraIssueCreation } from './jira/use-task-page-jira-issue-creation'
|
||||
import { useTaskPageJiraSummaryGeneration } from './jira/use-task-page-jira-summary-generation'
|
||||
import { useTaskPageLinearProjectCreation } from './linear/creation/use-task-page-linear-project-creation'
|
||||
import { useTaskPageLinearIssueCreation } from './linear/creation/use-task-page-linear-issue-creation'
|
||||
import { useTaskPageJiraIssueCreation } from './jira/creation/use-task-page-jira-issue-creation'
|
||||
import { useTaskPageJiraSummaryGeneration } from './jira/creation/use-task-page-jira-summary-generation'
|
||||
import { useTaskPageGlobalEffects } from './use-task-page-global-effects'
|
||||
import { useTaskPageLinearListEffects } from './linear/use-task-page-linear-list-effects'
|
||||
import { useTaskPageLinearInOrcaEffects } from './linear/use-task-page-linear-in-orca-effects'
|
||||
import { useTaskPageLinearCollectionEffects } from './linear/use-task-page-linear-collection-effects'
|
||||
import { useTaskPageJiraListEffects } from './jira/use-task-page-jira-list-effects'
|
||||
import { useTaskPageLinearListEffects } from './linear/issues/use-task-page-linear-list-effects'
|
||||
import { useTaskPageLinearInOrcaEffects } from './linear/issues/use-task-page-linear-in-orca-effects'
|
||||
import { useTaskPageLinearCollectionEffects } from './linear/projects/use-task-page-linear-collection-effects'
|
||||
import { useTaskPageJiraListEffects } from './jira/listing/use-task-page-jira-list-effects'
|
||||
import { useTaskPageComposerActions } from './use-task-page-composer-actions'
|
||||
import { TaskPageSurface } from './Surface'
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import { TaskPageGitHubWorkItemStateBadge } from '@/components/task-page/github/
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import { CircleDot, ChevronDown, Copy, CheckCircle2, Ban, ChevronRight } from 'lucide-react'
|
||||
import type { TaskPageGitHubWorkItemMutationRunner } from '../../linear/task-page-linear-jira-list-model'
|
||||
import type { TaskPageGitHubWorkItemMutationRunner } from '../mutations/task-page-github-work-item-mutation-types'
|
||||
import { TaskPageGitHubDuplicatePicker } from './DuplicatePicker'
|
||||
import { useGitHubDuplicateIssueCandidates } from '@/components/github/github-duplicate-issue-candidates'
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { githubProjectHost } from '../../../../../../shared/github/projects/proj
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import { LoaderCircle, ChevronDown, Check } from 'lucide-react'
|
||||
import type { TaskPageGitHubWorkItemMutationRunner } from '../../linear/task-page-linear-jira-list-model'
|
||||
import type { TaskPageGitHubWorkItemMutationRunner } from '../mutations/task-page-github-work-item-mutation-types'
|
||||
import { GitHubAssigneeAvatar } from './Avatars'
|
||||
import { GitHubUserAvatar } from '@/components/github/github-user-avatar'
|
||||
import { codeHostErrorText } from '@/lib/source-control/remote/code-host-error-copy'
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { useAppStore } from '@/store/index'
|
||||
import type { TaskPageJiraCreationMetadataModel } from '../../jira/use-task-page-jira-creation-metadata'
|
||||
import type { TaskPageJiraCreationMetadataModel } from '../../jira/creation/use-task-page-jira-creation-metadata'
|
||||
|
||||
// Why the list projection's input model: the landing run's later-stage model carries these same fields.
|
||||
type LandingRequestInput = Pick<
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useGitHubTaskItems } from '@/hooks/useGitHubTaskItems'
|
||||
import type { TaskPageJiraCreationMetadataModel } from '../../jira/use-task-page-jira-creation-metadata'
|
||||
import type { TaskPageJiraCreationMetadataModel } from '../../jira/creation/use-task-page-jira-creation-metadata'
|
||||
import type { GitHubWorkItem } from '../../../../../../shared/github/work-items/work-item-types'
|
||||
import { taskPageGitHubItemKey } from '@/components/task-page/github/mutations/task-page-github-work-item-mutation-keys'
|
||||
import { deriveAdvertisedTotalPages } from '../../../../../../shared/work-items/work-item-empty-page'
|
||||
|
||||
+23
@@ -32,3 +32,26 @@ export type BeginTaskPageGitHubWorkItemMutationResult = {
|
||||
families: string[]
|
||||
key: TaskPageGitHubMutationKey
|
||||
}
|
||||
|
||||
export type TaskPageGitHubWorkItemMutationRunner = {
|
||||
run: (input: {
|
||||
item: GitHubWorkItem
|
||||
intent: TaskPageGitHubMutationIntent
|
||||
sourceContext?: TaskSourceContext | null
|
||||
mutate: () => Promise<{
|
||||
ok?: boolean
|
||||
error?:
|
||||
| string
|
||||
| {
|
||||
message?: string
|
||||
}
|
||||
} | void>
|
||||
successToast?: string
|
||||
errorToast: string
|
||||
}) => Promise<'confirmed' | 'rolled_back' | 'stale'>
|
||||
isIntentPending: (input: {
|
||||
item: GitHubWorkItem
|
||||
intent: TaskPageGitHubMutationIntent
|
||||
sourceContext?: TaskSourceContext | null
|
||||
}) => boolean
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { TaskPageLinearCreationStateModel } from '../../linear/use-task-page-linear-creation-state'
|
||||
import type { TaskPageLinearCreationStateModel } from '../../linear/creation/use-task-page-linear-creation-state'
|
||||
import { getGitHubTaskKind } from '../../../../../../shared/work-items/task-github-search'
|
||||
import { useMemo, useLayoutEffect, useEffect, useCallback, useRef } from 'react'
|
||||
import { parseTaskQuery } from '../../../../../../shared/work-items/task-query'
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import { LoaderCircle, GitMerge, ChevronDown, ExternalLink } from 'lucide-react'
|
||||
import type { TaskPageGitHubWorkItemMutationRunner } from '../../linear/task-page-linear-jira-list-model'
|
||||
import type { TaskPageGitHubWorkItemMutationRunner } from '../mutations/task-page-github-work-item-mutation-types'
|
||||
import { resolveTaskPullRequestRepo } from './task-page-github-review-model'
|
||||
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
|
||||
export function PRMergeCell({
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { cn } from '@/lib/class-names'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { Popover, PopoverTrigger } from '@/components/ui/popover'
|
||||
import type { TaskPageGitHubWorkItemMutationRunner } from '../../linear/task-page-linear-jira-list-model'
|
||||
import type { TaskPageGitHubWorkItemMutationRunner } from '../mutations/task-page-github-work-item-mutation-types'
|
||||
import {
|
||||
mergeReviewerSuggestions,
|
||||
resolveTaskPullRequestRepo
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import type { GitHubRepositoryIdentity as GitHubOwnerRepo } from '../../../../..
|
||||
import type { GitHubWorkItem } from '../../../../../../shared/github/work-items/work-item-types'
|
||||
import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import type { TaskSourceContext } from '../../../../../../shared/work-items/task-source-context'
|
||||
import type { TaskPageGitHubWorkItemMutationRunner } from '../../linear/task-page-linear-jira-list-model'
|
||||
import type { TaskPageGitHubWorkItemMutationRunner } from '../mutations/task-page-github-work-item-mutation-types'
|
||||
import { buildRequestedReviewUsers } from './task-page-github-review-model'
|
||||
|
||||
type ReviewerActionsInput = {
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import {
|
||||
} from '../../../../../../shared/github/checks/complete-pr-check-owner'
|
||||
import { getWorkItemRepo } from '@/lib/work-items/work-item-repo-row'
|
||||
import type { GitHubWorkItem } from '../../../../../../shared/github/work-items/work-item-types'
|
||||
import type { TaskPageJiraCreationMetadataModel } from '../../jira/use-task-page-jira-creation-metadata'
|
||||
import type { TaskPageJiraCreationMetadataModel } from '../../jira/creation/use-task-page-jira-creation-metadata'
|
||||
import { deriveTaskPagePRCheckSummary } from '../../task-page-pr-check-summary'
|
||||
import { PR_CHECKS_EAGER_PREFETCH_LIMIT } from '../../task-page-source-context'
|
||||
import { getTaskPageRepoSourceContext } from '../../../../lib/work-items/task-repo-source-context'
|
||||
|
||||
@@ -7,14 +7,14 @@ import { useTranslate } from '@/i18n/use-translate'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { jiraListGridStyle } from '../../jira-list-columns'
|
||||
import { JiraListColumnsMenu } from '../../jira-list-columns-menu'
|
||||
import { TaskPageJiraSortControls } from './task-page-jira-sort-controls'
|
||||
import { TaskPageJiraErrorBanner } from '../linear/task-page-linear-jira-list-model'
|
||||
import { TaskPageJiraTextFallbackNotice } from './task-page-jira-text-fallback-notice'
|
||||
import { TaskPageJiraIssueList } from '@/components/task-page/jira/task-page-jira-issue-list'
|
||||
import { TaskPageJiraBoardView } from '@/components/task-page/jira/task-page-jira-board-view'
|
||||
import { TaskPageJiraSortControls } from './listing/task-page-jira-sort-controls'
|
||||
import { TaskPageJiraErrorBanner } from './listing/task-page-jira-error-banner'
|
||||
import { TaskPageJiraTextFallbackNotice } from './listing/task-page-jira-text-fallback-notice'
|
||||
import { TaskPageJiraIssueList } from '@/components/task-page/jira/listing/task-page-jira-issue-list'
|
||||
import { TaskPageJiraBoardView } from '@/components/task-page/jira/boards/task-page-jira-board-view'
|
||||
import { TaskPageJiraViewControls } from '@/components/task-page/jira/task-page-jira-view-controls'
|
||||
import { formatRelativeTime } from '../task-page-source-context'
|
||||
import { getJiraStatusTone } from '@/components/task-page/jira/task-page-jira-status-tone'
|
||||
import { getJiraStatusTone } from '@/components/task-page/jira/listing/task-page-jira-status-tone'
|
||||
import JiraIssueWorkspace from '@/components/JiraIssueWorkspace'
|
||||
import { TaskPageLinearContent } from '../linear/Content'
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TaskPageJiraStatusFilters } from './TaskPageJiraStatusFilters'
|
||||
import { TaskPageJiraSavedFilters } from './TaskPageJiraSavedFilters'
|
||||
import { TaskPageJiraFavouriteFilters } from './TaskPageJiraFavouriteFilters'
|
||||
import { TaskPageJiraStatusFilters } from './filters/TaskPageJiraStatusFilters'
|
||||
import { TaskPageJiraSavedFilters } from './filters/TaskPageJiraSavedFilters'
|
||||
import { TaskPageJiraFavouriteFilters } from './filters/TaskPageJiraFavouriteFilters'
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import type { ComponentProps } from 'react'
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { TaskPageJiraIssueAssigneeField } from './IssueAssigneeField'
|
||||
|
||||
const search = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/runtime/jira/runtime-jira-user-fields-client', () => ({
|
||||
jiraListAssignableUsersForProject: search
|
||||
}))
|
||||
afterEach(cleanup)
|
||||
|
||||
it('drops old provider candidates before another host with identical project IDs can select them', async () => {
|
||||
const onSelect = vi.fn()
|
||||
const model: ComponentProps<typeof TaskPageJiraIssueAssigneeField>['model'] = {
|
||||
settings: null,
|
||||
jiraTaskSourceContext: null,
|
||||
jiraStatusCurrent: true,
|
||||
jiraStatus: {
|
||||
connected: true,
|
||||
activeSiteId: 'same-site',
|
||||
viewer: { accountId: 'local-me', displayName: 'Local me', email: null }
|
||||
},
|
||||
jiraCreateFields: [{ key: 'assignee', name: 'Assignee', required: false }],
|
||||
newJiraIssueTargetProject: { id: '100', key: 'PRJ', name: 'Project', siteId: 'same-site' },
|
||||
newJiraIssueAssignee: null,
|
||||
setNewJiraIssueAssignee: onSelect,
|
||||
newJiraIssueSubmitting: false,
|
||||
providerRuntimeContextKey: 'local'
|
||||
}
|
||||
search.mockResolvedValueOnce([{ accountId: 'local-user', displayName: 'Local user' }])
|
||||
const { rerender } = render(<TaskPageJiraIssueAssigneeField model={model} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Assignee' }))
|
||||
await screen.findByRole('button', { name: 'Local user' })
|
||||
expect(screen.getByRole('button', { name: 'Assign to me (Local me)' })).toBeTruthy()
|
||||
expect(search).toHaveBeenCalledWith(
|
||||
{ kind: 'default-scope', settings: null },
|
||||
'PRJ',
|
||||
'',
|
||||
'same-site'
|
||||
)
|
||||
|
||||
search.mockReturnValueOnce(new Promise(() => {}))
|
||||
rerender(
|
||||
<TaskPageJiraIssueAssigneeField
|
||||
model={{ ...model, providerRuntimeContextKey: 'runtime:remote', jiraStatusCurrent: false }}
|
||||
/>
|
||||
)
|
||||
expect(screen.queryByRole('button', { name: 'Local user' })).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Assignee' }))
|
||||
await waitFor(() => expect(search).toHaveBeenCalledTimes(2))
|
||||
expect(screen.queryByRole('button', { name: 'Local user' })).toBeNull()
|
||||
expect(screen.queryByRole('button', { name: 'Assign to me (Local me)' })).toBeNull()
|
||||
rerender(
|
||||
<TaskPageJiraIssueAssigneeField
|
||||
model={{
|
||||
...model,
|
||||
providerRuntimeContextKey: 'runtime:remote',
|
||||
jiraStatus: {
|
||||
...model.jiraStatus,
|
||||
viewer: { accountId: 'remote-me', displayName: 'Remote me', email: null }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Assign to me (Remote me)' }))
|
||||
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ accountId: 'remote-me' }))
|
||||
})
|
||||
+3
-3
@@ -8,13 +8,13 @@ import {
|
||||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { JiraTransitionFieldsForm } from '../../jira-transition-fields-form'
|
||||
import { classifyTransitionRequirements } from '../../../../../shared/jira/jira-transition-fields'
|
||||
import { JiraTransitionFieldsForm } from '../../../jira-transition-fields-form'
|
||||
import { classifyTransitionRequirements } from '../../../../../../shared/jira/jira-transition-fields'
|
||||
import type {
|
||||
JiraIssue,
|
||||
JiraIssueUpdate,
|
||||
JiraTransition
|
||||
} from '../../../../../shared/jira/jira-types'
|
||||
} from '../../../../../../shared/jira/jira-types'
|
||||
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
|
||||
|
||||
export type JiraBoardMovePrompt = {
|
||||
+2
-2
@@ -3,9 +3,9 @@ import type {
|
||||
JiraIssueUpdate,
|
||||
JiraMutationResult,
|
||||
JiraTransition
|
||||
} from '../../../../../shared/jira/jira-types'
|
||||
} from '../../../../../../shared/jira/jira-types'
|
||||
import type { TaskPageJiraBoardSection } from './task-page-jira-board-sections'
|
||||
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
|
||||
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
|
||||
import {
|
||||
findJiraBoardSectionTransitions,
|
||||
loadTaskPageJiraIssueTransitions
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { JiraIssue, JiraProjectStatusOrder } from '../../../../../shared/jira/jira-types'
|
||||
import { groupJiraIssuesByStatus } from './task-page-jira-issue-list'
|
||||
import type { JiraIssue, JiraProjectStatusOrder } from '../../../../../../shared/jira/jira-types'
|
||||
import { groupJiraIssuesByStatus } from '../listing/task-page-jira-issue-list'
|
||||
|
||||
export type TaskPageJiraBoardSection = {
|
||||
key: string
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { JiraIssue, JiraTransition } from '../../../../../shared/jira/jira-types'
|
||||
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
|
||||
import type { JiraIssue, JiraTransition } from '../../../../../../shared/jira/jira-types'
|
||||
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
|
||||
import { jiraListTransitionsForTarget } from '@/runtime/jira/runtime-jira-detail-client'
|
||||
import { getRuntimeTargetHostId } from '@/runtime/rpc/targets/runtime-client-target'
|
||||
import { createMetadataRequestStore, loadMetadata } from '@/hooks/metadata-request-cache'
|
||||
+6
-6
@@ -3,21 +3,21 @@ import { useAppStore } from '@/store/index'
|
||||
import { getEnvironmentRuntimeContextKey } from '@/runtime/hosts/observations/provider-runtime-context'
|
||||
import { runtimeTargetEnvironmentId } from '@/runtime/rpc/targets/runtime-client-target'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { classifyTransitionRequirements } from '../../../../../shared/jira/jira-transition-fields'
|
||||
import { classifyTransitionRequirements } from '../../../../../../shared/jira/jira-transition-fields'
|
||||
import type {
|
||||
JiraIssue,
|
||||
JiraIssueUpdate,
|
||||
JiraProjectStatusOrder,
|
||||
JiraTransition
|
||||
} from '../../../../../shared/jira/jira-types'
|
||||
import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context'
|
||||
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
|
||||
} from '../../../../../../shared/jira/jira-types'
|
||||
import type { TaskSourceContext } from '../../../../../../shared/work-items/task-source-context'
|
||||
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
|
||||
import { TaskPageJiraBoard, jiraBoardIssueRefKey } from './task-page-jira-board'
|
||||
import type { TaskPageJiraBoardSection } from './task-page-jira-board-sections'
|
||||
import { JiraBoardMoveDialog, type JiraBoardMovePrompt } from './task-page-jira-board-move-dialog'
|
||||
import { applyJiraBoardMove, getJiraBoardMoveTransitions } from './task-page-jira-board-move'
|
||||
import { formatRelativeTime } from '../task-page-source-context'
|
||||
import { enqueueJiraIssueMutation, jiraIssueRequestScope } from '../../jira-issue-mutation-queue'
|
||||
import { formatRelativeTime } from '../../task-page-source-context'
|
||||
import { enqueueJiraIssueMutation, jiraIssueRequestScope } from '../../../jira-issue-mutation-queue'
|
||||
|
||||
type Props = {
|
||||
issues: JiraIssue[]
|
||||
+1
-1
@@ -17,7 +17,7 @@ import {
|
||||
writeJiraBoardIssueDragData
|
||||
} from '@/lib/work-items/jira/jira-board-drag-payload'
|
||||
import type { JiraBoardIssueDragRef } from '@/lib/work-items/jira/jira-board-drag-payload'
|
||||
import type { JiraIssue, JiraProjectStatusOrder } from '../../../../../shared/jira/jira-types'
|
||||
import type { JiraIssue, JiraProjectStatusOrder } from '../../../../../../shared/jira/jira-types'
|
||||
import {
|
||||
buildJiraBoardSections,
|
||||
type TaskPageJiraBoardSection
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import { getJiraSelfUser } from '@/components/jira-self-user'
|
||||
import { JiraUserPicker, type JiraUserPickerFixedOption } from '@/components/jira-user-picker'
|
||||
import { hasJiraAssigneeCreateField } from '@/components/task-page/jira/task-page-jira-create-fields'
|
||||
import { hasJiraAssigneeCreateField } from '@/components/task-page/jira/creation/task-page-jira-create-fields'
|
||||
import { getJiraProjectSelectionKey } from '@/components/task-page/jira/task-page-jira-project-selection'
|
||||
import { jiraListAssignableUsersForProject } from '@/runtime/jira/runtime-jira-user-fields-client'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -34,7 +34,7 @@ import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
getJiraCreateAllowedValueLabel,
|
||||
isJiraScalarUserCreateField
|
||||
} from '@/components/task-page/jira/task-page-jira-create-fields'
|
||||
} from '@/components/task-page/jira/creation/task-page-jira-create-fields'
|
||||
import { TaskPageJiraIssueAssigneeField } from './IssueAssigneeField'
|
||||
import { TaskPageJiraIssueUserField } from './IssueUserField'
|
||||
export function TaskPageJiraIssueDialog({
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { JiraUserPicker } from '@/components/jira-user-picker'
|
||||
import type { RowLessSource as RuntimeJiraSettings } from '@/runtime/hosts/selection/default-creation-host'
|
||||
import type { JiraUser } from '../../../../../shared/jira/jira-types'
|
||||
import type { JiraUser } from '../../../../../../shared/jira/jira-types'
|
||||
|
||||
export function TaskPageJiraIssueUserField({
|
||||
providerSettings,
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { buildJiraCreateTextAdf } from '@/components/jira-create-adf'
|
||||
import type { JiraCreateField } from '../../../../../shared/jira/jira-types'
|
||||
import type { JiraCreateField } from '../../../../../../shared/jira/jira-types'
|
||||
|
||||
// 'assignee' is system here because the dialog renders a dedicated picker for it.
|
||||
const JIRA_CREATE_SYSTEM_FIELD_KEYS = new Set([
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import { useJiraCreateAssignee } from '../../use-jira-create-assignee'
|
||||
import { useJiraCreateAssignee } from '../../../use-jira-create-assignee'
|
||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
|
||||
|
||||
import { filterJiraProjectPickerProjects } from '@/components/jira-project-picker-filter'
|
||||
import { isVisibleJiraCreateField } from '@/components/task-page/jira/task-page-jira-create-fields'
|
||||
import { isVisibleJiraCreateField } from '@/components/task-page/jira/creation/task-page-jira-create-fields'
|
||||
import {
|
||||
jiraProjectDisplayLabelComparator,
|
||||
getJiraProjectSelectionKey
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import type { TaskPageGitHubMutationStateModel } from '../github/mutations/use-task-page-github-mutation-state'
|
||||
import type { TaskPageGitHubMutationStateModel } from '../../github/mutations/use-task-page-github-mutation-state'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import type { JiraIssueType, JiraCreateField } from '../../../../../shared/jira/jira-types'
|
||||
import type { JiraIssueType, JiraCreateField } from '../../../../../../shared/jira/jira-types'
|
||||
import { useTaskCreationDraftRetention } from '@/components/use-task-creation-draft-retention'
|
||||
import { writeNewJiraIssueDraft } from '../task-page-draft-storage'
|
||||
import { writeNewJiraIssueDraft } from '../../task-page-draft-storage'
|
||||
import { useTaskPageJiraCreationProjects } from './use-task-page-jira-creation-projects'
|
||||
export function useTaskPageJiraCreationStatePrelude(model: TaskPageGitHubMutationStateModel) {
|
||||
const {
|
||||
+2
-2
@@ -2,12 +2,12 @@ import {
|
||||
taskProviderErrorMessage,
|
||||
taskProviderErrorText
|
||||
} from '@/lib/work-items/task-provider-error-copy'
|
||||
import type { TaskPageLinearIssueCreationModel } from '../linear/use-task-page-linear-issue-creation'
|
||||
import type { TaskPageLinearIssueCreationModel } from '../../linear/creation/use-task-page-linear-issue-creation'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
buildJiraCreateSubmission,
|
||||
hasJiraAssigneeCreateField
|
||||
} from '@/components/task-page/jira/task-page-jira-create-fields'
|
||||
} from '@/components/task-page/jira/creation/task-page-jira-create-fields'
|
||||
import { jiraCreateIssue } from '@/runtime/jira/runtime-jira-user-fields-client'
|
||||
import { jiraGetIssue } from '@/runtime/jira/runtime-jira-client'
|
||||
import { toast } from 'sonner'
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
import { act, useState } from 'react'
|
||||
import { renderHook, cleanup } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { JiraIssueSummaryGenerationResult } from '../../../../../shared/jira/jira-issue-summary-generation'
|
||||
import type { JiraIssueSummaryGenerationResult } from '../../../../../../shared/jira/jira-issue-summary-generation'
|
||||
import {
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
toRuntimeExecutionHostId
|
||||
} from '../../../../../shared/execution-host'
|
||||
} from '../../../../../../shared/execution-host'
|
||||
import { useTaskPageJiraSummaryGeneration } from './use-task-page-jira-summary-generation'
|
||||
|
||||
const mocks = vi.hoisted(() => ({ generate: vi.fn(), cancel: vi.fn(), error: vi.fn() }))
|
||||
+4
-4
@@ -3,16 +3,16 @@ import {
|
||||
taskProviderErrorText
|
||||
} from '@/lib/work-items/task-provider-error-copy'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createNonSecureContextUuid as createBrowserUuid } from '../../../../../shared/non-secure-context-uuid'
|
||||
import { createNonSecureContextUuid as createBrowserUuid } from '../../../../../../shared/non-secure-context-uuid'
|
||||
import { isWebClientLocation } from '@/app-shell/platform/web-client-location'
|
||||
import { ipcOrRuntimeTargetForHostId } from '@/runtime/rpc/targets/runtime-client-target'
|
||||
import {
|
||||
generateRuntimeJiraIssueSummary,
|
||||
cancelRuntimeJiraIssueSummary
|
||||
} from '@/runtime/jira/runtime-jira-summary-generation-client'
|
||||
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
|
||||
import type { JiraIssueSummaryGenerationRequest } from '../../../../../shared/jira/jira-issue-summary-generation'
|
||||
import { getTaskSourceCacheScope } from '../../../../../shared/work-items/task-source-context'
|
||||
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
|
||||
import type { JiraIssueSummaryGenerationRequest } from '../../../../../../shared/jira/jira-issue-summary-generation'
|
||||
import { getTaskSourceCacheScope } from '../../../../../../shared/work-items/task-source-context'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { TaskPageJiraIssueCreationModel } from './use-task-page-jira-issue-creation'
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
||||
CommandItem,
|
||||
CommandEmpty
|
||||
} from '@/components/ui/command'
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
|
||||
export function TaskPageJiraFavouriteFilters({
|
||||
model
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { isUtf8ByteLengthWithinLimit } from '../../../../../shared/utf8-byte-limits'
|
||||
import { isUtf8ByteLengthWithinLimit } from '../../../../../../shared/utf8-byte-limits'
|
||||
import { useState } from 'react'
|
||||
import { Check, ListFilter, Pencil, Plus, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
MAX_JIRA_SAVED_FILTER_NAME_LENGTH,
|
||||
MAX_JIRA_SAVED_FILTERS,
|
||||
type JiraSavedFilter
|
||||
} from '../../../../../shared/jira/jira-saved-filters'
|
||||
} from '../../../../../../shared/jira/jira-saved-filters'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import {
|
||||
Command,
|
||||
+3
-3
@@ -2,13 +2,13 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { buildJiraPresetJql } from '../../../../../shared/jira/jira-preset-jql'
|
||||
import { buildJiraPresetJql } from '../../../../../../shared/jira/jira-preset-jql'
|
||||
import {
|
||||
getJiraStatusCatalogScopeKey,
|
||||
includeSelectedJiraStatuses
|
||||
} from './task-page-jira-status-catalog'
|
||||
import { restrictJiraQueryToProject } from '../../../../../shared/jira/jira-project-reference'
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import { restrictJiraQueryToProject } from '../../../../../../shared/jira/jira-project-reference'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
|
||||
export function TaskPageJiraStatusFilters({
|
||||
model
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import type { JiraIssue } from '../../../../../shared/jira/jira-types'
|
||||
import type { JiraProjectReference } from '../../../../../shared/jira/jira-project-reference'
|
||||
import { getJiraProjectSelectionKey } from './task-page-jira-project-selection'
|
||||
import type { JiraIssue } from '../../../../../../shared/jira/jira-types'
|
||||
import type { JiraProjectReference } from '../../../../../../shared/jira/jira-project-reference'
|
||||
import { getJiraProjectSelectionKey } from '../task-page-jira-project-selection'
|
||||
|
||||
export function getJiraStatusCatalogScopeKey(
|
||||
sourceScopeKey: string,
|
||||
+2
-2
@@ -4,8 +4,8 @@ import {
|
||||
jiraListFavouriteFiltersForTarget,
|
||||
type JiraFavouriteFiltersResult
|
||||
} from '@/runtime/jira/runtime-jira-favourite-filters-client'
|
||||
import type { JiraFavouriteFilter } from '../../../../../shared/jira/jira-favourite-filters'
|
||||
import type { TaskPageLinearViewStateModel } from '../linear/use-task-page-linear-view-state'
|
||||
import type { JiraFavouriteFilter } from '../../../../../../shared/jira/jira-favourite-filters'
|
||||
import type { TaskPageLinearViewStateModel } from '../../linear/use-task-page-linear-view-state'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { act, cleanup, renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store/index'
|
||||
|
||||
import type { Repo } from '../../../../../shared/repo-types'
|
||||
import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import { useTaskPageJiraProjectScope } from './use-task-page-jira-project-scope'
|
||||
|
||||
const { readSavedFilters, mutateSavedFilters, readQuickFilters, writeQuickFilters } = vi.hoisted(
|
||||
+8
-5
@@ -1,9 +1,12 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { Repo } from '../../../../../shared/repo-types'
|
||||
import type { JiraProjectReference } from '../../../../../shared/jira/jira-project-reference'
|
||||
import { getRepoExecutionHostId, type ExecutionHostId } from '../../../../../shared/execution-host'
|
||||
import { resolveJiraSourceHostId } from '../../../../../shared/jira/jira-source-host'
|
||||
import { getRepoHostIdentity } from '../../../../../shared/repo-host-identity'
|
||||
import type { Repo } from '../../../../../../shared/repo-types'
|
||||
import type { JiraProjectReference } from '../../../../../../shared/jira/jira-project-reference'
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../../../shared/execution-host'
|
||||
import { resolveJiraSourceHostId } from '../../../../../../shared/jira/jira-source-host'
|
||||
import { getRepoHostIdentity } from '../../../../../../shared/repo-host-identity'
|
||||
|
||||
export function useTaskPageJiraProjectScope(
|
||||
repos: readonly Repo[],
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, cleanup, renderHook, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { useTaskPageJiraQuickFilters } from './use-task-page-jira-quick-filters'
|
||||
import type { JiraQuickFilterState } from '../../../../../../shared/jira/jira-quick-filters'
|
||||
import { toRuntimeExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
const { read, write } = vi.hoisted(() => ({ read: vi.fn(), write: vi.fn() }))
|
||||
vi.mock('@/runtime/jira/runtime-jira-quick-filter-state', () => ({
|
||||
readJiraQuickFilterState: read,
|
||||
writeJiraQuickFilterState: write
|
||||
}))
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
afterEach(cleanup)
|
||||
function model(
|
||||
host: string | null,
|
||||
accountHost = 'account-owner'
|
||||
): Parameters<typeof useTaskPageJiraQuickFilters>[0] {
|
||||
return {
|
||||
accountBackedTaskSourceHostId: toRuntimeExecutionHostId(accountHost),
|
||||
taskSource: 'jira',
|
||||
taskResumeApplied: true,
|
||||
jiraTaskSourceContext: host
|
||||
? {
|
||||
kind: 'task-source',
|
||||
provider: 'jira',
|
||||
projectId: 'project',
|
||||
hostId: toRuntimeExecutionHostId(host)
|
||||
}
|
||||
: null
|
||||
}
|
||||
}
|
||||
it('ignores a late mutation reply after changing Jira execution hosts', async () => {
|
||||
read
|
||||
.mockResolvedValueOnce({ statuses: ['Blocked'] })
|
||||
.mockResolvedValueOnce({ statuses: ['Open'] })
|
||||
let finish: (value: JiraQuickFilterState) => void = () => {}
|
||||
write.mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
finish = resolve
|
||||
})
|
||||
)
|
||||
const { result, rerender } = renderHook(({ host }) => useTaskPageJiraQuickFilters(model(host)), {
|
||||
initialProps: { host: 'a' }
|
||||
})
|
||||
await waitFor(() => expect(result.current.jiraQuickFiltersLoading).toBe(false))
|
||||
let pending: Promise<void> = Promise.resolve()
|
||||
act(() => {
|
||||
pending = result.current.selectJiraStatuses(['In Progress'])
|
||||
})
|
||||
rerender({ host: 'b' })
|
||||
await waitFor(() => expect(result.current.selectedJiraStatuses).toEqual(['Open']))
|
||||
await act(async () => {
|
||||
finish({ statuses: ['In Progress'] })
|
||||
await pending
|
||||
})
|
||||
expect(result.current.selectedJiraStatuses).toEqual(['Open'])
|
||||
})
|
||||
+2
-2
@@ -7,8 +7,8 @@ import {
|
||||
import {
|
||||
normalizeJiraQuickFilterState,
|
||||
type JiraQuickFilterState
|
||||
} from '../../../../../shared/jira/jira-quick-filters'
|
||||
import type { TaskPageLinearViewStateModel } from '../linear/use-task-page-linear-view-state'
|
||||
} from '../../../../../../shared/jira/jira-quick-filters'
|
||||
import type { TaskPageLinearViewStateModel } from '../../linear/use-task-page-linear-view-state'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, renderHook, waitFor } from '@testing-library/react'
|
||||
|
||||
import { useTaskPageJiraSavedFilters } from './use-task-page-jira-saved-filters'
|
||||
import { toRuntimeExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
const { read, mutate } = vi.hoisted(() => ({ read: vi.fn(), mutate: vi.fn() }))
|
||||
vi.mock('@/runtime/jira/runtime-jira-saved-filter-state', () => ({
|
||||
readJiraSavedFilterState: read,
|
||||
mutateJiraSavedFilters: mutate
|
||||
}))
|
||||
afterEach(cleanup)
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
const filter = { id: 'mine', name: 'My work', jql: 'project = APP' }
|
||||
const initial = { filters: [filter], activeFilterId: filter.id }
|
||||
function model(
|
||||
host: string | null,
|
||||
accountHost = 'account-owner'
|
||||
): Parameters<typeof useTaskPageJiraSavedFilters>[0] {
|
||||
return {
|
||||
taskSource: 'jira',
|
||||
taskResumeApplied: true,
|
||||
accountBackedTaskSourceHostId: toRuntimeExecutionHostId(accountHost),
|
||||
jiraTaskSourceContext: host
|
||||
? {
|
||||
kind: 'task-source',
|
||||
provider: 'jira',
|
||||
projectId: 'project',
|
||||
hostId: toRuntimeExecutionHostId(host)
|
||||
}
|
||||
: null
|
||||
}
|
||||
}
|
||||
it('ignores an old host read that completes after switching execution hosts', async () => {
|
||||
let resolveOld: (value: typeof initial) => void = () => {}
|
||||
read
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveOld = resolve
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce({ filters: [], activeFilterId: null })
|
||||
const setQuery = vi.fn()
|
||||
const { result, rerender } = renderHook(
|
||||
({ host }) => useTaskPageJiraSavedFilters(model(host), setQuery),
|
||||
{ initialProps: { host: 'host-a' } }
|
||||
)
|
||||
rerender({ host: 'host-b' })
|
||||
await waitFor(() => expect(result.current.jiraSavedFiltersReady).toBe(true))
|
||||
await act(async () => {
|
||||
resolveOld(initial)
|
||||
})
|
||||
expect(result.current.jiraSavedFilters).toEqual([])
|
||||
expect(setQuery).not.toHaveBeenCalledWith(filter.jql)
|
||||
})
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { createNonSecureContextUuid } from '../../../../../shared/non-secure-context-uuid'
|
||||
import { createNonSecureContextUuid } from '../../../../../../shared/non-secure-context-uuid'
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ipcOrRuntimeTargetForHostId } from '@/runtime/rpc/targets/runtime-client-target'
|
||||
import {
|
||||
@@ -9,8 +9,8 @@ import type {
|
||||
JiraSavedFilterMutation,
|
||||
JiraSavedFilter,
|
||||
JiraSavedFilterState
|
||||
} from '../../../../../shared/jira/jira-saved-filters'
|
||||
import type { TaskPageLinearViewStateModel } from '../linear/use-task-page-linear-view-state'
|
||||
} from '../../../../../../shared/jira/jira-saved-filters'
|
||||
import type { TaskPageLinearViewStateModel } from '../../linear/use-task-page-linear-view-state'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import type { CacheEntry } from '@/store/github/cache/cache-model'
|
||||
import {
|
||||
getTaskSourceCacheScope,
|
||||
type TaskSourceContext
|
||||
} from '../../../../../shared/work-items/task-source-context'
|
||||
import type { JiraIssue } from '../../../../../shared/jira/jira-types'
|
||||
} from '../../../../../../shared/work-items/task-source-context'
|
||||
import type { JiraIssue } from '../../../../../../shared/jira/jira-types'
|
||||
|
||||
type JiraIssueCache = Record<string, CacheEntry<JiraIssue>>
|
||||
type JiraSearchCache = Record<string, CacheEntry<JiraIssue[]>>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useTranslate } from '../../../../i18n/use-translate'
|
||||
import type { TaskPageJiraLoadError } from './task-page-jira-load-state'
|
||||
import React from 'react'
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../../../ui/collapsible'
|
||||
import { AlertCircle, ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { Button } from '../../../ui/button'
|
||||
|
||||
export function TaskPageJiraErrorBanner({
|
||||
error,
|
||||
open,
|
||||
onOpenChange
|
||||
}: {
|
||||
error: TaskPageJiraLoadError
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}): React.JSX.Element {
|
||||
const t = useTranslate()
|
||||
return (
|
||||
<Collapsible
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
className="border-b border-border bg-destructive/10 px-4 py-3 text-sm text-destructive"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="mt-0.5 size-4 flex-none" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium leading-5">{error.title}</div>
|
||||
{error.details ? (
|
||||
<>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="-ml-1 mt-1 h-6 px-1.5 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||
>
|
||||
{open ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
||||
{t('auto.components.TaskPage.40eaf2c27c', 'Details')}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-1 rounded-md border border-destructive/20 bg-background/80 px-2 py-1.5 font-mono text-xs text-foreground">
|
||||
{error.details}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -4,12 +4,12 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useIntlLocale, useTranslate, type Translate } from '@/i18n/use-translate'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import type { JiraIssue } from '../../../../../shared/jira/jira-types'
|
||||
import type { JiraIssue } from '../../../../../../shared/jira/jira-types'
|
||||
import {
|
||||
formatJiraEstimate,
|
||||
formatJiraStoryPoints,
|
||||
type JiraListColumnId
|
||||
} from '../../jira-list-columns'
|
||||
} from '../../../jira-list-columns'
|
||||
import { getJiraPriorityTone } from './task-page-jira-status-tone'
|
||||
|
||||
const MUTED_CELL = 'block truncate text-[12px] text-muted-foreground'
|
||||
+5
-5
@@ -6,12 +6,12 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import { JiraIssueRowCopyMenu } from '../../JiraIssueRowCopyMenu'
|
||||
import { JiraLinkedWorkspaceAction } from '../../JiraLinkedWorkspaceAction'
|
||||
import type { Worktree } from '../../../../../shared/worktree/types'
|
||||
import { jiraListColumnVisibility, type JiraListColumn } from '../../jira-list-columns'
|
||||
import { JiraIssueRowCopyMenu } from '../../../JiraIssueRowCopyMenu'
|
||||
import { JiraLinkedWorkspaceAction } from '../../../JiraLinkedWorkspaceAction'
|
||||
import type { Worktree } from '../../../../../../shared/worktree/types'
|
||||
import { jiraListColumnVisibility, type JiraListColumn } from '../../../jira-list-columns'
|
||||
import { JiraIssueCell, JiraPriorityText, unassignedLabel } from './task-page-jira-issue-cells'
|
||||
import type { JiraIssue, JiraProjectStatusOrder } from '../../../../../shared/jira/jira-types'
|
||||
import type { JiraIssue, JiraProjectStatusOrder } from '../../../../../../shared/jira/jira-types'
|
||||
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
|
||||
|
||||
export type TaskPageJiraIssueSection = {
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import type { JiraIssue } from '../../../../../shared/jira/jira-types'
|
||||
import type { JiraIssue } from '../../../../../../shared/jira/jira-types'
|
||||
import type { LinkedJiraIssueRef } from '@/lib/work-items/jira/jira-issue-workspace-attachment'
|
||||
import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context'
|
||||
import { readLinkedTaskIssuesWithLimit } from '../../read-linked-task-issues'
|
||||
import type { TaskSourceContext } from '../../../../../../shared/work-items/task-source-context'
|
||||
import { readLinkedTaskIssuesWithLimit } from '../../../read-linked-task-issues'
|
||||
import { createTaskPageJiraLoadFailureState } from './task-page-jira-load-state'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { JiraIssue } from '../../../../../shared/jira/jira-types'
|
||||
import { parseJiraStatusError } from '../../../../../shared/jira/jira-status-error'
|
||||
import type { JiraIssue } from '../../../../../../shared/jira/jira-types'
|
||||
import { parseJiraStatusError } from '../../../../../../shared/jira/jira-status-error'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
export type TaskPageJiraLoadError = {
|
||||
+6
-3
@@ -1,6 +1,9 @@
|
||||
import type { JiraIssue } from '../../../../../shared/jira/jira-types'
|
||||
import { buildJiraTextMatchJql, mayBeJql } from '../../../../../shared/jira/jira-search-input-jql'
|
||||
import { searchJiraIssuesByKeyOrText } from '../../jira-key-or-text-search'
|
||||
import type { JiraIssue } from '../../../../../../shared/jira/jira-types'
|
||||
import {
|
||||
buildJiraTextMatchJql,
|
||||
mayBeJql
|
||||
} from '../../../../../../shared/jira/jira-search-input-jql'
|
||||
import { searchJiraIssuesByKeyOrText } from '../../../jira-key-or-text-search'
|
||||
import { getJiraBadRequestReason } from './task-page-jira-load-state'
|
||||
|
||||
export type TaskPageJiraJqlRejection = {
|
||||
+2
-2
@@ -15,8 +15,8 @@ import {
|
||||
getJiraListColumnLabel,
|
||||
jiraListColumnVisibility,
|
||||
type JiraListColumn
|
||||
} from '../../jira-list-columns'
|
||||
import type { JiraIssueSortColumn, JiraIssueSortDirection } from '../../jira-issue-sorter'
|
||||
} from '../../../jira-list-columns'
|
||||
import type { JiraIssueSortColumn, JiraIssueSortDirection } from '../../../jira-issue-sorter'
|
||||
|
||||
type TaskPageJiraSortControlsProps = {
|
||||
columns: readonly JiraListColumn[]
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { JiraIssue, JiraProjectStatusOrder } from '../../../../../shared/jira/jira-types'
|
||||
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
|
||||
import type { JiraIssue, JiraProjectStatusOrder } from '../../../../../../shared/jira/jira-types'
|
||||
import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target'
|
||||
import { runtimeTargetEnvironmentId } from '@/runtime/rpc/targets/runtime-client-target'
|
||||
import { getEnvironmentRuntimeContextKey } from '@/runtime/hosts/observations/provider-runtime-context'
|
||||
import { jiraGetProjectStatusOrder } from '@/runtime/jira/runtime-jira-client'
|
||||
+10
-10
@@ -1,26 +1,26 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { isRuntimeProviderSearchQueryWithinLimit } from '@/runtime/issue-search/runtime-provider-search-bounds'
|
||||
import { withCurrentSprintClause } from '../../../../../shared/jira/jira-jql-predicate'
|
||||
import { withCurrentSprintClause } from '../../../../../../shared/jira/jira-jql-predicate'
|
||||
import {
|
||||
buildJiraStatusCatalog,
|
||||
getJiraStatusCatalogScopeKey
|
||||
} from './task-page-jira-status-catalog'
|
||||
import type { TaskPageLinearCollectionEffectsModel } from '../linear/use-task-page-linear-collection-effects'
|
||||
} from '../filters/task-page-jira-status-catalog'
|
||||
import type { TaskPageLinearCollectionEffectsModel } from '../../linear/projects/use-task-page-linear-collection-effects'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import {
|
||||
getSingleJiraProjectScope,
|
||||
getTaskPageJiraStatusOrderScopeKey,
|
||||
getTaskPageJiraStatusOrderRuntimeScopeKey,
|
||||
loadTaskPageJiraProjectStatusOrder
|
||||
} from '@/components/task-page/jira/task-page-jira-status-order'
|
||||
import { createTaskPageJiraLoadFailureState } from '@/components/task-page/jira/task-page-jira-load-state'
|
||||
import { searchTaskPageJiraIssues } from '@/components/task-page/jira/task-page-jira-search'
|
||||
import { JIRA_ITEM_LIMIT } from '../task-page-source-context'
|
||||
import { useTaskSearchDebouncedApply } from '../use-task-search-debounced-apply'
|
||||
} from '@/components/task-page/jira/listing/task-page-jira-status-order'
|
||||
import { createTaskPageJiraLoadFailureState } from '@/components/task-page/jira/listing/task-page-jira-load-state'
|
||||
import { searchTaskPageJiraIssues } from '@/components/task-page/jira/listing/task-page-jira-search'
|
||||
import { JIRA_ITEM_LIMIT } from '../../task-page-source-context'
|
||||
import { useTaskSearchDebouncedApply } from '../../use-task-search-debounced-apply'
|
||||
import { readLinkedJiraIssues } from './task-page-jira-linked-issues'
|
||||
import { ipcOrRuntimeTargetForHostId } from '@/runtime/rpc/targets/runtime-client-target'
|
||||
import { buildJiraPresetJql } from '../../../../../shared/jira/jira-preset-jql'
|
||||
import { restrictJiraQueryToProject } from '../../../../../shared/jira/jira-project-reference'
|
||||
import { buildJiraPresetJql } from '../../../../../../shared/jira/jira-preset-jql'
|
||||
import { restrictJiraQueryToProject } from '../../../../../../shared/jira/jira-project-reference'
|
||||
export function useTaskPageJiraListEffects(model: TaskPageLinearCollectionEffectsModel) {
|
||||
const {
|
||||
accountBackedTaskSourceHostId,
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
import type { TaskPageLinearBoardModel } from '../linear/use-task-page-linear-board'
|
||||
import type { TaskPageLinearBoardModel } from '../../linear/issues/use-task-page-linear-board'
|
||||
import { useMemo, useCallback } from 'react'
|
||||
import { findTaskPageJiraIssue } from '@/components/task-page/jira/task-page-jira-cache-selectors'
|
||||
import { findTaskPageJiraIssue } from '@/components/task-page/jira/listing/task-page-jira-cache-selectors'
|
||||
import {
|
||||
getSingleJiraProjectScope,
|
||||
getTaskPageJiraStatusOrderScopeKey,
|
||||
getTaskPageJiraStatusOrderRuntimeScopeKey
|
||||
} from '@/components/task-page/jira/task-page-jira-status-order'
|
||||
} from '@/components/task-page/jira/listing/task-page-jira-status-order'
|
||||
import { ipcOrRuntimeTargetForHostId } from '@/runtime/rpc/targets/runtime-client-target'
|
||||
import {
|
||||
buildJiraIssueWorkspaceAttachmentIndex,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
findJiraIssueWorkspaceAttachmentsInIndex
|
||||
} from '@/lib/work-items/jira/jira-issue-workspace-attachment'
|
||||
import { filterLinkedJiraIssues } from './task-page-jira-linked-issues'
|
||||
import { sortJiraIssues } from '../../jira-issue-sorter'
|
||||
import { sortJiraIssues } from '../../../jira-issue-sorter'
|
||||
export function useTaskPageJiraListProjection(model: TaskPageLinearBoardModel) {
|
||||
const {
|
||||
accountBackedTaskSourceHostId,
|
||||
@@ -1,93 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { JiraIssue, JiraStatus, JiraTransition } from '../../../../../shared/jira/jira-types'
|
||||
import type { RuntimeClientTarget } from '../../../../../shared/runtime-client-target'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
jiraListTransitionsForTarget: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/jira/runtime-jira-detail-client', () => ({
|
||||
jiraListTransitionsForTarget: mocks.jiraListTransitionsForTarget
|
||||
}))
|
||||
vi.mock('@/runtime/jira/runtime-jira-detail-client', () => ({
|
||||
jiraListTransitionsForTarget: mocks.jiraListTransitionsForTarget
|
||||
}))
|
||||
|
||||
const { loadTaskPageJiraIssueTransitions } = await import('./task-page-jira-board-transitions')
|
||||
const target: RuntimeClientTarget = { kind: 'local' }
|
||||
|
||||
function status(id: string, name: string): JiraStatus {
|
||||
return { id, name, categoryKey: 'indeterminate', categoryName: 'In Progress' }
|
||||
}
|
||||
|
||||
function transition(id: string, to: JiraStatus): JiraTransition {
|
||||
return { id, name: `To ${to.name}`, to }
|
||||
}
|
||||
|
||||
function issue(key: string, statusValue: JiraStatus, siteId?: string): JiraIssue {
|
||||
return {
|
||||
id: key,
|
||||
key,
|
||||
siteId,
|
||||
title: key,
|
||||
url: `https://example.atlassian.net/browse/${key}`,
|
||||
project: { id: 'p1', key: 'STA', name: 'Stably' },
|
||||
issueType: { id: 't1', name: 'Task' },
|
||||
status: statusValue,
|
||||
labels: [],
|
||||
updatedAt: '2026-08-07T12:00:00.000Z',
|
||||
createdAt: '2026-08-07T12:00:00.000Z'
|
||||
}
|
||||
}
|
||||
|
||||
describe('loadTaskPageJiraIssueTransitions', () => {
|
||||
beforeEach(() => {
|
||||
mocks.jiraListTransitionsForTarget.mockReset()
|
||||
})
|
||||
|
||||
it('surfaces a failed transitions request and allows an explicit retry', async () => {
|
||||
mocks.jiraListTransitionsForTarget.mockRejectedValue(new Error('offline'))
|
||||
await expect(
|
||||
loadTaskPageJiraIssueTransitions(
|
||||
target,
|
||||
'scope',
|
||||
issue('STA-500', status('2', 'In Progress'))
|
||||
)
|
||||
).rejects.toThrow('offline')
|
||||
mocks.jiraListTransitionsForTarget.mockResolvedValue([])
|
||||
await expect(
|
||||
loadTaskPageJiraIssueTransitions(
|
||||
target,
|
||||
'scope',
|
||||
issue('STA-500', status('2', 'In Progress')),
|
||||
true
|
||||
)
|
||||
).resolves.toEqual([])
|
||||
expect(mocks.jiraListTransitionsForTarget).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('keeps concurrent local and remote metadata apart even with the same scope', async () => {
|
||||
const remote: RuntimeClientTarget = { kind: 'environment', environmentId: 'owner' }
|
||||
const currentIssue = issue('STA-OWNER', status('2', 'In Progress'), 'site-a')
|
||||
const localAction = transition('local-done', status('3', 'Done'))
|
||||
const remoteAction = transition('remote-done', status('3', 'Done'))
|
||||
mocks.jiraListTransitionsForTarget.mockImplementation((owner: RuntimeClientTarget) =>
|
||||
Promise.resolve(owner.kind === 'local' ? [localAction] : [remoteAction])
|
||||
)
|
||||
await expect(
|
||||
Promise.all([
|
||||
loadTaskPageJiraIssueTransitions(target, 'same-scope', currentIssue),
|
||||
loadTaskPageJiraIssueTransitions(remote, 'same-scope', currentIssue)
|
||||
])
|
||||
).resolves.toEqual([[localAction], [remoteAction]])
|
||||
await expect(
|
||||
loadTaskPageJiraIssueTransitions(remote, 'same-scope', currentIssue)
|
||||
).resolves.toEqual([remoteAction])
|
||||
expect(mocks.jiraListTransitionsForTarget).toHaveBeenCalledTimes(2)
|
||||
expect(mocks.jiraListTransitionsForTarget).toHaveBeenCalledWith(
|
||||
remote,
|
||||
currentIssue.key,
|
||||
currentIssue.siteId
|
||||
)
|
||||
})
|
||||
})
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, cleanup, renderHook, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
favouriteTransport as transport,
|
||||
favourite,
|
||||
named,
|
||||
emptySaved,
|
||||
resetFavouriteTransport,
|
||||
useFavouriteTaskPage,
|
||||
type FavouriteFixtureProps
|
||||
} from '../../__mocks__/task-page-jira-favourite-fixture'
|
||||
|
||||
afterEach(cleanup)
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
resetFavouriteTransport()
|
||||
})
|
||||
|
||||
async function ready(result: { current: ReturnType<typeof useFavouriteTaskPage> }) {
|
||||
act(() => result.current.loadJiraFavouriteFilters())
|
||||
await waitFor(() => {
|
||||
expect(result.current.jiraSavedFiltersLoading).toBe(false)
|
||||
expect(result.current.jiraFavouriteFiltersLoading).toBe(false)
|
||||
expect(result.current.jiraLoading).toBe(false)
|
||||
})
|
||||
}
|
||||
|
||||
it('waits for confirmed named deselection before changing query or linked mode', async () => {
|
||||
transport.readSaved.mockResolvedValue({ ...emptySaved, activeFilterId: named.id })
|
||||
const { result } = renderHook(() => useFavouriteTaskPage())
|
||||
await ready(result)
|
||||
act(() => result.current.setJiraHasWorkspace(true))
|
||||
const deferred = Promise.withResolvers<typeof emptySaved>()
|
||||
transport.mutateSaved.mockReturnValueOnce(deferred.promise)
|
||||
let applying = Promise.resolve(false)
|
||||
act(() => {
|
||||
applying = result.current.applyJiraFavouriteFilter(result.current.jiraFavouriteFilters[0])
|
||||
})
|
||||
expect(result.current.jiraFavouriteFilter).toBeNull()
|
||||
expect(result.current.jiraSearchInput).toBe(named.jql)
|
||||
expect(result.current.jiraHasWorkspace).toBe(true)
|
||||
await act(async () => {
|
||||
deferred.resolve(emptySaved)
|
||||
expect(await applying).toBe(true)
|
||||
})
|
||||
expect(result.current.jiraHasWorkspace).toBe(false)
|
||||
expect(result.current.jiraSearchInput).toBe('')
|
||||
expect(result.current.jiraFavouriteFilter?.id).toBe(favourite.id)
|
||||
expect(transport.mutateSaved).toHaveBeenCalledExactlyOnceWith(
|
||||
{ kind: 'environment', environmentId: 'owner-a' },
|
||||
{ kind: 'select', id: null }
|
||||
)
|
||||
})
|
||||
|
||||
it.each(['site', 'owner', 'disabled'] as const)(
|
||||
'fences a delayed handoff across %s A→B→A',
|
||||
async (kind) => {
|
||||
transport.readSaved.mockResolvedValue({ ...emptySaved, activeFilterId: named.id })
|
||||
const { result, rerender } = renderHook(
|
||||
(props: FavouriteFixtureProps) => useFavouriteTaskPage(props),
|
||||
{
|
||||
initialProps: {}
|
||||
}
|
||||
)
|
||||
await ready(result)
|
||||
const deferred = Promise.withResolvers<typeof emptySaved>()
|
||||
transport.mutateSaved.mockReturnValueOnce(deferred.promise)
|
||||
let applying = Promise.resolve(false)
|
||||
act(() => {
|
||||
applying = result.current.applyJiraFavouriteFilter(result.current.jiraFavouriteFilters[0])
|
||||
})
|
||||
const other: FavouriteFixtureProps = {
|
||||
hostId: kind === 'owner' ? 'runtime:owner-b' : 'runtime:owner-a',
|
||||
siteId: kind === 'site' ? 'site-b' : 'site-a',
|
||||
enabled: kind !== 'disabled'
|
||||
}
|
||||
rerender(other)
|
||||
rerender({ hostId: 'runtime:owner-a', siteId: 'site-a', enabled: true })
|
||||
await ready(result)
|
||||
await act(async () => {
|
||||
deferred.resolve(emptySaved)
|
||||
expect(await applying).toBe(false)
|
||||
})
|
||||
expect(result.current.jiraFavouriteFilter).toBeNull()
|
||||
expect(result.current.jiraSearchInput).not.toContain('12345')
|
||||
}
|
||||
)
|
||||
@@ -1,93 +0,0 @@
|
||||
import { defaultJiraListColumnIds, visibleJiraListColumns } from '../../jira-list-columns'
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
|
||||
import React from 'react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
|
||||
import { TaskPageJiraIssueList } from './task-page-jira-issue-list'
|
||||
|
||||
import type { JiraIssue } from '../../../../../shared/jira/jira-types'
|
||||
|
||||
const { jiraGetProjectStatusOrderMock } = vi.hoisted(() => ({
|
||||
jiraGetProjectStatusOrderMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/jira/runtime-jira-client', () => ({
|
||||
jiraGetProjectStatusOrder: (...args: unknown[]) => jiraGetProjectStatusOrderMock(...args)
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.restoreAllMocks()
|
||||
jiraGetProjectStatusOrderMock.mockReset()
|
||||
})
|
||||
|
||||
function jiraIssue(
|
||||
key: string,
|
||||
title: string,
|
||||
statusId: string,
|
||||
statusName: string,
|
||||
options: { priority?: string; projectKey?: string; siteId?: string } = {}
|
||||
): JiraIssue {
|
||||
const siteId = options.siteId ?? 'site-1'
|
||||
const projectKey = options.projectKey ?? 'ALP'
|
||||
return {
|
||||
id: `${siteId}:${key}`,
|
||||
key,
|
||||
title,
|
||||
url: `https://example.atlassian.net/browse/${key}`,
|
||||
siteId,
|
||||
siteName: 'Example Jira',
|
||||
project: { id: projectKey, key: projectKey, name: projectKey, siteId },
|
||||
issueType: { id: '10001', name: 'Bug' },
|
||||
status: { id: statusId, name: statusName, categoryKey: 'new', categoryName: statusName },
|
||||
priority: options.priority ? { id: options.priority, name: options.priority } : undefined,
|
||||
labels: [],
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z'
|
||||
}
|
||||
}
|
||||
|
||||
describe('Jira issue status grouping', () => {
|
||||
it('collapses and expands a status group through its accessible trigger', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
React.createElement(
|
||||
TooltipProvider,
|
||||
null,
|
||||
React.createElement(TaskPageJiraIssueList, {
|
||||
columns: visibleJiraListColumns(defaultJiraListColumnIds()),
|
||||
formatUpdatedAt: () => 'today',
|
||||
getStatusTone: () => 'border-border',
|
||||
issues: [
|
||||
jiraIssue('ALP-1', 'First issue', '1', 'To Do'),
|
||||
jiraIssue('ALP-2', 'Second issue', '1', 'To Do')
|
||||
],
|
||||
onOpenIssue: vi.fn(),
|
||||
onStartWorkspace: vi.fn(),
|
||||
selectedIssue: null,
|
||||
showSiteContext: false,
|
||||
statusOrder: null
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'To Do 2' })
|
||||
expect(trigger).toHaveAttribute('data-variant', 'ghost')
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByText('First issue')).toBeInTheDocument()
|
||||
|
||||
await user.click(trigger)
|
||||
expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByText('First issue')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(trigger)
|
||||
expect(screen.getByText('First issue')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,122 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { TaskPageJiraIssueList } from './task-page-jira-issue-list'
|
||||
import { defaultJiraListColumnIds, visibleJiraListColumns } from '../../jira-list-columns'
|
||||
import type { JiraIssue } from '../../../../../shared/jira/jira-types'
|
||||
const writeClipboardText = vi.fn().mockResolvedValue(undefined)
|
||||
const issue: JiraIssue = {
|
||||
id: '1',
|
||||
key: 'APP-1',
|
||||
title: 'Fix login',
|
||||
url: 'https://acme.atlassian.net/browse/APP-1',
|
||||
project: { id: '1', key: 'APP', name: 'App' },
|
||||
issueType: { id: '1', name: 'Bug' },
|
||||
status: { id: '1', name: 'Open', categoryKey: 'new', categoryName: 'Open' },
|
||||
labels: [],
|
||||
createdAt: '2026-10-01',
|
||||
updatedAt: '2026-10-01'
|
||||
}
|
||||
const onOpenIssue = vi.fn()
|
||||
function renderList() {
|
||||
return render(
|
||||
<TooltipProvider>
|
||||
<TaskPageJiraIssueList
|
||||
columns={visibleJiraListColumns(defaultJiraListColumnIds())}
|
||||
issues={[issue]}
|
||||
selectedIssue={null}
|
||||
showSiteContext={false}
|
||||
statusOrder={null}
|
||||
formatUpdatedAt={() => 'Today'}
|
||||
getStatusTone={() => ''}
|
||||
onOpenIssue={onOpenIssue}
|
||||
onStartWorkspace={vi.fn()}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
beforeEach(() => {
|
||||
Object.assign(window, { api: { ui: { writeClipboardText } } })
|
||||
vi.clearAllMocks()
|
||||
localStorage.clear()
|
||||
})
|
||||
afterEach(cleanup)
|
||||
describe('Jira row quick copy', () => {
|
||||
it('offers the same copy actions in the row context menu', async () => {
|
||||
renderList()
|
||||
const user = userEvent.setup()
|
||||
await user.pointer({ keys: '[MouseRight]', target: screen.getByText('Fix login') })
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Copy URL' }))
|
||||
expect(writeClipboardText).toHaveBeenCalledWith(issue.url)
|
||||
expect(onOpenIssue).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders a template preview with additional instructions and copies it', async () => {
|
||||
renderList()
|
||||
const user = userEvent.setup()
|
||||
await user.click(screen.getByRole('button', { name: 'Copy context for APP-1' }))
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Copy prompt from template' }))
|
||||
await user.type(
|
||||
screen.getByLabelText('Additional instructions (optional)'),
|
||||
'Cover SSH behavior.'
|
||||
)
|
||||
const preview = screen.getByLabelText('Preview')
|
||||
expect(preview).toBeInstanceOf(HTMLTextAreaElement)
|
||||
if (!(preview instanceof HTMLTextAreaElement)) {
|
||||
throw new Error('Expected a textarea preview')
|
||||
}
|
||||
expect(preview.value).toContain('Cover SSH behavior.')
|
||||
await user.click(screen.getByRole('button', { name: 'Copy prompt' }))
|
||||
expect(writeClipboardText).toHaveBeenCalledWith(expect.stringContaining('APP-1'))
|
||||
expect(writeClipboardText).toHaveBeenCalledWith(expect.stringContaining('Cover SSH behavior.'))
|
||||
expect(
|
||||
screen.queryByRole('dialog', { name: 'Copy prompt from template' })
|
||||
).not.toBeInTheDocument()
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('button', { name: 'Copy context for APP-1' })).toHaveFocus()
|
||||
)
|
||||
})
|
||||
|
||||
it('reuses a custom template after closing and reopening the picker', async () => {
|
||||
renderList()
|
||||
const user = userEvent.setup()
|
||||
async function openCustomTemplate() {
|
||||
await user.click(screen.getByRole('button', { name: 'Copy context for APP-1' }))
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Copy prompt from template' }))
|
||||
await user.click(screen.getByRole('combobox', { name: 'Template' }))
|
||||
await user.click(screen.getByRole('option', { name: 'Custom template' }))
|
||||
}
|
||||
await openCustomTemplate()
|
||||
await user.clear(screen.getByLabelText('Template text'))
|
||||
await user.type(screen.getByLabelText('Template text'), 'Review $jira-issue')
|
||||
await user.click(screen.getByRole('button', { name: 'Cancel' }))
|
||||
await openCustomTemplate()
|
||||
expect(screen.getByLabelText('Template text')).toHaveValue('Review $jira-issue')
|
||||
await user.click(screen.getByRole('button', { name: 'Copy prompt' }))
|
||||
expect(writeClipboardText).toHaveBeenCalledWith(
|
||||
`Review Complete Jira issue APP-1: Fix login\n\n${issue.url}`
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the template picker open when the clipboard write fails', async () => {
|
||||
writeClipboardText.mockRejectedValueOnce(new Error('Clipboard unavailable'))
|
||||
renderList()
|
||||
const user = userEvent.setup()
|
||||
await user.click(screen.getByRole('button', { name: 'Copy context for APP-1' }))
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Copy prompt from template' }))
|
||||
await user.click(screen.getByRole('button', { name: 'Copy prompt' }))
|
||||
expect(screen.getByRole('dialog', { name: 'Copy prompt from template' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('copies the key from the row without opening the issue', async () => {
|
||||
renderList()
|
||||
const user = userEvent.setup()
|
||||
await user.click(screen.getByRole('button', { name: 'Copy context for APP-1' }))
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Copy key' }))
|
||||
expect(writeClipboardText).toHaveBeenCalledWith('APP-1')
|
||||
expect(onOpenIssue).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTaskPageJiraQuickFilters } from './use-task-page-jira-quick-filters'
|
||||
import type { JiraStatusCatalogItem } from './task-page-jira-status-catalog'
|
||||
import { useTaskPageJiraSavedFilters } from './use-task-page-jira-saved-filters'
|
||||
import { useTaskPageJiraFavouriteFilters } from './use-task-page-jira-favourite-filters'
|
||||
import { useTaskPageJiraQuickFilters } from './filters/use-task-page-jira-quick-filters'
|
||||
import type { JiraStatusCatalogItem } from './filters/task-page-jira-status-catalog'
|
||||
import { useTaskPageJiraSavedFilters } from './filters/use-task-page-jira-saved-filters'
|
||||
import { useTaskPageJiraFavouriteFilters } from './filters/use-task-page-jira-favourite-filters'
|
||||
import type { JiraFavouriteFilter } from '../../../../../shared/jira/jira-favourite-filters'
|
||||
import type { JiraSavedFilter } from '../../../../../shared/jira/jira-saved-filters'
|
||||
import type { TaskPageLinearViewStateModel } from '../linear/use-task-page-linear-view-state'
|
||||
@@ -11,8 +11,8 @@ import type {
|
||||
JiraProjectStatusOrder,
|
||||
JiraPriority
|
||||
} from '../../../../../shared/jira/jira-types'
|
||||
import type { TaskPageJiraLoadError } from '@/components/task-page/jira/task-page-jira-load-state'
|
||||
import type { TaskPageJiraJqlRejection } from '@/components/task-page/jira/task-page-jira-search'
|
||||
import type { TaskPageJiraLoadError } from '@/components/task-page/jira/listing/task-page-jira-load-state'
|
||||
import type { TaskPageJiraJqlRejection } from '@/components/task-page/jira/listing/task-page-jira-search'
|
||||
import type { JiraPresetId } from '@/components/task-page/task-page-localized-options'
|
||||
import type {
|
||||
JiraIssueSortColumn,
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
} from '@/runtime/rpc/targets/runtime-client-target'
|
||||
import { getEnvironmentRuntimeContextKey } from '@/runtime/hosts/observations/provider-runtime-context'
|
||||
import { getTaskSourceCacheScope } from '../../../../../shared/work-items/task-source-context'
|
||||
import { useTaskPageJiraProjectScope } from './use-task-page-jira-project-scope'
|
||||
import { useTaskPageJiraProjectScope } from './filters/use-task-page-jira-project-scope'
|
||||
const EMPTY_JIRA_PRIORITIES: JiraPrioritiesBySite = new Map()
|
||||
|
||||
export function useTaskPageJiraListState(model: TaskPageLinearViewStateModel) {
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, cleanup, renderHook, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { useTaskPageJiraQuickFilters } from './use-task-page-jira-quick-filters'
|
||||
import type { JiraQuickFilterState } from '../../../../../shared/jira/jira-quick-filters'
|
||||
import { toRuntimeExecutionHostId } from '../../../../../shared/execution-host'
|
||||
const { read, write } = vi.hoisted(() => ({ read: vi.fn(), write: vi.fn() }))
|
||||
vi.mock('@/runtime/jira/runtime-jira-quick-filter-state', () => ({
|
||||
readJiraQuickFilterState: read,
|
||||
writeJiraQuickFilterState: write
|
||||
}))
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
afterEach(cleanup)
|
||||
function model(
|
||||
host: string | null,
|
||||
accountHost = 'account-owner'
|
||||
): Parameters<typeof useTaskPageJiraQuickFilters>[0] {
|
||||
return {
|
||||
accountBackedTaskSourceHostId: toRuntimeExecutionHostId(accountHost),
|
||||
taskSource: 'jira',
|
||||
taskResumeApplied: true,
|
||||
jiraTaskSourceContext: host
|
||||
? {
|
||||
kind: 'task-source',
|
||||
provider: 'jira',
|
||||
projectId: 'project',
|
||||
hostId: toRuntimeExecutionHostId(host)
|
||||
}
|
||||
: null
|
||||
}
|
||||
}
|
||||
it('ignores a late mutation reply after changing Jira execution hosts', async () => {
|
||||
read
|
||||
.mockResolvedValueOnce({ statuses: ['Blocked'] })
|
||||
.mockResolvedValueOnce({ statuses: ['Open'] })
|
||||
let finish: (value: JiraQuickFilterState) => void = () => {}
|
||||
write.mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
finish = resolve
|
||||
})
|
||||
)
|
||||
const { result, rerender } = renderHook(({ host }) => useTaskPageJiraQuickFilters(model(host)), {
|
||||
initialProps: { host: 'a' }
|
||||
})
|
||||
await waitFor(() => expect(result.current.jiraQuickFiltersLoading).toBe(false))
|
||||
let pending: Promise<void> = Promise.resolve()
|
||||
act(() => {
|
||||
pending = result.current.selectJiraStatuses(['In Progress'])
|
||||
})
|
||||
rerender({ host: 'b' })
|
||||
await waitFor(() => expect(result.current.selectedJiraStatuses).toEqual(['Open']))
|
||||
await act(async () => {
|
||||
finish({ statuses: ['In Progress'] })
|
||||
await pending
|
||||
})
|
||||
expect(result.current.selectedJiraStatuses).toEqual(['Open'])
|
||||
})
|
||||
it.each(['success', 'failure'] as const)(
|
||||
'keeps a new owner visit pending after an old %s settlement',
|
||||
async (outcome) => {
|
||||
read.mockResolvedValue({ statuses: ['Open'] })
|
||||
let finishOld: (state: JiraQuickFilterState) => void = () => {}
|
||||
let failOld: (reason: Error) => void = () => {}
|
||||
let finishNew: (state: JiraQuickFilterState) => void = () => {}
|
||||
write
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve, reject) => {
|
||||
finishOld = resolve
|
||||
failOld = reject
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
finishNew = resolve
|
||||
})
|
||||
)
|
||||
const { result, rerender } = renderHook(
|
||||
({ host }) => useTaskPageJiraQuickFilters(model(host)),
|
||||
{ initialProps: { host: 'a' } }
|
||||
)
|
||||
await waitFor(() => expect(result.current.jiraQuickFiltersLoading).toBe(false))
|
||||
let oldWrite = Promise.resolve()
|
||||
act(() => {
|
||||
oldWrite = result.current.selectJiraStatuses(['Closed']).catch(() => {})
|
||||
})
|
||||
rerender({ host: 'b' })
|
||||
await waitFor(() => expect(result.current.jiraQuickFiltersLoading).toBe(false))
|
||||
rerender({ host: 'a' })
|
||||
await waitFor(() => expect(result.current.jiraQuickFiltersLoading).toBe(false))
|
||||
let newWrite = Promise.resolve()
|
||||
act(() => {
|
||||
newWrite = result.current.selectJiraStatuses(['In Progress'])
|
||||
})
|
||||
await act(async () => {
|
||||
if (outcome === 'success') {
|
||||
finishOld({ statuses: ['Closed'] })
|
||||
} else {
|
||||
failOld(new Error('Old request failed'))
|
||||
}
|
||||
await oldWrite
|
||||
})
|
||||
expect(result.current.jiraQuickFiltersPending).toBe(true)
|
||||
expect(result.current.selectedJiraStatuses).toEqual(['Open'])
|
||||
expect(result.current.jiraQuickFiltersError).toBeNull()
|
||||
await act(async () => {
|
||||
finishNew({ statuses: ['In Progress'] })
|
||||
await newWrite
|
||||
})
|
||||
expect(result.current.selectedJiraStatuses).toEqual(['In Progress'])
|
||||
expect(result.current.jiraQuickFiltersPending).toBe(false)
|
||||
}
|
||||
)
|
||||
@@ -1,149 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, renderHook, waitFor } from '@testing-library/react'
|
||||
import { startTransition, Suspense } from 'react'
|
||||
import { useTaskPageJiraSavedFilters } from './use-task-page-jira-saved-filters'
|
||||
import { toRuntimeExecutionHostId } from '../../../../../shared/execution-host'
|
||||
const { read, mutate } = vi.hoisted(() => ({ read: vi.fn(), mutate: vi.fn() }))
|
||||
vi.mock('@/runtime/jira/runtime-jira-saved-filter-state', () => ({
|
||||
readJiraSavedFilterState: read,
|
||||
mutateJiraSavedFilters: mutate
|
||||
}))
|
||||
afterEach(cleanup)
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
const filter = { id: 'mine', name: 'My work', jql: 'project = APP' }
|
||||
const initial = { filters: [filter], activeFilterId: filter.id }
|
||||
function model(
|
||||
host: string | null,
|
||||
accountHost = 'account-owner'
|
||||
): Parameters<typeof useTaskPageJiraSavedFilters>[0] {
|
||||
return {
|
||||
taskSource: 'jira',
|
||||
taskResumeApplied: true,
|
||||
accountBackedTaskSourceHostId: toRuntimeExecutionHostId(accountHost),
|
||||
jiraTaskSourceContext: host
|
||||
? {
|
||||
kind: 'task-source',
|
||||
provider: 'jira',
|
||||
projectId: 'project',
|
||||
hostId: toRuntimeExecutionHostId(host)
|
||||
}
|
||||
: null
|
||||
}
|
||||
}
|
||||
it('ignores an old host read that completes after switching execution hosts', async () => {
|
||||
let resolveOld: (value: typeof initial) => void = () => {}
|
||||
read
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveOld = resolve
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce({ filters: [], activeFilterId: null })
|
||||
const setQuery = vi.fn()
|
||||
const { result, rerender } = renderHook(
|
||||
({ host }) => useTaskPageJiraSavedFilters(model(host), setQuery),
|
||||
{ initialProps: { host: 'host-a' } }
|
||||
)
|
||||
rerender({ host: 'host-b' })
|
||||
await waitFor(() => expect(result.current.jiraSavedFiltersReady).toBe(true))
|
||||
await act(async () => {
|
||||
resolveOld(initial)
|
||||
})
|
||||
expect(result.current.jiraSavedFilters).toEqual([])
|
||||
expect(setQuery).not.toHaveBeenCalledWith(filter.jql)
|
||||
})
|
||||
it.each(['success', 'failure'] as const)(
|
||||
'keeps a new visit to the owner pending after an old write settles with %s',
|
||||
async (settlement) => {
|
||||
read.mockResolvedValue(initial)
|
||||
let settleOld = () => {}
|
||||
let settleNew = () => {}
|
||||
mutate
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
settleOld = () =>
|
||||
settlement === 'success'
|
||||
? resolve({ filters: [], activeFilterId: null })
|
||||
: reject(new Error('Old host disconnected'))
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
settleNew = () => resolve(initial)
|
||||
})
|
||||
)
|
||||
const setQuery = vi.fn()
|
||||
const { result, rerender } = renderHook(
|
||||
({ host }) => useTaskPageJiraSavedFilters(model(host), setQuery),
|
||||
{ initialProps: { host: 'host-a' } }
|
||||
)
|
||||
await waitFor(() => expect(result.current.jiraSavedFiltersReady).toBe(true))
|
||||
let oldWrite = Promise.resolve(false)
|
||||
act(() => {
|
||||
oldWrite = result.current.applyJiraSavedFilter(filter).catch(() => false)
|
||||
})
|
||||
rerender({ host: 'host-b' })
|
||||
await waitFor(() => expect(result.current.jiraSavedFiltersLoading).toBe(false))
|
||||
rerender({ host: 'host-a' })
|
||||
await waitFor(() => expect(read).toHaveBeenCalledTimes(3))
|
||||
await waitFor(() => expect(result.current.jiraSavedFiltersLoading).toBe(false))
|
||||
let newWrite = Promise.resolve(false)
|
||||
act(() => {
|
||||
newWrite = result.current.applyJiraSavedFilter(filter)
|
||||
})
|
||||
expect(result.current.jiraSavedFiltersPending).toBe(true)
|
||||
setQuery.mockClear()
|
||||
await act(async () => {
|
||||
settleOld()
|
||||
await oldWrite
|
||||
})
|
||||
expect(result.current.jiraSavedFilters).toEqual([filter])
|
||||
expect(result.current.jiraSavedFiltersPending).toBe(true)
|
||||
expect(result.current.jiraSavedFiltersError).toBeNull()
|
||||
expect(setQuery).not.toHaveBeenCalled()
|
||||
await act(async () => {
|
||||
settleNew()
|
||||
await newWrite
|
||||
})
|
||||
expect(result.current.jiraSavedFiltersPending).toBe(false)
|
||||
expect(setQuery).toHaveBeenCalledWith(filter.jql)
|
||||
}
|
||||
)
|
||||
it('applies only the committed owner while another owner render is suspended', async () => {
|
||||
read.mockResolvedValue(initial)
|
||||
mutate.mockResolvedValue(initial)
|
||||
const setQuery = vi.fn()
|
||||
const suspended = new Promise(() => {})
|
||||
const { result, rerender } = renderHook(
|
||||
({ host, suspend }) => {
|
||||
const filters = useTaskPageJiraSavedFilters(model(host), setQuery)
|
||||
if (suspend) {
|
||||
throw suspended
|
||||
}
|
||||
return filters
|
||||
},
|
||||
{
|
||||
initialProps: { host: 'host-a', suspend: false },
|
||||
wrapper: ({ children }) => <Suspense fallback={null}>{children}</Suspense>
|
||||
}
|
||||
)
|
||||
await waitFor(() => expect(result.current.jiraSavedFiltersReady).toBe(true))
|
||||
const committedAction = result.current.applyJiraSavedFilter
|
||||
act(() => {
|
||||
startTransition(() => rerender({ host: 'host-b', suspend: true }))
|
||||
})
|
||||
await act(async () => {
|
||||
expect(await committedAction(filter)).toBe(true)
|
||||
})
|
||||
expect(mutate).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'host-a' },
|
||||
{ kind: 'select', id: filter.id }
|
||||
)
|
||||
expect(read).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -4,11 +4,11 @@ import { LoaderCircle } from 'lucide-react'
|
||||
import { LinearIcon } from '@/components/task-page/task-page-localized-options'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { TaskPageLinearProjectOverview } from './ProjectOverview'
|
||||
import { TaskPageLinearProjectList } from './ProjectList'
|
||||
import { TaskPageLinearCustomViewList } from './CustomViewList'
|
||||
import { TaskPageLinearCustomViewProjects } from './CustomViewProjects'
|
||||
import { TaskPageLinearIssueList } from './IssueList'
|
||||
import { TaskPageLinearProjectOverview } from './projects/ProjectOverview'
|
||||
import { TaskPageLinearProjectList } from './projects/ProjectList'
|
||||
import { TaskPageLinearCustomViewList } from './views/CustomViewList'
|
||||
import { TaskPageLinearCustomViewProjects } from './views/CustomViewProjects'
|
||||
import { TaskPageLinearIssueList } from './issues/IssueList'
|
||||
export function TaskPageLinearContent({
|
||||
model
|
||||
}: {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import { TaskPageLinearIssueBoard } from './IssueBoard'
|
||||
export function TaskPageLinearIssueRows({
|
||||
model
|
||||
}: {
|
||||
model: TaskPageComposerActionsModel
|
||||
}): React.JSX.Element | null {
|
||||
return <TaskPageLinearIssueBoard model={model} />
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import type { LinearIssue } from '../../../../../shared/linear/issue-types'
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { TaskPageLinearIssueTable } from './IssueTable'
|
||||
import type { LinearIssueListRow } from './task-page-linear-issue-model'
|
||||
|
||||
vi.mock('./task-page-linear-issue-model', () => ({ LinearStateCell: () => null }))
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
function issue(id: string): LinearIssue {
|
||||
return {
|
||||
id,
|
||||
identifier: `COR-${id}`,
|
||||
title: `Issue ${id}`,
|
||||
url: `https://linear.app/core/issue/COR-${id}`,
|
||||
state: { name: 'Todo', type: 'unstarted', color: '#000' },
|
||||
team: { id: 'core', name: 'Core', key: 'COR' },
|
||||
labels: [],
|
||||
labelIds: [],
|
||||
priority: 0,
|
||||
updatedAt: '2026-10-01T00:00:00Z'
|
||||
}
|
||||
}
|
||||
|
||||
const first = issue('1')
|
||||
const second = issue('2')
|
||||
const third = issue('3')
|
||||
const rows: LinearIssueListRow[] = [
|
||||
{ type: 'section', key: 'status:Todo', label: 'Todo', count: 2 },
|
||||
{ type: 'issue', issue: first },
|
||||
{ type: 'issue', issue: second },
|
||||
{ type: 'section', key: 'status:Done', label: 'Done', count: 1 },
|
||||
{ type: 'issue', issue: third }
|
||||
]
|
||||
|
||||
function table(linearIssueListRows = rows) {
|
||||
const fixture = {
|
||||
selectedLinearWorkspaceId: 'all',
|
||||
linearTaskSourceContext: null,
|
||||
selectedLinearIssueId: null,
|
||||
openLinearDetailPage: vi.fn(),
|
||||
linearIssueAttachmentIndex: new Map(),
|
||||
effectiveLinearDisplayProperties: new Set(),
|
||||
linearIssueGridStyle: {},
|
||||
linearIssueListRows,
|
||||
handleOpenOrUseLinearItem: vi.fn()
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The fixture supplies every model field read by this table.
|
||||
const model = fixture as unknown as TaskPageComposerActionsModel
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TaskPageLinearIssueTable model={model} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
it('tracks the group key through refreshed rows and leaves ungrouped issues visible', async () => {
|
||||
const user = userEvent.setup()
|
||||
const view = render(table())
|
||||
await user.click(screen.getByRole('button', { name: 'Todo 2' }))
|
||||
const refreshed: LinearIssueListRow[] = [
|
||||
...rows.slice(3),
|
||||
{ type: 'section', key: 'status:Todo', label: 'Todo', count: 3 },
|
||||
...rows.slice(1, 3),
|
||||
{ type: 'issue', issue: issue('4') }
|
||||
]
|
||||
view.rerender(table(refreshed))
|
||||
expect(screen.getByRole('button', { name: 'Todo 3', expanded: false })).toBeTruthy()
|
||||
expect(screen.queryByText('Issue 4')).toBeNull()
|
||||
expect(screen.getByText('Issue 3')).toBeTruthy()
|
||||
|
||||
view.rerender(table([{ type: 'issue', issue: first }]))
|
||||
expect(screen.getByText('Issue 1')).toBeTruthy()
|
||||
view.unmount()
|
||||
render(table())
|
||||
expect(screen.getByRole('button', { name: 'Todo 2', expanded: true })).toBeTruthy()
|
||||
})
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { linearLabelCountLabel } from './linear-attribute-count-labels'
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { ImeInput } from '@/lib/text-input/ime/ime-text-field'
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||
import { isScreenSubmitShortcut } from '@/lib/keyboard/screen-submit-shortcut'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { ImeInput } from '@/lib/text-input/ime/ime-text-field'
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { linearLabelCountLabel, linearMemberCountLabel } from './linear-attribute-count-labels'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import type { TaskPageJiraListProjectionModel } from '../jira/use-task-page-jira-list-projection'
|
||||
import type { TaskPageJiraListProjectionModel } from '../../jira/listing/use-task-page-jira-list-projection'
|
||||
import { useState, useMemo, useEffect, useReducer, type SetStateAction } from 'react'
|
||||
import { useTeamMembers, useTeamLabels, useTeamStates } from '@/hooks/useIssueMetadata'
|
||||
import {
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
completeLinearIssueForm,
|
||||
handoffLinearIssueForm as handoff
|
||||
} from '@/components/use-task-creation-draft-retention'
|
||||
import { useNewLinearIssueProjects } from '../../use-new-linear-issue-projects'
|
||||
import { useNewLinearIssueProjects } from '../../../use-new-linear-issue-projects'
|
||||
import { useContextualTour } from '@/components/contextual-tours/use-contextual-tour'
|
||||
import { writeNewLinearIssueDraft, writeNewLinearProjectDraft } from '../task-page-draft-storage'
|
||||
import { writeNewLinearIssueDraft, writeNewLinearProjectDraft } from '../../task-page-draft-storage'
|
||||
import { defaultScopeSource } from '@/runtime/hosts/selection/default-creation-host'
|
||||
type FormAction = (value: LinearIssueForm, target: string) => LinearIssueForm
|
||||
export function useLinearIssueCreationForm(
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { linearCreateIssue } from '@/runtime/linear/runtime-linear-issue-client'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { defaultScopeSource } from '@/runtime/hosts/selection/default-creation-host'
|
||||
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
|
||||
export function useTaskPageLinearIssueCreation(model: TaskPageLinearProjectCreationModel) {
|
||||
+1
-1
@@ -2,7 +2,7 @@ import {
|
||||
taskProviderErrorMessage,
|
||||
taskProviderErrorText
|
||||
} from '@/lib/work-items/task-provider-error-copy'
|
||||
import type { TaskPageGitHubIssueCreationModel } from '../github/issue-creation/use-task-page-github-issue-creation'
|
||||
import type { TaskPageGitHubIssueCreationModel } from '../../github/issue-creation/use-task-page-github-issue-creation'
|
||||
import { useCallback } from 'react'
|
||||
import { linearCreateProject } from '@/runtime/linear/runtime-linear-project-client'
|
||||
import { toast } from 'sonner'
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// @vitest-environment happy-dom
|
||||
// @vitest-environment happy-dom
|
||||
import { reads, task, store } from './task-page-linear-refresh-test-fixtures'
|
||||
import '@/store/slices/linear/linear-issue-read'
|
||||
|
||||
import { writeLinearIssue } from '@/lib/work-items/linear/linear-issue-write'
|
||||
|
||||
import { defaultScopeSource } from '@/runtime/hosts/selection/default-creation-host'
|
||||
import { getLinearSourceScope } from '@/store/slices/linear/linear-slice-scope'
|
||||
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { act } from '@testing-library/react'
|
||||
import { deferred } from '../../../../store/slices/linear/linear-slice-test-harness'
|
||||
|
||||
const cacheTime = Date.parse('2026-01-01T00:00:00Z')
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ['Date'] })
|
||||
vi.setSystemTime(cacheTime)
|
||||
})
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
it('captured default runtime cannot seed or patch the focused foreign runtime cache', async () => {
|
||||
const s = store()
|
||||
const pending = deferred<{ ok: boolean }>()
|
||||
const saving = writeLinearIssue(
|
||||
task(),
|
||||
{ state: task('Done').state },
|
||||
defaultScopeSource(null),
|
||||
() => pending.promise
|
||||
)
|
||||
// SAFETY: only activeRuntimeEnvironmentId is read by this captured-scope fixture.
|
||||
s.setState({
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this fixture supplies the real consumer's exercised fields and actions; unrelated full-page/provider services are intentionally absent.
|
||||
settings: { activeRuntimeEnvironmentId: 'remote' } as never,
|
||||
linearIssueCache: { 'w::same-uuid': { data: task('Remote'), fetchedAt: cacheTime } }
|
||||
})
|
||||
const remote = s.getState().fetchLinearIssue(task().id, 'w').acquire()
|
||||
expect(remote.currentView().data?.state.name).toBe('Remote')
|
||||
reads.detail.mockResolvedValue(task('Local observation'))
|
||||
const lease = s
|
||||
.getState()
|
||||
.fetchLinearIssue(task().id, 'w', {
|
||||
scope: getLinearSourceScope({ kind: 'local' }),
|
||||
force: true
|
||||
})
|
||||
.acquire()
|
||||
expect(lease.currentView().data).toBeNull()
|
||||
expect((await lease.waitForCurrentTerminal()).data?.state.name).toBe('Done')
|
||||
await act(async () => {
|
||||
pending.resolve({ ok: false })
|
||||
await saving
|
||||
})
|
||||
expect(s.getState().linearIssueCache['w::same-uuid'].data?.state.name).toBe('Remote')
|
||||
expect(remote.currentView().kind).toBe('data')
|
||||
expect(remote.currentView().data?.state.name).toBe('Remote')
|
||||
remote.release()
|
||||
lease.release()
|
||||
})
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
import type * as ReactModule from 'react'
|
||||
import type { LinearEditState } from '../../../linear-item-drawer-types'
|
||||
|
||||
import { getLinearSourceScope } from '@/store/slices/linear/linear-slice-scope'
|
||||
import { defaultScopeSource } from '@/runtime/hosts/selection/default-creation-host'
|
||||
import { LinearIssueTextEditor } from '../../../LinearIssueTextEditor'
|
||||
import { useLinearIssueEditController } from '../../../linear-item-drawer-edit-controller'
|
||||
import type { LinearIssue } from '../../../../../../shared/linear/issue-types'
|
||||
import { useStore } from 'zustand'
|
||||
|
||||
import { afterEach, beforeEach, vi, type Mock } from 'vitest'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import { createTestStore, issue } from '../../../../store/slices/linear/linear-slice-test-harness'
|
||||
import { clearLinearRequestMaps } from '../../../../store/slices/linear/linear-slice-request-state'
|
||||
|
||||
const deferredIssueSetters = vi.hoisted(() => {
|
||||
const updates: VoidFunction[] = []
|
||||
return { enabled: false, updates }
|
||||
})
|
||||
vi.mock('react', async (original) => {
|
||||
const actual = await original<typeof ReactModule>()
|
||||
return {
|
||||
...actual,
|
||||
useReducer<S, A>(reducer: (previous: S, action: A) => S, initial: S) {
|
||||
const [value, dispatch] = actual.useReducer(reducer, initial)
|
||||
const send = actual.useCallback((action: A) => {
|
||||
if (
|
||||
deferredIssueSetters.enabled &&
|
||||
typeof initial === 'object' &&
|
||||
initial !== null &&
|
||||
'whole' in initial
|
||||
) {
|
||||
deferredIssueSetters.updates.push(() => dispatch(action))
|
||||
} else {
|
||||
dispatch(action)
|
||||
}
|
||||
}, [])
|
||||
return [value, send] as const
|
||||
}
|
||||
}
|
||||
})
|
||||
export { deferredIssueSetters }
|
||||
|
||||
const active = vi.hoisted(() => ({ store: vi.fn<() => Store>() }))
|
||||
|
||||
vi.mock('@/store/index', () => ({
|
||||
useAppStore: Object.assign(
|
||||
(selector: (state: ReturnType<Store['getState']>) => unknown) =>
|
||||
useStore(active.store(), selector),
|
||||
{
|
||||
getState: () => active.store().getState(),
|
||||
subscribe: (...args: Parameters<Store['subscribe']>) => active.store().subscribe(...args)
|
||||
}
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n/i18n', async (original) => ({
|
||||
...(await original<Record<string, unknown>>()),
|
||||
translate: (_key: string, fallback: string, values?: Record<string, string>) =>
|
||||
fallback.replace(/{{(\w+)}}/g, (match, key) => values?.[key] ?? match)
|
||||
}))
|
||||
|
||||
const reads: {
|
||||
list: Mock
|
||||
search: Mock
|
||||
project: Mock
|
||||
view: Mock
|
||||
detail: Mock
|
||||
update: Mock
|
||||
create: Mock
|
||||
projects: Mock
|
||||
connect: Mock
|
||||
test: Mock
|
||||
select: Mock
|
||||
} = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
search: vi.fn(),
|
||||
project: vi.fn(),
|
||||
view: vi.fn(),
|
||||
detail: vi.fn(),
|
||||
update: vi.fn(),
|
||||
create: vi.fn(),
|
||||
projects: vi.fn(),
|
||||
connect: vi.fn(),
|
||||
test: vi.fn(),
|
||||
select: vi.fn()
|
||||
}))
|
||||
|
||||
const accountViewer = vi.hoisted(() => ({
|
||||
displayName: 'User',
|
||||
email: null,
|
||||
organizationName: 'Acme'
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/linear/runtime-linear-client', async (original) => ({
|
||||
...(await original<Record<string, unknown>>()),
|
||||
linearTestConnection: (...args: unknown[]) => reads.test(...args),
|
||||
linearSelectWorkspace: (...args: unknown[]) => reads.select(...args),
|
||||
linearDisconnect: async () => {},
|
||||
linearConnect: (...args: unknown[]) => reads.connect(...args),
|
||||
linearStatus: async () => ({ connected: true, viewer: accountViewer, selectedWorkspaceId: 'w' }),
|
||||
linearListIssues: (...args: unknown[]) => reads.list(...args),
|
||||
linearSearchIssues: (...args: unknown[]) => reads.search(...args)
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/linear/runtime-linear-project-client', async (original) => ({
|
||||
...(await original<Record<string, unknown>>()),
|
||||
linearListProjects: reads.projects,
|
||||
linearTeamStates: async () => [{ id: 'done', name: 'Done', type: 'completed', color: '#888888' }],
|
||||
linearListProjectIssues: (...args: unknown[]) => reads.project(...args),
|
||||
linearListCustomViewIssues: (...args: unknown[]) => reads.view(...args)
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/linear/runtime-linear-issue-client', async (original) => ({
|
||||
...(await original<Record<string, unknown>>()),
|
||||
linearGetIssue: reads.detail,
|
||||
linearIssueComments: async () => [],
|
||||
linearUpdateIssue: reads.update,
|
||||
linearCreateIssue: reads.create
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/useIssueMetadata', async (original) => ({
|
||||
...(await original<Record<string, unknown>>()),
|
||||
useTeamLabels: () => ({ data: [{ id: 'bug', name: 'Bug' }], loading: false }),
|
||||
useTeamMembers: () => ({ data: [], loading: false }),
|
||||
clearLinearMetadataCache: vi.fn(),
|
||||
useTeamStates: () => ({
|
||||
data: [{ id: 'done', name: 'Done', type: 'completed', color: '#888888' }],
|
||||
loading: false
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/store/slices/worktrees/listing/use-worktree-catalog', async (original) => {
|
||||
const empty: [] = []
|
||||
return { ...(await original<Record<string, unknown>>()), useAllWorktrees: () => empty }
|
||||
})
|
||||
|
||||
vi.mock('@/store/workspaces/catalog/workspace-record-fields', async (original) => {
|
||||
const empty: [] = []
|
||||
return { ...(await original<Record<string, unknown>>()), selectFolderWorkspaces: () => empty }
|
||||
})
|
||||
|
||||
vi.mock('@/components/LinearIssueMarkdownDescriptionEditor', () => ({
|
||||
LinearIssueMarkdownDescriptionEditor: () => null
|
||||
}))
|
||||
|
||||
vi.mock('@/components/linear-item-drawer-sheet', () => ({
|
||||
renderLinearItemDrawerSheet: (props: {
|
||||
displayed: LinearIssue | null
|
||||
editState: LinearEditState | null
|
||||
}) =>
|
||||
props.displayed && props.editState ? (
|
||||
<DrawerFields row={props.displayed} editState={props.editState} />
|
||||
) : (
|
||||
<div />
|
||||
)
|
||||
}))
|
||||
|
||||
export function DrawerFields({ row, editState }: { row: LinearIssue; editState: LinearEditState }) {
|
||||
const controller = useLinearIssueEditController({ issue: row, editState })
|
||||
return (
|
||||
<>
|
||||
<output data-testid="drawer">
|
||||
{row.state.name}:{editState.priority}:{editState.labels.join(',')}
|
||||
</output>
|
||||
<button onClick={() => controller.handlePriorityChange('4')}>Priority</button>
|
||||
<button onClick={() => controller.handleLabelToggle('bug')}>Label</button>
|
||||
<LinearIssueTextEditor issue={row} fields="title" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function task(state = 'In Review') {
|
||||
return {
|
||||
...issue('same-uuid'),
|
||||
identifier: 'DAT-124',
|
||||
workspaceId: 'w',
|
||||
state: { name: state, type: state === 'Done' ? 'completed' : 'started', color: '#888888' }
|
||||
}
|
||||
}
|
||||
|
||||
export function store() {
|
||||
const result = createTestStore()
|
||||
result.setState({
|
||||
linearStatus: { connected: true, viewer: accountViewer, selectedWorkspaceId: 'w' },
|
||||
linearStatusObserved: true,
|
||||
linearStatusChecked: true,
|
||||
linearStatusContextKey: getLinearSourceScope(defaultScopeSource(null)).runtimeContextKey
|
||||
})
|
||||
active.store.mockReturnValue(result)
|
||||
return result
|
||||
}
|
||||
|
||||
export type Store = ReturnType<typeof store>
|
||||
|
||||
beforeEach(() => {
|
||||
deferredIssueSetters.enabled = false
|
||||
deferredIssueSetters.updates.length = 0
|
||||
vi.resetAllMocks()
|
||||
reads.connect.mockResolvedValue({ ok: true, viewer: accountViewer })
|
||||
reads.test.mockResolvedValue({ ok: true, viewer: accountViewer })
|
||||
clearLinearRequestMaps()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
clearLinearRequestMaps()
|
||||
})
|
||||
|
||||
export { reads }
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import {
|
||||
resolveLinearIssueEmptyKind,
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
} from './task-page-linear-issue-empty-state'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { LinearIcon } from '@/components/task-page/task-page-localized-options'
|
||||
import { isIntegrationCredentialDecryptionError } from '../../../../../shared/integration-credential-errors'
|
||||
import { isIntegrationCredentialDecryptionError } from '../../../../../../shared/integration-credential-errors'
|
||||
import {
|
||||
clampLinearIssueListLimit,
|
||||
LINEAR_ISSUE_LIST_MAX
|
||||
} from '../../../../../shared/linear/issue-read-limits'
|
||||
import { LINEAR_ITEM_LIMIT } from '../task-page-source-context'
|
||||
} from '../../../../../../shared/linear/issue-read-limits'
|
||||
import { LINEAR_ITEM_LIMIT } from '../../task-page-source-context'
|
||||
import { TaskPageLinearIssueBoardColumns } from './IssueBoardColumns'
|
||||
import { TaskPageLinearIssueTable } from './IssueTable'
|
||||
export function TaskPageLinearIssueBoard({
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import { findLinearIssueWorkspaceAttachmentInIndex } from '@/lib/work-items/linear/linear-issue-workspace-attachment'
|
||||
import { getWorktreeAttachmentLabel } from '@/lib/workspace/display/worktree-attachment-label'
|
||||
@@ -7,8 +7,8 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { FolderOpen, ArrowRight, ExternalLink } from 'lucide-react'
|
||||
import { LinearStateCell } from './task-page-linear-issue-model'
|
||||
import { formatRelativeTime } from '../task-page-source-context'
|
||||
import { LinearStateCell } from './task-page-linear-state-cell'
|
||||
import { formatRelativeTime } from '../../task-page-source-context'
|
||||
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
|
||||
export function TaskPageLinearIssueBoardColumns({
|
||||
model
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import { TaskPageLinearIssueToolbar } from './IssueToolbar'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { TaskPageLinearIssueRows } from './IssueRows'
|
||||
import { TaskPageLinearIssueBoard } from './IssueBoard'
|
||||
import { LinearCollectionNotice } from '@/components/linear-project-view-surfaces'
|
||||
import { PaginationBar } from '../PaginationBar'
|
||||
import { PaginationBar } from '../../PaginationBar'
|
||||
export function TaskPageLinearIssueList({
|
||||
model
|
||||
}: {
|
||||
@@ -62,7 +62,7 @@ export function TaskPageLinearIssueList({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<TaskPageLinearIssueRows model={model} />
|
||||
<TaskPageLinearIssueBoard model={model} />
|
||||
{selectedLinearProject && linearProjectTab === 'issues' ? (
|
||||
<>
|
||||
<LinearCollectionNotice
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
import { useState } from 'react'
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import { ChevronDown, FolderOpen, ArrowRight, ExternalLink } from 'lucide-react'
|
||||
import { findLinearIssueWorkspaceAttachmentInIndex } from '@/lib/work-items/linear/linear-issue-workspace-attachment'
|
||||
import { getWorktreeAttachmentLabel } from '@/lib/workspace/display/worktree-attachment-label'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import { LinearPriorityIcon } from '@/components/linear-priority-icon'
|
||||
import { LinearStateCell } from './task-page-linear-issue-model'
|
||||
import { LinearStateCell } from './task-page-linear-state-cell'
|
||||
import { useIntlLocale, useTranslate } from '@/i18n/use-translate'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||
import { formatRelativeTime } from '../task-page-source-context'
|
||||
import { formatRelativeTime } from '../../task-page-source-context'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
|
||||
export function TaskPageLinearIssueTable({
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import type { TaskPageComposerActionsModel } from '../use-task-page-composer-actions'
|
||||
import type { TaskPageComposerActionsModel } from '../../use-task-page-composer-actions'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { ChevronLeft, SlidersHorizontal, List, ArrowDownUp, Eye } from 'lucide-react'
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
LinearViewMode,
|
||||
LinearGroupBy,
|
||||
LinearOrderBy
|
||||
} from '../../../../../shared/linear/issue-view-resume-state'
|
||||
} from '../../../../../../shared/linear/issue-view-resume-state'
|
||||
export function TaskPageLinearIssueToolbar({
|
||||
model
|
||||
}: {
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import type { LinearCollectionResult } from '../../../../../shared/linear/workspace-types'
|
||||
import { LINEAR_ISSUE_LIST_MAX } from '../../../../../shared/linear/issue-read-limits'
|
||||
import type { LinearIssue } from '../../../../../shared/linear/issue-types'
|
||||
import type { LinearCollectionResult } from '../../../../../../shared/linear/workspace-types'
|
||||
import { LINEAR_ISSUE_LIST_MAX } from '../../../../../../shared/linear/issue-read-limits'
|
||||
import type { LinearIssue } from '../../../../../../shared/linear/issue-types'
|
||||
|
||||
function linearIssueKey(issue: LinearIssue): string {
|
||||
return JSON.stringify([issue.workspaceId, issue.id])
|
||||
+7
-7
@@ -1,18 +1,18 @@
|
||||
import { readLinkedTaskIssuesWithLimit } from '../../read-linked-task-issues'
|
||||
import { readLinkedTaskIssuesWithLimit } from '../../../read-linked-task-issues'
|
||||
import type {
|
||||
LinearIssueRead,
|
||||
LinearIssueLease,
|
||||
LinearIssueView
|
||||
} from '@/store/slices/linear/linear-issue-read-lifecycle'
|
||||
import type { LinearIssue } from '../../../../../shared/linear/issue-types'
|
||||
import type { LinearWorkspace } from '../../../../../shared/linear/workspace-types'
|
||||
import type { Worktree } from '../../../../../shared/worktree/types'
|
||||
import { parseLinearIssueInput } from '../../../../../shared/linear/links'
|
||||
import type { LinearIssue } from '../../../../../../shared/linear/issue-types'
|
||||
import type { LinearWorkspace } from '../../../../../../shared/linear/workspace-types'
|
||||
import type { Worktree } from '../../../../../../shared/worktree/types'
|
||||
import { parseLinearIssueInput } from '../../../../../../shared/linear/links'
|
||||
import {
|
||||
getTaskSourceCacheScope,
|
||||
type TaskSourceContext
|
||||
} from '../../../../../shared/work-items/task-source-context'
|
||||
import { normalizeLinearIdentifier } from '../../../lib/work-items/linear/linear-issue-workspace-attachment'
|
||||
} from '../../../../../../shared/work-items/task-source-context'
|
||||
import { normalizeLinearIdentifier } from '../../../../lib/work-items/linear/linear-issue-workspace-attachment'
|
||||
|
||||
export type LinkedLinearIssueRef = {
|
||||
identifier: string
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
isEmptyLinearIssueAttributeFilter,
|
||||
type LinearIssueAttributeFilter
|
||||
} from '../../../../../shared/linear/issue-attribute-filter'
|
||||
} from '../../../../../../shared/linear/issue-attribute-filter'
|
||||
|
||||
export type LinearIssueEmptyKind =
|
||||
| 'context'
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import type { LinearIssue } from '../../../../../../shared/linear/issue-types'
|
||||
import type {
|
||||
LinearOrderBy,
|
||||
LinearGroupBy
|
||||
} from '../../../../../../shared/linear/issue-view-resume-state'
|
||||
import { getLinearPriorityLabel } from '../../task-page-localized-options'
|
||||
import { translate } from '../../../../i18n/i18n'
|
||||
import { compareNumericLocaleText } from '../../../../../../shared/locale-text-collators'
|
||||
import type { Translate } from '../../../../i18n/use-translate'
|
||||
|
||||
export type LinearGroupSection = {
|
||||
key: string
|
||||
label: string
|
||||
issues: LinearIssue[]
|
||||
}
|
||||
|
||||
export function getLinearPriorityRank(priority: number): number {
|
||||
return priority === 0 ? 5 : priority
|
||||
}
|
||||
|
||||
export function compareLinearIssues(
|
||||
a: LinearIssue,
|
||||
b: LinearIssue,
|
||||
orderBy: LinearOrderBy
|
||||
): number {
|
||||
if (orderBy === 'updated') {
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
}
|
||||
if (orderBy === 'identifier') {
|
||||
return compareNumericLocaleText(a.identifier, b.identifier)
|
||||
}
|
||||
const priorityDelta = getLinearPriorityRank(a.priority) - getLinearPriorityRank(b.priority)
|
||||
if (priorityDelta !== 0) {
|
||||
return priorityDelta
|
||||
}
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
}
|
||||
|
||||
export function getLinearIssueGroup(
|
||||
issue: LinearIssue,
|
||||
groupBy: LinearGroupBy,
|
||||
t: Translate = translate
|
||||
): {
|
||||
key: string
|
||||
label: string
|
||||
} {
|
||||
if (groupBy === 'status') {
|
||||
return {
|
||||
key: `status:${issue.state.name}`,
|
||||
label: issue.state.name
|
||||
}
|
||||
}
|
||||
if (groupBy === 'assignee') {
|
||||
return {
|
||||
key: `assignee:${issue.assignee?.id ?? 'unassigned'}`,
|
||||
label:
|
||||
issue.assignee?.displayName ?? t('auto.components.TaskPage.groupUnassigned', 'Unassigned')
|
||||
}
|
||||
}
|
||||
if (groupBy === 'priority') {
|
||||
return {
|
||||
key: `priority:${issue.priority}`,
|
||||
label: getLinearPriorityLabel(issue.priority)
|
||||
}
|
||||
}
|
||||
if (groupBy === 'team') {
|
||||
return {
|
||||
key: `team:${issue.team.id}`,
|
||||
label: issue.team.name
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: 'all',
|
||||
label: t('auto.components.TaskPage.dfc0c79bd8', 'Issues')
|
||||
}
|
||||
}
|
||||
|
||||
export function groupLinearIssues(
|
||||
issues: LinearIssue[],
|
||||
groupBy: LinearGroupBy,
|
||||
orderBy: LinearOrderBy,
|
||||
t: Translate = translate
|
||||
): LinearGroupSection[] {
|
||||
const sorted = [...issues].sort((a, b) => compareLinearIssues(a, b, orderBy))
|
||||
if (groupBy === 'none') {
|
||||
return [
|
||||
{
|
||||
key: 'all',
|
||||
label: t('auto.components.TaskPage.dfc0c79bd8', 'Issues'),
|
||||
issues: sorted
|
||||
}
|
||||
]
|
||||
}
|
||||
const sections = new Map<string, LinearGroupSection>()
|
||||
for (const issue of sorted) {
|
||||
const group = getLinearIssueGroup(issue, groupBy, t)
|
||||
const section = sections.get(group.key)
|
||||
if (section) {
|
||||
section.issues.push(issue)
|
||||
} else {
|
||||
sections.set(group.key, {
|
||||
key: group.key,
|
||||
label: group.label,
|
||||
issues: [issue]
|
||||
})
|
||||
}
|
||||
}
|
||||
return [...sections.values()]
|
||||
}
|
||||
+3
-3
@@ -2,12 +2,12 @@ import {
|
||||
isEmptyLinearIssueAttributeFilter,
|
||||
linearIssueAttributeFilterSignature,
|
||||
type LinearIssueAttributeFilter
|
||||
} from '../../../../../shared/linear/issue-attribute-filter'
|
||||
} from '../../../../../../shared/linear/issue-attribute-filter'
|
||||
import {
|
||||
getTaskSourceCacheScope,
|
||||
type TaskSourceContext
|
||||
} from '../../../../../shared/work-items/task-source-context'
|
||||
import type { LinearIssueListReadArgs } from '../../../store/slices/linear/linear-slice-contract'
|
||||
} from '../../../../../../shared/work-items/task-source-context'
|
||||
import type { LinearIssueListReadArgs } from '../../../../store/slices/linear/linear-slice-contract'
|
||||
|
||||
export function isLinearIssueSearchActive(immediateQuery: string, appliedQuery: string): boolean {
|
||||
return immediateQuery.trim().length > 0 || appliedQuery.trim().length > 0
|
||||
+15
-75
@@ -1,84 +1,24 @@
|
||||
import { getLinearSourceScope } from '@/store/slices/linear/linear-slice-scope'
|
||||
import { writeLinearIssue } from '@/lib/work-items/linear/linear-issue-write'
|
||||
import { taskProviderErrorMessage } from '@/lib/work-items/task-provider-error-copy'
|
||||
import type { LinearIssue } from '../../../../../shared/linear/issue-types'
|
||||
import type { LinearCustomViewModel } from '../../../../../shared/linear/project-types'
|
||||
import type {
|
||||
LinearCollectionResult,
|
||||
LinearWorkflowState
|
||||
} from '../../../../../shared/linear/workspace-types'
|
||||
import type { TaskSourceContext } from '../../../../../shared/work-items/task-source-context'
|
||||
import { getLinearSourceScope } from '../../../../store/slices/linear/linear-slice-scope'
|
||||
import { writeLinearIssue } from '../../../../lib/work-items/linear/linear-issue-write'
|
||||
import { taskProviderErrorMessage } from '../../../../lib/work-items/task-provider-error-copy'
|
||||
import type { LinearIssue } from '../../../../../../shared/linear/issue-types'
|
||||
import type { TaskSourceContext } from '../../../../../../shared/work-items/task-source-context'
|
||||
import React, { useState, useRef, useCallback } from 'react'
|
||||
import { useAppStore } from '@/store/index'
|
||||
import { useTeamStates } from '@/hooks/useIssueMetadata'
|
||||
import { linearUpdateIssue } from '@/runtime/linear/runtime-linear-issue-client'
|
||||
import { useAppStore } from '../../../../store/index'
|
||||
import { useTeamStates } from '../../../../hooks/useIssueMetadata'
|
||||
import { linearUpdateIssue } from '../../../../runtime/linear/runtime-linear-issue-client'
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useTranslate } from '@/i18n/use-translate'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
|
||||
import { cn } from '@/lib/class-names'
|
||||
import { translate } from '../../../../i18n/i18n'
|
||||
import { useTranslate } from '../../../../i18n/use-translate'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '../../../ui/popover'
|
||||
import { cn } from '../../../../lib/class-names'
|
||||
import {
|
||||
getLinearStatePillStyle,
|
||||
getLinearStateMarkerStyle
|
||||
} from '@/components/linear-state-pill-style'
|
||||
} from '../../../linear-state-pill-style'
|
||||
import { LoaderCircle, ChevronDown } from 'lucide-react'
|
||||
import { defaultScopeSource } from '@/runtime/hosts/selection/default-creation-host'
|
||||
export type LinearProjectTab = 'overview' | 'issues'
|
||||
export type LinearGroupSection = {
|
||||
key: string
|
||||
label: string
|
||||
issues: LinearIssue[]
|
||||
}
|
||||
export type LinearIssueListRow =
|
||||
| {
|
||||
type: 'section'
|
||||
key: string
|
||||
label: string
|
||||
count: number
|
||||
}
|
||||
| {
|
||||
type: 'issue'
|
||||
issue: LinearIssue
|
||||
}
|
||||
export const LINEAR_CUSTOM_VIEW_MODELS = [
|
||||
'issue',
|
||||
'project'
|
||||
] satisfies readonly LinearCustomViewModel[]
|
||||
export function mergeLinearCollectionResults<T>(
|
||||
results: LinearCollectionResult<T>[]
|
||||
): LinearCollectionResult<T> {
|
||||
const errors = results.flatMap((result) => result.errors ?? [])
|
||||
return {
|
||||
items: results.flatMap((result) => result.items),
|
||||
...(errors.length > 0
|
||||
? {
|
||||
errors
|
||||
}
|
||||
: {}),
|
||||
...(results.some((result) => result.hasMore)
|
||||
? {
|
||||
hasMore: true
|
||||
}
|
||||
: {})
|
||||
}
|
||||
}
|
||||
export function getLinearStatusSectionState(
|
||||
section: LinearGroupSection
|
||||
): LinearIssue['state'] | null {
|
||||
if (!section.key.startsWith('status:')) {
|
||||
return null
|
||||
}
|
||||
return section.issues[0]?.state ?? null
|
||||
}
|
||||
export function findLinearWorkflowStateForStatus(
|
||||
states: LinearWorkflowState[],
|
||||
targetState: LinearIssue['state']
|
||||
): LinearWorkflowState | undefined {
|
||||
return (
|
||||
states.find((state) => state.name === targetState.name && state.type === targetState.type) ??
|
||||
states.find((state) => state.name === targetState.name)
|
||||
)
|
||||
}
|
||||
import { defaultScopeSource } from '../../../../runtime/hosts/selection/default-creation-host'
|
||||
|
||||
export function LinearStateCell({
|
||||
issue,
|
||||
className,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { LinearTeam } from '../../../../../shared/linear/workspace-types'
|
||||
import type { LinearTeam } from '../../../../../../shared/linear/workspace-types'
|
||||
|
||||
// Treat malformed persisted selections as sticky-all; unvalidated strings previously crashed array filtering.
|
||||
export function storedLinearTeamSelection(value: unknown): string[] | null {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user