@unitedstatespowersquadrons/components 1.0.8 → 1.0.10
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/ActionButton.tsx +4 -4
- package/BaseActionButton.tsx +9 -9
- package/Flags/Officer.tsx +1 -1
- package/Flags/Signal.tsx +1 -1
- package/IconButton.tsx +3 -3
- package/Reducer/Example/reducer.tsx +64 -0
- package/Reducer/Example/types.ts +9 -0
- package/Reducer/buildReducer.tsx +60 -0
- package/Reducer/index.tsx +3 -0
- package/Reducer/types.ts +34 -0
- package/Reducer/wrapDispatch.tsx +74 -0
- package/SelectionIconButton.tsx +2 -2
- package/StripedTable/Table.tsx +1 -1
- package/Toasts/Toast.tsx +4 -4
- package/Toasts/Toasts.tsx +3 -3
- package/Toasts/index.tsx +6 -3
- package/Toasts/toastHelpers.ts +47 -0
- package/eslint.config.mjs +9 -1
- package/index.tsx +3 -0
- package/package.json +4 -2
- package/types.ts +1 -16
- package/Toasts/createDismissToast.ts +0 -16
package/ActionButton.tsx
CHANGED
|
@@ -76,7 +76,7 @@ const ActionButton = (props: ActionButtonProps) => {
|
|
|
76
76
|
const buttonContents = () => {
|
|
77
77
|
return(
|
|
78
78
|
<>
|
|
79
|
-
<FontAwesomeIcon
|
|
79
|
+
<FontAwesomeIcon fa={fa} icon={icon} mode={mode} style={style} />
|
|
80
80
|
<span>{text}</span>
|
|
81
81
|
{subtext && <small className={classes.subText}>({subtext})</small>}
|
|
82
82
|
</>
|
|
@@ -89,10 +89,10 @@ const ActionButton = (props: ActionButtonProps) => {
|
|
|
89
89
|
return(
|
|
90
90
|
<BaseActionButton
|
|
91
91
|
className={classNames(classes.button, colorClassName, sizeClassName(), className)}
|
|
92
|
+
confirm={confirm}
|
|
92
93
|
id={id}
|
|
93
94
|
onClick={onClick}
|
|
94
95
|
title={title}
|
|
95
|
-
confirm={confirm}
|
|
96
96
|
>
|
|
97
97
|
{buttonContents()}
|
|
98
98
|
</BaseActionButton>
|
|
@@ -103,11 +103,11 @@ const ActionButton = (props: ActionButtonProps) => {
|
|
|
103
103
|
return(
|
|
104
104
|
<BaseActionButton
|
|
105
105
|
className={classNames(classes.button, colorClassName, sizeClassName(), className)}
|
|
106
|
+
confirm={confirm}
|
|
106
107
|
href={href}
|
|
107
108
|
id={id}
|
|
108
109
|
method={method}
|
|
109
110
|
title={title}
|
|
110
|
-
confirm={confirm}
|
|
111
111
|
>
|
|
112
112
|
{buttonContents()}
|
|
113
113
|
</BaseActionButton>
|
|
@@ -139,10 +139,10 @@ const ActionButton = (props: ActionButtonProps) => {
|
|
|
139
139
|
return(
|
|
140
140
|
<BaseActionButton
|
|
141
141
|
className={classNames(classes.button, colorClassName, sizeClassName(), className)}
|
|
142
|
+
confirm={confirm}
|
|
142
143
|
id={id}
|
|
143
144
|
onClick={onClick}
|
|
144
145
|
title={title}
|
|
145
|
-
confirm={confirm}
|
|
146
146
|
>
|
|
147
147
|
{buttonContents()}
|
|
148
148
|
</BaseActionButton>
|
package/BaseActionButton.tsx
CHANGED
|
@@ -47,11 +47,11 @@ const BaseActionButton = (props: BaseActionButtonProps) => {
|
|
|
47
47
|
|
|
48
48
|
return(
|
|
49
49
|
<button
|
|
50
|
-
type="button"
|
|
51
|
-
id={id}
|
|
52
50
|
className={classNames("action-button", classes.button, className)}
|
|
51
|
+
id={id}
|
|
53
52
|
onClick={wrappedOnClick}
|
|
54
53
|
title={title}
|
|
54
|
+
type="button"
|
|
55
55
|
>
|
|
56
56
|
{children}
|
|
57
57
|
</button>
|
|
@@ -66,8 +66,8 @@ const BaseActionButton = (props: BaseActionButtonProps) => {
|
|
|
66
66
|
|
|
67
67
|
return(
|
|
68
68
|
<form action={href} method="POST" ref={formRef}>
|
|
69
|
-
<input
|
|
70
|
-
<input
|
|
69
|
+
<input name="authenticity_token" type="hidden" value={getCsrfTokenHeader()} />
|
|
70
|
+
<input name="_method" type="hidden" value={method.toUpperCase()} />
|
|
71
71
|
</form>
|
|
72
72
|
);
|
|
73
73
|
};
|
|
@@ -86,11 +86,11 @@ const BaseActionButton = (props: BaseActionButtonProps) => {
|
|
|
86
86
|
<>
|
|
87
87
|
{form()}
|
|
88
88
|
<button
|
|
89
|
-
type="button"
|
|
90
|
-
id={id}
|
|
91
89
|
className={classNames("action-button", classes.button, className)}
|
|
90
|
+
id={id}
|
|
92
91
|
onClick={submitForm}
|
|
93
92
|
title={title}
|
|
93
|
+
type="button"
|
|
94
94
|
>
|
|
95
95
|
{children}
|
|
96
96
|
</button>
|
|
@@ -109,11 +109,11 @@ const BaseActionButton = (props: BaseActionButtonProps) => {
|
|
|
109
109
|
|
|
110
110
|
return(
|
|
111
111
|
<button
|
|
112
|
-
type="button"
|
|
113
|
-
onClick={onClick}
|
|
114
|
-
id={id}
|
|
115
112
|
className={classNames("action-button", classes.button, className)}
|
|
113
|
+
id={id}
|
|
114
|
+
onClick={onClick}
|
|
116
115
|
title={title}
|
|
116
|
+
type="button"
|
|
117
117
|
>
|
|
118
118
|
{children}
|
|
119
119
|
</button>
|
package/Flags/Officer.tsx
CHANGED
|
@@ -80,7 +80,7 @@ const OfficerFlag = (props: Props) => {
|
|
|
80
80
|
|
|
81
81
|
const combinedPath = `${flagBaseUrl}/${formatDir}/${subDirectory()}${displayRank}${sizeExtension}.${extension}`;
|
|
82
82
|
|
|
83
|
-
return <img className={className} src={combinedPath}
|
|
83
|
+
return <img alt={displayRank} className={className} src={combinedPath} />;
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
export default OfficerFlag;
|
package/Flags/Signal.tsx
CHANGED
|
@@ -52,7 +52,7 @@ const SignalFlag = (props: Props) => {
|
|
|
52
52
|
const combinedUrl = `${signalBaseUrl}/${formatDir}/${dir()}/${flagValue()}.${extension}`;
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
<img
|
|
55
|
+
<img alt={alt || flag} className={className} src={combinedUrl} />
|
|
56
56
|
);
|
|
57
57
|
};
|
|
58
58
|
|
package/IconButton.tsx
CHANGED
|
@@ -33,7 +33,7 @@ const IconButton = (props: IconButtonProps) => {
|
|
|
33
33
|
const { className, fa, icon, id, mode, style, title } = props;
|
|
34
34
|
const classes = useStyles();
|
|
35
35
|
|
|
36
|
-
const configuredIcon = <FontAwesomeIcon
|
|
36
|
+
const configuredIcon = <FontAwesomeIcon css={className} fa={fa} icon={icon} mode={mode} style={style} />;
|
|
37
37
|
|
|
38
38
|
const combinedClassName = classNames(classes.icon, className);
|
|
39
39
|
|
|
@@ -43,10 +43,10 @@ const IconButton = (props: IconButtonProps) => {
|
|
|
43
43
|
return(
|
|
44
44
|
<BaseActionButton
|
|
45
45
|
className={combinedClassName}
|
|
46
|
+
confirm={confirm}
|
|
46
47
|
id={id}
|
|
47
48
|
onClick={onClick}
|
|
48
49
|
title={title}
|
|
49
|
-
confirm={confirm}
|
|
50
50
|
>
|
|
51
51
|
{configuredIcon}
|
|
52
52
|
</BaseActionButton>
|
|
@@ -57,11 +57,11 @@ const IconButton = (props: IconButtonProps) => {
|
|
|
57
57
|
return(
|
|
58
58
|
<BaseActionButton
|
|
59
59
|
className={combinedClassName}
|
|
60
|
+
confirm={confirm}
|
|
60
61
|
href={href}
|
|
61
62
|
id={id}
|
|
62
63
|
method={method}
|
|
63
64
|
title={title}
|
|
64
|
-
confirm={confirm}
|
|
65
65
|
>
|
|
66
66
|
{configuredIcon}
|
|
67
67
|
</BaseActionButton>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
import { Draft } from "immer";
|
|
3
|
+
import { AppState, ServerResponse } from "./types";
|
|
4
|
+
import {
|
|
5
|
+
buildReducer,
|
|
6
|
+
DispatchFunc,
|
|
7
|
+
DispatchHandler,
|
|
8
|
+
RailsAppAction,
|
|
9
|
+
ReducerHandler,
|
|
10
|
+
wrapDispatch,
|
|
11
|
+
} from "../";
|
|
12
|
+
|
|
13
|
+
// If you want to add additional data to the server response, use type `RailsAppAction<ExampleServerResponse>`.
|
|
14
|
+
// Otherwise, just use type `RailsAppAction`.
|
|
15
|
+
//
|
|
16
|
+
// If you need to access the available `type`s for your defined AppAction, use `AppActionType<AppAction>`
|
|
17
|
+
//
|
|
18
|
+
type AppAction =
|
|
19
|
+
| RailsAppAction<ServerResponse>
|
|
20
|
+
| { type: "doSomething"; something: string };
|
|
21
|
+
|
|
22
|
+
export const exampleReducer = (draft: Draft<AppState>, action: AppAction): void => {
|
|
23
|
+
const { type } = action;
|
|
24
|
+
|
|
25
|
+
switch (type) {
|
|
26
|
+
case "doSomething":
|
|
27
|
+
draft.something = "changed";
|
|
28
|
+
|
|
29
|
+
break;
|
|
30
|
+
case "saveComplete": break;
|
|
31
|
+
case "saveFailed": break;
|
|
32
|
+
default: return type satisfies never;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const doSomething: ReducerHandler<AppAction> = (
|
|
37
|
+
dispatch: DispatchFunc<AppAction>
|
|
38
|
+
): DispatchFunc<AppAction> => {
|
|
39
|
+
const actionType = "doSomething";
|
|
40
|
+
|
|
41
|
+
const handler: DispatchHandler<AppAction> = (action: AppAction) => {
|
|
42
|
+
if (action.type !== actionType) return {};
|
|
43
|
+
|
|
44
|
+
const { something } = action;
|
|
45
|
+
|
|
46
|
+
const bodyJson = { something };
|
|
47
|
+
const href = "/do_something";
|
|
48
|
+
|
|
49
|
+
return { bodyJson, href };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// If you want to add additional data to the server response, pass in type `ServerResponse`.
|
|
53
|
+
// Otherwise, just leave the second type parameter unspecified.
|
|
54
|
+
return wrapDispatch<AppAction, ServerResponse>(dispatch, [actionType], handler);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const { AppProvider, useAppDispatch, useAppState } = buildReducer(
|
|
58
|
+
exampleReducer,
|
|
59
|
+
createContext<AppState | null>(null),
|
|
60
|
+
createContext<React.Dispatch<AppAction> | null>(null),
|
|
61
|
+
[
|
|
62
|
+
doSomething,
|
|
63
|
+
]
|
|
64
|
+
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useContext, useMemo } from "react";
|
|
2
|
+
import { Draft, produce } from "immer";
|
|
3
|
+
import { useImmerReducer } from "use-immer";
|
|
4
|
+
import { ReducerHandler } from "./types";
|
|
5
|
+
|
|
6
|
+
interface InitialAppProps<AppState> {
|
|
7
|
+
initialState: AppState;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function buildReducer<AppState, AppAction>(
|
|
12
|
+
reducer: (draft: Draft<AppState>, action: AppAction) => void,
|
|
13
|
+
StateContext: React.Context<AppState | null>,
|
|
14
|
+
DispatchContext: React.Context<React.Dispatch<AppAction> | null>,
|
|
15
|
+
handlers: ReducerHandler<AppAction>[] = []
|
|
16
|
+
) {
|
|
17
|
+
function AppProvider(props: InitialAppProps<AppState>) {
|
|
18
|
+
const [providerState, dispatch] = useImmerReducer(reducer, props.initialState, (state: AppState) => {
|
|
19
|
+
return produce(state, draft => draft);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const wrappedDispatch = useMemo(() => {
|
|
23
|
+
let d = dispatch;
|
|
24
|
+
|
|
25
|
+
handlers.forEach(handler => d = handler(d));
|
|
26
|
+
|
|
27
|
+
return d;
|
|
28
|
+
}, [dispatch]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<StateContext.Provider value={providerState}>
|
|
32
|
+
<DispatchContext.Provider value={wrappedDispatch}>
|
|
33
|
+
{props.children}
|
|
34
|
+
</DispatchContext.Provider>
|
|
35
|
+
</StateContext.Provider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function useAppState() {
|
|
40
|
+
const context = useContext(StateContext);
|
|
41
|
+
if (!context) {
|
|
42
|
+
throw new Error("useAppState has to be used within AppProvider");
|
|
43
|
+
}
|
|
44
|
+
return context;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function useAppDispatch() {
|
|
48
|
+
const context = useContext(DispatchContext);
|
|
49
|
+
if (!context) {
|
|
50
|
+
throw new Error("useAppDispatch has to be used within AppProvider");
|
|
51
|
+
}
|
|
52
|
+
return context;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
AppProvider,
|
|
57
|
+
useAppDispatch,
|
|
58
|
+
useAppState,
|
|
59
|
+
};
|
|
60
|
+
}
|
package/Reducer/types.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Toast } from "../Toasts";
|
|
2
|
+
|
|
3
|
+
export interface RailsResponse {
|
|
4
|
+
reload?: boolean;
|
|
5
|
+
status: "OK" | "ERROR";
|
|
6
|
+
toasts: Toast[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type DispatchFunc<T> = (action: T) => void;
|
|
10
|
+
export type DispatchHandler<T> = (action: T) => {
|
|
11
|
+
body?: string;
|
|
12
|
+
bodyJson?: object;
|
|
13
|
+
headers?: { [key: string]: string };
|
|
14
|
+
href?: string;
|
|
15
|
+
method?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ReducerHandler<AppAction> = (dispatch: DispatchFunc<AppAction>) => DispatchFunc<AppAction>;
|
|
19
|
+
|
|
20
|
+
export interface GenericAppAction {
|
|
21
|
+
type: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type RailsAppAction<R extends RailsResponse = RailsResponse> =
|
|
25
|
+
| { type: "saveComplete"; res: R }
|
|
26
|
+
| { type: "saveFailed" };
|
|
27
|
+
|
|
28
|
+
type ExtractField<T, K extends string> = T extends { [key in K]: infer U } ? U : never;
|
|
29
|
+
export type AppActionType<
|
|
30
|
+
A extends GenericAppAction,
|
|
31
|
+
Res extends RailsResponse = RailsResponse
|
|
32
|
+
> =
|
|
33
|
+
| ExtractField<A, "type">
|
|
34
|
+
| ExtractField<RailsAppAction<Res>, "type">;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppActionType,
|
|
3
|
+
DispatchFunc,
|
|
4
|
+
DispatchHandler,
|
|
5
|
+
GenericAppAction,
|
|
6
|
+
RailsAppAction,
|
|
7
|
+
RailsResponse,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import railsFetchJson from "../railsFetchJson";
|
|
10
|
+
|
|
11
|
+
type AppAction<Res extends RailsResponse = RailsResponse> =
|
|
12
|
+
| GenericAppAction
|
|
13
|
+
| RailsAppAction<Res>;
|
|
14
|
+
|
|
15
|
+
export default function wrapDispatch<
|
|
16
|
+
A extends AppAction<ServerResponse>,
|
|
17
|
+
ServerResponse extends RailsResponse = RailsResponse
|
|
18
|
+
>(
|
|
19
|
+
dispatch: DispatchFunc<A>,
|
|
20
|
+
appActionTypes: AppActionType<A>[],
|
|
21
|
+
handler: DispatchHandler<A>
|
|
22
|
+
): DispatchFunc<A> {
|
|
23
|
+
const updateDebounceTime = (("testEnv" in window) && window.testEnv) ? 50 : 250;
|
|
24
|
+
|
|
25
|
+
const updateHandler = async (action: A, signal: AbortSignal) => {
|
|
26
|
+
const { bodyJson, headers, href, method } = handler(action);
|
|
27
|
+
|
|
28
|
+
if (!href || !bodyJson) return;
|
|
29
|
+
|
|
30
|
+
const requestHeaders = headers || {};
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const res = await railsFetchJson<ServerResponse>(
|
|
34
|
+
href,
|
|
35
|
+
{ bodyJson, headers: requestHeaders, method: method || "POST", signal }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
dispatch({ res, type: "saveComplete" } as A);
|
|
39
|
+
} catch(e) {
|
|
40
|
+
// Ignore request aborts
|
|
41
|
+
if (e instanceof DOMException) return;
|
|
42
|
+
|
|
43
|
+
dispatch({ type: "saveFailed" } as A);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const timers: { [id: string]: number } = {};
|
|
48
|
+
const abortControllers: { [id: string]: AbortController } = {};
|
|
49
|
+
|
|
50
|
+
return (action: A) => {
|
|
51
|
+
// Run it like normal
|
|
52
|
+
dispatch(action);
|
|
53
|
+
|
|
54
|
+
// Skip if it's not one of the appActionTypes we care about
|
|
55
|
+
if (!appActionTypes.includes(action.type as AppActionType<A>)) return;
|
|
56
|
+
|
|
57
|
+
const timeoutKey = "userTimeout";
|
|
58
|
+
|
|
59
|
+
// Stop previous request
|
|
60
|
+
const timer = timers[timeoutKey];
|
|
61
|
+
if (timer) clearTimeout(timer);
|
|
62
|
+
delete timers[timeoutKey];
|
|
63
|
+
const oldAbortController = abortControllers[timeoutKey];
|
|
64
|
+
if (oldAbortController) oldAbortController.abort();
|
|
65
|
+
delete abortControllers[timeoutKey];
|
|
66
|
+
|
|
67
|
+
// Make request
|
|
68
|
+
const abortController = abortControllers[timeoutKey] = new AbortController();
|
|
69
|
+
const signal = abortController.signal;
|
|
70
|
+
timers[timeoutKey] = setTimeout(() => {
|
|
71
|
+
void updateHandler(action, signal);
|
|
72
|
+
}, updateDebounceTime);
|
|
73
|
+
};
|
|
74
|
+
}
|
package/SelectionIconButton.tsx
CHANGED
|
@@ -22,12 +22,12 @@ const SelectionIconButton = (props: Props) => {
|
|
|
22
22
|
|
|
23
23
|
return(
|
|
24
24
|
<IconButton
|
|
25
|
+
className={classNames(classes.icon, className)}
|
|
25
26
|
icon={icon}
|
|
26
27
|
id={id}
|
|
28
|
+
mode="duotone"
|
|
27
29
|
onClick={onClick}
|
|
28
30
|
title={title}
|
|
29
|
-
className={classNames(classes.icon, className)}
|
|
30
|
-
mode="duotone"
|
|
31
31
|
/>
|
|
32
32
|
);
|
|
33
33
|
};
|
package/StripedTable/Table.tsx
CHANGED
package/Toasts/Toast.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import classNames from "classnames";
|
|
3
3
|
import { createUseStyles } from "react-jss";
|
|
4
|
-
import { DismissFunc,
|
|
4
|
+
import { DismissFunc, Toast_Status, ToastProps } from "./types";
|
|
5
5
|
import FontAwesomeIcon from "../FontAwesomeIcon";
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
@@ -122,12 +122,12 @@ const Toast = (props: Props) => {
|
|
|
122
122
|
return (
|
|
123
123
|
<div className={classNames(classes.toast, statusClassName, opacityClass)} key={`toast-${id}`}>
|
|
124
124
|
<div className={classes.toastTitle}>
|
|
125
|
-
<FontAwesomeIcon icon={icon} mode="duotone"
|
|
125
|
+
<FontAwesomeIcon fa="fw" icon={icon} mode="duotone" />
|
|
126
126
|
<span>{title}</span>
|
|
127
127
|
</div>
|
|
128
128
|
<div className={classes.toastBody}>{renderBody()}</div>
|
|
129
|
-
<a className={classes.toastDismiss} title="Dismiss this message"
|
|
130
|
-
<FontAwesomeIcon icon="times" mode="duotone"
|
|
129
|
+
<a className={classes.toastDismiss} onClick={handleDismiss} title="Dismiss this message">
|
|
130
|
+
<FontAwesomeIcon css={classes.toastDismissIcon} fa="fw" icon="times" mode="duotone" />
|
|
131
131
|
</a>
|
|
132
132
|
</div>
|
|
133
133
|
);
|
package/Toasts/Toasts.tsx
CHANGED
|
@@ -2,9 +2,9 @@ import React from "react";
|
|
|
2
2
|
import classNames from "classnames";
|
|
3
3
|
import { createUseStyles } from "react-jss";
|
|
4
4
|
import Toast from "./Toast";
|
|
5
|
-
import createDismissToast from "./
|
|
5
|
+
import { createDismissToast } from "./toastHelpers";
|
|
6
6
|
import { DismissFunc, ToastProps, ToastRemoveAppAction } from "./types";
|
|
7
|
-
import { DispatchFunc } from "../
|
|
7
|
+
import { DispatchFunc } from "../";
|
|
8
8
|
|
|
9
9
|
interface BaseProps {
|
|
10
10
|
toasts?: ToastProps[];
|
|
@@ -50,7 +50,7 @@ const Toasts = (props: Props) => {
|
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
52
|
<div className={classNames("toastsContainer", classes.toastsContainer)}>
|
|
53
|
-
{toasts?.map(toast => <Toast dismiss={dismissFunc()}
|
|
53
|
+
{toasts?.map(toast => <Toast dismiss={dismissFunc()} key={toast.id} toast={toast} />)}
|
|
54
54
|
</div>
|
|
55
55
|
);
|
|
56
56
|
};
|
package/Toasts/index.tsx
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { default as Toasts } from "./Toasts";
|
|
2
|
-
export
|
|
2
|
+
export * from "./toastHelpers";
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
export type
|
|
4
|
+
export * from "./types";
|
|
5
|
+
export type * from "./types";
|
|
6
|
+
|
|
7
|
+
// Legacy exports for compatibility
|
|
8
|
+
export { createDismissToast } from "./toastHelpers";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Draft } from "immer";
|
|
2
|
+
import { Toast, ToastProps, ToastRemoveAppAction } from "./types";
|
|
3
|
+
import { AppStateWithToasts, DispatchFunc } from "../";
|
|
4
|
+
|
|
5
|
+
export function createDismissToast(dispatch: DispatchFunc<ToastRemoveAppAction>) {
|
|
6
|
+
const dismissToast = (toastId: number) => {
|
|
7
|
+
dispatch({ toastId, type: "dismissToast" });
|
|
8
|
+
|
|
9
|
+
setTimeout(() => dispatch({ toastId, type: "removeToast" }), 2000);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return dismissToast;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function handleToasts<T extends AppStateWithToasts>(draft: Draft<T>, toasts: Toast[]) {
|
|
16
|
+
const handleToastsFunc = () => {
|
|
17
|
+
if (toasts.length > 0) {
|
|
18
|
+
toasts.forEach(toast => addToast(draft, toast));
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return handleToastsFunc;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function addToast<T extends AppStateWithToasts>(draft: Draft<T>, toastOptions: Toast) {
|
|
26
|
+
const { body, status, timeout, title } = toastOptions;
|
|
27
|
+
|
|
28
|
+
// Increment current toastId to prevent collisions
|
|
29
|
+
const id = draft.toastId ? draft.toastId + 1 : 1;
|
|
30
|
+
draft.toastId = id;
|
|
31
|
+
|
|
32
|
+
const toast = { body, id, status, timeout, title } as ToastProps;
|
|
33
|
+
|
|
34
|
+
// Initialize toasts array if not provided by the server
|
|
35
|
+
if (!draft.toasts) draft.toasts = [];
|
|
36
|
+
|
|
37
|
+
// Do not store multiple identical toasts
|
|
38
|
+
if (draft.toasts.some(t => {
|
|
39
|
+
return (
|
|
40
|
+
toast.body === t.body &&
|
|
41
|
+
toast.status === t.status &&
|
|
42
|
+
toast.title === t.title
|
|
43
|
+
);
|
|
44
|
+
})) return;
|
|
45
|
+
|
|
46
|
+
draft.toasts.push(toast);
|
|
47
|
+
}
|
package/eslint.config.mjs
CHANGED
|
@@ -30,6 +30,13 @@ export default tseslint.config(
|
|
|
30
30
|
"quotes": ["error", "double"],
|
|
31
31
|
"semi": ["error", "always"],
|
|
32
32
|
"sort-keys": ["error", "asc", { caseSensitive: true, minKeys: 2, natural: false }],
|
|
33
|
+
"sort-imports": [
|
|
34
|
+
"error",
|
|
35
|
+
{
|
|
36
|
+
"ignoreCase": true,
|
|
37
|
+
"ignoreDeclarationSort": true,
|
|
38
|
+
}
|
|
39
|
+
],
|
|
33
40
|
"arrow-parens": ["error", "as-needed"],
|
|
34
41
|
"comma-dangle": [
|
|
35
42
|
"error",
|
|
@@ -145,11 +152,12 @@ export default tseslint.config(
|
|
|
145
152
|
"stylistic/no-extra-semi": "error",
|
|
146
153
|
"stylistic/semi": ["error", "always"],
|
|
147
154
|
"stylistic/no-multiple-empty-lines": "error",
|
|
155
|
+
"stylistic/jsx-sort-props": "error",
|
|
148
156
|
"stylistic/array-element-newline": [
|
|
149
157
|
"error",
|
|
150
158
|
{
|
|
151
159
|
"ArrayExpression": "consistent",
|
|
152
|
-
"ArrayPattern": { "minItems":
|
|
160
|
+
"ArrayPattern": { "minItems": 5 },
|
|
153
161
|
}
|
|
154
162
|
],
|
|
155
163
|
"stylistic/object-property-newline": [
|
package/index.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export * from "./Reducer";
|
|
2
|
+
|
|
1
3
|
export { default as FontAwesomeIcon } from "./FontAwesomeIcon";
|
|
2
4
|
|
|
3
5
|
export { default as railsFetchJson } from "./railsFetchJson";
|
|
@@ -17,6 +19,7 @@ export { default as Colors } from "./Colors";
|
|
|
17
19
|
export { default as Styles } from "./Styles";
|
|
18
20
|
|
|
19
21
|
export type * from "./types";
|
|
22
|
+
export type * from "./Reducer";
|
|
20
23
|
export type * from "./Colors";
|
|
21
24
|
export type * from "./FontAwesomeIcon";
|
|
22
25
|
export type * from "./ActionButton";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unitedstatespowersquadrons/components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "USPS shared React components library",
|
|
5
5
|
"main": "index.tsx",
|
|
6
6
|
"scripts": {
|
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
"homepage": "https://github.com/unitedstatespowersquadrons/components#readme",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"classnames": "^2.5.1",
|
|
21
|
+
"immer": "^10.1.1",
|
|
21
22
|
"react": "^19.1.0",
|
|
22
23
|
"react-jss": "^10.10.0",
|
|
23
|
-
"typescript": "^5.8.3"
|
|
24
|
+
"typescript": "^5.8.3",
|
|
25
|
+
"use-immer": "^0.11.0"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
26
28
|
"@stylistic/eslint-plugin": "^5.1.0",
|
package/types.ts
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { ToastProps } from "./Toasts";
|
|
3
3
|
|
|
4
4
|
export interface AppStateWithToasts {
|
|
5
5
|
toasts: ToastProps[];
|
|
6
6
|
toastId?: number;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export type DispatchFunc<T> = (action: T) => void;
|
|
10
|
-
export type DispatchHandler<T> = (action: T) => {
|
|
11
|
-
body?: string;
|
|
12
|
-
bodyJson?: object;
|
|
13
|
-
headers?: { [key: string]: string };
|
|
14
|
-
href?: string;
|
|
15
|
-
method?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
9
|
export type OnClickHandler = (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
19
10
|
export type OnInputChangeHandler = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void;
|
|
20
11
|
export type OnClickOrKeyboardHandler = (event: OnClickOrKeyboardEvent) => void;
|
|
@@ -44,9 +35,3 @@ export interface OnClickProps {
|
|
|
44
35
|
confirm?: string | undefined;
|
|
45
36
|
onClick: OnClickHandler;
|
|
46
37
|
}
|
|
47
|
-
|
|
48
|
-
export interface RailsResponse {
|
|
49
|
-
reload?: boolean;
|
|
50
|
-
status: "OK" | "ERROR";
|
|
51
|
-
toasts: Toast[];
|
|
52
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { ToastRemoveAppAction } from "./types";
|
|
2
|
-
import { DispatchFunc } from "../types";
|
|
3
|
-
|
|
4
|
-
const createDismissToast = (dispatch: DispatchFunc<ToastRemoveAppAction>) => {
|
|
5
|
-
const dismissToast = (toastId: number) => {
|
|
6
|
-
dispatch({ toastId, type: "dismissToast" });
|
|
7
|
-
|
|
8
|
-
setTimeout(() => {
|
|
9
|
-
dispatch({ toastId, type: "removeToast" });
|
|
10
|
-
}, 2000);
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
return dismissToast;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export default createDismissToast;
|