dirk-cfx-react 1.1.94 → 1.1.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{chunk-ZPFW7C2A.js → chunk-3QPPR6OY.js} +3 -3
  2. package/dist/{chunk-ZPFW7C2A.js.map → chunk-3QPPR6OY.js.map} +1 -1
  3. package/dist/{chunk-MYNNCLMA.js → chunk-4QSXUENK.js} +325 -43
  4. package/dist/chunk-4QSXUENK.js.map +1 -0
  5. package/dist/{chunk-YA2PXBP6.cjs → chunk-5XODUPUE.cjs} +325 -43
  6. package/dist/chunk-5XODUPUE.cjs.map +1 -0
  7. package/dist/{chunk-3T2CCW5E.cjs → chunk-6CNT35D6.cjs} +9 -9
  8. package/dist/{chunk-3T2CCW5E.cjs.map → chunk-6CNT35D6.cjs.map} +1 -1
  9. package/dist/{chunk-KGLO3ZAS.js → chunk-ABBZHNLP.js} +3 -3
  10. package/dist/{chunk-KGLO3ZAS.js.map → chunk-ABBZHNLP.js.map} +1 -1
  11. package/dist/{chunk-QOQQ3ERZ.cjs → chunk-EOSHBZQK.cjs} +17 -17
  12. package/dist/{chunk-QOQQ3ERZ.cjs.map → chunk-EOSHBZQK.cjs.map} +1 -1
  13. package/dist/{chunk-Z7N5AQJW.js → chunk-GFODSZSP.js} +3 -3
  14. package/dist/{chunk-Z7N5AQJW.js.map → chunk-GFODSZSP.js.map} +1 -1
  15. package/dist/{chunk-K3GDU6EY.cjs → chunk-HG2J72NC.cjs} +167 -167
  16. package/dist/{chunk-K3GDU6EY.cjs.map → chunk-HG2J72NC.cjs.map} +1 -1
  17. package/dist/{chunk-E2SFEBBB.cjs → chunk-HOVRS2Z6.cjs} +4 -4
  18. package/dist/{chunk-E2SFEBBB.cjs.map → chunk-HOVRS2Z6.cjs.map} +1 -1
  19. package/dist/{chunk-VKYONY7E.cjs → chunk-MY3Z7LPB.cjs} +5 -5
  20. package/dist/{chunk-VKYONY7E.cjs.map → chunk-MY3Z7LPB.cjs.map} +1 -1
  21. package/dist/{chunk-HAB6H274.js → chunk-UGDMOWVM.js} +6 -6
  22. package/dist/{chunk-HAB6H274.js.map → chunk-UGDMOWVM.js.map} +1 -1
  23. package/dist/{chunk-NVMOGS7Y.js → chunk-WUALULEH.js} +4 -4
  24. package/dist/{chunk-NVMOGS7Y.js.map → chunk-WUALULEH.js.map} +1 -1
  25. package/dist/components/index.cjs +76 -76
  26. package/dist/components/index.js +5 -5
  27. package/dist/hooks/index.cjs +22 -22
  28. package/dist/hooks/index.js +4 -4
  29. package/dist/index.cjs +147 -147
  30. package/dist/index.js +6 -6
  31. package/dist/providers/index.cjs +5 -5
  32. package/dist/providers/index.js +3 -3
  33. package/dist/utils/index.cjs +48 -48
  34. package/dist/utils/index.js +1 -1
  35. package/package.json +1 -1
  36. package/dist/chunk-MYNNCLMA.js.map +0 -1
  37. package/dist/chunk-YA2PXBP6.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { noop, fetchNui } from './chunk-MYNNCLMA.js';
1
+ import { noop, fetchNui } from './chunk-4QSXUENK.js';
2
2
  import { useRef, useEffect } from 'react';
3
3
  import { create } from 'zustand';
4
4
 
@@ -142,5 +142,5 @@ function createScriptConfig(defaultValue) {
142
142
  }
143
143
 
144
144
  export { createScriptConfig, getScriptConfigInstance, useNuiEvent };
145
- //# sourceMappingURL=chunk-Z7N5AQJW.js.map
146
- //# sourceMappingURL=chunk-Z7N5AQJW.js.map
145
+ //# sourceMappingURL=chunk-GFODSZSP.js.map
146
+ //# sourceMappingURL=chunk-GFODSZSP.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks/useNuiEvent.ts","../src/hooks/useScriptConfig.ts"],"names":[],"mappings":"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","file":"chunk-Z7N5AQJW.js","sourcesContent":["import { MutableRefObject, useEffect, useRef } from \"react\";\r\nimport { noop } from \"../utils/misc\";\r\n\r\nexport interface NuiMessageData<T = unknown> {\r\n action: string;\r\n data: T;\r\n}\r\n\r\nexport type NuiHandlerSignature<T> = ( data: T) => void;\r\n\r\n/**\r\n * A hook that manage events listeners for receiving data from the client scripts\r\n * @param action The specific `action` that should be listened for.\r\n * @param handler The callback function that will handle data relayed by this hook\r\n *\r\n * @example\r\n * useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {\r\n * // whatever logic you want\r\n * })\r\n *\r\n **/\r\n\r\nexport const useNuiEvent = <T = unknown>(\r\n action: string,\r\n handler: ( data: T) => void,\r\n) => {\r\n const savedHandler: MutableRefObject<NuiHandlerSignature<T>> = useRef(noop);\r\n\r\n // Make sure we handle for a reactive handler\r\n useEffect(() => {\r\n savedHandler.current = handler;\r\n }, [handler]);\r\n\r\n useEffect(() => {\r\n const eventListener = (event: MessageEvent<NuiMessageData<T>>) => {\r\n const { action: eventAction, data } = event.data;\r\n\r\n if (savedHandler.current) {\r\n if (eventAction === action) {\r\n savedHandler.current( data);\r\n }\r\n }\r\n };\r\n\r\n window.addEventListener(\"message\", eventListener);\r\n // Remove Event Listener on component dirkup\r\n return () => window.removeEventListener(\"message\", eventListener);\r\n }, [action]);\r\n};\r\n","import { fetchNui } from \"@/utils\";\r\nimport { useNuiEvent } from \"@/hooks/useNuiEvent\";\r\nimport { create } from \"zustand\";\r\n\r\ntype ScriptConfigUpdateMeta<T> = {\r\n client_version?: number;\r\n latestVersion?: number;\r\n latestData?: Partial<T>;\r\n changed_paths?: Array<{ path: string; old: unknown; new: unknown }>;\r\n lastEditor?: { source?: number; name?: string; identifier?: string };\r\n};\r\n\r\nexport type ScriptConfigHistoryChange = {\r\n path: string;\r\n old: unknown;\r\n new: unknown;\r\n};\r\n\r\nexport type ScriptConfigHistoryEntry = {\r\n at_unix: number;\r\n at_utc: string;\r\n script: string;\r\n admin?: { source?: number; name?: string; identifier?: string };\r\n expected_version?: number;\r\n applied_version?: number;\r\n changes: ScriptConfigHistoryChange[];\r\n};\r\n\r\nexport type ScriptConfigHistoryRequest = {\r\n offset?: number;\r\n limit?: number;\r\n query?: string;\r\n path?: string;\r\n admin?: string;\r\n fromUnix?: number;\r\n toUnix?: number;\r\n};\r\n\r\nexport type ScriptConfigHistoryResponse = {\r\n success: boolean;\r\n _error?: string;\r\n data?: {\r\n items: ScriptConfigHistoryEntry[];\r\n total: number;\r\n limit: number;\r\n offset: number;\r\n nextOffset?: number;\r\n };\r\n};\r\n\r\ntype NuiResponse<T> = {\r\n success: boolean;\r\n message?: string;\r\n _error?: string;\r\n meta?: ScriptConfigUpdateMeta<T>;\r\n};\r\n\r\n// Server-only \"sliver\" response (GET_SERVER_ONLY_SCRIPT_CONFIG). Identical\r\n// envelope to GET_FULL_SCRIPT_CONFIG — only the inner key differs (`serverOnly`\r\n// instead of `config`). `serverOnly` is the x-serverOnly subtree in the SAME\r\n// nested shape/paths as `config` (a deep object, NOT flat dot-paths); `{}` when\r\n// the schema declares no server-only fields.\r\nexport type ServerOnlyScriptConfigResponse<T> = {\r\n success: boolean;\r\n _error?: string;\r\n data?: { serverOnly: Partial<T>; clientVersion: number };\r\n};\r\n\r\n// Recursive deep-merge: returns a new object where `patch`'s plain-object\r\n// branches are merged INTO `base`'s, and non-object values (scalars, arrays)\r\n// from `patch` replace `base`. Used to top up the server-only sliver onto the\r\n// already-cached client-visible config WITHOUT the top-level spread-replace\r\n// `fetchScriptConfig` uses — a shallow spread would clobber sibling\r\n// client-visible keys that live under a shared parent section. By construction\r\n// the two views are disjoint (serverOnly is the set-complement of the\r\n// client-visible view), but we still deep-merge so a shared parent section keeps\r\n// both its client-visible and server-only children.\r\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\r\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\r\n}\r\n\r\nfunction deepMerge<T>(base: T, patch: Partial<T>): T {\r\n if (!isPlainObject(base) || !isPlainObject(patch)) {\r\n // patch wins for non-object values (arrays/scalars are replaced wholesale).\r\n return (patch as unknown as T) ?? base;\r\n }\r\n const out: Record<string, unknown> = { ...(base as Record<string, unknown>) };\r\n for (const key of Object.keys(patch as Record<string, unknown>)) {\r\n const patchVal = (patch as Record<string, unknown>)[key];\r\n const baseVal = out[key];\r\n out[key] = isPlainObject(baseVal) && isPlainObject(patchVal)\r\n ? deepMerge(baseVal, patchVal as Record<string, unknown>)\r\n : patchVal;\r\n }\r\n return out as T;\r\n}\r\n\r\n// Derive the set of changed TOP-LEVEL section keys from a useForm changed-set\r\n// of dotted paths (e.g. [\"stores.0.name\", \"basic.weightUnit\"] → {\"stores\",\"basic\"}).\r\n// CONSERVATIVE by design: any changed path under section X marks the WHOLE\r\n// section X as changed, so the caller sends its full current value. This is what\r\n// lets deletions inside a section (a removed array element) propagate, because\r\n// the server/client overwrite the section wholesale rather than deep-merging.\r\nfunction changedTopLevelSections(changedFields?: readonly string[]): string[] {\r\n if (!changedFields || changedFields.length === 0) return [];\r\n const sections = new Set<string>();\r\n for (const path of changedFields) {\r\n if (typeof path !== \"string\" || path.length === 0) continue;\r\n const dot = path.indexOf(\".\");\r\n sections.add(dot === -1 ? path : path.slice(0, dot));\r\n }\r\n return Array.from(sections);\r\n}\r\n\r\n// ── Singleton registry ────────────────────────────────────────────────────────\r\n\r\nexport interface ScriptConfigInstance<T = any> {\r\n store: { getState: () => T; setState: (partial: Partial<T> | ((prev: T) => T)) => void };\r\n updateConfig: (newConfig: Partial<T>, changedFields?: readonly string[]) => Promise<NuiResponse<T>>;\r\n resetConfig: () => Promise<{ success: boolean; _error?: string }>;\r\n getHistory: (params?: ScriptConfigHistoryRequest) => Promise<ScriptConfigHistoryResponse>;\r\n fetchConfig: () => Promise<T | null>;\r\n // Admin-only top-up: fetches the server-only sliver and DEEP-MERGES it onto\r\n // the already-cached client-visible store state. Returns the merged full\r\n // config, or null on failure / no-permission. The sliver is in-memory only —\r\n // it is NEVER persisted to KVP or any cache (KVP holds client-visible only).\r\n fetchServerOnly: () => Promise<T | null>;\r\n}\r\n\r\nlet _instance: ScriptConfigInstance | null = null;\r\n\r\nexport function getScriptConfigInstance<T = any>(): ScriptConfigInstance<T> {\r\n if (!_instance) throw new Error(\"[dirk-cfx-react] createScriptConfig must be called before using ConfigPanel\");\r\n return _instance as ScriptConfigInstance<T>;\r\n}\r\n\r\nexport function createScriptConfig<T>(defaultValue: T) {\r\n const store = create<T>(() => defaultValue);\r\n let clientVersion = 0;\r\n\r\n const useScriptConfigHooks = () => {\r\n useNuiEvent<{ config?: Partial<T>; clientVersion?: number; sectionReplace?: boolean }>(\r\n \"UPDATE_SCRIPT_CONFIG\",\r\n (data) => {\r\n if (!data) return;\r\n\r\n if (typeof data.clientVersion === \"number\") {\r\n clientVersion = data.clientVersion as number;\r\n }\r\n\r\n if (data.config && typeof data.config === \"object\") {\r\n // Both apply modes use the same top-level merge-by-replace:\r\n // - sectionReplace: `config` holds only the changed sections; spreading\r\n // them over `prev` wholesale-overwrites those keys and leaves every\r\n // other (unsent) section exactly as-is. We MUST NOT full-replace here\r\n // or the unsent sections would vanish from the UI.\r\n // - fullReplace / merge / initial: `config` holds the FULL config, so\r\n // the same spread effectively replaces every key.\r\n store.setState((prev) => ({ ...prev, ...(data.config as Partial<T>) }));\r\n }\r\n }\r\n );\r\n };\r\n\r\n const fetchScriptConfig = async (): Promise<T | null> => {\r\n try {\r\n const response = await fetchNui<{\r\n success: boolean;\r\n data?: { config: T; clientVersion: number };\r\n }>(\"GET_FULL_SCRIPT_CONFIG\");\r\n\r\n if (response?.success && response.data?.config) {\r\n store.setState(() => response.data!.config as T);\r\n if (typeof response.data.clientVersion === \"number\") {\r\n clientVersion = response.data.clientVersion;\r\n }\r\n return response.data.config;\r\n }\r\n } catch { /* fallback to current store state */ }\r\n return null;\r\n };\r\n\r\n // Option B top-up. Called ONLY by the admin editor on panel open. The\r\n // baseline (client-visible config) is already in the store — pushed by Lua\r\n // via UPDATE_SCRIPT_CONFIG (handled in useScriptConfigHooks above) and cached\r\n // in KVP on the Lua side. Here we fetch ONLY the server-only sliver\r\n // (permission-gated server-side on canEditScript) and DEEP-MERGE it onto that\r\n // baseline to form the full editor view. We deliberately do NOT spread-replace\r\n // (as fetchScriptConfig does) — that would clobber sibling client-visible keys\r\n // under a shared parent section.\r\n //\r\n // SECURITY: this sliver is admin-only and in-memory only. It is NEVER written\r\n // to KVP or any cache; it is re-fetched fresh on every panel open.\r\n const fetchServerOnlyScriptConfig = async (): Promise<T | null> => {\r\n try {\r\n const response = await fetchNui<ServerOnlyScriptConfigResponse<T>>(\r\n \"GET_SERVER_ONLY_SCRIPT_CONFIG\"\r\n );\r\n\r\n if (response?.success && response.data) {\r\n // clientVersion here matches the value already in the store from the\r\n // Lua push; keep it in sync (safe no-op if identical).\r\n if (typeof response.data.clientVersion === \"number\") {\r\n clientVersion = response.data.clientVersion;\r\n }\r\n const sliver = response.data.serverOnly;\r\n // An empty sliver ({} — schema declares no server-only paths) is a valid\r\n // no-op merge: deepMerge returns the baseline unchanged.\r\n const merged = deepMerge(store.getState() as T, (sliver ?? {}) as Partial<T>);\r\n store.setState(() => merged as T);\r\n return merged;\r\n }\r\n } catch { /* fall back to the client-visible baseline already in the store */ }\r\n return null;\r\n };\r\n\r\n // `changedFields` is the useForm tracked changed-set (dotted paths). When\r\n // supplied AND it resolves to ≥1 top-level section, we send a SECTION-DELTA:\r\n // only the changed sections, each as its WHOLE current value, with\r\n // sectionReplace:true. The server/client then overwrite those keys wholesale\r\n // (which is what lets deletions inside a section propagate). When omitted or\r\n // empty, we fall back to the legacy full-send (server defaults to\r\n // fullReplace=true when no `sectionReplace` flag is present).\r\n const updateScriptConfig = async (\r\n newConfig: Partial<T>,\r\n changedFields?: readonly string[]\r\n ): Promise<NuiResponse<T>> => {\r\n store.setState((prev) => ({ ...prev, ...newConfig }));\r\n\r\n const sections = changedTopLevelSections(changedFields);\r\n\r\n let payload: { data: Partial<T>; expectedVersion: number; sectionReplace?: boolean };\r\n if (sections.length > 0) {\r\n // Section-delta: flat object keyed by top-level section name → whole\r\n // current section value (pulled from the just-applied store state so it\r\n // reflects the user's edits, including deletions).\r\n const current = store.getState() as Record<string, unknown>;\r\n const delta: Record<string, unknown> = {};\r\n for (const key of sections) delta[key] = current[key];\r\n payload = { data: delta as Partial<T>, expectedVersion: clientVersion, sectionReplace: true };\r\n } else {\r\n // No changed-set provided (or nothing resolved) but the user explicitly\r\n // saved → legacy full-send. No sectionReplace flag ⇒ server fullReplace.\r\n payload = { data: newConfig, expectedVersion: clientVersion };\r\n }\r\n\r\n const response = await fetchNui<NuiResponse<T>>(\"UPDATE_SCRIPT_CONFIG\", payload);\r\n\r\n if (response?.meta?.client_version != null) {\r\n clientVersion = response.meta.client_version as number;\r\n }\r\n\r\n if (response?.success === false && response?.meta?.latestData) {\r\n store.setState((prev) => ({ ...prev, ...(response.meta!.latestData as Partial<T>) }));\r\n }\r\n\r\n return response;\r\n };\r\n\r\n const getScriptConfigHistory = async (\r\n params: ScriptConfigHistoryRequest = {}\r\n ): Promise<ScriptConfigHistoryResponse> => {\r\n return fetchNui<ScriptConfigHistoryResponse>('GET_SCRIPT_CONFIG_HISTORY', params);\r\n };\r\n\r\n const resetConfig = async (): Promise<{ success: boolean; _error?: string }> => {\r\n const response = await fetchNui<{ success: boolean; _error?: string }>('RESET_SCRIPT_CONFIG');\r\n if (response?.success) {\r\n // Option B: after a reset the server restores defaults and broadcasts a\r\n // fresh client-visible UPDATE_SCRIPT_CONFIG push (handled in\r\n // useScriptConfigHooks → store baseline). We only need to re-merge the\r\n // server-only sliver onto that refreshed baseline to rebuild the full\r\n // editor view — no GET_FULL_SCRIPT_CONFIG round-trip. fetchServerOnly\r\n // merges onto whatever client-visible baseline is currently in the store,\r\n // so it's safe regardless of broadcast/callback ordering.\r\n await fetchServerOnlyScriptConfig();\r\n }\r\n return response;\r\n };\r\n\r\n _instance = {\r\n store,\r\n updateConfig: updateScriptConfig,\r\n resetConfig,\r\n getHistory: getScriptConfigHistory,\r\n fetchConfig: fetchScriptConfig,\r\n fetchServerOnly: fetchServerOnlyScriptConfig,\r\n };\r\n\r\n return {store, updateScriptConfig, resetConfig, getScriptConfigHistory, useScriptConfigHooks, fetchScriptConfig, fetchServerOnlyScriptConfig}\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/hooks/useNuiEvent.ts","../src/hooks/useScriptConfig.ts"],"names":[],"mappings":"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","file":"chunk-GFODSZSP.js","sourcesContent":["import { MutableRefObject, useEffect, useRef } from \"react\";\r\nimport { noop } from \"../utils/misc\";\r\n\r\nexport interface NuiMessageData<T = unknown> {\r\n action: string;\r\n data: T;\r\n}\r\n\r\nexport type NuiHandlerSignature<T> = ( data: T) => void;\r\n\r\n/**\r\n * A hook that manage events listeners for receiving data from the client scripts\r\n * @param action The specific `action` that should be listened for.\r\n * @param handler The callback function that will handle data relayed by this hook\r\n *\r\n * @example\r\n * useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {\r\n * // whatever logic you want\r\n * })\r\n *\r\n **/\r\n\r\nexport const useNuiEvent = <T = unknown>(\r\n action: string,\r\n handler: ( data: T) => void,\r\n) => {\r\n const savedHandler: MutableRefObject<NuiHandlerSignature<T>> = useRef(noop);\r\n\r\n // Make sure we handle for a reactive handler\r\n useEffect(() => {\r\n savedHandler.current = handler;\r\n }, [handler]);\r\n\r\n useEffect(() => {\r\n const eventListener = (event: MessageEvent<NuiMessageData<T>>) => {\r\n const { action: eventAction, data } = event.data;\r\n\r\n if (savedHandler.current) {\r\n if (eventAction === action) {\r\n savedHandler.current( data);\r\n }\r\n }\r\n };\r\n\r\n window.addEventListener(\"message\", eventListener);\r\n // Remove Event Listener on component dirkup\r\n return () => window.removeEventListener(\"message\", eventListener);\r\n }, [action]);\r\n};\r\n","import { fetchNui } from \"@/utils\";\r\nimport { useNuiEvent } from \"@/hooks/useNuiEvent\";\r\nimport { create } from \"zustand\";\r\n\r\ntype ScriptConfigUpdateMeta<T> = {\r\n client_version?: number;\r\n latestVersion?: number;\r\n latestData?: Partial<T>;\r\n changed_paths?: Array<{ path: string; old: unknown; new: unknown }>;\r\n lastEditor?: { source?: number; name?: string; identifier?: string };\r\n};\r\n\r\nexport type ScriptConfigHistoryChange = {\r\n path: string;\r\n old: unknown;\r\n new: unknown;\r\n};\r\n\r\nexport type ScriptConfigHistoryEntry = {\r\n at_unix: number;\r\n at_utc: string;\r\n script: string;\r\n admin?: { source?: number; name?: string; identifier?: string };\r\n expected_version?: number;\r\n applied_version?: number;\r\n changes: ScriptConfigHistoryChange[];\r\n};\r\n\r\nexport type ScriptConfigHistoryRequest = {\r\n offset?: number;\r\n limit?: number;\r\n query?: string;\r\n path?: string;\r\n admin?: string;\r\n fromUnix?: number;\r\n toUnix?: number;\r\n};\r\n\r\nexport type ScriptConfigHistoryResponse = {\r\n success: boolean;\r\n _error?: string;\r\n data?: {\r\n items: ScriptConfigHistoryEntry[];\r\n total: number;\r\n limit: number;\r\n offset: number;\r\n nextOffset?: number;\r\n };\r\n};\r\n\r\ntype NuiResponse<T> = {\r\n success: boolean;\r\n message?: string;\r\n _error?: string;\r\n meta?: ScriptConfigUpdateMeta<T>;\r\n};\r\n\r\n// Server-only \"sliver\" response (GET_SERVER_ONLY_SCRIPT_CONFIG). Identical\r\n// envelope to GET_FULL_SCRIPT_CONFIG — only the inner key differs (`serverOnly`\r\n// instead of `config`). `serverOnly` is the x-serverOnly subtree in the SAME\r\n// nested shape/paths as `config` (a deep object, NOT flat dot-paths); `{}` when\r\n// the schema declares no server-only fields.\r\nexport type ServerOnlyScriptConfigResponse<T> = {\r\n success: boolean;\r\n _error?: string;\r\n data?: { serverOnly: Partial<T>; clientVersion: number };\r\n};\r\n\r\n// Recursive deep-merge: returns a new object where `patch`'s plain-object\r\n// branches are merged INTO `base`'s, and non-object values (scalars, arrays)\r\n// from `patch` replace `base`. Used to top up the server-only sliver onto the\r\n// already-cached client-visible config WITHOUT the top-level spread-replace\r\n// `fetchScriptConfig` uses — a shallow spread would clobber sibling\r\n// client-visible keys that live under a shared parent section. By construction\r\n// the two views are disjoint (serverOnly is the set-complement of the\r\n// client-visible view), but we still deep-merge so a shared parent section keeps\r\n// both its client-visible and server-only children.\r\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\r\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\r\n}\r\n\r\nfunction deepMerge<T>(base: T, patch: Partial<T>): T {\r\n if (!isPlainObject(base) || !isPlainObject(patch)) {\r\n // patch wins for non-object values (arrays/scalars are replaced wholesale).\r\n return (patch as unknown as T) ?? base;\r\n }\r\n const out: Record<string, unknown> = { ...(base as Record<string, unknown>) };\r\n for (const key of Object.keys(patch as Record<string, unknown>)) {\r\n const patchVal = (patch as Record<string, unknown>)[key];\r\n const baseVal = out[key];\r\n out[key] = isPlainObject(baseVal) && isPlainObject(patchVal)\r\n ? deepMerge(baseVal, patchVal as Record<string, unknown>)\r\n : patchVal;\r\n }\r\n return out as T;\r\n}\r\n\r\n// Derive the set of changed TOP-LEVEL section keys from a useForm changed-set\r\n// of dotted paths (e.g. [\"stores.0.name\", \"basic.weightUnit\"] → {\"stores\",\"basic\"}).\r\n// CONSERVATIVE by design: any changed path under section X marks the WHOLE\r\n// section X as changed, so the caller sends its full current value. This is what\r\n// lets deletions inside a section (a removed array element) propagate, because\r\n// the server/client overwrite the section wholesale rather than deep-merging.\r\nfunction changedTopLevelSections(changedFields?: readonly string[]): string[] {\r\n if (!changedFields || changedFields.length === 0) return [];\r\n const sections = new Set<string>();\r\n for (const path of changedFields) {\r\n if (typeof path !== \"string\" || path.length === 0) continue;\r\n const dot = path.indexOf(\".\");\r\n sections.add(dot === -1 ? path : path.slice(0, dot));\r\n }\r\n return Array.from(sections);\r\n}\r\n\r\n// ── Singleton registry ────────────────────────────────────────────────────────\r\n\r\nexport interface ScriptConfigInstance<T = any> {\r\n store: { getState: () => T; setState: (partial: Partial<T> | ((prev: T) => T)) => void };\r\n updateConfig: (newConfig: Partial<T>, changedFields?: readonly string[]) => Promise<NuiResponse<T>>;\r\n resetConfig: () => Promise<{ success: boolean; _error?: string }>;\r\n getHistory: (params?: ScriptConfigHistoryRequest) => Promise<ScriptConfigHistoryResponse>;\r\n fetchConfig: () => Promise<T | null>;\r\n // Admin-only top-up: fetches the server-only sliver and DEEP-MERGES it onto\r\n // the already-cached client-visible store state. Returns the merged full\r\n // config, or null on failure / no-permission. The sliver is in-memory only —\r\n // it is NEVER persisted to KVP or any cache (KVP holds client-visible only).\r\n fetchServerOnly: () => Promise<T | null>;\r\n}\r\n\r\nlet _instance: ScriptConfigInstance | null = null;\r\n\r\nexport function getScriptConfigInstance<T = any>(): ScriptConfigInstance<T> {\r\n if (!_instance) throw new Error(\"[dirk-cfx-react] createScriptConfig must be called before using ConfigPanel\");\r\n return _instance as ScriptConfigInstance<T>;\r\n}\r\n\r\nexport function createScriptConfig<T>(defaultValue: T) {\r\n const store = create<T>(() => defaultValue);\r\n let clientVersion = 0;\r\n\r\n const useScriptConfigHooks = () => {\r\n useNuiEvent<{ config?: Partial<T>; clientVersion?: number; sectionReplace?: boolean }>(\r\n \"UPDATE_SCRIPT_CONFIG\",\r\n (data) => {\r\n if (!data) return;\r\n\r\n if (typeof data.clientVersion === \"number\") {\r\n clientVersion = data.clientVersion as number;\r\n }\r\n\r\n if (data.config && typeof data.config === \"object\") {\r\n // Both apply modes use the same top-level merge-by-replace:\r\n // - sectionReplace: `config` holds only the changed sections; spreading\r\n // them over `prev` wholesale-overwrites those keys and leaves every\r\n // other (unsent) section exactly as-is. We MUST NOT full-replace here\r\n // or the unsent sections would vanish from the UI.\r\n // - fullReplace / merge / initial: `config` holds the FULL config, so\r\n // the same spread effectively replaces every key.\r\n store.setState((prev) => ({ ...prev, ...(data.config as Partial<T>) }));\r\n }\r\n }\r\n );\r\n };\r\n\r\n const fetchScriptConfig = async (): Promise<T | null> => {\r\n try {\r\n const response = await fetchNui<{\r\n success: boolean;\r\n data?: { config: T; clientVersion: number };\r\n }>(\"GET_FULL_SCRIPT_CONFIG\");\r\n\r\n if (response?.success && response.data?.config) {\r\n store.setState(() => response.data!.config as T);\r\n if (typeof response.data.clientVersion === \"number\") {\r\n clientVersion = response.data.clientVersion;\r\n }\r\n return response.data.config;\r\n }\r\n } catch { /* fallback to current store state */ }\r\n return null;\r\n };\r\n\r\n // Option B top-up. Called ONLY by the admin editor on panel open. The\r\n // baseline (client-visible config) is already in the store — pushed by Lua\r\n // via UPDATE_SCRIPT_CONFIG (handled in useScriptConfigHooks above) and cached\r\n // in KVP on the Lua side. Here we fetch ONLY the server-only sliver\r\n // (permission-gated server-side on canEditScript) and DEEP-MERGE it onto that\r\n // baseline to form the full editor view. We deliberately do NOT spread-replace\r\n // (as fetchScriptConfig does) — that would clobber sibling client-visible keys\r\n // under a shared parent section.\r\n //\r\n // SECURITY: this sliver is admin-only and in-memory only. It is NEVER written\r\n // to KVP or any cache; it is re-fetched fresh on every panel open.\r\n const fetchServerOnlyScriptConfig = async (): Promise<T | null> => {\r\n try {\r\n const response = await fetchNui<ServerOnlyScriptConfigResponse<T>>(\r\n \"GET_SERVER_ONLY_SCRIPT_CONFIG\"\r\n );\r\n\r\n if (response?.success && response.data) {\r\n // clientVersion here matches the value already in the store from the\r\n // Lua push; keep it in sync (safe no-op if identical).\r\n if (typeof response.data.clientVersion === \"number\") {\r\n clientVersion = response.data.clientVersion;\r\n }\r\n const sliver = response.data.serverOnly;\r\n // An empty sliver ({} — schema declares no server-only paths) is a valid\r\n // no-op merge: deepMerge returns the baseline unchanged.\r\n const merged = deepMerge(store.getState() as T, (sliver ?? {}) as Partial<T>);\r\n store.setState(() => merged as T);\r\n return merged;\r\n }\r\n } catch { /* fall back to the client-visible baseline already in the store */ }\r\n return null;\r\n };\r\n\r\n // `changedFields` is the useForm tracked changed-set (dotted paths). When\r\n // supplied AND it resolves to ≥1 top-level section, we send a SECTION-DELTA:\r\n // only the changed sections, each as its WHOLE current value, with\r\n // sectionReplace:true. The server/client then overwrite those keys wholesale\r\n // (which is what lets deletions inside a section propagate). When omitted or\r\n // empty, we fall back to the legacy full-send (server defaults to\r\n // fullReplace=true when no `sectionReplace` flag is present).\r\n const updateScriptConfig = async (\r\n newConfig: Partial<T>,\r\n changedFields?: readonly string[]\r\n ): Promise<NuiResponse<T>> => {\r\n store.setState((prev) => ({ ...prev, ...newConfig }));\r\n\r\n const sections = changedTopLevelSections(changedFields);\r\n\r\n let payload: { data: Partial<T>; expectedVersion: number; sectionReplace?: boolean };\r\n if (sections.length > 0) {\r\n // Section-delta: flat object keyed by top-level section name → whole\r\n // current section value (pulled from the just-applied store state so it\r\n // reflects the user's edits, including deletions).\r\n const current = store.getState() as Record<string, unknown>;\r\n const delta: Record<string, unknown> = {};\r\n for (const key of sections) delta[key] = current[key];\r\n payload = { data: delta as Partial<T>, expectedVersion: clientVersion, sectionReplace: true };\r\n } else {\r\n // No changed-set provided (or nothing resolved) but the user explicitly\r\n // saved → legacy full-send. No sectionReplace flag ⇒ server fullReplace.\r\n payload = { data: newConfig, expectedVersion: clientVersion };\r\n }\r\n\r\n const response = await fetchNui<NuiResponse<T>>(\"UPDATE_SCRIPT_CONFIG\", payload);\r\n\r\n if (response?.meta?.client_version != null) {\r\n clientVersion = response.meta.client_version as number;\r\n }\r\n\r\n if (response?.success === false && response?.meta?.latestData) {\r\n store.setState((prev) => ({ ...prev, ...(response.meta!.latestData as Partial<T>) }));\r\n }\r\n\r\n return response;\r\n };\r\n\r\n const getScriptConfigHistory = async (\r\n params: ScriptConfigHistoryRequest = {}\r\n ): Promise<ScriptConfigHistoryResponse> => {\r\n return fetchNui<ScriptConfigHistoryResponse>('GET_SCRIPT_CONFIG_HISTORY', params);\r\n };\r\n\r\n const resetConfig = async (): Promise<{ success: boolean; _error?: string }> => {\r\n const response = await fetchNui<{ success: boolean; _error?: string }>('RESET_SCRIPT_CONFIG');\r\n if (response?.success) {\r\n // Option B: after a reset the server restores defaults and broadcasts a\r\n // fresh client-visible UPDATE_SCRIPT_CONFIG push (handled in\r\n // useScriptConfigHooks → store baseline). We only need to re-merge the\r\n // server-only sliver onto that refreshed baseline to rebuild the full\r\n // editor view — no GET_FULL_SCRIPT_CONFIG round-trip. fetchServerOnly\r\n // merges onto whatever client-visible baseline is currently in the store,\r\n // so it's safe regardless of broadcast/callback ordering.\r\n await fetchServerOnlyScriptConfig();\r\n }\r\n return response;\r\n };\r\n\r\n _instance = {\r\n store,\r\n updateConfig: updateScriptConfig,\r\n resetConfig,\r\n getHistory: getScriptConfigHistory,\r\n fetchConfig: fetchScriptConfig,\r\n fetchServerOnly: fetchServerOnlyScriptConfig,\r\n };\r\n\r\n return {store, updateScriptConfig, resetConfig, getScriptConfigHistory, useScriptConfigHooks, fetchScriptConfig, fetchServerOnlyScriptConfig}\r\n}\r\n"]}