Bin
2025-12-17 1442f92732d7c5311a627a7ba3aaa0bb8ffc539f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import type React from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { shallowEqualObjects } from "shallow-equal";
import { addVisitedProject } from "@humansignal/core";
import { useAuth } from "@humansignal/core/providers/AuthProvider";
import { FF_UNSAVED_CHANGES, isFF } from "../utils/feature-flags";
import { useAPI, type WrappedResponse } from "./ApiProvider";
import { useAppStore } from "./AppStoreProvider";
import { useParams } from "./RoutesProvider";
import { atom, useSetAtom } from "jotai";
 
type Empty = Record<string, never>;
 
export const projectAtom = atom<APIProject | Empty>({});
 
type Context = {
  project: APIProject | Empty;
  fetchProject: (id?: string | number, force?: boolean) => Promise<APIProject | void>;
  updateProject: (fields: APIProject) => Promise<WrappedResponse<APIProject>>;
  invalidateCache: () => void;
};
 
export const ProjectContext = createContext<Context>({} as Context);
ProjectContext.displayName = "ProjectContext";
 
const projectCache = new Map<number, APIProject>();
 
type UpdateProjectOptions = {
  returnErrors?: boolean;
};
 
export const ProjectProvider: React.FunctionComponent = ({ children }) => {
  const api = useAPI();
  const params = useParams();
  const { user } = useAuth();
  const { update: updateStore } = useAppStore();
  // @todo use null for missed project data
  const [projectData, _setProjectData] = useState<APIProject | Empty>(projectCache.get(+params.id) ?? {});
  const setProject = useSetAtom(projectAtom);
 
  const setProjectData = (project: APIProject | Empty) => {
    _setProjectData(project);
    setProject(project);
  };
 
  const fetchProject: Context["fetchProject"] = useCallback(
    async (id, force = false) => {
      const finalProjectId = +(id ?? params.id);
 
      if (isNaN(finalProjectId)) return;
 
      if (!force && projectCache.has(finalProjectId) && projectCache.get(finalProjectId) !== undefined) {
        setProjectData({ ...projectCache.get(finalProjectId)! });
      }
 
      const result = await api.callApi<APIProject>("project", {
        params: { pk: finalProjectId },
        errorFilter: () => false,
      });
 
      const projectInfo = result as unknown as APIProject;
 
      if (shallowEqualObjects(projectData, projectInfo) === false) {
        setProjectData(projectInfo);
        updateStore({ project: projectInfo });
        projectCache.set(projectInfo.id, projectInfo);
      }
 
      if (projectInfo?.id) {
        addVisitedProject(projectInfo.id, user?.id);
      }
 
      return projectInfo;
    },
    [params],
  );
 
  const updateProject: Context["updateProject"] = useCallback(
    async (fields: APIProject, options?: UpdateProjectOptions) => {
      const result = await api.callApi<APIProject>("updateProject", {
        params: {
          pk: projectData.id,
        },
        body: fields,
        errorFilter: options?.returnErrors ? undefined : () => true,
      });
 
      if (isFF(FF_UNSAVED_CHANGES)) {
        if (result?.$meta?.ok) {
          setProjectData(result as unknown as APIProject);
          updateStore({ project: result });
          projectCache.set(result.id, result);
        }
      } else {
        if (result.$meta) {
          setProjectData(result as unknown as APIProject);
          updateStore({ project: result });
        }
      }
 
      return result;
    },
    [projectData, setProjectData, updateStore],
  );
 
  useEffect(() => {
    if (+params.id !== projectData?.id) {
      setProjectData({});
    }
    fetchProject();
  }, [params]);
 
  useEffect(() => {
    return () => projectCache.clear();
  }, []);
 
  return (
    <ProjectContext.Provider
      value={{
        project: projectData,
        fetchProject,
        updateProject,
        invalidateCache() {
          projectCache.clear();
          setProjectData({});
        },
      }}
    >
      {children}
    </ProjectContext.Provider>
  );
};
 
// without this extra typing VSCode doesn't see the type after import :(
export const useProject: () => Context = () => {
  return useContext(ProjectContext) ?? {};
};