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==",
|
"integrity": "sha512-dsfE4BHJkLQW+reOS6b17xhZ/6FB1rB8eRRvO08nn5o+voxf3i74tuyFWNH6djdfgX7Sm5s6LD8t6mJug4dpDw==",
|
||||||
"dev": true
|
"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": {
|
"@types/pg": {
|
||||||
"version": "7.11.2",
|
"version": "7.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-7.11.2.tgz",
|
"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": {
|
"object-visit": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
|
||||||
@ -5697,6 +5712,12 @@
|
|||||||
"symbol-observable": "^1.2.0"
|
"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": {
|
"redux-saga": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.1.tgz",
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
"@types/http-errors": "^1.6.2",
|
"@types/http-errors": "^1.6.2",
|
||||||
"@types/luxon": "^1.15.2",
|
"@types/luxon": "^1.15.2",
|
||||||
"@types/node": "^12.11.2",
|
"@types/node": "^12.11.2",
|
||||||
|
"@types/object-hash": "^1.3.0",
|
||||||
"@types/pg": "^7.11.2",
|
"@types/pg": "^7.11.2",
|
||||||
"@types/pino": "^5.8.12",
|
"@types/pino": "^5.8.12",
|
||||||
"@types/react": "^16.9.9",
|
"@types/react": "^16.9.9",
|
||||||
@ -57,6 +58,7 @@
|
|||||||
"@types/webpack-dev-middleware": "^2.0.3",
|
"@types/webpack-dev-middleware": "^2.0.3",
|
||||||
"@types/yargs": "^13.0.3",
|
"@types/yargs": "^13.0.3",
|
||||||
"css-loader": "^3.2.0",
|
"css-loader": "^3.2.0",
|
||||||
|
"object-hash": "^2.0.0",
|
||||||
"pg-monitor": "^1.3.1",
|
"pg-monitor": "^1.3.1",
|
||||||
"pino-pretty": "^3.2.2",
|
"pino-pretty": "^3.2.2",
|
||||||
"react": "^16.11.0",
|
"react": "^16.11.0",
|
||||||
@ -65,6 +67,7 @@
|
|||||||
"react-router": "^5.1.2",
|
"react-router": "^5.1.2",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"redux": "^4.0.4",
|
"redux": "^4.0.4",
|
||||||
|
"redux-devtools-extension": "^2.13.8",
|
||||||
"redux-saga": "^1.1.1",
|
"redux-saga": "^1.1.1",
|
||||||
"ts-loader": "^6.2.0",
|
"ts-loader": "^6.2.0",
|
||||||
"ts-node": "^8.4.1",
|
"ts-node": "^8.4.1",
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { AppState } from "@kredens/frontend/store";
|
import { AppState } from "@kredens/frontend/store";
|
||||||
import { deleteTask, scheduleTask } from "@kredens/frontend/store/tasks/actions";
|
import { deleteTask, scheduleTask } from "@kredens/frontend/store/tasks/actions";
|
||||||
import { Task, TaskScheduleType } from "@kredens/frontend/store/tasks/types";
|
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";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [taskName, setTaskName] = useState("");
|
const [taskName, setTaskName] = useState("");
|
||||||
const tasks = useSelector<AppState, { [key: string]: Task }>(state => state.tasks.items);
|
const tasks = useSelector<AppState, { [key: string]: Task }>(state => state.tasks.items);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch({type: "FETCH_TASKS"});
|
||||||
|
}, [])
|
||||||
|
|
||||||
const onTaskAddClick = () => {
|
const onTaskAddClick = () => {
|
||||||
dispatch(scheduleTask(Math.random().toString(36), {
|
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";
|
import { tasksReducer } from "./tasks/reducers";
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
@ -7,8 +10,14 @@ const rootReducer = combineReducers({
|
|||||||
|
|
||||||
export type AppState = ReturnType<typeof rootReducer>;
|
export type AppState = ReturnType<typeof rootReducer>;
|
||||||
|
|
||||||
|
const sagaMiddleware = createSagaMiddleware();
|
||||||
export default function configureStore() {
|
export default function configureStore() {
|
||||||
const store = createStore(rootReducer);
|
const store = createStore(
|
||||||
|
rootReducer,
|
||||||
|
composeWithDevTools(applyMiddleware(sagaMiddleware))
|
||||||
|
);
|
||||||
|
|
||||||
|
sagaMiddleware.run(rootSaga);
|
||||||
|
|
||||||
return store;
|
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 {
|
export function scheduleTask(id: string, task: Task): TasksActionType {
|
||||||
return {
|
return {
|
||||||
@ -14,3 +15,35 @@ export function deleteTask(id: string): TasksActionType {
|
|||||||
id
|
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";
|
import { TasksAction, TasksActionType, TasksState } from "./types";
|
||||||
|
|
||||||
const initialState: TasksState = {
|
const initialState: TasksState = {
|
||||||
items: {}
|
items: {},
|
||||||
|
queries: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function tasksReducer(
|
export function tasksReducer(
|
||||||
@ -24,6 +26,45 @@ export function tasksReducer(
|
|||||||
.filter(([key]) => key !== action.id)
|
.filter(([key]) => key !== action.id)
|
||||||
.reduce((res, [key, task]) => ({ ...res, [key]: task }), {})
|
.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: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -19,26 +19,82 @@ export interface Task {
|
|||||||
schedule: TaskSchedule;
|
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 {
|
export interface TasksState {
|
||||||
items: {
|
items: {
|
||||||
[key: string]: Task;
|
[key: string]: Task;
|
||||||
};
|
};
|
||||||
|
queries: {
|
||||||
|
[key: string]: TaskQueryResult;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TasksAction {
|
export enum TasksAction {
|
||||||
SCHEDULE_TASK = "SCHEDULE_TASK",
|
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 {
|
interface ScheduleTaskAction {
|
||||||
type: typeof TasksAction.SCHEDULE_TASK;
|
type: TasksAction.SCHEDULE_TASK;
|
||||||
id: string;
|
id: string;
|
||||||
task: Task;
|
task: Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteTaskAction {
|
interface DeleteTaskAction {
|
||||||
type: typeof TasksAction.DELETE_TASK;
|
type: TasksAction.DELETE_TASK;
|
||||||
id: string;
|
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",
|
"extends": "../../tslint.json",
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-implicit-dependencies": [true, "dev", ["@kredens/frontend"]]
|
"no-implicit-dependencies": [
|
||||||
|
true,
|
||||||
|
"dev",
|
||||||
|
["@kredens/frontend"]
|
||||||
|
],
|
||||||
|
"no-submodule-imports": [
|
||||||
|
true,
|
||||||
|
"@kredens/frontend",
|
||||||
|
"redux-saga/effects"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,7 +12,11 @@
|
|||||||
"@kredens"
|
"@kredens"
|
||||||
],
|
],
|
||||||
"no-implicit-dependencies": [true, ["@kredens", "@kredens/frontend"]],
|
"no-implicit-dependencies": [true, ["@kredens", "@kredens/frontend"]],
|
||||||
"object-literal-sort-keys": [true, "match-declaration-order-only", "shorthand-first"],
|
"object-literal-sort-keys": [
|
||||||
|
true,
|
||||||
|
"match-declaration-order-only",
|
||||||
|
"shorthand-first"
|
||||||
|
],
|
||||||
"max-classes-per-file": false
|
"max-classes-per-file": false
|
||||||
},
|
},
|
||||||
"rulesDirectory": []
|
"rulesDirectory": []
|
||||||
|
Loading…
x
Reference in New Issue
Block a user