From b49fadae833aabf5d497a742109cdbf0fe361322 Mon Sep 17 00:00:00 2001 From: Hein Date: Thu, 23 Oct 2025 15:49:14 +0200 Subject: [PATCH] docs(changeset): Extra api options, local data options --- .changeset/nasty-bottles-ask.md | 5 + src/Gridler/GridlerDataGrid.tsx | 14 +- src/Gridler/components/GridlerStore.tsx | 9 +- src/Gridler/components/RightMenuIcon.tsx | 2 +- .../adaptors/GlidlerAPIAdaptorForGoLangv2.tsx | 21 +- .../adaptors/GlidlerFormAdaptor.tsx | 6 +- .../adaptors/GlidlerLocalDataAdaptor.tsx | 54 +++- src/Gridler/index.ts | 4 +- src/Gridler/stories/Examples.goapi.tsx | 9 +- src/Gridler/utils/golang-restapi-v2/index.ts | 270 ++++++++++++++++++ src/Gridler/utils/golang-restapi-v2/types.ts | 81 ------ src/Gridler/utils/index.ts | 1 + 12 files changed, 369 insertions(+), 107 deletions(-) create mode 100644 .changeset/nasty-bottles-ask.md create mode 100644 src/Gridler/utils/golang-restapi-v2/index.ts delete mode 100644 src/Gridler/utils/golang-restapi-v2/types.ts diff --git a/.changeset/nasty-bottles-ask.md b/.changeset/nasty-bottles-ask.md new file mode 100644 index 0000000..7b29010 --- /dev/null +++ b/.changeset/nasty-bottles-ask.md @@ -0,0 +1,5 @@ +--- +'@warkypublic/oranguru': patch +--- + +Extra api options, local data options diff --git a/src/Gridler/GridlerDataGrid.tsx b/src/Gridler/GridlerDataGrid.tsx index bf5ac1a..9157d83 100644 --- a/src/Gridler/GridlerDataGrid.tsx +++ b/src/Gridler/GridlerDataGrid.tsx @@ -13,7 +13,7 @@ import { BottomBar } from './components/BottomBar'; import { Computer } from './components/Computer'; import { useGridlerStore } from './components/GridlerStore'; import { Pager } from './components/Pager'; -import { RightMenuIcon } from './components/RightMenuIcon'; +import { GridlerRightMenuIcon } from './components/RightMenuIcon'; import { SortSprite } from './components/sprites/Sort'; import { SortDownSprite } from './components/sprites/SortDown'; import { SortUpSprite } from './components/sprites/SortUp'; @@ -237,11 +237,13 @@ export const GridlerDataGrid = () => { onVisibleRegionChanged={onVisibleRegionChanged} ref={refMerged as React.Ref} rightElement={ - - {sections?.rightElementStart} - - {sections?.rightElementEnd} - + sections?.rightElementDisabled ? undefined : ( + + {sections?.rightElementStart} + + {sections?.rightElementEnd} + + ) } rowHeight={rowHeight ?? 22} //rowMarkersCheckboxStyle='square' diff --git a/src/Gridler/components/GridlerStore.tsx b/src/Gridler/components/GridlerStore.tsx index 1820416..483ab93 100644 --- a/src/Gridler/components/GridlerStore.tsx +++ b/src/Gridler/components/GridlerStore.tsx @@ -93,6 +93,7 @@ export interface GridlerProps extends PropsWithChildren { bottom?: React.ReactNode; left?: React.ReactNode; right?: React.ReactNode; + rightElementDisabled?: boolean; rightElementEnd?: React.ReactNode; rightElementStart?: React.ReactNode; top?: React.ReactNode; @@ -100,6 +101,7 @@ export interface GridlerProps extends PropsWithChildren { selectedRow?: number; selectMode?: 'cell' | 'row'; showMenu?: (id: string, options?: Partial) => void; + title?: string; tooltipBarProps?: React.HTMLAttributes; total_rows?: number; uniqueid: string; @@ -426,13 +428,10 @@ const { Provider, useStore: useGridlerStore } = createSyncStore, title: s.title ?? 'Grid' }] : coldef ? [ + { leftSection: , title: s.title ?? 'Grid' }, { items: [ { diff --git a/src/Gridler/components/RightMenuIcon.tsx b/src/Gridler/components/RightMenuIcon.tsx index b71a8f0..62f1e22 100644 --- a/src/Gridler/components/RightMenuIcon.tsx +++ b/src/Gridler/components/RightMenuIcon.tsx @@ -3,7 +3,7 @@ import { IconMenu2 } from '@tabler/icons-react'; import { useGridlerStore } from './GridlerStore'; -export function RightMenuIcon() { +export function GridlerRightMenuIcon() { const { loadingData, onContextClick } = useGridlerStore((s) => ({ loadingData: s.loadingData, onContextClick: s.onContextClick, diff --git a/src/Gridler/components/adaptors/GlidlerAPIAdaptorForGoLangv2.tsx b/src/Gridler/components/adaptors/GlidlerAPIAdaptorForGoLangv2.tsx index eb8672f..065ff8b 100644 --- a/src/Gridler/components/adaptors/GlidlerAPIAdaptorForGoLangv2.tsx +++ b/src/Gridler/components/adaptors/GlidlerAPIAdaptorForGoLangv2.tsx @@ -3,10 +3,15 @@ import React, { useEffect } from 'react'; import type { APIOptions } from '../../utils/types'; +import { GoAPIHeaders, type GoAPIOperation } from '../../utils/golang-restapi-v2'; import { useGridlerStore } from '../GridlerStore'; -//The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders. -export const GlidlerAPIAdaptorForGoLangv2 = React.memo((props: APIOptions) => { +export interface GlidlerAPIAdaptorForGoLangv2Props extends APIOptions { + initialData?: Array; + options?: Array; +} + +function _GlidlerAPIAdaptorForGoLangv2(props: GlidlerAPIAdaptorForGoLangv2Props) { const [setStateFN, setState, getState, addError, mounted] = useGridlerStore((s) => [ s.setStateFN, s.setState, @@ -49,6 +54,13 @@ export const GlidlerAPIAdaptorForGoLangv2 = React.memo((props: APIOptions) => { }); } + if (props.options && props.options.length > 0) { + const optionHeaders = GoAPIHeaders(props.options); + for (const oh in optionHeaders) { + head.set(oh, optionHeaders[oh]); + } + } + const currentRequestIndex = _active_requests?.findIndex((f) => f.page === index) ?? -1; _active_requests?.forEach((r) => { if ((r.page >= 0 && r.page < index - 2) || (index >= 0 && r.page > index + 2)) { @@ -142,6 +154,9 @@ export const GlidlerAPIAdaptorForGoLangv2 = React.memo((props: APIOptions) => { }, [props.url, props.authtoken, mounted, setState]); return <>; -}); +} + +//The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders. +export const GlidlerAPIAdaptorForGoLangv2 = React.memo(_GlidlerAPIAdaptorForGoLangv2); GlidlerAPIAdaptorForGoLangv2.displayName = 'Gridler-GlidlerAPIAdaptorForGoLangv2'; diff --git a/src/Gridler/components/adaptors/GlidlerFormAdaptor.tsx b/src/Gridler/components/adaptors/GlidlerFormAdaptor.tsx index 1c407a8..764fb9f 100644 --- a/src/Gridler/components/adaptors/GlidlerFormAdaptor.tsx +++ b/src/Gridler/components/adaptors/GlidlerFormAdaptor.tsx @@ -45,9 +45,9 @@ export function GlidlerFormAdaptor(props: { } const items = [] as Array; - if (defaultItems && id === 'cell') { - items.push(...(defaultItems as Array)); - } + + items.push(...(defaultItems as Array)); + const rows = getState('_gridSelection')?.rows.toArray() ?? []; const manyRows = rows.length > 1; diff --git a/src/Gridler/components/adaptors/GlidlerLocalDataAdaptor.tsx b/src/Gridler/components/adaptors/GlidlerLocalDataAdaptor.tsx index be61bff..fbc5214 100644 --- a/src/Gridler/components/adaptors/GlidlerLocalDataAdaptor.tsx +++ b/src/Gridler/components/adaptors/GlidlerLocalDataAdaptor.tsx @@ -1,15 +1,40 @@ import React, { useEffect } from 'react'; -import { useGridlerStore } from '../GridlerStore'; +import type { GridlerColumns } from '../Column'; -export interface GlidlerLocalDataAdaptorProps { - data: Array; +import { type FilterOption, type SortOption, useGridlerStore } from '../GridlerStore'; + +export interface GlidlerLocalDataAdaptorProps { + data: Array; + onColumnFilter?: ( + colFilters: Array | undefined, + cols: GridlerColumns | undefined, + data: Array + ) => Array; + onColumnSort?: ( + colSort: Array | undefined, + cols: GridlerColumns | undefined, + data: Array + ) => Array; } //The computer component does not need to be recalculated on every render, so we use React.memo to prevent unnecessary re-renders. -export const GlidlerLocalDataAdaptor = React.memo((props: GlidlerLocalDataAdaptorProps) => { +function _GlidlerLocalDataAdaptor(props: GlidlerLocalDataAdaptorProps) { const [setState, getState, mounted] = useGridlerStore((s) => [s.setState, s.getState, s.mounted]); + const { colFilters, colSort, columns } = useGridlerStore((s) => ({ + colFilters: s.colFilters, + colOrder: s.colOrder, + colSize: s.colSize, + colSort: s.colSort, + columns: s.columns, + })); + + const refChanged = React.useRef({ + colFilters: colFilters, + colSort: colSort, + }); + const useAPIQuery: (index: number) => Promise = async (index: number) => { const pageSize = getState('pageSize'); @@ -25,7 +50,26 @@ export const GlidlerLocalDataAdaptor = React.memo((props: GlidlerLocalDataAdapto setState('useAPIQuery', useAPIQuery); }, [mounted, setState]); + useEffect(() => { + if (props.onColumnSort && colSort !== refChanged?.current?.colSort) { + const sortedData = props.onColumnSort(colSort, columns, props.data as Array); + setState('total_rows', sortedData.length); + setState('data', sortedData); + refChanged.current.colSort = colSort; + } + }, [colSort, props.onColumnSort]); + + useEffect(() => { + if (props.onColumnFilter && colFilters !== refChanged?.current?.colFilters) { + const filteredData = props.onColumnFilter(colFilters, columns, props.data as Array); + setState('total_rows', filteredData.length); + setState('data', filteredData); + refChanged.current.colFilters = colFilters; + } + }, [colFilters, props.onColumnFilter]); + return <>; -}); +} +export const GlidlerLocalDataAdaptor = React.memo(_GlidlerLocalDataAdaptor); GlidlerLocalDataAdaptor.displayName = 'Gridler-GlidlerLocalDataAdaptor'; diff --git a/src/Gridler/index.ts b/src/Gridler/index.ts index aecf9f1..dc603ef 100644 --- a/src/Gridler/index.ts +++ b/src/Gridler/index.ts @@ -3,4 +3,6 @@ export {GlidlerFormAdaptor } from './components/adaptors/GlidlerFormAdaptor' export {GlidlerLocalDataAdaptor } from './components/adaptors/GlidlerLocalDataAdaptor' export * from './components/Column' export {useGridlerStore } from './components/GridlerStore' -export {Gridler} from './Gridler' \ No newline at end of file +export { GridlerRightMenuIcon } from './components/RightMenuIcon' +export {Gridler} from './Gridler' +export * from './utils' \ No newline at end of file diff --git a/src/Gridler/stories/Examples.goapi.tsx b/src/Gridler/stories/Examples.goapi.tsx index 0bf2659..8f04e5e 100644 --- a/src/Gridler/stories/Examples.goapi.tsx +++ b/src/Gridler/stories/Examples.goapi.tsx @@ -106,13 +106,18 @@ export const GridlerGoAPIExampleEventlog = () => { //console.log('GridlerGoAPIExampleEventlog onChange', v); setValues(v); }} - sections={sections} + sections={{ ...sections, rightElementDisabled: false }} selectedRow={selectRow ? parseInt(selectRow, 10) : undefined} selectMode="row" + title="Go API Example" uniqueid="gridtest" values={values} > - + { diff --git a/src/Gridler/utils/golang-restapi-v2/index.ts b/src/Gridler/utils/golang-restapi-v2/index.ts new file mode 100644 index 0000000..b4c09ec --- /dev/null +++ b/src/Gridler/utils/golang-restapi-v2/index.ts @@ -0,0 +1,270 @@ +import {b64EncodeUnicode} from '@warkypublic/artemis-kit/base64' +const TOKEN_KEY = 'gridler_golang_restapi_v2_token' + +export type APIOptionsType = { + autocreate?: boolean + autoref?: boolean + baseurl?: string + getAPIProvider?: () => { provider: string; providerKey: string } + getAuthToken?: () => string + operations?: Array + postfix?: string + prefix?: string + requestTimeoutSec?: number +} + + + +export interface APIResponse { + errmsg: string + payload?: any + retval: number +} +export interface FetchAPIOperation { + name?: string + op?: string + type: GoAPIHeaderTypes //x-fieldfilter + value: string +} +/** + * @description Types for the Go Rest API headers + * @typedef {String} GoAPIEnum +*/ +export type GoAPIEnum = + | 'advsql' + | 'api-key' + | 'api-range-from' + | 'api-range-size' + | 'api-range-total' + | 'api-src' + | 'api' + | 'association_autocreate' + | 'association_autoupdate' + | 'association-update' + | 'cql-sel' + | 'cursor-backward'// For x cursor-backward header + | 'cursor-forward' // For x cursor-forward header + | 'custom-sql-join' + | 'custom-sql-or' + | 'custom-sql-w' + | 'detailapi' + | 'distinct' + | 'expand' + | 'fetch-rownumber' + | 'fieldfilter' + | 'fieldfilter' + | 'files' //For x files header + | 'func' + | 'limit' + | 'no-return' + | 'not-select-fields' + | 'offset' + | 'parm' + | 'pkrow' + | 'preload' + | 'searchand' + | 'searchfilter' + | 'searchfilter' + | 'searchop' + | 'searchop' + | 'searchor' + | 'select-fields' + | 'simpleapi' + | 'skipcache' + | 'skipcount' + | 'sort' + + +export type GoAPIHeaderKeys = `x-${GoAPIEnum}` + + +export type GoAPIHeaderTypes = GoAPIEnum & string + + +export interface GoAPIOperation { + name?: string + op?: string + type: GoAPIHeaderTypes //x-fieldfilter + value: string +} +export interface MetaData { + limit?: number + offset?: number + total?: number +} + + +/** + * Builds an array of objects by encoding specific values and setting headers. + * + * @param {Array} ops - The array of FetchAPIOperation objects to be built. + * @param {Headers} [headers] - Optional headers to be set. + * @return {Array} - The built array of FetchAPIOperation objects. + */ +const buildGoAPIOperation = ( + ops: Array, + headers?: Headers +): Array => { + const newops = [...ops.filter((i) => i !== undefined && i.type !== undefined)] + + + for (let i = 0; i < newops.length; i++) { + if (!newops[i].name || newops[i].name === '') { + newops[i].name = '' + } + if (newops[i].type === 'files' && !newops[i].value.startsWith('__')) { + newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__` + } + if (newops[i].type === 'advsql' && !newops[i].value.startsWith('__')) { + newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__` + } + + if (newops[i].type === 'custom-sql-or' && !newops[i].value.startsWith('__')) { + newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__` + } + + if (newops[i].type === 'custom-sql-join' && !newops[i].value.startsWith('__')) { + newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__` + } + if (newops[i].type === 'not-select-fields' && !newops[i].value.startsWith('__')) { + newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__` + } + if (newops[i].type === 'custom-sql-w' && !newops[i].value.startsWith('__')) { + newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__` + } + if (newops[i].type === 'select-fields' && !newops[i].value.startsWith('__')) { + newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__` + } + if (newops[i].type === 'cql-sel' && !newops[i].value.startsWith('__')) { + newops[i].value = `__${b64EncodeUnicode(newops[i].value)}__` + } + + if (headers) { + if (!newops || newops.length === 0) { + headers.set(`x-limit`, '10') + } + + if (newops[i].type === 'association_autoupdate') { + headers.set(`association_autoupdate`, newops[i].value ?? '1') + } + if (newops[i].type === 'association_autocreate') { + headers.set(`association_autocreate`, newops[i].value ?? '1') + } + if ( + newops[i].type === 'searchop' || + newops[i].type === 'searchor' || + newops[i].type === 'searchand' + ) { + headers.set( + encodeURIComponent(`x-${newops[i].type}-${newops[i].op}-${newops[i].name}`), + String(newops[i].value) + ) + } else { + headers.set( + encodeURIComponent( + `x-${newops[i].type}${newops[i].name && newops[i].name !== '' ? '-' + newops[i].name : ''}` + ), + String(newops[i].value) + ) + } + } + } + + return newops +} + +/** + * Retrieves the headers from an array of FetchAPIOperation objects and returns them as an object. + * + * @param {Array} ops - The array of FetchAPIOperation objects. + * @return {{ [key: string]: string }} - The headers as an object with string keys and string values. + */ +const GoAPIHeaders = ( + ops: Array, + headers?: Headers +): { [key: string]: string } => { + const head = new Headers() + const headerlist: Record = {} + + const authToken = getAuthToken?.() + if (authToken && authToken !== '') { + + head.set('Authorization', `Token ${authToken}`) + } else { + const token = getAuthToken() + if (token) { + head.set('Authorization', `Token ${token}`) + } + } + + + if (headers) { + headers.forEach((v, k) => { + head.set(k, v) + }) + } + const distinctOperations: Array = [] + + for (const value of ops?.filter((val) => !!val) ?? []) { + const index = distinctOperations.findIndex( + (searchValue) => searchValue.name === value.name && searchValue.type === value.type + ) + if (index === -1) { + distinctOperations.push(value) + } else { + distinctOperations[index] = value + } + } + + buildGoAPIOperation(distinctOperations, head) + + head?.forEach((v, k) => { + headerlist[k] = v + }) + + if (headers) { + for (const key of Object.keys(headerlist)) { + headers.set(key, headerlist[key]) + } + } + + return headerlist +} + +const callbacks = { + getAuthToken: () => { + if (localStorage) { + const token = localStorage.getItem(TOKEN_KEY) + if (token) { + return token + } + } + return undefined + } +} + +/** + * Retrieves the authentication token from local storage. + * + * @return {string | undefined} The authentication token if found, otherwise undefined + */ +const getAuthToken = () => callbacks?.getAuthToken?.() + +const setAuthTokenCallback = (cb: ()=> string) => { + callbacks.getAuthToken = cb + return callbacks.getAuthToken +} + +/** + * Sets the authentication token in the local storage. + * + * @param {string} token - The authentication token to be set. + */ +const setAuthToken = (token: string) => { + if (localStorage) { + localStorage.setItem(TOKEN_KEY, token) + } +} + + +export {buildGoAPIOperation,getAuthToken,GoAPIHeaders,setAuthToken,setAuthTokenCallback} \ No newline at end of file diff --git a/src/Gridler/utils/golang-restapi-v2/types.ts b/src/Gridler/utils/golang-restapi-v2/types.ts deleted file mode 100644 index 2af6ae7..0000000 --- a/src/Gridler/utils/golang-restapi-v2/types.ts +++ /dev/null @@ -1,81 +0,0 @@ -export type APIOptionsType = { - autocreate?: boolean - autoref?: boolean - baseurl?: string - getAPIProvider?: () => { provider: string; providerKey: string } - getAuthToken?: () => string - operations?: Array - postfix?: string - prefix?: string - requestTimeoutSec?: number -} - - - -export interface APIResponse { - errmsg: string - payload?: any - retval: number -} -export interface FetchAPIOperation { - name?: string - op?: string - type: FetchOpTypes //x-fieldfilter - value: string -} -export type FetchOpTypes = GoAPIEnum & string - - -/** - * @description Types for the Go Rest API headers - * @typedef {String} GoAPIEnum -*/ -export type GoAPIEnum = 'advsql' - | 'api-key' - | 'api-range-from' - | 'api-range-size' - | 'api-range-total' - | 'api-src' - | 'api' - | 'association_autocreate' - | 'association_autoupdate' - | 'association-update' - | 'cql-sel' - | 'custom-sql-join' - | 'custom-sql-or' - | 'custom-sql-w' - | 'detailapi' - | 'distinct' - | 'expand' - | 'fetch-rownumber' - | 'fieldfilter' - | 'fieldfilter' - | 'func' - | 'limit' - | 'no-return' - | 'not-select-fields' - | 'offset' - | 'parm' - | 'pkrow' - | 'preload' - | 'searchfilter' - | 'searchfilter' - | 'searchop' - | 'searchop' - | 'select-fields' - | 'simpleapi' - | 'skipcache' - | 'skipcount' - | 'sort' - - -export type GoAPIHeaderKeys = `x-${GoAPIEnum}` - - -export type MetaCallback = (data: MetaData) => void - -export interface MetaData { - limit?: number - offset?: number - total?: number -} diff --git a/src/Gridler/utils/index.ts b/src/Gridler/utils/index.ts index e69de29..7a7efe1 100644 --- a/src/Gridler/utils/index.ts +++ b/src/Gridler/utils/index.ts @@ -0,0 +1 @@ +export {type APIOptionsType,type FetchAPIOperation,GoAPIHeaders} from './golang-restapi-v2' \ No newline at end of file