mirror of
https://github.com/hex248/sprint.git
synced 2026-02-07 18:23:03 +00:00
deeplink fixes for tanstack implementation
This commit is contained in:
@@ -11,9 +11,13 @@ type SelectionContextValue = {
|
||||
projectKey: string;
|
||||
issueNumber: number | null;
|
||||
};
|
||||
selectOrganisation: (organisation: OrganisationResponse | null) => void;
|
||||
selectProject: (project: ProjectResponse | null) => void;
|
||||
selectIssue: (issue: IssueResponse | null) => void;
|
||||
selectOrganisation: (organisation: OrganisationResponse | null, options?: SelectionOptions) => void;
|
||||
selectProject: (project: ProjectResponse | null, options?: SelectionOptions) => void;
|
||||
selectIssue: (issue: IssueResponse | null, options?: SelectionOptions) => void;
|
||||
};
|
||||
|
||||
type SelectionOptions = {
|
||||
skipUrlUpdate?: boolean;
|
||||
};
|
||||
|
||||
const SelectionContext = createContext<SelectionContextValue | null>(null);
|
||||
@@ -75,7 +79,8 @@ export function SelectionProvider({ children }: { children: ReactNode }) {
|
||||
);
|
||||
const [selectedIssueId, setSelectedIssueId] = useState<number | null>(null);
|
||||
|
||||
const selectOrganisation = useCallback((organisation: OrganisationResponse | null) => {
|
||||
const selectOrganisation = useCallback(
|
||||
(organisation: OrganisationResponse | null, options?: SelectionOptions) => {
|
||||
const id = organisation?.Organisation.id ?? null;
|
||||
setSelectedOrganisationId(id);
|
||||
setSelectedProjectId(null);
|
||||
@@ -83,29 +88,37 @@ export function SelectionProvider({ children }: { children: ReactNode }) {
|
||||
if (id != null) localStorage.setItem("selectedOrganisationId", `${id}`);
|
||||
else localStorage.removeItem("selectedOrganisationId");
|
||||
localStorage.removeItem("selectedProjectId");
|
||||
if (!options?.skipUrlUpdate) {
|
||||
updateUrlParams({
|
||||
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;
|
||||
setSelectedProjectId(id);
|
||||
setSelectedIssueId(null);
|
||||
if (id != null) localStorage.setItem("selectedProjectId", `${id}`);
|
||||
else localStorage.removeItem("selectedProjectId");
|
||||
if (!options?.skipUrlUpdate) {
|
||||
updateUrlParams({
|
||||
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;
|
||||
setSelectedIssueId(id);
|
||||
if (!options?.skipUrlUpdate) {
|
||||
updateUrlParams({ issueNumber: issue?.Issue.number ?? null });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value = useMemo<SelectionContextValue>(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** biome-ignore-all lint/correctness/useExhaustiveDependencies: <> */
|
||||
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import AccountDialog from "@/components/account-dialog";
|
||||
import { IssueDetailPane } from "@/components/issue-detail-pane";
|
||||
import { IssueModal } from "@/components/issue-modal";
|
||||
@@ -33,15 +34,29 @@ export default function App() {
|
||||
selectedOrganisationId,
|
||||
selectedProjectId,
|
||||
selectedIssueId,
|
||||
initialParams,
|
||||
selectOrganisation,
|
||||
selectProject,
|
||||
selectIssue,
|
||||
} = 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: projectsData = [] } = useProjects(selectedOrganisationId);
|
||||
const { data: issuesData = [] } = useIssues(selectedProjectId);
|
||||
const { data: issuesData = [], isFetched: issuesFetched } = useIssues(selectedProjectId);
|
||||
const selectedIssue = useSelectedIssue();
|
||||
|
||||
const organisations = useMemo(
|
||||
@@ -53,87 +68,114 @@ export default function App() {
|
||||
[projectsData],
|
||||
);
|
||||
|
||||
const deepLinkStateRef = useRef({
|
||||
appliedOrg: false,
|
||||
appliedProject: false,
|
||||
appliedIssue: false,
|
||||
orgMatched: false,
|
||||
projectMatched: false,
|
||||
const findById = <T,>(items: T[], id: number | null | undefined, getId: (item: T) => number) =>
|
||||
id == null ? null : (items.find((item) => getId(item) === id) ?? null);
|
||||
const selectFallback = <T,>(items: T[], selected: T | null) => selected ?? items[0] ?? null;
|
||||
const findOrgBySlug = (slug: string) =>
|
||||
organisations.find((org) => org.Organisation.slug.toLowerCase() === slug) ?? null;
|
||||
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(() => {
|
||||
if (organisations.length === 0) return;
|
||||
|
||||
let selected = organisations.find((org) => org.Organisation.id === selectedOrganisationId) ?? null;
|
||||
const deepLinkState = deepLinkStateRef.current;
|
||||
|
||||
if (!selected && initialParams.orgSlug && !deepLinkState.appliedOrg) {
|
||||
const match = organisations.find(
|
||||
(org) => org.Organisation.slug.toLowerCase() === initialParams.orgSlug,
|
||||
);
|
||||
deepLinkState.appliedOrg = true;
|
||||
deepLinkState.orgMatched = Boolean(match);
|
||||
if (match) {
|
||||
selected = match;
|
||||
}
|
||||
if (deepLinkActive && deepLinkFlowRef.current.stage !== "org") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
selected = organisations[0] ?? null;
|
||||
let selected = findById(organisations, selectedOrganisationId, (org) => org.Organisation.id);
|
||||
if (deepLinkActive && deepLinkFlowRef.current.orgSlug) {
|
||||
selected = findOrgBySlug(deepLinkFlowRef.current.orgSlug) ?? selected;
|
||||
}
|
||||
selected = selectFallback(organisations, selected);
|
||||
|
||||
if (!selected) return;
|
||||
|
||||
if (deepLinkActive) {
|
||||
deepLinkFlowRef.current.targetOrgId = selected.Organisation.id;
|
||||
deepLinkFlowRef.current.stage = "project";
|
||||
if (selected.Organisation.id !== selectedOrganisationId) {
|
||||
selectOrganisation(selected, { skipUrlUpdate: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected && selected.Organisation.id !== selectedOrganisationId) {
|
||||
if (selected.Organisation.id !== selectedOrganisationId) {
|
||||
selectOrganisation(selected);
|
||||
}
|
||||
}, [organisations, selectedOrganisationId, initialParams.orgSlug]);
|
||||
}, [organisations, selectedOrganisationId, deepLinkActive, selectOrganisation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projects.length === 0) return;
|
||||
|
||||
let selected = projects.find((project) => project.Project.id === selectedProjectId) ?? null;
|
||||
const deepLinkState = deepLinkStateRef.current;
|
||||
|
||||
if (
|
||||
!selected &&
|
||||
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;
|
||||
}
|
||||
if (!deepLinkActive && selectedProjectId == null) {
|
||||
selectProject(projects[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
selected = projects[0] ?? null;
|
||||
if (deepLinkActive) {
|
||||
const flow = deepLinkFlowRef.current;
|
||||
if (flow.stage !== "project") return;
|
||||
if (flow.targetOrgId != null && selectedOrganisationId !== flow.targetOrgId) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
let selected = findById(projects, selectedProjectId, (project) => project.Project.id);
|
||||
selected = selectFallback(projects, selected);
|
||||
if (selected && selected.Project.id !== selectedProjectId) {
|
||||
selectProject(selected);
|
||||
}
|
||||
}, [projects, selectedProjectId, initialParams.projectKey]);
|
||||
}, [projects, selectedProjectId, selectedOrganisationId, deepLinkActive, selectProject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (issuesData.length === 0) return;
|
||||
|
||||
const deepLinkState = deepLinkStateRef.current;
|
||||
if (
|
||||
initialParams.issueNumber != null &&
|
||||
deepLinkState.projectMatched &&
|
||||
!deepLinkState.appliedIssue
|
||||
) {
|
||||
const match = issuesData.find((issue) => issue.Issue.number === initialParams.issueNumber);
|
||||
deepLinkState.appliedIssue = true;
|
||||
if (!deepLinkActive) return;
|
||||
const flow = deepLinkFlowRef.current;
|
||||
if (flow.stage !== "issue") return;
|
||||
if (flow.targetProjectId != null && selectedProjectId !== flow.targetProjectId) {
|
||||
return;
|
||||
}
|
||||
if (!issuesFetched) return;
|
||||
if (flow.issueNumber != null) {
|
||||
const match = issuesData.find((issue) => issue.Issue.number === flow.issueNumber);
|
||||
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 (
|
||||
<main className={`w-full h-screen flex flex-col gap-${BREATHING_ROOM} p-${BREATHING_ROOM}`}>
|
||||
|
||||
5
todo.md
5
todo.md
@@ -1,15 +1,10 @@
|
||||
# HIGH PRIORITY
|
||||
|
||||
- data management system
|
||||
- data can be cached
|
||||
- data can be read easily from anywhere
|
||||
- data can be invalidated easily from anywhere
|
||||
- sprints
|
||||
- timeline display
|
||||
- display sprints
|
||||
- issues
|
||||
- issue type (options stored on Organisation)
|
||||
- FIX: weird side-scrolling on issue-table
|
||||
|
||||
# LOW PRIORITY
|
||||
|
||||
|
||||
Reference in New Issue
Block a user