import debounce from "lodash.debounce"; import _get from "lodash.get"; import _set from "lodash.set"; import ReactGA from "react-ga4"; import { toast } from "react-hot-toast"; import { create } from "zustand"; import { defaultJson } from "src/constants/data"; import { FileFormat } from "src/enums/file.enum"; import { contentToJson, jsonToContent } from "src/lib/utils/json/jsonAdapter"; import { isIframe } from "src/lib/utils/widget"; import { documentSvc } from "src/services/document.service"; import useConfig from "./useConfig"; import useGraph from "./useGraph"; import useJson from "./useJson"; import useUser from "./useUser"; type SetContents = { contents?: string; hasChanges?: boolean; skipUpdate?: boolean; }; type Query = string | string[] | undefined; interface JsonActions { getContents: () => string; getFormat: () => FileFormat; getHasChanges: () => boolean; setError: (error: string | null) => void; setHasChanges: (hasChanges: boolean) => void; setContents: (data: SetContents) => void; fetchFile: (fileId: string) => void; fetchUrl: (url: string) => void; editContents: (path: string, value: string, callback?: () => void) => void; setFormat: (format: FileFormat) => void; clear: () => void; setFile: (fileData: File) => void; setJsonSchema: (jsonSchema: object | null) => void; checkEditorSession: (url: Query, widget?: boolean) => void; } export type File = { id: string; views: number; owner_email: string; name: string; content: string; private: boolean; format: FileFormat; created_at: string; updated_at: string; }; const initialStates = { fileData: null as File | null, format: FileFormat.JSON, contents: defaultJson, error: null as any, hasChanges: false, jsonSchema: null as object | null, }; export type FileStates = typeof initialStates; const isURL = (value: string) => { return /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi.test( value ); }; const debouncedUpdateJson = debounce((value: unknown) => { useGraph.getState().setLoading(true); useJson.getState().setJson(JSON.stringify(value, null, 2)); }, 800); const filterArrayAndObjectFields = (obj: object) => { const result = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { if (Array.isArray(obj[key]) || typeof obj[key] === "object") { result[key] = obj[key]; } } } return result; }; const useFile = create()((set, get) => ({ ...initialStates, clear: () => { set({ contents: "" }); useJson.getState().clear(); }, setJsonSchema: jsonSchema => { if (useUser.getState().premium) set({ jsonSchema }); }, setFile: fileData => { set({ fileData, format: fileData.format || FileFormat.JSON }); get().setContents({ contents: fileData.content, hasChanges: false }); }, getContents: () => get().contents, getFormat: () => get().format, getHasChanges: () => get().hasChanges, setFormat: async format => { try { const prevFormat = get().format; set({ format }); const contentJson = await contentToJson(get().contents, prevFormat); const jsonContent = await jsonToContent(JSON.stringify(contentJson, null, 2), format); get().setContents({ contents: jsonContent }); ReactGA.event({ action: "change_data_format", category: "User" }); } catch (error) { get().clear(); console.warn("The content was unable to be converted, so it was cleared instead."); } }, setContents: async ({ contents, hasChanges = true, skipUpdate = false }) => { try { set({ ...(contents && { contents }), error: null, hasChanges }); const isFetchURL = window.location.href.includes("?"); const json = await contentToJson(get().contents, get().format); if (!useConfig.getState().liveTransformEnabled && skipUpdate) return; if (get().hasChanges && contents && contents.length < 80_000 && !isIframe() && !isFetchURL) { sessionStorage.setItem("content", contents); sessionStorage.setItem("format", get().format); set({ hasChanges: true }); } debouncedUpdateJson(json); } catch (error: any) { if (error?.mark?.snippet) return set({ error: error.mark.snippet }); if (error?.message) set({ error: error.message }); useJson.setState({ loading: false }); useGraph.setState({ loading: false }); } }, setError: error => set({ error }), setHasChanges: hasChanges => set({ hasChanges }), fetchUrl: async url => { try { const res = await fetch(url); const json = await res.json(); const jsonStr = JSON.stringify(json, null, 2); get().setContents({ contents: jsonStr }); return useJson.setState({ json: jsonStr, loading: false }); } catch (error) { get().clear(); toast.error("Failed to fetch document from URL!"); } }, checkEditorSession: (url, widget) => { if (url && typeof url === "string") { if (isURL(url)) return get().fetchUrl(url); return get().fetchFile(url); } let contents = defaultJson; const sessionContent = sessionStorage.getItem("content") as string | null; const format = sessionStorage.getItem("format") as FileFormat | null; if (sessionContent && !widget) contents = sessionContent; if (format) set({ format }); get().setContents({ contents, hasChanges: false }); }, fetchFile: async id => { try { const { data, error } = await documentSvc.getById(id); if (error) throw error; if (data?.length) get().setFile(data[0]); if (data?.length === 0) throw new Error("Document not found"); } catch (error: any) { if (error?.message) toast.error(error?.message); get().setContents({ contents: defaultJson, hasChanges: false }); } }, editContents: async (path, value, callback) => { try { if (!value) return; let tempValue = value; const pathJson = _get(JSON.parse(useJson.getState().json), path.replace("{Root}.", "")); const changedValue = JSON.parse(value); if (typeof changedValue !== "string") { tempValue = { ...filterArrayAndObjectFields(pathJson), ...changedValue, }; } else { tempValue = tempValue.replaceAll('"', ""); } const newJson = _set( JSON.parse(useJson.getState().json), path.replace("{Root}.", ""), tempValue ); const contents = await jsonToContent(JSON.stringify(newJson, null, 2), get().format); get().setContents({ contents }); if (callback) callback(); } catch (error) { toast.error("Invalid Property!"); } }, })); export default useFile;