Fix issues in UI

pull/1509/head
LASER-Yi 3 years ago
parent 6eb14a2754
commit 42d19eaa42

@ -5,7 +5,7 @@ import {
createReducer,
} from "@reduxjs/toolkit";
import {} from "jest";
import { differenceWith, intersectionWith, isString } from "lodash";
import { differenceWith, intersectionWith, isString, uniq } from "lodash";
import { defaultList, defaultState, TestType } from "../tests/helper";
import { createAsyncEntityReducer } from "../utils/factory";
@ -181,6 +181,7 @@ it("entity update all resolved", async () => {
const id = v.id.toString();
expect(entities.content.ids[index]).toEqual(id);
expect(entities.content.entities[id]).toEqual(v);
expect(entities.didLoaded).toContain(id);
});
});
});
@ -224,6 +225,9 @@ it("delete entity item", async () => {
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);
@ -242,6 +246,7 @@ it("entity update by range", async () => {
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");
@ -258,12 +263,24 @@ it("entity update by duplicative range", async () => {
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]));

@ -69,6 +69,7 @@ it("list all uninitialized -> succeeded", async () => {
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");
});
@ -109,6 +110,7 @@ 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");
@ -147,6 +149,7 @@ it("list ids update duplicative data", async () => {
await store.dispatch(idsResolved([2, 3]));
use((list) => {
expect(list.content).toHaveLength(4);
expect(list.didLoaded).toHaveLength(4);
expect(list.state).toEqual("succeeded");
});
});
@ -156,6 +159,7 @@ it("list ids update new data", async () => {
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");

@ -34,9 +34,10 @@ export function useSeries() {
export function useSerieBy(id: number) {
const series = useSerieEntities();
const action = useReduxAction(seriesUpdateById);
const serie = useEntityItemById(series, id.toString());
const serie = useEntityItemById(series, String(id));
const update = useCallback(() => {
console.log("try loading", id);
if (!isNaN(id)) {
action([id]);
}

@ -14,7 +14,7 @@ import {
movieUpdateWantedById,
movieUpdateWantedByRange,
} from "../actions";
import { AsyncUtility } from "../utils/async";
import { AsyncUtility } from "../utils";
import {
createAsyncEntityReducer,
createAsyncItemReducer,

@ -18,7 +18,7 @@ import {
seriesUpdateWantedById,
seriesUpdateWantedByRange,
} from "../actions";
import { AsyncReducer, AsyncUtility } from "../utils/async";
import { AsyncUtility, ReducerUtility } from "../utils";
import {
createAsyncEntityReducer,
createAsyncItemReducer,
@ -53,7 +53,7 @@ const reducer = createReducer(defaultSeries, (builder) => {
const series = state.seriesList;
const dirtyIds = action.payload.map(String);
AsyncReducer.markDirty(series, dirtyIds);
ReducerUtility.markDirty(series, dirtyIds);
// Update episode list
const episodes = state.episodeList;
@ -62,7 +62,7 @@ const reducer = createReducer(defaultSeries, (builder) => {
.filter((v) => dirtyIdsSet.has(v.sonarrSeriesId.toString()))
.map((v) => String(v.sonarrEpisodeId));
AsyncReducer.markDirty(episodes, dirtyEpisodeIds);
ReducerUtility.markDirty(episodes, dirtyEpisodeIds);
});
createAsyncEntityReducer(builder, (s) => s.wantedEpisodesList, {

@ -11,7 +11,7 @@ import {
systemUpdateStatus,
systemUpdateTasks,
} from "../actions";
import { AsyncUtility } from "../utils/async";
import { AsyncUtility } from "../utils";
import { createAsyncItemReducer } from "../utils/factory";
interface System {

@ -1,4 +1,4 @@
import { AsyncUtility } from "../utils/async";
import { AsyncUtility } from "../utils";
export interface TestType {
id: number;

@ -1,5 +1,5 @@
import {} from "jest";
import { AsyncUtility } from "../async";
import { AsyncUtility } from "..";
interface AsyncTest {
id: string;

@ -14,8 +14,8 @@ import {
pullAll,
pullAllWith,
} from "lodash";
import { ReducerUtility } from ".";
import { conditionalLog } from "../../utilites/logger";
import { AsyncReducer } from "./async";
interface ActionParam<T, ID = null> {
range?: AsyncThunk<T, Parameter.Range, {}>;
@ -81,6 +81,8 @@ export function createAsyncListReducer<S, T, ID extends Async.IdType>(
meta: { arg },
} = action;
const strIds = arg.map(String);
const keyName = list.keyName as keyof T;
action.payload.forEach((v) => {
@ -92,7 +94,8 @@ export function createAsyncListReducer<S, T, ID extends Async.IdType>(
}
});
AsyncReducer.updateDirty(list, arg.map(String));
ReducerUtility.updateDirty(list, strIds);
ReducerUtility.updateDidLoaded(list, strIds);
})
.addCase(ids.rejected, (state, action) => {
const list = getList(state);
@ -111,7 +114,8 @@ export function createAsyncListReducer<S, T, ID extends Async.IdType>(
return String((lhs as T)[keyName]) === rhs;
});
AsyncReducer.removeDirty(list, removeIds);
ReducerUtility.removeDirty(list, removeIds);
ReducerUtility.removeDidLoaded(list, removeIds);
});
all &&
@ -126,6 +130,11 @@ export function createAsyncListReducer<S, T, ID extends Async.IdType>(
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);
@ -136,7 +145,7 @@ export function createAsyncListReducer<S, T, ID extends Async.IdType>(
dirty &&
builder.addCase(dirty, (state, action) => {
const list = getList(state);
AsyncReducer.markDirty(list, action.payload.map(String));
ReducerUtility.markDirty(list, action.payload.map(String));
});
}
@ -177,15 +186,22 @@ export function createAsyncEntityReducer<S, T, ID extends Async.IdType>(
checkSizeUpdate(entity, total);
const idsToUpdate = data.map((v) => String(v[keyName]));
entity.content.ids.splice(start, length, ...idsToUpdate);
data.forEach((v) => {
const key = String(v[keyName]);
entity.content.entities[key] = v as Draft<T>;
});
AsyncReducer.updateDirty(entity, idsToUpdate);
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);
@ -233,7 +249,10 @@ export function createAsyncEntityReducer<S, T, ID extends Async.IdType>(
entity.content.entities[key] = v as Draft<T>;
});
AsyncReducer.updateDirty(entity, arg.map(String));
const allIds = arg.map(String);
ReducerUtility.updateDirty(entity, allIds);
ReducerUtility.updateDidLoaded(entity, allIds);
})
.addCase(ids.rejected, (state, action) => {
const entity = getEntity(state);
@ -251,7 +270,9 @@ export function createAsyncEntityReducer<S, T, ID extends Async.IdType>(
const idsToDelete = action.payload.map(String);
pullAll(entity.content.ids, idsToDelete);
AsyncReducer.removeDirty(entity, idsToDelete);
ReducerUtility.removeDirty(entity, idsToDelete);
ReducerUtility.removeDidLoaded(entity, idsToDelete);
omit(entity.content.entities, idsToDelete);
});
@ -288,6 +309,9 @@ export function createAsyncEntityReducer<S, T, ID extends Async.IdType>(
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);
@ -298,6 +322,6 @@ export function createAsyncEntityReducer<S, T, ID extends Async.IdType>(
dirty &&
builder.addCase(dirty, (state, action) => {
const entity = getEntity(state);
AsyncReducer.markDirty(entity, action.payload.map(String));
ReducerUtility.markDirty(entity, action.payload.map(String));
});
}

@ -1,5 +1,5 @@
import { Draft } from "@reduxjs/toolkit";
import { difference, uniq } from "lodash";
import { difference, pullAll, uniq } from "lodash";
export namespace AsyncUtility {
export function getDefaultItem<T>(): Async.Item<T> {
@ -15,6 +15,7 @@ export namespace AsyncUtility {
state: "uninitialized",
keyName: key,
dirtyEntities: [],
didLoaded: [],
content: [],
error: null,
};
@ -24,6 +25,7 @@ export namespace AsyncUtility {
return {
state: "uninitialized",
dirtyEntities: [],
didLoaded: [],
content: {
keyName: key,
ids: [],
@ -34,7 +36,7 @@ export namespace AsyncUtility {
}
}
export namespace AsyncReducer {
export namespace ReducerUtility {
type DirtyType = Draft<Async.Entity<any>> | Draft<Async.List<any>>;
export function markDirty<T extends DirtyType>(
entity: T,
@ -63,9 +65,24 @@ export namespace AsyncReducer {
entity: T,
removedIds: string[]
) {
entity.dirtyEntities = difference(entity.dirtyEntities, removedIds);
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);
}
}

@ -12,11 +12,13 @@ declare namespace Async {
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[];
};
}

@ -140,26 +140,20 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
[episodes]
);
const updateLanguage = useCallback(
(lang: Nullable<Language.Info>) => {
if (lang) {
const list = pending.map((v) => {
const form = v.form;
return {
...v,
form: {
...form,
language: lang.code2,
hi: lang.hi ?? false,
forced: lang.forced ?? false,
},
};
const updateLanguage = useCallback((lang: Nullable<Language.Info>) => {
if (lang) {
const { code2, hi, forced } = lang;
setPending((pending) => {
return pending.map((v) => {
const newValue = { ...v };
newValue.form.language = code2;
newValue.form.hi = hi ?? false;
newValue.form.forced = forced ?? false;
return newValue;
});
setPending(list);
}
},
[pending]
);
});
}
}, []);
const setFiles = useCallback(
(files: File[]) => {

@ -31,20 +31,23 @@ export function useEntityItemById<T>(
entity: Async.Entity<T>,
id: string
): Async.Item<T> {
const { content, dirtyEntities, error, state } = entity;
const { content, dirtyEntities, didLoaded, error, state } = entity;
const item = useEntityToItem(content, id);
const newState = useMemo<Async.State>(() => {
if (state === "dirty") {
if (dirtyEntities.find((v) => v === id)) {
return "dirty";
} else {
return "succeeded";
}
} else {
return state;
switch (state) {
case "loading":
return state;
default:
if (dirtyEntities.find((v) => v === id)) {
return "dirty";
} else if (!didLoaded.find((v) => v === id)) {
return "uninitialized";
} else {
return state;
}
}
}, [dirtyEntities, id, state]);
}, [dirtyEntities, id, state, didLoaded]);
return useMemo(
() => ({ content: item, state: newState, error }),
@ -52,32 +55,6 @@ export function useEntityItemById<T>(
);
}
// export function useListItemById<T>(
// list: Async.List<T>,
// id: string
// ): Async.Item<T> {
// const { content, dirtyEntities, error, state, keyName } = list;
// const item = useMemo(
// () => content.find((v) => String(v[keyName]) === id) ?? null,
// [content, id, keyName]
// );
// const newState = useMemo<Async.State>(() => {
// if (state === "loading" || state === "uninitialized") {
// return state;
// } else if (dirtyEntities.find((v) => v === id)) {
// return "dirty";
// } else {
// return "succeeded";
// }
// }, [dirtyEntities, error, id, state]);
// return useMemo(
// () => ({ content: item, state: newState, error }),
// [item, newState, error]
// );
// }
export function useOnLoadedOnce(callback: () => void, entity: Async.Base<any>) {
const [didLoaded, setLoaded] = useState(false);

Loading…
Cancel
Save