Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Odin React
![GitHub package.json version](https://img.shields.io/github/package-json/v/stfc-aeg/odin-react)
![GitHub package.json dev/peer/optional dependency version](https://img.shields.io/github/package-json/dependency-version/stfc-aeg/odin-react/peer/react)
![GitHub package.json prod dependency version](https://img.shields.io/github/package-json/dependency-version/stfc-aeg/odin-react/react)
![GitHub package.json dev/peer/optional dependency version](https://img.shields.io/github/package-json/dependency-version/stfc-aeg/odin-react/dev/vite)

Component Library designed for Odin Control Interfaces, build with [Vite](https://vite.dev/)
Expand Down
4 changes: 2 additions & 2 deletions docs/Introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import "./storydocs.css"
# Welcome to Odin React
![Test Odin React](https://github.com/stfc-aeg/odin-react/actions/workflows/test-ui.yml/badge.svg)
![GitHub package.json version](https://img.shields.io/github/package-json/v/stfc-aeg/odin-react)
![GitHub package.json dev/peer/optional dependency version](https://img.shields.io/github/package-json/dependency-version/stfc-aeg/odin-react/peer/react)
![GitHub package.json prod dependency version](https://img.shields.io/github/package-json/dependency-version/stfc-aeg/odin-react/react)
![GitHub package.json dev/peer/optional dependency version](https://img.shields.io/github/package-json/dependency-version/stfc-aeg/odin-react/dev/vite)

Odin React is a [React](https://react.dev/) package designed to make the
Expand Down Expand Up @@ -62,7 +62,7 @@ import "./storydocs.css"
Vite's compilation step bundles source code, image assets, and any
dependencies together into a minified static web resource.

<a className="sb-action-link" href="https://react.dev/" target="_blank"
<a className="sb-action-link" href="https://vite.dev/" target="_blank"
>Learn more ></a>
</Col>

Expand Down
4 changes: 2 additions & 2 deletions docs/demoPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const DemoPage = (
changeMessage("Trigger Clicked\nAwaiting Data");
}

const PostMethod = (num: number) => {
const PostMethod = ({num}: {num: number}) => {
changeMessage("Post Method\nNumber: " + num);
}

Expand All @@ -41,7 +41,7 @@ const DemoPage = (
<InputGroup.Text>Trigger</InputGroup.Text>
<EndpointButton endpoint={endpoint} fullpath='trigger' value={42}
pre_method={PreMethod} post_method={PostMethod}
post_args={[endpoint.data.rand_num]}>
post_args={{num: endpoint.data.rand_num}}>
Click me!
</EndpointButton>
<Form.Control as="textarea" readOnly value={triggeredStateMessage} />
Expand Down
9 changes: 6 additions & 3 deletions lib/components/AdapterEndpoint/index.mock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface EndpointData extends actual.ParamNode {
}
}
};
volt: number;
graph_data: number[];
}

Expand Down Expand Up @@ -78,7 +79,8 @@ const testAdapterData: EndpointData = {
selected: "item 1",
toggle: true,
trigger: null,
graph_data: Array.from(Array(360), (_, x) => Math.sin(x * (Math.PI / 180)))
graph_data: Array.from(Array(360), (_, x) => Math.sin(x * (Math.PI / 180))),
volt: 1500
}

const testLiveData: LiveViewData = {
Expand All @@ -101,7 +103,8 @@ const metadataPaths: { [key: string]: Partial<MetadataValue> } = {
"num_val": { min: 10, max: 90 },
"clip_data": {min: -25, max: 25},
"rand_num": { writeable: false },
"dict": { writeable: false, type: "dict" }
"dict": { writeable: false, type: "dict" },
"volt": {min: 100, max: 5000, units: "mV", name: "Voltage" }
}

const testMetadata = createMetadata(testAdapterData, metadataPaths);
Expand Down Expand Up @@ -243,7 +246,7 @@ const transformMockCode = async (source: string, _?: StoryContext, adapterName:
ret_string = [addEndpointString,
"",
"return (",
source.replace(/endpoint={{[\s\S]*updateFlag: Symbol\(mocked\)\s*}}/, ` endpoint={endpoint}`),
source.replaceAll(/endpoint={{[\s\S]*?updateFlag: Symbol\(mocked\),?\s*}}/g, ` endpoint={endpoint}`),
")"].join("\n");
}

Expand Down
78 changes: 70 additions & 8 deletions lib/components/WithEndpoint/EndpointButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@ import type { Meta, StoryObj } from '@storybook/react-vite';

import { EndpointButton } from './EndpointButton';

import { fn, expect, spyOn } from 'storybook/test';
import { fn, expect, spyOn, Mock } from 'storybook/test';

// Mocked Endpoint
import { useAdapterEndpoint, transformMockCode } from '../AdapterEndpoint/index.mock';

type testMethodProps = {
value?: number;
message: string;
}

const testPreMethod = ({value, message}: testMethodProps) => {
console.log(message);

return (value ?? 0) * 2;
}

const meta = {
component: EndpointButton,
args: {
Expand Down Expand Up @@ -43,10 +54,48 @@ const meta = {
}
} satisfies Meta<typeof EndpointButton>;

const metaWithFunc = {
component: (EndpointButton<testMethodProps, undefined>),
args: {
endpoint: undefined,
fullpath: "trigger",
},
argTypes: {
endpoint: {
control: {
disable: true
}
},
value: {
table: {
type: {
summary: "ParamTree",
detail: "String, Number, Boolean, null/undefined, or an array or dict of those values"
}
}
},
},
parameters: {
layout: "centered",
docs: {
source: {
transform: transformMockCode,
language: "tsx"
}
},
},
render: (args) => {
args.endpoint = useAdapterEndpoint("test", "http://localhost:1338");
return <EndpointButton {...args}>Test Button: {args.fullpath}</EndpointButton>
}
} satisfies Meta<typeof EndpointButton<testMethodProps, undefined>>;

export default meta;

type Story = StoryObj<typeof meta>;

type StoryWithFunc = StoryObj<typeof metaWithFunc>;

/** Standard use of the EndpointButton. */
export const Default: Story = {
args: {
Expand All @@ -57,7 +106,6 @@ export const Default: Story = {
const put = spyOn(args.endpoint, "put").mockName("endpoint.put");
await userEvent.click(canvas.getByRole("button"));

await expect(put).toHaveBeenCalled();
await expect(put).toHaveBeenCalledWith({value: true}, "trigger");
await expect(put).toHaveReturnedWith({value: null});
}
Expand All @@ -84,17 +132,31 @@ export const DisabledBecauseParam: Story = {
}
}

/** The Button can be provided a function (and optional args) */
export const PreTrigger: Story = {
/**
* The Button can be provided a function (and optional args). This function
* can receive the Param value; and if its the pre_method, can return a value
* to send via the PUT request instead of the original value (so it may modify
* the value before sending) */
export const PreTrigger: StoryWithFunc = {
args: {
pre_method: fn(),
pre_args: ["Pre Function Arg"]
value: 12,
pre_method: fn(testPreMethod),
post_method: fn(() => {console.log("Post Method without Args")}),
pre_args: { value: undefined, message: "Pre Function" }
},
play: async ({canvas, args, userEvent}) => {
const put = spyOn(args.endpoint, "put").mockName("endpoint.put");
await userEvent.click(canvas.getByRole("button"));

await expect(args.pre_method).toHaveBeenCalledWith(...(args.pre_args as string[]));
await expect(put).toHaveBeenCalled();
await expect(args.pre_method).toHaveBeenCalledWith(args.pre_args);
await expect(put).toHaveBeenCalledWith({"value": 24}, "trigger");
await expect(args.post_method).toHaveBeenCalled();

// ensuring order of pre/post methods and put method
const pre_order = (args.pre_method as Mock<typeof testPreMethod>).mock.invocationCallOrder[0];
const put_order = put.mock.invocationCallOrder[0];
const post_order = (args.post_method as Mock<() => void>).mock.invocationCallOrder[0];
await expect(pre_order).toBeLessThan(put_order);
await expect(put_order).toBeLessThan(post_order);
}
}
6 changes: 3 additions & 3 deletions lib/components/WithEndpoint/EndpointButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { ButtonProps } from "react-bootstrap";
import { Button } from "react-bootstrap";

import type { EndpointProps } from "./util";
import type { EndpointProps, ArgType } from "./util";
import { useRequestHandler } from "./util";

type EndpointButtonProps<PreArgs extends unknown[], PostArgs extends unknown[]> =
type EndpointButtonProps<PreArgs extends ArgType, PostArgs extends ArgType> =
EndpointProps<PreArgs, PostArgs> & Omit<ButtonProps, keyof EndpointProps<PreArgs, PostArgs>>;


Expand All @@ -20,7 +20,7 @@ type EndpointButtonProps<PreArgs extends unknown[], PostArgs extends unknown[]>
* PUTs the value prop if supplied, otherwise PUTs whatever the parameter is
* on the Tree.
*/
const EndpointButton = <PreArgs extends unknown[], PostArgs extends unknown[]>(
const EndpointButton = <PreArgs extends ArgType, PostArgs extends ArgType>(
{ endpoint, fullpath, value, disabled,
pre_method, pre_args,
post_method, post_args,
Expand Down
4 changes: 2 additions & 2 deletions lib/components/WithEndpoint/EndpointCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useRequestHandler } from "./util";

import style from './styles.module.css'

type EndpointCheckboxProps<PreArgs extends unknown[], PostArgs extends unknown[]> =
type EndpointCheckboxProps<PreArgs extends Record<string, unknown>, PostArgs extends Record<string, unknown>> =
EndpointProps<PreArgs, PostArgs> & Omit<FormCheckProps, keyof EndpointProps<PreArgs, PostArgs>>;


Expand All @@ -19,7 +19,7 @@ type EndpointCheckboxProps<PreArgs extends unknown[], PostArgs extends unknown[]
* Based on the [Bootstrap FormCheck](https://react-bootstrap.netlify.app/docs/forms/checks-radios),
* so all props available on that component can be set here.
*/
const EndpointCheckbox = <PreArgs extends unknown[], PostArgs extends unknown[]>(
const EndpointCheckbox = <PreArgs extends Record<string, unknown>, PostArgs extends Record<string, unknown>>(
{ endpoint, fullpath, value, disabled,
pre_method, pre_args,
post_method, post_args,
Expand Down
4 changes: 2 additions & 2 deletions lib/components/WithEndpoint/EndpointDoubleSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ComponentProps, useRef, useState, useEffect } from "react";
import { getValueFromPath } from "../AdapterEndpoint";
import { MetadataValue } from "../AdapterEndpoint/AdapterEndpoint.types";

type EndpointDoubleSliderProps<PreArgs extends unknown[], PostArgs extends unknown[]> =
type EndpointDoubleSliderProps<PreArgs extends Record<string, unknown>, PostArgs extends Record<string, unknown>> =
EndpointProps<PreArgs, PostArgs> & SliderProps;

/**
Expand All @@ -19,7 +19,7 @@ type EndpointDoubleSliderProps<PreArgs extends unknown[], PostArgs extends unkno
* Designed to be used with a Parameter that represents a min and max
* value of some sort as a pair of numbers in an array.
*/
const EndpointDoubleSlider = <PreArgs extends unknown[], PostArgs extends unknown[]>(
const EndpointDoubleSlider = <PreArgs extends Record<string, unknown>, PostArgs extends Record<string, unknown>>(
{ endpoint, fullpath, value, disabled, min, max,
pre_method, pre_args,
post_method, post_args,
Expand Down
4 changes: 2 additions & 2 deletions lib/components/WithEndpoint/EndpointDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useRequestHandler } from "./util";
import { getValueFromPath } from "../AdapterEndpoint";
import { MetadataValue } from "../AdapterEndpoint/AdapterEndpoint.types";

type EndpointDropdownProps<PreArgs extends unknown[], PostArgs extends unknown[]> =
type EndpointDropdownProps<PreArgs extends Record<string, unknown>, PostArgs extends Record<string, unknown>> =
EndpointProps<PreArgs, PostArgs> & Partial<DropdownButtonProps>;


Expand All @@ -19,7 +19,7 @@ type EndpointDropdownProps<PreArgs extends unknown[], PostArgs extends unknown[]
* Based on the [Bootstrap DropdownButton](https://react-bootstrap.netlify.app/docs/components/dropdowns),
* so any props on that component can also be set here.
*/
const EndpointDropdown = <PreArgs extends unknown[], PostArgs extends unknown[]>(
const EndpointDropdown = <PreArgs extends Record<string, unknown>, PostArgs extends Record<string, unknown>>(
{ endpoint, fullpath, value, disabled,
pre_method, pre_args,
post_method, post_args,
Expand Down
35 changes: 34 additions & 1 deletion lib/components/WithEndpoint/EndpointInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';

import { EndpointInput } from './EndpointInput';
import { useAdapterEndpoint, resetMockData, transformMockCode } from '../AdapterEndpoint/index.mock';
import { expect, spyOn } from 'storybook/test';
import { expect, spyOn, fn } from 'storybook/test';

const meta = {
component: EndpointInput,
Expand Down Expand Up @@ -85,6 +85,39 @@ export const Number: Story = {
}
}

const preCapMethod = ({value}: {value?: string}) => {
console.log(`Pre Func called with ${value}`);
return value?.toUpperCase();
}

/** We can manipulate the sent string/number value with the pre_method */
export const PreCapitalise: Story = {
args: {
pre_method: fn(preCapMethod),
pre_args: {value: undefined}
},
play: async({args, canvas, userEvent}) => {
const put = spyOn(args.endpoint, "put").mockName("endpoint.put");
const input = canvas.getByRole("textbox");
const put_string = "new value";

await expect(input).toHaveDisplayValue(args.endpoint.data.string_val as string);

await userEvent.clear(input);
await userEvent.type(input, put_string);

await expect(put).not.toHaveBeenCalled();
await userEvent.keyboard("[Enter]");

await expect(args.pre_method).toHaveBeenCalledWith({value: put_string});
await expect(put).toHaveBeenCalledWith({"value": put_string.toUpperCase()}, args.fullpath);
await expect(input).toHaveDisplayValue(put_string.toUpperCase());


}
}

/** Input will be readonly if the parameter is not writeable */
export const ReadOnly: Story = {
args: {
fullpath: "rand_num"
Expand Down
6 changes: 3 additions & 3 deletions lib/components/WithEndpoint/EndpointInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { MetadataValue } from '../AdapterEndpoint/AdapterEndpoint.types';
import { useRequestHandler, type EndpointProps } from './util';


type EndpointInputProps<PreArgs extends unknown[], PostArgs extends unknown[]> =
type EndpointInputProps<PreArgs extends Record<string, unknown>, PostArgs extends Record<string, unknown>> =
EndpointProps<PreArgs, PostArgs> & Omit<FormControlProps, keyof EndpointProps<PreArgs, PostArgs>>;


Expand All @@ -21,7 +21,7 @@ type EndpointInputProps<PreArgs extends unknown[], PostArgs extends unknown[]> =
*
* Can be used for both string and number based Parameters.
*/
const EndpointInput = <PreArgs extends unknown[], PostArgs extends unknown[]>(
const EndpointInput = <PreArgs extends Record<string, unknown>, PostArgs extends Record<string, unknown>>(
{ endpoint, fullpath, value, disabled, min, max,
pre_method, pre_args,
post_method, post_args,
Expand Down Expand Up @@ -78,7 +78,7 @@ const EndpointInput = <PreArgs extends unknown[], PostArgs extends unknown[]>(

return (
<Form.Control ref={component} onChange={onChangeHandler} onKeyUp={onEnterHandler} value={compVal}
min={compMin} max={compMax} disabled={disable} type={type} {...rest} style={style} />
min={compMin} max={compMax} disabled={disable} type={type} style={style} {...rest} />
)
}

Expand Down
Loading