parent
9b05a3a63a
commit
6f9c7f3da2
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,379 @@
|
||||
import {
|
||||
configureStore,
|
||||
createAction,
|
||||
createAsyncThunk,
|
||||
createReducer,
|
||||
} from "@reduxjs/toolkit";
|
||||
import {} from "jest";
|
||||
import { differenceWith, intersectionWith, isString } from "lodash";
|
||||
import { defaultList, defaultState, TestType } from "../tests/helper";
|
||||
import { createAsyncEntityReducer } from "../utils/factory";
|
||||
|
||||
const newItem: TestType = {
|
||||
id: 123,
|
||||
name: "extended",
|
||||
};
|
||||
|
||||
const longerList: TestType[] = [...defaultList, newItem];
|
||||
const shorterList: TestType[] = defaultList.slice(0, defaultList.length - 1);
|
||||
|
||||
const allResolved = createAsyncThunk("all/resolved", () => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve) => {
|
||||
resolve({ total: defaultList.length, data: defaultList });
|
||||
});
|
||||
});
|
||||
|
||||
const allResolvedLonger = createAsyncThunk("all/longer/resolved", () => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve) => {
|
||||
resolve({ total: longerList.length, data: longerList });
|
||||
});
|
||||
});
|
||||
|
||||
const allResolvedShorter = createAsyncThunk("all/shorter/resolved", () => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve) => {
|
||||
resolve({ total: shorterList.length, data: shorterList });
|
||||
});
|
||||
});
|
||||
|
||||
const idsResolved = createAsyncThunk("ids/resolved", (param: number[]) => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve) => {
|
||||
resolve({
|
||||
total: defaultList.length,
|
||||
data: intersectionWith(defaultList, param, (l, r) => l.id === r),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const idsResolvedLonger = createAsyncThunk(
|
||||
"ids/longer/resolved",
|
||||
(param: number[]) => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve) => {
|
||||
resolve({
|
||||
total: longerList.length,
|
||||
data: intersectionWith(longerList, param, (l, r) => l.id === r),
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const idsResolvedShorter = createAsyncThunk(
|
||||
"ids/shorter/resolved",
|
||||
(param: number[]) => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve) => {
|
||||
resolve({
|
||||
total: shorterList.length,
|
||||
data: intersectionWith(shorterList, param, (l, r) => l.id === r),
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const rangeResolved = createAsyncThunk(
|
||||
"range/resolved",
|
||||
(param: Parameter.Range) => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve) => {
|
||||
resolve({
|
||||
total: defaultList.length,
|
||||
data: defaultList.slice(param.start, param.start + param.length),
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const rangeResolvedLonger = createAsyncThunk(
|
||||
"range/longer/resolved",
|
||||
(param: Parameter.Range) => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve) => {
|
||||
resolve({
|
||||
total: longerList.length,
|
||||
data: longerList.slice(param.start, param.start + param.length),
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const rangeResolvedShorter = createAsyncThunk(
|
||||
"range/shorter/resolved",
|
||||
(param: Parameter.Range) => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve) => {
|
||||
resolve({
|
||||
total: shorterList.length,
|
||||
data: shorterList.slice(param.start, param.start + param.length),
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const allRejected = createAsyncThunk("all/rejected", () => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve, rejected) => {
|
||||
rejected("Error");
|
||||
});
|
||||
});
|
||||
const idsRejected = createAsyncThunk("ids/rejected", (param: number[]) => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve, rejected) => {
|
||||
rejected("Error");
|
||||
});
|
||||
});
|
||||
const rangeRejected = createAsyncThunk(
|
||||
"range/rejected",
|
||||
(param: Parameter.Range) => {
|
||||
return new Promise<AsyncDataWrapper<TestType>>((resolve, rejected) => {
|
||||
rejected("Error");
|
||||
});
|
||||
}
|
||||
);
|
||||
const removeIds = createAction<number[]>("remove/id");
|
||||
const dirty = createAction<number[]>("dirty/id");
|
||||
|
||||
const reducer = createReducer(defaultState, (builder) => {
|
||||
createAsyncEntityReducer(builder, (s) => s.entities, {
|
||||
all: allResolved,
|
||||
range: rangeResolved,
|
||||
ids: idsResolved,
|
||||
dirty,
|
||||
removeIds,
|
||||
});
|
||||
createAsyncEntityReducer(builder, (s) => s.entities, {
|
||||
all: allRejected,
|
||||
range: rangeRejected,
|
||||
ids: idsRejected,
|
||||
});
|
||||
|
||||
createAsyncEntityReducer(builder, (s) => s.entities, {
|
||||
all: allResolvedLonger,
|
||||
range: rangeResolvedLonger,
|
||||
ids: idsResolvedLonger,
|
||||
});
|
||||
|
||||
createAsyncEntityReducer(builder, (s) => s.entities, {
|
||||
all: allResolvedShorter,
|
||||
range: rangeResolvedShorter,
|
||||
ids: idsResolvedShorter,
|
||||
});
|
||||
});
|
||||
|
||||
function createStore() {
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
});
|
||||
expect(store.getState()).toEqual(defaultState);
|
||||
return store;
|
||||
}
|
||||
|
||||
let store = createStore();
|
||||
|
||||
function use(callback: (entities: Async.Entity<TestType>) => void) {
|
||||
const entities = store.getState().entities;
|
||||
callback(entities);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
});
|
||||
|
||||
it("entity update all resolved", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
use((entities) => {
|
||||
expect(entities.dirtyEntities).toHaveLength(0);
|
||||
expect(entities.error).toBeNull();
|
||||
expect(entities.state).toBe("succeeded");
|
||||
defaultList.forEach((v, index) => {
|
||||
const id = v.id.toString();
|
||||
expect(entities.content.ids[index]).toEqual(id);
|
||||
expect(entities.content.entities[id]).toEqual(v);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("entity update all rejected", async () => {
|
||||
await store.dispatch(allRejected());
|
||||
use((entities) => {
|
||||
expect(entities.dirtyEntities).toHaveLength(0);
|
||||
expect(entities.error).not.toBeNull();
|
||||
expect(entities.state).toBe("failed");
|
||||
expect(entities.content.ids).toHaveLength(0);
|
||||
expect(entities.content.entities).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
it("entity mark dirty", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
|
||||
store.dispatch(dirty([1, 2, 3]));
|
||||
use((entities) => {
|
||||
expect(entities.error).toBeNull();
|
||||
expect(entities.state).toBe("dirty");
|
||||
defaultList.forEach((v, index) => {
|
||||
const id = v.id.toString();
|
||||
expect(entities.content.ids[index]).toEqual(id);
|
||||
expect(entities.content.entities[id]).toEqual(v);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("delete entity item", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
|
||||
const idsToRemove = [0, 1, 3, 5];
|
||||
const expectResults = differenceWith(
|
||||
defaultList,
|
||||
idsToRemove,
|
||||
(l, r) => l.id === r
|
||||
);
|
||||
|
||||
store.dispatch(removeIds(idsToRemove));
|
||||
use((entities) => {
|
||||
expect(entities.state).toBe("succeeded");
|
||||
expectResults.forEach((v, index) => {
|
||||
const id = v.id.toString();
|
||||
expect(entities.content.ids[index]).toEqual(id);
|
||||
expect(entities.content.entities[id]).toEqual(v);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("entity update by range", async () => {
|
||||
await store.dispatch(rangeResolved({ start: 0, length: 2 }));
|
||||
await store.dispatch(rangeResolved({ start: 4, length: 2 }));
|
||||
use((entities) => {
|
||||
expect(entities.content.ids).toHaveLength(defaultList.length);
|
||||
expect(entities.content.ids.filter(isString)).toHaveLength(4);
|
||||
[0, 1, 4, 5].forEach((v) => {
|
||||
const id = v.toString();
|
||||
expect(entities.content.ids).toContain(id);
|
||||
expect(entities.content.entities[id].id).toEqual(v);
|
||||
});
|
||||
expect(entities.error).toBeNull();
|
||||
expect(entities.state).toBe("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("entity update by duplicative range", async () => {
|
||||
await store.dispatch(rangeResolved({ start: 0, length: 2 }));
|
||||
await store.dispatch(rangeResolved({ start: 1, length: 2 }));
|
||||
use((entities) => {
|
||||
expect(entities.content.ids).toHaveLength(defaultList.length);
|
||||
expect(entities.content.ids.filter(isString)).toHaveLength(3);
|
||||
defaultList.slice(0, 3).forEach((v) => {
|
||||
const id = v.id.toString();
|
||||
expect(entities.content.ids).toContain(id);
|
||||
expect(entities.content.entities[id]).toEqual(v);
|
||||
});
|
||||
expect(entities.error).toBeNull();
|
||||
expect(entities.state).toBe("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("entity resolved by dirty", async () => {
|
||||
await store.dispatch(rangeResolved({ start: 0, length: 2 }));
|
||||
store.dispatch(dirty([1, 2, 3]));
|
||||
await store.dispatch(rangeResolved({ start: 1, length: 2 }));
|
||||
use((entities) => {
|
||||
expect(entities.dirtyEntities).not.toContain("1");
|
||||
expect(entities.dirtyEntities).not.toContain("2");
|
||||
expect(entities.dirtyEntities).toContain("3");
|
||||
expect(entities.state).toBe("dirty");
|
||||
});
|
||||
await store.dispatch(rangeResolved({ start: 1, length: 3 }));
|
||||
use((entities) => {
|
||||
expect(entities.dirtyEntities).not.toContain("1");
|
||||
expect(entities.dirtyEntities).not.toContain("2");
|
||||
expect(entities.dirtyEntities).not.toContain("3");
|
||||
expect(entities.state).toBe("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("entity update by ids", async () => {
|
||||
await store.dispatch(idsResolved([999]));
|
||||
use((entities) => {
|
||||
expect(entities.content.ids).toHaveLength(defaultList.length);
|
||||
expect(entities.content.ids.filter(isString)).toHaveLength(0);
|
||||
expect(entities.content.entities).not.toHaveProperty("999");
|
||||
expect(entities.error).toBeNull();
|
||||
expect(entities.state).toBe("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("entity resolved dirty by ids", async () => {
|
||||
await store.dispatch(idsResolved([0, 1, 2, 3, 4]));
|
||||
store.dispatch(dirty([0, 1, 2, 3]));
|
||||
await store.dispatch(idsResolved([0, 1]));
|
||||
use((entities) => {
|
||||
expect(entities.dirtyEntities).toHaveLength(2);
|
||||
expect(entities.content.ids.filter(isString)).toHaveLength(5);
|
||||
expect(entities.error).toBeNull();
|
||||
expect(entities.state).toBe("dirty");
|
||||
});
|
||||
});
|
||||
|
||||
it("entity resolved non-exist by ids", async () => {
|
||||
await store.dispatch(idsResolved([0, 1]));
|
||||
store.dispatch(dirty([999]));
|
||||
await store.dispatch(idsResolved([999]));
|
||||
use((entities) => {
|
||||
expect(entities.dirtyEntities).toHaveLength(0);
|
||||
expect(entities.state).toBe("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("entity update by variant range", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
|
||||
await store.dispatch(rangeResolvedLonger({ start: 0, length: 2 }));
|
||||
use((entities) => {
|
||||
expect(entities.dirtyEntities).toHaveLength(0);
|
||||
expect(entities.state).toBe("succeeded");
|
||||
expect(entities.content.ids).toHaveLength(longerList.length);
|
||||
expect(entities.content.ids.filter(isString)).toHaveLength(2);
|
||||
longerList.slice(0, 2).forEach((v) => {
|
||||
const id = v.id.toString();
|
||||
expect(entities.content.ids).toContain(id);
|
||||
expect(entities.content.entities[id]).toEqual(v);
|
||||
});
|
||||
});
|
||||
|
||||
await store.dispatch(allResolved());
|
||||
await store.dispatch(rangeResolvedShorter({ start: 0, length: 2 }));
|
||||
use((entities) => {
|
||||
expect(entities.dirtyEntities).toHaveLength(0);
|
||||
expect(entities.state).toBe("succeeded");
|
||||
expect(entities.content.ids).toHaveLength(shorterList.length);
|
||||
expect(entities.content.ids.filter(isString)).toHaveLength(2);
|
||||
shorterList.slice(0, 2).forEach((v) => {
|
||||
const id = v.id.toString();
|
||||
expect(entities.content.ids).toContain(id);
|
||||
expect(entities.content.entities[id]).toEqual(v);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("entity update by variant ids", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
|
||||
await store.dispatch(idsResolvedLonger([2, 3, 4]));
|
||||
use((entities) => {
|
||||
expect(entities.dirtyEntities).toHaveLength(0);
|
||||
expect(entities.state).toBe("succeeded");
|
||||
expect(entities.content.ids).toHaveLength(longerList.length);
|
||||
expect(entities.content.ids.filter(isString)).toHaveLength(3);
|
||||
Array(3)
|
||||
.fill(undefined)
|
||||
.forEach((v) => {
|
||||
expect(entities.content.ids[v]).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
await store.dispatch(allResolved());
|
||||
await store.dispatch(idsResolvedShorter([2, 3, 4]));
|
||||
use((entities) => {
|
||||
expect(entities.dirtyEntities).toHaveLength(0);
|
||||
expect(entities.state).toBe("succeeded");
|
||||
expect(entities.content.ids).toHaveLength(shorterList.length);
|
||||
expect(entities.content.ids.filter(isString)).toHaveLength(3);
|
||||
Array(3)
|
||||
.fill(undefined)
|
||||
.forEach((v) => {
|
||||
expect(entities.content.ids[v]).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,155 @@
|
||||
import {
|
||||
configureStore,
|
||||
createAction,
|
||||
createAsyncThunk,
|
||||
createReducer,
|
||||
} from "@reduxjs/toolkit";
|
||||
import {} from "jest";
|
||||
import { defaultState, TestType } from "../tests/helper";
|
||||
import { createAsyncItemReducer } from "../utils/factory";
|
||||
|
||||
// Item
|
||||
const defaultItem: TestType = { id: 0, name: "test" };
|
||||
const allResolved = createAsyncThunk("all/resolved", () => {
|
||||
return new Promise<TestType>((resolve) => {
|
||||
resolve(defaultItem);
|
||||
});
|
||||
});
|
||||
const allRejected = createAsyncThunk("all/rejected", () => {
|
||||
return new Promise<TestType>((resolve, rejected) => {
|
||||
rejected("Error");
|
||||
});
|
||||
});
|
||||
const dirty = createAction("dirty/ids");
|
||||
|
||||
const reducer = createReducer(defaultState, (builder) => {
|
||||
createAsyncItemReducer(builder, (s) => s.item, { all: allResolved, dirty });
|
||||
createAsyncItemReducer(builder, (s) => s.item, { all: allRejected });
|
||||
});
|
||||
|
||||
function createStore() {
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
});
|
||||
expect(store.getState()).toEqual(defaultState);
|
||||
return store;
|
||||
}
|
||||
|
||||
let store = createStore();
|
||||
|
||||
function use(callback: (entities: Async.Item<TestType>) => void) {
|
||||
const item = store.getState().item;
|
||||
callback(item);
|
||||
}
|
||||
|
||||
// Begin Test Section
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
});
|
||||
|
||||
it("item loading", async () => {
|
||||
return new Promise<void>((done) => {
|
||||
store.dispatch(allResolved()).finally(() => {
|
||||
use((item) => {
|
||||
expect(item.error).toBeNull();
|
||||
expect(item.content).toEqual(defaultItem);
|
||||
});
|
||||
done();
|
||||
});
|
||||
use((item) => {
|
||||
expect(item.state).toBe("loading");
|
||||
expect(item.error).toBeNull();
|
||||
expect(item.content).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("item uninitialized -> succeeded", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
use((item) => {
|
||||
expect(item.state).toBe("succeeded");
|
||||
expect(item.error).toBeNull();
|
||||
expect(item.content).toEqual(defaultItem);
|
||||
});
|
||||
});
|
||||
|
||||
it("item uninitialized -> failed", async () => {
|
||||
await store.dispatch(allRejected());
|
||||
use((item) => {
|
||||
expect(item.state).toBe("failed");
|
||||
expect(item.error).not.toBeNull();
|
||||
expect(item.content).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("item uninitialized -> dirty", () => {
|
||||
store.dispatch(dirty());
|
||||
use((item) => {
|
||||
expect(item.state).toBe("uninitialized");
|
||||
expect(item.error).toBeNull();
|
||||
expect(item.content).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("item succeeded -> failed", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
await store.dispatch(allRejected());
|
||||
use((item) => {
|
||||
expect(item.state).toBe("failed");
|
||||
expect(item.error).not.toBeNull();
|
||||
expect(item.content).toEqual(defaultItem);
|
||||
});
|
||||
});
|
||||
|
||||
it("item failed -> succeeded", async () => {
|
||||
await store.dispatch(allRejected());
|
||||
await store.dispatch(allResolved());
|
||||
use((item) => {
|
||||
expect(item.state).toBe("succeeded");
|
||||
expect(item.error).toBeNull();
|
||||
expect(item.content).toEqual(defaultItem);
|
||||
});
|
||||
});
|
||||
|
||||
it("item succeeded -> dirty", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
store.dispatch(dirty());
|
||||
use((item) => {
|
||||
expect(item.state).toBe("dirty");
|
||||
expect(item.error).toBeNull();
|
||||
expect(item.content).toEqual(defaultItem);
|
||||
});
|
||||
});
|
||||
|
||||
it("item failed -> dirty", async () => {
|
||||
await store.dispatch(allRejected());
|
||||
store.dispatch(dirty());
|
||||
use((item) => {
|
||||
expect(item.state).toBe("dirty");
|
||||
expect(item.error).not.toBeNull();
|
||||
expect(item.content).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("item dirty -> failed", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
store.dispatch(dirty());
|
||||
await store.dispatch(allRejected());
|
||||
use((item) => {
|
||||
expect(item.state).toBe("failed");
|
||||
expect(item.error).not.toBeNull();
|
||||
expect(item.content).toEqual(defaultItem);
|
||||
});
|
||||
});
|
||||
|
||||
it("item dirty -> succeeded", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
store.dispatch(dirty());
|
||||
await store.dispatch(allResolved());
|
||||
use((item) => {
|
||||
expect(item.state).toBe("succeeded");
|
||||
expect(item.error).toBeNull();
|
||||
expect(item.content).toEqual(defaultItem);
|
||||
});
|
||||
});
|
@ -0,0 +1,248 @@
|
||||
import {
|
||||
configureStore,
|
||||
createAction,
|
||||
createAsyncThunk,
|
||||
createReducer,
|
||||
} from "@reduxjs/toolkit";
|
||||
import {} from "jest";
|
||||
import { intersectionWith } from "lodash";
|
||||
import { defaultList, defaultState, TestType } from "../tests/helper";
|
||||
import { createAsyncListReducer } from "../utils/factory";
|
||||
|
||||
const allResolved = createAsyncThunk("all/resolved", () => {
|
||||
return new Promise<TestType[]>((resolve) => {
|
||||
resolve(defaultList);
|
||||
});
|
||||
});
|
||||
const allRejected = createAsyncThunk("all/rejected", () => {
|
||||
return new Promise<TestType[]>((resolve, rejected) => {
|
||||
rejected("Error");
|
||||
});
|
||||
});
|
||||
const idsResolved = createAsyncThunk("ids/resolved", (param: number[]) => {
|
||||
return new Promise<TestType[]>((resolve) => {
|
||||
resolve(intersectionWith(defaultList, param, (l, r) => l.id === r));
|
||||
});
|
||||
});
|
||||
const idsRejected = createAsyncThunk("ids/rejected", (param: number[]) => {
|
||||
return new Promise<TestType[]>((resolve, rejected) => {
|
||||
rejected("Error");
|
||||
});
|
||||
});
|
||||
const removeIds = createAction<number[]>("remove/id");
|
||||
const dirty = createAction<number[]>("dirty/id");
|
||||
|
||||
const reducer = createReducer(defaultState, (builder) => {
|
||||
createAsyncListReducer(builder, (s) => s.list, {
|
||||
all: allResolved,
|
||||
ids: idsResolved,
|
||||
removeIds,
|
||||
dirty,
|
||||
});
|
||||
createAsyncListReducer(builder, (s) => s.list, {
|
||||
all: allRejected,
|
||||
ids: idsRejected,
|
||||
});
|
||||
});
|
||||
|
||||
function createStore() {
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
});
|
||||
expect(store.getState()).toEqual(defaultState);
|
||||
return store;
|
||||
}
|
||||
|
||||
let store = createStore();
|
||||
|
||||
function use(callback: (list: Async.List<TestType>) => void) {
|
||||
const list = store.getState().list;
|
||||
callback(list);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
});
|
||||
|
||||
it("list all uninitialized -> succeeded", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
use((list) => {
|
||||
expect(list.content).toEqual(defaultList);
|
||||
expect(list.dirtyEntities).toHaveLength(0);
|
||||
expect(list.error).toBeNull();
|
||||
expect(list.state).toEqual("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("list all uninitialized -> failed", async () => {
|
||||
await store.dispatch(allRejected());
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(0);
|
||||
expect(list.dirtyEntities).toHaveLength(0);
|
||||
expect(list.error).not.toBeNull();
|
||||
expect(list.state).toEqual("failed");
|
||||
});
|
||||
});
|
||||
|
||||
it("list uninitialized -> dirty", () => {
|
||||
store.dispatch(dirty([0, 1]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(0);
|
||||
expect(list.dirtyEntities).toHaveLength(0);
|
||||
expect(list.error).toBeNull();
|
||||
expect(list.state).toEqual("uninitialized");
|
||||
});
|
||||
});
|
||||
|
||||
it("list succeeded -> dirty", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
store.dispatch(dirty([1, 2, 3]));
|
||||
use((list) => {
|
||||
expect(list.content).toEqual(defaultList);
|
||||
expect(list.dirtyEntities).toHaveLength(3);
|
||||
expect(list.error).toBeNull();
|
||||
expect(list.state).toEqual("dirty");
|
||||
});
|
||||
});
|
||||
|
||||
it("list ids uninitialized -> succeeded", async () => {
|
||||
await store.dispatch(idsResolved([0, 1, 2]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(3);
|
||||
expect(list.dirtyEntities).toHaveLength(0);
|
||||
expect(list.error).toBeNull();
|
||||
expect(list.state).toEqual("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("list ids succeeded -> dirty", async () => {
|
||||
await store.dispatch(idsResolved([0, 1]));
|
||||
store.dispatch(dirty([2, 3]));
|
||||
use((list) => {
|
||||
expect(list.dirtyEntities).toHaveLength(2);
|
||||
expect(list.state).toEqual("dirty");
|
||||
});
|
||||
});
|
||||
|
||||
it("list ids succeeded -> dirty", async () => {
|
||||
await store.dispatch(idsResolved([0, 1, 2]));
|
||||
store.dispatch(dirty([2, 3]));
|
||||
use((list) => {
|
||||
expect(list.dirtyEntities).toHaveLength(2);
|
||||
expect(list.state).toEqual("dirty");
|
||||
});
|
||||
});
|
||||
|
||||
it("list ids update data", async () => {
|
||||
await store.dispatch(idsResolved([0, 1]));
|
||||
await store.dispatch(idsResolved([3, 4]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(4);
|
||||
expect(list.state).toEqual("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("list ids update duplicative data", async () => {
|
||||
await store.dispatch(idsResolved([0, 1, 2]));
|
||||
await store.dispatch(idsResolved([2, 3]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(4);
|
||||
expect(list.state).toEqual("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("list ids update new data", async () => {
|
||||
await store.dispatch(idsResolved([0, 1]));
|
||||
await store.dispatch(idsResolved([2, 3]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(4);
|
||||
expect(list.content[1].id).toBe(2);
|
||||
expect(list.content[0].id).toBe(3);
|
||||
expect(list.state).toEqual("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("list ids empty data", async () => {
|
||||
await store.dispatch(idsResolved([0, 1, 2]));
|
||||
await store.dispatch(idsResolved([999]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(3);
|
||||
expect(list.state).toEqual("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("list ids duplicative dirty", async () => {
|
||||
await store.dispatch(idsResolved([0]));
|
||||
store.dispatch(dirty([2, 2]));
|
||||
use((list) => {
|
||||
expect(list.dirtyEntities).toHaveLength(1);
|
||||
expect(list.dirtyEntities).toContain("2");
|
||||
expect(list.state).toEqual("dirty");
|
||||
});
|
||||
});
|
||||
|
||||
it("list ids resolved dirty", async () => {
|
||||
await store.dispatch(idsResolved([0, 1, 2]));
|
||||
store.dispatch(dirty([2, 3]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(3);
|
||||
expect(list.dirtyEntities).toContain("2");
|
||||
expect(list.dirtyEntities).toContain("3");
|
||||
expect(list.state).toBe("dirty");
|
||||
});
|
||||
});
|
||||
|
||||
it("list ids resolved dirty", async () => {
|
||||
await store.dispatch(idsResolved([0, 1, 2]));
|
||||
store.dispatch(dirty([1, 2, 3, 999]));
|
||||
await store.dispatch(idsResolved([1, 2]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(3);
|
||||
expect(list.dirtyEntities).not.toContain("1");
|
||||
expect(list.dirtyEntities).not.toContain("2");
|
||||
expect(list.state).toBe("dirty");
|
||||
});
|
||||
|
||||
await store.dispatch(idsResolved([3]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(4);
|
||||
expect(list.dirtyEntities).not.toContain("3");
|
||||
expect(list.state).toBe("dirty");
|
||||
});
|
||||
|
||||
await store.dispatch(idsResolved([999]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(4);
|
||||
expect(list.dirtyEntities).not.toContain("999");
|
||||
expect(list.state).toBe("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("list remove ids", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
const totalSize = store.getState().list.content.length;
|
||||
|
||||
store.dispatch(removeIds([1, 2]));
|
||||
use((list) => {
|
||||
expect(list.content).toHaveLength(totalSize - 2);
|
||||
expect(list.content.map((v) => v.id)).not.toContain(1);
|
||||
expect(list.content.map((v) => v.id)).not.toContain(2);
|
||||
expect(list.state).toEqual("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("list remove dirty ids", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
store.dispatch(dirty([1, 2, 3]));
|
||||
store.dispatch(removeIds([1, 2]));
|
||||
use((list) => {
|
||||
expect(list.dirtyEntities).not.toContain("1");
|
||||
expect(list.dirtyEntities).not.toContain("2");
|
||||
expect(list.state).toEqual("dirty");
|
||||
});
|
||||
store.dispatch(removeIds([3]));
|
||||
use((list) => {
|
||||
expect(list.dirtyEntities).toHaveLength(0);
|
||||
expect(list.state).toEqual("succeeded");
|
||||
});
|
||||
});
|
@ -1,123 +0,0 @@
|
||||
import {
|
||||
ActionCallback,
|
||||
ActionDispatcher,
|
||||
AsyncActionCreator,
|
||||
AsyncActionDispatcher,
|
||||
AvailableCreator,
|
||||
AvailableType,
|
||||
PromiseCreator,
|
||||
} from "../types";
|
||||
|
||||
function asyncActionFactory<T extends PromiseCreator>(
|
||||
type: string,
|
||||
promise: T,
|
||||
args: Parameters<T>
|
||||
): AsyncActionDispatcher<PromiseType<ReturnType<T>>> {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type,
|
||||
payload: {
|
||||
loading: true,
|
||||
parameters: args,
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
promise(...args)
|
||||
.then((val) => {
|
||||
dispatch({
|
||||
type,
|
||||
payload: {
|
||||
loading: false,
|
||||
item: val,
|
||||
parameters: args,
|
||||
},
|
||||
});
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
dispatch({
|
||||
type,
|
||||
error: true,
|
||||
payload: {
|
||||
loading: false,
|
||||
item: err,
|
||||
parameters: args,
|
||||
},
|
||||
});
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createAsyncAction<T extends PromiseCreator>(
|
||||
type: string,
|
||||
promise: T
|
||||
) {
|
||||
return (...args: Parameters<T>) => asyncActionFactory(type, promise, args);
|
||||
}
|
||||
|
||||
// Create a action which combine multiple ActionDispatcher and execute them at once
|
||||
function combineActionFactory(
|
||||
dispatchers: AvailableType<any>[]
|
||||
): ActionDispatcher {
|
||||
return (dispatch) => {
|
||||
dispatchers.forEach((fn) => {
|
||||
if (typeof fn === "function") {
|
||||
fn(dispatch);
|
||||
} else {
|
||||
dispatch(fn);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createCombineAction<T extends AvailableCreator>(fn: T) {
|
||||
return (...args: Parameters<T>) => combineActionFactory(fn(...args));
|
||||
}
|
||||
|
||||
function combineAsyncActionFactory(
|
||||
dispatchers: AsyncActionDispatcher<any>[]
|
||||
): AsyncActionDispatcher<any> {
|
||||
return (dispatch) => {
|
||||
const promises = dispatchers.map((v) => v(dispatch));
|
||||
return Promise.all(promises) as Promise<any>;
|
||||
};
|
||||
}
|
||||
|
||||
export function createAsyncCombineAction<T extends AsyncActionCreator>(fn: T) {
|
||||
return (...args: Parameters<T>) => combineAsyncActionFactory(fn(...args));
|
||||
}
|
||||
|
||||
export function callbackActionFactory(
|
||||
dispatchers: AsyncActionDispatcher<any>[],
|
||||
success: ActionCallback,
|
||||
error?: ActionCallback
|
||||
): ActionDispatcher<any> {
|
||||
return (dispatch) => {
|
||||
const promises = dispatchers.map((v) => v(dispatch));
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
const action = success();
|
||||
if (action !== undefined) {
|
||||
dispatch(action);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const action = error && error();
|
||||
if (action !== undefined) {
|
||||
dispatch(action);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createCallbackAction<T extends AsyncActionCreator>(
|
||||
fn: T,
|
||||
success: ActionCallback,
|
||||
error?: ActionCallback
|
||||
) {
|
||||
return (...args: Parameters<T>) =>
|
||||
callbackActionFactory(fn(args), success, error);
|
||||
}
|
@ -1,47 +1,78 @@
|
||||
import { createDeleteAction } from "../../@socketio/reducer";
|
||||
import { createAction, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { MoviesApi } from "../../apis";
|
||||
import {
|
||||
MOVIES_DELETE_ITEMS,
|
||||
MOVIES_DELETE_WANTED_ITEMS,
|
||||
MOVIES_UPDATE_BLACKLIST,
|
||||
MOVIES_UPDATE_HISTORY_LIST,
|
||||
MOVIES_UPDATE_LIST,
|
||||
MOVIES_UPDATE_WANTED_LIST,
|
||||
} from "../constants";
|
||||
import { createAsyncAction } from "./factory";
|
||||
|
||||
export const movieUpdateList = createAsyncAction(
|
||||
MOVIES_UPDATE_LIST,
|
||||
(id?: number[]) => MoviesApi.movies(id)
|
||||
export const movieUpdateByRange = createAsyncThunk(
|
||||
"movies/update/range",
|
||||
async (params: Parameter.Range) => {
|
||||
const response = await MoviesApi.moviesBy(params);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieDeleteItems = createDeleteAction(MOVIES_DELETE_ITEMS);
|
||||
export const movieUpdateById = createAsyncThunk(
|
||||
"movies/update/id",
|
||||
async (ids: number[]) => {
|
||||
const response = await MoviesApi.movies(ids);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieUpdateWantedList = createAsyncAction(
|
||||
MOVIES_UPDATE_WANTED_LIST,
|
||||
(radarrid: number[]) => MoviesApi.wantedBy(radarrid)
|
||||
export const movieUpdateAll = createAsyncThunk(
|
||||
"movies/update/all",
|
||||
async () => {
|
||||
const response = await MoviesApi.movies();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieDeleteWantedItems = createDeleteAction(
|
||||
MOVIES_DELETE_WANTED_ITEMS
|
||||
export const movieRemoveById = createAction<number[]>("movies/remove");
|
||||
|
||||
export const movieMarkDirtyById = createAction<number[]>(
|
||||
"movies/mark_dirty/id"
|
||||
);
|
||||
|
||||
export const movieUpdateWantedByRange = createAsyncAction(
|
||||
MOVIES_UPDATE_WANTED_LIST,
|
||||
(start: number, length: number) => MoviesApi.wanted(start, length)
|
||||
export const movieUpdateWantedById = createAsyncThunk(
|
||||
"movies/wanted/update/id",
|
||||
async (ids: number[]) => {
|
||||
const response = await MoviesApi.wantedBy(ids);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieUpdateHistoryList = createAsyncAction(
|
||||
MOVIES_UPDATE_HISTORY_LIST,
|
||||
() => MoviesApi.history()
|
||||
export const movieRemoveWantedById = createAction<number[]>(
|
||||
"movies/wanted/remove/id"
|
||||
);
|
||||
|
||||
export const movieUpdateByRange = createAsyncAction(
|
||||
MOVIES_UPDATE_LIST,
|
||||
(start: number, length: number) => MoviesApi.moviesBy(start, length)
|
||||
export const movieMarkWantedDirtyById = createAction<number[]>(
|
||||
"movies/wanted/mark_dirty/id"
|
||||
);
|
||||
|
||||
export const movieUpdateWantedByRange = createAsyncThunk(
|
||||
"movies/wanted/update/range",
|
||||
async (params: Parameter.Range) => {
|
||||
const response = await MoviesApi.wanted(params);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieUpdateHistory = createAsyncThunk(
|
||||
"movies/history/update",
|
||||
async () => {
|
||||
const response = await MoviesApi.history();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieMarkHistoryDirty = createAction("movies/history/mark_dirty");
|
||||
|
||||
export const movieUpdateBlacklist = createAsyncThunk(
|
||||
"movies/blacklist/update",
|
||||
async () => {
|
||||
const response = await MoviesApi.blacklist();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieUpdateBlacklist = createAsyncAction(
|
||||
MOVIES_UPDATE_BLACKLIST,
|
||||
() => MoviesApi.blacklist()
|
||||
export const movieMarkBlacklistDirty = createAction(
|
||||
"movies/blacklist/mark_dirty"
|
||||
);
|
||||
|
@ -1,61 +1,100 @@
|
||||
import { createDeleteAction } from "../../@socketio/reducer";
|
||||
import { createAction, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { EpisodesApi, SeriesApi } from "../../apis";
|
||||
import {
|
||||
SERIES_DELETE_EPISODES,
|
||||
SERIES_DELETE_ITEMS,
|
||||
SERIES_DELETE_WANTED_ITEMS,
|
||||
SERIES_UPDATE_BLACKLIST,
|
||||
SERIES_UPDATE_EPISODE_LIST,
|
||||
SERIES_UPDATE_HISTORY_LIST,
|
||||
SERIES_UPDATE_LIST,
|
||||
SERIES_UPDATE_WANTED_LIST,
|
||||
} from "../constants";
|
||||
import { createAsyncAction } from "./factory";
|
||||
|
||||
export const seriesUpdateWantedList = createAsyncAction(
|
||||
SERIES_UPDATE_WANTED_LIST,
|
||||
(episodeid: number[]) => EpisodesApi.wantedBy(episodeid)
|
||||
export const seriesUpdateWantedById = createAsyncThunk(
|
||||
"series/wanted/update/id",
|
||||
async (episodeid: number[]) => {
|
||||
const response = await EpisodesApi.wantedBy(episodeid);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const seriesDeleteWantedItems = createDeleteAction(
|
||||
SERIES_DELETE_WANTED_ITEMS
|
||||
export const seriesUpdateWantedByRange = createAsyncThunk(
|
||||
"series/wanted/update/range",
|
||||
async (params: Parameter.Range) => {
|
||||
const response = await EpisodesApi.wanted(params);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const seriesUpdateWantedByRange = createAsyncAction(
|
||||
SERIES_UPDATE_WANTED_LIST,
|
||||
(start: number, length: number) => EpisodesApi.wanted(start, length)
|
||||
export const seriesRemoveWantedById = createAction<number[]>(
|
||||
"series/wanted/remove/id"
|
||||
);
|
||||
|
||||
export const seriesUpdateList = createAsyncAction(
|
||||
SERIES_UPDATE_LIST,
|
||||
(id?: number[]) => SeriesApi.series(id)
|
||||
export const seriesMarkWantedDirtyById = createAction<number[]>(
|
||||
"series/wanted/mark_dirty/episode_id"
|
||||
);
|
||||
|
||||
export const seriesDeleteItems = createDeleteAction(SERIES_DELETE_ITEMS);
|
||||
export const seriesRemoveById = createAction<number[]>("series/remove");
|
||||
|
||||
export const episodeUpdateBy = createAsyncAction(
|
||||
SERIES_UPDATE_EPISODE_LIST,
|
||||
(seriesid: number[]) => EpisodesApi.bySeriesId(seriesid)
|
||||
export const seriesMarkDirtyById = createAction<number[]>(
|
||||
"series/mark_dirty/id"
|
||||
);
|
||||
|
||||
export const episodeDeleteItems = createDeleteAction(SERIES_DELETE_EPISODES);
|
||||
export const seriesUpdateById = createAsyncThunk(
|
||||
"series/update/id",
|
||||
async (ids: number[]) => {
|
||||
const response = await SeriesApi.series(ids);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const seriesUpdateAll = createAsyncThunk(
|
||||
"series/update/all",
|
||||
async () => {
|
||||
const response = await SeriesApi.series();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const seriesUpdateByRange = createAsyncThunk(
|
||||
"series/update/range",
|
||||
async (params: Parameter.Range) => {
|
||||
const response = await SeriesApi.seriesBy(params);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const episodesRemoveById = createAction<number[]>("episodes/remove");
|
||||
|
||||
export const episodeUpdateById = createAsyncAction(
|
||||
SERIES_UPDATE_EPISODE_LIST,
|
||||
(episodeid: number[]) => EpisodesApi.byEpisodeId(episodeid)
|
||||
export const episodesMarkDirtyById = createAction<number[]>(
|
||||
"episodes/mark_dirty/id"
|
||||
);
|
||||
|
||||
export const seriesUpdateByRange = createAsyncAction(
|
||||
SERIES_UPDATE_LIST,
|
||||
(start: number, length: number) => SeriesApi.seriesBy(start, length)
|
||||
export const episodeUpdateBySeriesId = createAsyncThunk(
|
||||
"episodes/update/series_id",
|
||||
async (seriesid: number[]) => {
|
||||
const response = await EpisodesApi.bySeriesId(seriesid);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const seriesUpdateHistoryList = createAsyncAction(
|
||||
SERIES_UPDATE_HISTORY_LIST,
|
||||
() => EpisodesApi.history()
|
||||
export const episodeUpdateById = createAsyncThunk(
|
||||
"episodes/update/episodes_id",
|
||||
async (episodeid: number[]) => {
|
||||
const response = await EpisodesApi.byEpisodeId(episodeid);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const episodesUpdateHistory = createAsyncThunk(
|
||||
"episodes/history/update",
|
||||
async () => {
|
||||
const response = await EpisodesApi.history();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const episodesMarkHistoryDirty = createAction("episodes/history/update");
|
||||
|
||||
export const episodesUpdateBlacklist = createAsyncThunk(
|
||||
"episodes/blacklist/update",
|
||||
async () => {
|
||||
const response = await EpisodesApi.blacklist();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const seriesUpdateBlacklist = createAsyncAction(
|
||||
SERIES_UPDATE_BLACKLIST,
|
||||
() => EpisodesApi.blacklist()
|
||||
export const episodesMarkBlacklistDirty = createAction(
|
||||
"episodes/blacklist/update"
|
||||
);
|
||||
|
@ -1,63 +1,45 @@
|
||||
import { createAction } from "redux-actions";
|
||||
import { createAction, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { BadgesApi } from "../../apis";
|
||||
import {
|
||||
SITE_BADGE_UPDATE,
|
||||
SITE_INITIALIZED,
|
||||
SITE_INITIALIZE_FAILED,
|
||||
SITE_NEED_AUTH,
|
||||
SITE_NOTIFICATIONS_ADD,
|
||||
SITE_NOTIFICATIONS_REMOVE,
|
||||
SITE_OFFLINE_UPDATE,
|
||||
SITE_PROGRESS_ADD,
|
||||
SITE_PROGRESS_REMOVE,
|
||||
SITE_SIDEBAR_UPDATE,
|
||||
} from "../constants";
|
||||
import { createAsyncAction, createCallbackAction } from "./factory";
|
||||
import { systemUpdateLanguagesAll, systemUpdateSettings } from "./system";
|
||||
|
||||
export const bootstrap = createCallbackAction(
|
||||
() => [systemUpdateLanguagesAll(), systemUpdateSettings(), badgeUpdateAll()],
|
||||
() => siteInitialized(),
|
||||
() => siteInitializationFailed()
|
||||
);
|
||||
import { systemUpdateAllSettings } from "./system";
|
||||
|
||||
// TODO: Override error messages
|
||||
export const siteInitializationFailed = createAction(SITE_INITIALIZE_FAILED);
|
||||
export const siteBootstrap = createAsyncThunk(
|
||||
"site/bootstrap",
|
||||
(_: undefined, { dispatch }) => {
|
||||
return Promise.all([
|
||||
dispatch(systemUpdateAllSettings()),
|
||||
dispatch(siteUpdateBadges()),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
const siteInitialized = createAction(SITE_INITIALIZED);
|
||||
export const siteUpdateInitialization = createAction<string | true>(
|
||||
"site/initialization/update"
|
||||
);
|
||||
|
||||
export const siteRedirectToAuth = createAction(SITE_NEED_AUTH);
|
||||
export const siteRedirectToAuth = createAction("site/redirect_auth");
|
||||
|
||||
export const badgeUpdateAll = createAsyncAction(SITE_BADGE_UPDATE, () =>
|
||||
BadgesApi.all()
|
||||
export const siteAddNotifications = createAction<Server.Notification[]>(
|
||||
"site/notifications/add"
|
||||
);
|
||||
|
||||
export const siteAddNotifications = createAction(
|
||||
SITE_NOTIFICATIONS_ADD,
|
||||
(notification: ReduxStore.Notification[]) => notification
|
||||
export const siteRemoveNotifications = createAction<string>(
|
||||
"site/notifications/remove"
|
||||
);
|
||||
|
||||
export const siteRemoveNotifications = createAction(
|
||||
SITE_NOTIFICATIONS_REMOVE,
|
||||
(id: string) => id
|
||||
export const siteAddProgress = createAction<Server.Progress[]>(
|
||||
"site/progress/add"
|
||||
);
|
||||
|
||||
export const siteAddProgress = createAction(
|
||||
SITE_PROGRESS_ADD,
|
||||
(progress: ReduxStore.Progress[]) => progress
|
||||
);
|
||||
export const siteRemoveProgress = createAction<string>("site/progress/remove");
|
||||
|
||||
export const siteRemoveProgress = createAction(
|
||||
SITE_PROGRESS_REMOVE,
|
||||
(id: string) => id
|
||||
);
|
||||
export const siteChangeSidebar = createAction<string>("site/sidebar/update");
|
||||
|
||||
export const siteChangeSidebar = createAction(
|
||||
SITE_SIDEBAR_UPDATE,
|
||||
(id: string) => id
|
||||
);
|
||||
export const siteUpdateOffline = createAction<boolean>("site/offline/update");
|
||||
|
||||
export const siteUpdateOffline = createAction(
|
||||
SITE_OFFLINE_UPDATE,
|
||||
(state: boolean) => state
|
||||
export const siteUpdateBadges = createAsyncThunk(
|
||||
"site/badges/update",
|
||||
async () => {
|
||||
const response = await BadgesApi.all();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
@ -1,64 +1,87 @@
|
||||
import { createAction, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { ProvidersApi, SystemApi } from "../../apis";
|
||||
import {
|
||||
SYSTEM_UPDATE_HEALTH,
|
||||
SYSTEM_UPDATE_LANGUAGES_LIST,
|
||||
SYSTEM_UPDATE_LANGUAGES_PROFILE_LIST,
|
||||
SYSTEM_UPDATE_LOGS,
|
||||
SYSTEM_UPDATE_PROVIDERS,
|
||||
SYSTEM_UPDATE_RELEASES,
|
||||
SYSTEM_UPDATE_SETTINGS,
|
||||
SYSTEM_UPDATE_STATUS,
|
||||
SYSTEM_UPDATE_TASKS,
|
||||
} from "../constants";
|
||||
import { createAsyncAction, createAsyncCombineAction } from "./factory";
|
||||
|
||||
export const systemUpdateLanguagesAll = createAsyncCombineAction(() => [
|
||||
systemUpdateLanguages(),
|
||||
systemUpdateLanguagesProfiles(),
|
||||
export const systemUpdateAllSettings = createAsyncThunk(
|
||||
"system/update",
|
||||
async (_: undefined, { dispatch }) => {
|
||||
await Promise.all([
|
||||
dispatch(systemUpdateSettings()),
|
||||
dispatch(systemUpdateLanguages()),
|
||||
dispatch(systemUpdateLanguagesProfiles()),
|
||||
]);
|
||||
|
||||
export const systemUpdateLanguages = createAsyncAction(
|
||||
SYSTEM_UPDATE_LANGUAGES_LIST,
|
||||
() => SystemApi.languages()
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateLanguagesProfiles = createAsyncAction(
|
||||
SYSTEM_UPDATE_LANGUAGES_PROFILE_LIST,
|
||||
() => SystemApi.languagesProfileList()
|
||||
export const systemUpdateLanguages = createAsyncThunk(
|
||||
"system/languages/update",
|
||||
async () => {
|
||||
const response = await SystemApi.languages();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateStatus = createAsyncAction(SYSTEM_UPDATE_STATUS, () =>
|
||||
SystemApi.status()
|
||||
export const systemUpdateLanguagesProfiles = createAsyncThunk(
|
||||
"system/languages/profile/update",
|
||||
async () => {
|
||||
const response = await SystemApi.languagesProfileList();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateHealth = createAsyncAction(SYSTEM_UPDATE_HEALTH, () =>
|
||||
SystemApi.health()
|
||||
export const systemUpdateStatus = createAsyncThunk(
|
||||
"system/status/update",
|
||||
async () => {
|
||||
const response = await SystemApi.status();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateTasks = createAsyncAction(SYSTEM_UPDATE_TASKS, () =>
|
||||
SystemApi.getTasks()
|
||||
export const systemUpdateHealth = createAsyncThunk(
|
||||
"system/health/update",
|
||||
async () => {
|
||||
const response = await SystemApi.health();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateLogs = createAsyncAction(SYSTEM_UPDATE_LOGS, () =>
|
||||
SystemApi.logs()
|
||||
export const systemMarkTasksDirty = createAction("system/tasks/mark_dirty");
|
||||
|
||||
export const systemUpdateTasks = createAsyncThunk(
|
||||
"system/tasks/update",
|
||||
async () => {
|
||||
const response = await SystemApi.tasks();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateReleases = createAsyncAction(
|
||||
SYSTEM_UPDATE_RELEASES,
|
||||
() => SystemApi.releases()
|
||||
export const systemUpdateLogs = createAsyncThunk(
|
||||
"system/logs/update",
|
||||
async () => {
|
||||
const response = await SystemApi.logs();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateSettings = createAsyncAction(
|
||||
SYSTEM_UPDATE_SETTINGS,
|
||||
() => SystemApi.settings()
|
||||
export const systemUpdateReleases = createAsyncThunk(
|
||||
"system/releases/update",
|
||||
async () => {
|
||||
const response = await SystemApi.releases();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const providerUpdateList = createAsyncAction(
|
||||
SYSTEM_UPDATE_PROVIDERS,
|
||||
() => ProvidersApi.providers()
|
||||
export const systemUpdateSettings = createAsyncThunk(
|
||||
"system/settings/update",
|
||||
async () => {
|
||||
const response = await SystemApi.settings();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateSettingsAll = createAsyncCombineAction(() => [
|
||||
systemUpdateSettings(),
|
||||
systemUpdateLanguagesAll(),
|
||||
]);
|
||||
export const providerUpdateList = createAsyncThunk(
|
||||
"providers/update",
|
||||
async () => {
|
||||
const response = await ProvidersApi.providers();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
@ -1,43 +0,0 @@
|
||||
// Provider action
|
||||
|
||||
// System action
|
||||
export const SYSTEM_UPDATE_LANGUAGES_LIST = "UPDATE_ALL_LANGUAGES_LIST";
|
||||
export const SYSTEM_UPDATE_LANGUAGES_PROFILE_LIST =
|
||||
"UPDATE_LANGUAGES_PROFILE_LIST";
|
||||
export const SYSTEM_UPDATE_STATUS = "UPDATE_SYSTEM_STATUS";
|
||||
export const SYSTEM_UPDATE_HEALTH = "UPDATE_SYSTEM_HEALTH";
|
||||
export const SYSTEM_UPDATE_TASKS = "UPDATE_SYSTEM_TASKS";
|
||||
export const SYSTEM_UPDATE_LOGS = "UPDATE_SYSTEM_LOGS";
|
||||
export const SYSTEM_UPDATE_RELEASES = "SYSTEM_UPDATE_RELEASES";
|
||||
export const SYSTEM_UPDATE_SETTINGS = "UPDATE_SYSTEM_SETTINGS";
|
||||
export const SYSTEM_UPDATE_PROVIDERS = "SYSTEM_UPDATE_PROVIDERS";
|
||||
|
||||
// Series action
|
||||
export const SERIES_UPDATE_WANTED_LIST = "UPDATE_SERIES_WANTED_LIST";
|
||||
export const SERIES_DELETE_WANTED_ITEMS = "SERIES_DELETE_WANTED_ITEMS";
|
||||
export const SERIES_UPDATE_EPISODE_LIST = "UPDATE_SERIES_EPISODE_LIST";
|
||||
export const SERIES_DELETE_EPISODES = "SERIES_DELETE_EPISODES";
|
||||
export const SERIES_UPDATE_HISTORY_LIST = "UPDATE_SERIES_HISTORY_LIST";
|
||||
export const SERIES_UPDATE_LIST = "UPDATE_SEIRES_LIST";
|
||||
export const SERIES_DELETE_ITEMS = "SERIES_DELETE_ITEMS";
|
||||
export const SERIES_UPDATE_BLACKLIST = "UPDATE_SERIES_BLACKLIST";
|
||||
|
||||
// Movie action
|
||||
export const MOVIES_UPDATE_LIST = "UPDATE_MOVIE_LIST";
|
||||
export const MOVIES_DELETE_ITEMS = "MOVIES_DELETE_ITEMS";
|
||||
export const MOVIES_UPDATE_WANTED_LIST = "UPDATE_MOVIE_WANTED_LIST";
|
||||
export const MOVIES_DELETE_WANTED_ITEMS = "MOVIES_DELETE_WANTED_ITEMS";
|
||||
export const MOVIES_UPDATE_HISTORY_LIST = "UPDATE_MOVIE_HISTORY_LIST";
|
||||
export const MOVIES_UPDATE_BLACKLIST = "UPDATE_MOVIES_BLACKLIST";
|
||||
|
||||
// Site Action
|
||||
export const SITE_NEED_AUTH = "SITE_NEED_AUTH";
|
||||
export const SITE_INITIALIZED = "SITE_SYSTEM_INITIALIZED";
|
||||
export const SITE_INITIALIZE_FAILED = "SITE_INITIALIZE_FAILED";
|
||||
export const SITE_NOTIFICATIONS_ADD = "SITE_NOTIFICATIONS_ADD";
|
||||
export const SITE_NOTIFICATIONS_REMOVE = "SITE_NOTIFICATIONS_REMOVE";
|
||||
export const SITE_PROGRESS_ADD = "SITE_PROGRESS_ADD";
|
||||
export const SITE_PROGRESS_REMOVE = "SITE_PROGRESS_REMOVE";
|
||||
export const SITE_SIDEBAR_UPDATE = "SITE_SIDEBAR_UPDATE";
|
||||
export const SITE_BADGE_UPDATE = "SITE_BADGE_UPDATE";
|
||||
export const SITE_OFFLINE_UPDATE = "SITE_OFFLINE_UPDATE";
|
@ -0,0 +1,29 @@
|
||||
import { AsyncThunk } from "@reduxjs/toolkit";
|
||||
import { useEffect } from "react";
|
||||
import { log } from "../../utilites/logger";
|
||||
import { useReduxAction } from "./base";
|
||||
|
||||
export function useAutoUpdate(item: Async.Item<any>, update: () => void) {
|
||||
useEffect(() => {
|
||||
if (item.state === "uninitialized" || item.state === "dirty") {
|
||||
update();
|
||||
}
|
||||
}, [item.state, update]);
|
||||
}
|
||||
|
||||
export function useAutoDirtyUpdate(
|
||||
item: Async.List<any> | Async.Entity<any>,
|
||||
updateAction: AsyncThunk<any, number[], {}>
|
||||
) {
|
||||
const { state, dirtyEntities } = item;
|
||||
const hasDirty = dirtyEntities.length > 0 && state === "dirty";
|
||||
|
||||
const update = useReduxAction(updateAction);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasDirty) {
|
||||
log("info", "updating dirty entities...");
|
||||
update(dirtyEntities.map(Number));
|
||||
}
|
||||
}, [hasDirty, dirtyEntities, update]);
|
||||
}
|
@ -1,36 +1,24 @@
|
||||
import { ActionCreator } from "@reduxjs/toolkit";
|
||||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createCallbackAction } from "../actions/factory";
|
||||
import { ActionCallback, AsyncActionDispatcher } from "../types";
|
||||
import { AppDispatch, RootState } from "../store";
|
||||
|
||||
// function use
|
||||
export function useReduxStore<T extends (store: ReduxStore) => any>(
|
||||
export function useReduxStore<T extends (store: RootState) => any>(
|
||||
selector: T
|
||||
) {
|
||||
return useSelector<ReduxStore, ReturnType<T>>(selector);
|
||||
return useSelector<RootState, ReturnType<T>>(selector);
|
||||
}
|
||||
|
||||
export function useReduxAction<T extends (...args: any[]) => void>(action: T) {
|
||||
const dispatch = useDispatch();
|
||||
return useCallback((...args: Parameters<T>) => dispatch(action(...args)), [
|
||||
action,
|
||||
dispatch,
|
||||
]);
|
||||
export function useAppDispatch() {
|
||||
return useDispatch<AppDispatch>();
|
||||
}
|
||||
|
||||
export function useReduxActionWith<
|
||||
T extends (...args: any[]) => AsyncActionDispatcher<any>
|
||||
>(action: T, success: ActionCallback) {
|
||||
const dispatch = useDispatch();
|
||||
// TODO: Fix type
|
||||
export function useReduxAction<T extends ActionCreator<any>>(action: T) {
|
||||
const dispatch = useAppDispatch();
|
||||
return useCallback(
|
||||
(...args: Parameters<T>) => {
|
||||
const callbackAction = createCallbackAction(
|
||||
() => [action(...args)],
|
||||
success
|
||||
);
|
||||
|
||||
dispatch(callbackAction());
|
||||
},
|
||||
[dispatch, action, success]
|
||||
(...args: Parameters<T>) => dispatch(action(...args)),
|
||||
[action, dispatch]
|
||||
);
|
||||
}
|
||||
|
@ -1,393 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useSocketIOReducer, useWrapToOptionalId } from "../../@socketio/hooks";
|
||||
import { buildOrderList } from "../../utilites";
|
||||
import {
|
||||
episodeDeleteItems,
|
||||
episodeUpdateBy,
|
||||
episodeUpdateById,
|
||||
movieUpdateBlacklist,
|
||||
movieUpdateHistoryList,
|
||||
movieUpdateList,
|
||||
movieUpdateWantedList,
|
||||
providerUpdateList,
|
||||
seriesUpdateBlacklist,
|
||||
seriesUpdateHistoryList,
|
||||
seriesUpdateList,
|
||||
seriesUpdateWantedList,
|
||||
systemUpdateHealth,
|
||||
systemUpdateLanguages,
|
||||
systemUpdateLanguagesProfiles,
|
||||
systemUpdateLogs,
|
||||
systemUpdateReleases,
|
||||
systemUpdateSettingsAll,
|
||||
systemUpdateStatus,
|
||||
systemUpdateTasks,
|
||||
} from "../actions";
|
||||
import { useReduxAction, useReduxStore } from "./base";
|
||||
|
||||
function stateBuilder<T, D extends (...args: any[]) => any>(
|
||||
t: T,
|
||||
d: D
|
||||
): [Readonly<T>, D] {
|
||||
return [t, d];
|
||||
}
|
||||
|
||||
export function useSystemSettings() {
|
||||
const update = useReduxAction(systemUpdateSettingsAll);
|
||||
const items = useReduxStore((s) => s.system.settings);
|
||||
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemLogs() {
|
||||
const items = useReduxStore(({ system }) => system.logs);
|
||||
const update = useReduxAction(systemUpdateLogs);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemTasks() {
|
||||
const items = useReduxStore((s) => s.system.tasks);
|
||||
const update = useReduxAction(systemUpdateTasks);
|
||||
const reducer = useMemo<SocketIO.Reducer>(() => ({ key: "task", update }), [
|
||||
update,
|
||||
]);
|
||||
useSocketIOReducer(reducer);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemStatus() {
|
||||
const items = useReduxStore((s) => s.system.status.data);
|
||||
const update = useReduxAction(systemUpdateStatus);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemHealth() {
|
||||
const update = useReduxAction(systemUpdateHealth);
|
||||
const items = useReduxStore((s) => s.system.health);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemProviders() {
|
||||
const update = useReduxAction(providerUpdateList);
|
||||
const items = useReduxStore((d) => d.system.providers);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSystemReleases() {
|
||||
const items = useReduxStore(({ system }) => system.releases);
|
||||
const update = useReduxAction(systemUpdateReleases);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useLanguageProfiles() {
|
||||
const action = useReduxAction(systemUpdateLanguagesProfiles);
|
||||
const items = useReduxStore((s) => s.system.languagesProfiles.data);
|
||||
|
||||
return stateBuilder(items, action);
|
||||
}
|
||||
|
||||
export function useProfileBy(id: number | null | undefined) {
|
||||
const [profiles] = useLanguageProfiles();
|
||||
return useMemo(() => profiles.find((v) => v.profileId === id), [
|
||||
id,
|
||||
profiles,
|
||||
]);
|
||||
}
|
||||
|
||||
export function useLanguages(enabled: boolean = false) {
|
||||
const action = useReduxAction(systemUpdateLanguages);
|
||||
const items = useReduxStore((s) =>
|
||||
enabled ? s.system.enabledLanguage.data : s.system.languages.data
|
||||
);
|
||||
return stateBuilder(items, action);
|
||||
}
|
||||
|
||||
function useLanguageGetter(enabled: boolean = false) {
|
||||
const [languages] = useLanguages(enabled);
|
||||
return useCallback(
|
||||
(code?: string) => {
|
||||
if (code === undefined) {
|
||||
return undefined;
|
||||
} else {
|
||||
return languages.find((v) => v.code2 === code);
|
||||
}
|
||||
},
|
||||
[languages]
|
||||
);
|
||||
}
|
||||
|
||||
export function useLanguageBy(code?: string) {
|
||||
const getter = useLanguageGetter();
|
||||
return useMemo(() => getter(code), [code, getter]);
|
||||
}
|
||||
|
||||
// Convert languageprofile items to language
|
||||
export function useProfileItems(profile?: Profile.Languages) {
|
||||
const getter = useLanguageGetter(true);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
profile?.items.map<Language>(({ language, hi, forced }) => {
|
||||
const name = getter(language)?.name ?? "";
|
||||
return {
|
||||
hi: hi === "True",
|
||||
forced: forced === "True",
|
||||
code2: language,
|
||||
name,
|
||||
};
|
||||
}) ?? [],
|
||||
[getter, profile?.items]
|
||||
);
|
||||
}
|
||||
|
||||
export function useRawSeries() {
|
||||
const update = useReduxAction(seriesUpdateList);
|
||||
const items = useReduxStore((d) => d.series.seriesList);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSeries(order = true) {
|
||||
const [rawSeries, action] = useRawSeries();
|
||||
const series = useMemo<AsyncState<Item.Series[]>>(() => {
|
||||
const state = rawSeries.data;
|
||||
if (order) {
|
||||
return {
|
||||
...rawSeries,
|
||||
data: buildOrderList(state),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...rawSeries,
|
||||
data: Object.values(state.items),
|
||||
};
|
||||
}
|
||||
}, [rawSeries, order]);
|
||||
return stateBuilder(series, action);
|
||||
}
|
||||
|
||||
export function useSerieBy(id?: number) {
|
||||
const [series, updateSerie] = useRawSeries();
|
||||
const serie = useMemo<AsyncState<Item.Series | null>>(() => {
|
||||
const items = series.data.items;
|
||||
let item: Item.Series | null = null;
|
||||
if (id && !isNaN(id) && id in items) {
|
||||
item = items[id];
|
||||
}
|
||||
return {
|
||||
...series,
|
||||
data: item,
|
||||
};
|
||||
}, [id, series]);
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (id && !isNaN(id)) {
|
||||
updateSerie([id]);
|
||||
}
|
||||
}, [id, updateSerie]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serie.data === null) {
|
||||
update();
|
||||
}
|
||||
}, [serie.data, update]);
|
||||
return stateBuilder(serie, update);
|
||||
}
|
||||
|
||||
export function useEpisodesBy(seriesId?: number) {
|
||||
const action = useReduxAction(episodeUpdateBy);
|
||||
const update = useCallback(() => {
|
||||
if (seriesId !== undefined && !isNaN(seriesId)) {
|
||||
action([seriesId]);
|
||||
}
|
||||
}, [action, seriesId]);
|
||||
|
||||
const list = useReduxStore((d) => d.series.episodeList);
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (seriesId !== undefined && !isNaN(seriesId)) {
|
||||
return list.data.filter((v) => v.sonarrSeriesId === seriesId);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [seriesId, list.data]);
|
||||
|
||||
const state: AsyncState<Item.Episode[]> = useMemo(
|
||||
() => ({
|
||||
...list,
|
||||
data: items,
|
||||
}),
|
||||
[list, items]
|
||||
);
|
||||
|
||||
const actionById = useReduxAction(episodeUpdateById);
|
||||
const wrapActionById = useWrapToOptionalId(actionById);
|
||||
const deleteAction = useReduxAction(episodeDeleteItems);
|
||||
const episodeReducer = useMemo<SocketIO.Reducer>(
|
||||
() => ({ key: "episode", update: wrapActionById, delete: deleteAction }),
|
||||
[wrapActionById, deleteAction]
|
||||
);
|
||||
useSocketIOReducer(episodeReducer);
|
||||
|
||||
const wrapAction = useWrapToOptionalId(action);
|
||||
const seriesReducer = useMemo<SocketIO.Reducer>(
|
||||
() => ({ key: "series", update: wrapAction }),
|
||||
[wrapAction]
|
||||
);
|
||||
useSocketIOReducer(seriesReducer);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(state, update);
|
||||
}
|
||||
|
||||
export function useRawMovies() {
|
||||
const update = useReduxAction(movieUpdateList);
|
||||
const items = useReduxStore((d) => d.movie.movieList);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useMovies(order = true) {
|
||||
const [rawMovies, action] = useRawMovies();
|
||||
const movies = useMemo<AsyncState<Item.Movie[]>>(() => {
|
||||
const state = rawMovies.data;
|
||||
if (order) {
|
||||
return {
|
||||
...rawMovies,
|
||||
data: buildOrderList(state),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...rawMovies,
|
||||
data: Object.values(state.items),
|
||||
};
|
||||
}
|
||||
}, [rawMovies, order]);
|
||||
return stateBuilder(movies, action);
|
||||
}
|
||||
|
||||
export function useMovieBy(id?: number) {
|
||||
const [movies, updateMovies] = useRawMovies();
|
||||
const movie = useMemo<AsyncState<Item.Movie | null>>(() => {
|
||||
const items = movies.data.items;
|
||||
let item: Item.Movie | null = null;
|
||||
if (id && !isNaN(id) && id in items) {
|
||||
item = items[id];
|
||||
}
|
||||
return {
|
||||
...movies,
|
||||
data: item,
|
||||
};
|
||||
}, [id, movies]);
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (id && !isNaN(id)) {
|
||||
updateMovies([id]);
|
||||
}
|
||||
}, [id, updateMovies]);
|
||||
|
||||
useEffect(() => {
|
||||
if (movie.data === null) {
|
||||
update();
|
||||
}
|
||||
}, [movie.data, update]);
|
||||
return stateBuilder(movie, update);
|
||||
}
|
||||
|
||||
export function useWantedSeries() {
|
||||
const update = useReduxAction(seriesUpdateWantedList);
|
||||
const items = useReduxStore((d) => d.series.wantedEpisodesList);
|
||||
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useWantedMovies() {
|
||||
const update = useReduxAction(movieUpdateWantedList);
|
||||
const items = useReduxStore((d) => d.movie.wantedMovieList);
|
||||
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useBlacklistMovies() {
|
||||
const update = useReduxAction(movieUpdateBlacklist);
|
||||
const items = useReduxStore((d) => d.movie.blacklist);
|
||||
const reducer = useMemo<SocketIO.Reducer>(
|
||||
() => ({ key: "movie-blacklist", any: update }),
|
||||
[update]
|
||||
);
|
||||
useSocketIOReducer(reducer);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useBlacklistSeries() {
|
||||
const update = useReduxAction(seriesUpdateBlacklist);
|
||||
const items = useReduxStore((d) => d.series.blacklist);
|
||||
const reducer = useMemo<SocketIO.Reducer>(
|
||||
() => ({ key: "episode-blacklist", any: update }),
|
||||
[update]
|
||||
);
|
||||
useSocketIOReducer(reducer);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useMoviesHistory() {
|
||||
const update = useReduxAction(movieUpdateHistoryList);
|
||||
const items = useReduxStore((s) => s.movie.historyList);
|
||||
const reducer = useMemo<SocketIO.Reducer>(
|
||||
() => ({ key: "movie-history", update }),
|
||||
[update]
|
||||
);
|
||||
useSocketIOReducer(reducer);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
|
||||
export function useSeriesHistory() {
|
||||
const update = useReduxAction(seriesUpdateHistoryList);
|
||||
const items = useReduxStore((s) => s.series.historyList);
|
||||
const reducer = useMemo<SocketIO.Reducer>(
|
||||
() => ({ key: "episode-history", update }),
|
||||
[update]
|
||||
);
|
||||
useSocketIOReducer(reducer);
|
||||
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
return stateBuilder(items, update);
|
||||
}
|
||||
export * from "./movies";
|
||||
export * from "./series";
|
||||
export * from "./site";
|
||||
export * from "./system";
|
||||
|
@ -0,0 +1,70 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useEntityItemById, useEntityToList } from "../../utilites";
|
||||
import {
|
||||
movieUpdateBlacklist,
|
||||
movieUpdateById,
|
||||
movieUpdateHistory,
|
||||
movieUpdateWantedById,
|
||||
} from "../actions";
|
||||
import { useAutoDirtyUpdate, useAutoUpdate } from "./async";
|
||||
import { useReduxAction, useReduxStore } from "./base";
|
||||
|
||||
export function useMovieEntities() {
|
||||
const entities = useReduxStore((d) => d.movies.movieList);
|
||||
|
||||
useAutoDirtyUpdate(entities, movieUpdateById);
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
export function useMovies() {
|
||||
const rawMovies = useMovieEntities();
|
||||
const content = useEntityToList(rawMovies.content);
|
||||
const movies = useMemo<Async.List<Item.Movie>>(() => {
|
||||
return {
|
||||
...rawMovies,
|
||||
keyName: rawMovies.content.keyName,
|
||||
content,
|
||||
};
|
||||
}, [rawMovies, content]);
|
||||
return movies;
|
||||
}
|
||||
|
||||
export function useMovieBy(id: number) {
|
||||
const movies = useMovieEntities();
|
||||
const action = useReduxAction(movieUpdateById);
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (!isNaN(id)) {
|
||||
action([id]);
|
||||
}
|
||||
}, [id, action]);
|
||||
|
||||
const movie = useEntityItemById(movies, id.toString());
|
||||
|
||||
useAutoUpdate(movie, update);
|
||||
return movie;
|
||||
}
|
||||
|
||||
export function useWantedMovies() {
|
||||
const items = useReduxStore((d) => d.movies.wantedMovieList);
|
||||
|
||||
useAutoDirtyUpdate(items, movieUpdateWantedById);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useBlacklistMovies() {
|
||||
const update = useReduxAction(movieUpdateBlacklist);
|
||||
const items = useReduxStore((d) => d.movies.blacklist);
|
||||
|
||||
useAutoUpdate(items, update);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useMoviesHistory() {
|
||||
const update = useReduxAction(movieUpdateHistory);
|
||||
const items = useReduxStore((s) => s.movies.historyList);
|
||||
|
||||
useAutoUpdate(items, update);
|
||||
return items;
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useEntityItemById, useEntityToList } from "../../utilites";
|
||||
import {
|
||||
episodesUpdateBlacklist,
|
||||
episodesUpdateHistory,
|
||||
episodeUpdateById,
|
||||
episodeUpdateBySeriesId,
|
||||
seriesUpdateById,
|
||||
seriesUpdateWantedById,
|
||||
} from "../actions";
|
||||
import { useAutoDirtyUpdate, useAutoUpdate } from "./async";
|
||||
import { useReduxAction, useReduxStore } from "./base";
|
||||
|
||||
export function useSerieEntities() {
|
||||
const items = useReduxStore((d) => d.series.seriesList);
|
||||
|
||||
useAutoDirtyUpdate(items, seriesUpdateById);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useSeries() {
|
||||
const rawSeries = useSerieEntities();
|
||||
const content = useEntityToList(rawSeries.content);
|
||||
const series = useMemo<Async.List<Item.Series>>(() => {
|
||||
return {
|
||||
...rawSeries,
|
||||
keyName: rawSeries.content.keyName,
|
||||
content,
|
||||
};
|
||||
}, [rawSeries, content]);
|
||||
return series;
|
||||
}
|
||||
|
||||
export function useSerieBy(id: number) {
|
||||
const series = useSerieEntities();
|
||||
const action = useReduxAction(seriesUpdateById);
|
||||
const serie = useEntityItemById(series, id.toString());
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (!isNaN(id)) {
|
||||
action([id]);
|
||||
}
|
||||
}, [id, action]);
|
||||
|
||||
useAutoUpdate(serie, update);
|
||||
return serie;
|
||||
}
|
||||
|
||||
export function useEpisodesBy(seriesId: number) {
|
||||
const action = useReduxAction(episodeUpdateBySeriesId);
|
||||
const update = useCallback(() => {
|
||||
if (!isNaN(seriesId)) {
|
||||
action([seriesId]);
|
||||
}
|
||||
}, [action, seriesId]);
|
||||
|
||||
const episodes = useReduxStore((d) => d.series.episodeList);
|
||||
|
||||
const newContent = useMemo(() => {
|
||||
return episodes.content.filter((v) => v.sonarrSeriesId === seriesId);
|
||||
}, [seriesId, episodes.content]);
|
||||
|
||||
const newList: Async.List<Item.Episode> = useMemo(
|
||||
() => ({
|
||||
...episodes,
|
||||
content: newContent,
|
||||
}),
|
||||
[episodes, newContent]
|
||||
);
|
||||
|
||||
// FIXME
|
||||
useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
|
||||
useAutoDirtyUpdate(episodes, episodeUpdateById);
|
||||
|
||||
return newList;
|
||||
}
|
||||
|
||||
export function useWantedSeries() {
|
||||
const items = useReduxStore((d) => d.series.wantedEpisodesList);
|
||||
|
||||
useAutoDirtyUpdate(items, seriesUpdateWantedById);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useBlacklistSeries() {
|
||||
const update = useReduxAction(episodesUpdateBlacklist);
|
||||
const items = useReduxStore((d) => d.series.blacklist);
|
||||
|
||||
useAutoUpdate(items, update);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useSeriesHistory() {
|
||||
const update = useReduxAction(episodesUpdateHistory);
|
||||
const items = useReduxStore((s) => s.series.historyList);
|
||||
|
||||
useAutoUpdate(items, update);
|
||||
return items;
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
providerUpdateList,
|
||||
systemUpdateHealth,
|
||||
systemUpdateLogs,
|
||||
systemUpdateReleases,
|
||||
systemUpdateStatus,
|
||||
systemUpdateTasks,
|
||||
} from "../actions";
|
||||
import { useAutoUpdate } from "./async";
|
||||
import { useReduxAction, useReduxStore } from "./base";
|
||||
|
||||
export function useSystemSettings() {
|
||||
const items = useReduxStore((s) => s.system.settings);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useSystemLogs() {
|
||||
const items = useReduxStore(({ system }) => system.logs);
|
||||
const update = useReduxAction(systemUpdateLogs);
|
||||
|
||||
useAutoUpdate(items, update);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useSystemTasks() {
|
||||
const items = useReduxStore((s) => s.system.tasks);
|
||||
const update = useReduxAction(systemUpdateTasks);
|
||||
|
||||
useAutoUpdate(items, update);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useSystemStatus() {
|
||||
const items = useReduxStore((s) => s.system.status);
|
||||
const update = useReduxAction(systemUpdateStatus);
|
||||
|
||||
useAutoUpdate(items, update);
|
||||
return items.content;
|
||||
}
|
||||
|
||||
export function useSystemHealth() {
|
||||
const items = useReduxStore((s) => s.system.health);
|
||||
const update = useReduxAction(systemUpdateHealth);
|
||||
|
||||
useAutoUpdate(items, update);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useSystemProviders() {
|
||||
const update = useReduxAction(providerUpdateList);
|
||||
const items = useReduxStore((d) => d.system.providers);
|
||||
|
||||
useAutoUpdate(items, update);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useSystemReleases() {
|
||||
const items = useReduxStore(({ system }) => system.releases);
|
||||
const update = useReduxAction(systemUpdateReleases);
|
||||
|
||||
useAutoUpdate(items, update);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function useLanguageProfiles() {
|
||||
const items = useReduxStore((s) => s.system.languagesProfiles);
|
||||
|
||||
return items.content;
|
||||
}
|
||||
|
||||
export function useProfileBy(id: number | null | undefined) {
|
||||
const profiles = useLanguageProfiles();
|
||||
return useMemo(
|
||||
() => profiles?.find((v) => v.profileId === id),
|
||||
[id, profiles]
|
||||
);
|
||||
}
|
||||
|
||||
export function useLanguages() {
|
||||
const data = useReduxStore((s) => s.system.languages);
|
||||
|
||||
const languages = useMemo<Language.Info[]>(
|
||||
() => data.content?.map((v) => ({ code2: v.code2, name: v.name })) ?? [],
|
||||
[data.content]
|
||||
);
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
export function useEnabledLanguages() {
|
||||
const data = useReduxStore((s) => s.system.languages);
|
||||
|
||||
const enabled = useMemo<Language.Info[]>(
|
||||
() =>
|
||||
data.content
|
||||
?.filter((v) => v.enabled)
|
||||
.map((v) => ({ code2: v.code2, name: v.name })) ?? [],
|
||||
[data.content]
|
||||
);
|
||||
|
||||
return enabled;
|
||||
}
|
||||
|
||||
export function useLanguageBy(code?: string) {
|
||||
const languages = useLanguages();
|
||||
return useMemo(
|
||||
() => languages.find((v) => v.code2 === code),
|
||||
[languages, code]
|
||||
);
|
||||
}
|
||||
|
||||
// Convert languageprofile items to language
|
||||
export function useProfileItemsToLanguages(profile?: Language.Profile) {
|
||||
const languages = useLanguages();
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
profile?.items.map<Language.Info>(({ language: code, hi, forced }) => {
|
||||
const name = languages.find((v) => v.code2 === code)?.name ?? "";
|
||||
return {
|
||||
hi: hi === "True",
|
||||
forced: forced === "True",
|
||||
code2: code,
|
||||
name,
|
||||
};
|
||||
}) ?? [],
|
||||
[languages, profile?.items]
|
||||
);
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
import { combineReducers } from "redux";
|
||||
import movie from "./movie";
|
||||
import movies from "./movie";
|
||||
import series from "./series";
|
||||
import site from "./site";
|
||||
import system from "./system";
|
||||
|
||||
export default combineReducers({
|
||||
system,
|
||||
const AllReducers = {
|
||||
movies,
|
||||
series,
|
||||
movie,
|
||||
site,
|
||||
});
|
||||
system,
|
||||
};
|
||||
|
||||
export default AllReducers;
|
||||
|
@ -1,81 +1,64 @@
|
||||
import { Action, handleActions } from "redux-actions";
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import {
|
||||
MOVIES_DELETE_ITEMS,
|
||||
MOVIES_DELETE_WANTED_ITEMS,
|
||||
MOVIES_UPDATE_BLACKLIST,
|
||||
MOVIES_UPDATE_HISTORY_LIST,
|
||||
MOVIES_UPDATE_LIST,
|
||||
MOVIES_UPDATE_WANTED_LIST,
|
||||
} from "../constants";
|
||||
import { AsyncAction } from "../types";
|
||||
import { defaultAOS } from "../utils";
|
||||
movieMarkBlacklistDirty,
|
||||
movieMarkDirtyById,
|
||||
movieMarkHistoryDirty,
|
||||
movieMarkWantedDirtyById,
|
||||
movieRemoveById,
|
||||
movieRemoveWantedById,
|
||||
movieUpdateAll,
|
||||
movieUpdateBlacklist,
|
||||
movieUpdateById,
|
||||
movieUpdateByRange,
|
||||
movieUpdateHistory,
|
||||
movieUpdateWantedById,
|
||||
movieUpdateWantedByRange,
|
||||
} from "../actions";
|
||||
import { AsyncUtility } from "../utils/async";
|
||||
import {
|
||||
deleteOrderListItemBy,
|
||||
updateAsyncState,
|
||||
updateOrderIdState,
|
||||
} from "../utils/mapper";
|
||||
createAsyncEntityReducer,
|
||||
createAsyncItemReducer,
|
||||
} from "../utils/factory";
|
||||
|
||||
const reducer = handleActions<ReduxStore.Movie, any>(
|
||||
{
|
||||
[MOVIES_UPDATE_WANTED_LIST]: (
|
||||
state,
|
||||
action: AsyncAction<AsyncDataWrapper<Wanted.Movie>>
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
wantedMovieList: updateOrderIdState(
|
||||
action,
|
||||
state.wantedMovieList,
|
||||
"radarrId"
|
||||
),
|
||||
};
|
||||
},
|
||||
[MOVIES_DELETE_WANTED_ITEMS]: (state, action: Action<number[]>) => {
|
||||
return {
|
||||
...state,
|
||||
wantedMovieList: deleteOrderListItemBy(action, state.wantedMovieList),
|
||||
};
|
||||
},
|
||||
[MOVIES_UPDATE_HISTORY_LIST]: (
|
||||
state,
|
||||
action: AsyncAction<History.Movie[]>
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
historyList: updateAsyncState(action, state.historyList.data),
|
||||
};
|
||||
},
|
||||
[MOVIES_UPDATE_LIST]: (
|
||||
state,
|
||||
action: AsyncAction<AsyncDataWrapper<Item.Movie>>
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
movieList: updateOrderIdState(action, state.movieList, "radarrId"),
|
||||
};
|
||||
},
|
||||
[MOVIES_DELETE_ITEMS]: (state, action: Action<number[]>) => {
|
||||
return {
|
||||
...state,
|
||||
movieList: deleteOrderListItemBy(action, state.movieList),
|
||||
};
|
||||
},
|
||||
[MOVIES_UPDATE_BLACKLIST]: (
|
||||
state,
|
||||
action: AsyncAction<Blacklist.Movie[]>
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
blacklist: updateAsyncState(action, state.blacklist.data),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
movieList: defaultAOS(),
|
||||
wantedMovieList: defaultAOS(),
|
||||
historyList: { updating: true, data: [] },
|
||||
blacklist: { updating: true, data: [] },
|
||||
interface Movie {
|
||||
movieList: Async.Entity<Item.Movie>;
|
||||
wantedMovieList: Async.Entity<Wanted.Movie>;
|
||||
historyList: Async.Item<History.Movie[]>;
|
||||
blacklist: Async.Item<Blacklist.Movie[]>;
|
||||
}
|
||||
);
|
||||
|
||||
const defaultMovie: Movie = {
|
||||
movieList: AsyncUtility.getDefaultEntity("radarrId"),
|
||||
wantedMovieList: AsyncUtility.getDefaultEntity("radarrId"),
|
||||
historyList: AsyncUtility.getDefaultItem(),
|
||||
blacklist: AsyncUtility.getDefaultItem(),
|
||||
};
|
||||
|
||||
const reducer = createReducer(defaultMovie, (builder) => {
|
||||
createAsyncEntityReducer(builder, (s) => s.movieList, {
|
||||
range: movieUpdateByRange,
|
||||
ids: movieUpdateById,
|
||||
removeIds: movieRemoveById,
|
||||
all: movieUpdateAll,
|
||||
dirty: movieMarkDirtyById,
|
||||
});
|
||||
|
||||
createAsyncEntityReducer(builder, (s) => s.wantedMovieList, {
|
||||
range: movieUpdateWantedByRange,
|
||||
ids: movieUpdateWantedById,
|
||||
removeIds: movieRemoveWantedById,
|
||||
dirty: movieMarkWantedDirtyById,
|
||||
});
|
||||
|
||||
createAsyncItemReducer(builder, (s) => s.historyList, {
|
||||
all: movieUpdateHistory,
|
||||
dirty: movieMarkHistoryDirty,
|
||||
});
|
||||
|
||||
createAsyncItemReducer(builder, (s) => s.blacklist, {
|
||||
all: movieUpdateBlacklist,
|
||||
dirty: movieMarkBlacklistDirty,
|
||||
});
|
||||
});
|
||||
|
||||
export default reducer;
|
||||
|
@ -1,116 +1,96 @@
|
||||
import { Action, handleActions } from "redux-actions";
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import {
|
||||
SERIES_DELETE_EPISODES,
|
||||
SERIES_DELETE_ITEMS,
|
||||
SERIES_DELETE_WANTED_ITEMS,
|
||||
SERIES_UPDATE_BLACKLIST,
|
||||
SERIES_UPDATE_EPISODE_LIST,
|
||||
SERIES_UPDATE_HISTORY_LIST,
|
||||
SERIES_UPDATE_LIST,
|
||||
SERIES_UPDATE_WANTED_LIST,
|
||||
} from "../constants";
|
||||
import { AsyncAction } from "../types";
|
||||
import { defaultAOS } from "../utils";
|
||||
episodesMarkBlacklistDirty,
|
||||
episodesMarkDirtyById,
|
||||
episodesMarkHistoryDirty,
|
||||
episodesRemoveById,
|
||||
episodesUpdateBlacklist,
|
||||
episodesUpdateHistory,
|
||||
episodeUpdateById,
|
||||
episodeUpdateBySeriesId,
|
||||
seriesMarkDirtyById,
|
||||
seriesMarkWantedDirtyById,
|
||||
seriesRemoveById,
|
||||
seriesRemoveWantedById,
|
||||
seriesUpdateAll,
|
||||
seriesUpdateById,
|
||||
seriesUpdateByRange,
|
||||
seriesUpdateWantedById,
|
||||
seriesUpdateWantedByRange,
|
||||
} from "../actions";
|
||||
import { AsyncReducer, AsyncUtility } from "../utils/async";
|
||||
import {
|
||||
deleteAsyncListItemBy,
|
||||
deleteOrderListItemBy,
|
||||
updateAsyncList,
|
||||
updateAsyncState,
|
||||
updateOrderIdState,
|
||||
} from "../utils/mapper";
|
||||
createAsyncEntityReducer,
|
||||
createAsyncItemReducer,
|
||||
createAsyncListReducer,
|
||||
} from "../utils/factory";
|
||||
|
||||
const reducer = handleActions<ReduxStore.Series, any>(
|
||||
{
|
||||
[SERIES_UPDATE_WANTED_LIST]: (
|
||||
state,
|
||||
action: AsyncAction<AsyncDataWrapper<Wanted.Episode>>
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
wantedEpisodesList: updateOrderIdState(
|
||||
action,
|
||||
state.wantedEpisodesList,
|
||||
"sonarrEpisodeId"
|
||||
),
|
||||
};
|
||||
},
|
||||
[SERIES_DELETE_WANTED_ITEMS]: (state, action: Action<number[]>) => {
|
||||
return {
|
||||
...state,
|
||||
wantedEpisodesList: deleteOrderListItemBy(
|
||||
action,
|
||||
state.wantedEpisodesList
|
||||
),
|
||||
};
|
||||
},
|
||||
[SERIES_UPDATE_EPISODE_LIST]: (
|
||||
state,
|
||||
action: AsyncAction<Item.Episode[]>
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
episodeList: updateAsyncList(
|
||||
action,
|
||||
state.episodeList,
|
||||
"sonarrEpisodeId"
|
||||
),
|
||||
};
|
||||
},
|
||||
[SERIES_DELETE_EPISODES]: (state, action: Action<number[]>) => {
|
||||
return {
|
||||
...state,
|
||||
episodeList: deleteAsyncListItemBy(
|
||||
action,
|
||||
state.episodeList,
|
||||
"sonarrEpisodeId"
|
||||
),
|
||||
};
|
||||
},
|
||||
[SERIES_UPDATE_HISTORY_LIST]: (
|
||||
state,
|
||||
action: AsyncAction<History.Episode[]>
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
historyList: updateAsyncState(action, state.historyList.data),
|
||||
};
|
||||
},
|
||||
[SERIES_UPDATE_LIST]: (
|
||||
state,
|
||||
action: AsyncAction<AsyncDataWrapper<Item.Series>>
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
seriesList: updateOrderIdState(
|
||||
action,
|
||||
state.seriesList,
|
||||
"sonarrSeriesId"
|
||||
),
|
||||
};
|
||||
},
|
||||
[SERIES_DELETE_ITEMS]: (state, action: Action<number[]>) => {
|
||||
return {
|
||||
...state,
|
||||
seriesList: deleteOrderListItemBy(action, state.seriesList),
|
||||
};
|
||||
},
|
||||
[SERIES_UPDATE_BLACKLIST]: (
|
||||
state,
|
||||
action: AsyncAction<Blacklist.Episode[]>
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
blacklist: updateAsyncState(action, state.blacklist.data),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
seriesList: defaultAOS(),
|
||||
wantedEpisodesList: defaultAOS(),
|
||||
episodeList: { updating: true, data: [] },
|
||||
historyList: { updating: true, data: [] },
|
||||
blacklist: { updating: true, data: [] },
|
||||
interface Series {
|
||||
seriesList: Async.Entity<Item.Series>;
|
||||
wantedEpisodesList: Async.Entity<Wanted.Episode>;
|
||||
episodeList: Async.List<Item.Episode>;
|
||||
historyList: Async.Item<History.Episode[]>;
|
||||
blacklist: Async.Item<Blacklist.Episode[]>;
|
||||
}
|
||||
);
|
||||
|
||||
const defaultSeries: Series = {
|
||||
seriesList: AsyncUtility.getDefaultEntity("sonarrSeriesId"),
|
||||
wantedEpisodesList: AsyncUtility.getDefaultEntity("sonarrEpisodeId"),
|
||||
episodeList: AsyncUtility.getDefaultList("sonarrEpisodeId"),
|
||||
historyList: AsyncUtility.getDefaultItem(),
|
||||
blacklist: AsyncUtility.getDefaultItem(),
|
||||
};
|
||||
|
||||
const reducer = createReducer(defaultSeries, (builder) => {
|
||||
createAsyncEntityReducer(builder, (s) => s.seriesList, {
|
||||
range: seriesUpdateByRange,
|
||||
ids: seriesUpdateById,
|
||||
removeIds: seriesRemoveById,
|
||||
all: seriesUpdateAll,
|
||||
});
|
||||
|
||||
builder.addCase(seriesMarkDirtyById, (state, action) => {
|
||||
const series = state.seriesList;
|
||||
const dirtyIds = action.payload.map(String);
|
||||
|
||||
AsyncReducer.markDirty(series, dirtyIds);
|
||||
|
||||
// Update episode list
|
||||
const episodes = state.episodeList;
|
||||
const dirtyIdsSet = new Set(dirtyIds);
|
||||
const dirtyEpisodeIds = episodes.content
|
||||
.filter((v) => dirtyIdsSet.has(v.sonarrSeriesId.toString()))
|
||||
.map((v) => String(v.sonarrEpisodeId));
|
||||
|
||||
AsyncReducer.markDirty(episodes, dirtyEpisodeIds);
|
||||
});
|
||||
|
||||
createAsyncEntityReducer(builder, (s) => s.wantedEpisodesList, {
|
||||
range: seriesUpdateWantedByRange,
|
||||
ids: seriesUpdateWantedById,
|
||||
removeIds: seriesRemoveWantedById,
|
||||
dirty: seriesMarkWantedDirtyById,
|
||||
});
|
||||
|
||||
createAsyncItemReducer(builder, (s) => s.historyList, {
|
||||
all: episodesUpdateHistory,
|
||||
dirty: episodesMarkHistoryDirty,
|
||||
});
|
||||
|
||||
createAsyncItemReducer(builder, (s) => s.blacklist, {
|
||||
all: episodesUpdateBlacklist,
|
||||
dirty: episodesMarkBlacklistDirty,
|
||||
});
|
||||
|
||||
createAsyncListReducer(builder, (s) => s.episodeList, {
|
||||
ids: episodeUpdateBySeriesId,
|
||||
});
|
||||
|
||||
createAsyncListReducer(builder, (s) => s.episodeList, {
|
||||
ids: episodeUpdateById,
|
||||
removeIds: episodesRemoveById,
|
||||
dirty: episodesMarkDirtyById,
|
||||
});
|
||||
});
|
||||
|
||||
export default reducer;
|
||||
|
@ -1,121 +1,74 @@
|
||||
import { handleActions } from "redux-actions";
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import {
|
||||
SYSTEM_UPDATE_HEALTH,
|
||||
SYSTEM_UPDATE_LANGUAGES_LIST,
|
||||
SYSTEM_UPDATE_LANGUAGES_PROFILE_LIST,
|
||||
SYSTEM_UPDATE_LOGS,
|
||||
SYSTEM_UPDATE_PROVIDERS,
|
||||
SYSTEM_UPDATE_RELEASES,
|
||||
SYSTEM_UPDATE_SETTINGS,
|
||||
SYSTEM_UPDATE_STATUS,
|
||||
SYSTEM_UPDATE_TASKS,
|
||||
} from "../constants";
|
||||
import { updateAsyncState } from "../utils/mapper";
|
||||
providerUpdateList,
|
||||
systemMarkTasksDirty,
|
||||
systemUpdateHealth,
|
||||
systemUpdateLanguages,
|
||||
systemUpdateLanguagesProfiles,
|
||||
systemUpdateLogs,
|
||||
systemUpdateReleases,
|
||||
systemUpdateSettings,
|
||||
systemUpdateStatus,
|
||||
systemUpdateTasks,
|
||||
} from "../actions";
|
||||
import { AsyncUtility } from "../utils/async";
|
||||
import { createAsyncItemReducer } from "../utils/factory";
|
||||
|
||||
const reducer = handleActions<ReduxStore.System, any>(
|
||||
{
|
||||
[SYSTEM_UPDATE_LANGUAGES_LIST]: (state, action) => {
|
||||
const languages = updateAsyncState<Array<ApiLanguage>>(action, []);
|
||||
const enabledLanguage: AsyncState<ApiLanguage[]> = {
|
||||
...languages,
|
||||
data: languages.data.filter((v) => v.enabled),
|
||||
};
|
||||
const newState = {
|
||||
...state,
|
||||
languages,
|
||||
enabledLanguage,
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
[SYSTEM_UPDATE_LANGUAGES_PROFILE_LIST]: (state, action) => {
|
||||
const newState = {
|
||||
...state,
|
||||
languagesProfiles: updateAsyncState<Array<Profile.Languages>>(
|
||||
action,
|
||||
[]
|
||||
),
|
||||
};
|
||||
return newState;
|
||||
},
|
||||
[SYSTEM_UPDATE_STATUS]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
status: updateAsyncState<System.Status | undefined>(
|
||||
action,
|
||||
state.status.data
|
||||
),
|
||||
};
|
||||
},
|
||||
[SYSTEM_UPDATE_HEALTH]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
health: updateAsyncState(action, state.health.data),
|
||||
};
|
||||
},
|
||||
[SYSTEM_UPDATE_TASKS]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
tasks: updateAsyncState<Array<System.Task>>(action, state.tasks.data),
|
||||
};
|
||||
},
|
||||
[SYSTEM_UPDATE_PROVIDERS]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
providers: updateAsyncState(action, state.providers.data),
|
||||
};
|
||||
},
|
||||
[SYSTEM_UPDATE_LOGS]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
logs: updateAsyncState(action, state.logs.data),
|
||||
};
|
||||
},
|
||||
[SYSTEM_UPDATE_RELEASES]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
releases: updateAsyncState(action, state.releases.data),
|
||||
};
|
||||
},
|
||||
[SYSTEM_UPDATE_SETTINGS]: (state, action) => {
|
||||
return {
|
||||
...state,
|
||||
settings: updateAsyncState(action, state.settings.data),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
languages: { updating: true, data: [] },
|
||||
enabledLanguage: { updating: true, data: [] },
|
||||
languagesProfiles: { updating: true, data: [] },
|
||||
status: {
|
||||
updating: true,
|
||||
data: undefined,
|
||||
},
|
||||
health: {
|
||||
updating: true,
|
||||
data: [],
|
||||
},
|
||||
tasks: {
|
||||
updating: true,
|
||||
data: [],
|
||||
},
|
||||
providers: {
|
||||
updating: true,
|
||||
data: [],
|
||||
},
|
||||
logs: {
|
||||
updating: true,
|
||||
data: [],
|
||||
},
|
||||
releases: {
|
||||
updating: true,
|
||||
data: [],
|
||||
},
|
||||
settings: {
|
||||
updating: true,
|
||||
data: undefined,
|
||||
},
|
||||
interface System {
|
||||
languages: Async.Item<Language.Server[]>;
|
||||
languagesProfiles: Async.Item<Language.Profile[]>;
|
||||
status: Async.Item<System.Status>;
|
||||
health: Async.Item<System.Health[]>;
|
||||
tasks: Async.Item<System.Task[]>;
|
||||
providers: Async.Item<System.Provider[]>;
|
||||
logs: Async.Item<System.Log[]>;
|
||||
releases: Async.Item<ReleaseInfo[]>;
|
||||
settings: Async.Item<Settings>;
|
||||
}
|
||||
);
|
||||
|
||||
const defaultSystem: System = {
|
||||
languages: AsyncUtility.getDefaultItem(),
|
||||
languagesProfiles: AsyncUtility.getDefaultItem(),
|
||||
status: AsyncUtility.getDefaultItem(),
|
||||
health: AsyncUtility.getDefaultItem(),
|
||||
tasks: AsyncUtility.getDefaultItem(),
|
||||
providers: AsyncUtility.getDefaultItem(),
|
||||
logs: AsyncUtility.getDefaultItem(),
|
||||
releases: AsyncUtility.getDefaultItem(),
|
||||
settings: AsyncUtility.getDefaultItem(),
|
||||
};
|
||||
|
||||
const reducer = createReducer(defaultSystem, (builder) => {
|
||||
createAsyncItemReducer(builder, (s) => s.languages, {
|
||||
all: systemUpdateLanguages,
|
||||
});
|
||||
|
||||
createAsyncItemReducer(builder, (s) => s.languagesProfiles, {
|
||||
all: systemUpdateLanguagesProfiles,
|
||||
});
|
||||
createAsyncItemReducer(builder, (s) => s.status, { all: systemUpdateStatus });
|
||||
createAsyncItemReducer(builder, (s) => s.settings, {
|
||||
all: systemUpdateSettings,
|
||||
});
|
||||
createAsyncItemReducer(builder, (s) => s.releases, {
|
||||
all: systemUpdateReleases,
|
||||
});
|
||||
createAsyncItemReducer(builder, (s) => s.logs, {
|
||||
all: systemUpdateLogs,
|
||||
});
|
||||
|
||||
createAsyncItemReducer(builder, (s) => s.health, {
|
||||
all: systemUpdateHealth,
|
||||
});
|
||||
|
||||
createAsyncItemReducer(builder, (s) => s.tasks, {
|
||||
all: systemUpdateTasks,
|
||||
dirty: systemMarkTasksDirty,
|
||||
});
|
||||
|
||||
createAsyncItemReducer(builder, (s) => s.providers, {
|
||||
all: providerUpdateList,
|
||||
});
|
||||
});
|
||||
|
||||
export default reducer;
|
||||
|
@ -1,62 +0,0 @@
|
||||
interface ReduxStore {
|
||||
system: ReduxStore.System;
|
||||
series: ReduxStore.Series;
|
||||
movie: ReduxStore.Movie;
|
||||
site: ReduxStore.Site;
|
||||
}
|
||||
|
||||
namespace ReduxStore {
|
||||
interface Notification {
|
||||
type: "error" | "warning" | "info";
|
||||
id: string;
|
||||
message: string;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
interface Progress {
|
||||
id: string;
|
||||
header: string;
|
||||
name: string;
|
||||
value: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface Site {
|
||||
// Initialization state or error message
|
||||
initialized: boolean | string;
|
||||
auth: boolean;
|
||||
progress: Progress[];
|
||||
notifications: Notification[];
|
||||
sidebar: string;
|
||||
badges: Badge;
|
||||
offline: boolean;
|
||||
}
|
||||
|
||||
interface System {
|
||||
languages: AsyncState<Array<Language>>;
|
||||
enabledLanguage: AsyncState<Array<Language>>;
|
||||
languagesProfiles: AsyncState<Array<Profile.Languages>>;
|
||||
status: AsyncState<System.Status | undefined>;
|
||||
health: AsyncState<Array<System.Health>>;
|
||||
tasks: AsyncState<Array<System.Task>>;
|
||||
providers: AsyncState<Array<System.Provider>>;
|
||||
logs: AsyncState<Array<System.Log>>;
|
||||
releases: AsyncState<Array<ReleaseInfo>>;
|
||||
settings: AsyncState<Settings | undefined>;
|
||||
}
|
||||
|
||||
interface Series {
|
||||
seriesList: AsyncOrderState<Item.Series>;
|
||||
wantedEpisodesList: AsyncOrderState<Wanted.Episode>;
|
||||
episodeList: AsyncState<Item.Episode[]>;
|
||||
historyList: AsyncState<Array<History.Episode>>;
|
||||
blacklist: AsyncState<Array<Blacklist.Episode>>;
|
||||
}
|
||||
|
||||
interface Movie {
|
||||
movieList: AsyncOrderState<Item.Movie>;
|
||||
wantedMovieList: AsyncOrderState<Wanted.Movie>;
|
||||
historyList: AsyncState<Array<History.Movie>>;
|
||||
blacklist: AsyncState<Array<Blacklist.Movie>>;
|
||||
}
|
||||
}
|
@ -1,17 +1,15 @@
|
||||
import { applyMiddleware, createStore } from "redux";
|
||||
import logger from "redux-logger";
|
||||
import promise from "redux-promise";
|
||||
import trunk from "redux-thunk";
|
||||
import rootReducer from "../reducers";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import apis from "../../apis";
|
||||
import reducer from "../reducers";
|
||||
|
||||
const plugins = [promise, trunk];
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
});
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === "development" &&
|
||||
process.env["REACT_APP_LOG_REDUX_EVENT"] !== "false"
|
||||
) {
|
||||
plugins.push(logger);
|
||||
}
|
||||
// FIXME
|
||||
apis.dispatch = store.dispatch;
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
const store = createStore(rootReducer, applyMiddleware(...plugins));
|
||||
export default store;
|
||||
|
@ -0,0 +1,31 @@
|
||||
import { AsyncUtility } from "../utils/async";
|
||||
|
||||
export interface TestType {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Reducer {
|
||||
item: Async.Item<TestType>;
|
||||
list: Async.List<TestType>;
|
||||
entities: Async.Entity<TestType>;
|
||||
}
|
||||
|
||||
export const defaultState: Reducer = {
|
||||
item: AsyncUtility.getDefaultItem(),
|
||||
list: AsyncUtility.getDefaultList("id"),
|
||||
entities: AsyncUtility.getDefaultEntity("id"),
|
||||
};
|
||||
|
||||
export const defaultItem: TestType = { id: 0, name: "test" };
|
||||
|
||||
export const defaultList: TestType[] = [
|
||||
{ id: 0, name: "test" },
|
||||
{ id: 1, name: "test_1" },
|
||||
{ id: 2, name: "test_2" },
|
||||
{ id: 3, name: "test_3" },
|
||||
{ id: 4, name: "test_4" },
|
||||
{ id: 5, name: "test_5" },
|
||||
{ id: 6, name: "test_6" },
|
||||
{ id: 7, name: "test_6" },
|
||||
];
|
@ -1,22 +0,0 @@
|
||||
import { Dispatch } from "redux";
|
||||
import { Action } from "redux-actions";
|
||||
|
||||
interface AsyncPayload<Payload> {
|
||||
loading: boolean;
|
||||
item?: Payload | Error;
|
||||
parameters: any[];
|
||||
}
|
||||
|
||||
type AvailableType<T> = Action<T> | ActionDispatcher<T>;
|
||||
|
||||
type AsyncAction<Payload> = Action<AsyncPayload<Payload>>;
|
||||
type ActionDispatcher<T = any> = (dispatch: Dispatch<Action<T>>) => void;
|
||||
type AsyncActionDispatcher<T> = (
|
||||
dispatch: Dispatch<AsyncAction<T>>
|
||||
) => Promise<void>;
|
||||
|
||||
type PromiseCreator = (...args: any[]) => Promise<any>;
|
||||
type AvailableCreator = (...args: any[]) => AvailableType<any>[];
|
||||
type AsyncActionCreator = (...args: any[]) => AsyncActionDispatcher<any>[];
|
||||
|
||||
type ActionCallback = () => Action<any> | void;
|
@ -0,0 +1,32 @@
|
||||
import {} from "jest";
|
||||
import { AsyncUtility } from "../async";
|
||||
|
||||
interface AsyncTest {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
it("Item Init", () => {
|
||||
const item = AsyncUtility.getDefaultItem<AsyncTest>();
|
||||
expect(item.state).toEqual("uninitialized");
|
||||
expect(item.error).toBeNull();
|
||||
expect(item.content).toBeNull();
|
||||
});
|
||||
|
||||
it("List Init", () => {
|
||||
const list = AsyncUtility.getDefaultList<AsyncTest>("id");
|
||||
expect(list.state).toEqual("uninitialized");
|
||||
expect(list.dirtyEntities).toHaveLength(0);
|
||||
expect(list.error).toBeNull();
|
||||
expect(list.content).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("Entity Init", () => {
|
||||
const entity = AsyncUtility.getDefaultEntity<AsyncTest>("id");
|
||||
expect(entity.state).toEqual("uninitialized");
|
||||
expect(entity.dirtyEntities).toHaveLength(0);
|
||||
expect(entity.error).toBeNull();
|
||||
expect(entity.content.ids).toHaveLength(0);
|
||||
expect(entity.content.keyName).toBe("id");
|
||||
expect(entity.content.entities).toMatchObject({});
|
||||
});
|
@ -0,0 +1,71 @@
|
||||
import { Draft } from "@reduxjs/toolkit";
|
||||
import { difference, uniq } from "lodash";
|
||||
|
||||
export namespace AsyncUtility {
|
||||
export function getDefaultItem<T>(): Async.Item<T> {
|
||||
return {
|
||||
state: "uninitialized",
|
||||
content: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultList<T>(key: keyof T): Async.List<T> {
|
||||
return {
|
||||
state: "uninitialized",
|
||||
keyName: key,
|
||||
dirtyEntities: [],
|
||||
content: [],
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultEntity<T>(key: keyof T): Async.Entity<T> {
|
||||
return {
|
||||
state: "uninitialized",
|
||||
dirtyEntities: [],
|
||||
content: {
|
||||
keyName: key,
|
||||
ids: [],
|
||||
entities: {},
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace AsyncReducer {
|
||||
type DirtyType = Draft<Async.Entity<any>> | Draft<Async.List<any>>;
|
||||
export function markDirty<T extends DirtyType>(
|
||||
entity: T,
|
||||
dirtyIds: string[]
|
||||
) {
|
||||
if (entity.state !== "uninitialized" && entity.state !== "loading") {
|
||||
entity.state = "dirty";
|
||||
entity.dirtyEntities.push(...dirtyIds);
|
||||
entity.dirtyEntities = uniq(entity.dirtyEntities);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateDirty<T extends DirtyType>(
|
||||
entity: T,
|
||||
updatedIds: string[]
|
||||
) {
|
||||
entity.dirtyEntities = difference(entity.dirtyEntities, updatedIds);
|
||||
if (entity.dirtyEntities.length > 0) {
|
||||
entity.state = "dirty";
|
||||
} else {
|
||||
entity.state = "succeeded";
|
||||
}
|
||||
}
|
||||
|
||||
export function removeDirty<T extends DirtyType>(
|
||||
entity: T,
|
||||
removedIds: string[]
|
||||
) {
|
||||
entity.dirtyEntities = difference(entity.dirtyEntities, removedIds);
|
||||
if (entity.dirtyEntities.length === 0 && entity.state === "dirty") {
|
||||
entity.state = "succeeded";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,303 @@
|
||||
import {
|
||||
ActionCreatorWithoutPayload,
|
||||
ActionCreatorWithPayload,
|
||||
ActionReducerMapBuilder,
|
||||
AsyncThunk,
|
||||
Draft,
|
||||
} from "@reduxjs/toolkit";
|
||||
import {
|
||||
difference,
|
||||
findIndex,
|
||||
isNull,
|
||||
isString,
|
||||
omit,
|
||||
pullAll,
|
||||
pullAllWith,
|
||||
} from "lodash";
|
||||
import { conditionalLog } from "../../utilites/logger";
|
||||
import { AsyncReducer } from "./async";
|
||||
|
||||
interface ActionParam<T, ID = null> {
|
||||
range?: AsyncThunk<T, Parameter.Range, {}>;
|
||||
all?: AsyncThunk<T, void, {}>;
|
||||
ids?: AsyncThunk<T, ID[], {}>;
|
||||
removeIds?: ActionCreatorWithPayload<ID[]>;
|
||||
dirty?: ID extends null
|
||||
? ActionCreatorWithoutPayload
|
||||
: ActionCreatorWithPayload<ID[]>;
|
||||
}
|
||||
|
||||
export function createAsyncItemReducer<S, T>(
|
||||
builder: ActionReducerMapBuilder<S>,
|
||||
getItem: (state: Draft<S>) => Draft<Async.Item<T>>,
|
||||
actions: Pick<ActionParam<T>, "all" | "dirty">
|
||||
) {
|
||||
const { all, dirty } = actions;
|
||||
|
||||
all &&
|
||||
builder
|
||||
.addCase(all.pending, (state) => {
|
||||
const item = getItem(state);
|
||||
item.state = "loading";
|
||||
item.error = null;
|
||||
})
|
||||
.addCase(all.fulfilled, (state, action) => {
|
||||
const item = getItem(state);
|
||||
item.state = "succeeded";
|
||||
item.content = action.payload as Draft<T>;
|
||||
})
|
||||
.addCase(all.rejected, (state, action) => {
|
||||
const item = getItem(state);
|
||||
item.state = "failed";
|
||||
item.error = action.error.message ?? null;
|
||||
});
|
||||
|
||||
dirty &&
|
||||
builder.addCase(dirty, (state) => {
|
||||
const item = getItem(state);
|
||||
if (item.state !== "uninitialized") {
|
||||
item.state = "dirty";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function createAsyncListReducer<S, T, ID extends Async.IdType>(
|
||||
builder: ActionReducerMapBuilder<S>,
|
||||
getList: (state: Draft<S>) => Draft<Async.List<T>>,
|
||||
actions: ActionParam<T[], ID>
|
||||
) {
|
||||
const { ids, removeIds, all, dirty } = actions;
|
||||
ids &&
|
||||
builder
|
||||
.addCase(ids.pending, (state) => {
|
||||
const list = getList(state);
|
||||
list.state = "loading";
|
||||
list.error = null;
|
||||
})
|
||||
.addCase(ids.fulfilled, (state, action) => {
|
||||
const list = getList(state);
|
||||
|
||||
const {
|
||||
meta: { arg },
|
||||
} = action;
|
||||
|
||||
const keyName = list.keyName as keyof T;
|
||||
|
||||
action.payload.forEach((v) => {
|
||||
const idx = findIndex(list.content, [keyName, v[keyName]]);
|
||||
if (idx !== -1) {
|
||||
list.content.splice(idx, 1, v as Draft<T>);
|
||||
} else {
|
||||
list.content.unshift(v as Draft<T>);
|
||||
}
|
||||
});
|
||||
|
||||
AsyncReducer.updateDirty(list, arg.map(String));
|
||||
})
|
||||
.addCase(ids.rejected, (state, action) => {
|
||||
const list = getList(state);
|
||||
list.state = "failed";
|
||||
list.error = action.error.message ?? null;
|
||||
});
|
||||
|
||||
removeIds &&
|
||||
builder.addCase(removeIds, (state, action) => {
|
||||
const list = getList(state);
|
||||
const keyName = list.keyName as keyof T;
|
||||
|
||||
const removeIds = action.payload.map(String);
|
||||
|
||||
pullAllWith(list.content, removeIds, (lhs, rhs) => {
|
||||
return String((lhs as T)[keyName]) === rhs;
|
||||
});
|
||||
|
||||
AsyncReducer.removeDirty(list, removeIds);
|
||||
});
|
||||
|
||||
all &&
|
||||
builder
|
||||
.addCase(all.pending, (state) => {
|
||||
const list = getList(state);
|
||||
list.state = "loading";
|
||||
list.error = null;
|
||||
})
|
||||
.addCase(all.fulfilled, (state, action) => {
|
||||
const list = getList(state);
|
||||
list.state = "succeeded";
|
||||
list.content = action.payload as Draft<T[]>;
|
||||
list.dirtyEntities = [];
|
||||
})
|
||||
.addCase(all.rejected, (state, action) => {
|
||||
const list = getList(state);
|
||||
list.state = "failed";
|
||||
list.error = action.error.message ?? null;
|
||||
});
|
||||
|
||||
dirty &&
|
||||
builder.addCase(dirty, (state, action) => {
|
||||
const list = getList(state);
|
||||
AsyncReducer.markDirty(list, action.payload.map(String));
|
||||
});
|
||||
}
|
||||
|
||||
export function createAsyncEntityReducer<S, T, ID extends Async.IdType>(
|
||||
builder: ActionReducerMapBuilder<S>,
|
||||
getEntity: (state: Draft<S>) => Draft<Async.Entity<T>>,
|
||||
actions: ActionParam<AsyncDataWrapper<T>, ID>
|
||||
) {
|
||||
const { all, removeIds, ids, range, dirty } = actions;
|
||||
|
||||
const checkSizeUpdate = (entity: Draft<Async.Entity<T>>, newSize: number) => {
|
||||
if (entity.content.ids.length !== newSize) {
|
||||
// Reset Entity State
|
||||
entity.dirtyEntities = [];
|
||||
entity.content.ids = Array(newSize).fill(null);
|
||||
entity.content.entities = {};
|
||||
}
|
||||
};
|
||||
|
||||
range &&
|
||||
builder
|
||||
.addCase(range.pending, (state) => {
|
||||
const entity = getEntity(state);
|
||||
entity.state = "loading";
|
||||
entity.error = null;
|
||||
})
|
||||
.addCase(range.fulfilled, (state, action) => {
|
||||
const entity = getEntity(state);
|
||||
|
||||
const {
|
||||
meta: {
|
||||
arg: { start, length },
|
||||
},
|
||||
payload: { data, total },
|
||||
} = action;
|
||||
|
||||
const keyName = entity.content.keyName as keyof T;
|
||||
|
||||
checkSizeUpdate(entity, total);
|
||||
|
||||
const idsToUpdate = data.map((v) => String(v[keyName]));
|
||||
|
||||
entity.content.ids.splice(start, length, ...idsToUpdate);
|
||||
data.forEach((v) => {
|
||||
const key = String(v[keyName]);
|
||||
entity.content.entities[key] = v as Draft<T>;
|
||||
});
|
||||
|
||||
AsyncReducer.updateDirty(entity, idsToUpdate);
|
||||
})
|
||||
.addCase(range.rejected, (state, action) => {
|
||||
const entity = getEntity(state);
|
||||
entity.state = "failed";
|
||||
entity.error = action.error.message ?? null;
|
||||
});
|
||||
|
||||
ids &&
|
||||
builder
|
||||
.addCase(ids.pending, (state) => {
|
||||
const entity = getEntity(state);
|
||||
entity.state = "loading";
|
||||
entity.error = null;
|
||||
})
|
||||
.addCase(ids.fulfilled, (state, action) => {
|
||||
const entity = getEntity(state);
|
||||
|
||||
const {
|
||||
meta: { arg },
|
||||
payload: { data, total },
|
||||
} = action;
|
||||
|
||||
const keyName = entity.content.keyName as keyof T;
|
||||
|
||||
checkSizeUpdate(entity, total);
|
||||
|
||||
const idsToAdd = data.map((v) => String(v[keyName]));
|
||||
|
||||
// For new ids, remove null from list and add them
|
||||
const newIds = difference(
|
||||
idsToAdd,
|
||||
entity.content.ids.filter(isString)
|
||||
);
|
||||
const newSize = entity.content.ids.unshift(...newIds);
|
||||
Array(newSize - total)
|
||||
.fill(undefined)
|
||||
.forEach(() => {
|
||||
const idx = entity.content.ids.findIndex(isNull);
|
||||
conditionalLog(idx === -1, "Error when deleting ids from entity");
|
||||
entity.content.ids.splice(idx, 1);
|
||||
});
|
||||
|
||||
data.forEach((v) => {
|
||||
const key = String(v[keyName]);
|
||||
entity.content.entities[key] = v as Draft<T>;
|
||||
});
|
||||
|
||||
AsyncReducer.updateDirty(entity, arg.map(String));
|
||||
})
|
||||
.addCase(ids.rejected, (state, action) => {
|
||||
const entity = getEntity(state);
|
||||
entity.state = "failed";
|
||||
entity.error = action.error.message ?? null;
|
||||
});
|
||||
|
||||
removeIds &&
|
||||
builder.addCase(removeIds, (state, action) => {
|
||||
const entity = getEntity(state);
|
||||
conditionalLog(
|
||||
entity.state === "loading",
|
||||
"Try to delete async entity when it's now loading"
|
||||
);
|
||||
|
||||
const idsToDelete = action.payload.map(String);
|
||||
pullAll(entity.content.ids, idsToDelete);
|
||||
AsyncReducer.removeDirty(entity, idsToDelete);
|
||||
omit(entity.content.entities, idsToDelete);
|
||||
});
|
||||
|
||||
all &&
|
||||
builder
|
||||
.addCase(all.pending, (state) => {
|
||||
const entity = getEntity(state);
|
||||
entity.state = "loading";
|
||||
entity.error = null;
|
||||
})
|
||||
.addCase(all.fulfilled, (state, action) => {
|
||||
const entity = getEntity(state);
|
||||
|
||||
const {
|
||||
payload: { data, total },
|
||||
} = action;
|
||||
|
||||
conditionalLog(
|
||||
data.length !== total,
|
||||
"Length of data is mismatch with total length"
|
||||
);
|
||||
|
||||
const keyName = entity.content.keyName as keyof T;
|
||||
|
||||
entity.state = "succeeded";
|
||||
entity.dirtyEntities = [];
|
||||
entity.content.ids = data.map((v) => String(v[keyName]));
|
||||
entity.content.entities = data.reduce<
|
||||
Draft<{
|
||||
[id: string]: T;
|
||||
}>
|
||||
>((prev, curr) => {
|
||||
const id = String(curr[keyName]);
|
||||
prev[id] = curr as Draft<T>;
|
||||
return prev;
|
||||
}, {});
|
||||
})
|
||||
.addCase(all.rejected, (state, action) => {
|
||||
const entity = getEntity(state);
|
||||
entity.state = "failed";
|
||||
entity.error = action.error.message ?? null;
|
||||
});
|
||||
|
||||
dirty &&
|
||||
builder.addCase(dirty, (state, action) => {
|
||||
const entity = getEntity(state);
|
||||
AsyncReducer.markDirty(entity, action.payload.map(String));
|
||||
});
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
export function defaultAOS(): AsyncOrderState<any> {
|
||||
return {
|
||||
updating: true,
|
||||
data: {
|
||||
items: [],
|
||||
order: [],
|
||||
dirty: false,
|
||||
},
|
||||
};
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
import { difference, has, isArray, isNull, isNumber, uniqBy } from "lodash";
|
||||
import { Action } from "redux-actions";
|
||||
import { conditionalLog } from "../../utilites/logger";
|
||||
import { AsyncAction } from "../types";
|
||||
|
||||
export function updateAsyncState<Payload>(
|
||||
action: AsyncAction<Payload>,
|
||||
defVal: Readonly<Payload>
|
||||
): AsyncState<Payload> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
updating: true,
|
||||
data: defVal,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
data: defVal,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
updating: false,
|
||||
error: undefined,
|
||||
data: action.payload.item as Payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function updateOrderIdState<T extends LooseObject>(
|
||||
action: AsyncAction<AsyncDataWrapper<T>>,
|
||||
state: AsyncOrderState<T>,
|
||||
id: ItemIdType<T>
|
||||
): AsyncOrderState<T> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
data: {
|
||||
...state.data,
|
||||
dirty: true,
|
||||
},
|
||||
updating: true,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
data: {
|
||||
...state.data,
|
||||
dirty: true,
|
||||
},
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
};
|
||||
} else {
|
||||
const { data, total } = action.payload.item as AsyncDataWrapper<T>;
|
||||
const { parameters } = action.payload;
|
||||
const [start, length] = parameters;
|
||||
|
||||
// Convert item list to object
|
||||
const newItems = data.reduce<IdState<T>>(
|
||||
(prev, curr) => {
|
||||
const tid = curr[id];
|
||||
prev[tid] = curr;
|
||||
return prev;
|
||||
},
|
||||
{ ...state.data.items }
|
||||
);
|
||||
|
||||
let newOrder = [...state.data.order];
|
||||
|
||||
const countDist = total - newOrder.length;
|
||||
if (countDist > 0) {
|
||||
newOrder = Array(countDist).fill(null).concat(newOrder);
|
||||
} else if (countDist < 0) {
|
||||
// Completely drop old data if list has shrinked
|
||||
newOrder = Array(total).fill(null);
|
||||
}
|
||||
|
||||
const idList = newOrder.filter(isNumber);
|
||||
|
||||
const dataOrder: number[] = data.map((v) => v[id]);
|
||||
|
||||
if (typeof start === "number" && typeof length === "number") {
|
||||
newOrder.splice(start, length, ...dataOrder);
|
||||
} else if (isArray(start)) {
|
||||
// Find the null values and delete them, insert new values to the front of array
|
||||
const addition = difference(dataOrder, idList);
|
||||
let addCount = addition.length;
|
||||
newOrder.unshift(...addition);
|
||||
|
||||
newOrder = newOrder.flatMap((v) => {
|
||||
if (isNull(v) && addCount > 0) {
|
||||
--addCount;
|
||||
return [];
|
||||
} else {
|
||||
return [v];
|
||||
}
|
||||
}, []);
|
||||
|
||||
conditionalLog(
|
||||
addCount !== 0,
|
||||
"Error when replacing item in OrderIdState"
|
||||
);
|
||||
} else if (parameters.length === 0) {
|
||||
// TODO: Delete me -> Full Update
|
||||
newOrder = dataOrder;
|
||||
}
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
data: {
|
||||
dirty: true,
|
||||
items: newItems,
|
||||
order: newOrder,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteOrderListItemBy<T extends LooseObject>(
|
||||
action: Action<number[]>,
|
||||
state: AsyncOrderState<T>
|
||||
): AsyncOrderState<T> {
|
||||
const ids = action.payload;
|
||||
const { items, order } = state.data;
|
||||
const newItems = { ...items };
|
||||
ids.forEach((v) => {
|
||||
if (has(newItems, v)) {
|
||||
delete newItems[v];
|
||||
}
|
||||
});
|
||||
const newOrder = difference(order, ids);
|
||||
return {
|
||||
...state,
|
||||
data: {
|
||||
dirty: true,
|
||||
items: newItems,
|
||||
order: newOrder,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteAsyncListItemBy<T extends LooseObject>(
|
||||
action: Action<number[]>,
|
||||
state: AsyncState<T[]>,
|
||||
match: ItemIdType<T>
|
||||
): AsyncState<T[]> {
|
||||
const ids = new Set(action.payload);
|
||||
const data = [...state.data].filter((v) => !ids.has(v[match]));
|
||||
return {
|
||||
...state,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateAsyncList<T, ID extends keyof T>(
|
||||
action: AsyncAction<T[]>,
|
||||
state: AsyncState<T[]>,
|
||||
match: ID
|
||||
): AsyncState<T[]> {
|
||||
if (action.payload.loading) {
|
||||
return {
|
||||
...state,
|
||||
updating: true,
|
||||
};
|
||||
} else if (action.error !== undefined) {
|
||||
return {
|
||||
...state,
|
||||
updating: false,
|
||||
error: action.payload.item as Error,
|
||||
};
|
||||
} else {
|
||||
const olds = state.data as T[];
|
||||
const news = action.payload.item as T[];
|
||||
|
||||
const result = uniqBy([...news, ...olds], match);
|
||||
|
||||
return {
|
||||
updating: false,
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
declare namespace Async {
|
||||
type State = "loading" | "succeeded" | "failed" | "dirty" | "uninitialized";
|
||||
|
||||
type IdType = number | string;
|
||||
|
||||
type Base<T> = {
|
||||
state: State;
|
||||
content: T;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
type List<T> = Base<T[]> & {
|
||||
keyName: keyof T;
|
||||
dirtyEntities: string[];
|
||||
};
|
||||
|
||||
type Item<T> = Base<T | null>;
|
||||
|
||||
type Entity<T> = Base<EntityStruct<T>> & {
|
||||
dirtyEntities: string[];
|
||||
};
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
declare namespace Server {
|
||||
interface Notification {
|
||||
type: "error" | "warning" | "info";
|
||||
id: string;
|
||||
message: string;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
interface Progress {
|
||||
id: string;
|
||||
header: string;
|
||||
name: string;
|
||||
value: number;
|
||||
count: number;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import {} from "jest";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "../App";
|
||||
|
||||
it("renders", () => {
|
||||
const div = document.createElement("div");
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
@ -1,24 +1,28 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useDidMount } from "rooks";
|
||||
|
||||
type RequestReturn<F extends () => Promise<any>> = PromiseType<ReturnType<F>>;
|
||||
type Request = (...args: any[]) => Promise<any>;
|
||||
type Return<T extends Request> = PromiseType<ReturnType<T>>;
|
||||
|
||||
export function useAsyncRequest<F extends () => Promise<any>>(
|
||||
export function useAsyncRequest<F extends Request>(
|
||||
request: F,
|
||||
defaultData: RequestReturn<F>
|
||||
): [AsyncState<RequestReturn<F>>, () => void] {
|
||||
const [state, setState] = useState<AsyncState<RequestReturn<F>>>({
|
||||
updating: true,
|
||||
data: defaultData,
|
||||
initial: Return<F>
|
||||
): [Async.Base<Return<F>>, (...args: Parameters<F>) => void] {
|
||||
const [state, setState] = useState<Async.Base<Return<F>>>({
|
||||
state: "uninitialized",
|
||||
content: initial,
|
||||
error: null,
|
||||
});
|
||||
const update = useCallback(() => {
|
||||
setState((s) => ({ ...s, updating: true }));
|
||||
request()
|
||||
.then((res) => setState({ updating: false, data: res }))
|
||||
.catch((err) => setState((s) => ({ ...s, updating: false, err })));
|
||||
}, [request]);
|
||||
|
||||
useDidMount(update);
|
||||
const update = useCallback(
|
||||
(...args: Parameters<F>) => {
|
||||
setState((s) => ({ ...s, state: "loading" }));
|
||||
request(...args)
|
||||
.then((res) =>
|
||||
setState({ state: "succeeded", content: res, error: null })
|
||||
)
|
||||
.catch((error) => setState((s) => ({ ...s, state: "failed", error })));
|
||||
},
|
||||
[request]
|
||||
);
|
||||
|
||||
return [state, update];
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue