@alaarab/ogrid-core 1.0.0
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.
- package/README.md +37 -0
- package/dist/esm/hooks/index.js +1 -0
- package/dist/esm/hooks/useFilterOptions.js +40 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/types/columnTypes.js +1 -0
- package/dist/esm/types/dataGridTypes.js +27 -0
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/utils/exportToCsv.js +34 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/types/hooks/index.d.ts +2 -0
- package/dist/types/hooks/useFilterOptions.d.ts +16 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/types/columnTypes.d.ts +30 -0
- package/dist/types/types/dataGridTypes.d.ts +44 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/utils/exportToCsv.d.ts +9 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @alaarab/ogrid-core
|
|
2
|
+
|
|
3
|
+
Framework-agnostic types, hooks, and utilities for [OGrid](https://github.com/alaarab/ogrid) data tables.
|
|
4
|
+
|
|
5
|
+
This package is the shared foundation used by `@alaarab/ogrid-fluent` and `@alaarab/ogrid-material`. You typically don't need to install it directly -- both framework packages re-export everything from core.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @alaarab/ogrid-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What's Included
|
|
14
|
+
|
|
15
|
+
### Types
|
|
16
|
+
|
|
17
|
+
- `IColumnDef<T>` -- Column definition with sorting, filtering, and rendering
|
|
18
|
+
- `IDataSource<T>` -- Server-side data source interface
|
|
19
|
+
- `IFetchParams` -- Parameters for `fetchPage()`
|
|
20
|
+
- `IFilters` -- Unified filter values (text, multi-select, people)
|
|
21
|
+
- `UserLike` -- Minimal user shape for people picker
|
|
22
|
+
- `IColumnFilterDef`, `IColumnMeta`, `IPageResult`, `ColumnFilterType`
|
|
23
|
+
|
|
24
|
+
### Hooks
|
|
25
|
+
|
|
26
|
+
- `useFilterOptions(dataSource, fields)` -- Loads filter options for multi-select columns
|
|
27
|
+
|
|
28
|
+
### Utilities
|
|
29
|
+
|
|
30
|
+
- `toDataGridFilterProps(filters)` -- Splits `IFilters` into `multiSelectFilters`, `textFilters`, `peopleFilters`
|
|
31
|
+
- `toUserLike(user)` -- Converts a user-like object to `UserLike`
|
|
32
|
+
- `exportToCsv(items, columns, getValue, filename)` -- Full CSV export
|
|
33
|
+
- `buildCsvHeader`, `buildCsvRows`, `triggerCsvDownload`, `escapeCsvValue` -- Low-level CSV helpers
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useFilterOptions } from './useFilterOptions';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Load filter options for the given fields from a data source.
|
|
4
|
+
*
|
|
5
|
+
* Accepts `IDataSource<T>` or a plain `{ fetchFilterOptions }` object.
|
|
6
|
+
*/
|
|
7
|
+
export function useFilterOptions(dataSource, fields) {
|
|
8
|
+
const [filterOptions, setFilterOptions] = useState({});
|
|
9
|
+
const [loadingOptions, setLoadingOptions] = useState({});
|
|
10
|
+
const load = useCallback(async () => {
|
|
11
|
+
const fetcher = 'fetchFilterOptions' in dataSource && typeof dataSource.fetchFilterOptions === 'function'
|
|
12
|
+
? dataSource.fetchFilterOptions.bind(dataSource)
|
|
13
|
+
: undefined;
|
|
14
|
+
if (!fetcher) {
|
|
15
|
+
setFilterOptions({});
|
|
16
|
+
setLoadingOptions({});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const loading = {};
|
|
20
|
+
fields.forEach((f) => { loading[f] = true; });
|
|
21
|
+
setLoadingOptions(loading);
|
|
22
|
+
const results = {};
|
|
23
|
+
await Promise.all(fields.map(async (field) => {
|
|
24
|
+
try {
|
|
25
|
+
results[field] = await fetcher(field);
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
console.error(`Error loading filter options for ${field}:`, e);
|
|
29
|
+
results[field] = [];
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
setFilterOptions(results);
|
|
33
|
+
setLoadingOptions({});
|
|
34
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
+
}, [dataSource, fields.slice().sort().join(',')]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
load().catch(console.error);
|
|
38
|
+
}, [load]);
|
|
39
|
+
return { filterOptions, loadingOptions };
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function toUserLike(u) {
|
|
2
|
+
if (!u)
|
|
3
|
+
return undefined;
|
|
4
|
+
return {
|
|
5
|
+
id: u.id,
|
|
6
|
+
displayName: u.displayName,
|
|
7
|
+
email: 'email' in u && u.email ? u.email : (u.mail || u.userPrincipalName || ''),
|
|
8
|
+
photo: u.photo
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Split IFilters into DataGridTable's multiSelect, text, and people props. */
|
|
12
|
+
export function toDataGridFilterProps(filters) {
|
|
13
|
+
const multiSelectFilters = {};
|
|
14
|
+
const textFilters = {};
|
|
15
|
+
const peopleFilters = {};
|
|
16
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
continue;
|
|
19
|
+
if (Array.isArray(value))
|
|
20
|
+
multiSelectFilters[key] = value;
|
|
21
|
+
else if (typeof value === 'string')
|
|
22
|
+
textFilters[key] = value;
|
|
23
|
+
else if (typeof value === 'object' && value !== null && 'email' in value)
|
|
24
|
+
peopleFilters[key] = value;
|
|
25
|
+
}
|
|
26
|
+
return { multiSelectFilters, textFilters, peopleFilters };
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { toUserLike, toDataGridFilterProps } from './dataGridTypes';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function escapeCsvValue(value) {
|
|
2
|
+
if (value === null || value === undefined) {
|
|
3
|
+
return '';
|
|
4
|
+
}
|
|
5
|
+
const s = String(value);
|
|
6
|
+
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
|
7
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
8
|
+
}
|
|
9
|
+
return s;
|
|
10
|
+
}
|
|
11
|
+
export function buildCsvHeader(columns) {
|
|
12
|
+
return columns.map((c) => escapeCsvValue(c.name)).join(',');
|
|
13
|
+
}
|
|
14
|
+
export function buildCsvRows(items, columns, getValue) {
|
|
15
|
+
return items.map((item) => columns.map((c) => escapeCsvValue(getValue(item, c.columnId))).join(','));
|
|
16
|
+
}
|
|
17
|
+
export function exportToCsv(items, columns, getValue, filename) {
|
|
18
|
+
const header = buildCsvHeader(columns);
|
|
19
|
+
const rows = buildCsvRows(items, columns, getValue);
|
|
20
|
+
const csv = [header, ...rows].join('\n');
|
|
21
|
+
triggerCsvDownload(csv, filename ?? `export_${new Date().toISOString().slice(0, 10)}.csv`);
|
|
22
|
+
}
|
|
23
|
+
export function triggerCsvDownload(csvContent, filename) {
|
|
24
|
+
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
25
|
+
const link = document.createElement('a');
|
|
26
|
+
const url = URL.createObjectURL(blob);
|
|
27
|
+
link.setAttribute('href', url);
|
|
28
|
+
link.setAttribute('download', filename);
|
|
29
|
+
link.style.visibility = 'hidden';
|
|
30
|
+
document.body.appendChild(link);
|
|
31
|
+
link.click();
|
|
32
|
+
document.body.removeChild(link);
|
|
33
|
+
URL.revokeObjectURL(url);
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, } from './exportToCsv';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IDataSource } from '../types/dataGridTypes';
|
|
2
|
+
export interface UseFilterOptionsResult {
|
|
3
|
+
filterOptions: Record<string, string[]>;
|
|
4
|
+
loadingOptions: Record<string, boolean>;
|
|
5
|
+
}
|
|
6
|
+
/** Accepted data source shapes for useFilterOptions. */
|
|
7
|
+
type FilterOptionsSource = IDataSource<unknown> | {
|
|
8
|
+
fetchFilterOptions?: (field: string) => Promise<string[]>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Load filter options for the given fields from a data source.
|
|
12
|
+
*
|
|
13
|
+
* Accepts `IDataSource<T>` or a plain `{ fetchFilterOptions }` object.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useFilterOptions(dataSource: FilterOptionsSource, fields: string[]): UseFilterOptionsResult;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnDefinition, UserLike, IFilters, IFetchParams, IPageResult, IDataSource, } from './types';
|
|
2
|
+
export { toUserLike, toDataGridFilterProps } from './types';
|
|
3
|
+
export { useFilterOptions } from './hooks';
|
|
4
|
+
export type { UseFilterOptionsResult } from './hooks';
|
|
5
|
+
export { escapeCsvValue, buildCsvHeader, buildCsvRows, exportToCsv, triggerCsvDownload, } from './utils';
|
|
6
|
+
export type { CsvColumn } from './utils';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type ColumnFilterType = 'none' | 'text' | 'multiSelect' | 'people';
|
|
3
|
+
export interface IColumnFilterDef {
|
|
4
|
+
type: Exclude<ColumnFilterType, 'none'>;
|
|
5
|
+
filterField?: string;
|
|
6
|
+
optionsSource?: 'api' | 'static' | 'years';
|
|
7
|
+
options?: string[];
|
|
8
|
+
yearsCount?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface IColumnMeta {
|
|
11
|
+
columnId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
sortable?: boolean;
|
|
14
|
+
filterable?: false | IColumnFilterDef;
|
|
15
|
+
defaultVisible?: boolean;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
minWidth?: number;
|
|
18
|
+
defaultWidth?: number;
|
|
19
|
+
idealWidth?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface IColumnDef<T = unknown> extends IColumnMeta {
|
|
22
|
+
renderCell?: (item: T) => React.ReactNode;
|
|
23
|
+
compare?: (a: T, b: T) => number;
|
|
24
|
+
}
|
|
25
|
+
/** Minimal column info for the ColumnChooser (framework-agnostic). */
|
|
26
|
+
export interface IColumnDefinition {
|
|
27
|
+
columnId: string;
|
|
28
|
+
name: string;
|
|
29
|
+
required?: boolean;
|
|
30
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface UserLike {
|
|
2
|
+
id?: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
email: string;
|
|
5
|
+
photo?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function toUserLike(u: {
|
|
8
|
+
displayName: string;
|
|
9
|
+
mail?: string;
|
|
10
|
+
userPrincipalName?: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
photo?: string;
|
|
14
|
+
} | undefined): UserLike | undefined;
|
|
15
|
+
/** Unified filter values: text (string), multi-select (string[]), or people (UserLike). */
|
|
16
|
+
export interface IFilters {
|
|
17
|
+
[field: string]: string | string[] | UserLike | undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Split IFilters into DataGridTable's multiSelect, text, and people props. */
|
|
20
|
+
export declare function toDataGridFilterProps(filters: IFilters): {
|
|
21
|
+
multiSelectFilters: Record<string, string[]>;
|
|
22
|
+
textFilters: Record<string, string>;
|
|
23
|
+
peopleFilters: Record<string, UserLike | undefined>;
|
|
24
|
+
};
|
|
25
|
+
export interface IFetchParams {
|
|
26
|
+
page: number;
|
|
27
|
+
pageSize: number;
|
|
28
|
+
sort?: {
|
|
29
|
+
field: string;
|
|
30
|
+
direction: 'asc' | 'desc';
|
|
31
|
+
};
|
|
32
|
+
filters: IFilters;
|
|
33
|
+
}
|
|
34
|
+
export interface IPageResult<T> {
|
|
35
|
+
items: T[];
|
|
36
|
+
totalCount: number;
|
|
37
|
+
}
|
|
38
|
+
/** Data source API: fetch a page and optionally filter options / people. */
|
|
39
|
+
export interface IDataSource<T> {
|
|
40
|
+
fetchPage(params: IFetchParams): Promise<IPageResult<T>>;
|
|
41
|
+
fetchFilterOptions?(field: string): Promise<string[]>;
|
|
42
|
+
searchPeople?(query: string): Promise<UserLike[]>;
|
|
43
|
+
getUserByEmail?(email: string): Promise<UserLike | undefined>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { ColumnFilterType, IColumnFilterDef, IColumnMeta, IColumnDef, IColumnDefinition, } from './columnTypes';
|
|
2
|
+
export type { UserLike, IFilters, IFetchParams, IPageResult, IDataSource, } from './dataGridTypes';
|
|
3
|
+
export { toUserLike, toDataGridFilterProps } from './dataGridTypes';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CsvColumn {
|
|
2
|
+
columnId: string;
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function escapeCsvValue(value: unknown): string;
|
|
6
|
+
export declare function buildCsvHeader(columns: CsvColumn[]): string;
|
|
7
|
+
export declare function buildCsvRows<T>(items: T[], columns: CsvColumn[], getValue: (item: T, columnId: string) => string): string[];
|
|
8
|
+
export declare function exportToCsv<T>(items: T[], columns: CsvColumn[], getValue: (item: T, columnId: string) => string, filename?: string): void;
|
|
9
|
+
export declare function triggerCsvDownload(csvContent: string, filename: string): void;
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alaarab/ogrid-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OGrid core – framework-agnostic types, hooks, and utilities for OGrid data tables.",
|
|
5
|
+
"main": "dist/esm/index.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/types/index.d.ts",
|
|
11
|
+
"import": "./dist/esm/index.js",
|
|
12
|
+
"require": "./dist/esm/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "rimraf dist && tsc -p tsconfig.build.json",
|
|
17
|
+
"test": "jest"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["ogrid", "datatable", "react", "typescript", "grid", "core"],
|
|
20
|
+
"author": "Ala Arab",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": "^17.0.0 || ^18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|