mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
fix(linear): show a saved Linear credential that cannot be decrypted (#28867)
* fix(linear): show a saved Linear credential that cannot be decrypted The new-workspace Linear search treated a decrypt failure as an empty result, so a connected workspace still said to start typing. Record the existing decrypt message and offer Settings. * fix(linear): open Linear settings without an Active Server read The settings helper read activeRuntimeEnvironmentId, which the focus-read check counts as the Active Server setting. Resolve the source row's host id instead, which is the same host the helper already used. * fix(linear): carry a Linear read's credential error to the composer The composer showed "Start typing to search Linear issues." when the saved Linear credential could not be decrypted. The previous attempt wrote the read failure into the host-wide Linear status, which describes the Active Server and every workspace on it, while composer reads run on the repo's host and selected workspace. That caused search loops, blocked healthy hosts and workspaces, and a latch that stopped retrying. Now Linear search and list reads reject with the plain decrypt message instead of resolving empty, the composer's URL lookup asks issue reads to do the same, and the composer keeps that error per request: it clears when the next read starts and shows in the Linear and Smart tabs (and the attach-a-link search) with a Settings link that opens the Linear card on the read's host. Nothing writes read failures into the shared status any more. The Tasks page keeps showing no rows plus the error, as before. * fix(linear): keep attach actions and a steady error on Linear read failures - Type the composer test's repo fixture so CI's typecheck passes. - Attach a link: a Linear read error no longer hides the status row. The Jira-URL mode switch and the Linear-URL Retry stay; only the duplicate sentence is dropped when the empty list already shows it. - Composer: the status refresh after a first credential failure re-runs the same read. Keep the error on screen through that re-read instead of flashing a loading skeleton. * fix(linear): keep the attach status row in place; share the integrations Settings path - Attach a link: keep the status row mounted (empty) when the result list already shows the Linear read error, so the Reference type button doesn't jump across the row. - Composer: Jira and Linear Settings buttons now share one path that picks the source's host with getTaskSourceRuntimeSettings, so the Linear one no longer re-implements it. No behavior change. * fix(linear): make the Linear Settings action a list row; keep Jira URLs on the Linear tab quiet - Composer: the credential error and its Settings action are one list row (red sentence plus "Settings"). It fits the capped results popover and is reached with the arrow keys like every other row; Enter on it opens Settings. It is never highlighted by default, so a plain Enter still moves to the agent picker. - Attach a link: on the Linear tab, a pasted Jira URL shows only the existing "Select Smart or Jira" status, as before; the Linear search of the URL text no longer adds a red Linear error beside it. * fix(linear): show the Linear read error as one plain line with no Settings action The Settings action inside the results list needed its own highlight and Enter rules and still could not fit the capped popover. The list now shows one compact red line, in the Linear and Smart tabs and in attach-a-link: "Orca can't read your saved Linear login. Reconnect Linear in Settings to search issues." It names the fix in the same words as the neighbouring not-connected hint. The highlight resolver, the input's Enter handling, the composer's Settings handlers and the prop plumbing are back to main. * fix(linear): show an unreadable Linear login as a Reconnect Linear empty state on the Linear tab only The composer's Smart tab no longer mentions Linear's login; the Linear tab, Edit workspace links and the Tasks page show a calm title and sentence where results would be, matching the not-connected state. * fix(linear): drive the Tasks Reconnect card from the selected scope's read; plain words in workspace notices Host-wide status flags any broken workspace, so a healthy selected workspace with no results no longer shows Reconnect Linear. Projects and Views notices say the unreadable login in plain words. * fix(linear): keep the Tasks Reconnect card up while the same list re-reads The list effect cleared its error on every re-run, so a refresh flashed the raw status bar until the read failed again. Clear it on success instead. * refactor(linear): drop the unreachable typed-row padding from the Linear tab's Reconnect state The Linear tab never has a typed-text row, so the state always uses the roomy padding. * fix(linear): derive the Tasks Reconnect card from the plain list's read error in the board Avoids widening the selection model's existing type assertion; project and view tabs report errors inside their results. * fix(linear): say the unreadable Linear login in one short line
This commit is contained in:
@@ -10,6 +10,7 @@ import type {
|
||||
LinearProjectSummary
|
||||
} from '../../../shared/linear/project-types'
|
||||
import type { LinearWorkspaceError } from '../../../shared/linear/workspace-types'
|
||||
import { isIntegrationCredentialDecryptionError } from '../../../shared/integration-credential-errors'
|
||||
import {
|
||||
LinearCustomViewTableContent,
|
||||
type LinearCustomViewTableContentProps
|
||||
@@ -59,7 +60,13 @@ export function LinearCollectionNotice({
|
||||
<div className={cn('flex flex-wrap gap-2 px-3', hasMore ? 'pt-2' : 'py-2')}>
|
||||
{errors.map((error) => (
|
||||
<Badge key={`${error.workspaceId}-${error.type}`} variant="outline">
|
||||
{error.workspaceName ?? error.workspaceId}: {error.message}
|
||||
{error.workspaceName ?? error.workspaceId}:{' '}
|
||||
{isIntegrationCredentialDecryptionError(error.message)
|
||||
? translate(
|
||||
'auto.components.linear.project.view.surfaces.linearLoginUnreadable',
|
||||
'Unable to read saved Linear login. Reconnect it in Settings.'
|
||||
)
|
||||
: error.message}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import React from 'react'
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { credentialDecryptionMessage } from '../../../../shared/integration-credential-errors'
|
||||
import type { LinearIssue } from '../../../../shared/linear/issue-types'
|
||||
import type { LinearConnectionStatus } from '../../../../shared/linear/workspace-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { Command } from '@/components/ui/command'
|
||||
import {
|
||||
referenceLinearIssue,
|
||||
referenceRepo
|
||||
} from '../sidebar/workspace-reference-fixtures.test-support'
|
||||
import { SmartWorkspaceSourceResultList } from './SmartWorkspaceSourceResultList'
|
||||
import { useWorkItemSourceSearch } from './use-work-item-source-search'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
// Host-wide status carries no error: the composer must show the read's own failure.
|
||||
const linearStatus: LinearConnectionStatus = {
|
||||
connected: true,
|
||||
viewer: null,
|
||||
selectedWorkspaceId: 'workspace-1'
|
||||
}
|
||||
return {
|
||||
state: {
|
||||
checkLinearConnection: vi.fn(),
|
||||
linearStatus,
|
||||
linearStatusChecked: true,
|
||||
settings: null,
|
||||
preflightStatus: null,
|
||||
preflightStatusChecked: true,
|
||||
preflightStatusContextKey: 'test',
|
||||
refreshPreflightStatus: vi.fn(),
|
||||
searchLinearIssues: vi.fn<(query: string) => Promise<LinearIssue[]>>()
|
||||
}
|
||||
}
|
||||
})
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: typeof mocks.state) => unknown) => selector(mocks.state)
|
||||
}))
|
||||
vi.mock('@/lib/local-preflight-context', () => ({
|
||||
getLocalPreflightContext: () => ({}),
|
||||
localPreflightContextKey: () => 'test'
|
||||
}))
|
||||
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
|
||||
vi.mock('./use-jira-source-connection', () => ({
|
||||
useJiraSourceConnection: () => ({ status: null, loaded: false })
|
||||
}))
|
||||
vi.mock('./use-jira-url-source', () => ({ useJiraUrlSource: () => ({ intent: false }) }))
|
||||
vi.mock('../use-jira-project-inventory', () => ({
|
||||
useJiraProjectInventory: () => ({ projects: [], connected: true, error: null, loading: false })
|
||||
}))
|
||||
|
||||
const repo: Repo = { ...referenceRepo, executionHostId: 'runtime:env-1' }
|
||||
// Stable identity: search effects depend on the repos array.
|
||||
const repos = [repo]
|
||||
|
||||
function LinearPicker({ value }: { value: string }): React.JSX.Element {
|
||||
const controller = useWorkItemSourceSearch({
|
||||
repos,
|
||||
selectedRepo: repo,
|
||||
repoId: repo.id,
|
||||
value,
|
||||
branchesEnabled: false,
|
||||
repoBackedSourcesDisabled: true
|
||||
})
|
||||
return (
|
||||
<Command>
|
||||
<button type="button" onClick={() => controller.setMode('linear')}>
|
||||
Linear tab
|
||||
</button>
|
||||
<button type="button" onClick={() => controller.setMode('smart')}>
|
||||
Smart tab
|
||||
</button>
|
||||
<SmartWorkspaceSourceResultList model={controller} onSelect={() => {}} />
|
||||
</Command>
|
||||
)
|
||||
}
|
||||
|
||||
describe('composer Linear read errors', () => {
|
||||
afterEach(cleanup)
|
||||
|
||||
it('shows the read credential error on the Linear tab only, and a successful search clears it', async () => {
|
||||
mocks.state.searchLinearIssues.mockImplementation(async (query) => {
|
||||
if (query === 'broken') {
|
||||
throw new Error(credentialDecryptionMessage('Linear'))
|
||||
}
|
||||
return [referenceLinearIssue()]
|
||||
})
|
||||
const message = 'Reconnect Linear'
|
||||
const { rerender } = render(<LinearPicker value="broken" />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Linear tab' }))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('status').textContent).toBe(
|
||||
`${message}Unable to read saved Linear login. Reconnect it in Settings.`
|
||||
)
|
||||
)
|
||||
|
||||
// The status refresh after the failure re-runs the same read; the error must stay up.
|
||||
mocks.state.linearStatus = {
|
||||
...mocks.state.linearStatus,
|
||||
credentialError: credentialDecryptionMessage('Linear')
|
||||
}
|
||||
rerender(<LinearPicker value="broken" />)
|
||||
expect(screen.getByText(message)).toBeTruthy()
|
||||
await waitFor(() => expect(mocks.state.searchLinearIssues).toHaveBeenCalledTimes(2))
|
||||
expect(screen.getByText(message)).toBeTruthy()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Smart tab' }))
|
||||
expect(screen.queryByText(message)).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Linear tab' }))
|
||||
|
||||
rerender(<LinearPicker value="launch" />)
|
||||
await waitFor(() => expect(screen.getByText(/Task/)).toBeTruthy())
|
||||
expect(screen.queryByText(message)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -28,6 +28,7 @@ export function SmartWorkspaceSourceResultList({
|
||||
| 'searchResultRows'
|
||||
| 'linearStatusChecked'
|
||||
| 'linearStatus'
|
||||
| 'linearReadError'
|
||||
| 'showJiraSiteContext'
|
||||
| 'jiraConnectionStatus'
|
||||
| 'reserveLinearLoadingResults'
|
||||
@@ -53,8 +54,11 @@ export function SmartWorkspaceSourceResultList({
|
||||
showJiraSiteContext,
|
||||
jiraConnectionStatus,
|
||||
reserveLinearLoadingResults,
|
||||
showLinearUrlLoadingFeedback
|
||||
showLinearUrlLoadingFeedback,
|
||||
linearReadError
|
||||
} = model
|
||||
// Why: only the Linear tab speaks for Linear; Smart lists what the other sources return.
|
||||
const visibleLinearReadError = mode === 'linear' ? linearReadError : null
|
||||
return (
|
||||
<>
|
||||
{mode === 'gitlab' ? (
|
||||
@@ -117,6 +121,21 @@ export function SmartWorkspaceSourceResultList({
|
||||
<div key={index} className="h-8 animate-pulse rounded bg-muted/40" />
|
||||
))}
|
||||
</div>
|
||||
) : searchResultRows.length === 0 && visibleLinearReadError && !jiraSource.intent ? (
|
||||
<div role="status" aria-live="polite" className="px-3 py-6 text-center text-xs">
|
||||
<p className="font-medium text-foreground">
|
||||
{translate(
|
||||
'auto.components.new.workspace.SmartWorkspaceNameField.linearLoginUnreadableTitle',
|
||||
'Reconnect Linear'
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-0.5 text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.new.workspace.SmartWorkspaceNameField.linearLoginUnreadableBody',
|
||||
'Unable to read saved Linear login. Reconnect it in Settings.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : searchResultRows.length === 0 && !typedTextActionRow ? (
|
||||
<div className="px-3 py-6 text-center text-xs text-muted-foreground">
|
||||
{jiraSource.intent
|
||||
|
||||
+32
-5
@@ -4,6 +4,10 @@ import { searchRuntimeRepoBaseRefDetails } from '@/runtime/runtime-repo-client'
|
||||
import { lookupLinearIssueUrl } from '@/lib/linear-issue-url-lookup'
|
||||
import { searchJiraIssuesByKeyOrText } from '@/components/jira-key-or-text-search'
|
||||
import { linearWorkspaceScopeSignature } from '../../../../shared/linear/workspace-types'
|
||||
import {
|
||||
credentialDecryptionMessage,
|
||||
isIntegrationCredentialDecryptionError
|
||||
} from '../../../../shared/integration-credential-errors'
|
||||
import { getSmartWorkspaceLinearSearchQuery } from '../../../../shared/new-workspace/smart-workspace-linear-intent'
|
||||
import type { parseBoundedSmartWorkspaceLinearIssueUrlIntent } from '../../../../shared/new-workspace/smart-workspace-linear-intent'
|
||||
import { RESULT_LIMIT } from './smart-workspace-name-field-model'
|
||||
@@ -51,6 +55,7 @@ export function useSmartWorkspaceSecondarySearches({
|
||||
setLinearIssues,
|
||||
setLinearLoading,
|
||||
setSettledLinearUrlQuery,
|
||||
setLinearReadError,
|
||||
jiraSourceContext,
|
||||
jiraConnectionStatus,
|
||||
jiraProject,
|
||||
@@ -74,6 +79,7 @@ export function useSmartWorkspaceSecondarySearches({
|
||||
listLinearIssues,
|
||||
searchLinearIssues
|
||||
}
|
||||
const failedLinearReadRef = useRef<readonly unknown[] | null>(null)
|
||||
const linearConnected = linearStatus.connected === true
|
||||
const linearScopeSignature = linearWorkspaceScopeSignature(linearStatus)
|
||||
const branchSearchRequest = useMemo(
|
||||
@@ -150,16 +156,27 @@ export function useSmartWorkspaceSecondarySearches({
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled || !shouldQueryLinear || !linearConnected) {
|
||||
const readKey = [linearQuery, linearSourceContext, linearWorkspaceIdOverride] as const
|
||||
// Why: the status refresh after a failure re-runs this same read; don't blink its error.
|
||||
const rereadOfFailure =
|
||||
failedLinearReadRef.current?.every((part, index) => part === readKey[index]) === true
|
||||
const canQuery = !disabled && shouldQueryLinear && linearConnected
|
||||
if (!rereadOfFailure || !canQuery) {
|
||||
failedLinearReadRef.current = null
|
||||
setLinearReadError(null)
|
||||
}
|
||||
if (!canQuery) {
|
||||
setLinearIssues([])
|
||||
setLinearLoading(false)
|
||||
setSettledLinearUrlQuery(null)
|
||||
return
|
||||
}
|
||||
let stale = false
|
||||
setLinearLoading(true)
|
||||
const trimmed = linearQuery.trim()
|
||||
setSettledLinearUrlQuery(null)
|
||||
if (!rereadOfFailure) {
|
||||
setLinearLoading(true)
|
||||
setSettledLinearUrlQuery(null)
|
||||
}
|
||||
// Why: empty-query list must not briefly paint the previous non-empty result set.
|
||||
if (trimmed === '') {
|
||||
setLinearIssues([])
|
||||
@@ -169,7 +186,11 @@ export function useSmartWorkspaceSecondarySearches({
|
||||
intent: linearUrlIntent,
|
||||
knownStatus: linearStatusRef.current,
|
||||
sourceContext: linearSourceContext,
|
||||
fetchLinearIssue: linearReadMethodsRef.current.fetchLinearIssue
|
||||
fetchLinearIssue: (identifier, workspaceId, options) =>
|
||||
linearReadMethodsRef.current.fetchLinearIssue(identifier, workspaceId, {
|
||||
...options,
|
||||
rejectCredentialError: true
|
||||
})
|
||||
}).then((issue) => (issue ? [issue] : []))
|
||||
: trimmed
|
||||
? linearReadMethodsRef.current.searchLinearIssues(
|
||||
@@ -196,12 +217,18 @@ export function useSmartWorkspaceSecondarySearches({
|
||||
void request
|
||||
.then((issues) => {
|
||||
if (!stale) {
|
||||
failedLinearReadRef.current = null
|
||||
setLinearReadError(null)
|
||||
setLinearIssues(issues)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((error) => {
|
||||
if (!stale) {
|
||||
setLinearIssues([])
|
||||
// Why: the read reports its own host's failure; host-wide status may describe another host.
|
||||
const credentialError = isIntegrationCredentialDecryptionError(error)
|
||||
failedLinearReadRef.current = credentialError ? readKey : null
|
||||
setLinearReadError(credentialError ? credentialDecryptionMessage('Linear') : null)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -30,6 +30,7 @@ export function useWorkItemSourceState(textOnly: boolean, value: string) {
|
||||
null
|
||||
)
|
||||
const [settledLinearUrlQuery, setSettledLinearUrlQuery] = useState<string | null>(null)
|
||||
const [linearReadError, setLinearReadError] = useState<string | null>(null)
|
||||
const [jiraLoading, setJiraLoading] = useState(false)
|
||||
const [commandValue, setCommandValue] = useState('')
|
||||
const repoSlugCacheRef = useRef<Map<string, RepoSlug>>(new Map())
|
||||
@@ -67,6 +68,8 @@ export function useWorkItemSourceState(textOnly: boolean, value: string) {
|
||||
setLinearUrlLoadingFeedbackQuery,
|
||||
settledLinearUrlQuery,
|
||||
setSettledLinearUrlQuery,
|
||||
linearReadError,
|
||||
setLinearReadError,
|
||||
jiraLoading,
|
||||
setJiraLoading,
|
||||
commandValue,
|
||||
|
||||
@@ -258,7 +258,10 @@ export function WorkspaceAttachmentSearch({
|
||||
{query.trim() ? (
|
||||
<div className="mt-2 flex max-h-60 min-h-0 shrink flex-col overflow-hidden">
|
||||
<SmartWorkspaceSourceResultList
|
||||
model={controller}
|
||||
// Why: a Jira URL's status row owns the message; a Linear read of the URL text says nothing about it.
|
||||
model={
|
||||
taskUrlProvider === 'jira' ? { ...controller, linearReadError: null } : controller
|
||||
}
|
||||
onSelect={selectRow}
|
||||
isLinked={linked}
|
||||
isRowDisabled={() => props.disabled || controller.isQueryStale}
|
||||
|
||||
@@ -17,7 +17,9 @@ export function WorkspaceAttachmentSourceStatus({
|
||||
error: string | null
|
||||
onRetry: () => void
|
||||
}): React.JSX.Element {
|
||||
let message =
|
||||
// Why: on the Linear tab the empty result list explains an unreadable login.
|
||||
const linearLoginUnreadable = controller.mode === 'linear' && Boolean(controller.linearReadError)
|
||||
let message: string | null =
|
||||
error ?? translate('workspace.links.inlineSearchHint', 'Select a result to attach it.')
|
||||
let retry: (() => void) | null = null
|
||||
if (!error && taskUrlProvider === 'jira') {
|
||||
@@ -48,6 +50,9 @@ export function WorkspaceAttachmentSourceStatus({
|
||||
'Connect Linear in Settings to link this issue.'
|
||||
)
|
||||
retry = onRetry
|
||||
} else if (linearLoginUnreadable) {
|
||||
// Why: Retry re-reads once Linear is reconnected.
|
||||
retry = onRetry
|
||||
} else if (controller.unresolvedLinearUrlIntent || controller.linearLoading) {
|
||||
message = translate('workspace.links.loadingLinear', 'Loading Linear issue…')
|
||||
} else {
|
||||
@@ -58,13 +63,17 @@ export function WorkspaceAttachmentSourceStatus({
|
||||
retry = onRetry
|
||||
}
|
||||
}
|
||||
// Why: keep the row (and its action) so the sibling Reference type button doesn't shift.
|
||||
if (!error && linearLoginUnreadable && taskUrlProvider !== 'jira') {
|
||||
message = null
|
||||
}
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="flex min-w-0 items-center gap-2 text-xs text-muted-foreground"
|
||||
>
|
||||
<span>{message}</span>
|
||||
{message ? <span>{message}</span> : null}
|
||||
{retry ? (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
shouldOfferLinearIssueFetchMore
|
||||
} from '@/components/task-page-linear-issue-empty-state'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { LinearIcon } from '@/components/task-page-localized-options'
|
||||
import { isIntegrationCredentialDecryptionError } from '../../../../../shared/integration-credential-errors'
|
||||
import {
|
||||
clampLinearIssueListLimit,
|
||||
LINEAR_ISSUE_LIST_MAX
|
||||
@@ -26,11 +28,18 @@ export function TaskPageLinearIssueBoard({
|
||||
activeLinearIssues,
|
||||
activeLinearIssueLoading,
|
||||
activeLinearIssueError,
|
||||
linearError,
|
||||
activeLinearIssueHasCollectionError,
|
||||
activeLinearIssueContextLabel,
|
||||
linearSearchActive,
|
||||
filteredLinearIssues
|
||||
} = model
|
||||
// Why: the plain list's own read decides; host-wide status flags any broken workspace.
|
||||
const linearLoginUnreadable =
|
||||
activeLinearIssues.length === 0 &&
|
||||
!activeLinearIssueContextLabel &&
|
||||
Boolean(linearError) &&
|
||||
isIntegrationCredentialDecryptionError(linearError)
|
||||
return (
|
||||
<div
|
||||
className="min-h-0 flex-1 overflow-y-auto scrollbar-sleek"
|
||||
@@ -38,13 +47,26 @@ export function TaskPageLinearIssueBoard({
|
||||
scrollbarGutter: 'stable'
|
||||
}}
|
||||
>
|
||||
{activeLinearIssueError ? (
|
||||
{linearLoginUnreadable ? (
|
||||
<div className="m-4 flex flex-col items-center justify-center rounded-md border border-border/50 bg-muted/50 px-6 py-14 text-center shadow-sm">
|
||||
<LinearIcon className="mb-4 size-8 text-muted-foreground/60" />
|
||||
<p className="text-base font-medium text-foreground">
|
||||
{translate('auto.components.TaskPage.linearLoginUnreadableTitle', 'Reconnect Linear')}
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.TaskPage.linearLoginUnreadableBody',
|
||||
'Unable to read saved Linear login. Reconnect it in Settings.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : activeLinearIssueError ? (
|
||||
<div className="border-b border-border px-4 py-4 text-sm text-destructive">
|
||||
{activeLinearIssueError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{activeLinearIssueLoading && activeLinearIssues.length === 0 ? (
|
||||
{activeLinearIssueLoading && activeLinearIssues.length === 0 && !linearLoginUnreadable ? (
|
||||
<div className="divide-y divide-border/50">
|
||||
{Array.from({
|
||||
length: 12
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TaskPageGlobalEffectsModel } from './use-task-page-global-effects'
|
||||
import { useEffect } from 'react'
|
||||
import { isIntegrationCredentialDecryptionError } from '../../../shared/integration-credential-errors'
|
||||
import { saveLinearIssueView } from './linear-issue-view-storage'
|
||||
import { serializeLinearIssueViewResumeState } from '../../../shared/linear/issue-view-resume-state'
|
||||
import {
|
||||
@@ -139,7 +140,10 @@ export function useTaskPageLinearListEffects(model: TaskPageGlobalEffectsModel)
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
setLinearError(null)
|
||||
// Why: keep an unreadable-login error up during the re-read so the Reconnect card doesn't flash.
|
||||
setLinearError((current) =>
|
||||
current && isIntegrationCredentialDecryptionError(current) ? current : null
|
||||
)
|
||||
const trimmed = appliedLinearSearch.trim()
|
||||
const effectiveLinearIssueLimit = clampLinearIssueListLimit(linearIssueLimit)
|
||||
const searchActive = trimmed.length > 0
|
||||
@@ -233,6 +237,7 @@ export function useTaskPageLinearListEffects(model: TaskPageGlobalEffectsModel)
|
||||
) {
|
||||
return
|
||||
}
|
||||
setLinearError(null)
|
||||
if (readArgs.kind === 'search') {
|
||||
const issues = result as LinearIssue[]
|
||||
setLinearIssuesHasMore(false)
|
||||
@@ -264,6 +269,11 @@ export function useTaskPageLinearListEffects(model: TaskPageGlobalEffectsModel)
|
||||
) {
|
||||
return
|
||||
}
|
||||
// Why: a credential failure used to resolve as no rows; keep not showing the previous scope's rows.
|
||||
if (isIntegrationCredentialDecryptionError(err)) {
|
||||
setLinearIssues([])
|
||||
setLinearIssuesHasMore(false)
|
||||
}
|
||||
setLinearError(err instanceof Error ? err.message : 'Failed to load Linear issues.')
|
||||
setLinearLoading(false)
|
||||
})
|
||||
|
||||
@@ -2357,6 +2357,8 @@
|
||||
"851017590d": "Add Linear access",
|
||||
"228b25028f": "Browse and start work on your assigned Linear issues directly from here.",
|
||||
"6d56559467": "Connect your Linear account",
|
||||
"linearLoginUnreadableTitle": "Reconnect Linear",
|
||||
"linearLoginUnreadableBody": "Unable to read saved Linear login. Reconnect it in Settings.",
|
||||
"eee68073b2": "Open in Jira",
|
||||
"9497f2787c": "Start workspace",
|
||||
"eba87f2edb": "No Jira issues found",
|
||||
@@ -3116,6 +3118,7 @@
|
||||
"93e1f6bfca": "Loading",
|
||||
"98730088a6": ". Search or open Linear for the full set.",
|
||||
"06b887d622": "Showing first",
|
||||
"linearLoginUnreadable": "Unable to read saved Linear login. Reconnect it in Settings.",
|
||||
"2c4b1c2c08": "number",
|
||||
"f2cc1e0ff6": "Linear / Projects",
|
||||
"906b5e4cb8": "Linear / Projects / {{value0}}",
|
||||
@@ -14781,7 +14784,9 @@
|
||||
"jiraMode": "Jira",
|
||||
"jiraSelectBindFailed": "Couldn’t link this Jira issue. Pick the matching site or reconnect Jira, then try again.",
|
||||
"emoji": "Emoji",
|
||||
"loadingLinearIssue": "Loading Linear issue…"
|
||||
"loadingLinearIssue": "Loading Linear issue…",
|
||||
"linearLoginUnreadableTitle": "Reconnect Linear",
|
||||
"linearLoginUnreadableBody": "Unable to read saved Linear login. Reconnect it in Settings."
|
||||
},
|
||||
"ProjectCombobox": {
|
||||
"empty": "No projects match your search.",
|
||||
|
||||
@@ -2128,6 +2128,8 @@
|
||||
"851017590d": "Agregar acceso Linear",
|
||||
"228b25028f": "Explora y empieza a trabajar en tus issues asignados de Linear directamente desde aquí.",
|
||||
"6d56559467": "Conecta tu cuenta Linear",
|
||||
"linearLoginUnreadableTitle": "Vuelve a conectar Linear",
|
||||
"linearLoginUnreadableBody": "No se puede leer el inicio de sesión guardado de Linear. Vuelve a conectarlo en Configuración.",
|
||||
"eee68073b2": "Abrir en Jira",
|
||||
"9497f2787c": "Iniciar espacio de trabajo",
|
||||
"eba87f2edb": "No se encontraron issues de Jira",
|
||||
@@ -2887,6 +2889,7 @@
|
||||
"93e1f6bfca": "Cargando",
|
||||
"98730088a6": ". Busca o abre Linear para ver el conjunto completo.",
|
||||
"06b887d622": "Mostrando primero",
|
||||
"linearLoginUnreadable": "No se puede leer el inicio de sesión guardado de Linear. Vuelve a conectarlo en Configuración.",
|
||||
"2c4b1c2c08": "número",
|
||||
"f2cc1e0ff6": "Linear / Proyectos",
|
||||
"906b5e4cb8": "Linear / Proyectos / {{value0}}",
|
||||
@@ -14491,7 +14494,9 @@
|
||||
"jiraMode": "Jira",
|
||||
"jiraSelectBindFailed": "Couldn’t link this Jira issue. Pick the matching site or reconnect Jira, then try again.",
|
||||
"emoji": "Emoji",
|
||||
"loadingLinearIssue": "Cargando problema Linear..."
|
||||
"loadingLinearIssue": "Cargando problema Linear...",
|
||||
"linearLoginUnreadableTitle": "Vuelve a conectar Linear",
|
||||
"linearLoginUnreadableBody": "No se puede leer el inicio de sesión guardado de Linear. Vuelve a conectarlo en Configuración."
|
||||
},
|
||||
"ProjectCombobox": {
|
||||
"empty": "Ningún proyecto coincide con tu búsqueda.",
|
||||
|
||||
@@ -2237,6 +2237,8 @@
|
||||
"851017590d": "Ajouter l'accès Linear",
|
||||
"228b25028f": "Parcourez vos tickets Linear assignés et démarrez le travail directement depuis ici.",
|
||||
"6d56559467": "Connectez votre compte Linear",
|
||||
"linearLoginUnreadableTitle": "Reconnectez Linear",
|
||||
"linearLoginUnreadableBody": "Impossible de lire la connexion Linear enregistrée. Reconnectez-la dans les réglages.",
|
||||
"eee68073b2": "Ouvrir dans Jira",
|
||||
"9497f2787c": "Démarrer l'espace de travail",
|
||||
"eba87f2edb": "Aucun ticket Jira trouvé",
|
||||
@@ -2996,6 +2998,7 @@
|
||||
"93e1f6bfca": "Chargement",
|
||||
"98730088a6": ". Recherchez ou ouvrez Linear pour voir l'ensemble complet.",
|
||||
"06b887d622": "Affichage des premières",
|
||||
"linearLoginUnreadable": "Impossible de lire la connexion Linear enregistrée. Reconnectez-la dans les réglages.",
|
||||
"2c4b1c2c08": "nombre",
|
||||
"f2cc1e0ff6": "Linear / Projets",
|
||||
"906b5e4cb8": "Linear / Projets / {{value0}}",
|
||||
@@ -14613,7 +14616,9 @@
|
||||
"jiraMode": "Jira",
|
||||
"jiraSelectBindFailed": "Impossible d'associer cette issue Jira. Sélectionnez le site correspondant ou reconnectez Jira, puis réessayez.",
|
||||
"emoji": "Émoji",
|
||||
"loadingLinearIssue": "Chargement de l'issue Linear…"
|
||||
"loadingLinearIssue": "Chargement de l'issue Linear…",
|
||||
"linearLoginUnreadableTitle": "Reconnectez Linear",
|
||||
"linearLoginUnreadableBody": "Impossible de lire la connexion Linear enregistrée. Reconnectez-la dans les réglages."
|
||||
},
|
||||
"ProjectCombobox": {
|
||||
"empty": "Aucun projet ne correspond à votre recherche.",
|
||||
|
||||
@@ -2129,6 +2129,8 @@
|
||||
"851017590d": "Linear アクセスを追加",
|
||||
"228b25028f": "ここから直接、割り当てられた Linear Issue を参照して作業を開始できます。",
|
||||
"6d56559467": "Linear アカウントを接続する",
|
||||
"linearLoginUnreadableTitle": "Linear を再接続",
|
||||
"linearLoginUnreadableBody": "保存された Linear のログイン情報を読み取れません。設定で再接続してください。",
|
||||
"eee68073b2": "Jira で開く",
|
||||
"9497f2787c": "ワークスペースを開始",
|
||||
"eba87f2edb": "Jira Issue が見つかりません",
|
||||
@@ -2888,6 +2890,7 @@
|
||||
"93e1f6bfca": "読み込み中",
|
||||
"98730088a6": "。フルセットについては、Linear を検索するか開きます。",
|
||||
"06b887d622": "最初に表示しています",
|
||||
"linearLoginUnreadable": "保存された Linear のログイン情報を読み取れません。設定で再接続してください。",
|
||||
"2c4b1c2c08": "番号",
|
||||
"f2cc1e0ff6": "Linear / プロジェクト",
|
||||
"906b5e4cb8": "Linear / プロジェクト / {{value0}}",
|
||||
@@ -14496,7 +14499,9 @@
|
||||
"jiraMode": "Jira",
|
||||
"jiraSelectBindFailed": "この Jira Issue をリンクできませんでした。該当するサイトを選択するか Jira に再接続してから、もう一度お試しください。",
|
||||
"emoji": "絵文字",
|
||||
"loadingLinearIssue": "Linear Issue を読み込み中…"
|
||||
"loadingLinearIssue": "Linear Issue を読み込み中…",
|
||||
"linearLoginUnreadableTitle": "Linear を再接続",
|
||||
"linearLoginUnreadableBody": "保存された Linear のログイン情報を読み取れません。設定で再接続してください。"
|
||||
},
|
||||
"ProjectCombobox": {
|
||||
"empty": "検索条件に一致するプロジェクトはありません。",
|
||||
|
||||
@@ -2129,6 +2129,8 @@
|
||||
"851017590d": "Linear 액세스 추가",
|
||||
"228b25028f": "여기에서 직접 할당된 Linear 이슈를 찾아보고 작업을 시작하세요.",
|
||||
"6d56559467": "Linear 계정 연결",
|
||||
"linearLoginUnreadableTitle": "Linear 다시 연결",
|
||||
"linearLoginUnreadableBody": "저장된 Linear 로그인을 읽을 수 없습니다. 설정에서 다시 연결하세요.",
|
||||
"eee68073b2": "Jira에서 열기",
|
||||
"9497f2787c": "워크스페이스 시작",
|
||||
"eba87f2edb": "Jira 이슈가 발견되지 않았습니다.",
|
||||
@@ -2888,6 +2890,7 @@
|
||||
"93e1f6bfca": "로드 중",
|
||||
"98730088a6": ". 전체 세트를 보려면 Linear를 검색하거나 엽니다.",
|
||||
"06b887d622": "처음 표시",
|
||||
"linearLoginUnreadable": "저장된 Linear 로그인을 읽을 수 없습니다. 설정에서 다시 연결하세요.",
|
||||
"2c4b1c2c08": "숫자",
|
||||
"f2cc1e0ff6": "Linear / 프로젝트",
|
||||
"906b5e4cb8": "Linear / 프로젝트 / {{value0}}",
|
||||
@@ -14496,7 +14499,9 @@
|
||||
"jiraMode": "Jira",
|
||||
"jiraSelectBindFailed": "Couldn’t link this Jira issue. Pick the matching site or reconnect Jira, then try again.",
|
||||
"emoji": "Emoji",
|
||||
"loadingLinearIssue": "Linear 문제 로드 중…"
|
||||
"loadingLinearIssue": "Linear 문제 로드 중…",
|
||||
"linearLoginUnreadableTitle": "Linear 다시 연결",
|
||||
"linearLoginUnreadableBody": "저장된 Linear 로그인을 읽을 수 없습니다. 설정에서 다시 연결하세요."
|
||||
},
|
||||
"ProjectCombobox": {
|
||||
"empty": "검색과 일치하는 프로젝트가 없습니다.",
|
||||
|
||||
@@ -2129,6 +2129,8 @@
|
||||
"851017590d": "添加 Linear 访问",
|
||||
"228b25028f": "直接从这里浏览并开始处理分配给您的 Linear 议题。",
|
||||
"6d56559467": "连接您的 Linear 账户",
|
||||
"linearLoginUnreadableTitle": "重新连接 Linear",
|
||||
"linearLoginUnreadableBody": "无法读取已保存的 Linear 登录信息。请在设置中重新连接。",
|
||||
"eee68073b2": "在 Jira 中打开",
|
||||
"9497f2787c": "启动工作区",
|
||||
"eba87f2edb": "未发现 Jira 议题",
|
||||
@@ -2888,6 +2890,7 @@
|
||||
"93e1f6bfca": "加载中",
|
||||
"98730088a6": "。在 Linear 中搜索或打开以查看完整列表。",
|
||||
"06b887d622": "首先显示",
|
||||
"linearLoginUnreadable": "无法读取已保存的 Linear 登录信息。请在设置中重新连接。",
|
||||
"2c4b1c2c08": "数字",
|
||||
"f2cc1e0ff6": "Linear/项目",
|
||||
"906b5e4cb8": "Linear/项目/{{value0}}",
|
||||
@@ -14496,7 +14499,9 @@
|
||||
"jiraMode": "Jira",
|
||||
"jiraSelectBindFailed": "无法关联此 Jira 议题。请选择匹配的站点或重新连接 Jira,然后重试。",
|
||||
"emoji": "Emoji",
|
||||
"loadingLinearIssue": "正在加载 Linear 议题..."
|
||||
"loadingLinearIssue": "正在加载 Linear 议题...",
|
||||
"linearLoginUnreadableTitle": "重新连接 Linear",
|
||||
"linearLoginUnreadableBody": "无法读取已保存的 Linear 登录信息。请在设置中重新连接。"
|
||||
},
|
||||
"ProjectCombobox": {
|
||||
"empty": "没有匹配搜索的项目。",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { linearStatus } from '@/runtime/runtime-linear-client'
|
||||
import {
|
||||
credentialDecryptionMessage,
|
||||
isIntegrationCredentialDecryptionError
|
||||
} from '../../../shared/integration-credential-errors'
|
||||
import {
|
||||
findLinearIssueWorkspaceLookupIds,
|
||||
isLinearIssueUrlResolutionMatch,
|
||||
@@ -23,7 +27,10 @@ async function fetchMatchingLinearIssue(
|
||||
try {
|
||||
const issue = await fetchLinearIssue(intent.identifier, workspaceId, { sourceContext })
|
||||
return issue && isLinearIssueUrlResolutionMatch(intent, issue) ? issue : null
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (isIntegrationCredentialDecryptionError(error)) {
|
||||
throw error
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -46,6 +53,7 @@ export async function lookupLinearIssueUrl({
|
||||
readLinearStatus?: (sourceContext: TaskSourceContext | null) => Promise<LinearConnectionStatus>
|
||||
}): Promise<LinearIssue | null> {
|
||||
const triedWorkspaceIds = new Set<string>()
|
||||
const undecryptableWorkspaceIds = new Set<string>()
|
||||
const lookupInStatus = async (
|
||||
status: Pick<
|
||||
LinearConnectionStatus,
|
||||
@@ -62,7 +70,10 @@ export async function lookupLinearIssueUrl({
|
||||
workspaceId,
|
||||
sourceContext,
|
||||
fetchLinearIssue
|
||||
)
|
||||
).catch(() => {
|
||||
undecryptableWorkspaceIds.add(workspaceId)
|
||||
return null
|
||||
})
|
||||
if (issue) {
|
||||
return issue
|
||||
}
|
||||
@@ -76,5 +87,10 @@ export async function lookupLinearIssueUrl({
|
||||
}
|
||||
|
||||
const currentStatus = await readLinearStatus(sourceContext).catch(() => null)
|
||||
return currentStatus ? lookupInStatus(currentStatus) : null
|
||||
const issue = currentStatus ? await lookupInStatus(currentStatus) : null
|
||||
// Why: a healthy workspace may still match; report the credential failure only when none did.
|
||||
if (!issue && undecryptableWorkspaceIds.size > 0) {
|
||||
throw new Error(credentialDecryptionMessage('Linear'))
|
||||
}
|
||||
return issue
|
||||
}
|
||||
|
||||
@@ -65,42 +65,25 @@ describe('createLinearSlice caching', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('returns an empty list and refreshes status on Linear decrypt errors during list reads', async () => {
|
||||
it('rejects Linear search and list reads with the plain decrypt message and refreshes status', async () => {
|
||||
const store = createTestStore()
|
||||
const error = new Error(credentialDecryptionMessage('Linear'))
|
||||
const message = credentialDecryptionMessage('Linear')
|
||||
// Local reads arrive through IPC, which prefixes the main-process message.
|
||||
const error = new Error(`Error invoking remote method 'linear:searchIssues': Error: ${message}`)
|
||||
store.setState({
|
||||
linearStatus: { connected: true, viewer: null, selectedWorkspaceId: 'workspace-1' },
|
||||
linearListCache: {
|
||||
'workspace-1::list::all::36::': { data: { items: [issue('LIN-CACHED')] }, fetchedAt: 1 }
|
||||
}
|
||||
})
|
||||
linearStatus.mockResolvedValue({
|
||||
connected: true,
|
||||
viewer: null,
|
||||
credentialError: error.message
|
||||
})
|
||||
linearStatus.mockResolvedValue({ connected: true, viewer: null, credentialError: message })
|
||||
linearSearchIssues.mockRejectedValueOnce(error)
|
||||
linearListIssues.mockRejectedValueOnce(error)
|
||||
|
||||
await expect(store.getState().searchLinearIssues('bug', 36)).rejects.toThrow(new Error(message))
|
||||
await expect(
|
||||
store.getState().listLinearIssues({ kind: 'list', filter: 'all', limit: 36 }, { force: true })
|
||||
).resolves.toMatchObject({ items: [] })
|
||||
expect(linearStatus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns an empty list and refreshes status on Linear decrypt errors during searches', async () => {
|
||||
const store = createTestStore()
|
||||
const error = new Error(credentialDecryptionMessage('Linear'))
|
||||
store.setState({
|
||||
linearStatus: { connected: true, viewer: null, selectedWorkspaceId: 'workspace-1' }
|
||||
})
|
||||
linearStatus.mockResolvedValue({
|
||||
connected: true,
|
||||
viewer: null,
|
||||
credentialError: error.message
|
||||
})
|
||||
linearSearchIssues.mockRejectedValueOnce(error)
|
||||
|
||||
await expect(store.getState().searchLinearIssues('bug', 36)).resolves.toEqual([])
|
||||
).rejects.toThrow(new Error(message))
|
||||
expect(linearStatus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import type {
|
||||
LinearSliceSet,
|
||||
LinearFetchOptions
|
||||
} from './linear-slice-contract'
|
||||
import { isIntegrationCredentialDecryptionError } from '../../../../../shared/integration-credential-errors'
|
||||
import {
|
||||
credentialDecryptionMessage,
|
||||
isIntegrationCredentialDecryptionError
|
||||
} from '../../../../../shared/integration-credential-errors'
|
||||
import { linearGetIssue } from '@/runtime/runtime-linear-issue-mutations'
|
||||
import {
|
||||
clearLinearIssueCollectionRequestMaps,
|
||||
@@ -45,6 +48,10 @@ export function createLinearIssueDetailActions(
|
||||
if (isFresh(cached)) {
|
||||
return cached.data
|
||||
}
|
||||
// Why: cards and sub-issue reads treat an unreadable issue as missing; only the
|
||||
// composer's URL lookup asks to see the credential failure.
|
||||
const settle = (request: Promise<LinearIssue | null>): Promise<LinearIssue | null> =>
|
||||
options?.rejectCredentialError ? request : request.catch(() => null)
|
||||
|
||||
const inflight = inflightIssueRequests.get(issueCacheKey)
|
||||
if (
|
||||
@@ -52,7 +59,7 @@ export function createLinearIssueDetailActions(
|
||||
inflight.contextKey === contextKey &&
|
||||
inflight.mutationGeneration === getLinearMutationGeneration()
|
||||
) {
|
||||
return inflight.promise
|
||||
return settle(inflight.promise)
|
||||
}
|
||||
|
||||
let entry: InflightLinearIssueRequest
|
||||
@@ -94,6 +101,9 @@ export function createLinearIssueDetailActions(
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
}
|
||||
if (isIntegrationCredentialDecryptionError(error)) {
|
||||
throw new Error(credentialDecryptionMessage('Linear'))
|
||||
}
|
||||
return null
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -121,7 +131,7 @@ export function createLinearIssueDetailActions(
|
||||
mutationGeneration: requestMutationGeneration
|
||||
}
|
||||
inflightIssueRequests.set(issueCacheKey, entry)
|
||||
return promise
|
||||
return settle(promise)
|
||||
},
|
||||
|
||||
refreshLinearIssue: async (
|
||||
|
||||
@@ -8,7 +8,10 @@ import type {
|
||||
LinearSliceSet
|
||||
} from './linear-slice-contract'
|
||||
import { clampLinearIssueListLimit } from '../../../../../shared/linear/issue-read-limits'
|
||||
import { isIntegrationCredentialDecryptionError } from '../../../../../shared/integration-credential-errors'
|
||||
import {
|
||||
credentialDecryptionMessage,
|
||||
isIntegrationCredentialDecryptionError
|
||||
} from '../../../../../shared/integration-credential-errors'
|
||||
import {
|
||||
isLinearIssueAttributeFilterUnsupportedError,
|
||||
linearListIssues,
|
||||
@@ -91,9 +94,10 @@ export function createLinearIssueListActions(
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('[linear] searchLinearIssues failed:', error)
|
||||
const credentialError = isIntegrationCredentialDecryptionError(error)
|
||||
if (
|
||||
!(scope.explicitSource && options?.workspaceId !== undefined) &&
|
||||
(isIntegrationCredentialDecryptionError(error) || looksLikeAuthError(error)) &&
|
||||
(credentialError || looksLikeAuthError(error)) &&
|
||||
canWriteLinearReadResult(
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
@@ -105,7 +109,13 @@ export function createLinearIssueListActions(
|
||||
if (!shouldRefreshStatusAfterRead(workspaceId, get().linearStatus)) {
|
||||
void get().checkLinearConnection(true)
|
||||
}
|
||||
return []
|
||||
if (!credentialError) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
// Why: [] would read as "no matches"; the caller shows this read's own error.
|
||||
if (credentialError) {
|
||||
throw new Error(credentialDecryptionMessage('Linear'))
|
||||
}
|
||||
return get().linearSearchCache[cacheKey]?.data ?? []
|
||||
})
|
||||
@@ -203,9 +213,10 @@ export function createLinearIssueListActions(
|
||||
if (isLinearIssueAttributeFilterUnsupportedError(error)) {
|
||||
throw error
|
||||
}
|
||||
const credentialError = isIntegrationCredentialDecryptionError(error)
|
||||
if (
|
||||
!(scope.explicitSource && options?.workspaceId !== undefined) &&
|
||||
(isIntegrationCredentialDecryptionError(error) || looksLikeAuthError(error)) &&
|
||||
(credentialError || looksLikeAuthError(error)) &&
|
||||
canWriteLinearReadResult(
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
@@ -217,7 +228,12 @@ export function createLinearIssueListActions(
|
||||
if (!shouldRefreshStatusAfterRead(workspaceId, get().linearStatus)) {
|
||||
void get().checkLinearConnection(true)
|
||||
}
|
||||
return emptyLinearCollection<LinearIssue>()
|
||||
if (!credentialError) {
|
||||
return emptyLinearCollection<LinearIssue>()
|
||||
}
|
||||
}
|
||||
if (credentialError) {
|
||||
throw new Error(credentialDecryptionMessage('Linear'))
|
||||
}
|
||||
return get().linearListCache[cacheKey]?.data ?? emptyLinearCollection<LinearIssue>()
|
||||
})
|
||||
|
||||
@@ -33,6 +33,8 @@ export type LinearFetchOptions = {
|
||||
force?: boolean
|
||||
sourceContext?: TaskSourceContext | null
|
||||
workspaceId?: LinearWorkspaceSelection | null
|
||||
// Issue reads only: reject on an undecryptable credential instead of resolving null.
|
||||
rejectCredentialError?: boolean
|
||||
}
|
||||
export type LinearPatchOptions = { sourceContext?: TaskSourceContext | null }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user