docs(changeset): Extra api options, local data options

This commit is contained in:
Hein 2025-10-23 15:49:14 +02:00
parent 9506d123f3
commit b49fadae83
12 changed files with 369 additions and 107 deletions

View File

@ -0,0 +1,5 @@
---
'@warkypublic/oranguru': patch
---
Extra api options, local data options

View File

@ -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<DataEditorRef>}
rightElement={
<Group>
{sections?.rightElementStart}
<RightMenuIcon />
{sections?.rightElementEnd}
</Group>
sections?.rightElementDisabled ? undefined : (
<Group>
{sections?.rightElementStart}
<GridlerRightMenuIcon />
{sections?.rightElementEnd}
</Group>
)
}
rowHeight={rowHeight ?? 22}
//rowMarkersCheckboxStyle='square'

View File

@ -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<MantineBetterMenuInstance>) => void;
title?: string;
tooltipBarProps?: React.HTMLAttributes<HTMLDivElement>;
total_rows?: number;
uniqueid: string;
@ -426,13 +428,10 @@ const { Provider, useStore: useGridlerStore } = createSyncStore<GridlerStoreStat
const items =
area === 'menu'
? [
{
label: `Side menu`,
},
]
? [{ leftSection: <IconGrid4x4 size={16} />, title: s.title ?? 'Grid' }]
: coldef
? [
{ leftSection: <IconGrid4x4 size={16} />, title: s.title ?? 'Grid' },
{
items: [
{

View File

@ -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,

View File

@ -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<T = unknown> extends APIOptions {
initialData?: Array<T>;
options?: Array<GoAPIOperation>;
}
function _GlidlerAPIAdaptorForGoLangv2<T = unknown>(props: GlidlerAPIAdaptorForGoLangv2Props<T>) {
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';

View File

@ -45,9 +45,9 @@ export function GlidlerFormAdaptor(props: {
}
const items = [] as Array<MantineBetterMenuInstanceItem>;
if (defaultItems && id === 'cell') {
items.push(...(defaultItems as Array<MantineBetterMenuInstanceItem>));
}
items.push(...(defaultItems as Array<MantineBetterMenuInstanceItem>));
const rows = getState('_gridSelection')?.rows.toArray() ?? [];
const manyRows = rows.length > 1;

View File

@ -1,15 +1,40 @@
import React, { useEffect } from 'react';
import { useGridlerStore } from '../GridlerStore';
import type { GridlerColumns } from '../Column';
export interface GlidlerLocalDataAdaptorProps {
data: Array<unknown>;
import { type FilterOption, type SortOption, useGridlerStore } from '../GridlerStore';
export interface GlidlerLocalDataAdaptorProps<T = unknown> {
data: Array<T>;
onColumnFilter?: (
colFilters: Array<FilterOption> | undefined,
cols: GridlerColumns | undefined,
data: Array<T>
) => Array<T>;
onColumnSort?: (
colSort: Array<SortOption> | undefined,
cols: GridlerColumns | undefined,
data: Array<T>
) => Array<T>;
}
//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<T = unknown>(props: GlidlerLocalDataAdaptorProps<T>) {
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<any> = 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<T>);
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<T>);
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';

View File

@ -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'
export { GridlerRightMenuIcon } from './components/RightMenuIcon'
export {Gridler} from './Gridler'
export * from './utils'

View File

@ -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}
>
<GlidlerAPIAdaptorForGoLangv2 authtoken={apiKey} url={`${apiUrl}/public/process`} />
<GlidlerAPIAdaptorForGoLangv2
authtoken={apiKey}
//options={[{ type: 'fieldfilter', name: 'process', value: 'test' }]}
url={`${apiUrl}/public/process`}
/>
<Gridler.FormAdaptor
descriptionField={'process'}
onRequestForm={(request, data) => {

View File

@ -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<FetchAPIOperation>
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<FetchAPIOperation>} ops - The array of FetchAPIOperation objects to be built.
* @param {Headers} [headers] - Optional headers to be set.
* @return {Array<FetchAPIOperation>} - The built array of FetchAPIOperation objects.
*/
const buildGoAPIOperation = (
ops: Array<FetchAPIOperation>,
headers?: Headers
): Array<FetchAPIOperation> => {
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<FetchAPIOperation>} 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<FetchAPIOperation>,
headers?: Headers
): { [key: string]: string } => {
const head = new Headers()
const headerlist: Record<string,string> = {}
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<FetchAPIOperation> = []
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}

View File

@ -1,81 +0,0 @@
export type APIOptionsType = {
autocreate?: boolean
autoref?: boolean
baseurl?: string
getAPIProvider?: () => { provider: string; providerKey: string }
getAuthToken?: () => string
operations?: Array<FetchAPIOperation>
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
}

View File

@ -0,0 +1 @@
export {type APIOptionsType,type FetchAPIOperation,GoAPIHeaders} from './golang-restapi-v2'