deeplink fixes for tanstack implementation

This commit is contained in:
Oliver Bryan
2026-01-20 18:13:11 +00:00
parent 75e06f7518
commit 1a98ec96da
3 changed files with 138 additions and 88 deletions

View File

@@ -11,9 +11,13 @@ type SelectionContextValue = {
projectKey: string; projectKey: string;
issueNumber: number | null; issueNumber: number | null;
}; };
selectOrganisation: (organisation: OrganisationResponse | null) => void; selectOrganisation: (organisation: OrganisationResponse | null, options?: SelectionOptions) => void;
selectProject: (project: ProjectResponse | null) => void; selectProject: (project: ProjectResponse | null, options?: SelectionOptions) => void;
selectIssue: (issue: IssueResponse | null) => void; selectIssue: (issue: IssueResponse | null, options?: SelectionOptions) => void;
};
type SelectionOptions = {
skipUrlUpdate?: boolean;
}; };
const SelectionContext = createContext<SelectionContextValue | null>(null); const SelectionContext = createContext<SelectionContextValue | null>(null);
@@ -75,37 +79,46 @@ export function SelectionProvider({ children }: { children: ReactNode }) {
); );
const [selectedIssueId, setSelectedIssueId] = useState<number | null>(null); const [selectedIssueId, setSelectedIssueId] = useState<number | null>(null);
const selectOrganisation = useCallback((organisation: OrganisationResponse | null) => { const selectOrganisation = useCallback(
const id = organisation?.Organisation.id ?? null; (organisation: OrganisationResponse | null, options?: SelectionOptions) => {
setSelectedOrganisationId(id); const id = organisation?.Organisation.id ?? null;
setSelectedProjectId(null); setSelectedOrganisationId(id);
setSelectedIssueId(null); setSelectedProjectId(null);
if (id != null) localStorage.setItem("selectedOrganisationId", `${id}`); setSelectedIssueId(null);
else localStorage.removeItem("selectedOrganisationId"); if (id != null) localStorage.setItem("selectedOrganisationId", `${id}`);
localStorage.removeItem("selectedProjectId"); else localStorage.removeItem("selectedOrganisationId");
updateUrlParams({ localStorage.removeItem("selectedProjectId");
orgSlug: organisation?.Organisation.slug.toLowerCase() ?? null, if (!options?.skipUrlUpdate) {
projectKey: null, updateUrlParams({
issueNumber: null, orgSlug: organisation?.Organisation.slug.toLowerCase() ?? null,
}); projectKey: null,
}, []); issueNumber: null,
});
}
},
[],
);
const selectProject = useCallback((project: ProjectResponse | null) => { const selectProject = useCallback((project: ProjectResponse | null, options?: SelectionOptions) => {
const id = project?.Project.id ?? null; const id = project?.Project.id ?? null;
setSelectedProjectId(id); setSelectedProjectId(id);
setSelectedIssueId(null); setSelectedIssueId(null);
if (id != null) localStorage.setItem("selectedProjectId", `${id}`); if (id != null) localStorage.setItem("selectedProjectId", `${id}`);
else localStorage.removeItem("selectedProjectId"); else localStorage.removeItem("selectedProjectId");
updateUrlParams({ if (!options?.skipUrlUpdate) {
projectKey: project?.Project.key.toLowerCase() ?? null, updateUrlParams({
issueNumber: null, projectKey: project?.Project.key.toLowerCase() ?? null,
}); issueNumber: null,
});
}
}, []); }, []);
const selectIssue = useCallback((issue: IssueResponse | null) => { const selectIssue = useCallback((issue: IssueResponse | null, options?: SelectionOptions) => {
const id = issue?.Issue.id ?? null; const id = issue?.Issue.id ?? null;
setSelectedIssueId(id); setSelectedIssueId(id);
updateUrlParams({ issueNumber: issue?.Issue.number ?? null }); if (!options?.skipUrlUpdate) {
updateUrlParams({ issueNumber: issue?.Issue.number ?? null });
}
}, []); }, []);
const value = useMemo<SelectionContextValue>( const value = useMemo<SelectionContextValue>(

View File

@@ -1,6 +1,7 @@
/** biome-ignore-all lint/correctness/useExhaustiveDependencies: <> */ /** biome-ignore-all lint/correctness/useExhaustiveDependencies: <> */
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { useLocation } from "react-router-dom";
import AccountDialog from "@/components/account-dialog"; import AccountDialog from "@/components/account-dialog";
import { IssueDetailPane } from "@/components/issue-detail-pane"; import { IssueDetailPane } from "@/components/issue-detail-pane";
import { IssueModal } from "@/components/issue-modal"; import { IssueModal } from "@/components/issue-modal";
@@ -33,15 +34,29 @@ export default function App() {
selectedOrganisationId, selectedOrganisationId,
selectedProjectId, selectedProjectId,
selectedIssueId, selectedIssueId,
initialParams,
selectOrganisation, selectOrganisation,
selectProject, selectProject,
selectIssue, selectIssue,
} = useSelection(); } = useSelection();
const location = useLocation();
const deepLinkParams = useMemo(() => {
const params = new URLSearchParams(location.search);
const orgSlug = params.get("o")?.trim().toLowerCase() ?? "";
const projectKey = params.get("p")?.trim().toLowerCase() ?? "";
const issueParam = params.get("i")?.trim() ?? "";
const issueNumber = issueParam === "" ? null : Number.parseInt(issueParam, 10);
return {
orgSlug,
projectKey,
issueNumber: issueNumber != null && Number.isNaN(issueNumber) ? null : issueNumber,
};
}, [location.search]);
const { data: organisationsData = [] } = useOrganisations(); const { data: organisationsData = [] } = useOrganisations();
const { data: projectsData = [] } = useProjects(selectedOrganisationId); const { data: projectsData = [] } = useProjects(selectedOrganisationId);
const { data: issuesData = [] } = useIssues(selectedProjectId); const { data: issuesData = [], isFetched: issuesFetched } = useIssues(selectedProjectId);
const selectedIssue = useSelectedIssue(); const selectedIssue = useSelectedIssue();
const organisations = useMemo( const organisations = useMemo(
@@ -53,87 +68,114 @@ export default function App() {
[projectsData], [projectsData],
); );
const deepLinkStateRef = useRef({ const findById = <T,>(items: T[], id: number | null | undefined, getId: (item: T) => number) =>
appliedOrg: false, id == null ? null : (items.find((item) => getId(item) === id) ?? null);
appliedProject: false, const selectFallback = <T,>(items: T[], selected: T | null) => selected ?? items[0] ?? null;
appliedIssue: false, const findOrgBySlug = (slug: string) =>
orgMatched: false, organisations.find((org) => org.Organisation.slug.toLowerCase() === slug) ?? null;
projectMatched: false, const findProjectByKey = (key: string) =>
projects.find((project) => project.Project.key.toLowerCase() === key) ?? null;
const deepLinkActive = deepLinkParams.projectKey !== "" || deepLinkParams.issueNumber != null;
const deepLinkFlowRef = useRef({
stage: "idle" as "idle" | "org" | "project" | "issue" | "done",
orgSlug: "",
projectKey: "",
issueNumber: null as number | null,
targetOrgId: null as number | null,
targetProjectId: null as number | null,
}); });
useEffect(() => {
deepLinkFlowRef.current = {
stage: deepLinkActive ? "org" : "idle",
orgSlug: deepLinkParams.orgSlug,
projectKey: deepLinkParams.projectKey,
issueNumber: deepLinkParams.issueNumber,
targetOrgId: null,
targetProjectId: null,
};
}, [deepLinkActive, deepLinkParams.orgSlug, deepLinkParams.projectKey, deepLinkParams.issueNumber]);
useEffect(() => { useEffect(() => {
if (organisations.length === 0) return; if (organisations.length === 0) return;
let selected = organisations.find((org) => org.Organisation.id === selectedOrganisationId) ?? null; if (deepLinkActive && deepLinkFlowRef.current.stage !== "org") {
const deepLinkState = deepLinkStateRef.current; return;
}
if (!selected && initialParams.orgSlug && !deepLinkState.appliedOrg) { let selected = findById(organisations, selectedOrganisationId, (org) => org.Organisation.id);
const match = organisations.find( if (deepLinkActive && deepLinkFlowRef.current.orgSlug) {
(org) => org.Organisation.slug.toLowerCase() === initialParams.orgSlug, selected = findOrgBySlug(deepLinkFlowRef.current.orgSlug) ?? selected;
); }
deepLinkState.appliedOrg = true; selected = selectFallback(organisations, selected);
deepLinkState.orgMatched = Boolean(match);
if (match) { if (!selected) return;
selected = match;
if (deepLinkActive) {
deepLinkFlowRef.current.targetOrgId = selected.Organisation.id;
deepLinkFlowRef.current.stage = "project";
if (selected.Organisation.id !== selectedOrganisationId) {
selectOrganisation(selected, { skipUrlUpdate: true });
} }
return;
} }
if (!selected) { if (selected.Organisation.id !== selectedOrganisationId) {
selected = organisations[0] ?? null;
}
if (selected && selected.Organisation.id !== selectedOrganisationId) {
selectOrganisation(selected); selectOrganisation(selected);
} }
}, [organisations, selectedOrganisationId, initialParams.orgSlug]); }, [organisations, selectedOrganisationId, deepLinkActive, selectOrganisation]);
useEffect(() => { useEffect(() => {
if (projects.length === 0) return; if (projects.length === 0) return;
if (!deepLinkActive && selectedProjectId == null) {
selectProject(projects[0]);
return;
}
let selected = projects.find((project) => project.Project.id === selectedProjectId) ?? null; if (deepLinkActive) {
const deepLinkState = deepLinkStateRef.current; const flow = deepLinkFlowRef.current;
if (flow.stage !== "project") return;
if ( if (flow.targetOrgId != null && selectedOrganisationId !== flow.targetOrgId) {
!selected && return;
initialParams.projectKey &&
deepLinkState.orgMatched &&
!deepLinkState.appliedProject
) {
const match = projects.find(
(project) => project.Project.key.toLowerCase() === initialParams.projectKey,
);
deepLinkState.appliedProject = true;
deepLinkState.projectMatched = Boolean(match);
if (match) {
selected = match;
} }
let selected = findById(projects, selectedProjectId, (project) => project.Project.id);
if (flow.projectKey) {
selected = findProjectByKey(flow.projectKey) ?? selected;
}
selected = selectFallback(projects, selected);
if (!selected) return;
flow.targetProjectId = selected.Project.id;
flow.stage = "issue";
if (selected.Project.id !== selectedProjectId) {
selectProject(selected, { skipUrlUpdate: true });
}
return;
} }
if (!selected) { let selected = findById(projects, selectedProjectId, (project) => project.Project.id);
selected = projects[0] ?? null; selected = selectFallback(projects, selected);
}
if (selected && selected.Project.id !== selectedProjectId) { if (selected && selected.Project.id !== selectedProjectId) {
selectProject(selected); selectProject(selected);
} }
}, [projects, selectedProjectId, initialParams.projectKey]); }, [projects, selectedProjectId, selectedOrganisationId, deepLinkActive, selectProject]);
useEffect(() => { useEffect(() => {
if (issuesData.length === 0) return; if (!deepLinkActive) return;
const flow = deepLinkFlowRef.current;
const deepLinkState = deepLinkStateRef.current; if (flow.stage !== "issue") return;
if ( if (flow.targetProjectId != null && selectedProjectId !== flow.targetProjectId) {
initialParams.issueNumber != null && return;
deepLinkState.projectMatched && }
!deepLinkState.appliedIssue if (!issuesFetched) return;
) { if (flow.issueNumber != null) {
const match = issuesData.find((issue) => issue.Issue.number === initialParams.issueNumber); const match = issuesData.find((issue) => issue.Issue.number === flow.issueNumber);
deepLinkState.appliedIssue = true;
if (match && match.Issue.id !== selectedIssueId) { if (match && match.Issue.id !== selectedIssueId) {
selectIssue(match); selectIssue(match, { skipUrlUpdate: true });
} }
} }
}, [issuesData, selectedIssueId, initialParams.issueNumber]); flow.stage = "done";
}, [deepLinkActive, issuesData, issuesFetched, selectedIssueId, selectedProjectId, selectIssue]);
return ( return (
<main className={`w-full h-screen flex flex-col gap-${BREATHING_ROOM} p-${BREATHING_ROOM}`}> <main className={`w-full h-screen flex flex-col gap-${BREATHING_ROOM} p-${BREATHING_ROOM}`}>

View File

@@ -1,15 +1,10 @@
# HIGH PRIORITY # HIGH PRIORITY
- data management system
- data can be cached
- data can be read easily from anywhere
- data can be invalidated easily from anywhere
- sprints - sprints
- timeline display - timeline display
- display sprints - display sprints
- issues - issues
- issue type (options stored on Organisation) - issue type (options stored on Organisation)
- FIX: weird side-scrolling on issue-table
# LOW PRIORITY # LOW PRIORITY