Create working PoC of frontend app
This commit is contained in:
@@ -7,5 +7,5 @@ buildNpmPackage {
|
|||||||
pname = "actual-importer-frontend";
|
pname = "actual-importer-frontend";
|
||||||
version = "0.0.1";
|
version = "0.0.1";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
npmDepsHash = "sha256-CC9mDUyLBPigwlccQMauNoqzHKMeOppPu5y2eJXIzLY=";
|
npmDepsHash = "sha256-+HDXY0d3gvXJWy4GPa6vCIqLnq1mCP8kimek5Zl8u80=";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import LoadFilePage from "./pages/LoadFilePage/LoadFilePage";
|
import LoadFilePage from "./pages/LoadFilePage/LoadFilePage";
|
||||||
|
import PrepareTransactionsPage from "./pages/PrepareTransactionsPage/PrepareTransactionsPage";
|
||||||
|
import ResultPage from "./pages/ResultPage/ResultPage";
|
||||||
import { AppStoreProvider } from "./store/AppStore";
|
import { AppStoreProvider } from "./store/AppStore";
|
||||||
|
import { Wizard } from "./wizard/Wizard";
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<AppStoreProvider>
|
<AppStoreProvider>
|
||||||
<LoadFilePage />
|
<Wizard>
|
||||||
|
<LoadFilePage />
|
||||||
|
<PrepareTransactionsPage />
|
||||||
|
<ResultPage />
|
||||||
|
</Wizard>
|
||||||
</AppStoreProvider>
|
</AppStoreProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ export default function LoadFilePage() {
|
|||||||
...data
|
...data
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'MOVE_STEP',
|
||||||
|
payload: {
|
||||||
|
offset: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
77
web/src/pages/PrepareTransactionsPage/ImportBar.tsx
Normal file
77
web/src/pages/PrepareTransactionsPage/ImportBar.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import type { ImportOptions } from "../../types/api";
|
||||||
|
|
||||||
|
export type ImportBarProps = {
|
||||||
|
onSubmit: (opts: ImportOptions) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ImportBar({ onSubmit }: ImportBarProps) {
|
||||||
|
const [formData, setFormData] = useState<ImportOptions>({ mode: 'import' });
|
||||||
|
|
||||||
|
|
||||||
|
const handleSubmit = useCallback((e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
onSubmit(formData);
|
||||||
|
}, [formData, onSubmit]);
|
||||||
|
|
||||||
|
const changeMode = useCallback((mode: ImportOptions['mode']) => {
|
||||||
|
if (mode === 'add') {
|
||||||
|
setFormData({ ...formData, mode });
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
setFormData({ mode });
|
||||||
|
}
|
||||||
|
}, [formData, setFormData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Import mode</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="buttons has-addons is-left">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`button ${formData.mode === 'import' ? "is-primary is-selected" : ""}`}
|
||||||
|
onClick={() => changeMode('import')}>Import</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`button ${formData.mode === 'add' ? "is-primary is-selected" : ""}`}
|
||||||
|
onClick={() => changeMode('add')}>Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.mode === 'add' && <div className="field">
|
||||||
|
<label className="label">Additional options</label>
|
||||||
|
<div className="control">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.learnCategories}
|
||||||
|
onChange={e => setFormData({ ...formData, learnCategories: e.target.checked})}/> Learn categories
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="control">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.runTransfers}
|
||||||
|
onChange={e => setFormData({ ...formData, runTransfers: e.target.checked})}/> Run transfers
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
<div className="field mt-5">
|
||||||
|
<div className="control">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="button is-link is-fullwidth">Submit transactions</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { useStore } from "../../store/AppStore";
|
||||||
|
import TransactionsTable from "./TransactionsTable";
|
||||||
|
import { SkippedLines } from "./SkippedLines";
|
||||||
|
import ImportBar from "./ImportBar";
|
||||||
|
import type { ImportOptions } from "../../types/api";
|
||||||
|
import { submitTransactions } from "../../services/api.service";
|
||||||
|
|
||||||
|
|
||||||
|
export default function PrepareTransactionsPage() {
|
||||||
|
const { state, dispatch } = useStore();
|
||||||
|
|
||||||
|
const desiredTransactions = useMemo(() => state.transactions.filter((_, i) => !state.filteredOut.includes(i)), [state, state.filteredOut]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(async (opts: ImportOptions) => {
|
||||||
|
if(!state.server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await submitTransactions(desiredTransactions, state.server, opts);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_RESULT',
|
||||||
|
payload: response
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'MOVE_STEP',
|
||||||
|
payload: {
|
||||||
|
offset: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [desiredTransactions, state]);
|
||||||
|
|
||||||
|
const filterOut = useCallback((index: number) => dispatch({
|
||||||
|
type: 'FILTER_OUT',
|
||||||
|
payload: { index }
|
||||||
|
}), [dispatch]);
|
||||||
|
|
||||||
|
const removeFilter = useCallback((index: number) => dispatch({
|
||||||
|
type: 'REMOVE_FILTER',
|
||||||
|
payload: { index }
|
||||||
|
}), [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="columns mt-6">
|
||||||
|
<div className="column is-8 is-offset-2">
|
||||||
|
<div className="box">
|
||||||
|
<h2 className="title is-4 has-text-centered">Prepare Transactions</h2>
|
||||||
|
|
||||||
|
<div className="content">
|
||||||
|
<h3 className="title is-5">Considered transactions ({state.transactions.length})</h3>
|
||||||
|
<TransactionsTable
|
||||||
|
transactions={state.transactions}
|
||||||
|
filteredOut={state.filteredOut}
|
||||||
|
filterOut={filterOut}
|
||||||
|
removeFilter={removeFilter} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content">
|
||||||
|
<h3 className="title is-5">Skipped lines ({state.skipped.length})</h3>
|
||||||
|
<SkippedLines skipped={state.skipped} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content">
|
||||||
|
<ImportBar onSubmit={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.skipped {
|
||||||
|
content: red;
|
||||||
|
}
|
||||||
19
web/src/pages/PrepareTransactionsPage/SkippedLines.tsx
Normal file
19
web/src/pages/PrepareTransactionsPage/SkippedLines.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import styles from "./SkippedLines.module.css";
|
||||||
|
|
||||||
|
export type SkippedLinesProps = {
|
||||||
|
skipped: string[][];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SkippedLines({ skipped }: SkippedLinesProps) {
|
||||||
|
if (skipped.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.skipped}>
|
||||||
|
<pre>
|
||||||
|
{skipped.map(s => `▶️ ${s.join("🔸")}`).join("\n")}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.transactionTable tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled td {
|
||||||
|
color: var(--bulma-text-light);
|
||||||
|
}
|
||||||
63
web/src/pages/PrepareTransactionsPage/TransactionsTable.tsx
Normal file
63
web/src/pages/PrepareTransactionsPage/TransactionsTable.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import type { Transaction } from "../../types/api";
|
||||||
|
import styles from "./TransactionTable.module.css";
|
||||||
|
|
||||||
|
export type TransactionsTableProps = {
|
||||||
|
transactions: Transaction[];
|
||||||
|
filteredOut: number[];
|
||||||
|
filterOut: (index: number) => void;
|
||||||
|
removeFilter: (index: number) => void;
|
||||||
|
readonly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TransactionsTable({ transactions, filteredOut, filterOut, removeFilter, readonly }: TransactionsTableProps) {
|
||||||
|
|
||||||
|
const changeSelection = useCallback((index: number) => {
|
||||||
|
if (readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(filteredOut.includes(index)) {
|
||||||
|
removeFilter(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
filterOut(index);
|
||||||
|
}
|
||||||
|
}, [filteredOut, filterOut, removeFilter]);
|
||||||
|
|
||||||
|
|
||||||
|
const renderRow = useCallback((transaction: Transaction, index: number) => (
|
||||||
|
<tr key={index} onClick={() => changeSelection(index)} className={`${!readonly && filteredOut.includes(index) ? styles.disabled : ""}`}>
|
||||||
|
<td>{transaction.kind}</td>
|
||||||
|
<td>{transaction.date}</td>
|
||||||
|
<td>{transaction.from}</td>
|
||||||
|
<td>{transaction.to}</td>
|
||||||
|
<td>{transaction.amount}</td>
|
||||||
|
<td>{transaction.title}</td>
|
||||||
|
<td>{transaction.id}</td>
|
||||||
|
</tr>
|
||||||
|
), [changeSelection]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="table-container">
|
||||||
|
<table className={`table is-hoverable ${!readonly ? styles.transactionTable : ""}`}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<th>Kind</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>From</th>
|
||||||
|
<th>To</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>ID</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{transactions.map(renderRow)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
web/src/pages/ResultPage/ResultPage.tsx
Normal file
47
web/src/pages/ResultPage/ResultPage.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useStore } from "../../store/AppStore";
|
||||||
|
|
||||||
|
export default function ResultPage() {
|
||||||
|
const { state } = useStore();
|
||||||
|
|
||||||
|
const errors = useMemo(() => {
|
||||||
|
if (!state.result?.errors) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.result.errors.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="notification is-danger">
|
||||||
|
<p>Errors:</p>
|
||||||
|
<ul>
|
||||||
|
{state.result.errors.map(e => <li>{e.message}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [state.result?.errors]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="columns mt-6">
|
||||||
|
<div className="column is-8 is-offset-2">
|
||||||
|
<div className="box">
|
||||||
|
<h2 className="title is-4 has-text-centered">Import Result</h2>
|
||||||
|
<div className="content">
|
||||||
|
<div className="notification is-info">
|
||||||
|
<ul>
|
||||||
|
<li>Added: <strong>{state.result?.added?.length ?? 0}</strong></li>
|
||||||
|
<li>Updated: <strong>{state.result?.updated?.length ?? 0}</strong></li>
|
||||||
|
<li>Errors: <strong>{state.result?.errors?.length ?? 0}</strong></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="content">
|
||||||
|
{errors}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ConfigResponse, PrepareResponse } from "../types/api";
|
import type { ConfigResponse, ImportOptions, PrepareResponse, SubmitResponse, Transaction } from "../types/api";
|
||||||
|
|
||||||
export async function fetchConfig(): Promise<ConfigResponse> {
|
export async function fetchConfig(): Promise<ConfigResponse> {
|
||||||
const response = await fetch("http://localhost:3000/config")
|
const response = await fetch("/config")
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data as ConfigResponse;
|
return data as ConfigResponse;
|
||||||
}
|
}
|
||||||
@@ -12,11 +12,30 @@ export async function loadTransactions(csvFile: File, profile: string, server: s
|
|||||||
payload.append("profile", profile);
|
payload.append("profile", profile);
|
||||||
payload.append("server", server);
|
payload.append("server", server);
|
||||||
|
|
||||||
const response = await fetch("http://localhost:3000/prepare", {
|
const response = await fetch("/prepare", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: payload
|
body: payload
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data as PrepareResponse;
|
return data as PrepareResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitTransactions(transactions: Transaction[], server: string, opts: ImportOptions): Promise<SubmitResponse> {
|
||||||
|
const payload = {
|
||||||
|
transactions,
|
||||||
|
server,
|
||||||
|
opts
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("/submit", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data as SubmitResponse;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { State, Action, StoreContext } from "../types/store";
|
import type { StoreContext } from "../types/store";
|
||||||
import { createContext, useContext, useMemo, useReducer } from "react";
|
import { createContext, useContext, useMemo, useReducer } from "react";
|
||||||
|
import { initialState } from "./state";
|
||||||
|
import { reducer } from "./reducer";
|
||||||
|
|
||||||
export type AppStoreProviderProps = {
|
export type AppStoreProviderProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -8,34 +10,6 @@ export type AppStoreProviderProps = {
|
|||||||
export type UseStoreReturnType = StoreContext & {
|
export type UseStoreReturnType = StoreContext & {
|
||||||
};
|
};
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
|
||||||
switch(action.type) {
|
|
||||||
case 'UPDATE_CONFIG':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
availableProfiles: action.payload.profiles,
|
|
||||||
availableServers: action.payload.servers,
|
|
||||||
defaultProfile: action.payload.defaultProfile,
|
|
||||||
defaultServer: action.payload.defaultServer,
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'UPDATE_DATA':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
profile: action.payload.profile,
|
|
||||||
server: action.payload.server,
|
|
||||||
transactions: action.payload.transactions,
|
|
||||||
skipped: action.payload.skipped,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: State = {
|
|
||||||
availableProfiles: [],
|
|
||||||
availableServers: [],
|
|
||||||
transactions: [],
|
|
||||||
skipped: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppContext = createContext<StoreContext>({
|
const AppContext = createContext<StoreContext>({
|
||||||
state: initialState,
|
state: initialState,
|
||||||
|
|||||||
59
web/src/store/reducer.ts
Normal file
59
web/src/store/reducer.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { Action, State } from "../types/store";
|
||||||
|
|
||||||
|
export function reducer(state: State, action: Action): State {
|
||||||
|
switch(action.type) {
|
||||||
|
case 'UPDATE_CONFIG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
availableProfiles: action.payload.profiles,
|
||||||
|
availableServers: action.payload.servers,
|
||||||
|
defaultProfile: action.payload.defaultProfile,
|
||||||
|
defaultServer: action.payload.defaultServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_DATA':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
profile: action.payload.profile,
|
||||||
|
server: action.payload.server,
|
||||||
|
transactions: action.payload.transactions,
|
||||||
|
skipped: action.payload.skipped,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_STEP':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
wizard: {
|
||||||
|
...state.wizard,
|
||||||
|
step: action.payload.step
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MOVE_STEP':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
wizard: {
|
||||||
|
...state.wizard,
|
||||||
|
step: state.wizard.step + action.payload.offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'FILTER_OUT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filteredOut: [...state.filteredOut, action.payload.index]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'REMOVE_FILTER':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filteredOut: state.filteredOut.filter(x => x !== action.payload.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_RESULT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
result: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
web/src/store/state.ts
Normal file
14
web/src/store/state.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { State } from "../types/store";
|
||||||
|
|
||||||
|
export const initialState: State = {
|
||||||
|
availableProfiles: [],
|
||||||
|
availableServers: [],
|
||||||
|
transactions: [],
|
||||||
|
skipped: [],
|
||||||
|
|
||||||
|
wizard: {
|
||||||
|
step: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
filteredOut: []
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ export type ConfigResponse = {
|
|||||||
|
|
||||||
export type PrepareResponse = {
|
export type PrepareResponse = {
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
skipped: string[];
|
skipped: string[][];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Transaction = {
|
export type Transaction = {
|
||||||
@@ -20,4 +20,18 @@ export type Transaction = {
|
|||||||
title: string;
|
title: string;
|
||||||
id: string;
|
id: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImportOptions =
|
||||||
|
| { mode: 'add', learnCategories?: boolean, runTransfers?: boolean }
|
||||||
|
| { mode: 'import' };
|
||||||
|
|
||||||
|
export type SubmitResponse = {
|
||||||
|
errors?: {
|
||||||
|
message: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
added: unknown[];
|
||||||
|
|
||||||
|
updated: unknown[];
|
||||||
};
|
};
|
||||||
@@ -8,7 +8,22 @@ export type State = {
|
|||||||
profile?: string;
|
profile?: string;
|
||||||
server?: string;
|
server?: string;
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
skipped: string[];
|
skipped: string[][];
|
||||||
|
|
||||||
|
wizard: {
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
filteredOut: number[];
|
||||||
|
|
||||||
|
result?: {
|
||||||
|
errors?: {
|
||||||
|
message: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
added: unknown[];
|
||||||
|
updated: unknown[];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
@@ -27,7 +42,43 @@ export type Action =
|
|||||||
profile: string;
|
profile: string;
|
||||||
server: string;
|
server: string;
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
skipped: string[];
|
skipped: string[][];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_STEP',
|
||||||
|
payload: {
|
||||||
|
step: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'MOVE_STEP',
|
||||||
|
payload: {
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'FILTER_OUT',
|
||||||
|
payload: {
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'REMOVE_FILTER',
|
||||||
|
payload: {
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_RESULT',
|
||||||
|
payload: {
|
||||||
|
errors?: {
|
||||||
|
message: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
added: unknown[];
|
||||||
|
|
||||||
|
updated: unknown[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
web/src/wizard/Wizard.tsx
Normal file
11
web/src/wizard/Wizard.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useStore } from "../store/AppStore";
|
||||||
|
|
||||||
|
export type WizardProps = {
|
||||||
|
children: React.ReactNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Wizard({ children }: WizardProps) {
|
||||||
|
const { state } = useStore();
|
||||||
|
|
||||||
|
return children[state.wizard.step];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user