mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Performance improvements (#2925)
* Add sqlite_stat4 build tag * Simplify studio filter criterion queries * Prevent useList loading data before filter initialized
This commit is contained in:
parent
d274f86390
commit
25bc750295
9 changed files with 183 additions and 117 deletions
2
Makefile
2
Makefile
|
|
@ -54,7 +54,7 @@ build: pre-build
|
||||||
build:
|
build:
|
||||||
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/internal/api.version=$(STASH_VERSION)' -X 'github.com/stashapp/stash/internal/api.buildstamp=$(BUILD_DATE)' -X 'github.com/stashapp/stash/internal/api.githash=$(GITHASH)')
|
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/internal/api.version=$(STASH_VERSION)' -X 'github.com/stashapp/stash/internal/api.buildstamp=$(BUILD_DATE)' -X 'github.com/stashapp/stash/internal/api.githash=$(GITHASH)')
|
||||||
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/internal/manager/config.officialBuild=$(OFFICIAL_BUILD)')
|
$(eval LDFLAGS := $(LDFLAGS) -X 'github.com/stashapp/stash/internal/manager/config.officialBuild=$(OFFICIAL_BUILD)')
|
||||||
go build $(OUTPUT) -mod=vendor -v -tags "sqlite_omit_load_extension osusergo netgo" $(GO_BUILD_FLAGS) -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS) $(PLATFORM_SPECIFIC_LDFLAGS)" ./cmd/stash
|
go build $(OUTPUT) -mod=vendor -v -tags "sqlite_omit_load_extension sqlite_stat4 osusergo netgo" $(GO_BUILD_FLAGS) -ldflags "$(LDFLAGS) $(EXTRA_LDFLAGS) $(PLATFORM_SPECIFIC_LDFLAGS)" ./cmd/stash
|
||||||
|
|
||||||
# strips debug symbols from the release build
|
# strips debug symbols from the release build
|
||||||
build-release: EXTRA_LDFLAGS := -s -w
|
build-release: EXTRA_LDFLAGS := -s -w
|
||||||
|
|
|
||||||
|
|
@ -744,7 +744,6 @@ type hierarchicalMultiCriterionHandlerBuilder struct {
|
||||||
foreignTable string
|
foreignTable string
|
||||||
foreignFK string
|
foreignFK string
|
||||||
|
|
||||||
derivedTable string
|
|
||||||
parentFK string
|
parentFK string
|
||||||
relationsTable string
|
relationsTable string
|
||||||
}
|
}
|
||||||
|
|
@ -867,9 +866,15 @@ func (m *hierarchicalMultiCriterionHandlerBuilder) handler(criterion *models.Hie
|
||||||
|
|
||||||
valuesClause := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth)
|
valuesClause := getHierarchicalValues(ctx, m.tx, criterion.Value, m.foreignTable, m.relationsTable, m.parentFK, criterion.Depth)
|
||||||
|
|
||||||
f.addLeftJoin("(SELECT column1 AS root_id, column2 AS item_id FROM ("+valuesClause+"))", m.derivedTable, fmt.Sprintf("%s.item_id = %s.%s", m.derivedTable, m.primaryTable, m.foreignFK))
|
switch criterion.Modifier {
|
||||||
|
case models.CriterionModifierIncludes:
|
||||||
addHierarchicalConditionClauses(f, criterion, m.derivedTable, "root_id")
|
f.addWhere(fmt.Sprintf("%s.%s IN (SELECT column2 FROM (%s))", m.primaryTable, m.foreignFK, valuesClause))
|
||||||
|
case models.CriterionModifierIncludesAll:
|
||||||
|
f.addWhere(fmt.Sprintf("%s.%s IN (SELECT column2 FROM (%s))", m.primaryTable, m.foreignFK, valuesClause))
|
||||||
|
f.addHaving(fmt.Sprintf("count(distinct %s.%s) IS %d", m.primaryTable, m.foreignFK, len(criterion.Value)))
|
||||||
|
case models.CriterionModifierExcludes:
|
||||||
|
f.addWhere(fmt.Sprintf("%s.%s NOT IN (SELECT column2 FROM (%s)) OR %[1]s.%[2]s IS NULL", m.primaryTable, m.foreignFK, valuesClause))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -938,7 +938,6 @@ func galleryStudioCriterionHandler(qb *GalleryStore, studios *models.Hierarchica
|
||||||
primaryTable: galleryTable,
|
primaryTable: galleryTable,
|
||||||
foreignTable: studioTable,
|
foreignTable: studioTable,
|
||||||
foreignFK: studioIDColumn,
|
foreignFK: studioIDColumn,
|
||||||
derivedTable: "studio",
|
|
||||||
parentFK: "parent_id",
|
parentFK: "parent_id",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -922,7 +922,6 @@ func imageStudioCriterionHandler(qb *ImageStore, studios *models.HierarchicalMul
|
||||||
primaryTable: imageTable,
|
primaryTable: imageTable,
|
||||||
foreignTable: studioTable,
|
foreignTable: studioTable,
|
||||||
foreignFK: studioIDColumn,
|
foreignFK: studioIDColumn,
|
||||||
derivedTable: "studio",
|
|
||||||
parentFK: "parent_id",
|
parentFK: "parent_id",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,6 @@ func movieStudioCriterionHandler(qb *movieQueryBuilder, studios *models.Hierarch
|
||||||
primaryTable: movieTable,
|
primaryTable: movieTable,
|
||||||
foreignTable: studioTable,
|
foreignTable: studioTable,
|
||||||
foreignFK: studioIDColumn,
|
foreignFK: studioIDColumn,
|
||||||
derivedTable: "studio",
|
|
||||||
parentFK: "parent_id",
|
parentFK: "parent_id",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1244,7 +1244,6 @@ func sceneStudioCriterionHandler(qb *SceneStore, studios *models.HierarchicalMul
|
||||||
primaryTable: sceneTable,
|
primaryTable: sceneTable,
|
||||||
foreignTable: studioTable,
|
foreignTable: studioTable,
|
||||||
foreignFK: studioIDColumn,
|
foreignFK: studioIDColumn,
|
||||||
derivedTable: "studio",
|
|
||||||
parentFK: "parent_id",
|
parentFK: "parent_id",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1787,9 +1787,9 @@ func TestSceneCountByPerformerID(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func scenesToIDs(i []*models.Scene) []int {
|
func scenesToIDs(i []*models.Scene) []int {
|
||||||
var ret []int
|
ret := make([]int, len(i))
|
||||||
for _, ii := range i {
|
for i, v := range i {
|
||||||
ret = append(ret, ii.ID)
|
ret[i] = v.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
@ -3304,44 +3304,74 @@ func TestSceneQueryPerformerTags(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSceneQueryStudio(t *testing.T) {
|
func TestSceneQueryStudio(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
tests := []struct {
|
||||||
sqb := db.Scene
|
name string
|
||||||
studioCriterion := models.HierarchicalMultiCriterionInput{
|
q string
|
||||||
|
studioCriterion models.HierarchicalMultiCriterionInput
|
||||||
|
expectedIDs []int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"includes",
|
||||||
|
"",
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{
|
Value: []string{
|
||||||
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
||||||
},
|
},
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
},
|
||||||
|
[]int{sceneIDs[sceneIdxWithStudio]},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"excludes",
|
||||||
|
getSceneStringValue(sceneIdxWithStudio, titleField),
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
},
|
||||||
|
[]int{},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"excludes includes null",
|
||||||
|
getSceneStringValue(sceneIdxWithGallery, titleField),
|
||||||
|
models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
},
|
||||||
|
[]int{sceneIDs[sceneIdxWithGallery]},
|
||||||
|
false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qb := db.Scene
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
|
studioCriterion := tt.studioCriterion
|
||||||
|
|
||||||
sceneFilter := models.SceneFilterType{
|
sceneFilter := models.SceneFilterType{
|
||||||
Studios: &studioCriterion,
|
Studios: &studioCriterion,
|
||||||
}
|
}
|
||||||
|
|
||||||
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil)
|
var findFilter *models.FindFilterType
|
||||||
|
if tt.q != "" {
|
||||||
assert.Len(t, scenes, 1)
|
findFilter = &models.FindFilterType{
|
||||||
|
Q: &tt.q,
|
||||||
// ensure id is correct
|
}
|
||||||
assert.Equal(t, sceneIDs[sceneIdxWithStudio], scenes[0].ID)
|
|
||||||
|
|
||||||
studioCriterion = models.HierarchicalMultiCriterionInput{
|
|
||||||
Value: []string{
|
|
||||||
strconv.Itoa(studioIDs[studioIdxWithScene]),
|
|
||||||
},
|
|
||||||
Modifier: models.CriterionModifierExcludes,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
q := getSceneStringValue(sceneIdxWithStudio, titleField)
|
scenes := queryScene(ctx, t, qb, &sceneFilter, findFilter)
|
||||||
findFilter := models.FindFilterType{
|
|
||||||
Q: &q,
|
|
||||||
}
|
|
||||||
|
|
||||||
scenes = queryScene(ctx, t, sqb, &sceneFilter, &findFilter)
|
assert.ElementsMatch(t, scenesToIDs(scenes), tt.expectedIDs)
|
||||||
assert.Len(t, scenes, 0)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSceneQueryStudioDepth(t *testing.T) {
|
func TestSceneQueryStudioDepth(t *testing.T) {
|
||||||
withTxn(func(ctx context.Context) error {
|
withTxn(func(ctx context.Context) error {
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,12 @@ export const useFindDefaultFilter = (mode: GQL.FilterMode) =>
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useFindGalleries = (filter: ListFilterModel) =>
|
export const useFindGalleries = (filter?: ListFilterModel) =>
|
||||||
GQL.useFindGalleriesQuery({
|
GQL.useFindGalleriesQuery({
|
||||||
|
skip: filter === undefined,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter?.makeFindFilter(),
|
||||||
gallery_filter: filter.makeFilter(),
|
gallery_filter: filter?.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -82,11 +83,12 @@ export const queryFindGalleries = (filter: ListFilterModel) =>
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useFindScenes = (filter: ListFilterModel) =>
|
export const useFindScenes = (filter?: ListFilterModel) =>
|
||||||
GQL.useFindScenesQuery({
|
GQL.useFindScenesQuery({
|
||||||
|
skip: filter === undefined,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter?.makeFindFilter(),
|
||||||
scene_filter: filter.makeFilter(),
|
scene_filter: filter?.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -107,11 +109,12 @@ export const queryFindScenesByID = (sceneIDs: number[]) =>
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useFindSceneMarkers = (filter: ListFilterModel) =>
|
export const useFindSceneMarkers = (filter?: ListFilterModel) =>
|
||||||
GQL.useFindSceneMarkersQuery({
|
GQL.useFindSceneMarkersQuery({
|
||||||
|
skip: filter === undefined,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter?.makeFindFilter(),
|
||||||
scene_marker_filter: filter.makeFilter(),
|
scene_marker_filter: filter?.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -124,11 +127,12 @@ export const queryFindSceneMarkers = (filter: ListFilterModel) =>
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useFindImages = (filter: ListFilterModel) =>
|
export const useFindImages = (filter?: ListFilterModel) =>
|
||||||
GQL.useFindImagesQuery({
|
GQL.useFindImagesQuery({
|
||||||
|
skip: filter === undefined,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter?.makeFindFilter(),
|
||||||
image_filter: filter.makeFilter(),
|
image_filter: filter?.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -141,11 +145,12 @@ export const queryFindImages = (filter: ListFilterModel) =>
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useFindStudios = (filter: ListFilterModel) =>
|
export const useFindStudios = (filter?: ListFilterModel) =>
|
||||||
GQL.useFindStudiosQuery({
|
GQL.useFindStudiosQuery({
|
||||||
|
skip: filter === undefined,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter?.makeFindFilter(),
|
||||||
studio_filter: filter.makeFilter(),
|
studio_filter: filter?.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -158,11 +163,12 @@ export const queryFindStudios = (filter: ListFilterModel) =>
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useFindMovies = (filter: ListFilterModel) =>
|
export const useFindMovies = (filter?: ListFilterModel) =>
|
||||||
GQL.useFindMoviesQuery({
|
GQL.useFindMoviesQuery({
|
||||||
|
skip: filter === undefined,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter?.makeFindFilter(),
|
||||||
movie_filter: filter.makeFilter(),
|
movie_filter: filter?.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -175,19 +181,21 @@ export const queryFindMovies = (filter: ListFilterModel) =>
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useFindPerformers = (filter: ListFilterModel) =>
|
export const useFindPerformers = (filter?: ListFilterModel) =>
|
||||||
GQL.useFindPerformersQuery({
|
GQL.useFindPerformersQuery({
|
||||||
|
skip: filter === undefined,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter?.makeFindFilter(),
|
||||||
performer_filter: filter.makeFilter(),
|
performer_filter: filter?.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useFindTags = (filter: ListFilterModel) =>
|
export const useFindTags = (filter?: ListFilterModel) =>
|
||||||
GQL.useFindTagsQuery({
|
GQL.useFindTagsQuery({
|
||||||
|
skip: filter === undefined,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter?.makeFindFilter(),
|
||||||
tag_filter: filter.makeFilter(),
|
tag_filter: filter?.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export interface IListHookOperation<T> {
|
||||||
result: T,
|
result: T,
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
selectedIds: Set<string>
|
selectedIds: Set<string>
|
||||||
) => void;
|
) => Promise<void>;
|
||||||
isDisplayed?: (
|
isDisplayed?: (
|
||||||
result: T,
|
result: T,
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
|
|
@ -160,20 +160,20 @@ interface IQueryResult {
|
||||||
|
|
||||||
interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
|
interface IQuery<T extends IQueryResult, T2 extends IDataItem> {
|
||||||
filterMode: FilterMode;
|
filterMode: FilterMode;
|
||||||
useData: (filter: ListFilterModel) => T;
|
useData: (filter?: ListFilterModel) => T;
|
||||||
getData: (data: T) => T2[];
|
getData: (data: T) => T2[];
|
||||||
getCount: (data: T) => number;
|
getCount: (data: T) => number;
|
||||||
getMetadataByline: (data: T) => React.ReactNode;
|
getMetadataByline: (data: T) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRenderListProps {
|
interface IRenderListProps {
|
||||||
filter: ListFilterModel;
|
filter?: ListFilterModel;
|
||||||
filterOptions: ListFilterOptions;
|
filterOptions: ListFilterOptions;
|
||||||
onChangePage: (page: number) => void;
|
onChangePage: (page: number) => void;
|
||||||
updateFilter: (filter: ListFilterModel) => void;
|
updateFilter: (filter: ListFilterModel) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RenderList = <
|
const useRenderList = <
|
||||||
QueryResult extends IQueryResult,
|
QueryResult extends IQueryResult,
|
||||||
QueryData extends IDataItem
|
QueryData extends IDataItem
|
||||||
>({
|
>({
|
||||||
|
|
@ -200,31 +200,44 @@ const RenderList = <
|
||||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
const [lastClickedId, setLastClickedId] = useState<string>();
|
||||||
|
|
||||||
const [editingCriterion, setEditingCriterion] = useState<
|
const [editingCriterion, setEditingCriterion] = useState<
|
||||||
Criterion<CriterionValue> | undefined
|
Criterion<CriterionValue>
|
||||||
>(undefined);
|
>();
|
||||||
const [newCriterion, setNewCriterion] = useState(false);
|
const [newCriterion, setNewCriterion] = useState(false);
|
||||||
|
|
||||||
const result = useData(filter);
|
const result = useData(filter);
|
||||||
const totalCount = getCount(result);
|
const totalCount = getCount(result);
|
||||||
const metadataByline = getMetadataByline(result);
|
const metadataByline = getMetadataByline(result);
|
||||||
const items = getData(result);
|
const items = getData(result);
|
||||||
const pages = Math.ceil(totalCount / filter.itemsPerPage);
|
|
||||||
|
|
||||||
// handle case where page is more than there are pages
|
// handle case where page is more than there are pages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (filter === undefined) return;
|
||||||
|
|
||||||
|
const pages = Math.ceil(totalCount / filter.itemsPerPage);
|
||||||
if (pages > 0 && filter.currentPage > pages) {
|
if (pages > 0 && filter.currentPage > pages) {
|
||||||
onChangePage(pages);
|
onChangePage(pages);
|
||||||
}
|
}
|
||||||
}, [pages, filter.currentPage, onChangePage]);
|
}, [filter, onChangePage, totalCount]);
|
||||||
|
|
||||||
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (filter === undefined) return;
|
||||||
|
|
||||||
Mousetrap.bind("f", () => setNewCriterion(true));
|
Mousetrap.bind("f", () => setNewCriterion(true));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Mousetrap.unbind("f");
|
||||||
|
};
|
||||||
|
}, [filter]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (filter === undefined) return;
|
||||||
|
|
||||||
|
const pages = Math.ceil(totalCount / filter.itemsPerPage);
|
||||||
Mousetrap.bind("right", () => {
|
Mousetrap.bind("right", () => {
|
||||||
const maxPage = totalCount / filter.itemsPerPage;
|
if (filter.currentPage < pages) {
|
||||||
if (filter.currentPage < maxPage) {
|
|
||||||
onChangePage(filter.currentPage + 1);
|
onChangePage(filter.currentPage + 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -234,25 +247,18 @@ const RenderList = <
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Mousetrap.bind("shift+right", () => {
|
Mousetrap.bind("shift+right", () => {
|
||||||
const maxPage = totalCount / filter.itemsPerPage + 1;
|
onChangePage(Math.min(pages, filter.currentPage + 10));
|
||||||
onChangePage(Math.min(maxPage, filter.currentPage + 10));
|
|
||||||
});
|
});
|
||||||
Mousetrap.bind("shift+left", () => {
|
Mousetrap.bind("shift+left", () => {
|
||||||
onChangePage(Math.max(1, filter.currentPage - 10));
|
onChangePage(Math.max(1, filter.currentPage - 10));
|
||||||
});
|
});
|
||||||
Mousetrap.bind("ctrl+end", () => {
|
Mousetrap.bind("ctrl+end", () => {
|
||||||
const maxPage = totalCount / filter.itemsPerPage + 1;
|
onChangePage(pages);
|
||||||
onChangePage(maxPage);
|
|
||||||
});
|
});
|
||||||
Mousetrap.bind("ctrl+home", () => {
|
Mousetrap.bind("ctrl+home", () => {
|
||||||
onChangePage(1);
|
onChangePage(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
let unbindExtras: () => void;
|
|
||||||
if (addKeybinds) {
|
|
||||||
unbindExtras = addKeybinds(result, filter, selectedIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind("right");
|
Mousetrap.unbind("right");
|
||||||
Mousetrap.unbind("left");
|
Mousetrap.unbind("left");
|
||||||
|
|
@ -260,12 +266,22 @@ const RenderList = <
|
||||||
Mousetrap.unbind("shift+left");
|
Mousetrap.unbind("shift+left");
|
||||||
Mousetrap.unbind("ctrl+end");
|
Mousetrap.unbind("ctrl+end");
|
||||||
Mousetrap.unbind("ctrl+home");
|
Mousetrap.unbind("ctrl+home");
|
||||||
|
|
||||||
if (unbindExtras) {
|
|
||||||
unbindExtras();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
});
|
}, [filter, onChangePage, totalCount]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (filter === undefined) return;
|
||||||
|
|
||||||
|
if (addKeybinds) {
|
||||||
|
const unbindExtras = addKeybinds(result, filter, selectedIds);
|
||||||
|
return () => {
|
||||||
|
unbindExtras();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [addKeybinds, filter, result, selectedIds]);
|
||||||
|
|
||||||
|
// Don't continue if filter is undefined
|
||||||
|
// There are no hooks below this point so this is valid
|
||||||
|
if (filter === undefined) return;
|
||||||
|
|
||||||
function singleSelect(id: string, selected: boolean) {
|
function singleSelect(id: string, selected: boolean) {
|
||||||
setLastClickedId(id);
|
setLastClickedId(id);
|
||||||
|
|
@ -334,24 +350,24 @@ const RenderList = <
|
||||||
setLastClickedId(undefined);
|
setLastClickedId(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSelectNone() {
|
const onSelectNone = () => {
|
||||||
const newSelectedIds: Set<string> = new Set();
|
const newSelectedIds: Set<string> = new Set();
|
||||||
setSelectedIds(newSelectedIds);
|
setSelectedIds(newSelectedIds);
|
||||||
setLastClickedId(undefined);
|
setLastClickedId(undefined);
|
||||||
}
|
};
|
||||||
|
|
||||||
function onChangeZoom(newZoomIndex: number) {
|
const onChangeZoom = (newZoomIndex: number) => {
|
||||||
const newFilter = cloneDeep(filter);
|
const newFilter = cloneDeep(filter);
|
||||||
newFilter.zoomIndex = newZoomIndex;
|
newFilter.zoomIndex = newZoomIndex;
|
||||||
updateFilter(newFilter);
|
updateFilter(newFilter);
|
||||||
}
|
};
|
||||||
|
|
||||||
function onOperationClicked(o: IListHookOperation<QueryResult>) {
|
const onOperationClicked = async (o: IListHookOperation<QueryResult>) => {
|
||||||
o.onClick(result, filter, selectedIds);
|
await o.onClick(result, filter, selectedIds);
|
||||||
if (o.postRefetch) {
|
if (o.postRefetch) {
|
||||||
result.refetch();
|
result.refetch();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const operations =
|
const operations =
|
||||||
otherOperations &&
|
otherOperations &&
|
||||||
|
|
@ -409,11 +425,12 @@ const RenderList = <
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
function maybeRenderContent() {
|
const maybeRenderContent = () => {
|
||||||
if (result.loading || result.error) {
|
if (result.loading || result.error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pages = Math.ceil(totalCount / filter.itemsPerPage);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderPagination()}
|
{renderPagination()}
|
||||||
|
|
@ -433,18 +450,18 @@ const RenderList = <
|
||||||
{renderPagination()}
|
{renderPagination()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function onChangeDisplayMode(displayMode: DisplayMode) {
|
const onChangeDisplayMode = (displayMode: DisplayMode) => {
|
||||||
const newFilter = cloneDeep(filter);
|
const newFilter = cloneDeep(filter);
|
||||||
newFilter.displayMode = displayMode;
|
newFilter.displayMode = displayMode;
|
||||||
updateFilter(newFilter);
|
updateFilter(newFilter);
|
||||||
}
|
};
|
||||||
|
|
||||||
function onAddCriterion(
|
const onAddCriterion = (
|
||||||
criterion: Criterion<CriterionValue>,
|
criterion: Criterion<CriterionValue>,
|
||||||
oldId?: string
|
oldId?: string
|
||||||
) {
|
) => {
|
||||||
const newFilter = cloneDeep(filter);
|
const newFilter = cloneDeep(filter);
|
||||||
|
|
||||||
// Find if we are editing an existing criteria, then modify that. Or create a new one.
|
// Find if we are editing an existing criteria, then modify that. Or create a new one.
|
||||||
|
|
@ -468,22 +485,22 @@ const RenderList = <
|
||||||
updateFilter(newFilter);
|
updateFilter(newFilter);
|
||||||
setEditingCriterion(undefined);
|
setEditingCriterion(undefined);
|
||||||
setNewCriterion(false);
|
setNewCriterion(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
function onRemoveCriterion(removedCriterion: Criterion<CriterionValue>) {
|
const onRemoveCriterion = (removedCriterion: Criterion<CriterionValue>) => {
|
||||||
const newFilter = cloneDeep(filter);
|
const newFilter = cloneDeep(filter);
|
||||||
newFilter.criteria = newFilter.criteria.filter(
|
newFilter.criteria = newFilter.criteria.filter(
|
||||||
(criterion) => criterion.getId() !== removedCriterion.getId()
|
(criterion) => criterion.getId() !== removedCriterion.getId()
|
||||||
);
|
);
|
||||||
newFilter.currentPage = 1;
|
newFilter.currentPage = 1;
|
||||||
updateFilter(newFilter);
|
updateFilter(newFilter);
|
||||||
}
|
};
|
||||||
|
|
||||||
function updateCriteria(c: Criterion<CriterionValue>[]) {
|
const updateCriteria = (c: Criterion<CriterionValue>[]) => {
|
||||||
const newFilter = cloneDeep(filter);
|
const newFilter = cloneDeep(filter);
|
||||||
newFilter.criteria = c.slice();
|
newFilter.criteria = c.slice();
|
||||||
setNewCriterion(false);
|
setNewCriterion(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
function onCancelAddCriterion() {
|
function onCancelAddCriterion() {
|
||||||
setEditingCriterion(undefined);
|
setEditingCriterion(undefined);
|
||||||
|
|
@ -732,10 +749,14 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderFilter = useMemo(() => {
|
const renderFilter = useMemo(() => {
|
||||||
return !options.filterHook ? filter : options.filterHook(cloneDeep(filter));
|
if (filterInitialised) {
|
||||||
}, [filter, options]);
|
return options.filterHook
|
||||||
|
? options.filterHook(cloneDeep(filter))
|
||||||
|
: filter;
|
||||||
|
}
|
||||||
|
}, [filterInitialised, filter, options]);
|
||||||
|
|
||||||
const { contentTemplate, onSelectChange } = RenderList({
|
const renderList = useRenderList({
|
||||||
...options,
|
...options,
|
||||||
filter: renderFilter,
|
filter: renderFilter,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
|
|
@ -743,12 +764,18 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||||
updateFilter,
|
updateFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const template = !filterInitialised ? (
|
const template = renderList ? (
|
||||||
<LoadingIndicator />
|
renderList.contentTemplate
|
||||||
) : (
|
) : (
|
||||||
<>{contentTemplate}</>
|
<LoadingIndicator />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function onSelectChange(id: string, selected: boolean, shiftKey: boolean) {
|
||||||
|
if (renderList) {
|
||||||
|
renderList.onSelectChange(id, selected, shiftKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filter,
|
filter,
|
||||||
template,
|
template,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue