mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 08:03:09 +00:00
Route Linear and Jira tasks by source context
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -34,8 +34,8 @@ Done means: a new agent can read the shared types and understand exactly where w
|
||||
- [~] Update GitHub cache keys to include provider identity and source host/runtime identity so two hosts with different GitHub accounts cannot collide.
|
||||
- [x] Update GitLab IPC/preload/main handlers to accept explicit project/source context instead of path-only selectors.
|
||||
- [~] Guard or retire GitLab path-only fallbacks where they can mutate or fetch from the wrong host.
|
||||
- [~] Update Linear routing to use an explicit source account/workspace context instead of settings.activeRuntimeEnvironmentId.
|
||||
- [~] Update Jira routing to use an explicit source account/site/project context instead of settings.activeRuntimeEnvironmentId.
|
||||
- [x] Update Linear routing to use an explicit source account/workspace context instead of settings.activeRuntimeEnvironmentId.
|
||||
- [x] Update Jira routing to use an explicit source account/site/project context instead of settings.activeRuntimeEnvironmentId.
|
||||
- [~] Add provider diagnostics that show which host/account/site a task source is using.
|
||||
- [~] Add error states for unavailable source hosts, missing provider auth, unsupported provider on host, and version-skewed remote servers.
|
||||
|
||||
@@ -226,3 +226,4 @@ Done means: reviewers and future agents can understand the vision, implementatio
|
||||
- [x] 2026-06-12: Completed source-context unit coverage by adding GitLab/Linear/Jira provider identity cache-scope serialization tests and rerunning shared source-context, GitHub source/cache routing, UI open-task, and task-navigation history suites (6 files / 261 tests) plus targeted oxlint.
|
||||
- [x] 2026-06-12: Reconciled Add Project host-selection tests with the disabled-host UX: disconnected SSH hosts and blocked runtime hosts remain visible but disabled, the hook falls back to Local Mac instead of selecting a disconnected SSH host, and selection handlers ignore unavailable hosts; verified Add Project host selector/selection/dialog, sidebar host options, and execution-host registry tests (5 files / 34 tests) plus targeted oxlint.
|
||||
- [x] 2026-06-12: Re-verified project-first drag/reorder behavior across workspace manual order, drag units, workspace-board pointer drops, host-section order/rows, repo reorder host splitting, and project-header drag helpers; 9 files / 62 tests passed plus targeted oxlint.
|
||||
- [x] 2026-06-12: Routed Linear and Jira Tasks reads, create flows, list/detail mutations, comments, metadata, and New Workspace handoff through explicit account-backed `TaskSourceContext` while preserving legacy focused-runtime cache keys for old callers; verified with Linear/Jira source-routing regressions, focused Linear/Jira slice tests (2 files / 48 tests), targeted oxlint, `git diff --check`, and full `pnpm run typecheck`.
|
||||
|
||||
@@ -42,12 +42,14 @@ import type {
|
||||
JiraTransition,
|
||||
JiraUser
|
||||
} from '../../../shared/types'
|
||||
import type { TaskSourceContext } from '../../../shared/task-source-context'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type JiraIssueWorkspaceProps = {
|
||||
issue: JiraIssue | null
|
||||
onUse: (issue: JiraIssue) => void
|
||||
onClose: () => void
|
||||
sourceContext?: TaskSourceContext | null
|
||||
}
|
||||
|
||||
const relativeFormatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
|
||||
@@ -111,9 +113,11 @@ async function copyTextToClipboard(text: string, label: string): Promise<void> {
|
||||
export default function JiraIssueWorkspace({
|
||||
issue,
|
||||
onUse,
|
||||
onClose
|
||||
onClose,
|
||||
sourceContext
|
||||
}: JiraIssueWorkspaceProps): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const providerSettings = sourceContext ?? settings
|
||||
const patchJiraIssue = useAppStore((s) => s.patchJiraIssue)
|
||||
const [fullIssue, setFullIssue] = useState<JiraIssue | null>(null)
|
||||
const [issueLoading, setIssueLoading] = useState(false)
|
||||
@@ -139,7 +143,7 @@ export default function JiraIssueWorkspace({
|
||||
setCommentsLoading(true)
|
||||
setCommentsError(null)
|
||||
try {
|
||||
let fetched = await jiraIssueComments(settings, targetIssue.key, targetIssue.siteId)
|
||||
let fetched = await jiraIssueComments(providerSettings, targetIssue.key, targetIssue.siteId)
|
||||
if (requestId !== requestIdRef.current) {
|
||||
return
|
||||
}
|
||||
@@ -159,7 +163,7 @@ export default function JiraIssueWorkspace({
|
||||
}
|
||||
}
|
||||
},
|
||||
[settings]
|
||||
[providerSettings]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -186,7 +190,7 @@ export default function JiraIssueWorkspace({
|
||||
setCommentsError(null)
|
||||
setIssueLoading(true)
|
||||
|
||||
void jiraGetIssue(settings, issue.key, issue.siteId)
|
||||
void jiraGetIssue(providerSettings, issue.key, issue.siteId)
|
||||
.then((result) => {
|
||||
if (requestId !== requestIdRef.current) {
|
||||
return
|
||||
@@ -205,9 +209,9 @@ export default function JiraIssueWorkspace({
|
||||
})
|
||||
|
||||
void Promise.all([
|
||||
jiraListTransitions(settings, issue.key, issue.siteId),
|
||||
jiraListPriorities(settings, issue.siteId),
|
||||
jiraListAssignableUsers(settings, issue.key, undefined, issue.siteId)
|
||||
jiraListTransitions(providerSettings, issue.key, issue.siteId),
|
||||
jiraListPriorities(providerSettings, issue.siteId),
|
||||
jiraListAssignableUsers(providerSettings, issue.key, undefined, issue.siteId)
|
||||
])
|
||||
.then(([nextTransitions, nextPriorities, nextUsers]) => {
|
||||
if (requestId !== requestIdRef.current) {
|
||||
@@ -220,14 +224,14 @@ export default function JiraIssueWorkspace({
|
||||
.catch(() => {})
|
||||
|
||||
void loadComments(issue, requestId)
|
||||
}, [issue, loadComments, settings])
|
||||
}, [issue, loadComments, providerSettings])
|
||||
|
||||
const refreshIssue = useCallback(async (): Promise<void> => {
|
||||
if (!displayed) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const latest = await jiraGetIssue(settings, displayed.key, displayed.siteId)
|
||||
const latest = await jiraGetIssue(providerSettings, displayed.key, displayed.siteId)
|
||||
if (latest) {
|
||||
setFullIssue(latest)
|
||||
patchJiraIssue(latest.key, latest)
|
||||
@@ -235,7 +239,7 @@ export default function JiraIssueWorkspace({
|
||||
} catch {
|
||||
// Keep the visible issue snapshot if refresh fails.
|
||||
}
|
||||
}, [displayed, patchJiraIssue, settings])
|
||||
}, [displayed, patchJiraIssue, providerSettings])
|
||||
|
||||
const mutateIssue = useCallback(
|
||||
async (
|
||||
@@ -253,7 +257,7 @@ export default function JiraIssueWorkspace({
|
||||
setFullIssue({ ...displayed, ...optimistic })
|
||||
patchJiraIssue(displayed.key, optimistic)
|
||||
}
|
||||
const result = await jiraUpdateIssue(settings, displayed.key, updates, siteId)
|
||||
const result = await jiraUpdateIssue(providerSettings, displayed.key, updates, siteId)
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
@@ -273,7 +277,7 @@ export default function JiraIssueWorkspace({
|
||||
setPendingField(null)
|
||||
}
|
||||
},
|
||||
[displayed, patchJiraIssue, pendingField, refreshIssue, settings, siteId]
|
||||
[displayed, patchJiraIssue, pendingField, refreshIssue, providerSettings, siteId]
|
||||
)
|
||||
|
||||
const handleSaveTitle = useCallback(() => {
|
||||
@@ -309,7 +313,12 @@ export default function JiraIssueWorkspace({
|
||||
}
|
||||
setCommentSubmitting(true)
|
||||
try {
|
||||
const result = await jiraAddIssueComment(settings, displayed.key, body, displayed.siteId)
|
||||
const result = await jiraAddIssueComment(
|
||||
providerSettings,
|
||||
displayed.key,
|
||||
body,
|
||||
displayed.siteId
|
||||
)
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
@@ -331,7 +340,7 @@ export default function JiraIssueWorkspace({
|
||||
} finally {
|
||||
setCommentSubmitting(false)
|
||||
}
|
||||
}, [commentDraft, commentSubmitting, displayed, settings])
|
||||
}, [commentDraft, commentSubmitting, displayed, providerSettings])
|
||||
|
||||
const actionItems = useMemo(() => {
|
||||
if (!displayed) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useAppStore } from '@/store'
|
||||
import { getScreenSubmitShortcutLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut'
|
||||
import { linearUpdateIssue } from '@/runtime/runtime-linear-client'
|
||||
import type { LinearIssue } from '../../../shared/types'
|
||||
import type { TaskSourceContext } from '../../../shared/task-source-context'
|
||||
import {
|
||||
getLinearIssueTextSavePlan,
|
||||
type LinearIssueTextField
|
||||
@@ -24,6 +25,7 @@ type LinearIssueTextEditorProps = {
|
||||
onIssueChange: (patch: Pick<LinearIssue, 'title'> | Pick<LinearIssue, 'description'>) => void
|
||||
density?: 'page' | 'drawer'
|
||||
fields?: 'all' | 'title' | 'description'
|
||||
sourceContext?: TaskSourceContext | null
|
||||
}
|
||||
|
||||
function useAutosizeTextArea(value: string): React.RefObject<HTMLTextAreaElement | null> {
|
||||
@@ -45,9 +47,11 @@ export function LinearIssueTextEditor({
|
||||
issue,
|
||||
onIssueChange,
|
||||
density = 'page',
|
||||
fields = 'all'
|
||||
fields = 'all',
|
||||
sourceContext
|
||||
}: LinearIssueTextEditorProps): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const providerSettings = sourceContext ?? settings
|
||||
const patchLinearIssue = useAppStore((s) => s.patchLinearIssue)
|
||||
const [draftState, setDraftState] = useState(() => createLinearIssueTextDraftState(issue))
|
||||
const [savingField, setSavingField] = useState<LinearIssueTextField | null>(null)
|
||||
@@ -111,7 +115,7 @@ export function LinearIssueTextEditor({
|
||||
onIssueChange(patch)
|
||||
patchLinearIssue(issue.id, patch)
|
||||
try {
|
||||
const result = await linearUpdateIssue(settings, issue.id, patch, issue.workspaceId)
|
||||
const result = await linearUpdateIssue(providerSettings, issue.id, patch, issue.workspaceId)
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
@@ -156,7 +160,7 @@ export function LinearIssueTextEditor({
|
||||
mountedRef,
|
||||
onIssueChange,
|
||||
patchLinearIssue,
|
||||
settings,
|
||||
providerSettings,
|
||||
titleDraft,
|
||||
updateDescriptionDraft,
|
||||
updateTitleDraft
|
||||
|
||||
@@ -57,6 +57,7 @@ import type {
|
||||
LinearIssueChildSummary,
|
||||
LinearProjectSummary
|
||||
} from '../../../shared/types'
|
||||
import type { TaskSourceContext } from '../../../shared/task-source-context'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type LinearIssueWorkspaceProps = {
|
||||
@@ -66,6 +67,7 @@ type LinearIssueWorkspaceProps = {
|
||||
onClose: () => void
|
||||
variant?: 'sheet' | 'page'
|
||||
backLabel?: string
|
||||
sourceContext?: TaskSourceContext | null
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text: string, label: string): Promise<void> {
|
||||
@@ -111,12 +113,15 @@ function LinearIssueAvatar({
|
||||
|
||||
function LinearIssueSubIssueButton({
|
||||
issue,
|
||||
onOpenIssue
|
||||
onOpenIssue,
|
||||
sourceContext
|
||||
}: {
|
||||
issue: LinearIssue
|
||||
onOpenIssue: (issue: LinearIssue) => void
|
||||
sourceContext?: TaskSourceContext | null
|
||||
}): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const providerSettings = sourceContext ?? settings
|
||||
const fetchLinearIssue = useAppStore((s) => s.fetchLinearIssue)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [title, setTitle] = useState('')
|
||||
@@ -145,7 +150,9 @@ function LinearIssueSubIssueButton({
|
||||
async (subIssue: LinearIssueChildSummary) => {
|
||||
setOpeningSubIssueId(subIssue.id)
|
||||
try {
|
||||
const fullIssue = await fetchLinearIssue(subIssue.id, issue.workspaceId)
|
||||
const fullIssue = await fetchLinearIssue(subIssue.id, issue.workspaceId, {
|
||||
sourceContext
|
||||
})
|
||||
if (!mountedRef.current) {
|
||||
return
|
||||
}
|
||||
@@ -173,7 +180,7 @@ function LinearIssueSubIssueButton({
|
||||
}
|
||||
}
|
||||
},
|
||||
[fetchLinearIssue, issue.workspaceId, mountedRef, onOpenIssue]
|
||||
[fetchLinearIssue, issue.workspaceId, mountedRef, onOpenIssue, sourceContext]
|
||||
)
|
||||
|
||||
const handleCreate = useCallback(async () => {
|
||||
@@ -183,7 +190,7 @@ function LinearIssueSubIssueButton({
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const result = await linearCreateSubIssue(settings, {
|
||||
const result = await linearCreateSubIssue(providerSettings, {
|
||||
parentIssueId: issue.id,
|
||||
teamId: issue.team.id,
|
||||
title: trimmed,
|
||||
@@ -235,7 +242,7 @@ function LinearIssueSubIssueButton({
|
||||
issue.subIssues,
|
||||
issue.team.id,
|
||||
issue.workspaceId,
|
||||
settings,
|
||||
providerSettings,
|
||||
title
|
||||
])
|
||||
|
||||
@@ -311,12 +318,15 @@ function LinearIssueSubIssueButton({
|
||||
|
||||
function LinearIssueSidebarProjectCard({
|
||||
issue,
|
||||
onProjectChanged
|
||||
onProjectChanged,
|
||||
sourceContext
|
||||
}: {
|
||||
issue: LinearIssue
|
||||
onProjectChanged: (project: LinearProjectSummary) => void
|
||||
sourceContext?: TaskSourceContext | null
|
||||
}): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const providerSettings = sourceContext ?? settings
|
||||
const patchLinearIssue = useAppStore((s) => s.patchLinearIssue)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
@@ -331,7 +341,7 @@ function LinearIssueSidebarProjectCard({
|
||||
let cancelled = false
|
||||
const timeout = window.setTimeout(() => {
|
||||
setLoading(true)
|
||||
void linearListProjects(settings, query, 20, issue.workspaceId)
|
||||
void linearListProjects(providerSettings, query, 20, issue.workspaceId)
|
||||
.then((result) => {
|
||||
if (!cancelled) {
|
||||
setProjects(result.items)
|
||||
@@ -359,14 +369,14 @@ function LinearIssueSidebarProjectCard({
|
||||
cancelled = true
|
||||
window.clearTimeout(timeout)
|
||||
}
|
||||
}, [issue.workspaceId, open, query, settings])
|
||||
}, [issue.workspaceId, open, providerSettings, query])
|
||||
|
||||
const handleSelectProject = useCallback(
|
||||
async (project: LinearProjectSummary) => {
|
||||
setSavingProjectId(project.id)
|
||||
try {
|
||||
const result = await linearUpdateIssue(
|
||||
settings,
|
||||
providerSettings,
|
||||
issue.id,
|
||||
{ projectId: project.id },
|
||||
issue.workspaceId
|
||||
@@ -394,7 +404,7 @@ function LinearIssueSidebarProjectCard({
|
||||
setSavingProjectId(null)
|
||||
}
|
||||
},
|
||||
[issue.id, issue.workspaceId, onProjectChanged, patchLinearIssue, settings]
|
||||
[issue.id, issue.workspaceId, onProjectChanged, patchLinearIssue, providerSettings]
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -482,9 +492,11 @@ export default function LinearIssueWorkspace({
|
||||
onOpenIssue,
|
||||
onClose,
|
||||
variant = 'sheet',
|
||||
backLabel = 'Back'
|
||||
backLabel = 'Back',
|
||||
sourceContext
|
||||
}: LinearIssueWorkspaceProps): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const providerSettings = sourceContext ?? settings
|
||||
const [fullIssue, setFullIssue] = useState<LinearIssue | null>(null)
|
||||
const [issueLoading, setIssueLoading] = useState(false)
|
||||
const [comments, setComments] = useState<LinearComment[]>([])
|
||||
@@ -519,7 +531,7 @@ export default function LinearIssueWorkspace({
|
||||
}
|
||||
try {
|
||||
let fetched = (await linearIssueComments(
|
||||
settings,
|
||||
providerSettings,
|
||||
targetIssue.id,
|
||||
targetIssue.workspaceId
|
||||
)) as LinearComment[]
|
||||
@@ -542,7 +554,7 @@ export default function LinearIssueWorkspace({
|
||||
}
|
||||
}
|
||||
},
|
||||
[mountedRef, settings]
|
||||
[mountedRef, providerSettings]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -558,7 +570,7 @@ export default function LinearIssueWorkspace({
|
||||
return
|
||||
}
|
||||
|
||||
const issueKey = `${settings?.activeRuntimeEnvironmentId ?? 'local'}:${issue.workspaceId ?? 'selected'}:${issue.id}`
|
||||
const issueKey = `${sourceContext?.hostId ?? settings?.activeRuntimeEnvironmentId ?? 'local'}:${issue.workspaceId ?? 'selected'}:${issue.id}`
|
||||
if (hydratedIssueKeyRef.current === issueKey) {
|
||||
return
|
||||
}
|
||||
@@ -576,7 +588,7 @@ export default function LinearIssueWorkspace({
|
||||
|
||||
// Why: issue hydration and comments are separate surfaces; a comments
|
||||
// failure should not blank the issue detail the user selected.
|
||||
void linearGetIssue(settings, issue.id, issue.workspaceId)
|
||||
void linearGetIssue(providerSettings, issue.id, issue.workspaceId)
|
||||
.then((issueResult) => {
|
||||
if (!mountedRef.current || requestId !== requestIdRef.current) {
|
||||
return
|
||||
@@ -616,7 +628,7 @@ export default function LinearIssueWorkspace({
|
||||
})
|
||||
|
||||
void loadComments(issue, requestId)
|
||||
}, [issue, loadComments, mountedRef, settings])
|
||||
}, [issue, loadComments, mountedRef, providerSettings, settings, sourceContext?.hostId])
|
||||
|
||||
const displayed = fullIssue ?? issue
|
||||
|
||||
@@ -796,9 +808,17 @@ export default function LinearIssueWorkspace({
|
||||
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-sleek">
|
||||
<div className="mx-auto grid w-full grid-cols-1 gap-10 px-7 py-10 lg:grid-cols-[minmax(0,1fr)_320px] lg:px-10 xl:px-12">
|
||||
<main className="min-w-0">
|
||||
<LinearIssueTextEditor issue={displayed} onIssueChange={handleIssueTextChange} />
|
||||
<LinearIssueTextEditor
|
||||
issue={displayed}
|
||||
onIssueChange={handleIssueTextChange}
|
||||
sourceContext={sourceContext}
|
||||
/>
|
||||
|
||||
<LinearIssueSubIssueButton issue={displayed} onOpenIssue={onOpenIssue} />
|
||||
<LinearIssueSubIssueButton
|
||||
issue={displayed}
|
||||
onOpenIssue={onOpenIssue}
|
||||
sourceContext={sourceContext}
|
||||
/>
|
||||
|
||||
<section className="mt-12 border-t border-border/60 pt-9">
|
||||
<div className="mb-8 flex items-center justify-between gap-3">
|
||||
@@ -894,6 +914,7 @@ export default function LinearIssueWorkspace({
|
||||
workspaceId={displayed.workspaceId}
|
||||
onCommentAdded={handleCommentAdded}
|
||||
variant="linear-page"
|
||||
sourceContext={sourceContext}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
@@ -905,11 +926,13 @@ export default function LinearIssueWorkspace({
|
||||
editState={editState}
|
||||
onEditStateChange={handleEditStateChange}
|
||||
layout="properties"
|
||||
sourceContext={sourceContext}
|
||||
/>
|
||||
) : null}
|
||||
<LinearIssueSidebarProjectCard
|
||||
issue={displayed}
|
||||
onProjectChanged={handleProjectChanged}
|
||||
sourceContext={sourceContext}
|
||||
/>
|
||||
<section className="rounded-xl border border-border/60 bg-card text-card-foreground shadow-xs">
|
||||
<div className="flex h-10 items-center gap-1 border-b border-border/50 px-4 text-sm font-medium text-muted-foreground">
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
} from '@/components/linear-state-pill-style'
|
||||
import { LinearPriorityIcon } from '@/components/linear-priority-icon'
|
||||
import type { LinearIssue, LinearComment } from '../../../shared/types'
|
||||
import type { TaskSourceContext } from '../../../shared/task-source-context'
|
||||
import {
|
||||
linearAddIssueComment,
|
||||
linearGetIssue,
|
||||
@@ -118,6 +119,7 @@ type LinearItemDrawerProps = {
|
||||
issue: LinearIssue | null
|
||||
onUse: (issue: LinearIssue) => void
|
||||
onClose: () => void
|
||||
sourceContext?: TaskSourceContext | null
|
||||
}
|
||||
|
||||
export type LinearEditState = {
|
||||
@@ -134,18 +136,21 @@ type EditSectionProps = {
|
||||
editState: LinearEditState
|
||||
onEditStateChange: (patch: Partial<LinearEditState>) => void
|
||||
layout?: 'chips' | 'properties'
|
||||
sourceContext?: TaskSourceContext | null
|
||||
}
|
||||
|
||||
export function LinearIssueEditSection({
|
||||
issue,
|
||||
editState,
|
||||
onEditStateChange,
|
||||
layout = 'chips'
|
||||
layout = 'chips',
|
||||
sourceContext
|
||||
}: EditSectionProps): React.JSX.Element {
|
||||
const [labelPopoverOpen, setLabelPopoverOpen] = useState(false)
|
||||
const [estimatePopoverOpen, setEstimatePopoverOpen] = useState(false)
|
||||
const patchLinearIssue = useAppStore((s) => s.patchLinearIssue)
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const providerSettings = sourceContext ?? settings
|
||||
const { isPending, run } = useImmediateMutation()
|
||||
|
||||
const {
|
||||
@@ -159,9 +164,9 @@ export function LinearIssueEditSection({
|
||||
const [estimateInput, setEstimateInput] = useState(() => formatLinearEstimateInput(localEstimate))
|
||||
|
||||
const teamId = issue.team?.id || null
|
||||
const states = useTeamStates(teamId, settings, issue.workspaceId)
|
||||
const labels = useTeamLabels(teamId, settings, issue.workspaceId)
|
||||
const members = useTeamMembers(teamId, settings, issue.workspaceId)
|
||||
const states = useTeamStates(teamId, providerSettings, issue.workspaceId)
|
||||
const labels = useTeamLabels(teamId, providerSettings, issue.workspaceId)
|
||||
const members = useTeamMembers(teamId, providerSettings, issue.workspaceId)
|
||||
|
||||
const handleEstimatePopoverOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
@@ -184,7 +189,7 @@ export function LinearIssueEditSection({
|
||||
const stateValue = { name: newState.name, type: newState.type, color: newState.color }
|
||||
|
||||
run('state', {
|
||||
mutate: () => linearUpdateIssue(settings, issue.id, { stateId }, issue.workspaceId),
|
||||
mutate: () => linearUpdateIssue(providerSettings, issue.id, { stateId }, issue.workspaceId),
|
||||
onOptimistic: () => {
|
||||
onEditStateChange({ state: stateValue })
|
||||
patchLinearIssue(issue.id, { state: stateValue })
|
||||
@@ -203,7 +208,7 @@ export function LinearIssueEditSection({
|
||||
issue.id,
|
||||
issue.workspaceId,
|
||||
localState,
|
||||
settings,
|
||||
providerSettings,
|
||||
states.data,
|
||||
patchLinearIssue,
|
||||
run,
|
||||
@@ -216,7 +221,8 @@ export function LinearIssueEditSection({
|
||||
const priority = parseInt(value, 10)
|
||||
const prevPriority = localPriority
|
||||
run('priority', {
|
||||
mutate: () => linearUpdateIssue(settings, issue.id, { priority }, issue.workspaceId),
|
||||
mutate: () =>
|
||||
linearUpdateIssue(providerSettings, issue.id, { priority }, issue.workspaceId),
|
||||
onOptimistic: () => {
|
||||
onEditStateChange({ priority })
|
||||
patchLinearIssue(issue.id, { priority })
|
||||
@@ -231,14 +237,23 @@ export function LinearIssueEditSection({
|
||||
onError: (err) => toast.error(err)
|
||||
})
|
||||
},
|
||||
[issue.id, issue.workspaceId, localPriority, settings, patchLinearIssue, run, onEditStateChange]
|
||||
[
|
||||
issue.id,
|
||||
issue.workspaceId,
|
||||
localPriority,
|
||||
providerSettings,
|
||||
patchLinearIssue,
|
||||
run,
|
||||
onEditStateChange
|
||||
]
|
||||
)
|
||||
|
||||
const handleEstimateChange = useCallback(
|
||||
(estimate: number | null) => {
|
||||
const prevEstimate = localEstimate
|
||||
run('estimate', {
|
||||
mutate: () => linearUpdateIssue(settings, issue.id, { estimate }, issue.workspaceId),
|
||||
mutate: () =>
|
||||
linearUpdateIssue(providerSettings, issue.id, { estimate }, issue.workspaceId),
|
||||
onOptimistic: () => {
|
||||
onEditStateChange({ estimate })
|
||||
patchLinearIssue(issue.id, { estimate })
|
||||
@@ -254,7 +269,15 @@ export function LinearIssueEditSection({
|
||||
onError: (err) => toast.error(err)
|
||||
})
|
||||
},
|
||||
[issue.id, issue.workspaceId, localEstimate, settings, patchLinearIssue, run, onEditStateChange]
|
||||
[
|
||||
issue.id,
|
||||
issue.workspaceId,
|
||||
localEstimate,
|
||||
providerSettings,
|
||||
patchLinearIssue,
|
||||
run,
|
||||
onEditStateChange
|
||||
]
|
||||
)
|
||||
|
||||
const handleEstimateSubmit = useCallback(() => {
|
||||
@@ -287,7 +310,8 @@ export function LinearIssueEditSection({
|
||||
? { id: member.id, displayName: member.displayName, avatarUrl: member.avatarUrl }
|
||||
: undefined
|
||||
run('assignee', {
|
||||
mutate: () => linearUpdateIssue(settings, issue.id, { assigneeId }, issue.workspaceId),
|
||||
mutate: () =>
|
||||
linearUpdateIssue(providerSettings, issue.id, { assigneeId }, issue.workspaceId),
|
||||
onOptimistic: () => {
|
||||
onEditStateChange({ assignee: newAssignee })
|
||||
patchLinearIssue(issue.id, { assignee: newAssignee })
|
||||
@@ -306,7 +330,7 @@ export function LinearIssueEditSection({
|
||||
issue.id,
|
||||
issue.workspaceId,
|
||||
localAssignee,
|
||||
settings,
|
||||
providerSettings,
|
||||
members.data,
|
||||
patchLinearIssue,
|
||||
run,
|
||||
@@ -328,7 +352,12 @@ export function LinearIssueEditSection({
|
||||
|
||||
run('labels', {
|
||||
mutate: () =>
|
||||
linearUpdateIssue(settings, issue.id, { labelIds: newLabelIds }, issue.workspaceId),
|
||||
linearUpdateIssue(
|
||||
providerSettings,
|
||||
issue.id,
|
||||
{ labelIds: newLabelIds },
|
||||
issue.workspaceId
|
||||
),
|
||||
onOptimistic: () => {
|
||||
onEditStateChange({ labelIds: newLabelIds, labels: newLabels })
|
||||
patchLinearIssue(issue.id, { labelIds: newLabelIds, labels: newLabels })
|
||||
@@ -348,7 +377,7 @@ export function LinearIssueEditSection({
|
||||
issue.workspaceId,
|
||||
localLabelIds,
|
||||
localLabels,
|
||||
settings,
|
||||
providerSettings,
|
||||
labels.data,
|
||||
patchLinearIssue,
|
||||
run,
|
||||
@@ -994,14 +1023,17 @@ export function LinearIssueCommentFooter({
|
||||
issueId,
|
||||
workspaceId,
|
||||
onCommentAdded,
|
||||
variant = 'compact'
|
||||
variant = 'compact',
|
||||
sourceContext
|
||||
}: {
|
||||
issueId: string
|
||||
workspaceId?: string | null
|
||||
onCommentAdded: (comment: LinearLocalComment) => void
|
||||
variant?: 'compact' | 'linear-page'
|
||||
sourceContext?: TaskSourceContext | null
|
||||
}): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const providerSettings = sourceContext ?? settings
|
||||
const submitShortcutLabel = getScreenSubmitShortcutLabel()
|
||||
const [body, setBody] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
@@ -1030,7 +1062,7 @@ export function LinearIssueCommentFooter({
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const result = await linearAddIssueComment(settings, issueId, trimmed, workspaceId)
|
||||
const result = await linearAddIssueComment(providerSettings, issueId, trimmed, workspaceId)
|
||||
const typed = result as { ok: boolean; id?: string; error?: string }
|
||||
if (!mountedRef.current) {
|
||||
return
|
||||
@@ -1062,7 +1094,7 @@ export function LinearIssueCommentFooter({
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
}, [body, issueId, onCommentAdded, settings, workspaceId])
|
||||
}, [body, issueId, onCommentAdded, providerSettings, workspaceId])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
@@ -1168,7 +1200,8 @@ export function initLinearIssueEditState(issue: LinearIssue): LinearEditState {
|
||||
export default function LinearItemDrawer({
|
||||
issue,
|
||||
onUse,
|
||||
onClose
|
||||
onClose,
|
||||
sourceContext
|
||||
}: LinearItemDrawerProps): React.JSX.Element {
|
||||
const [fullIssue, setFullIssue] = useState<LinearIssue | null>(null)
|
||||
const [comments, setComments] = useState<LinearComment[]>([])
|
||||
@@ -1178,6 +1211,7 @@ export default function LinearItemDrawer({
|
||||
const hasEditedRef = useRef(false)
|
||||
const optimisticCommentsRef = useRef<LinearComment[]>([])
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const providerSettings = sourceContext ?? settings
|
||||
|
||||
const handleEditStateChange = useCallback((patch: Partial<LinearEditState>) => {
|
||||
hasEditedRef.current = true
|
||||
@@ -1214,7 +1248,7 @@ export default function LinearItemDrawer({
|
||||
|
||||
// Why: fetch issue and comments independently so a transient comments
|
||||
// failure doesn't discard the successfully-fetched issue data.
|
||||
linearGetIssue(settings, issue.id, issue.workspaceId)
|
||||
linearGetIssue(providerSettings, issue.id, issue.workspaceId)
|
||||
.then((issueResult) => {
|
||||
if (requestId !== requestIdRef.current) {
|
||||
return
|
||||
@@ -1231,7 +1265,7 @@ export default function LinearItemDrawer({
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
linearIssueComments(settings, issue.id, issue.workspaceId)
|
||||
linearIssueComments(providerSettings, issue.id, issue.workspaceId)
|
||||
.then((commentsResult) => {
|
||||
if (requestId !== requestIdRef.current) {
|
||||
return
|
||||
@@ -1256,7 +1290,7 @@ export default function LinearItemDrawer({
|
||||
}
|
||||
})
|
||||
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [issue?.id, issue?.workspaceId, settings])
|
||||
}, [issue?.id, issue?.workspaceId, providerSettings])
|
||||
|
||||
// Why: same pointer-events fix as GitHubItemDialog — Radix may leave
|
||||
// pointer-events: none on body when overlays transition.
|
||||
@@ -1343,6 +1377,7 @@ export default function LinearItemDrawer({
|
||||
onIssueChange={handleIssueTextChange}
|
||||
density="drawer"
|
||||
fields="title"
|
||||
sourceContext={sourceContext}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
|
||||
@@ -1400,6 +1435,7 @@ export default function LinearItemDrawer({
|
||||
issue={displayed}
|
||||
editState={editState}
|
||||
onEditStateChange={handleEditStateChange}
|
||||
sourceContext={sourceContext}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1411,6 +1447,7 @@ export default function LinearItemDrawer({
|
||||
onIssueChange={handleIssueTextChange}
|
||||
density="drawer"
|
||||
fields="description"
|
||||
sourceContext={sourceContext}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1472,6 +1509,7 @@ export default function LinearItemDrawer({
|
||||
issueId={displayed.id}
|
||||
workspaceId={displayed.workspaceId}
|
||||
onCommentAdded={handleCommentAdded}
|
||||
sourceContext={sourceContext}
|
||||
/>
|
||||
<div className="flex-none border-t border-border/60 bg-background/40 px-4 py-3">
|
||||
<Button
|
||||
|
||||
@@ -500,14 +500,17 @@ function findLinearWorkflowStateForStatus(
|
||||
|
||||
function LinearStateCell({
|
||||
issue,
|
||||
className
|
||||
className,
|
||||
sourceContext
|
||||
}: {
|
||||
issue: LinearIssue
|
||||
className?: string
|
||||
sourceContext?: TaskSourceContext | null
|
||||
}): React.JSX.Element {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const providerSettings = sourceContext ?? settings
|
||||
const patchLinearIssue = useAppStore((s) => s.patchLinearIssue)
|
||||
const states = useTeamStates(issue.team.id, settings, issue.workspaceId)
|
||||
const states = useTeamStates(issue.team.id, providerSettings, issue.workspaceId)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [pending, setPending] = useState(false)
|
||||
const reqRef = useRef(0)
|
||||
@@ -534,7 +537,7 @@ function LinearStateCell({
|
||||
|
||||
setPending(true)
|
||||
patchLinearIssue(issue.id, { state: nextState })
|
||||
void linearUpdateIssue(settings, issue.id, { stateId }, issue.workspaceId)
|
||||
void linearUpdateIssue(providerSettings, issue.id, { stateId }, issue.workspaceId)
|
||||
.then((result) => {
|
||||
if (reqId !== reqRef.current) {
|
||||
return
|
||||
@@ -571,7 +574,7 @@ function LinearStateCell({
|
||||
issue.workspaceId,
|
||||
patchLinearIssue,
|
||||
pending,
|
||||
settings,
|
||||
providerSettings,
|
||||
states.data
|
||||
]
|
||||
)
|
||||
@@ -2768,7 +2771,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
taskSource === 'github' || taskSource === 'gitlab'
|
||||
? taskSourceRepoContexts.flatMap((context) => {
|
||||
const host = hostRegistryById.get(context.hostId)
|
||||
if (!host || (host.health === 'local' || host.health === 'available')) {
|
||||
if (!host || host.health === 'local' || host.health === 'available') {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
@@ -2786,6 +2789,59 @@ export default function TaskPage(): React.JSX.Element {
|
||||
() => getSettingsFocusedExecutionHostId(settings),
|
||||
[settings]
|
||||
)
|
||||
const fallbackTaskSourceProjectId = useMemo(() => {
|
||||
const firstRepoContext = selectedRepos
|
||||
.map((repo) => getTaskPageRepoSourceContext(repo, 'github'))
|
||||
.find((context): context is TaskSourceContext => context !== null)
|
||||
return firstRepoContext?.projectId ?? 'account-backed-task-source'
|
||||
}, [selectedRepos])
|
||||
const linearTaskSourceContext = useMemo(
|
||||
() =>
|
||||
normalizeTaskSourceContext({
|
||||
provider: 'linear',
|
||||
projectId: fallbackTaskSourceProjectId,
|
||||
hostId: accountBackedTaskSourceHostId,
|
||||
providerIdentity: {
|
||||
provider: 'linear',
|
||||
workspaceId:
|
||||
selectedLinearWorkspaceId && selectedLinearWorkspaceId !== 'all'
|
||||
? selectedLinearWorkspaceId
|
||||
: null,
|
||||
workspaceName:
|
||||
selectedLinearWorkspace?.organizationName ??
|
||||
selectedLinearWorkspace?.displayName ??
|
||||
null
|
||||
},
|
||||
accountLabel:
|
||||
selectedLinearWorkspace?.organizationName ?? selectedLinearWorkspace?.displayName ?? null
|
||||
}),
|
||||
[
|
||||
accountBackedTaskSourceHostId,
|
||||
fallbackTaskSourceProjectId,
|
||||
selectedLinearWorkspace,
|
||||
selectedLinearWorkspaceId
|
||||
]
|
||||
)
|
||||
const jiraTaskSourceContext = useMemo(
|
||||
() =>
|
||||
normalizeTaskSourceContext({
|
||||
provider: 'jira',
|
||||
projectId: fallbackTaskSourceProjectId,
|
||||
hostId: accountBackedTaskSourceHostId,
|
||||
providerIdentity: {
|
||||
provider: 'jira',
|
||||
siteId: selectedJiraSiteId && selectedJiraSiteId !== 'all' ? selectedJiraSiteId : null,
|
||||
siteUrl: selectedJiraSite?.siteUrl ?? null
|
||||
},
|
||||
accountLabel: selectedJiraSite?.displayName ?? selectedJiraSite?.siteUrl ?? null
|
||||
}),
|
||||
[
|
||||
accountBackedTaskSourceHostId,
|
||||
fallbackTaskSourceProjectId,
|
||||
selectedJiraSite,
|
||||
selectedJiraSiteId
|
||||
]
|
||||
)
|
||||
const accountBackedTaskSourceHostAvailability = useMemo<TaskSourceHostAvailability[]>(() => {
|
||||
if (taskSource !== 'linear' && taskSource !== 'jira') {
|
||||
return []
|
||||
@@ -3653,7 +3709,10 @@ export default function TaskPage(): React.JSX.Element {
|
||||
let cancelled = false
|
||||
|
||||
if (context.kind === 'project') {
|
||||
void fetchLinearProject(context.id, context.workspaceId, { force: true })
|
||||
void fetchLinearProject(context.id, context.workspaceId, {
|
||||
force: true,
|
||||
sourceContext: linearTaskSourceContext
|
||||
})
|
||||
.then((project) => {
|
||||
if (cancelled) {
|
||||
return
|
||||
@@ -3689,7 +3748,8 @@ export default function TaskPage(): React.JSX.Element {
|
||||
setLinearCustomViewsLoading(true)
|
||||
setLinearCustomViewsError(null)
|
||||
void fetchLinearCustomView(context.id, context.workspaceId, context.model, {
|
||||
force: true
|
||||
force: true,
|
||||
sourceContext: linearTaskSourceContext
|
||||
})
|
||||
.then((restoredView) => {
|
||||
if (cancelled) {
|
||||
@@ -3722,6 +3782,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
fetchLinearProject,
|
||||
listLinearCustomViews,
|
||||
linearConnected,
|
||||
linearTaskSourceContext,
|
||||
setTaskResumeState,
|
||||
taskResumeApplied,
|
||||
taskResumeState?.linearContext,
|
||||
@@ -3748,7 +3809,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
// available for new-issue creation while the replacement fetch is pending,
|
||||
// but a workspace-scoped cache can keep the selector usable immediately.
|
||||
setAvailableTeams(cachedTeams ?? [])
|
||||
void listLinearTeams(selectedLinearWorkspaceId)
|
||||
void listLinearTeams(selectedLinearWorkspaceId, { sourceContext: linearTaskSourceContext })
|
||||
.then((teams) => {
|
||||
if (!cancelled) {
|
||||
setAvailableTeams(teams)
|
||||
@@ -3770,7 +3831,8 @@ export default function TaskPage(): React.JSX.Element {
|
||||
linearTeamRefreshNonce,
|
||||
taskResumeApplied,
|
||||
getCachedLinearTeams,
|
||||
listLinearTeams
|
||||
listLinearTeams,
|
||||
linearTaskSourceContext
|
||||
])
|
||||
|
||||
const [availableJiraProjects, setAvailableJiraProjects] = useState<JiraProject[]>([])
|
||||
@@ -3788,7 +3850,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
let cancelled = false
|
||||
setAvailableJiraProjects([])
|
||||
setJiraProjectsLoading(true)
|
||||
void jiraListProjects(settings, selectedJiraSiteId)
|
||||
void jiraListProjects(jiraTaskSourceContext ?? settings, selectedJiraSiteId)
|
||||
.then((projects) => {
|
||||
if (!cancelled) {
|
||||
setAvailableJiraProjects(projects)
|
||||
@@ -3807,7 +3869,14 @@ export default function TaskPage(): React.JSX.Element {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [settings, taskSource, jiraConnected, selectedJiraSiteId, taskResumeApplied])
|
||||
}, [
|
||||
settings,
|
||||
taskSource,
|
||||
jiraConnected,
|
||||
selectedJiraSiteId,
|
||||
taskResumeApplied,
|
||||
jiraTaskSourceContext
|
||||
])
|
||||
|
||||
// Why: stable key for `selectedRepos` so the GitLab fetch effect below
|
||||
// doesn't re-run on every parent re-render just because the array
|
||||
@@ -4419,7 +4488,11 @@ export default function TaskPage(): React.JSX.Element {
|
||||
}
|
||||
|
||||
try {
|
||||
const states = await linearTeamStates(settings, issue.team.id, issue.workspaceId)
|
||||
const states = await linearTeamStates(
|
||||
linearTaskSourceContext ?? settings,
|
||||
issue.team.id,
|
||||
issue.workspaceId
|
||||
)
|
||||
const workflowState = findLinearWorkflowStateForStatus(states, targetState)
|
||||
if (!workflowState) {
|
||||
toast.error(
|
||||
@@ -4443,7 +4516,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
applyFallbackState(nextState)
|
||||
|
||||
const result = await linearUpdateIssue(
|
||||
settings,
|
||||
linearTaskSourceContext ?? settings,
|
||||
issue.id,
|
||||
{ stateId: workflowState.id },
|
||||
issue.workspaceId
|
||||
@@ -4481,6 +4554,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
linearStatusBoardEnabled,
|
||||
patchScopedLinearIssue,
|
||||
patchLinearIssue,
|
||||
linearTaskSourceContext,
|
||||
settings
|
||||
]
|
||||
)
|
||||
@@ -4580,7 +4654,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
const targetWorkspaceId =
|
||||
newLinearIssueTargetTeam.workspaceId ||
|
||||
(selectedLinearWorkspaceId !== 'all' ? selectedLinearWorkspaceId : null)
|
||||
linearListProjects(settings, undefined, 100, targetWorkspaceId)
|
||||
linearListProjects(linearTaskSourceContext ?? settings, undefined, 100, targetWorkspaceId)
|
||||
.then((p) => {
|
||||
if (!cancelled) {
|
||||
setNewLinearIssueProjects(p.items)
|
||||
@@ -4601,6 +4675,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
linearConnected,
|
||||
newLinearIssueOpen,
|
||||
newLinearIssueTargetTeam,
|
||||
linearTaskSourceContext,
|
||||
settings,
|
||||
selectedLinearWorkspaceId
|
||||
])
|
||||
@@ -4866,7 +4941,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
setAvailableJiraIssueTypes([])
|
||||
setJiraIssueTypesLoading(true)
|
||||
void jiraListIssueTypes(
|
||||
settings,
|
||||
jiraTaskSourceContext ?? settings,
|
||||
newJiraIssueTargetProject.id,
|
||||
newJiraIssueTargetProject.siteId
|
||||
)
|
||||
@@ -4892,7 +4967,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [settings, jiraConnected, newJiraIssueOpen, newJiraIssueTargetProject])
|
||||
}, [settings, jiraConnected, newJiraIssueOpen, newJiraIssueTargetProject, jiraTaskSourceContext])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -4913,7 +4988,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
setJiraCreateFieldsError(null)
|
||||
setNewJiraIssueCustomFieldValues({})
|
||||
void jiraListCreateFields(
|
||||
settings,
|
||||
jiraTaskSourceContext ?? settings,
|
||||
newJiraIssueTargetProject.id,
|
||||
newJiraIssueTargetType.id,
|
||||
newJiraIssueTargetProject.siteId
|
||||
@@ -4938,7 +5013,14 @@ export default function TaskPage(): React.JSX.Element {
|
||||
// responses after the user switches either selector.
|
||||
cancelled = true
|
||||
}
|
||||
}, [settings, jiraConnected, newJiraIssueOpen, newJiraIssueTargetProject, newJiraIssueTargetType])
|
||||
}, [
|
||||
settings,
|
||||
jiraConnected,
|
||||
newJiraIssueOpen,
|
||||
newJiraIssueTargetProject,
|
||||
newJiraIssueTargetType,
|
||||
jiraTaskSourceContext
|
||||
])
|
||||
|
||||
// Why: defense-in-depth safety net applied to the current page's items.
|
||||
// The active tab scopes requests to issues or PRs, and this keeps stale
|
||||
@@ -5766,7 +5848,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
}
|
||||
setNewLinearProjectSubmitting(true)
|
||||
try {
|
||||
const result = await linearCreateProject(settings, {
|
||||
const result = await linearCreateProject(linearTaskSourceContext ?? settings, {
|
||||
name,
|
||||
description: newLinearProjectDescription.trim() || undefined,
|
||||
content: newLinearProjectContent.trim() || undefined,
|
||||
@@ -5840,6 +5922,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
newLinearProjectTargetDate,
|
||||
newLinearProjectTargetTeam,
|
||||
openLinearProjectContext,
|
||||
linearTaskSourceContext,
|
||||
settings
|
||||
])
|
||||
|
||||
@@ -5867,7 +5950,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
setNewLinearIssueSubmitting(true)
|
||||
const submitProviderRuntimeContextKey = providerRuntimeContextKey
|
||||
try {
|
||||
const result = await linearCreateIssue(settings, {
|
||||
const result = await linearCreateIssue(linearTaskSourceContext ?? settings, {
|
||||
teamId: newLinearIssueTargetTeam.id,
|
||||
title,
|
||||
description: newLinearIssueBody || undefined,
|
||||
@@ -5914,7 +5997,11 @@ export default function TaskPage(): React.JSX.Element {
|
||||
|
||||
// Why: auto-select the new issue in the inline workspace so the user
|
||||
// sees exactly what was filed, mirroring the GitHub create-issue flow.
|
||||
void linearGetIssue(settings, result.id, newLinearIssueTargetTeam.workspaceId)
|
||||
void linearGetIssue(
|
||||
linearTaskSourceContext ?? settings,
|
||||
result.id,
|
||||
newLinearIssueTargetTeam.workspaceId
|
||||
)
|
||||
.then((full) => {
|
||||
if (submitProviderRuntimeContextKey !== providerRuntimeContextKeyRef.current) {
|
||||
return
|
||||
@@ -5942,6 +6029,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
providerRuntimeContextKey,
|
||||
selectedLinearProject,
|
||||
setSelectedLinearIssue,
|
||||
linearTaskSourceContext,
|
||||
settings
|
||||
])
|
||||
|
||||
@@ -5960,7 +6048,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
setNewJiraIssueSubmitting(true)
|
||||
const submitProviderRuntimeContextKey = providerRuntimeContextKey
|
||||
try {
|
||||
const result = await jiraCreateIssue(settings, {
|
||||
const result = await jiraCreateIssue(jiraTaskSourceContext ?? settings, {
|
||||
siteId: newJiraIssueTargetProject.siteId,
|
||||
projectId: newJiraIssueTargetProject.id,
|
||||
issueTypeId: newJiraIssueTargetType.id,
|
||||
@@ -5997,7 +6085,11 @@ export default function TaskPage(): React.JSX.Element {
|
||||
setNewJiraIssueCustomFieldValues({})
|
||||
setJiraRefreshNonce((n) => n + 1)
|
||||
|
||||
void jiraGetIssue(settings, result.key, newJiraIssueTargetProject.siteId)
|
||||
void jiraGetIssue(
|
||||
jiraTaskSourceContext ?? settings,
|
||||
result.key,
|
||||
newJiraIssueTargetProject.siteId
|
||||
)
|
||||
.then((full) => {
|
||||
if (submitProviderRuntimeContextKey !== providerRuntimeContextKeyRef.current) {
|
||||
return
|
||||
@@ -6025,6 +6117,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
newJiraIssueTargetType,
|
||||
newJiraIssueTitle,
|
||||
providerRuntimeContextKey,
|
||||
jiraTaskSourceContext,
|
||||
settings,
|
||||
setSelectedJiraIssue,
|
||||
visibleJiraCreateFields
|
||||
@@ -6172,7 +6265,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
trimmed.length > 0
|
||||
? ({ kind: 'search', query: trimmed, limit: LINEAR_ITEM_LIMIT } as const)
|
||||
: ({ kind: 'list', filter: 'all', limit: effectiveLinearIssueLimit } as const)
|
||||
const cachedResult = getCachedLinearIssues(readArgs)
|
||||
const cachedResult = getCachedLinearIssues(readArgs, { sourceContext: linearTaskSourceContext })
|
||||
if (readArgs.kind === 'search') {
|
||||
setLinearIssuesHasMore(false)
|
||||
if (cachedResult) {
|
||||
@@ -6214,10 +6307,12 @@ export default function TaskPage(): React.JSX.Element {
|
||||
const request =
|
||||
readArgs.kind === 'search'
|
||||
? searchLinearIssues(readArgs.query, LINEAR_ITEM_LIMIT, {
|
||||
force: forceRefresh || shouldProbeOnLanding
|
||||
force: forceRefresh || shouldProbeOnLanding,
|
||||
sourceContext: linearTaskSourceContext
|
||||
})
|
||||
: listLinearIssues(readArgs.filter, effectiveLinearIssueLimit, {
|
||||
force: forceRefresh || shouldProbeOnLanding
|
||||
force: forceRefresh || shouldProbeOnLanding,
|
||||
sourceContext: linearTaskSourceContext
|
||||
})
|
||||
|
||||
void request
|
||||
@@ -6279,7 +6374,8 @@ export default function TaskPage(): React.JSX.Element {
|
||||
linearIssueLimit,
|
||||
linearRefreshNonce,
|
||||
taskResumeApplied,
|
||||
getCachedLinearIssues
|
||||
getCachedLinearIssues,
|
||||
linearTaskSourceContext
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -6309,7 +6405,8 @@ export default function TaskPage(): React.JSX.Element {
|
||||
setLinearProjectsLoading(force || cached === null)
|
||||
setLinearProjectsError(null)
|
||||
void listLinearProjectsFromStore(query || undefined, LINEAR_ITEM_LIMIT, undefined, {
|
||||
force
|
||||
force,
|
||||
sourceContext: linearTaskSourceContext
|
||||
})
|
||||
.then((result) => {
|
||||
if (!cancelled) {
|
||||
@@ -6338,7 +6435,8 @@ export default function TaskPage(): React.JSX.Element {
|
||||
selectedLinearProject,
|
||||
appliedLinearProjectSearch,
|
||||
linearRefreshNonce,
|
||||
getCachedLinearProjects
|
||||
getCachedLinearProjects,
|
||||
linearTaskSourceContext
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -6350,7 +6448,8 @@ export default function TaskPage(): React.JSX.Element {
|
||||
setLinearProjectDetailLoading(true)
|
||||
setLinearProjectDetailError(null)
|
||||
void fetchLinearProject(selectedLinearProject.id, selectedLinearProject.workspaceId, {
|
||||
force: linearRefreshNonce > 0
|
||||
force: linearRefreshNonce > 0,
|
||||
sourceContext: linearTaskSourceContext
|
||||
})
|
||||
.then((project) => {
|
||||
if (!cancelled) {
|
||||
@@ -6376,7 +6475,13 @@ export default function TaskPage(): React.JSX.Element {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [fetchLinearProject, linearRefreshNonce, selectedLinearProject, setTaskResumeState])
|
||||
}, [
|
||||
fetchLinearProject,
|
||||
linearRefreshNonce,
|
||||
selectedLinearProject,
|
||||
setTaskResumeState,
|
||||
linearTaskSourceContext
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedLinearProject?.workspaceId || linearProjectTab !== 'issues') {
|
||||
@@ -6390,7 +6495,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
selectedLinearProject.id,
|
||||
selectedLinearProject.workspaceId,
|
||||
effectiveLimit,
|
||||
{ force: linearRefreshNonce > 0 }
|
||||
{ force: linearRefreshNonce > 0, sourceContext: linearTaskSourceContext }
|
||||
)
|
||||
.then((result) => {
|
||||
if (!cancelled) {
|
||||
@@ -6414,6 +6519,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
linearProjectTab,
|
||||
linearRefreshNonce,
|
||||
listLinearProjectIssues,
|
||||
linearTaskSourceContext,
|
||||
selectedLinearProject
|
||||
])
|
||||
|
||||
@@ -6441,7 +6547,10 @@ export default function TaskPage(): React.JSX.Element {
|
||||
// models avoids a second, redundant Issues/Projects switch.
|
||||
void Promise.all(
|
||||
LINEAR_CUSTOM_VIEW_MODELS.map((model) =>
|
||||
listLinearCustomViews(model, LINEAR_ITEM_LIMIT, undefined, { force })
|
||||
listLinearCustomViews(model, LINEAR_ITEM_LIMIT, undefined, {
|
||||
force,
|
||||
sourceContext: linearTaskSourceContext
|
||||
})
|
||||
)
|
||||
)
|
||||
.then((result) => {
|
||||
@@ -6471,7 +6580,8 @@ export default function TaskPage(): React.JSX.Element {
|
||||
selectedLinearCustomView,
|
||||
linearRefreshNonce,
|
||||
getCachedLinearCustomViews,
|
||||
listLinearCustomViews
|
||||
listLinearCustomViews,
|
||||
linearTaskSourceContext
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -6490,13 +6600,13 @@ export default function TaskPage(): React.JSX.Element {
|
||||
selectedLinearCustomView.id,
|
||||
selectedLinearCustomView.workspaceId,
|
||||
issueLimit,
|
||||
{ force: linearRefreshNonce > 0 }
|
||||
{ force: linearRefreshNonce > 0, sourceContext: linearTaskSourceContext }
|
||||
)
|
||||
: listLinearCustomViewProjects(
|
||||
selectedLinearCustomView.id,
|
||||
selectedLinearCustomView.workspaceId,
|
||||
LINEAR_ITEM_LIMIT,
|
||||
{ force: linearRefreshNonce > 0 }
|
||||
{ force: linearRefreshNonce > 0, sourceContext: linearTaskSourceContext }
|
||||
)
|
||||
void request
|
||||
.then((result) => {
|
||||
@@ -6526,6 +6636,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
linearCustomViewIssueLimit,
|
||||
listLinearCustomViewIssues,
|
||||
listLinearCustomViewProjects,
|
||||
linearTaskSourceContext,
|
||||
selectedLinearCustomView
|
||||
])
|
||||
|
||||
@@ -6606,8 +6717,10 @@ export default function TaskPage(): React.JSX.Element {
|
||||
const trimmed = appliedJiraSearch.trim()
|
||||
const request =
|
||||
trimmed.length > 0
|
||||
? searchJiraIssues(trimmed, JIRA_ITEM_LIMIT)
|
||||
: listJiraIssues(activeJiraPreset, JIRA_ITEM_LIMIT)
|
||||
? searchJiraIssues(trimmed, JIRA_ITEM_LIMIT, { sourceContext: jiraTaskSourceContext })
|
||||
: listJiraIssues(activeJiraPreset, JIRA_ITEM_LIMIT, {
|
||||
sourceContext: jiraTaskSourceContext
|
||||
})
|
||||
|
||||
void request
|
||||
.then((issues) => {
|
||||
@@ -6636,7 +6749,8 @@ export default function TaskPage(): React.JSX.Element {
|
||||
appliedJiraSearch,
|
||||
activeJiraPreset,
|
||||
jiraRefreshNonce,
|
||||
taskResumeApplied
|
||||
taskResumeApplied,
|
||||
jiraTaskSourceContext
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -6677,11 +6791,12 @@ export default function TaskPage(): React.JSX.Element {
|
||||
const linkedWorkItem = buildLinearIssueLinkedWorkItem(issue)
|
||||
openModal('new-workspace-composer', {
|
||||
linkedWorkItem,
|
||||
taskSourceContext: linearTaskSourceContext,
|
||||
prefilledName: getLinearIssueWorkspaceName(issue),
|
||||
telemetrySource: 'sidebar'
|
||||
})
|
||||
},
|
||||
[openModal]
|
||||
[linearTaskSourceContext, openModal]
|
||||
)
|
||||
|
||||
const handleUseLinearItem = useCallback(
|
||||
@@ -6775,11 +6890,12 @@ export default function TaskPage(): React.JSX.Element {
|
||||
}
|
||||
openModal('new-workspace-composer', {
|
||||
linkedWorkItem,
|
||||
taskSourceContext: jiraTaskSourceContext,
|
||||
prefilledName: getJiraIssueWorkspaceSeed(issue),
|
||||
telemetrySource: 'sidebar'
|
||||
})
|
||||
},
|
||||
[openModal]
|
||||
[jiraTaskSourceContext, openModal]
|
||||
)
|
||||
|
||||
const handleUseJiraItem = useCallback(
|
||||
@@ -8935,6 +9051,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
issue={selectedJiraIssue}
|
||||
onUse={handleUseJiraItem}
|
||||
onClose={() => setSelectedJiraIssue(null)}
|
||||
sourceContext={jiraTaskSourceContext}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -8946,6 +9063,7 @@ export default function TaskPage(): React.JSX.Element {
|
||||
onUse={handleUseLinearItem}
|
||||
onOpenIssue={openRelatedLinearIssue}
|
||||
onClose={closeTaskDetailPage}
|
||||
sourceContext={linearTaskSourceContext}
|
||||
/>
|
||||
) : !linearStatusReady ? (
|
||||
<div className="mt-4 flex items-center justify-center py-14">
|
||||
@@ -9546,7 +9664,11 @@ export default function TaskPage(): React.JSX.Element {
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground">
|
||||
{effectiveLinearDisplayProperties.has('state') ? (
|
||||
<LinearStateCell issue={issue} className="px-1.5 py-0.5" />
|
||||
<LinearStateCell
|
||||
issue={issue}
|
||||
className="px-1.5 py-0.5"
|
||||
sourceContext={linearTaskSourceContext}
|
||||
/>
|
||||
) : null}
|
||||
{effectiveLinearDisplayProperties.has('assignee') ? (
|
||||
<span>
|
||||
@@ -9661,7 +9783,11 @@ export default function TaskPage(): React.JSX.Element {
|
||||
</div>
|
||||
<div className="mt-1 flex min-w-0 items-center gap-1.5 lg:!hidden">
|
||||
{effectiveLinearDisplayProperties.has('state') ? (
|
||||
<LinearStateCell issue={issue} className="px-1.5 py-0.5" />
|
||||
<LinearStateCell
|
||||
issue={issue}
|
||||
className="px-1.5 py-0.5"
|
||||
sourceContext={linearTaskSourceContext}
|
||||
/>
|
||||
) : null}
|
||||
{effectiveLinearDisplayProperties.has('assignee') ? (
|
||||
<span className="min-w-0 truncate text-[11px] text-muted-foreground">
|
||||
@@ -9703,7 +9829,11 @@ export default function TaskPage(): React.JSX.Element {
|
||||
|
||||
{effectiveLinearDisplayProperties.has('state') ? (
|
||||
<div className="flex min-w-0 max-lg:!hidden">
|
||||
<LinearStateCell issue={issue} className="max-w-full px-2 py-0.5" />
|
||||
<LinearStateCell
|
||||
issue={issue}
|
||||
className="max-w-full px-2 py-0.5"
|
||||
sourceContext={linearTaskSourceContext}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -6,15 +6,16 @@ import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-cl
|
||||
import {
|
||||
linearTeamLabels,
|
||||
linearTeamMembers,
|
||||
linearTeamStates
|
||||
linearTeamStates,
|
||||
type RuntimeLinearSettings
|
||||
} from '@/runtime/runtime-linear-client'
|
||||
import type {
|
||||
GitHubAssignableUser,
|
||||
GlobalSettings,
|
||||
LinearWorkflowState,
|
||||
LinearLabel,
|
||||
LinearMember
|
||||
} from '../../../shared/types'
|
||||
import { getTaskSourceRuntimeSettings } from '../../../shared/task-source-context'
|
||||
import {
|
||||
clearMetadataRequestStore,
|
||||
createMetadataRequestStore,
|
||||
@@ -193,10 +194,12 @@ const linearMemberStore = createMetadataRequestStore<LinearMember[]>()
|
||||
|
||||
function linearMetadataCacheKey(
|
||||
teamId: string,
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
settings: RuntimeLinearSettings,
|
||||
workspaceId?: string | null
|
||||
): string {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const runtimeSettings =
|
||||
settings && 'kind' in settings ? getTaskSourceRuntimeSettings(settings) : settings
|
||||
const target = getActiveRuntimeTarget(runtimeSettings)
|
||||
const workspaceKey = workspaceId ?? 'selected'
|
||||
return target.kind === 'environment'
|
||||
? `runtime:${target.environmentId}:${workspaceKey}:${teamId}`
|
||||
@@ -216,7 +219,7 @@ export function clearGitHubMetadataCache(): void {
|
||||
|
||||
export function useTeamStates(
|
||||
teamId: string | null,
|
||||
settings?: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null,
|
||||
settings?: RuntimeLinearSettings,
|
||||
workspaceId?: string | null
|
||||
): MetadataState<LinearWorkflowState[]> {
|
||||
const [state, setState] = useState<MetadataState<LinearWorkflowState[]>>({
|
||||
@@ -278,7 +281,7 @@ export function useTeamStates(
|
||||
|
||||
export function useTeamLabels(
|
||||
teamId: string | null,
|
||||
settings?: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null,
|
||||
settings?: RuntimeLinearSettings,
|
||||
workspaceId?: string | null
|
||||
): MetadataState<LinearLabel[]> {
|
||||
const [state, setState] = useState<MetadataState<LinearLabel[]>>({
|
||||
@@ -338,7 +341,7 @@ export function useTeamLabels(
|
||||
|
||||
export function useTeamMembers(
|
||||
teamId: string | null,
|
||||
settings?: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null,
|
||||
settings?: RuntimeLinearSettings,
|
||||
workspaceId?: string | null
|
||||
): MetadataState<LinearMember[]> {
|
||||
const [state, setState] = useState<MetadataState<LinearMember[]>>({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { create } from 'zustand'
|
||||
import type { AppState } from '../types'
|
||||
import type { JiraConnectionStatus, JiraIssue, JiraViewer } from '../../../../shared/types'
|
||||
import type { TaskSourceContext } from '../../../../shared/task-source-context'
|
||||
import { credentialDecryptionMessage } from '../../../../shared/integration-credential-errors'
|
||||
import { createJiraSlice } from './jira'
|
||||
|
||||
@@ -72,6 +73,19 @@ function issue(key: string): JiraIssue {
|
||||
}
|
||||
}
|
||||
|
||||
function jiraSourceContext(environmentId: string, siteId = 'site-1'): TaskSourceContext {
|
||||
return {
|
||||
kind: 'task-source',
|
||||
provider: 'jira',
|
||||
projectId: 'logical-project',
|
||||
hostId: `runtime:${environmentId}`,
|
||||
providerIdentity: {
|
||||
provider: 'jira',
|
||||
siteId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('createJiraSlice runtime context', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -117,6 +131,25 @@ describe('createJiraSlice runtime context', () => {
|
||||
expect(store.getState().jiraIssueCache['selected::ORC-1']?.data?.title).toBe('Remote issue')
|
||||
})
|
||||
|
||||
it('routes explicit source reads through their source context when focused runtime changes', async () => {
|
||||
const store = createTestStore()
|
||||
store.setState({
|
||||
jiraStatus: { connected: true, viewer: null, selectedSiteId: 'site-1' }
|
||||
})
|
||||
const sourceContext = jiraSourceContext('source-runtime')
|
||||
const sourceResult = deferred<JiraIssue[]>()
|
||||
jiraListIssues.mockReturnValueOnce(sourceResult.promise)
|
||||
|
||||
const request = store.getState().listJiraIssues('assigned', 30, { sourceContext })
|
||||
store.setState({ settings: { activeRuntimeEnvironmentId: 'focused-runtime' } as never })
|
||||
|
||||
sourceResult.resolve([{ ...issue('ALP-1'), title: 'Source issue' }])
|
||||
await expect(request).resolves.toMatchObject([{ key: 'ALP-1', title: 'Source issue' }])
|
||||
expect(jiraListIssues).toHaveBeenCalledWith(sourceContext, 'assigned', 30, 'site-1')
|
||||
expect(Object.values(store.getState().jiraSearchCache)).toHaveLength(1)
|
||||
expect(store.getState().jiraSearchCache['site-1::list::assigned::30']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns a failed Jira connect result when the active runtime changes before completion', async () => {
|
||||
const store = createTestStore()
|
||||
const connectResult = deferred<{ ok: true; viewer: JiraViewer }>()
|
||||
|
||||
@@ -24,6 +24,11 @@ import {
|
||||
} from '@/runtime/runtime-jira-client'
|
||||
import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
getTaskSourceCacheScope,
|
||||
getTaskSourceRuntimeSettings,
|
||||
type TaskSourceContext
|
||||
} from '../../../../shared/task-source-context'
|
||||
|
||||
const CACHE_TTL = 60_000
|
||||
const MAX_CACHE_ENTRIES = 500
|
||||
@@ -59,6 +64,15 @@ type InflightJiraReadRequest<T> = {
|
||||
mutationGeneration: number
|
||||
}
|
||||
|
||||
type JiraReadOptions = { sourceContext?: TaskSourceContext | null }
|
||||
|
||||
type JiraReadScope = {
|
||||
settings: AppState['settings'] | TaskSourceContext | null
|
||||
contextKey: string
|
||||
cachePrefix: string | null
|
||||
explicitSource: boolean
|
||||
}
|
||||
|
||||
const inflightIssueRequests = new Map<string, InflightJiraReadRequest<JiraIssue | null>>()
|
||||
const inflightSearchRequests = new Map<string, InflightJiraReadRequest<JiraIssue[]>>()
|
||||
const inflightListRequests = new Map<string, InflightJiraReadRequest<JiraIssue[]>>()
|
||||
@@ -100,14 +114,40 @@ function isCurrentJiraRuntimeContext(contextKey: string, settings: AppState['set
|
||||
function canWriteJiraReadResult(
|
||||
contextKey: string,
|
||||
mutationGeneration: number,
|
||||
settings: AppState['settings']
|
||||
settings: AppState['settings'],
|
||||
explicitSource = false
|
||||
): boolean {
|
||||
return (
|
||||
mutationGeneration === jiraMutationGeneration &&
|
||||
isCurrentJiraRuntimeContext(contextKey, settings)
|
||||
(explicitSource || isCurrentJiraRuntimeContext(contextKey, settings))
|
||||
)
|
||||
}
|
||||
|
||||
function getJiraReadScope(
|
||||
settings: AppState['settings'],
|
||||
sourceContext?: TaskSourceContext | null
|
||||
): JiraReadScope {
|
||||
if (!sourceContext) {
|
||||
return {
|
||||
settings,
|
||||
contextKey: getProviderRuntimeContextKey(settings),
|
||||
cachePrefix: null,
|
||||
explicitSource: false
|
||||
}
|
||||
}
|
||||
const runtimeSettings = getTaskSourceRuntimeSettings(sourceContext)
|
||||
return {
|
||||
settings: sourceContext,
|
||||
contextKey: `${getProviderRuntimeContextKey(runtimeSettings)}::${getTaskSourceCacheScope(sourceContext)}`,
|
||||
cachePrefix: getTaskSourceCacheScope(sourceContext),
|
||||
explicitSource: true
|
||||
}
|
||||
}
|
||||
|
||||
function scopedJiraCacheKey(scope: JiraReadScope, key: string): string {
|
||||
return scope.cachePrefix ? `${scope.cachePrefix}::${key}` : key
|
||||
}
|
||||
|
||||
export type JiraSlice = {
|
||||
jiraStatus: JiraConnectionStatus
|
||||
jiraStatusChecked: boolean
|
||||
@@ -126,9 +166,17 @@ export type JiraSlice = {
|
||||
) => Promise<{ ok: true; viewer: JiraViewer } | { ok: false; error: string }>
|
||||
selectJiraSite: (siteId: JiraSiteSelection) => Promise<void>
|
||||
disconnectJira: (siteId?: string | null) => Promise<void>
|
||||
fetchJiraIssue: (key: string, siteId?: string | null) => Promise<JiraIssue | null>
|
||||
searchJiraIssues: (jql: string, limit?: number) => Promise<JiraIssue[]>
|
||||
listJiraIssues: (filter?: JiraIssueFilter, limit?: number) => Promise<JiraIssue[]>
|
||||
fetchJiraIssue: (
|
||||
key: string,
|
||||
siteId?: string | null,
|
||||
options?: JiraReadOptions
|
||||
) => Promise<JiraIssue | null>
|
||||
searchJiraIssues: (jql: string, limit?: number, options?: JiraReadOptions) => Promise<JiraIssue[]>
|
||||
listJiraIssues: (
|
||||
filter?: JiraIssueFilter,
|
||||
limit?: number,
|
||||
options?: JiraReadOptions
|
||||
) => Promise<JiraIssue[]>
|
||||
patchJiraIssue: (issueKey: string, patch: Partial<JiraIssue>) => void
|
||||
}
|
||||
|
||||
@@ -295,9 +343,10 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
})
|
||||
},
|
||||
|
||||
fetchJiraIssue: async (key, siteId) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const issueCacheKey = `${siteId ?? 'selected'}::${key}`
|
||||
fetchJiraIssue: async (key, siteId, options) => {
|
||||
const scope = getJiraReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const issueCacheKey = scopedJiraCacheKey(scope, `${siteId ?? 'selected'}::${key}`)
|
||||
const cached = get().jiraIssueCache[issueCacheKey] ?? get().jiraIssueCache[key]
|
||||
if (isFresh(cached)) {
|
||||
return cached.data
|
||||
@@ -312,11 +361,16 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
}
|
||||
let entry: InflightJiraReadRequest<JiraIssue | null>
|
||||
const requestMutationGeneration = jiraMutationGeneration
|
||||
const promise = jiraGetIssue(get().settings, key, siteId)
|
||||
const promise = jiraGetIssue(scope.settings, key, siteId)
|
||||
.then((issue) => {
|
||||
if (
|
||||
inflightIssueRequests.get(issueCacheKey) === entry &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
jiraIssueCache: evictStaleEntries({
|
||||
@@ -331,14 +385,24 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
console.warn('[jira] fetchJiraIssue failed:', error)
|
||||
if (
|
||||
isIntegrationCredentialDecryptionError(error) &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
if (!shouldRefreshStatusAfterRead(siteId, get().jiraStatus)) {
|
||||
void get().checkJiraConnection()
|
||||
}
|
||||
} else if (
|
||||
looksLikeAuthError(error) &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set({ jiraStatus: { connected: false, viewer: null } })
|
||||
}
|
||||
@@ -350,7 +414,12 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
}
|
||||
if (
|
||||
shouldRefreshStatusAfterRead(siteId, get().jiraStatus) &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkJiraConnection()
|
||||
}
|
||||
@@ -360,10 +429,11 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
return promise
|
||||
},
|
||||
|
||||
searchJiraIssues: async (jql, limit = 30) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
searchJiraIssues: async (jql, limit = 30, options) => {
|
||||
const scope = getJiraReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const siteId = getSelectedSiteId(get().jiraStatus)
|
||||
const cacheKey = `${siteId ?? 'default'}::${jql}::${limit}`
|
||||
const cacheKey = scopedJiraCacheKey(scope, `${siteId ?? 'default'}::${jql}::${limit}`)
|
||||
const cached = get().jiraSearchCache[cacheKey]
|
||||
if (isFresh(cached)) {
|
||||
return cached.data ?? []
|
||||
@@ -378,11 +448,16 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
}
|
||||
let entry: InflightJiraReadRequest<JiraIssue[]>
|
||||
const requestMutationGeneration = jiraMutationGeneration
|
||||
const promise = jiraSearchIssues(get().settings, jql, limit, siteId)
|
||||
const promise = jiraSearchIssues(scope.settings, jql, limit, siteId)
|
||||
.then((issues) => {
|
||||
if (
|
||||
inflightSearchRequests.get(cacheKey) === entry &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
jiraSearchCache: evictStaleEntries({
|
||||
@@ -397,14 +472,24 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
console.warn('[jira] searchJiraIssues failed:', error)
|
||||
if (
|
||||
isIntegrationCredentialDecryptionError(error) &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
if (!shouldRefreshStatusAfterRead(siteId, get().jiraStatus)) {
|
||||
void get().checkJiraConnection()
|
||||
}
|
||||
} else if (
|
||||
looksLikeAuthError(error) &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set({ jiraStatus: { connected: false, viewer: null } })
|
||||
}
|
||||
@@ -416,7 +501,12 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
}
|
||||
if (
|
||||
shouldRefreshStatusAfterRead(siteId, get().jiraStatus) &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkJiraConnection()
|
||||
}
|
||||
@@ -426,10 +516,11 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
return promise
|
||||
},
|
||||
|
||||
listJiraIssues: async (filter = 'assigned', limit = 30) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
listJiraIssues: async (filter = 'assigned', limit = 30, options) => {
|
||||
const scope = getJiraReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const siteId = getSelectedSiteId(get().jiraStatus)
|
||||
const cacheKey = `${siteId ?? 'default'}::list::${filter}::${limit}`
|
||||
const cacheKey = scopedJiraCacheKey(scope, `${siteId ?? 'default'}::list::${filter}::${limit}`)
|
||||
const cached = get().jiraSearchCache[cacheKey]
|
||||
if (isFresh(cached)) {
|
||||
return cached.data ?? []
|
||||
@@ -444,11 +535,16 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
}
|
||||
let entry: InflightJiraReadRequest<JiraIssue[]>
|
||||
const requestMutationGeneration = jiraMutationGeneration
|
||||
const promise = jiraListIssues(get().settings, filter, limit, siteId)
|
||||
const promise = jiraListIssues(scope.settings, filter, limit, siteId)
|
||||
.then((issues) => {
|
||||
if (
|
||||
inflightListRequests.get(cacheKey) === entry &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
jiraSearchCache: evictStaleEntries({
|
||||
@@ -463,14 +559,24 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
console.warn('[jira] listJiraIssues failed:', error)
|
||||
if (
|
||||
isIntegrationCredentialDecryptionError(error) &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
if (!shouldRefreshStatusAfterRead(siteId, get().jiraStatus)) {
|
||||
void get().checkJiraConnection()
|
||||
}
|
||||
} else if (
|
||||
looksLikeAuthError(error) &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set({ jiraStatus: { connected: false, viewer: null } })
|
||||
}
|
||||
@@ -482,7 +588,12 @@ export const createJiraSlice: StateCreator<AppState, [], [], JiraSlice> = (set,
|
||||
}
|
||||
if (
|
||||
shouldRefreshStatusAfterRead(siteId, get().jiraStatus) &&
|
||||
canWriteJiraReadResult(contextKey, requestMutationGeneration, get().settings)
|
||||
canWriteJiraReadResult(
|
||||
contextKey,
|
||||
requestMutationGeneration,
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkJiraConnection()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
LinearTeam,
|
||||
LinearViewer
|
||||
} from '../../../../shared/types'
|
||||
import type { TaskSourceContext } from '../../../../shared/task-source-context'
|
||||
import { credentialDecryptionMessage } from '../../../../shared/integration-credential-errors'
|
||||
import { createLinearSlice } from './linear'
|
||||
|
||||
@@ -88,6 +89,22 @@ function project(id: string): LinearProjectSummary {
|
||||
return { id, name: id, workspaceId: 'workspace-1', workspaceName: 'Workspace' }
|
||||
}
|
||||
|
||||
function linearSourceContext(
|
||||
environmentId: string,
|
||||
workspaceId = 'workspace-1'
|
||||
): TaskSourceContext {
|
||||
return {
|
||||
kind: 'task-source',
|
||||
provider: 'linear',
|
||||
projectId: 'logical-project',
|
||||
hostId: `runtime:${environmentId}`,
|
||||
providerIdentity: {
|
||||
provider: 'linear',
|
||||
workspaceId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((res) => {
|
||||
@@ -752,6 +769,31 @@ describe('createLinearSlice caching', () => {
|
||||
expect(store.getState().getCachedLinearTeams('workspace-1')).toMatchObject([{ id: 'team-1' }])
|
||||
})
|
||||
|
||||
it('routes explicit source reads through their source context when focused runtime changes', async () => {
|
||||
const store = createTestStore()
|
||||
store.setState({
|
||||
linearStatus: { connected: true, viewer: null, selectedWorkspaceId: 'workspace-1' }
|
||||
})
|
||||
const sourceContext = linearSourceContext('source-runtime')
|
||||
const sourceResult = deferred<LinearCollectionResult<LinearIssue>>()
|
||||
linearListIssues.mockReturnValueOnce(sourceResult.promise)
|
||||
|
||||
const request = store.getState().listLinearIssues('all', 36, { sourceContext })
|
||||
store.setState({ settings: { activeRuntimeEnvironmentId: 'focused-runtime' } as never })
|
||||
|
||||
sourceResult.resolve({ items: [issue('LIN-SOURCE')] })
|
||||
await expect(request).resolves.toMatchObject({ items: [{ id: 'LIN-SOURCE' }] })
|
||||
expect(linearListIssues).toHaveBeenCalledWith(sourceContext, 'all', 36, 'workspace-1')
|
||||
expect(
|
||||
store
|
||||
.getState()
|
||||
.getCachedLinearIssues({ kind: 'list', filter: 'all', limit: 36 }, { sourceContext })
|
||||
).toMatchObject({ items: [{ id: 'LIN-SOURCE' }] })
|
||||
expect(
|
||||
store.getState().getCachedLinearIssues({ kind: 'list', filter: 'all', limit: 36 })
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('patches issue-cache entries keyed by workspace-qualified ids', () => {
|
||||
const store = createTestStore()
|
||||
store.setState({
|
||||
|
||||
@@ -42,6 +42,11 @@ import {
|
||||
} from '@/runtime/runtime-linear-client'
|
||||
import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
getTaskSourceCacheScope,
|
||||
getTaskSourceRuntimeSettings,
|
||||
type TaskSourceContext
|
||||
} from '../../../../shared/task-source-context'
|
||||
|
||||
const CACHE_TTL = 60_000 // 60s — same as GitHub work-items revalidation TTL
|
||||
const TEAM_CACHE_TTL = 10 * 60_000 // Teams change rarely and block visible Linear rows.
|
||||
@@ -353,7 +358,14 @@ type LinearIssueReadArgs =
|
||||
| { kind: 'search'; query: string; limit?: number }
|
||||
| { kind: 'list'; filter?: 'assigned' | 'created' | 'all' | 'completed'; limit?: number }
|
||||
|
||||
type LinearFetchOptions = { force?: boolean }
|
||||
type LinearFetchOptions = { force?: boolean; sourceContext?: TaskSourceContext | null }
|
||||
|
||||
type LinearReadScope = {
|
||||
settings: AppState['settings'] | TaskSourceContext | null
|
||||
contextKey: string
|
||||
cachePrefix: string | null
|
||||
explicitSource: boolean
|
||||
}
|
||||
|
||||
function beginLinearMutation(): number {
|
||||
linearMutationGeneration += 1
|
||||
@@ -376,15 +388,41 @@ function canWriteLinearReadResult(
|
||||
contextKey: string,
|
||||
generation: number,
|
||||
mutationGeneration: number,
|
||||
settings: AppState['settings']
|
||||
settings: AppState['settings'],
|
||||
explicitSource = false
|
||||
): boolean {
|
||||
return (
|
||||
generation === linearCacheGeneration &&
|
||||
mutationGeneration === linearMutationGeneration &&
|
||||
isCurrentLinearRuntimeContext(contextKey, settings)
|
||||
(explicitSource || isCurrentLinearRuntimeContext(contextKey, settings))
|
||||
)
|
||||
}
|
||||
|
||||
function getLinearReadScope(
|
||||
settings: AppState['settings'],
|
||||
sourceContext?: TaskSourceContext | null
|
||||
): LinearReadScope {
|
||||
if (!sourceContext) {
|
||||
return {
|
||||
settings,
|
||||
contextKey: getProviderRuntimeContextKey(settings),
|
||||
cachePrefix: null,
|
||||
explicitSource: false
|
||||
}
|
||||
}
|
||||
const runtimeSettings = getTaskSourceRuntimeSettings(sourceContext)
|
||||
return {
|
||||
settings: sourceContext,
|
||||
contextKey: `${getProviderRuntimeContextKey(runtimeSettings)}::${getTaskSourceCacheScope(sourceContext)}`,
|
||||
cachePrefix: getTaskSourceCacheScope(sourceContext),
|
||||
explicitSource: true
|
||||
}
|
||||
}
|
||||
|
||||
function scopedLinearCacheKey(scope: LinearReadScope, key: string): string {
|
||||
return scope.cachePrefix ? `${scope.cachePrefix}::${key}` : key
|
||||
}
|
||||
|
||||
export type LinearSlice = {
|
||||
linearStatus: LinearConnectionStatus
|
||||
linearStatusChecked: boolean
|
||||
@@ -414,12 +452,21 @@ export type LinearSlice = {
|
||||
selectLinearWorkspace: (workspaceId: LinearWorkspaceSelection) => Promise<void>
|
||||
disconnectLinear: () => Promise<void>
|
||||
disconnectLinearWorkspace: (workspaceId: string) => Promise<void>
|
||||
fetchLinearIssue: (id: string, workspaceId?: string | null) => Promise<LinearIssue | null>
|
||||
refreshLinearIssue: (id: string, workspaceId?: string | null) => Promise<LinearIssue | null>
|
||||
fetchLinearIssue: (
|
||||
id: string,
|
||||
workspaceId?: string | null,
|
||||
options?: LinearFetchOptions
|
||||
) => Promise<LinearIssue | null>
|
||||
refreshLinearIssue: (
|
||||
id: string,
|
||||
workspaceId?: string | null,
|
||||
options?: LinearFetchOptions
|
||||
) => Promise<LinearIssue | null>
|
||||
getCachedLinearIssues: (
|
||||
args: LinearIssueReadArgs
|
||||
args: LinearIssueReadArgs,
|
||||
options?: Pick<LinearFetchOptions, 'sourceContext'>
|
||||
) => LinearIssue[] | LinearCollectionResult<LinearIssue> | null
|
||||
prefetchLinearIssues: (args: LinearIssueReadArgs) => void
|
||||
prefetchLinearIssues: (args: LinearIssueReadArgs, options?: LinearFetchOptions) => void
|
||||
searchLinearIssues: (
|
||||
query: string,
|
||||
limit?: number,
|
||||
@@ -802,9 +849,14 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
})
|
||||
},
|
||||
|
||||
fetchLinearIssue: async (id: string, workspaceId?: string | null) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const issueCacheKey = `${workspaceId ?? 'selected'}::${id}`
|
||||
fetchLinearIssue: async (
|
||||
id: string,
|
||||
workspaceId?: string | null,
|
||||
options?: LinearFetchOptions
|
||||
) => {
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const issueCacheKey = scopedLinearCacheKey(scope, `${workspaceId ?? 'selected'}::${id}`)
|
||||
const cached = get().linearIssueCache[issueCacheKey] ?? get().linearIssueCache[id]
|
||||
if (isFresh(cached)) {
|
||||
return cached.data
|
||||
@@ -822,7 +874,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
let entry: InflightLinearIssueRequest
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise = linearGetIssue(get().settings, id, workspaceId)
|
||||
const promise = linearGetIssue(scope.settings, id, workspaceId)
|
||||
.then((issue) => {
|
||||
const data = issue as LinearIssue | null
|
||||
if (
|
||||
@@ -831,7 +883,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -851,7 +904,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -868,7 +922,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -885,8 +940,13 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
return promise
|
||||
},
|
||||
|
||||
refreshLinearIssue: async (id: string, workspaceId?: string | null) => {
|
||||
const issueCacheKey = `${workspaceId ?? 'selected'}::${id}`
|
||||
refreshLinearIssue: async (
|
||||
id: string,
|
||||
workspaceId?: string | null,
|
||||
options?: LinearFetchOptions
|
||||
) => {
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const issueCacheKey = scopedLinearCacheKey(scope, `${workspaceId ?? 'selected'}::${id}`)
|
||||
inflightIssueRequests.delete(issueCacheKey)
|
||||
clearLinearIssueCollectionRequestMaps()
|
||||
set((s) => {
|
||||
@@ -909,26 +969,37 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
linearCustomViewIssueCache: {}
|
||||
}
|
||||
})
|
||||
return get().fetchLinearIssue(id, workspaceId)
|
||||
return get().fetchLinearIssue(id, workspaceId, options)
|
||||
},
|
||||
|
||||
getCachedLinearIssues: (args) => {
|
||||
getCachedLinearIssues: (args, options) => {
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const workspaceId = getSelectedWorkspaceId(get().linearStatus)
|
||||
if (args.kind === 'search') {
|
||||
const cacheKey = linearSearchCacheKey(workspaceId, args.query, args.limit ?? 20)
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearSearchCacheKey(workspaceId, args.query, args.limit ?? 20)
|
||||
)
|
||||
return get().linearSearchCache[cacheKey]?.data ?? null
|
||||
}
|
||||
const limit = clampLinearIssueListLimit(args.limit)
|
||||
const cacheKey = linearListCacheKey(workspaceId, args.filter ?? 'assigned', limit)
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearListCacheKey(workspaceId, args.filter ?? 'assigned', limit)
|
||||
)
|
||||
return get().linearListCache[cacheKey]?.data ?? null
|
||||
},
|
||||
|
||||
prefetchLinearIssues: (args) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
prefetchLinearIssues: (args, options) => {
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const workspaceId = getSelectedWorkspaceId(get().linearStatus)
|
||||
if (args.kind === 'search') {
|
||||
const limit = args.limit ?? 20
|
||||
const cacheKey = linearSearchCacheKey(workspaceId, args.query, limit)
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearSearchCacheKey(workspaceId, args.query, limit)
|
||||
)
|
||||
const inflight = inflightSearchRequests.get(cacheKey)
|
||||
if (
|
||||
isFresh(get().linearSearchCache[cacheKey]) ||
|
||||
@@ -939,12 +1010,15 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
return
|
||||
}
|
||||
void get()
|
||||
.searchLinearIssues(args.query, limit)
|
||||
.searchLinearIssues(args.query, limit, options)
|
||||
.catch(() => {})
|
||||
return
|
||||
}
|
||||
const limit = clampLinearIssueListLimit(args.limit)
|
||||
const cacheKey = linearListCacheKey(workspaceId, args.filter ?? 'assigned', limit)
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearListCacheKey(workspaceId, args.filter ?? 'assigned', limit)
|
||||
)
|
||||
const inflight = inflightListRequests.get(cacheKey)
|
||||
if (
|
||||
isFresh(get().linearListCache[cacheKey]) ||
|
||||
@@ -955,14 +1029,15 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
return
|
||||
}
|
||||
void get()
|
||||
.listLinearIssues(args.filter, limit)
|
||||
.listLinearIssues(args.filter, limit, options)
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
searchLinearIssues: async (query: string, limit = 20, options) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const workspaceId = getSelectedWorkspaceId(get().linearStatus)
|
||||
const cacheKey = linearSearchCacheKey(workspaceId, query, limit)
|
||||
const cacheKey = scopedLinearCacheKey(scope, linearSearchCacheKey(workspaceId, query, limit))
|
||||
const cached = get().linearSearchCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached)) {
|
||||
return cached.data ?? []
|
||||
@@ -981,7 +1056,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
let entry: InflightLinearListRequest
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise = linearSearchIssues(get().settings, query, limit, workspaceId)
|
||||
const promise = linearSearchIssues(scope.settings, query, limit, workspaceId)
|
||||
.then((issues) => {
|
||||
const data = issues as LinearIssue[]
|
||||
if (
|
||||
@@ -990,7 +1065,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -1010,7 +1086,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
if (!shouldRefreshStatusAfterRead(workspaceId, get().linearStatus)) {
|
||||
@@ -1030,7 +1107,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1049,10 +1127,14 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
},
|
||||
|
||||
listLinearIssues: async (filter = 'assigned', limit = 20, options) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const workspaceId = getSelectedWorkspaceId(get().linearStatus)
|
||||
const effectiveLimit = clampLinearIssueListLimit(limit)
|
||||
const cacheKey = linearListCacheKey(workspaceId, filter, effectiveLimit)
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearListCacheKey(workspaceId, filter, effectiveLimit)
|
||||
)
|
||||
const cached = get().linearListCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached)) {
|
||||
return cached.data ?? emptyLinearCollection<LinearIssue>()
|
||||
@@ -1072,7 +1154,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise: Promise<LinearCollectionResult<LinearIssue>> = linearListIssues(
|
||||
get().settings,
|
||||
scope.settings,
|
||||
filter,
|
||||
effectiveLimit,
|
||||
workspaceId
|
||||
@@ -1085,7 +1167,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -1105,7 +1188,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
if (!shouldRefreshStatusAfterRead(workspaceId, get().linearStatus)) {
|
||||
@@ -1125,7 +1209,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1149,9 +1234,10 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
},
|
||||
|
||||
listLinearTeams: async (workspaceId, options) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const resolvedWorkspaceId = workspaceId ?? getSelectedWorkspaceId(get().linearStatus)
|
||||
const cacheKey = linearTeamsCacheKey(resolvedWorkspaceId)
|
||||
const cacheKey = scopedLinearCacheKey(scope, linearTeamsCacheKey(resolvedWorkspaceId))
|
||||
const cached = get().linearTeamCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached, TEAM_CACHE_TTL)) {
|
||||
return cached.data ?? []
|
||||
@@ -1170,7 +1256,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
let entry: InflightLinearTeamRequest
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise = linearListTeams(get().settings, resolvedWorkspaceId)
|
||||
const promise = linearListTeams(scope.settings, resolvedWorkspaceId)
|
||||
.then((teams) => {
|
||||
const data = teams as LinearTeam[]
|
||||
if (
|
||||
@@ -1179,7 +1265,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -1199,7 +1286,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
if (!shouldRefreshStatusAfterRead(resolvedWorkspaceId, get().linearStatus)) {
|
||||
@@ -1219,7 +1307,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1244,10 +1333,14 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
},
|
||||
|
||||
listLinearProjects: async (query, limit = 20, workspaceId, options) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const resolvedWorkspaceId = workspaceId ?? getSelectedWorkspaceId(get().linearStatus)
|
||||
const trimmed = query?.trim() || undefined
|
||||
const cacheKey = linearCollectionCacheKey(resolvedWorkspaceId, 'projects', trimmed, limit)
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearCollectionCacheKey(resolvedWorkspaceId, 'projects', trimmed, limit)
|
||||
)
|
||||
const cached = get().linearProjectCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached)) {
|
||||
return cached.data ?? emptyLinearCollection<LinearProjectSummary>()
|
||||
@@ -1266,7 +1359,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
let entry: InflightLinearCollectionRequest<LinearProjectSummary>
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise = linearListProjects(get().settings, trimmed, limit, resolvedWorkspaceId, {
|
||||
const promise = linearListProjects(scope.settings, trimmed, limit, resolvedWorkspaceId, {
|
||||
force: options?.force
|
||||
})
|
||||
.then((result) => {
|
||||
@@ -1276,7 +1369,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -1296,7 +1390,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1315,7 +1410,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1334,8 +1430,12 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
},
|
||||
|
||||
fetchLinearProject: async (id, workspaceId, options) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const cacheKey = linearCollectionCacheKey(workspaceId, 'project-detail', id)
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearCollectionCacheKey(workspaceId, 'project-detail', id)
|
||||
)
|
||||
const cached = get().linearProjectDetailCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached)) {
|
||||
return cached.data
|
||||
@@ -1354,7 +1454,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
let entry: InflightLinearDetailRequest<LinearProjectDetail | null>
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise = linearGetProject(get().settings, id, workspaceId, {
|
||||
const promise = linearGetProject(scope.settings, id, workspaceId, {
|
||||
force: options?.force
|
||||
})
|
||||
.then((project) => {
|
||||
@@ -1364,7 +1464,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -1384,7 +1485,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1408,7 +1510,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1426,13 +1529,12 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
},
|
||||
|
||||
listLinearProjectIssues: async (projectId, workspaceId, limit = 20, options) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const effectiveLimit = clampLinearIssueListLimit(limit)
|
||||
const cacheKey = linearCollectionCacheKey(
|
||||
workspaceId,
|
||||
'project-issues',
|
||||
projectId,
|
||||
effectiveLimit
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearCollectionCacheKey(workspaceId, 'project-issues', projectId, effectiveLimit)
|
||||
)
|
||||
const cached = get().linearProjectIssueCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached)) {
|
||||
@@ -1453,7 +1555,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise = linearListProjectIssues(
|
||||
get().settings,
|
||||
scope.settings,
|
||||
projectId,
|
||||
effectiveLimit,
|
||||
workspaceId,
|
||||
@@ -1468,7 +1570,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -1488,7 +1591,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1515,7 +1619,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1540,9 +1645,13 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
},
|
||||
|
||||
listLinearCustomViews: async (model, limit = 20, workspaceId, options) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const resolvedWorkspaceId = workspaceId ?? getSelectedWorkspaceId(get().linearStatus)
|
||||
const cacheKey = linearCollectionCacheKey(resolvedWorkspaceId, 'custom-views', model, limit)
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearCollectionCacheKey(resolvedWorkspaceId, 'custom-views', model, limit)
|
||||
)
|
||||
const cached = get().linearCustomViewCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached)) {
|
||||
return cached.data ?? emptyLinearCollection<LinearCustomViewSummary>()
|
||||
@@ -1561,7 +1670,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
let entry: InflightLinearCollectionRequest<LinearCustomViewSummary>
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise = linearListCustomViews(get().settings, model, limit, resolvedWorkspaceId, {
|
||||
const promise = linearListCustomViews(scope.settings, model, limit, resolvedWorkspaceId, {
|
||||
force: options?.force
|
||||
})
|
||||
.then((result) => {
|
||||
@@ -1571,7 +1680,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -1591,7 +1701,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1611,7 +1722,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1630,8 +1742,12 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
},
|
||||
|
||||
fetchLinearCustomView: async (viewId, workspaceId, model, options) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const cacheKey = linearCollectionCacheKey(workspaceId, 'custom-view-detail', model, viewId)
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearCollectionCacheKey(workspaceId, 'custom-view-detail', model, viewId)
|
||||
)
|
||||
const cached = get().linearCustomViewDetailCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached)) {
|
||||
return cached.data
|
||||
@@ -1650,7 +1766,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
let entry: InflightLinearDetailRequest<LinearCustomViewSummary | null>
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise = linearGetCustomView(get().settings, viewId, model, workspaceId, {
|
||||
const promise = linearGetCustomView(scope.settings, viewId, model, workspaceId, {
|
||||
force: options?.force
|
||||
})
|
||||
.then((view) => {
|
||||
@@ -1660,7 +1776,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -1680,7 +1797,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1704,7 +1822,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1722,13 +1841,12 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
},
|
||||
|
||||
listLinearCustomViewIssues: async (viewId, workspaceId, limit = 20, options) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const effectiveLimit = clampLinearIssueListLimit(limit)
|
||||
const cacheKey = linearCollectionCacheKey(
|
||||
workspaceId,
|
||||
'custom-view-issues',
|
||||
viewId,
|
||||
effectiveLimit
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearCollectionCacheKey(workspaceId, 'custom-view-issues', viewId, effectiveLimit)
|
||||
)
|
||||
const cached = get().linearCustomViewIssueCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached)) {
|
||||
@@ -1749,7 +1867,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise = linearListCustomViewIssues(
|
||||
get().settings,
|
||||
scope.settings,
|
||||
viewId,
|
||||
effectiveLimit,
|
||||
workspaceId,
|
||||
@@ -1764,7 +1882,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -1784,7 +1903,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1811,7 +1931,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1830,8 +1951,12 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
},
|
||||
|
||||
listLinearCustomViewProjects: async (viewId, workspaceId, limit = 20, options) => {
|
||||
const contextKey = getProviderRuntimeContextKey(get().settings)
|
||||
const cacheKey = linearCollectionCacheKey(workspaceId, 'custom-view-projects', viewId, limit)
|
||||
const scope = getLinearReadScope(get().settings, options?.sourceContext)
|
||||
const { contextKey } = scope
|
||||
const cacheKey = scopedLinearCacheKey(
|
||||
scope,
|
||||
linearCollectionCacheKey(workspaceId, 'custom-view-projects', viewId, limit)
|
||||
)
|
||||
const cached = get().linearCustomViewProjectCache[cacheKey]
|
||||
if (!options?.force && isFresh(cached)) {
|
||||
return cached.data ?? emptyLinearCollection<LinearProjectSummary>()
|
||||
@@ -1850,7 +1975,7 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
let entry: InflightLinearCollectionRequest<LinearProjectSummary>
|
||||
const requestCacheGeneration = linearCacheGeneration
|
||||
const requestMutationGeneration = linearMutationGeneration
|
||||
const promise = linearListCustomViewProjects(get().settings, viewId, limit, workspaceId, {
|
||||
const promise = linearListCustomViewProjects(scope.settings, viewId, limit, workspaceId, {
|
||||
force: options?.force
|
||||
})
|
||||
.then((result) => {
|
||||
@@ -1860,7 +1985,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
set((s) => ({
|
||||
@@ -1880,7 +2006,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
@@ -1900,7 +2027,8 @@ export const createLinearSlice: StateCreator<AppState, [], [], LinearSlice> = (s
|
||||
contextKey,
|
||||
requestCacheGeneration,
|
||||
requestMutationGeneration,
|
||||
get().settings
|
||||
get().settings,
|
||||
scope.explicitSource
|
||||
)
|
||||
) {
|
||||
void get().checkLinearConnection(true)
|
||||
|
||||
Reference in New Issue
Block a user