diff --git a/comps/form/Form.tsx b/comps/form/Form.tsx index 0c238ce..1a4a53e 100755 --- a/comps/form/Form.tsx +++ b/comps/form/Form.tsx @@ -83,7 +83,7 @@ export const Form: FC = (props) => { fm.size.height = e.contentRect.height; fm.size.width = e.contentRect.width; - if (fm.status === "ready" && !isEditor) fm.status = "resizing"; + // if (fm.status === "ready" && !isEditor) fm.status = "resizing"; if (fm.props.layout === "auto") { if (fm.size.width > 650) { diff --git a/comps/list/TableList.tsx b/comps/list/TableList.tsx index ec941f6..fc65b03 100755 --- a/comps/list/TableList.tsx +++ b/comps/list/TableList.tsx @@ -27,6 +27,7 @@ import { getFilter } from "../filter/utils/get-filter"; import { Skeleton } from "../ui/skeleton"; import "./TableList.css"; import { sortTree } from "./utils/sort-tree"; +import { call_prasi_events } from "../../.."; type OnRowClick = (arg: { row: any; @@ -209,6 +210,9 @@ export const TableList: FC = ({ const orderBy = local.sort.orderBy || undefined; const where = filterWhere(filter_name, __props); + + call_prasi_events("tablelist", "where", [where]); + const load_args: any = { async reload() {}, orderBy, @@ -384,7 +388,6 @@ export const TableList: FC = ({ const width = parseInt(getProp(child, "width", {})); if (type === "checkbox") { const on_click = getProp(child, "opt__on_click", ""); - console.log({ on_click }); columns.push({ key, name, diff --git a/exports.tsx b/exports.tsx index 04af480..a9d9298 100755 --- a/exports.tsx +++ b/exports.tsx @@ -56,6 +56,7 @@ export { generateRelation } from "@/comps/form/gen/gen-rel"; export { genTableEdit } from "@/comps/form/gen/gen-table-edit"; export { generateMasterDetail } from "@/comps/md/gen/md-gen"; export { parseGenField } from "@/gen/utils"; + /** ETC */ export { filterModifier } from "@/comps/filter/utils/filter-modifier"; export { generateField } from "@/comps/form/gen/gen-field"; @@ -75,6 +76,8 @@ export { prasi_gen } from "@/gen/prasi_gen"; export { FormatValue } from "@/utils/format-value"; export { GetValue } from "@/utils/get-value"; export { password } from "@/utils/password"; +export { prasi_events, call_prasi_events } from "lib/utils/prasi-events"; + /** Session */ export { Login } from "@/preset/login/Login"; export { generateLogin } from "@/preset/login/utils/generate"; @@ -102,7 +105,6 @@ export { PanelHeader } from "@/comps/tab/parts/PanelHead"; /*Popup*/ export { Popup } from "@/comps/popup/PopUp"; -// Detail export { Detail } from "@/comps/custom/Detail"; export { ButtonUpload } from "@/preset/profile/ButtonUpload"; export { Profile } from "@/preset/profile/Profile"; diff --git a/gen/prop/gen_prop_fields.ts b/gen/prop/gen_prop_fields.ts index cad6832..d48d8b4 100755 --- a/gen/prop/gen_prop_fields.ts +++ b/gen/prop/gen_prop_fields.ts @@ -1,4 +1,4 @@ -import get from "lodash.get"; +import { get as kget, set as kset } from "@/utils/idb-keyval"; const single = {} as Record< string, @@ -30,15 +30,6 @@ const single = {} as Record< } >; -const load_single = async (table: string) => { - if (!single[table]) { - single[table] = { - cols: await db._schema.columns(table as any), - rels: await db._schema.rels(table as any), - }; - } - return single[table]; -}; export const gen_prop_fields = async (gen_table: string, depth?: number) => { if (typeof db === "undefined") return ["- No Database -"]; @@ -49,127 +40,33 @@ export const gen_prop_fields = async (gen_table: string, depth?: number) => { } catch (e) { id_site = window.location.hostname; } - const schema = getSchemaOnStorage(id_site, gen_table); -if (!schema) { - console.log({depth}) - const result = await load_layer_schema( - typeof depth === "undefined" ? 3 : depth, - {}, - gen_table - ); - // const { cols, rels } = await load_single(gen_table); - // if (cols) { - // for (const [k, v] of Object.entries(cols) as any) { - // result.push({ - // value: JSON.stringify({ - // name: k, - // is_pk: v.is_pk, - // type: v.db_type || v.type, - // optional: v.optional, - // }), - // label: k, - // checked: v.is_pk, - // }); - // } - // } - // if (rels) { - // for (const [k, v] of Object.entries(rels)) { - // let options = [] as any; - // const to = v.to; - // const from = v.from; - // const parent_name = k; - // const parent_rel = v; - // if (to) { - // const { cols, rels } = await load_single(to.table); - // // if (cols) { - // // for (const [k, v] of Object.entries(cols)) { - // // options.push({ - // // value: JSON.stringify({ - // // name: k, - // // is_pk: v.is_pk, - // // type: v.db_type || v.type, - // // optional: v.optional, - // // }), - // // label: k, - // // checked: v.is_pk, - // // }); - // // } - // // } - // // if (rels) { - // // for (const [k, v] of Object.entries(rels)) { - // // let sub_opt = []; - // // const to = v.to; - // // const from = v.from; - // // const { cols } = await load_single(v.to.table); - // // for (const [k, v] of Object.entries(cols)) { - // // sub_opt.push({ - // // value: JSON.stringify({ - // // name: k, - // // is_pk: v.is_pk, - // // type: v.db_type || v.type, - // // optional: v.optional, - // // }), - // // label: k, - // // checked: v.is_pk, - // // }); - // // } - // // options.push({ - // // value: JSON.stringify({ - // // name: k, - // // is_pk: false, - // // type: v.type, - // // optional: true, - // // relation: { from, to }, - // // }), - // // label: k, - // // options: sub_opt, - // // checked: - // // parent_rel.type === "has-many" && - // // parent_rel.from.table === v.to.table, - // // }); - // // } - // // } - // } - // result.push({ - // value: JSON.stringify({ - // name: k, - // is_pk: false, - // type: v.type, - // optional: true, - // relation: { from, to }, - // }), - // label: k, - // options, - // }); - // } - // } - try { - saveSchemaOnStorage(result, id_site, gen_table); - } catch (e: any) { - console.error(e.message); - } - console.log({result}) - return result; - } else { - console.log({schema}) - return schema; - } + return await loadSchemaLayer( + id_site, + typeof depth === "undefined" ? 3 : depth, + {}, + gen_table + ); }; -const load_layer_schema = async (depth: number, arg: any, table: string) => { + +const loadSchemaLayer = async ( + id_site: string, + depth: number, + arg: any, + table: string +) => { let current_depth = 1; - console.log({ depth, current_depth, arg, table }); - const result = await get_layer(depth, current_depth, arg, table); - console.log({ result }); + const result = await get_layer(id_site, depth, current_depth, arg, table); return result; }; + const get_layer = async ( + id_site: string, depth: number, current: number, arg: any, table: string ) => { - const { cols, rels } = await load_single(table); - console.log({cols, rels , table}) + const { cols, rels } = await loadSingle(id_site, table); const options = []; if (cols) { for (const [k, v] of Object.entries(cols)) { @@ -192,6 +89,7 @@ const get_layer = async ( const to = v.to; const from = v.from; const r_rels = (await get_layer( + id_site, depth, current + 1, arg, @@ -214,32 +112,39 @@ const get_layer = async ( } return options; }; -const saveSchemaOnStorage = (res: any, id_site: string, table: string) => { - let schemaSite = null; - let schema_master_detail: Record = {}; - const keys = `schema-md-${id_site}`; - try { - let smd = localStorage.getItem(keys) as string; - schemaSite = JSON.parse(smd); - } catch (error) {} - try { - schema_master_detail = { - ...schemaSite, - [table]: JSON.stringify(res), - }; - localStorage.setItem(keys, JSON.stringify(schema_master_detail)); - } catch (e: any) { - console.error(e.message); + +const loadSingle = async (id_site: string, table: string) => { + const ls_key = `schema-md-${id_site}`; + const idb_key = `${id_site}-${table}`; + let cached_raw = localStorage.getItem(ls_key); + let cached_keys: string[] = []; + if (cached_raw) { + try { + let res = JSON.parse(cached_raw); + if (!Array.isArray(res)) { + localStorage.setItem(ls_key, JSON.stringify([])); + res = []; + } + cached_keys = res; + } catch (e) {} } -}; -const getSchemaOnStorage = (id_site: string, table: string) => { - const keys = `schema-md-${id_site}`; - let schemaSite = null; - try { - let smd = localStorage.getItem(keys) as string; - schemaSite = JSON.parse(smd); - } catch (error) {} - const schema = get(schemaSite, `${table}`); - if (schema) return JSON.parse(schema); - return null; + + let cached = null; + if (cached_keys.includes(table)) { + cached = await kget(idb_key); + } + + if (!single[table]) { + if (cached) { + single[table] = cached; + } else { + single[table] = { + cols: await db._schema.columns(table as any), + rels: await db._schema.rels(table as any), + }; + await kset(idb_key, single[table]); + localStorage.setItem(ls_key, JSON.stringify([...cached_keys, table])); + } + } + return single[table]; }; diff --git a/preset/menu/Menu.tsx b/preset/menu/Menu.tsx index 5569816..bb6f8df 100755 --- a/preset/menu/Menu.tsx +++ b/preset/menu/Menu.tsx @@ -40,7 +40,7 @@ export const Menu: FC = (props) => { return (
-
+
( + request: IDBRequest | IDBTransaction +): Promise { + return new Promise((resolve, reject) => { + // @ts-ignore - file size hacks + request.oncomplete = request.onsuccess = () => resolve(request.result); + // @ts-ignore - file size hacks + request.onabort = request.onerror = () => reject(request.error); + }); +} + +export function createStore(dbName: string, storeName: string): UseStore { + const request = indexedDB.open(dbName); + request.onupgradeneeded = () => request.result.createObjectStore(storeName); + const dbp = promisifyRequest(request); + + return (txMode, callback) => + dbp.then((db) => + callback(db.transaction(storeName, txMode).objectStore(storeName)) + ); +} + +export type UseStore = ( + txMode: IDBTransactionMode, + callback: (store: IDBObjectStore) => T | PromiseLike +) => Promise; + +let defaultGetStoreFunc: UseStore | undefined; + +function defaultGetStore() { + if (!defaultGetStoreFunc) { + defaultGetStoreFunc = createStore("keyval-store", "keyval"); + } + return defaultGetStoreFunc; +} + +/** + * Get a value by its key. + * + * @param key + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function get( + key: IDBValidKey, + customStore = defaultGetStore() +): Promise { + return customStore("readonly", (store) => promisifyRequest(store.get(key))); +} + +/** + * Set a value with a key. + * + * @param key + * @param value + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function set( + key: IDBValidKey, + value: any, + customStore = defaultGetStore() +): Promise { + return customStore("readwrite", (store) => { + store.put(value, key); + return promisifyRequest(store.transaction); + }); +} + +/** + * Set multiple values at once. This is faster than calling set() multiple times. + * It's also atomic – if one of the pairs can't be added, none will be added. + * + * @param entries Array of entries, where each entry is an array of `[key, value]`. + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function setMany( + entries: [IDBValidKey, any][], + customStore = defaultGetStore() +): Promise { + return customStore("readwrite", (store) => { + entries.forEach((entry) => store.put(entry[1], entry[0])); + return promisifyRequest(store.transaction); + }); +} + +/** + * Get multiple values by their keys + * + * @param keys + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function getMany( + keys: IDBValidKey[], + customStore = defaultGetStore() +): Promise { + return customStore("readonly", (store) => + Promise.all(keys.map((key) => promisifyRequest(store.get(key)))) + ); +} + +/** + * Update a value. This lets you see the old value and update it as an atomic operation. + * + * @param key + * @param updater A callback that takes the old value and returns a new value. + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function update( + key: IDBValidKey, + updater: (oldValue: T | undefined) => T, + customStore = defaultGetStore() +): Promise { + return customStore( + "readwrite", + (store) => + // Need to create the promise manually. + // If I try to chain promises, the transaction closes in browsers + // that use a promise polyfill (IE10/11). + new Promise((resolve, reject) => { + store.get(key).onsuccess = function () { + try { + store.put(updater(this.result), key); + resolve(promisifyRequest(store.transaction)); + } catch (err) { + reject(err); + } + }; + }) + ); +} + +/** + * Delete a particular key from the store. + * + * @param key + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function del( + key: IDBValidKey, + customStore = defaultGetStore() +): Promise { + return customStore("readwrite", (store) => { + store.delete(key); + return promisifyRequest(store.transaction); + }); +} + +/** + * Delete multiple keys at once. + * + * @param keys List of keys to delete. + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function delMany( + keys: IDBValidKey[], + customStore = defaultGetStore() +): Promise { + return customStore("readwrite", (store: IDBObjectStore) => { + keys.forEach((key: IDBValidKey) => store.delete(key)); + return promisifyRequest(store.transaction); + }); +} + +/** + * Clear all values in the store. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function clear(customStore = defaultGetStore()): Promise { + return customStore("readwrite", (store) => { + store.clear(); + return promisifyRequest(store.transaction); + }); +} + +function eachCursor( + store: IDBObjectStore, + callback: (cursor: IDBCursorWithValue) => void +): Promise { + store.openCursor().onsuccess = function () { + if (!this.result) return; + callback(this.result); + this.result.continue(); + }; + return promisifyRequest(store.transaction); +} + +/** + * Get all keys in the store. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function keys( + customStore = defaultGetStore() +): Promise { + return customStore("readonly", (store) => { + // Fast path for modern browsers + if (store.getAllKeys) { + return promisifyRequest( + store.getAllKeys() as unknown as IDBRequest + ); + } + + const items: KeyType[] = []; + + return eachCursor(store, (cursor) => + items.push(cursor.key as KeyType) + ).then(() => items); + }); +} + +/** + * Get all values in the store. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function values(customStore = defaultGetStore()): Promise { + return customStore("readonly", (store) => { + // Fast path for modern browsers + if (store.getAll) { + return promisifyRequest(store.getAll() as IDBRequest); + } + + const items: T[] = []; + + return eachCursor(store, (cursor) => items.push(cursor.value as T)).then( + () => items + ); + }); +} + +/** + * Get all entries in the store. Each entry is an array of `[key, value]`. + * + * @param customStore Method to get a custom store. Use with caution (see the docs). + */ +export function entries( + customStore = defaultGetStore() +): Promise<[KeyType, ValueType][]> { + return customStore("readonly", (store) => { + // Fast path for modern browsers + // (although, hopefully we'll get a simpler path some day) + if (store.getAll && store.getAllKeys) { + return Promise.all([ + promisifyRequest( + store.getAllKeys() as unknown as IDBRequest + ), + promisifyRequest(store.getAll() as IDBRequest), + ]).then(([keys, values]) => keys.map((key, i) => [key, values[i]])); + } + + const items: [KeyType, ValueType][] = []; + + return customStore("readonly", (store) => + eachCursor(store, (cursor) => + items.push([cursor.key as KeyType, cursor.value]) + ).then(() => items) + ); + }); +} diff --git a/utils/prasi-events.ts b/utils/prasi-events.ts new file mode 100755 index 0000000..d851aea --- /dev/null +++ b/utils/prasi-events.ts @@ -0,0 +1,39 @@ +import { FMLocal } from "../.."; + +const events = { + form: { + where: async (fm: FMLocal, where: any) => {}, + before_update: async (fm: FMLocal) => {}, + after_update: async (fm: FMLocal) => {}, + before_insert: async (fm: FMLocal) => {}, + after_insert: async (fm: FMLocal) => {}, + before_load: async (fm: FMLocal) => {}, + after_load: async (fm: FMLocal) => {}, + }, + tablelist: { where: async (where: any) => {} }, +}; + +let w = null as any; +if (typeof window !== "undefined") { + w = window; + if (!w.prasi_events) { + w.prasi_events = events; + } +} + +type PRASI_EVENT = typeof events; +export const prasi_events: PRASI_EVENT = w.prasi_events; +export const call_prasi_events = async < + K extends keyof PRASI_EVENT, + L extends keyof PRASI_EVENT[K] +>( + k: K, + l: L, + args: any[] +) => { + const fn = prasi_events?.[k]?.[l] as any; + + if (fn) { + await fn(...args); + } +};