Playing with sagas for a stuf implementation of tasks
This commit is contained in:
parent
feda28ec47
commit
64b0cdc951
21
package-lock.json
generated
21
package-lock.json
generated
@ -295,6 +295,15 @@
|
||||
"integrity": "sha512-dsfE4BHJkLQW+reOS6b17xhZ/6FB1rB8eRRvO08nn5o+voxf3i74tuyFWNH6djdfgX7Sm5s6LD8t6mJug4dpDw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/object-hash": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-1.3.0.tgz",
|
||||
"integrity": "sha512-il4NIe4jTx4lfhkKaksmmGHw5EsVkO8sHWkpJHM9m59r1dtsVadLSrJqdE8zU74NENDAsR3oLIOlooRAXlPLNA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/pg": {
|
||||
"version": "7.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-7.11.2.tgz",
|
||||
@ -4652,6 +4661,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"object-hash": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.0.tgz",
|
||||
"integrity": "sha512-I7zGBH0rDKwVGeGZpZoFaDhIwvJa3l1CZE+8VchylXbInNiCj7sxxea9P5dTM4ftKR5//nrqxrdeGSTWL2VpBA==",
|
||||
"dev": true
|
||||
},
|
||||
"object-visit": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
|
||||
@ -5697,6 +5712,12 @@
|
||||
"symbol-observable": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"redux-devtools-extension": {
|
||||
"version": "2.13.8",
|
||||
"resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz",
|
||||
"integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==",
|
||||
"dev": true
|
||||
},
|
||||
"redux-saga": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.1.tgz",
|
||||
|
@ -47,6 +47,7 @@
|
||||
"@types/http-errors": "^1.6.2",
|
||||
"@types/luxon": "^1.15.2",
|
||||
"@types/node": "^12.11.2",
|
||||
"@types/object-hash": "^1.3.0",
|
||||
"@types/pg": "^7.11.2",
|
||||
"@types/pino": "^5.8.12",
|
||||
"@types/react": "^16.9.9",
|
||||
@ -57,6 +58,7 @@
|
||||
"@types/webpack-dev-middleware": "^2.0.3",
|
||||
"@types/yargs": "^13.0.3",
|
||||
"css-loader": "^3.2.0",
|
||||
"object-hash": "^2.0.0",
|
||||
"pg-monitor": "^1.3.1",
|
||||
"pino-pretty": "^3.2.2",
|
||||
"react": "^16.11.0",
|
||||
@ -65,6 +67,7 @@
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"redux": "^4.0.4",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"redux-saga": "^1.1.1",
|
||||
"ts-loader": "^6.2.0",
|
||||
"ts-node": "^8.4.1",
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { AppState } from "@kredens/frontend/store";
|
||||
import { deleteTask, scheduleTask } from "@kredens/frontend/store/tasks/actions";
|
||||
import { Task, TaskScheduleType } from "@kredens/frontend/store/tasks/types";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
export default () => {
|
||||
const [taskName, setTaskName] = useState("");
|
||||
const tasks = useSelector<AppState, { [key: string]: Task }>(state => state.tasks.items);
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch({type: "FETCH_TASKS"});
|
||||
}, [])
|
||||
|
||||
const onTaskAddClick = () => {
|
||||
dispatch(scheduleTask(Math.random().toString(36), {
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { combineReducers, createStore } from "redux";
|
||||
import { applyMiddleware, combineReducers, createStore } from "redux";
|
||||
import { composeWithDevTools } from "redux-devtools-extension";
|
||||
import createSagaMiddleware from "redux-saga";
|
||||
import rootSaga from "./sagas";
|
||||
import { tasksReducer } from "./tasks/reducers";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
@ -7,8 +10,14 @@ const rootReducer = combineReducers({
|
||||
|
||||
export type AppState = ReturnType<typeof rootReducer>;
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
export default function configureStore() {
|
||||
const store = createStore(rootReducer);
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
composeWithDevTools(applyMiddleware(sagaMiddleware))
|
||||
);
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
return store;
|
||||
}
|
||||
|
35
src/frontend/store/sagas.ts
Normal file
35
src/frontend/store/sagas.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { all, call, put, takeEvery } from "redux-saga/effects";
|
||||
import { getTasks, Task as APITask } from "../api/tasks";
|
||||
import { taskFetchError, taskFetchOk, taskFetchStart } from "./tasks/actions";
|
||||
import { Task, TaskQuery, TaskScheduleType } from "./tasks/types";
|
||||
|
||||
export function* fetchTasksSaga(query: TaskQuery = { limit: 10 }) {
|
||||
yield put(taskFetchStart(query));
|
||||
|
||||
try {
|
||||
const tasks: APITask[] = yield call(getTasks);
|
||||
yield put(
|
||||
taskFetchOk(
|
||||
query,
|
||||
tasks
|
||||
.map<[string, Task]>(t => [
|
||||
t.id.toString(),
|
||||
{
|
||||
name: t.name,
|
||||
schedule: {
|
||||
type: TaskScheduleType.Once,
|
||||
due: t.due.toISO()
|
||||
}
|
||||
}
|
||||
])
|
||||
.reduce((res, [id, task]) => ({ ...res, [id]: task }), {})
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
yield put(taskFetchError(query, `${error}`));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([yield takeEvery("FETCH_TASKS", fetchTasksSaga)]);
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { Task, TasksAction, TasksActionType } from "./types";
|
||||
import { DateTime } from "luxon";
|
||||
import { Task, TaskQuery, TasksAction, TasksActionType } from "./types";
|
||||
|
||||
export function scheduleTask(id: string, task: Task): TasksActionType {
|
||||
return {
|
||||
@ -14,3 +15,35 @@ export function deleteTask(id: string): TasksActionType {
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
export function taskFetchStart(query: TaskQuery): TasksActionType {
|
||||
return {
|
||||
type: TasksAction.TASKS_FETCH_START,
|
||||
query,
|
||||
started: DateTime.utc().toISO()
|
||||
};
|
||||
}
|
||||
|
||||
export function taskFetchOk(
|
||||
query: TaskQuery,
|
||||
results: { [key: string]: Task }
|
||||
): TasksActionType {
|
||||
return {
|
||||
type: TasksAction.TASKS_FETCH_OK,
|
||||
query,
|
||||
results,
|
||||
fetched: DateTime.utc().toISO()
|
||||
};
|
||||
}
|
||||
|
||||
export function taskFetchError(
|
||||
query: TaskQuery,
|
||||
error: string
|
||||
): TasksActionType {
|
||||
return {
|
||||
type: TasksAction.TASKS_FETCH_ERROR,
|
||||
query,
|
||||
error,
|
||||
fetched: DateTime.utc().toISO()
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import objectHash from "object-hash";
|
||||
import { TasksAction, TasksActionType, TasksState } from "./types";
|
||||
|
||||
const initialState: TasksState = {
|
||||
items: {}
|
||||
items: {},
|
||||
queries: {}
|
||||
};
|
||||
|
||||
export function tasksReducer(
|
||||
@ -24,6 +26,45 @@ export function tasksReducer(
|
||||
.filter(([key]) => key !== action.id)
|
||||
.reduce((res, [key, task]) => ({ ...res, [key]: task }), {})
|
||||
};
|
||||
case TasksAction.TASKS_FETCH_START:
|
||||
return {
|
||||
...state,
|
||||
queries: {
|
||||
...state.queries,
|
||||
[objectHash(action.query)]: {
|
||||
result: "fetching",
|
||||
started: action.started
|
||||
}
|
||||
}
|
||||
};
|
||||
case TasksAction.TASKS_FETCH_OK:
|
||||
return {
|
||||
...state,
|
||||
items: {
|
||||
...state.items,
|
||||
...action.results
|
||||
},
|
||||
queries: {
|
||||
...state.queries,
|
||||
[objectHash(action.query)]: {
|
||||
result: "ok",
|
||||
items: Object.keys(action.results),
|
||||
fetched: action.fetched
|
||||
}
|
||||
}
|
||||
};
|
||||
case TasksAction.TASKS_FETCH_ERROR:
|
||||
return {
|
||||
...state,
|
||||
queries: {
|
||||
...state.queries,
|
||||
[objectHash(action.query)]: {
|
||||
result: "error",
|
||||
error: action.error,
|
||||
fetched: action.fetched
|
||||
}
|
||||
}
|
||||
};
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
|
@ -19,26 +19,82 @@ export interface Task {
|
||||
schedule: TaskSchedule;
|
||||
}
|
||||
|
||||
export interface TaskQuery {
|
||||
query?: string;
|
||||
after?: string;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
interface TaskQueryOk {
|
||||
result: "ok";
|
||||
items: string[];
|
||||
fetched: string;
|
||||
}
|
||||
|
||||
interface TaskQueryFetching {
|
||||
result: "fetching";
|
||||
started: string;
|
||||
}
|
||||
|
||||
interface TaskQueryError {
|
||||
result: "error";
|
||||
error: string;
|
||||
fetched: string;
|
||||
}
|
||||
|
||||
export type TaskQueryResult = TaskQueryOk | TaskQueryFetching | TaskQueryError;
|
||||
|
||||
export interface TasksState {
|
||||
items: {
|
||||
[key: string]: Task;
|
||||
};
|
||||
queries: {
|
||||
[key: string]: TaskQueryResult;
|
||||
};
|
||||
}
|
||||
|
||||
export enum TasksAction {
|
||||
SCHEDULE_TASK = "SCHEDULE_TASK",
|
||||
DELETE_TASK = "DELETE_TASK"
|
||||
DELETE_TASK = "DELETE_TASK",
|
||||
TASKS_FETCH_START = "TASKS_FETCH_START",
|
||||
TASKS_FETCH_OK = "TASKS_FETCH_OK",
|
||||
TASKS_FETCH_ERROR = "TASKS_FETCH_ERROR"
|
||||
}
|
||||
|
||||
interface ScheduleTaskAction {
|
||||
type: typeof TasksAction.SCHEDULE_TASK;
|
||||
type: TasksAction.SCHEDULE_TASK;
|
||||
id: string;
|
||||
task: Task;
|
||||
}
|
||||
|
||||
interface DeleteTaskAction {
|
||||
type: typeof TasksAction.DELETE_TASK;
|
||||
type: TasksAction.DELETE_TASK;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type TasksActionType = ScheduleTaskAction | DeleteTaskAction;
|
||||
interface TaskFetchStartAction {
|
||||
type: TasksAction.TASKS_FETCH_START;
|
||||
query: TaskQuery;
|
||||
started: string;
|
||||
}
|
||||
|
||||
interface TaskFetchOkAction {
|
||||
type: TasksAction.TASKS_FETCH_OK;
|
||||
query: TaskQuery;
|
||||
results: { [key: string]: Task };
|
||||
fetched: string;
|
||||
}
|
||||
|
||||
interface TaskFetchErrorAction {
|
||||
type: TasksAction.TASKS_FETCH_ERROR;
|
||||
query: TaskQuery;
|
||||
error: string;
|
||||
fetched: string;
|
||||
}
|
||||
|
||||
export type TasksActionType =
|
||||
| ScheduleTaskAction
|
||||
| DeleteTaskAction
|
||||
| TaskFetchOkAction
|
||||
| TaskFetchStartAction
|
||||
| TaskFetchErrorAction;
|
||||
|
@ -1,6 +1,15 @@
|
||||
{
|
||||
"extends": "../../tslint.json",
|
||||
"rules": {
|
||||
"no-implicit-dependencies": [true, "dev", ["@kredens/frontend"]]
|
||||
}
|
||||
}
|
||||
"extends": "../../tslint.json",
|
||||
"rules": {
|
||||
"no-implicit-dependencies": [
|
||||
true,
|
||||
"dev",
|
||||
["@kredens/frontend"]
|
||||
],
|
||||
"no-submodule-imports": [
|
||||
true,
|
||||
"@kredens/frontend",
|
||||
"redux-saga/effects"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
38
tslint.json
38
tslint.json
@ -1,19 +1,23 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"interface-name": [true, "never-prefix"],
|
||||
"no-submodule-imports": [
|
||||
true,
|
||||
"graphql/language",
|
||||
"graphql/type",
|
||||
"vue-loader/lib/plugin",
|
||||
"@kredens"
|
||||
],
|
||||
"no-implicit-dependencies": [true, ["@kredens", "@kredens/frontend"]],
|
||||
"object-literal-sort-keys": [true, "match-declaration-order-only", "shorthand-first"],
|
||||
"max-classes-per-file": false
|
||||
},
|
||||
"rulesDirectory": []
|
||||
"defaultSeverity": "error",
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"interface-name": [true, "never-prefix"],
|
||||
"no-submodule-imports": [
|
||||
true,
|
||||
"graphql/language",
|
||||
"graphql/type",
|
||||
"vue-loader/lib/plugin",
|
||||
"@kredens"
|
||||
],
|
||||
"no-implicit-dependencies": [true, ["@kredens", "@kredens/frontend"]],
|
||||
"object-literal-sort-keys": [
|
||||
true,
|
||||
"match-declaration-order-only",
|
||||
"shorthand-first"
|
||||
],
|
||||
"max-classes-per-file": false
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user