mirror of
https://github.com/morpheus65535/bazarr
synced 2025-02-21 21:47:15 +00:00
Fix issues in UI
This commit is contained in:
parent
6eb14a2754
commit
42d19eaa42
13 changed files with 114 additions and 78 deletions
|
@ -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);
|
||||
}
|
||||
}
|
2
frontend/src/@types/async.d.ts
vendored
2
frontend/src/@types/async.d.ts
vendored
|
@ -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…
Reference in a new issue