diff --git a/example/my-app/src/App.css b/example/my-app/src/App.css index 764732cf..5fe6895d 100644 --- a/example/my-app/src/App.css +++ b/example/my-app/src/App.css @@ -30,12 +30,13 @@ span.error { span.await { background-color: #52d2d2; } -input { + +input, select { padding: 5px; margin: 0 5px 0 0; } -input, button { +input, button, select, textarea { border: 1px solid #51887a; border-radius: 0; margin-top: 2px; @@ -97,6 +98,18 @@ table > thead .option-row button { margin-right: 5px; } +table > thead .filter-rules { + float: right; +} + +.filter-rules { + +} + +.filter-group, .filter-rule { + margin-left: 40px; +} + table > tbody > tr { border-bottom: 1px solid #dddddd; } @@ -121,9 +134,6 @@ button.next-page{ } button.prev-page{ float: left; - /* icon */ - - } diff --git a/example/my-app/src/App.tsx b/example/my-app/src/App.tsx index fed35439..20ad6b25 100644 --- a/example/my-app/src/App.tsx +++ b/example/my-app/src/App.tsx @@ -1,10 +1,12 @@ -import React, {useEffect, useReducer, useState} from 'react'; +import React, {useEffect, useState} from 'react'; import './App.css'; import * as openai from './workflow/github_com_sashabaranov_go-openai' import * as schemaless from './workflow/github_com_widmogrod_mkunion_x_storage_schemaless' import * as workflow from './workflow/github_com_widmogrod_mkunion_x_workflow' +import * as predicate from "./workflow/github_com_widmogrod_mkunion_x_storage_predicate"; import * as schema from "./workflow/github_com_widmogrod_mkunion_x_schema"; import {Chat} from "./Chat"; +import {PaginatedTable} from "./component/PaginatedTable"; function flowCreate(flow: workflow.Flow) { return fetch('http://localhost:8080/flow', { @@ -30,6 +32,7 @@ type ListProps = { [key: string]: boolean } limit?: number, + where?: predicate.WherePredicates prevPage?: string, nextPage?: string, @@ -49,6 +52,7 @@ function storageList(input: ListProps): Promise>), @@ -723,10 +727,7 @@ function App() { load={(state) => { return listFlows({ - limit: state.limit, - sort: state.sort, - prevPage: state.prevPage, - nextPage: state.nextPage, + ...state, }) }} mapData={(data) => { @@ -750,10 +751,7 @@ function App() { load={(state) => { return listStates({ - limit: state.limit, - sort: state.sort, - prevPage: state.prevPage, - nextPage: state.nextPage, + ...state, }) }} actions={[ @@ -786,7 +784,7 @@ function App() { } } ]} - mapData={(input) => { + mapData={(input, ctx) => { if (!input.Data) { return
nothing
} @@ -811,6 +809,46 @@ function App() { case "schema.String": return <> workflow.Done + + {done.Result["schema.String"]} @@ -842,8 +880,29 @@ function App() { return ( <> workflow.Await + + @@ -1074,233 +1133,6 @@ function SchemaValue(props: { data?: schema.Schema }) { return } -type Cursor = string - -type PaginatedTableState = { - limit: number - sort: PaginatedTableSort - selected: { [key: string]: schemaless.Record } - - prevPage?: Cursor - nextPage?: Cursor -} - -type PaginatedTableSort = { - [key: string]: boolean -} - -type PaginatedTableAction = { - name: string - action: (state: PaginatedTableState, ctx: PaginatedTableContext) => void -} - -type PaginatedTableContext = { - refresh: () => void - clearSelection: () => void -} - - -type PaginatedTableProps = { - limit?: number - sort?: PaginatedTableSort - load: (input: PaginatedTableState) => Promise>> - mapData?: (data: schemaless.Record) => JSX.Element - actions?: PaginatedTableAction[] -} - -function PaginatedTable(props: PaginatedTableProps) { - const [data, setData] = useState({Items: [] as any[]} as schemaless.PageResult>) - const [state, setState] = useState({ - limit: props.limit || 3, - sort: props.sort || {}, - selected: {}, - } as PaginatedTableState) - - useEffect(() => { - props.load(state).then(setData) - }, [state]) - - const ctx = { - refresh: () => { - props.load(state).then(setData) - }, - clearSelection: () => { - setState({ - ...state, - selected: {}, - }) - } - } as PaginatedTableContext - - const changeSort = (key: string) => (e: React.MouseEvent) => { - e.preventDefault() - - let newSort = {...state.sort} - if (newSort[key] === undefined) { - newSort[key] = true - } else if (newSort[key]) { - newSort[key] = false - } else { - delete newSort[key] - } - - setState({ - ...state, - sort: newSort, - }) - } - - const sortState = (key: string) => { - if (state.sort[key] === undefined) { - return "sort-none" - } else if (state.sort[key]) { - return "sort-asc" - } else { - return "sort-desc" - } - } - - - const selectRowToggle = (item: schemaless.Record) => () => { - if (!item.ID) { - return - } - - let selected = {...state.selected} - if (selected[item.ID]) { - delete selected[item.ID] - } else { - selected[item.ID] = item - } - - setState({ - ...state, - selected: selected, - }) - } - - const isSelected = (item: schemaless.Record) => { - if (!item.ID) { - return false - } - - return state.selected[item.ID] !== undefined - } - - const batchSelection = (e: React.MouseEvent) => { - e.preventDefault() - - let selectionLength = Object.keys(state.selected).length - if (selectionLength > 0) { - setState({ - ...state, - selected: {}, - }) - } else { - let selected = {} as { [key: string]: schemaless.Record } - data.Items?.forEach((item) => { - if (!item.ID) { - return - } - - selected[item.ID] = item - }) - - setState({ - ...state, - selected: selected - } as PaginatedTableState) - } - } - - const batchSelectionState = () => { - let selectionLength = Object.keys(state.selected).length - if (selectionLength === 0) { - return "selected-none" - } - if (selectionLength === data.Items?.length) { - return "selected-all" - } - - return "selected-some" - } - - const applyAction = (action: PaginatedTableAction) => (e: React.MouseEvent) => { - e.preventDefault() - action.action(state, ctx) - } - - const nextPage = (e: React.MouseEvent) => { - e.preventDefault() - setState({ - ...state, - nextPage: data.Next?.After, - prevPage: undefined, - }) - } - - const prevPage = (e: React.MouseEvent) => { - e.preventDefault() - setState({ - ...state, - nextPage: undefined, - prevPage: data.Prev?.Before, - }) - } - - return - - - - - - - - - - - - - - {data.Items && data.Items.length > 0 ? data.Items.map((item) => { - return ( - - - - - - - - ); - }) : ( - - - - )} - - - - - - -
- - - {props.actions && props.actions.map((action) => { - return - })} -
- - IDTypeVersionData
{item.ID}{item.Type}{item.Version}{props.mapData && props.mapData(item)}
No data
- {data.Next && } - {data.Prev && } -
-} - function WorkflowToString(props: { flow?: workflow.Workflow }) { const [str, setStr] = useState("") diff --git a/example/my-app/src/component/PaginatedTable.tsx b/example/my-app/src/component/PaginatedTable.tsx new file mode 100644 index 00000000..2ec9f71f --- /dev/null +++ b/example/my-app/src/component/PaginatedTable.tsx @@ -0,0 +1,505 @@ +import React, {useEffect, useState} from "react"; +import * as schemaless from "../workflow/github_com_widmogrod_mkunion_x_storage_schemaless"; +import * as schema from "../workflow/github_com_widmogrod_mkunion_x_schema"; +import * as predicate from "../workflow/github_com_widmogrod_mkunion_x_storage_predicate"; + +export type Cursor = string + +export type PaginatedTableState = { + limit: number + sort: PaginatedTableSort + selected: { [key: string]: schemaless.Record } + + prevPage?: Cursor + nextPage?: Cursor + + where?: predicate.WherePredicates +} + +export type PaginatedTableSort = { + [key: string]: boolean +} + +export type PaginatedTableAction = { + name: string + action: (state: PaginatedTableState, ctx: PaginatedTableContext) => void +} + +export type PaginatedCompare = { + location: string + operation: "==" | "!=" | "<" | "<=" | ">" | ">=" + literal: schema.Schema +} + +export type PaginatedTableContext = { + refresh: () => void + clearSelection: () => void + filter: (x: predicate.WherePredicates) => void +} + + +export type PaginatedTableProps = { + limit?: number + sort?: PaginatedTableSort + load: (input: PaginatedTableState) => Promise>> + mapData?: (data: schemaless.Record, ctx: PaginatedTableContext) => JSX.Element + actions?: PaginatedTableAction[] +} + +export function PaginatedTable(props: PaginatedTableProps) { + const [data, setData] = useState({Items: [] as any[]} as schemaless.PageResult>) + const [state, setState] = useState({ + limit: props.limit || 3, + sort: props.sort || {}, + selected: {}, + } as PaginatedTableState) + + useEffect(() => { + props.load(state).then(setData) + }, [state]) + + const ctx = { + refresh: () => { + props.load(state).then(setData) + }, + clearSelection: () => { + setState({ + ...state, + selected: {}, + }) + }, + filter: (x: predicate.WherePredicates) => { + setState({ + ...state, + where: mergeFilters(state.where, x) + }) + } + } as PaginatedTableContext + + const changeSort = (key: string) => (e: React.MouseEvent) => { + e.preventDefault() + + let newSort = {...state.sort} + if (newSort[key] === undefined) { + newSort[key] = true + } else if (newSort[key]) { + newSort[key] = false + } else { + delete newSort[key] + } + + setState({ + ...state, + sort: newSort, + }) + } + + const sortState = (key: string) => { + if (state.sort[key] === undefined) { + return "sort-none" + } else if (state.sort[key]) { + return "sort-asc" + } else { + return "sort-desc" + } + } + + + const selectRowToggle = (item: schemaless.Record) => () => { + if (!item.ID) { + return + } + + let selected = {...state.selected} + if (selected[item.ID]) { + delete selected[item.ID] + } else { + selected[item.ID] = item + } + + setState({ + ...state, + selected: selected, + }) + } + + const isSelected = (item: schemaless.Record) => { + if (!item.ID) { + return false + } + + return state.selected[item.ID] !== undefined + } + + const batchSelection = (e: React.MouseEvent) => { + e.preventDefault() + + let selectionLength = Object.keys(state.selected).length + if (selectionLength > 0) { + setState({ + ...state, + selected: {}, + }) + } else { + let selected = {} as { [key: string]: schemaless.Record } + data.Items?.forEach((item) => { + if (!item.ID) { + return + } + + selected[item.ID] = item + }) + + setState({ + ...state, + selected: selected + } as PaginatedTableState) + } + } + + const batchSelectionState = () => { + let selectionLength = Object.keys(state.selected).length + if (selectionLength === 0) { + return "selected-none" + } + if (selectionLength === data.Items?.length) { + return "selected-all" + } + + return "selected-some" + } + + const applyAction = (action: PaginatedTableAction) => (e: React.MouseEvent) => { + e.preventDefault() + action.action(state, ctx) + } + + const nextPage = (e: React.MouseEvent) => { + e.preventDefault() + setState({ + ...state, + nextPage: data.Next?.After, + prevPage: undefined, + }) + } + + const prevPage = (e: React.MouseEvent) => { + e.preventDefault() + setState({ + ...state, + nextPage: undefined, + prevPage: data.Prev?.Before, + }) + } + + return + + + + + + + + + + + + + + {data.Items && data.Items.length > 0 ? data.Items.map((item) => { + return ( + + + + + + + + ); + }) : ( + + + + )} + + + + + + +
+ + + {props.actions && props.actions.map((action) => { + return + })} + + { + setState({ + ...state, + where: where, + }) + }}/> +
+ + IDTypeVersionData
{item.ID}{item.Type}{item.Version}{props.mapData && props.mapData(item, ctx)}
No data
+ {data.Next && } + {data.Prev && } +
+} + +function mergeFilters(a?: predicate.WherePredicates, b?: predicate.WherePredicates): predicate.WherePredicates | undefined { + if (a === undefined) { + return b + } + if (b === undefined) { + return a + } + + let result = {...a} + result.Params = {...a.Params, ...b.Params} + result.Predicate = mergePredicate(a.Predicate, b.Predicate) + + return result +} + +function mergePredicate(a?: predicate.Predicate, b?: predicate.Predicate): predicate.Predicate | undefined { + if (a === undefined) { + return b + } + if (b === undefined) { + return a + } + + return { + "$type": "predicate.And", + "predicate.And": { + L: [a, b], + }, + } +} + + +function WherePredicateRender(props: { + where?: predicate.WherePredicates, + onChange: (where?: predicate.WherePredicates) => void +}) { + if (!props.where) { + return <> + } + + return
+ { + if (!where) { + props.onChange(undefined) + return + } else { + props.onChange({ + ...props.where, + Predicate: where, + }) + } + }}/> +
+} + +function PredicateRender(props: { + predicate?: predicate.Predicate, + onChange?: (where?: predicate.Predicate) => void +}) { + if (!props.predicate) { + return <> + } + + switch (props.predicate.$type) { + case "predicate.And": + let and = props.predicate["predicate.And"] + + if (!and.L) { + return <> + } + + return
+ + (AND + {and.L?.map((x) => { + return { + let predicates = and.L?.map((y) => (y === x) ? where : y).filter((y) => y !== undefined) as predicate.Predicate[] + + if (predicates.length === 0) { + props.onChange && props.onChange(undefined) + return + } + + props.onChange && props.onChange({ + "$type": "predicate.And", + "predicate.And": { + L: predicates + } + }) + }} + /> + })} + ) +
+ + case "predicate.Or": + let or = props.predicate["predicate.Or"] + + if (!or.L) { + return <> + } + + return
+ or {or.L?.map((x) => { + return + }).join(" or ")} +
+ + case "predicate.Not": + let not = props.predicate["predicate.Not"] + + if (!not.P) { + return <> + } + + return
+ not +
+ + case "predicate.Compare": + let compare = props.predicate["predicate.Compare"] + + return
+ + + + +
+ } + + return
Unknown predicate
+} + +function BindableValueRender(props: { bindable?: predicate.Bindable }) { + if (!props.bindable) { + return <> + } + + switch (props.bindable.$type) { + case "predicate.BindValue": + let value = props.bindable["predicate.BindValue"] + + return + + case "predicate.Literal": + let literal = props.bindable["predicate.Literal"] + + return + + case "predicate.Locatable": + let locatable = props.bindable["predicate.Locatable"] + + return
+ {locatable.Location} +
+ } + + return
Unknown bindable {JSON.stringify(props.bindable)}
+} + +function SchemaValue(props: { data?: schema.Schema }) { + if (!props.data) { + return <> + } + + switch (props.data.$type) { + case "schema.String": + return + case "schema.Number": + return <>{props.data["schema.Number"]} + case "schema.Binary": + return <>binary + case "schema.Bool": + return <>{props.data["schema.Bool"]} + case "schema.List": + const listData = props.data["schema.List"]; + return ( +
    + {listData && listData.map((item, index) => ( +
  • + +
  • + ))} +
+ ); + + case "schema.Map": + const mapData = props.data["schema.Map"]; + const keys = Object.keys(mapData); + + if (keys && keys.length === 0) { + return <>; // If the map is empty, return null (no table to display) + } + + return ( + + + + + + + + + {keys && keys.map((key) => ( + + + + + ))} + +
KeyValue
{key} + +
+ ); + + } + + return
+ Unknown schema {JSON.stringify(props.data)} +
+} \ No newline at end of file