| 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<FileStates & JsonActions>()((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; | |