parent
6b82a734e2
commit
d8d2300980
@ -1,17 +0,0 @@
|
||||
import BGT from "./";
|
||||
|
||||
export function useIsAnyTaskRunning() {
|
||||
return BGT.isRunning();
|
||||
}
|
||||
|
||||
export function useIsAnyTaskRunningWithId(ids: number[]) {
|
||||
return BGT.hasId(ids);
|
||||
}
|
||||
|
||||
export function useIsGroupTaskRunning(groupName: string) {
|
||||
return BGT.has(groupName);
|
||||
}
|
||||
|
||||
export function useIsGroupTaskRunningWithId(groupName: string, id: number) {
|
||||
return BGT.find(groupName, id);
|
||||
}
|
@ -1,406 +0,0 @@
|
||||
import {
|
||||
configureStore,
|
||||
createAction,
|
||||
createAsyncThunk,
|
||||
createReducer,
|
||||
} from "@reduxjs/toolkit";
|
||||
import {} from "jest";
|
||||
import { differenceWith, intersectionWith, isString, uniq } 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 reset = createAction("reset");
|
||||
|
||||
const reducer = createReducer(defaultState, (builder) => {
|
||||
createAsyncEntityReducer(builder, (s) => s.entities, {
|
||||
all: allResolved,
|
||||
range: rangeResolved,
|
||||
ids: idsResolved,
|
||||
dirty,
|
||||
removeIds,
|
||||
reset,
|
||||
});
|
||||
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);
|
||||
expect(entities.didLoaded).toContain(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 reset", async () => {
|
||||
await store.dispatch(allResolved());
|
||||
store.dispatch(reset());
|
||||
use((entities) => {
|
||||
expect(entities).toEqual(defaultState.entities);
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
idsToRemove.map(String).forEach((v) => {
|
||||
expect(entities.didLoaded).not.toContain(v);
|
||||
});
|
||||
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.didLoaded).toContain(id);
|
||||
});
|
||||
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.didLoaded.filter((v) => v === id)).toHaveLength(1);
|
||||
});
|
||||
expect(entities.error).toBeNull();
|
||||
expect(entities.state).toBe("succeeded");
|
||||
});
|
||||
});
|
||||
|
||||
it("entity update by range and ids", async () => {
|
||||
await store.dispatch(rangeResolved({ start: 0, length: 2 }));
|
||||
await store.dispatch(idsResolved([3]));
|
||||
await store.dispatch(rangeResolved({ start: 2, length: 2 }));
|
||||
use((entries) => {
|
||||
const ids = entries.content.ids.filter(isString);
|
||||
const dedupIds = uniq(ids);
|
||||
expect(ids.length).toBe(dedupIds.length);
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,155 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
@ -1,252 +0,0 @@
|
||||
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.didLoaded).toHaveLength(defaultList.length);
|
||||
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.didLoaded).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.didLoaded).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.didLoaded).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,4 +1,38 @@
|
||||
export * from "./movie";
|
||||
export * from "./series";
|
||||
export * from "./site";
|
||||
export * from "./system";
|
||||
import { createAction, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { waitFor } from "../../utilities";
|
||||
|
||||
export const setSiteStatus = createAction<Site.Status>("site/status/update");
|
||||
|
||||
export const setUnauthenticated = createAction("site/unauthenticated");
|
||||
|
||||
export const setOfflineStatus = createAction<boolean>("site/offline/update");
|
||||
|
||||
export const addNotifications = createAction<Server.Notification[]>(
|
||||
"site/notifications/add"
|
||||
);
|
||||
|
||||
export const removeNotification = createAction<string>(
|
||||
"site/notifications/remove"
|
||||
);
|
||||
|
||||
export const siteAddProgress =
|
||||
createAction<Site.Progress[]>("site/progress/add");
|
||||
|
||||
export const siteUpdateProgressCount = createAction<{
|
||||
id: string;
|
||||
count: number;
|
||||
}>("site/progress/update_count");
|
||||
|
||||
export const siteRemoveProgress = createAsyncThunk(
|
||||
"site/progress/remove",
|
||||
async (ids: string[]) => {
|
||||
await waitFor(3 * 1000);
|
||||
return ids;
|
||||
}
|
||||
);
|
||||
|
||||
export const siteUpdateNotifier = createAction<string>(
|
||||
"site/progress/update_notifier"
|
||||
);
|
||||
|
||||
export const setSidebar = createAction<boolean>("site/sidebar/update");
|
||||
|
@ -1,84 +0,0 @@
|
||||
import { createAction, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { MoviesApi } from "../../apis";
|
||||
|
||||
export const movieUpdateByRange = createAsyncThunk(
|
||||
"movies/update/range",
|
||||
async (params: Parameter.Range) => {
|
||||
const response = await MoviesApi.moviesBy(params);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieUpdateById = createAsyncThunk(
|
||||
"movies/update/id",
|
||||
async (ids: number[]) => {
|
||||
const response = await MoviesApi.movies(ids);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieUpdateAll = createAsyncThunk(
|
||||
"movies/update/all",
|
||||
async () => {
|
||||
const response = await MoviesApi.movies();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieRemoveById = createAction<number[]>("movies/remove");
|
||||
|
||||
export const movieMarkDirtyById = createAction<number[]>(
|
||||
"movies/mark_dirty/id"
|
||||
);
|
||||
|
||||
export const movieUpdateWantedById = createAsyncThunk(
|
||||
"movies/wanted/update/id",
|
||||
async (ids: number[]) => {
|
||||
const response = await MoviesApi.wantedBy(ids);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieRemoveWantedById = createAction<number[]>(
|
||||
"movies/wanted/remove/id"
|
||||
);
|
||||
|
||||
export const movieResetWanted = createAction("movies/wanted/reset");
|
||||
|
||||
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 movieUpdateHistoryByRange = createAsyncThunk(
|
||||
"movies/history/update/range",
|
||||
async (params: Parameter.Range) => {
|
||||
const response = await MoviesApi.history(params);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieMarkHistoryDirty = createAction<number[]>(
|
||||
"movies/history/mark_dirty"
|
||||
);
|
||||
|
||||
export const movieResetHistory = createAction("movie/history/reset");
|
||||
|
||||
export const movieUpdateBlacklist = createAsyncThunk(
|
||||
"movies/blacklist/update",
|
||||
async () => {
|
||||
const response = await MoviesApi.blacklist();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const movieMarkBlacklistDirty = createAction(
|
||||
"movies/blacklist/mark_dirty"
|
||||
);
|
@ -1,106 +0,0 @@
|
||||
import { createAction, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { EpisodesApi, SeriesApi } from "../../apis";
|
||||
|
||||
export const seriesUpdateWantedById = createAsyncThunk(
|
||||
"series/wanted/update/id",
|
||||
async (episodeid: number[]) => {
|
||||
const response = await EpisodesApi.wantedBy(episodeid);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const seriesUpdateWantedByRange = createAsyncThunk(
|
||||
"series/wanted/update/range",
|
||||
async (params: Parameter.Range) => {
|
||||
const response = await EpisodesApi.wanted(params);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const seriesRemoveWantedById = createAction<number[]>(
|
||||
"series/wanted/remove/id"
|
||||
);
|
||||
|
||||
export const seriesResetWanted = createAction("series/wanted/reset");
|
||||
|
||||
export const seriesMarkWantedDirtyById = createAction<number[]>(
|
||||
"series/wanted/mark_dirty/episode_id"
|
||||
);
|
||||
|
||||
export const seriesRemoveById = createAction<number[]>("series/remove");
|
||||
|
||||
export const seriesMarkDirtyById = createAction<number[]>(
|
||||
"series/mark_dirty/id"
|
||||
);
|
||||
|
||||
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 episodesMarkDirtyById = createAction<number[]>(
|
||||
"episodes/mark_dirty/id"
|
||||
);
|
||||
|
||||
export const episodeUpdateBySeriesId = createAsyncThunk(
|
||||
"episodes/update/series_id",
|
||||
async (seriesid: number[]) => {
|
||||
const response = await EpisodesApi.bySeriesId(seriesid);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const episodeUpdateById = createAsyncThunk(
|
||||
"episodes/update/episodes_id",
|
||||
async (episodeid: number[]) => {
|
||||
const response = await EpisodesApi.byEpisodeId(episodeid);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const episodesUpdateHistoryByRange = createAsyncThunk(
|
||||
"episodes/history/update/range",
|
||||
async (param: Parameter.Range) => {
|
||||
const response = await EpisodesApi.history(param);
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const episodesMarkHistoryDirty = createAction<number[]>(
|
||||
"episodes/history/update"
|
||||
);
|
||||
|
||||
export const episodesResetHistory = createAction("episodes/history/reset");
|
||||
|
||||
export const episodesUpdateBlacklist = createAsyncThunk(
|
||||
"episodes/blacklist/update",
|
||||
async () => {
|
||||
const response = await EpisodesApi.blacklist();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const episodesMarkBlacklistDirty = createAction(
|
||||
"episodes/blacklist/update"
|
||||
);
|
@ -1,62 +0,0 @@
|
||||
import { createAction, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { BadgesApi } from "../../apis";
|
||||
import { waitFor } from "../../utilities";
|
||||
import { systemUpdateAllSettings } from "./system";
|
||||
|
||||
export const siteBootstrap = createAsyncThunk(
|
||||
"site/bootstrap",
|
||||
(_: undefined, { dispatch }) => {
|
||||
return Promise.all([
|
||||
dispatch(systemUpdateAllSettings()),
|
||||
dispatch(siteUpdateBadges()),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
export const siteUpdateInitialization = createAction<string | true>(
|
||||
"site/initialization/update"
|
||||
);
|
||||
|
||||
export const siteRedirectToAuth = createAction("site/redirect_auth");
|
||||
|
||||
export const siteAddNotifications = createAction<Server.Notification[]>(
|
||||
"site/notifications/add"
|
||||
);
|
||||
|
||||
export const siteRemoveNotifications = createAction<string>(
|
||||
"site/notifications/remove"
|
||||
);
|
||||
|
||||
export const siteAddProgress =
|
||||
createAction<Site.Progress[]>("site/progress/add");
|
||||
|
||||
export const siteUpdateProgressCount = createAction<{
|
||||
id: string;
|
||||
count: number;
|
||||
}>("site/progress/update_count");
|
||||
|
||||
export const siteRemoveProgress = createAsyncThunk(
|
||||
"site/progress/remove",
|
||||
async (ids: string[]) => {
|
||||
await waitFor(3 * 1000);
|
||||
return ids;
|
||||
}
|
||||
);
|
||||
|
||||
export const siteUpdateNotifier = createAction<string>(
|
||||
"site/progress/update_notifier"
|
||||
);
|
||||
|
||||
export const siteChangeSidebarVisibility = createAction<boolean>(
|
||||
"site/sidebar/visibility"
|
||||
);
|
||||
|
||||
export const siteUpdateOffline = createAction<boolean>("site/offline/update");
|
||||
|
||||
export const siteUpdateBadges = createAsyncThunk(
|
||||
"site/badges/update",
|
||||
async () => {
|
||||
const response = await BadgesApi.all();
|
||||
return response;
|
||||
}
|
||||
);
|
@ -1,87 +0,0 @@
|
||||
import { createAction, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { ProvidersApi, SystemApi } from "../../apis";
|
||||
|
||||
export const systemUpdateAllSettings = createAsyncThunk(
|
||||
"system/update",
|
||||
async (_: undefined, { dispatch }) => {
|
||||
await Promise.all([
|
||||
dispatch(systemUpdateSettings()),
|
||||
dispatch(systemUpdateLanguages()),
|
||||
dispatch(systemUpdateLanguagesProfiles()),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateLanguages = createAsyncThunk(
|
||||
"system/languages/update",
|
||||
async () => {
|
||||
const response = await SystemApi.languages();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateLanguagesProfiles = createAsyncThunk(
|
||||
"system/languages/profile/update",
|
||||
async () => {
|
||||
const response = await SystemApi.languagesProfileList();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateStatus = createAsyncThunk(
|
||||
"system/status/update",
|
||||
async () => {
|
||||
const response = await SystemApi.status();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateHealth = createAsyncThunk(
|
||||
"system/health/update",
|
||||
async () => {
|
||||
const response = await SystemApi.health();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
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 systemUpdateLogs = createAsyncThunk(
|
||||
"system/logs/update",
|
||||
async () => {
|
||||
const response = await SystemApi.logs();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateReleases = createAsyncThunk(
|
||||
"system/releases/update",
|
||||
async () => {
|
||||
const response = await SystemApi.releases();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const systemUpdateSettings = createAsyncThunk(
|
||||
"system/settings/update",
|
||||
async () => {
|
||||
const response = await SystemApi.settings();
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const providerUpdateList = createAsyncThunk(
|
||||
"providers/update",
|
||||
async () => {
|
||||
const response = await ProvidersApi.providers();
|
||||
return response;
|
||||
}
|
||||
);
|
@ -1,29 +0,0 @@
|
||||
import { AsyncThunk } from "@reduxjs/toolkit";
|
||||
import { useEffect } from "react";
|
||||
import { log } from "../../utilities/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,4 +1,39 @@
|
||||
export * from "./movies";
|
||||
export * from "./series";
|
||||
export * from "./site";
|
||||
export * from "./system";
|
||||
import { useSystemSettings } from "apis/hooks";
|
||||
import { useCallback } from "react";
|
||||
import { addNotifications } from "../actions";
|
||||
import { useReduxAction, useReduxStore } from "./base";
|
||||
|
||||
export function useNotification(id: string, timeout: number = 5000) {
|
||||
const add = useReduxAction(addNotifications);
|
||||
|
||||
return useCallback(
|
||||
(msg: Omit<Server.Notification, "id" | "timeout">) => {
|
||||
const notification: Server.Notification = {
|
||||
...msg,
|
||||
id,
|
||||
timeout,
|
||||
};
|
||||
add([notification]);
|
||||
},
|
||||
[add, timeout, id]
|
||||
);
|
||||
}
|
||||
|
||||
export function useIsOffline() {
|
||||
return useReduxStore((s) => s.offline);
|
||||
}
|
||||
|
||||
export function useIsSonarrEnabled() {
|
||||
const { data } = useSystemSettings();
|
||||
return data?.general.use_sonarr ?? true;
|
||||
}
|
||||
|
||||
export function useIsRadarrEnabled() {
|
||||
const { data } = useSystemSettings();
|
||||
return data?.general.use_radarr ?? true;
|
||||
}
|
||||
|
||||
export function useShowOnlyDesired() {
|
||||
const { data } = useSystemSettings();
|
||||
return data?.general.embedded_subs_show_desired ?? false;
|
||||
}
|
||||
|
@ -1,67 +0,0 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useEntityItemById, useEntityToList } from "../../utilities";
|
||||
import {
|
||||
movieUpdateBlacklist,
|
||||
movieUpdateById,
|
||||
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 items = useReduxStore((s) => s.movies.historyList);
|
||||
|
||||
return items;
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useEntityItemById, useEntityToList } from "../../utilities";
|
||||
import {
|
||||
episodesUpdateBlacklist,
|
||||
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, String(id));
|
||||
|
||||
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 items = useReduxStore((s) => s.series.historyList);
|
||||
|
||||
return items;
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { useSystemSettings } from ".";
|
||||
import { siteAddNotifications } from "../actions";
|
||||
import { useReduxAction, useReduxStore } from "./base";
|
||||
|
||||
export function useNotification(id: string, timeout: number = 5000) {
|
||||
const add = useReduxAction(siteAddNotifications);
|
||||
|
||||
return useCallback(
|
||||
(msg: Omit<Server.Notification, "id" | "timeout">) => {
|
||||
const notification: Server.Notification = {
|
||||
...msg,
|
||||
id,
|
||||
timeout,
|
||||
};
|
||||
add([notification]);
|
||||
},
|
||||
[add, timeout, id]
|
||||
);
|
||||
}
|
||||
|
||||
export function useIsOffline() {
|
||||
return useReduxStore((s) => s.site.offline);
|
||||
}
|
||||
|
||||
export function useIsSonarrEnabled() {
|
||||
const settings = useSystemSettings();
|
||||
return settings.content?.general.use_sonarr ?? true;
|
||||
}
|
||||
|
||||
export function useIsRadarrEnabled() {
|
||||
const settings = useSystemSettings();
|
||||
return settings.content?.general.use_radarr ?? true;
|
||||
}
|
||||
|
||||
export function useShowOnlyDesired() {
|
||||
const settings = useSystemSettings();
|
||||
return settings.content?.general.embedded_subs_show_desired ?? false;
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
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,13 +1,110 @@
|
||||
import movies from "./movie";
|
||||
import series from "./series";
|
||||
import site from "./site";
|
||||
import system from "./system";
|
||||
|
||||
const AllReducers = {
|
||||
movies,
|
||||
series,
|
||||
site,
|
||||
system,
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import { intersectionWith, pullAllWith, remove, sortBy, uniqBy } from "lodash";
|
||||
import apis from "../../apis/queries/client";
|
||||
import { isProdEnv } from "../../utilities";
|
||||
import {
|
||||
addNotifications,
|
||||
removeNotification,
|
||||
setOfflineStatus,
|
||||
setSidebar,
|
||||
setSiteStatus,
|
||||
setUnauthenticated,
|
||||
siteAddProgress,
|
||||
siteRemoveProgress,
|
||||
siteUpdateNotifier,
|
||||
siteUpdateProgressCount,
|
||||
} from "../actions";
|
||||
|
||||
interface Site {
|
||||
// Initialization state or error message
|
||||
status: Site.Status;
|
||||
offline: boolean;
|
||||
progress: Site.Progress[];
|
||||
notifier: {
|
||||
content: string | null;
|
||||
timestamp: string;
|
||||
};
|
||||
notifications: Server.Notification[];
|
||||
showSidebar: boolean;
|
||||
}
|
||||
|
||||
const defaultSite: Site = {
|
||||
status: "uninitialized",
|
||||
progress: [],
|
||||
notifier: {
|
||||
content: null,
|
||||
timestamp: String(Date.now()),
|
||||
},
|
||||
notifications: [],
|
||||
showSidebar: false,
|
||||
offline: false,
|
||||
};
|
||||
|
||||
export default AllReducers;
|
||||
const reducer = createReducer(defaultSite, (builder) => {
|
||||
builder
|
||||
.addCase(setUnauthenticated, (state) => {
|
||||
if (!isProdEnv) {
|
||||
apis._resetApi("NEED_AUTH");
|
||||
}
|
||||
state.status = "unauthenticated";
|
||||
})
|
||||
.addCase(setSiteStatus, (state, action) => {
|
||||
state.status = action.payload;
|
||||
});
|
||||
|
||||
builder
|
||||
.addCase(addNotifications, (state, action) => {
|
||||
state.notifications = uniqBy(
|
||||
[...action.payload, ...state.notifications],
|
||||
(v) => v.id
|
||||
);
|
||||
state.notifications = sortBy(state.notifications, (v) => v.id);
|
||||
})
|
||||
.addCase(removeNotification, (state, action) => {
|
||||
remove(state.notifications, (n) => n.id === action.payload);
|
||||
});
|
||||
|
||||
builder
|
||||
.addCase(siteAddProgress, (state, action) => {
|
||||
state.progress = uniqBy(
|
||||
[...action.payload, ...state.progress],
|
||||
(n) => n.id
|
||||
);
|
||||
state.progress = sortBy(state.progress, (v) => v.id);
|
||||
})
|
||||
.addCase(siteRemoveProgress.pending, (state, action) => {
|
||||
// Mark completed
|
||||
intersectionWith(
|
||||
state.progress,
|
||||
action.meta.arg,
|
||||
(l, r) => l.id === r
|
||||
).forEach((v) => {
|
||||
v.value = v.count + 1;
|
||||
});
|
||||
})
|
||||
.addCase(siteRemoveProgress.fulfilled, (state, action) => {
|
||||
pullAllWith(state.progress, action.payload, (l, r) => l.id === r);
|
||||
})
|
||||
.addCase(siteUpdateProgressCount, (state, action) => {
|
||||
const { id, count } = action.payload;
|
||||
const progress = state.progress.find((v) => v.id === id);
|
||||
if (progress) {
|
||||
progress.count = count;
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(siteUpdateNotifier, (state, action) => {
|
||||
state.notifier.content = action.payload;
|
||||
state.notifier.timestamp = String(Date.now());
|
||||
});
|
||||
|
||||
builder
|
||||
.addCase(setSidebar, (state, action) => {
|
||||
state.showSidebar = action.payload;
|
||||
})
|
||||
.addCase(setOfflineStatus, (state, action) => {
|
||||
state.offline = action.payload;
|
||||
});
|
||||
});
|
||||
|
||||
export default reducer;
|
||||
|
@ -1,68 +0,0 @@
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import {
|
||||
movieMarkBlacklistDirty,
|
||||
movieMarkDirtyById,
|
||||
movieMarkHistoryDirty,
|
||||
movieMarkWantedDirtyById,
|
||||
movieRemoveById,
|
||||
movieRemoveWantedById,
|
||||
movieResetHistory,
|
||||
movieResetWanted,
|
||||
movieUpdateAll,
|
||||
movieUpdateBlacklist,
|
||||
movieUpdateById,
|
||||
movieUpdateByRange,
|
||||
movieUpdateHistoryByRange,
|
||||
movieUpdateWantedById,
|
||||
movieUpdateWantedByRange,
|
||||
} from "../actions";
|
||||
import { AsyncUtility } from "../utils";
|
||||
import {
|
||||
createAsyncEntityReducer,
|
||||
createAsyncItemReducer,
|
||||
} from "../utils/factory";
|
||||
|
||||
interface Movie {
|
||||
movieList: Async.Entity<Item.Movie>;
|
||||
wantedMovieList: Async.Entity<Wanted.Movie>;
|
||||
historyList: Async.Entity<History.Movie>;
|
||||
blacklist: Async.Item<Blacklist.Movie[]>;
|
||||
}
|
||||
|
||||
const defaultMovie: Movie = {
|
||||
movieList: AsyncUtility.getDefaultEntity("radarrId"),
|
||||
wantedMovieList: AsyncUtility.getDefaultEntity("radarrId"),
|
||||
historyList: AsyncUtility.getDefaultEntity("id"),
|
||||
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,
|
||||
reset: movieResetWanted,
|
||||
});
|
||||
|
||||
createAsyncEntityReducer(builder, (s) => s.historyList, {
|
||||
range: movieUpdateHistoryByRange,
|
||||
dirty: movieMarkHistoryDirty,
|
||||
reset: movieResetHistory,
|
||||
});
|
||||
|
||||
createAsyncItemReducer(builder, (s) => s.blacklist, {
|
||||
all: movieUpdateBlacklist,
|
||||
dirty: movieMarkBlacklistDirty,
|
||||
});
|
||||
});
|
||||
|
||||
export default reducer;
|
@ -1,100 +0,0 @@
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import {
|
||||
episodesMarkBlacklistDirty,
|
||||
episodesMarkDirtyById,
|
||||
episodesMarkHistoryDirty,
|
||||
episodesRemoveById,
|
||||
episodesResetHistory,
|
||||
episodesUpdateBlacklist,
|
||||
episodesUpdateHistoryByRange,
|
||||
episodeUpdateById,
|
||||
episodeUpdateBySeriesId,
|
||||
seriesMarkDirtyById,
|
||||
seriesMarkWantedDirtyById,
|
||||
seriesRemoveById,
|
||||
seriesRemoveWantedById,
|
||||
seriesResetWanted,
|
||||
seriesUpdateAll,
|
||||
seriesUpdateById,
|
||||
seriesUpdateByRange,
|
||||
seriesUpdateWantedById,
|
||||
seriesUpdateWantedByRange,
|
||||
} from "../actions";
|
||||
import { AsyncUtility, ReducerUtility } from "../utils";
|
||||
import {
|
||||
createAsyncEntityReducer,
|
||||
createAsyncItemReducer,
|
||||
createAsyncListReducer,
|
||||
} from "../utils/factory";
|
||||
|
||||
interface Series {
|
||||
seriesList: Async.Entity<Item.Series>;
|
||||
wantedEpisodesList: Async.Entity<Wanted.Episode>;
|
||||
episodeList: Async.List<Item.Episode>;
|
||||
historyList: Async.Entity<History.Episode>;
|
||||
blacklist: Async.Item<Blacklist.Episode[]>;
|
||||
}
|
||||
|
||||
const defaultSeries: Series = {
|
||||
seriesList: AsyncUtility.getDefaultEntity("sonarrSeriesId"),
|
||||
wantedEpisodesList: AsyncUtility.getDefaultEntity("sonarrEpisodeId"),
|
||||
episodeList: AsyncUtility.getDefaultList("sonarrEpisodeId"),
|
||||
historyList: AsyncUtility.getDefaultEntity("id"),
|
||||
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);
|
||||
|
||||
ReducerUtility.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));
|
||||
|
||||
ReducerUtility.markDirty(episodes, dirtyEpisodeIds);
|
||||
});
|
||||
|
||||
createAsyncEntityReducer(builder, (s) => s.wantedEpisodesList, {
|
||||
range: seriesUpdateWantedByRange,
|
||||
ids: seriesUpdateWantedById,
|
||||
removeIds: seriesRemoveWantedById,
|
||||
dirty: seriesMarkWantedDirtyById,
|
||||
reset: seriesResetWanted,
|
||||
});
|
||||
|
||||
createAsyncEntityReducer(builder, (s) => s.historyList, {
|
||||
range: episodesUpdateHistoryByRange,
|
||||
dirty: episodesMarkHistoryDirty,
|
||||
reset: episodesResetHistory,
|
||||
});
|
||||
|
||||
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,130 +0,0 @@
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import { intersectionWith, pullAllWith, remove, sortBy, uniqBy } from "lodash";
|
||||
import apis from "../../apis";
|
||||
import { isProdEnv } from "../../utilities";
|
||||
import {
|
||||
siteAddNotifications,
|
||||
siteAddProgress,
|
||||
siteBootstrap,
|
||||
siteChangeSidebarVisibility,
|
||||
siteRedirectToAuth,
|
||||
siteRemoveNotifications,
|
||||
siteRemoveProgress,
|
||||
siteUpdateBadges,
|
||||
siteUpdateInitialization,
|
||||
siteUpdateNotifier,
|
||||
siteUpdateOffline,
|
||||
siteUpdateProgressCount,
|
||||
} from "../actions/site";
|
||||
|
||||
interface Site {
|
||||
// Initialization state or error message
|
||||
initialized: boolean | string;
|
||||
offline: boolean;
|
||||
auth: boolean;
|
||||
progress: Site.Progress[];
|
||||
notifier: {
|
||||
content: string | null;
|
||||
timestamp: string;
|
||||
};
|
||||
notifications: Server.Notification[];
|
||||
showSidebar: boolean;
|
||||
badges: Badge;
|
||||
}
|
||||
|
||||
const defaultSite: Site = {
|
||||
initialized: false,
|
||||
auth: true,
|
||||
progress: [],
|
||||
notifier: {
|
||||
content: null,
|
||||
timestamp: String(Date.now()),
|
||||
},
|
||||
notifications: [],
|
||||
showSidebar: false,
|
||||
badges: {
|
||||
movies: 0,
|
||||
episodes: 0,
|
||||
providers: 0,
|
||||
status: 0,
|
||||
},
|
||||
offline: false,
|
||||
};
|
||||
|
||||
const reducer = createReducer(defaultSite, (builder) => {
|
||||
builder
|
||||
.addCase(siteBootstrap.fulfilled, (state) => {
|
||||
state.initialized = true;
|
||||
})
|
||||
.addCase(siteBootstrap.rejected, (state) => {
|
||||
state.initialized = "An Error Occurred When Initializing Bazarr UI";
|
||||
})
|
||||
.addCase(siteRedirectToAuth, (state) => {
|
||||
if (!isProdEnv) {
|
||||
apis._resetApi("NEED_AUTH");
|
||||
}
|
||||
state.auth = false;
|
||||
})
|
||||
.addCase(siteUpdateInitialization, (state, action) => {
|
||||
state.initialized = action.payload;
|
||||
});
|
||||
|
||||
builder
|
||||
.addCase(siteAddNotifications, (state, action) => {
|
||||
state.notifications = uniqBy(
|
||||
[...action.payload, ...state.notifications],
|
||||
(v) => v.id
|
||||
);
|
||||
state.notifications = sortBy(state.notifications, (v) => v.id);
|
||||
})
|
||||
.addCase(siteRemoveNotifications, (state, action) => {
|
||||
remove(state.notifications, (n) => n.id === action.payload);
|
||||
});
|
||||
|
||||
builder
|
||||
.addCase(siteAddProgress, (state, action) => {
|
||||
state.progress = uniqBy(
|
||||
[...action.payload, ...state.progress],
|
||||
(n) => n.id
|
||||
);
|
||||
state.progress = sortBy(state.progress, (v) => v.id);
|
||||
})
|
||||
.addCase(siteRemoveProgress.pending, (state, action) => {
|
||||
// Mark completed
|
||||
intersectionWith(
|
||||
state.progress,
|
||||
action.meta.arg,
|
||||
(l, r) => l.id === r
|
||||
).forEach((v) => {
|
||||
v.value = v.count + 1;
|
||||
});
|
||||
})
|
||||
.addCase(siteRemoveProgress.fulfilled, (state, action) => {
|
||||
pullAllWith(state.progress, action.payload, (l, r) => l.id === r);
|
||||
})
|
||||
.addCase(siteUpdateProgressCount, (state, action) => {
|
||||
const { id, count } = action.payload;
|
||||
const progress = state.progress.find((v) => v.id === id);
|
||||
if (progress) {
|
||||
progress.count = count;
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(siteUpdateNotifier, (state, action) => {
|
||||
state.notifier.content = action.payload;
|
||||
state.notifier.timestamp = String(Date.now());
|
||||
});
|
||||
|
||||
builder
|
||||
.addCase(siteChangeSidebarVisibility, (state, action) => {
|
||||
state.showSidebar = action.payload;
|
||||
})
|
||||
.addCase(siteUpdateOffline, (state, action) => {
|
||||
state.offline = action.payload;
|
||||
})
|
||||
.addCase(siteUpdateBadges.fulfilled, (state, action) => {
|
||||
state.badges = action.payload;
|
||||
});
|
||||
});
|
||||
|
||||
export default reducer;
|
@ -1,74 +0,0 @@
|
||||
import { createReducer } from "@reduxjs/toolkit";
|
||||
import {
|
||||
providerUpdateList,
|
||||
systemMarkTasksDirty,
|
||||
systemUpdateHealth,
|
||||
systemUpdateLanguages,
|
||||
systemUpdateLanguagesProfiles,
|
||||
systemUpdateLogs,
|
||||
systemUpdateReleases,
|
||||
systemUpdateSettings,
|
||||
systemUpdateStatus,
|
||||
systemUpdateTasks,
|
||||
} from "../actions";
|
||||
import { AsyncUtility } from "../utils";
|
||||
import { createAsyncItemReducer } from "../utils/factory";
|
||||
|
||||
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,31 +0,0 @@
|
||||
import { AsyncUtility } from "../utils";
|
||||
|
||||
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,32 +0,0 @@
|
||||
import {} from "jest";
|
||||
import { AsyncUtility } from "..";
|
||||
|
||||
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({});
|
||||
});
|
@ -1,339 +0,0 @@
|
||||
import {
|
||||
ActionCreatorWithoutPayload,
|
||||
ActionCreatorWithPayload,
|
||||
ActionReducerMapBuilder,
|
||||
AsyncThunk,
|
||||
Draft,
|
||||
} from "@reduxjs/toolkit";
|
||||
import {
|
||||
difference,
|
||||
findIndex,
|
||||
isNull,
|
||||
isString,
|
||||
omit,
|
||||
pullAll,
|
||||
pullAllWith,
|
||||
} from "lodash";
|
||||
import { ReducerUtility } from ".";
|
||||
import { conditionalLog } from "../../utilities/logger";
|
||||
|
||||
interface ActionParam<T, ID = null> {
|
||||
range?: AsyncThunk<T, Parameter.Range, {}>;
|
||||
all?: AsyncThunk<T, void, {}>;
|
||||
ids?: AsyncThunk<T, ID[], {}>;
|
||||
removeIds?: ActionCreatorWithPayload<ID[]>;
|
||||
reset?: ActionCreatorWithoutPayload;
|
||||
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 strIds = arg.map(String);
|
||||
|
||||
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>);
|
||||
}
|
||||
});
|
||||
|
||||
ReducerUtility.updateDirty(list, strIds);
|
||||
ReducerUtility.updateDidLoaded(list, strIds);
|
||||
})
|
||||
.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;
|
||||
});
|
||||
|
||||
ReducerUtility.removeDirty(list, removeIds);
|
||||
ReducerUtility.removeDidLoaded(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 = [];
|
||||
|
||||
const ids = action.payload.map((v) =>
|
||||
String(v[list.keyName as keyof T])
|
||||
);
|
||||
ReducerUtility.updateDidLoaded(list, ids);
|
||||
})
|
||||
.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);
|
||||
ReducerUtility.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, reset } = 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);
|
||||
|
||||
data.forEach((v) => {
|
||||
const key = String(v[keyName]);
|
||||
entity.content.entities[key] = v as Draft<T>;
|
||||
});
|
||||
|
||||
const idsToUpdate = data.map((v) => String(v[keyName]));
|
||||
|
||||
// Remove duplicated ids
|
||||
const pulledSize =
|
||||
total - pullAll(entity.content.ids, idsToUpdate).length;
|
||||
entity.content.ids.push(...Array(pulledSize).fill(null));
|
||||
|
||||
entity.content.ids.splice(start, length, ...idsToUpdate);
|
||||
|
||||
ReducerUtility.updateDirty(entity, idsToUpdate);
|
||||
ReducerUtility.updateDidLoaded(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>;
|
||||
});
|
||||
|
||||
const allIds = arg.map(String);
|
||||
|
||||
ReducerUtility.updateDirty(entity, allIds);
|
||||
ReducerUtility.updateDidLoaded(entity, allIds);
|
||||
})
|
||||
.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);
|
||||
ReducerUtility.removeDirty(entity, idsToDelete);
|
||||
ReducerUtility.removeDidLoaded(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;
|
||||
}, {});
|
||||
|
||||
const allIds = entity.content.ids.filter(isString);
|
||||
ReducerUtility.updateDidLoaded(entity, allIds);
|
||||
})
|
||||
.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);
|
||||
ReducerUtility.markDirty(entity, action.payload.map(String));
|
||||
});
|
||||
|
||||
reset &&
|
||||
builder.addCase(reset, (state) => {
|
||||
const entity = getEntity(state);
|
||||
entity.content.entities = {};
|
||||
entity.content.ids = [];
|
||||
entity.didLoaded = [];
|
||||
entity.dirtyEntities = [];
|
||||
entity.error = null;
|
||||
entity.state = "uninitialized";
|
||||
});
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
import { Draft } from "@reduxjs/toolkit";
|
||||
import { difference, pullAll, 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: [],
|
||||
didLoaded: [],
|
||||
content: [],
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultEntity<T>(key: keyof T): Async.Entity<T> {
|
||||
return {
|
||||
state: "uninitialized",
|
||||
dirtyEntities: [],
|
||||
didLoaded: [],
|
||||
content: {
|
||||
keyName: key,
|
||||
ids: [],
|
||||
entities: {},
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ReducerUtility {
|
||||
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[]
|
||||
) {
|
||||
pullAll(entity.dirtyEntities, removedIds);
|
||||
if (entity.dirtyEntities.length === 0 && entity.state === "dirty") {
|
||||
entity.state = "succeeded";
|
||||
}
|
||||
}
|
||||
|
||||
export function updateDidLoaded<T extends DirtyType>(
|
||||
entity: T,
|
||||
loadedIds: string[]
|
||||
) {
|
||||
entity.didLoaded.push(...loadedIds);
|
||||
entity.didLoaded = uniq(entity.didLoaded);
|
||||
}
|
||||
|
||||
export function removeDidLoaded<T extends DirtyType>(
|
||||
entity: T,
|
||||
removedIds: string[]
|
||||
) {
|
||||
pullAll(entity.didLoaded, removedIds);
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
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[];
|
||||
didLoaded: string[];
|
||||
};
|
||||
|
||||
type Item<T> = Base<T | null>;
|
||||
|
||||
type Entity<T> = Base<EntityStruct<T>> & {
|
||||
dirtyEntities: string[];
|
||||
didLoaded: string[];
|
||||
};
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
type RangeQuery<T> = (
|
||||
param: Parameter.Range
|
||||
) => Promise<DataWrapperWithTotal<T>>;
|
@ -1,39 +0,0 @@
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useBlacklistMovies } from "../../@redux/hooks";
|
||||
import { MoviesApi } from "../../apis";
|
||||
import { AsyncOverlay, ContentHeader } from "../../components";
|
||||
import Table from "./table";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const BlacklistMoviesView: FunctionComponent<Props> = () => {
|
||||
const blacklist = useBlacklistMovies();
|
||||
return (
|
||||
<AsyncOverlay ctx={blacklist}>
|
||||
{({ content }) => (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Movies Blacklist - Bazarr</title>
|
||||
</Helmet>
|
||||
<ContentHeader>
|
||||
<ContentHeader.AsyncButton
|
||||
icon={faTrash}
|
||||
disabled={content?.length === 0}
|
||||
promise={() => MoviesApi.deleteBlacklist(true)}
|
||||
>
|
||||
Remove All
|
||||
</ContentHeader.AsyncButton>
|
||||
</ContentHeader>
|
||||
<Row>
|
||||
<Table blacklist={content ?? []}></Table>
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
</AsyncOverlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlacklistMoviesView;
|
@ -1,39 +0,0 @@
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useBlacklistSeries } from "../../@redux/hooks";
|
||||
import { EpisodesApi } from "../../apis";
|
||||
import { AsyncOverlay, ContentHeader } from "../../components";
|
||||
import Table from "./table";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const BlacklistSeriesView: FunctionComponent<Props> = () => {
|
||||
const blacklist = useBlacklistSeries();
|
||||
return (
|
||||
<AsyncOverlay ctx={blacklist}>
|
||||
{({ content }) => (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Series Blacklist - Bazarr</title>
|
||||
</Helmet>
|
||||
<ContentHeader>
|
||||
<ContentHeader.AsyncButton
|
||||
icon={faTrash}
|
||||
disabled={content?.length === 0}
|
||||
promise={() => EpisodesApi.deleteBlacklist(true)}
|
||||
>
|
||||
Remove All
|
||||
</ContentHeader.AsyncButton>
|
||||
</ContentHeader>
|
||||
<Row>
|
||||
<Table blacklist={content ?? []}></Table>
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
</AsyncOverlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlacklistSeriesView;
|
@ -1,177 +0,0 @@
|
||||
import { faCheck, faList, faUndo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { AsyncThunk } from "@reduxjs/toolkit";
|
||||
import { uniqBy } from "lodash";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { Container, Dropdown, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Column } from "react-table";
|
||||
import { useIsAnyTaskRunning } from "../../../@modules/task/hooks";
|
||||
import { useLanguageProfiles } from "../../../@redux/hooks";
|
||||
import { useAppDispatch } from "../../../@redux/hooks/base";
|
||||
import { ContentHeader } from "../../../components";
|
||||
import { GetItemId, isNonNullable } from "../../../utilities";
|
||||
import Table from "./table";
|
||||
|
||||
export interface SharedProps<T extends Item.Base> {
|
||||
name: string;
|
||||
loader: (params: Parameter.Range) => void;
|
||||
columns: Column<T>[];
|
||||
modify: (form: FormType.ModifyItem) => Promise<void>;
|
||||
state: Async.Entity<T>;
|
||||
}
|
||||
|
||||
interface Props<T extends Item.Base = Item.Base> extends SharedProps<T> {
|
||||
updateAction: AsyncThunk<AsyncDataWrapper<T>, void, {}>;
|
||||
}
|
||||
|
||||
function BaseItemView<T extends Item.Base>({
|
||||
updateAction,
|
||||
...shared
|
||||
}: Props<T>) {
|
||||
const state = shared.state;
|
||||
|
||||
const [pendingEditMode, setPendingEdit] = useState(false);
|
||||
const [editMode, setEdit] = useState(false);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const update = useCallback(() => {
|
||||
dispatch(updateAction()).then(() => {
|
||||
setPendingEdit((edit) => {
|
||||
// Hack to remove all dependencies
|
||||
setEdit(edit);
|
||||
return edit;
|
||||
});
|
||||
setDirty([]);
|
||||
});
|
||||
}, [dispatch, updateAction]);
|
||||
|
||||
const [selections, setSelections] = useState<T[]>([]);
|
||||
const [dirtyItems, setDirty] = useState<T[]>([]);
|
||||
|
||||
const profiles = useLanguageProfiles();
|
||||
|
||||
const profileOptions = useMemo<JSX.Element[]>(() => {
|
||||
const items: JSX.Element[] = [];
|
||||
if (profiles) {
|
||||
items.push(
|
||||
<Dropdown.Item key="clear-profile">Clear Profile</Dropdown.Item>
|
||||
);
|
||||
items.push(<Dropdown.Divider key="dropdown-divider"></Dropdown.Divider>);
|
||||
items.push(
|
||||
...profiles.map((v) => (
|
||||
<Dropdown.Item key={v.profileId} eventKey={v.profileId.toString()}>
|
||||
{v.name}
|
||||
</Dropdown.Item>
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [profiles]);
|
||||
|
||||
const changeProfiles = useCallback(
|
||||
(key: Nullable<string>) => {
|
||||
const id = key ? parseInt(key) : null;
|
||||
const newItems = selections.map((v) => {
|
||||
const item = { ...v };
|
||||
item.profileId = id;
|
||||
return item;
|
||||
});
|
||||
setDirty((dirty) => {
|
||||
return uniqBy([...newItems, ...dirty], GetItemId);
|
||||
});
|
||||
},
|
||||
[selections]
|
||||
);
|
||||
|
||||
const startEdit = useCallback(() => {
|
||||
if (shared.state.content.ids.every(isNonNullable)) {
|
||||
setEdit(true);
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
setPendingEdit(true);
|
||||
}, [shared.state.content.ids, update]);
|
||||
|
||||
const endEdit = useCallback(() => {
|
||||
setEdit(false);
|
||||
setDirty([]);
|
||||
setPendingEdit(false);
|
||||
setSelections([]);
|
||||
}, []);
|
||||
|
||||
const save = useCallback(() => {
|
||||
const form: FormType.ModifyItem = {
|
||||
id: [],
|
||||
profileid: [],
|
||||
};
|
||||
dirtyItems.forEach((v) => {
|
||||
const id = GetItemId(v);
|
||||
form.id.push(id);
|
||||
form.profileid.push(v.profileId);
|
||||
});
|
||||
return shared.modify(form);
|
||||
}, [dirtyItems, shared]);
|
||||
|
||||
const hasTask = useIsAnyTaskRunning();
|
||||
|
||||
return (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>{shared.name} - Bazarr</title>
|
||||
</Helmet>
|
||||
<ContentHeader scroll={false}>
|
||||
{editMode ? (
|
||||
<React.Fragment>
|
||||
<ContentHeader.Group pos="start">
|
||||
<Dropdown onSelect={changeProfiles}>
|
||||
<Dropdown.Toggle
|
||||
disabled={selections.length === 0}
|
||||
variant="light"
|
||||
>
|
||||
Change Profile
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>{profileOptions}</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ContentHeader.Group>
|
||||
<ContentHeader.Group pos="end">
|
||||
<ContentHeader.Button icon={faUndo} onClick={endEdit}>
|
||||
Cancel
|
||||
</ContentHeader.Button>
|
||||
<ContentHeader.AsyncButton
|
||||
icon={faCheck}
|
||||
disabled={dirtyItems.length === 0 || hasTask}
|
||||
promise={save}
|
||||
onSuccess={endEdit}
|
||||
>
|
||||
Save
|
||||
</ContentHeader.AsyncButton>
|
||||
</ContentHeader.Group>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<ContentHeader.Button
|
||||
updating={pendingEditMode !== editMode}
|
||||
disabled={
|
||||
(state.content.ids.length === 0 && state.state === "loading") ||
|
||||
hasTask
|
||||
}
|
||||
icon={faList}
|
||||
onClick={startEdit}
|
||||
>
|
||||
Mass Edit
|
||||
</ContentHeader.Button>
|
||||
)}
|
||||
</ContentHeader>
|
||||
<Row>
|
||||
<Table
|
||||
{...shared}
|
||||
dirtyItems={dirtyItems}
|
||||
editMode={editMode}
|
||||
select={setSelections}
|
||||
></Table>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default BaseItemView;
|
@ -1,78 +0,0 @@
|
||||
import { uniqBy } from "lodash";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { TableOptions, TableUpdater, useRowSelect } from "react-table";
|
||||
import { SharedProps } from ".";
|
||||
import {
|
||||
AsyncPageTable,
|
||||
ItemEditorModal,
|
||||
SimpleTable,
|
||||
useShowModal,
|
||||
} from "../../../components";
|
||||
import { TableStyleProps } from "../../../components/tables/BaseTable";
|
||||
import { useCustomSelection } from "../../../components/tables/plugins";
|
||||
import { GetItemId, useEntityToList } from "../../../utilities";
|
||||
|
||||
interface Props<T extends Item.Base> extends SharedProps<T> {
|
||||
dirtyItems: readonly T[];
|
||||
editMode: boolean;
|
||||
select: React.Dispatch<T[]>;
|
||||
}
|
||||
|
||||
function Table<T extends Item.Base>({
|
||||
state,
|
||||
dirtyItems,
|
||||
modify,
|
||||
editMode,
|
||||
select,
|
||||
columns,
|
||||
loader,
|
||||
name,
|
||||
}: Props<T>) {
|
||||
const showModal = useShowModal();
|
||||
|
||||
const updateRow = useCallback<TableUpdater<T>>(
|
||||
(row, modalKey: string) => {
|
||||
showModal(modalKey, row.original);
|
||||
},
|
||||
[showModal]
|
||||
);
|
||||
|
||||
const orderList = useEntityToList(state.content);
|
||||
|
||||
const data = useMemo(
|
||||
() => uniqBy([...dirtyItems, ...orderList], GetItemId),
|
||||
[dirtyItems, orderList]
|
||||
);
|
||||
|
||||
const options: Partial<TableOptions<T> & TableStyleProps<T>> = {
|
||||
emptyText: `No ${name} Found`,
|
||||
update: updateRow,
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{editMode ? (
|
||||
// TODO: Use PageTable
|
||||
<SimpleTable
|
||||
{...options}
|
||||
columns={columns}
|
||||
data={data}
|
||||
onSelect={select}
|
||||
isSelecting={true}
|
||||
plugins={[useRowSelect, useCustomSelection]}
|
||||
></SimpleTable>
|
||||
) : (
|
||||
<AsyncPageTable
|
||||
{...options}
|
||||
columns={columns}
|
||||
entity={state}
|
||||
loader={loader}
|
||||
data={[]}
|
||||
></AsyncPageTable>
|
||||
)}
|
||||
<ItemEditorModal modalKey="edit" submit={modify}></ItemEditorModal>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default Table;
|
@ -1,149 +0,0 @@
|
||||
import { merge } from "lodash";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Col, Container } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { useDidMount } from "rooks";
|
||||
import {
|
||||
HistoryApi,
|
||||
ProvidersApi,
|
||||
SystemApi,
|
||||
useAsyncRequest,
|
||||
} from "../../apis";
|
||||
import {
|
||||
AsyncOverlay,
|
||||
AsyncSelector,
|
||||
ContentHeader,
|
||||
LanguageSelector,
|
||||
Selector,
|
||||
} from "../../components";
|
||||
import { actionOptions, timeframeOptions } from "./options";
|
||||
|
||||
function converter(item: History.Stat) {
|
||||
const movies = item.movies.map((v) => ({
|
||||
date: v.date,
|
||||
movies: v.count,
|
||||
}));
|
||||
const series = item.series.map((v) => ({
|
||||
date: v.date,
|
||||
series: v.count,
|
||||
}));
|
||||
const result = merge(movies, series);
|
||||
return result;
|
||||
}
|
||||
|
||||
const providerLabel = (item: System.Provider) => item.name;
|
||||
|
||||
const SelectorContainer: FunctionComponent = ({ children }) => (
|
||||
<Col xs={6} lg={3} className="p-1">
|
||||
{children}
|
||||
</Col>
|
||||
);
|
||||
|
||||
const HistoryStats: FunctionComponent = () => {
|
||||
const [languages, updateLanguages] = useAsyncRequest(
|
||||
SystemApi.languages.bind(SystemApi)
|
||||
);
|
||||
const [providerList, updateProviderParam] = useAsyncRequest(
|
||||
ProvidersApi.providers.bind(ProvidersApi)
|
||||
);
|
||||
|
||||
const updateProvider = useCallback(
|
||||
() => updateProviderParam(true),
|
||||
[updateProviderParam]
|
||||
);
|
||||
|
||||
useDidMount(() => {
|
||||
updateLanguages(true);
|
||||
});
|
||||
|
||||
const [timeframe, setTimeframe] = useState<History.TimeframeOptions>("month");
|
||||
const [action, setAction] = useState<Nullable<History.ActionOptions>>(null);
|
||||
const [lang, setLanguage] = useState<Nullable<Language.Info>>(null);
|
||||
const [provider, setProvider] = useState<Nullable<System.Provider>>(null);
|
||||
|
||||
const [stats, update] = useAsyncRequest(HistoryApi.stats.bind(HistoryApi));
|
||||
|
||||
useEffect(() => {
|
||||
update(timeframe, action ?? undefined, provider?.name, lang?.code2);
|
||||
}, [timeframe, action, provider?.name, lang?.code2, update]);
|
||||
|
||||
return (
|
||||
// TODO: Responsive
|
||||
<Container fluid className="vh-75">
|
||||
<Helmet>
|
||||
<title>History Statistics - Bazarr</title>
|
||||
</Helmet>
|
||||
<AsyncOverlay ctx={stats}>
|
||||
{({ content }) => (
|
||||
<React.Fragment>
|
||||
<ContentHeader scroll={false}>
|
||||
<SelectorContainer>
|
||||
<Selector
|
||||
placeholder="Time..."
|
||||
options={timeframeOptions}
|
||||
value={timeframe}
|
||||
onChange={(v) => setTimeframe(v ?? "month")}
|
||||
></Selector>
|
||||
</SelectorContainer>
|
||||
<SelectorContainer>
|
||||
<Selector
|
||||
placeholder="Action..."
|
||||
clearable
|
||||
options={actionOptions}
|
||||
value={action}
|
||||
onChange={setAction}
|
||||
></Selector>
|
||||
</SelectorContainer>
|
||||
<SelectorContainer>
|
||||
<AsyncSelector
|
||||
placeholder="Provider..."
|
||||
clearable
|
||||
state={providerList}
|
||||
label={providerLabel}
|
||||
update={updateProvider}
|
||||
onChange={setProvider}
|
||||
></AsyncSelector>
|
||||
</SelectorContainer>
|
||||
<SelectorContainer>
|
||||
<LanguageSelector
|
||||
clearable
|
||||
options={languages.content ?? []}
|
||||
value={lang}
|
||||
onChange={setLanguage}
|
||||
></LanguageSelector>
|
||||
</SelectorContainer>
|
||||
</ContentHeader>
|
||||
<ResponsiveContainer height="100%">
|
||||
<BarChart data={content ? converter(content) : []}>
|
||||
<CartesianGrid strokeDasharray="4 2"></CartesianGrid>
|
||||
<XAxis dataKey="date"></XAxis>
|
||||
<YAxis allowDecimals={false}></YAxis>
|
||||
<Tooltip></Tooltip>
|
||||
<Legend verticalAlign="top"></Legend>
|
||||
<Bar name="Series" dataKey="series" fill="#2493B6"></Bar>
|
||||
<Bar name="Movies" dataKey="movies" fill="#FFC22F"></Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</AsyncOverlay>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryStats;
|
@ -1,40 +0,0 @@
|
||||
import { capitalize } from "lodash";
|
||||
import React from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Column } from "react-table";
|
||||
import { AsyncPageTable } from "../../components";
|
||||
|
||||
interface Props<T extends History.Base> {
|
||||
type: "movies" | "series";
|
||||
state: Readonly<Async.Entity<T>>;
|
||||
loader: (param: Parameter.Range) => void;
|
||||
columns: Column<T>[];
|
||||
}
|
||||
|
||||
function HistoryGenericView<T extends History.Base = History.Base>({
|
||||
state,
|
||||
loader,
|
||||
columns,
|
||||
type,
|
||||
}: Props<T>) {
|
||||
const typeName = capitalize(type);
|
||||
return (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>{typeName} History - Bazarr</title>
|
||||
</Helmet>
|
||||
<Row>
|
||||
<AsyncPageTable
|
||||
emptyText={`Nothing Found in ${typeName} History`}
|
||||
entity={state}
|
||||
loader={loader}
|
||||
columns={columns}
|
||||
data={[]}
|
||||
></AsyncPageTable>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default HistoryGenericView;
|
@ -1,66 +0,0 @@
|
||||
import { faDownload, faSync, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { systemUpdateLogs } from "../../@redux/actions";
|
||||
import { useSystemLogs } from "../../@redux/hooks";
|
||||
import { useReduxAction } from "../../@redux/hooks/base";
|
||||
import { SystemApi } from "../../apis";
|
||||
import { AsyncOverlay, ContentHeader } from "../../components";
|
||||
import { Environment } from "../../utilities";
|
||||
import Table from "./table";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const SystemLogsView: FunctionComponent<Props> = () => {
|
||||
const logs = useSystemLogs();
|
||||
const update = useReduxAction(systemUpdateLogs);
|
||||
|
||||
const [resetting, setReset] = useState(false);
|
||||
|
||||
const download = useCallback(() => {
|
||||
window.open(`${Environment.baseUrl}/bazarr.log`);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AsyncOverlay ctx={logs}>
|
||||
{({ content, state }) => (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Logs - Bazarr (System)</title>
|
||||
</Helmet>
|
||||
<ContentHeader>
|
||||
<ContentHeader.Button
|
||||
updating={state === "loading"}
|
||||
icon={faSync}
|
||||
onClick={update}
|
||||
>
|
||||
Refresh
|
||||
</ContentHeader.Button>
|
||||
<ContentHeader.Button icon={faDownload} onClick={download}>
|
||||
Download
|
||||
</ContentHeader.Button>
|
||||
<ContentHeader.Button
|
||||
updating={resetting}
|
||||
icon={faTrash}
|
||||
onClick={() => {
|
||||
setReset(true);
|
||||
SystemApi.deleteLogs().finally(() => {
|
||||
setReset(false);
|
||||
update();
|
||||
});
|
||||
}}
|
||||
>
|
||||
Empty
|
||||
</ContentHeader.Button>
|
||||
</ContentHeader>
|
||||
<Row>
|
||||
<Table logs={content ?? []}></Table>
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
</AsyncOverlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemLogsView;
|
@ -1,50 +0,0 @@
|
||||
import { faSync, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { providerUpdateList } from "../../@redux/actions";
|
||||
import { useSystemProviders } from "../../@redux/hooks";
|
||||
import { useReduxAction } from "../../@redux/hooks/base";
|
||||
import { ProvidersApi } from "../../apis";
|
||||
import { AsyncOverlay, ContentHeader } from "../../components";
|
||||
import Table from "./table";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const SystemProvidersView: FunctionComponent<Props> = () => {
|
||||
const providers = useSystemProviders();
|
||||
const update = useReduxAction(providerUpdateList);
|
||||
|
||||
return (
|
||||
<AsyncOverlay ctx={providers}>
|
||||
{({ content, state }) => (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Providers - Bazarr (System)</title>
|
||||
</Helmet>
|
||||
<ContentHeader>
|
||||
<ContentHeader.Button
|
||||
updating={state === "loading"}
|
||||
icon={faSync}
|
||||
onClick={update}
|
||||
>
|
||||
Refresh
|
||||
</ContentHeader.Button>
|
||||
<ContentHeader.AsyncButton
|
||||
icon={faTrash}
|
||||
promise={() => ProvidersApi.reset()}
|
||||
onSuccess={update}
|
||||
>
|
||||
Reset
|
||||
</ContentHeader.AsyncButton>
|
||||
</ContentHeader>
|
||||
<Row>
|
||||
<Table providers={content ?? []}></Table>
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
</AsyncOverlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemProvidersView;
|
@ -1,42 +0,0 @@
|
||||
import { faSync } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { systemMarkTasksDirty } from "../../@redux/actions";
|
||||
import { useSystemTasks } from "../../@redux/hooks";
|
||||
import { useReduxAction } from "../../@redux/hooks/base";
|
||||
import { AsyncOverlay, ContentHeader } from "../../components";
|
||||
import Table from "./table";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const SystemTasksView: FunctionComponent<Props> = () => {
|
||||
const tasks = useSystemTasks();
|
||||
const update = useReduxAction(systemMarkTasksDirty);
|
||||
|
||||
return (
|
||||
<AsyncOverlay ctx={tasks}>
|
||||
{({ content, state }) => (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Tasks - Bazarr (System)</title>
|
||||
</Helmet>
|
||||
<ContentHeader>
|
||||
<ContentHeader.Button
|
||||
updating={state === "loading"}
|
||||
icon={faSync}
|
||||
onClick={update}
|
||||
>
|
||||
Refresh
|
||||
</ContentHeader.Button>
|
||||
</ContentHeader>
|
||||
<Row>
|
||||
<Table tasks={content ?? []}></Table>
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
</AsyncOverlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemTasksView;
|
@ -1,65 +0,0 @@
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { capitalize } from "lodash";
|
||||
import React from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Column } from "react-table";
|
||||
import { dispatchTask } from "../../@modules/task";
|
||||
import { useIsGroupTaskRunning } from "../../@modules/task/hooks";
|
||||
import { createTask } from "../../@modules/task/utilities";
|
||||
import { AsyncPageTable, ContentHeader } from "../../components";
|
||||
|
||||
interface Props<T extends Wanted.Base> {
|
||||
type: "movies" | "series";
|
||||
columns: Column<T>[];
|
||||
state: Async.Entity<T>;
|
||||
loader: (params: Parameter.Range) => void;
|
||||
searchAll: () => Promise<void>;
|
||||
}
|
||||
|
||||
const TaskGroupName = "Searching wanted subtitles...";
|
||||
|
||||
function GenericWantedView<T extends Wanted.Base>({
|
||||
type,
|
||||
columns,
|
||||
state,
|
||||
loader,
|
||||
searchAll,
|
||||
}: Props<T>) {
|
||||
const typeName = capitalize(type);
|
||||
|
||||
const dataCount = Object.keys(state.content.entities).length;
|
||||
|
||||
const hasTask = useIsGroupTaskRunning(TaskGroupName);
|
||||
|
||||
return (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>Wanted {typeName} - Bazarr</title>
|
||||
</Helmet>
|
||||
<ContentHeader>
|
||||
<ContentHeader.Button
|
||||
disabled={dataCount === 0 || hasTask}
|
||||
onClick={() => {
|
||||
const task = createTask(type, undefined, searchAll);
|
||||
dispatchTask(TaskGroupName, [task], "Searching...");
|
||||
}}
|
||||
icon={faSearch}
|
||||
>
|
||||
Search All
|
||||
</ContentHeader.Button>
|
||||
</ContentHeader>
|
||||
<Row>
|
||||
<AsyncPageTable
|
||||
entity={state}
|
||||
loader={loader}
|
||||
emptyText={`No Missing ${typeName} Subtitles`}
|
||||
columns={columns}
|
||||
data={[]}
|
||||
></AsyncPageTable>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default GenericWantedView;
|
@ -1,9 +1,9 @@
|
||||
import { Entrance } from "index";
|
||||
import {} from "jest";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "../App";
|
||||
|
||||
it("renders", () => {
|
||||
const div = document.createElement("div");
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.render(<Entrance />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
type Request = (...args: any[]) => Promise<any>;
|
||||
type Return<T extends Request> = PromiseType<ReturnType<T>>;
|
||||
|
||||
export function useAsyncRequest<F extends Request>(
|
||||
request: F
|
||||
): [Async.Item<Return<F>>, (...args: Parameters<F>) => void] {
|
||||
const [state, setState] = useState<Async.Item<Return<F>>>({
|
||||
state: "uninitialized",
|
||||
content: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const requestRef = useRef(request);
|
||||
|
||||
const update = useCallback(
|
||||
(...args: Parameters<F>) => {
|
||||
setState((s) => ({ ...s, state: "loading" }));
|
||||
requestRef
|
||||
.current(...args)
|
||||
.then((res) =>
|
||||
setState({ state: "succeeded", content: res, error: null })
|
||||
)
|
||||
.catch((error) => setState((s) => ({ ...s, state: "failed", error })));
|
||||
},
|
||||
[requestRef]
|
||||
);
|
||||
|
||||
return [state, update];
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
import {
|
||||
QueryClient,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "react-query";
|
||||
import { usePaginationQuery } from "../queries/hooks";
|
||||
import { QueryKeys } from "../queries/keys";
|
||||
import api from "../raw";
|
||||
|
||||
const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => {
|
||||
episodes.forEach((item) => {
|
||||
client.setQueryData(
|
||||
[
|
||||
QueryKeys.Series,
|
||||
item.sonarrSeriesId,
|
||||
QueryKeys.Episodes,
|
||||
item.sonarrEpisodeId,
|
||||
],
|
||||
item
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export function useEpisodesByIds(ids: number[]) {
|
||||
const client = useQueryClient();
|
||||
return useQuery(
|
||||
[QueryKeys.Series, QueryKeys.Episodes, ids],
|
||||
() => api.episodes.byEpisodeId(ids),
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
cacheEpisodes(client, data);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useEpisodesBySeriesId(id: number) {
|
||||
const client = useQueryClient();
|
||||
return useQuery(
|
||||
[QueryKeys.Series, id, QueryKeys.Episodes, QueryKeys.All],
|
||||
() => api.episodes.bySeriesId([id]),
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
cacheEpisodes(client, data);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useEpisodeWantedPagination() {
|
||||
return usePaginationQuery([QueryKeys.Series, QueryKeys.Wanted], (param) =>
|
||||
api.episodes.wanted(param)
|
||||
);
|
||||
}
|
||||
|
||||
export function useEpisodeBlacklist() {
|
||||
return useQuery(
|
||||
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
||||
() => api.episodes.blacklist()
|
||||
);
|
||||
}
|
||||
|
||||
export function useEpisodeAddBlacklist() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
||||
(param: {
|
||||
seriesId: number;
|
||||
episodeId: number;
|
||||
form: FormType.AddBlacklist;
|
||||
}) => {
|
||||
const { seriesId, episodeId, form } = param;
|
||||
return api.episodes.addBlacklist(seriesId, episodeId, form);
|
||||
},
|
||||
{
|
||||
onSuccess: (_, { seriesId, episodeId }) => {
|
||||
client.invalidateQueries([QueryKeys.Series, QueryKeys.Blacklist]);
|
||||
client.invalidateQueries([QueryKeys.Series, seriesId]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useEpisodeDeleteBlacklist() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
|
||||
(param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
|
||||
api.episodes.deleteBlacklist(param.all, param.form),
|
||||
{
|
||||
onSuccess: (_, param) => {
|
||||
client.invalidateQueries([QueryKeys.Series, QueryKeys.Blacklist]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useEpisodeHistoryPagination() {
|
||||
return usePaginationQuery(
|
||||
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.History],
|
||||
(param) => api.episodes.history(param)
|
||||
);
|
||||
}
|
||||
|
||||
export function useEpisodeHistory(episodeId?: number) {
|
||||
return useQuery(
|
||||
[QueryKeys.Series, QueryKeys.Episodes, QueryKeys.History, episodeId],
|
||||
() => {
|
||||
if (episodeId) {
|
||||
return api.episodes.historyBy(episodeId);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { useQuery } from "react-query";
|
||||
import { QueryKeys } from "../queries/keys";
|
||||
import api from "../raw";
|
||||
|
||||
export function useHistoryStats(
|
||||
time: History.TimeFrameOptions,
|
||||
action: History.ActionOptions | null,
|
||||
provider: System.Provider | null,
|
||||
language: Language.Info | null
|
||||
) {
|
||||
return useQuery(
|
||||
[QueryKeys.System, QueryKeys.History, { time, action, provider, language }],
|
||||
() =>
|
||||
api.history.stats(
|
||||
time,
|
||||
action ?? undefined,
|
||||
provider?.name,
|
||||
language?.code2
|
||||
)
|
||||
);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
export * from "./episodes";
|
||||
export * from "./histories";
|
||||
export * from "./languages";
|
||||
export * from "./movies";
|
||||
export * from "./providers";
|
||||
export * from "./series";
|
||||
export * from "./status";
|
||||
export * from "./subtitles";
|
||||
export * from "./system";
|
@ -0,0 +1,23 @@
|
||||
import { useQuery } from "react-query";
|
||||
import { QueryKeys } from "../queries/keys";
|
||||
import api from "../raw";
|
||||
|
||||
export function useLanguages(history?: boolean) {
|
||||
return useQuery(
|
||||
[QueryKeys.System, QueryKeys.Languages, history ?? false],
|
||||
() => api.system.languages(history),
|
||||
{
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useLanguageProfiles() {
|
||||
return useQuery(
|
||||
[QueryKeys.System, QueryKeys.LanguagesProfiles],
|
||||
() => api.system.languagesProfileList(),
|
||||
{
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
import {
|
||||
QueryClient,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "react-query";
|
||||
import { usePaginationQuery } from "../queries/hooks";
|
||||
import { QueryKeys } from "../queries/keys";
|
||||
import api from "../raw";
|
||||
|
||||
const cacheMovies = (client: QueryClient, movies: Item.Movie[]) => {
|
||||
movies.forEach((item) => {
|
||||
client.setQueryData([QueryKeys.Movies, item.radarrId], item);
|
||||
});
|
||||
};
|
||||
|
||||
export function useMoviesByIds(ids: number[]) {
|
||||
const client = useQueryClient();
|
||||
return useQuery([QueryKeys.Movies, ...ids], () => api.movies.movies(ids), {
|
||||
onSuccess: (data) => {
|
||||
cacheMovies(client, data);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useMovieById(id: number) {
|
||||
return useQuery([QueryKeys.Movies, id], async () => {
|
||||
const response = await api.movies.movies([id]);
|
||||
return response.length > 0 ? response[0] : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
export function useMovies() {
|
||||
const client = useQueryClient();
|
||||
return useQuery(
|
||||
[QueryKeys.Movies, QueryKeys.All],
|
||||
() => api.movies.movies(),
|
||||
{
|
||||
enabled: false,
|
||||
onSuccess: (data) => {
|
||||
cacheMovies(client, data);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useMoviesPagination() {
|
||||
return usePaginationQuery([QueryKeys.Movies], (param) =>
|
||||
api.movies.moviesBy(param)
|
||||
);
|
||||
}
|
||||
|
||||
export function useMovieModification() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.Movies],
|
||||
(form: FormType.ModifyItem) => api.movies.modify(form),
|
||||
{
|
||||
onSuccess: (_, form) => {
|
||||
form.id.forEach((v) => {
|
||||
client.invalidateQueries([QueryKeys.Movies, v]);
|
||||
});
|
||||
// TODO: query less
|
||||
client.invalidateQueries([QueryKeys.Movies]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useMovieAction() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.Actions, QueryKeys.Movies],
|
||||
(form: FormType.MoviesAction) => api.movies.action(form),
|
||||
{
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries([QueryKeys.Movies]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useMovieWantedPagination() {
|
||||
return usePaginationQuery([QueryKeys.Movies, QueryKeys.Wanted], (param) =>
|
||||
api.movies.wanted(param)
|
||||
);
|
||||
}
|
||||
|
||||
export function useMovieBlacklist() {
|
||||
return useQuery([QueryKeys.Movies, QueryKeys.Blacklist], () =>
|
||||
api.movies.blacklist()
|
||||
);
|
||||
}
|
||||
|
||||
export function useMovieAddBlacklist() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.Movies, QueryKeys.Blacklist],
|
||||
(param: { id: number; form: FormType.AddBlacklist }) => {
|
||||
const { id, form } = param;
|
||||
return api.movies.addBlacklist(id, form);
|
||||
},
|
||||
{
|
||||
onSuccess: (_, { id }) => {
|
||||
client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]);
|
||||
client.invalidateQueries([QueryKeys.Movies, id]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useMovieDeleteBlacklist() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.Movies, QueryKeys.Blacklist],
|
||||
(param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
|
||||
api.movies.deleteBlacklist(param.all, param.form),
|
||||
{
|
||||
onSuccess: (_, param) => {
|
||||
client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useMovieHistoryPagination() {
|
||||
return usePaginationQuery([QueryKeys.Movies, QueryKeys.History], (param) =>
|
||||
api.movies.history(param)
|
||||
);
|
||||
}
|
||||
|
||||
export function useMovieHistory(radarrId?: number) {
|
||||
return useQuery([QueryKeys.Movies, QueryKeys.History, radarrId], () => {
|
||||
if (radarrId) {
|
||||
return api.movies.historyBy(radarrId);
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { QueryKeys } from "../queries/keys";
|
||||
import api from "../raw";
|
||||
|
||||
export function useSystemProviders(history?: boolean) {
|
||||
return useQuery(
|
||||
[QueryKeys.System, QueryKeys.Providers, history ?? false],
|
||||
() => api.providers.providers(history)
|
||||
);
|
||||
}
|
||||
|
||||
export function useMoviesProvider(radarrId?: number) {
|
||||
return useQuery(
|
||||
[QueryKeys.System, QueryKeys.Providers, QueryKeys.Movies, radarrId],
|
||||
() => {
|
||||
if (radarrId) {
|
||||
return api.providers.movies(radarrId);
|
||||
}
|
||||
},
|
||||
{
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useEpisodesProvider(episodeId?: number) {
|
||||
return useQuery(
|
||||
[QueryKeys.System, QueryKeys.Providers, QueryKeys.Episodes, episodeId],
|
||||
() => {
|
||||
if (episodeId) {
|
||||
return api.providers.episodes(episodeId);
|
||||
}
|
||||
},
|
||||
{
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useResetProvider() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.System, QueryKeys.Providers],
|
||||
() => api.providers.reset(),
|
||||
{
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Providers]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useDownloadEpisodeSubtitles() {
|
||||
const client = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
[
|
||||
QueryKeys.System,
|
||||
QueryKeys.Providers,
|
||||
QueryKeys.Subtitles,
|
||||
QueryKeys.Episodes,
|
||||
],
|
||||
(param: {
|
||||
seriesId: number;
|
||||
episodeId: number;
|
||||
form: FormType.ManualDownload;
|
||||
}) =>
|
||||
api.providers.downloadEpisodeSubtitle(
|
||||
param.seriesId,
|
||||
param.episodeId,
|
||||
param.form
|
||||
),
|
||||
{
|
||||
onSuccess: (_, param) => {
|
||||
client.invalidateQueries([QueryKeys.Series, param.seriesId]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useDownloadMovieSubtitles() {
|
||||
const client = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
[
|
||||
QueryKeys.System,
|
||||
QueryKeys.Providers,
|
||||
QueryKeys.Subtitles,
|
||||
QueryKeys.Movies,
|
||||
],
|
||||
(param: { radarrId: number; form: FormType.ManualDownload }) =>
|
||||
api.providers.downloadMovieSubtitle(param.radarrId, param.form),
|
||||
{
|
||||
onSuccess: (_, param) => {
|
||||
client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import {
|
||||
QueryClient,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "react-query";
|
||||
import { usePaginationQuery } from "../queries/hooks";
|
||||
import { QueryKeys } from "../queries/keys";
|
||||
import api from "../raw";
|
||||
|
||||
function cacheSeries(client: QueryClient, series: Item.Series[]) {
|
||||
series.forEach((item) => {
|
||||
client.setQueryData([QueryKeys.Series, item.sonarrSeriesId], item);
|
||||
});
|
||||
}
|
||||
|
||||
export function useSeriesByIds(ids: number[]) {
|
||||
const client = useQueryClient();
|
||||
return useQuery([QueryKeys.Series, ...ids], () => api.series.series(ids), {
|
||||
onSuccess: (data) => {
|
||||
cacheSeries(client, data);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSeriesById(id: number) {
|
||||
return useQuery([QueryKeys.Series, id], async () => {
|
||||
const response = await api.series.series([id]);
|
||||
return response.length > 0 ? response[0] : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
export function useSeries() {
|
||||
const client = useQueryClient();
|
||||
return useQuery(
|
||||
[QueryKeys.Series, QueryKeys.All],
|
||||
() => api.series.series(),
|
||||
{
|
||||
enabled: false,
|
||||
onSuccess: (data) => {
|
||||
cacheSeries(client, data);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useSeriesPagination() {
|
||||
return usePaginationQuery([QueryKeys.Series], (param) =>
|
||||
api.series.seriesBy(param)
|
||||
);
|
||||
}
|
||||
|
||||
export function useSeriesModification() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.Series],
|
||||
(form: FormType.ModifyItem) => api.series.modify(form),
|
||||
{
|
||||
onSuccess: (_, form) => {
|
||||
form.id.forEach((v) => {
|
||||
client.invalidateQueries([QueryKeys.Series, v]);
|
||||
});
|
||||
client.invalidateQueries([QueryKeys.Series]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useSeriesAction() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.Actions, QueryKeys.Series],
|
||||
(form: FormType.SeriesAction) => api.series.action(form),
|
||||
{
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries([QueryKeys.Series]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { useIsMutating } from "react-query";
|
||||
import { QueryKeys } from "../queries/keys";
|
||||
|
||||
export function useIsAnyActionRunning() {
|
||||
return useIsMutating([QueryKeys.Actions]) > 0;
|
||||
}
|
||||
|
||||
export function useIsMovieActionRunning() {
|
||||
return useIsMutating([QueryKeys.Actions, QueryKeys.Movies]) > 0;
|
||||
}
|
||||
|
||||
export function useIsSeriesActionRunning() {
|
||||
return useIsMutating([QueryKeys.Actions, QueryKeys.Series]) > 0;
|
||||
}
|
||||
|
||||
export function useIsAnyMutationRunning() {
|
||||
return useIsMutating() > 0;
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { QueryKeys } from "../queries/keys";
|
||||
import api from "../raw";
|
||||
|
||||
export function useSubtitleAction() {
|
||||
const client = useQueryClient();
|
||||
interface Param {
|
||||
action: string;
|
||||
form: FormType.ModifySubtitle;
|
||||
}
|
||||
return useMutation(
|
||||
[QueryKeys.Subtitles],
|
||||
(param: Param) => api.subtitles.modify(param.action, param.form),
|
||||
{
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries([QueryKeys.History]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useEpisodeSubtitleModification() {
|
||||
const client = useQueryClient();
|
||||
|
||||
interface Param<T> {
|
||||
seriesId: number;
|
||||
episodeId: number;
|
||||
form: T;
|
||||
}
|
||||
|
||||
const download = useMutation(
|
||||
[QueryKeys.Subtitles, QueryKeys.Episodes],
|
||||
(param: Param<FormType.Subtitle>) =>
|
||||
api.episodes.downloadSubtitles(
|
||||
param.seriesId,
|
||||
param.episodeId,
|
||||
param.form
|
||||
),
|
||||
{
|
||||
onSuccess: (_, param) => {
|
||||
client.invalidateQueries([QueryKeys.Series, param.seriesId]);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const remove = useMutation(
|
||||
[QueryKeys.Subtitles, QueryKeys.Episodes],
|
||||
(param: Param<FormType.DeleteSubtitle>) =>
|
||||
api.episodes.deleteSubtitles(param.seriesId, param.episodeId, param.form),
|
||||
{
|
||||
onSuccess: (_, param) => {
|
||||
client.invalidateQueries([QueryKeys.Series, param.seriesId]);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const upload = useMutation(
|
||||
[QueryKeys.Subtitles, QueryKeys.Episodes],
|
||||
(param: Param<FormType.UploadSubtitle>) =>
|
||||
api.episodes.uploadSubtitles(param.seriesId, param.episodeId, param.form),
|
||||
{
|
||||
onSuccess: (_, { seriesId }) => {
|
||||
client.invalidateQueries([QueryKeys.Series, seriesId]);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { download, remove, upload };
|
||||
}
|
||||
|
||||
export function useMovieSubtitleModification() {
|
||||
const client = useQueryClient();
|
||||
|
||||
interface Param<T> {
|
||||
radarrId: number;
|
||||
form: T;
|
||||
}
|
||||
|
||||
const download = useMutation(
|
||||
[QueryKeys.Subtitles, QueryKeys.Movies],
|
||||
(param: Param<FormType.Subtitle>) =>
|
||||
api.movies.downloadSubtitles(param.radarrId, param.form),
|
||||
{
|
||||
onSuccess: (_, param) => {
|
||||
client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const remove = useMutation(
|
||||
[QueryKeys.Subtitles, QueryKeys.Movies],
|
||||
(param: Param<FormType.DeleteSubtitle>) =>
|
||||
api.movies.deleteSubtitles(param.radarrId, param.form),
|
||||
{
|
||||
onSuccess: (_, param) => {
|
||||
client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const upload = useMutation(
|
||||
[QueryKeys.Subtitles, QueryKeys.Movies],
|
||||
(param: Param<FormType.UploadSubtitle>) =>
|
||||
api.movies.uploadSubtitles(param.radarrId, param.form),
|
||||
{
|
||||
onSuccess: (_, { radarrId }) => {
|
||||
client.invalidateQueries([QueryKeys.Movies, radarrId]);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { download, remove, upload };
|
||||
}
|
||||
|
||||
export function useSubtitleInfos(names: string[]) {
|
||||
return useQuery([QueryKeys.Subtitles, QueryKeys.Infos, names], () =>
|
||||
api.subtitles.info(names)
|
||||
);
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
import { useMemo } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { setUnauthenticated } from "../../@redux/actions";
|
||||
import store from "../../@redux/store";
|
||||
import { QueryKeys } from "../queries/keys";
|
||||
import api from "../raw";
|
||||
|
||||
export function useBadges() {
|
||||
return useQuery([QueryKeys.System, QueryKeys.Badges], () => api.badges.all());
|
||||
}
|
||||
|
||||
export function useFileSystem(
|
||||
type: "bazarr" | "sonarr" | "radarr",
|
||||
path: string,
|
||||
enabled: boolean
|
||||
) {
|
||||
return useQuery(
|
||||
[QueryKeys.FileSystem, type, path],
|
||||
() => {
|
||||
if (type === "bazarr") {
|
||||
return api.files.bazarr(path);
|
||||
} else if (type === "radarr") {
|
||||
return api.files.radarr(path);
|
||||
} else if (type === "sonarr") {
|
||||
return api.files.sonarr(path);
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useSystemSettings() {
|
||||
return useQuery(
|
||||
[QueryKeys.System, QueryKeys.Settings],
|
||||
() => api.system.settings(),
|
||||
{
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useSettingsMutation() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.System, QueryKeys.Settings],
|
||||
(data: LooseObject) => api.system.updateSettings(data),
|
||||
{
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries([QueryKeys.System]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useServerSearch(query: string, enabled: boolean) {
|
||||
return useQuery(
|
||||
[QueryKeys.System, QueryKeys.Search, query],
|
||||
() => api.system.search(query),
|
||||
{
|
||||
enabled,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useSystemLogs() {
|
||||
return useQuery([QueryKeys.System, QueryKeys.Logs], () => api.system.logs(), {
|
||||
refetchOnWindowFocus: "always",
|
||||
refetchInterval: 1000 * 60,
|
||||
staleTime: 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteLogs() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.System, QueryKeys.Logs],
|
||||
() => api.system.deleteLogs(),
|
||||
{
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Logs]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useSystemTasks() {
|
||||
return useQuery(
|
||||
[QueryKeys.System, QueryKeys.Tasks],
|
||||
() => api.system.tasks(),
|
||||
{
|
||||
refetchOnWindowFocus: "always",
|
||||
refetchInterval: 1000 * 60,
|
||||
staleTime: 1000 * 10,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useRunTask() {
|
||||
const client = useQueryClient();
|
||||
return useMutation(
|
||||
[QueryKeys.System, QueryKeys.Tasks],
|
||||
(id: string) => api.system.runTask(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries([QueryKeys.System, QueryKeys.Tasks]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useSystemStatus() {
|
||||
return useQuery([QueryKeys.System, "status"], () => api.system.status());
|
||||
}
|
||||
|
||||
export function useSystemHealth() {
|
||||
return useQuery([QueryKeys.System, "health"], () => api.system.health());
|
||||
}
|
||||
|
||||
export function useSystemReleases() {
|
||||
return useQuery([QueryKeys.System, "releases"], () => api.system.releases());
|
||||
}
|
||||
|
||||
export function useSystem() {
|
||||
const client = useQueryClient();
|
||||
const { mutate: logout, isLoading: isLoggingOut } = useMutation(
|
||||
[QueryKeys.System, QueryKeys.Actions],
|
||||
() => api.system.logout(),
|
||||
{
|
||||
onSuccess: () => {
|
||||
store.dispatch(setUnauthenticated());
|
||||
client.clear();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { mutate: login, isLoading: isLoggingIn } = useMutation(
|
||||
[QueryKeys.System, QueryKeys.Actions],
|
||||
(param: { username: string; password: string }) =>
|
||||
api.system.login(param.username, param.password),
|
||||
{
|
||||
onSuccess: () => {
|
||||
window.location.reload();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { mutate: shutdown, isLoading: isShuttingDown } = useMutation(
|
||||
[QueryKeys.System, QueryKeys.Actions],
|
||||
() => api.system.shutdown(),
|
||||
{
|
||||
onSuccess: () => {
|
||||
client.clear();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { mutate: restart, isLoading: isRestarting } = useMutation(
|
||||
[QueryKeys.System, QueryKeys.Actions],
|
||||
() => api.system.restart(),
|
||||
{
|
||||
onSuccess: () => {
|
||||
client.clear();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
logout,
|
||||
shutdown,
|
||||
restart,
|
||||
login,
|
||||
isWorking: isLoggingOut || isShuttingDown || isRestarting || isLoggingIn,
|
||||
}),
|
||||
[
|
||||
isLoggingIn,
|
||||
isLoggingOut,
|
||||
isRestarting,
|
||||
isShuttingDown,
|
||||
login,
|
||||
logout,
|
||||
restart,
|
||||
shutdown,
|
||||
]
|
||||
);
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
QueryKey,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
UseQueryResult,
|
||||
} from "react-query";
|
||||
import { GetItemId } from "utilities";
|
||||
import { usePageSize } from "utilities/storage";
|
||||
import { QueryKeys } from "./keys";
|
||||
|
||||
export type UsePaginationQueryResult<T extends object> = UseQueryResult<
|
||||
DataWrapperWithTotal<T>
|
||||
> & {
|
||||
controls: {
|
||||
previousPage: () => void;
|
||||
nextPage: () => void;
|
||||
gotoPage: (index: number) => void;
|
||||
};
|
||||
paginationStatus: {
|
||||
totalCount: number;
|
||||
pageSize: number;
|
||||
pageCount: number;
|
||||
page: number;
|
||||
canPrevious: boolean;
|
||||
canNext: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export function usePaginationQuery<
|
||||
TObject extends object = object,
|
||||
TQueryKey extends QueryKey = QueryKey
|
||||
>(
|
||||
queryKey: TQueryKey,
|
||||
queryFn: RangeQuery<TObject>
|
||||
): UsePaginationQueryResult<TObject> {
|
||||
const client = useQueryClient();
|
||||
|
||||
const [page, setIndex] = useState(0);
|
||||
const [pageSize] = usePageSize();
|
||||
|
||||
const start = page * pageSize;
|
||||
|
||||
const results = useQuery(
|
||||
[...queryKey, QueryKeys.Range, { start, size: pageSize }],
|
||||
() => {
|
||||
const param: Parameter.Range = {
|
||||
start,
|
||||
length: pageSize,
|
||||
};
|
||||
return queryFn(param);
|
||||
},
|
||||
{
|
||||
onSuccess: ({ data }) => {
|
||||
data.forEach((item) => {
|
||||
const id = GetItemId(item);
|
||||
if (id) {
|
||||
client.setQueryData([...queryKey, id], item);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { data } = results;
|
||||
|
||||
const totalCount = data?.total ?? 0;
|
||||
const pageCount = Math.ceil(totalCount / pageSize);
|
||||
|
||||
const previousPage = useCallback(() => {
|
||||
setIndex((index) => Math.max(0, index - 1));
|
||||
}, []);
|
||||
|
||||
const nextPage = useCallback(() => {
|
||||
if (pageCount > 0) {
|
||||
setIndex((index) => Math.min(pageCount - 1, index + 1));
|
||||
}
|
||||
}, [pageCount]);
|
||||
|
||||
const gotoPage = useCallback(
|
||||
(idx: number) => {
|
||||
if (idx >= 0 && idx < pageCount) {
|
||||
setIndex(idx);
|
||||
}
|
||||
},
|
||||
[pageCount]
|
||||
);
|
||||
|
||||
// Reset page index if we out of bound
|
||||
useEffect(() => {
|
||||
if (pageCount === 0) return;
|
||||
|
||||
if (page >= pageCount) {
|
||||
setIndex(pageCount - 1);
|
||||
} else if (page < 0) {
|
||||
setIndex(0);
|
||||
}
|
||||
}, [page, pageCount]);
|
||||
|
||||
return {
|
||||
...results,
|
||||
paginationStatus: {
|
||||
totalCount,
|
||||
pageCount,
|
||||
pageSize,
|
||||
page,
|
||||
canPrevious: page > 0,
|
||||
canNext: page < pageCount - 1,
|
||||
},
|
||||
controls: {
|
||||
gotoPage,
|
||||
previousPage,
|
||||
nextPage,
|
||||
},
|
||||
};
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { QueryClient } from "react-query";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
staleTime: 1000 * 60,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default queryClient;
|
@ -0,0 +1,23 @@
|
||||
export enum QueryKeys {
|
||||
Movies = "movies",
|
||||
Episodes = "episodes",
|
||||
Series = "series",
|
||||
Badges = "badges",
|
||||
FileSystem = "file-system",
|
||||
System = "system",
|
||||
Settings = "settings",
|
||||
Subtitles = "subtitles",
|
||||
Providers = "providers",
|
||||
Languages = "languages",
|
||||
LanguagesProfiles = "languages-profiles",
|
||||
Blacklist = "blacklist",
|
||||
Search = "search",
|
||||
Actions = "actions",
|
||||
Tasks = "tasks",
|
||||
Logs = "logs",
|
||||
Infos = "infos",
|
||||
History = "history",
|
||||
Wanted = "wanted",
|
||||
Range = "range",
|
||||
All = "all",
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import badges from "./badges";
|
||||
import episodes from "./episodes";
|
||||
import files from "./files";
|
||||
import history from "./history";
|
||||
import movies from "./movies";
|
||||
import providers from "./providers";
|
||||
import series from "./series";
|
||||
import subtitles from "./subtitles";
|
||||
import system from "./system";
|
||||
import utils from "./utils";
|
||||
|
||||
const api = {
|
||||
badges,
|
||||
episodes,
|
||||
files,
|
||||
movies,
|
||||
series,
|
||||
providers,
|
||||
history,
|
||||
subtitles,
|
||||
system,
|
||||
utils,
|
||||
};
|
||||
|
||||
export default api;
|
@ -1,5 +1,5 @@
|
||||
import UIError from "pages/UIError";
|
||||
import React from "react";
|
||||
import UIError from "./UIError";
|
||||
|
||||
interface State {
|
||||
error: Error | null;
|
@ -1,7 +1,7 @@
|
||||
import { faFileExcel } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { AsyncButton } from "../../components";
|
||||
import { AsyncButton } from "..";
|
||||
|
||||
interface Props {
|
||||
history: History.Base;
|
@ -1,128 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { PluginHook, TableOptions, useTable } from "react-table";
|
||||
import { LoadingIndicator } from "..";
|
||||
import { usePageSize } from "../../@storage/local";
|
||||
import {
|
||||
ScrollToTop,
|
||||
useEntityByRange,
|
||||
useIsEntityLoaded,
|
||||
} from "../../utilities";
|
||||
import BaseTable, { TableStyleProps, useStyleAndOptions } from "./BaseTable";
|
||||
import PageControl from "./PageControl";
|
||||
import { useDefaultSettings } from "./plugins";
|
||||
|
||||
function useEntityPagination<T>(
|
||||
entity: Async.Entity<T>,
|
||||
loader: (range: Parameter.Range) => void,
|
||||
start: number,
|
||||
end: number
|
||||
): T[] {
|
||||
const { state, content } = entity;
|
||||
|
||||
const needInit = state === "uninitialized";
|
||||
const hasEmpty = useIsEntityLoaded(content, start, end) === false;
|
||||
|
||||
useEffect(() => {
|
||||
if (needInit || hasEmpty) {
|
||||
const length = end - start;
|
||||
loader({ start, length });
|
||||
}
|
||||
});
|
||||
|
||||
return useEntityByRange(content, start, end);
|
||||
}
|
||||
|
||||
type Props<T extends object> = TableOptions<T> &
|
||||
TableStyleProps<T> & {
|
||||
plugins?: PluginHook<T>[];
|
||||
entity: Async.Entity<T>;
|
||||
loader: (params: Parameter.Range) => void;
|
||||
};
|
||||
|
||||
export default function AsyncPageTable<T extends object>(props: Props<T>) {
|
||||
const { entity, plugins, loader, ...remain } = props;
|
||||
const { style, options } = useStyleAndOptions(remain);
|
||||
|
||||
const {
|
||||
state,
|
||||
content: { ids },
|
||||
} = entity;
|
||||
|
||||
// Impl a new pagination system instead of hacking into existing one
|
||||
const [pageIndex, setIndex] = useState(0);
|
||||
const [pageSize] = usePageSize();
|
||||
const totalRows = ids.length;
|
||||
const pageCount = Math.ceil(totalRows / pageSize);
|
||||
|
||||
const pageStart = pageIndex * pageSize;
|
||||
const pageEnd = pageStart + pageSize;
|
||||
|
||||
const data = useEntityPagination(entity, loader, pageStart, pageEnd);
|
||||
|
||||
const instance = useTable(
|
||||
{
|
||||
...options,
|
||||
data,
|
||||
},
|
||||
useDefaultSettings,
|
||||
...(plugins ?? [])
|
||||
);
|
||||
|
||||
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
|
||||
instance;
|
||||
|
||||
const previous = useCallback(() => {
|
||||
setIndex((idx) => idx - 1);
|
||||
}, []);
|
||||
|
||||
const next = useCallback(() => {
|
||||
setIndex((idx) => idx + 1);
|
||||
}, []);
|
||||
|
||||
const goto = useCallback((idx: number) => {
|
||||
setIndex(idx);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
ScrollToTop();
|
||||
}, [pageIndex]);
|
||||
|
||||
// Reset page index if we out of bound
|
||||
useEffect(() => {
|
||||
if (pageCount === 0) return;
|
||||
|
||||
if (pageIndex >= pageCount) {
|
||||
setIndex(pageCount - 1);
|
||||
} else if (pageIndex < 0) {
|
||||
setIndex(0);
|
||||
}
|
||||
}, [pageIndex, pageCount]);
|
||||
|
||||
if ((state === "loading" && data.length === 0) || state === "uninitialized") {
|
||||
return <LoadingIndicator></LoadingIndicator>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<BaseTable
|
||||
{...style}
|
||||
headers={headerGroups}
|
||||
rows={rows}
|
||||
prepareRow={prepareRow}
|
||||
tableProps={getTableProps()}
|
||||
tableBodyProps={getTableBodyProps()}
|
||||
></BaseTable>
|
||||
<PageControl
|
||||
count={pageCount}
|
||||
index={pageIndex}
|
||||
size={pageSize}
|
||||
total={totalRows}
|
||||
canPrevious={pageIndex > 0}
|
||||
canNext={pageIndex < pageCount - 1}
|
||||
previous={previous}
|
||||
next={next}
|
||||
goto={goto}
|
||||
></PageControl>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import { UsePaginationQueryResult } from "apis/queries/hooks";
|
||||
import React, { useEffect } from "react";
|
||||
import { PluginHook, TableOptions, useTable } from "react-table";
|
||||
import { ScrollToTop } from "utilities";
|
||||
import { LoadingIndicator } from "..";
|
||||
import BaseTable, { TableStyleProps, useStyleAndOptions } from "./BaseTable";
|
||||
import PageControl from "./PageControl";
|
||||
import { useDefaultSettings } from "./plugins";
|
||||
|
||||
type Props<T extends object> = TableOptions<T> &
|
||||
TableStyleProps<T> & {
|
||||
plugins?: PluginHook<T>[];
|
||||
query: UsePaginationQueryResult<T>;
|
||||
};
|
||||
|
||||
export default function QueryPageTable<T extends object>(props: Props<T>) {
|
||||
const { plugins, query, ...remain } = props;
|
||||
const { style, options } = useStyleAndOptions(remain);
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
paginationStatus: {
|
||||
page,
|
||||
pageCount,
|
||||
totalCount,
|
||||
canPrevious,
|
||||
canNext,
|
||||
pageSize,
|
||||
},
|
||||
controls: { previousPage, nextPage, gotoPage },
|
||||
} = query;
|
||||
|
||||
const instance = useTable(
|
||||
{
|
||||
...options,
|
||||
data: data?.data ?? [],
|
||||
},
|
||||
useDefaultSettings,
|
||||
...(plugins ?? [])
|
||||
);
|
||||
|
||||
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
|
||||
instance;
|
||||
|
||||
useEffect(() => {
|
||||
ScrollToTop();
|
||||
}, [page]);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingIndicator></LoadingIndicator>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<BaseTable
|
||||
{...style}
|
||||
headers={headerGroups}
|
||||
rows={rows}
|
||||
prepareRow={prepareRow}
|
||||
tableProps={getTableProps()}
|
||||
tableBodyProps={getTableBodyProps()}
|
||||
></BaseTable>
|
||||
<PageControl
|
||||
count={pageCount}
|
||||
index={page}
|
||||
size={pageSize}
|
||||
total={totalCount}
|
||||
canPrevious={canPrevious}
|
||||
canNext={canNext}
|
||||
previous={previousPage}
|
||||
next={nextPage}
|
||||
goto={gotoPage}
|
||||
></PageControl>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export { default as AsyncPageTable } from "./AsyncPageTable";
|
||||
export { default as GroupTable } from "./GroupTable";
|
||||
export { default as PageTable } from "./PageTable";
|
||||
export { default as QueryPageTable } from "./QueryPageTable";
|
||||
export { default as SimpleTable } from "./SimpleTable";
|
||||
|
@ -0,0 +1,36 @@
|
||||
import { UsePaginationQueryResult } from "apis/queries/hooks";
|
||||
import React from "react";
|
||||
import { Container, Row } from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Column } from "react-table";
|
||||
import { QueryPageTable } from "..";
|
||||
|
||||
interface Props<T extends History.Base> {
|
||||
name: string;
|
||||
query: UsePaginationQueryResult<T>;
|
||||
columns: Column<T>[];
|
||||
}
|
||||
|
||||
function HistoryView<T extends History.Base = History.Base>({
|
||||
columns,
|
||||
name,
|
||||
query,
|
||||
}: Props<T>) {
|
||||
return (
|
||||
<Container fluid>
|
||||
<Helmet>
|
||||
<title>{name} History - Bazarr</title>
|
||||
</Helmet>
|
||||
<Row>
|
||||
<QueryPageTable
|
||||
emptyText={`Nothing Found in ${name} History`}
|
||||
columns={columns}
|
||||
query={query}
|
||||
data={[]}
|
||||
></QueryPageTable>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default HistoryView;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue