mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
feat: people infinite scroll (#11326)
* feat: people infinite scroll * add infinite scroll to show & hide modal * update unit tests * show total people count instead of currently loaded * update personsearchdto
This commit is contained in:
parent
152421e288
commit
8e6bc13540
17 changed files with 236 additions and 63 deletions
|
@ -65,6 +65,7 @@ describe('/people', () => {
|
|||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
hasNextPage: false,
|
||||
total: 3,
|
||||
hidden: 1,
|
||||
people: [
|
||||
|
@ -80,6 +81,7 @@ describe('/people', () => {
|
|||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
hasNextPage: false,
|
||||
total: 3,
|
||||
hidden: 1,
|
||||
people: [
|
||||
|
@ -88,6 +90,21 @@ describe('/people', () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should support pagination', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/people')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.query({ withHidden: true, page: 2, size: 1 });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
hasNextPage: true,
|
||||
total: 3,
|
||||
hidden: 1,
|
||||
people: [expect.objectContaining({ name: 'visible_person' })],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /people/:id', () => {
|
||||
|
|
BIN
mobile/openapi/lib/api/people_api.dart
generated
BIN
mobile/openapi/lib/api/people_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/people_response_dto.dart
generated
BIN
mobile/openapi/lib/model/people_response_dto.dart
generated
Binary file not shown.
|
@ -3824,6 +3824,29 @@
|
|||
"get": {
|
||||
"operationId": "getAllPeople",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Page number for pagination",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"default": 1,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Number of items per page",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"default": 500,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "withHidden",
|
||||
"required": false,
|
||||
|
@ -9531,6 +9554,10 @@
|
|||
},
|
||||
"PeopleResponseDto": {
|
||||
"properties": {
|
||||
"hasNextPage": {
|
||||
"description": "This property was added in v1.110.0",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
|
|
@ -607,6 +607,8 @@ export type UpdatePartnerDto = {
|
|||
inTimeline: boolean;
|
||||
};
|
||||
export type PeopleResponseDto = {
|
||||
/** This property was added in v1.110.0 */
|
||||
hasNextPage?: boolean;
|
||||
hidden: number;
|
||||
people: PersonResponseDto[];
|
||||
total: number;
|
||||
|
@ -2173,13 +2175,17 @@ export function updatePartner({ id, updatePartnerDto }: {
|
|||
body: updatePartnerDto
|
||||
})));
|
||||
}
|
||||
export function getAllPeople({ withHidden }: {
|
||||
export function getAllPeople({ page, size, withHidden }: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
withHidden?: boolean;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PeopleResponseDto;
|
||||
}>(`/people${QS.query(QS.explode({
|
||||
page,
|
||||
size,
|
||||
withHidden
|
||||
}))}`, {
|
||||
...opts
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator';
|
||||
import { IsArray, IsInt, IsNotEmpty, IsString, Max, MaxDate, Min, ValidateNested } from 'class-validator';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
|
@ -63,6 +63,21 @@ export class MergePersonDto {
|
|||
export class PersonSearchDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
withHidden?: boolean;
|
||||
|
||||
/** Page number for pagination */
|
||||
@ApiPropertyOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
page: number = 1;
|
||||
|
||||
/** Number of items per page */
|
||||
@ApiPropertyOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
@Type(() => Number)
|
||||
size: number = 500;
|
||||
}
|
||||
|
||||
export class PersonResponseDto {
|
||||
|
@ -132,6 +147,10 @@ export class PeopleResponseDto {
|
|||
@ApiProperty({ type: 'integer' })
|
||||
hidden!: number;
|
||||
people!: PersonResponseDto[];
|
||||
|
||||
// TODO: make required after a few versions
|
||||
@PropertyLifecycle({ addedAt: 'v1.110.0' })
|
||||
hasNextPage?: boolean;
|
||||
}
|
||||
|
||||
export function mapPerson(person: PersonEntity): PersonResponseDto {
|
||||
|
|
|
@ -37,7 +37,7 @@ export interface PeopleStatistics {
|
|||
|
||||
export interface IPersonRepository {
|
||||
getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
|
||||
getAllForUser(userId: string, options: PersonSearchOptions): Promise<PersonEntity[]>;
|
||||
getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
|
||||
getAllWithoutFaces(): Promise<PersonEntity[]>;
|
||||
getById(personId: string): Promise<PersonEntity | null>;
|
||||
getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
|
||||
|
|
|
@ -37,9 +37,12 @@ ORDER BY
|
|||
"person"."isHidden" ASC,
|
||||
NULLIF("person"."name", '') IS NULL ASC,
|
||||
COUNT("face"."assetId") DESC,
|
||||
NULLIF("person"."name", '') ASC NULLS LAST
|
||||
NULLIF("person"."name", '') ASC NULLS LAST,
|
||||
"person"."createdAt" ASC
|
||||
LIMIT
|
||||
500
|
||||
11
|
||||
OFFSET
|
||||
10
|
||||
|
||||
-- PersonRepository.getAllWithoutFaces
|
||||
SELECT
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
UpdateFacesData,
|
||||
} from 'src/interfaces/person.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
import { Paginated, PaginationOptions, paginate } from 'src/utils/pagination';
|
||||
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
||||
import { FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
|
||||
|
||||
@Instrumentation()
|
||||
|
@ -64,8 +64,8 @@ export class PersonRepository implements IPersonRepository {
|
|||
return paginate(this.personRepository, pagination, options);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAllForUser(userId: string, options?: PersonSearchOptions): Promise<PersonEntity[]> {
|
||||
@GenerateSql({ params: [{ take: 10, skip: 10 }, DummyValue.UUID] })
|
||||
getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions): Paginated<PersonEntity> {
|
||||
const queryBuilder = this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
.leftJoin('person.faces', 'face')
|
||||
|
@ -76,15 +76,18 @@ export class PersonRepository implements IPersonRepository {
|
|||
.addOrderBy("NULLIF(person.name, '') IS NULL", 'ASC')
|
||||
.addOrderBy('COUNT(face.assetId)', 'DESC')
|
||||
.addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST')
|
||||
.addOrderBy('person.createdAt')
|
||||
.andWhere("person.thumbnailPath != ''")
|
||||
.having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 })
|
||||
.groupBy('person.id')
|
||||
.limit(500);
|
||||
.groupBy('person.id');
|
||||
if (!options?.withHidden) {
|
||||
queryBuilder.andWhere('person.isHidden = false');
|
||||
}
|
||||
|
||||
return queryBuilder.getMany();
|
||||
return paginatedBuilder(queryBuilder, {
|
||||
mode: PaginationMode.LIMIT_OFFSET,
|
||||
...pagination,
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
|
|
|
@ -115,9 +115,13 @@ describe(PersonService.name, () => {
|
|||
|
||||
describe('getAll', () => {
|
||||
it('should get all hidden and visible people with thumbnails', async () => {
|
||||
personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]);
|
||||
personMock.getAllForUser.mockResolvedValue({
|
||||
items: [personStub.withName, personStub.hidden],
|
||||
hasNextPage: false,
|
||||
});
|
||||
personMock.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 });
|
||||
await expect(sut.getAll(authStub.admin, { withHidden: true })).resolves.toEqual({
|
||||
await expect(sut.getAll(authStub.admin, { withHidden: true, page: 1, size: 10 })).resolves.toEqual({
|
||||
hasNextPage: false,
|
||||
total: 2,
|
||||
hidden: 1,
|
||||
people: [
|
||||
|
@ -132,7 +136,7 @@ describe(PersonService.name, () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, {
|
||||
expect(personMock.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, {
|
||||
minimumFaceCount: 3,
|
||||
withHidden: true,
|
||||
});
|
||||
|
|
|
@ -91,15 +91,22 @@ export class PersonService {
|
|||
}
|
||||
|
||||
async getAll(auth: AuthDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
|
||||
const { withHidden = false, page, size } = dto;
|
||||
const pagination = {
|
||||
take: size,
|
||||
skip: (page - 1) * size,
|
||||
};
|
||||
|
||||
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
||||
const people = await this.repository.getAllForUser(auth.user.id, {
|
||||
const { items, hasNextPage } = await this.repository.getAllForUser(pagination, auth.user.id, {
|
||||
minimumFaceCount: machineLearning.facialRecognition.minFaces,
|
||||
withHidden: dto.withHidden || false,
|
||||
withHidden,
|
||||
});
|
||||
const { total, hidden } = await this.repository.getNumberOfPeople(auth.user.id);
|
||||
|
||||
return {
|
||||
people: people.map((person) => mapPerson(person)),
|
||||
people: items.map((person) => mapPerson(person)),
|
||||
hasNextPage,
|
||||
total,
|
||||
hidden,
|
||||
};
|
||||
|
|
9
web/src/lib/__mocks__/intersection-observer.mock.ts
Normal file
9
web/src/lib/__mocks__/intersection-observer.mock.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { vi } from 'vitest';
|
||||
|
||||
export const getIntersectionObserverMock = () =>
|
||||
vi.fn(() => ({
|
||||
disconnect: vi.fn(),
|
||||
observe: vi.fn(),
|
||||
takeRecords: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
}));
|
|
@ -1,3 +1,4 @@
|
|||
import { getIntersectionObserverMock } from '$lib/__mocks__/intersection-observer.mock';
|
||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||
import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
|
||||
import type { PersonResponseDto } from '@immich/sdk';
|
||||
|
@ -18,6 +19,7 @@ describe('ManagePeopleVisibility Component', () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock());
|
||||
personVisible = personFactory.build({ isHidden: false });
|
||||
personHidden = personFactory.build({ isHidden: true });
|
||||
personWithoutName = personFactory.build({ isHidden: false, name: undefined });
|
||||
|
@ -32,7 +34,9 @@ describe('ManagePeopleVisibility Component', () => {
|
|||
const { getByText } = render(ManagePeopleVisibility, {
|
||||
props: {
|
||||
people: [personVisible, personHidden, personWithoutName],
|
||||
totalPeopleCount: 3,
|
||||
onClose: vi.fn(),
|
||||
loadNextPage: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -45,7 +49,9 @@ describe('ManagePeopleVisibility Component', () => {
|
|||
const { getByText, getByTitle } = render(ManagePeopleVisibility, {
|
||||
props: {
|
||||
people: [personVisible, personHidden, personWithoutName],
|
||||
totalPeopleCount: 3,
|
||||
onClose: vi.fn(),
|
||||
loadNextPage: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -63,7 +69,9 @@ describe('ManagePeopleVisibility Component', () => {
|
|||
const { getByText, getByTitle } = render(ManagePeopleVisibility, {
|
||||
props: {
|
||||
people: [personVisible, personHidden, personWithoutName],
|
||||
totalPeopleCount: 3,
|
||||
onClose: vi.fn(),
|
||||
loadNextPage: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -86,7 +94,9 @@ describe('ManagePeopleVisibility Component', () => {
|
|||
const { getByText, getByTitle } = render(ManagePeopleVisibility, {
|
||||
props: {
|
||||
people: [personVisible, personHidden, personWithoutName],
|
||||
totalPeopleCount: 3,
|
||||
onClose: vi.fn(),
|
||||
loadNextPage: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
|
@ -24,8 +25,10 @@
|
|||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
|
||||
export let people: PersonResponseDto[];
|
||||
export let onClose: () => void;
|
||||
export let totalPeopleCount: number;
|
||||
export let titleId: string | undefined = undefined;
|
||||
export let onClose: () => void;
|
||||
export let loadNextPage: () => void;
|
||||
|
||||
let toggleVisibility = ToggleVisibility.SHOW_ALL;
|
||||
let showLoadingSpinner = false;
|
||||
|
@ -121,7 +124,7 @@
|
|||
<CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
|
||||
<div class="flex gap-2 items-center">
|
||||
<p id={titleId} class="ml-2">{$t('show_and_hide_people')}</p>
|
||||
<p class="text-sm text-gray-400 dark:text-gray-600">({people.length.toLocaleString($locale)})</p>
|
||||
<p class="text-sm text-gray-400 dark:text-gray-600">({totalPeopleCount.toLocaleString($locale)})</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
|
@ -138,31 +141,29 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 mt-16">
|
||||
<div class="w-full grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
|
||||
{#each people as person, index (person.id)}
|
||||
{@const hidden = personIsHidden[person.id]}
|
||||
<button
|
||||
type="button"
|
||||
class="group relative"
|
||||
on:click={() => (personIsHidden[person.id] = !hidden)}
|
||||
aria-pressed={hidden}
|
||||
aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
|
||||
>
|
||||
<ImageThumbnail
|
||||
preload={index < 20}
|
||||
{hidden}
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
hiddenIconClass="text-white group-hover:text-black transition-colors"
|
||||
/>
|
||||
{#if person.name}
|
||||
<span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<PeopleInfiniteScroll {people} hasNextPage={true} {loadNextPage} let:person let:index>
|
||||
{@const hidden = personIsHidden[person.id]}
|
||||
<button
|
||||
type="button"
|
||||
class="group relative"
|
||||
on:click={() => (personIsHidden[person.id] = !hidden)}
|
||||
aria-pressed={hidden}
|
||||
aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
|
||||
>
|
||||
<ImageThumbnail
|
||||
preload={index < 20}
|
||||
{hidden}
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
hiddenIconClass="text-white group-hover:text-black transition-colors"
|
||||
/>
|
||||
{#if person.name}
|
||||
<span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
</PeopleInfiniteScroll>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import type { PersonResponseDto } from '@immich/sdk';
|
||||
|
||||
export let people: PersonResponseDto[];
|
||||
export let hasNextPage: boolean | undefined = undefined;
|
||||
export let loadNextPage: () => void;
|
||||
|
||||
let lastPersonContainer: HTMLElement | undefined;
|
||||
|
||||
const intersectionObserver = new IntersectionObserver((entries) => {
|
||||
const entry = entries.find((entry) => entry.target === lastPersonContainer);
|
||||
if (entry?.isIntersecting) {
|
||||
loadNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
$: if (lastPersonContainer) {
|
||||
intersectionObserver.disconnect();
|
||||
intersectionObserver.observe(lastPersonContainer);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
|
||||
{#each people as person, index (person.id)}
|
||||
{#if hasNextPage && index === people.length - 1}
|
||||
<div bind:this={lastPersonContainer}>
|
||||
<slot {person} {index} />
|
||||
</div>
|
||||
{:else}
|
||||
<slot {person} {index} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
|
@ -567,6 +567,7 @@
|
|||
"failed_to_get_people": "Failed to get people",
|
||||
"failed_to_load_asset": "Failed to load asset",
|
||||
"failed_to_load_assets": "Failed to load assets",
|
||||
"failed_to_load_people": "Failed to load people",
|
||||
"failed_to_stack_assets": "Failed to stack assets",
|
||||
"failed_to_unstack_assets": "Failed to un-stack assets",
|
||||
"import_path_already_exists": "This import path already exists.",
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { focusTrap } from '$lib/actions/focus-trap';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
|
||||
import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte';
|
||||
import PeopleCard from '$lib/components/faces-page/people-card.svelte';
|
||||
import PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte';
|
||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||
import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
|
@ -21,20 +23,26 @@
|
|||
import { handlePromiseError } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { clearQueryParam } from '$lib/utils/navigation';
|
||||
import { getPerson, mergePerson, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
getAllPeople,
|
||||
getPerson,
|
||||
mergePerson,
|
||||
searchPerson,
|
||||
updatePerson,
|
||||
type PersonResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { mdiAccountOff, mdiEyeOutline } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import type { PageData } from './$types';
|
||||
import { focusTrap } from '$lib/actions/focus-trap';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: people = data.people.people;
|
||||
$: visiblePeople = people.filter((people) => !people.isHidden);
|
||||
$: countVisiblePeople = searchName ? searchedPeopleLocal.length : visiblePeople.length;
|
||||
$: countVisiblePeople = searchName ? searchedPeopleLocal.length : data.people.total - data.people.hidden;
|
||||
$: showPeople = searchName ? searchedPeopleLocal : visiblePeople;
|
||||
|
||||
let selectHidden = false;
|
||||
|
@ -43,6 +51,7 @@
|
|||
let showSetBirthDateModal = false;
|
||||
let showMergeModal = false;
|
||||
let personName = '';
|
||||
let nextPage = data.people.hasNextPage ? 2 : null;
|
||||
let personMerge1: PersonResponseDto;
|
||||
let personMerge2: PersonResponseDto;
|
||||
let potentialMergePeople: PersonResponseDto[] = [];
|
||||
|
@ -70,6 +79,20 @@
|
|||
});
|
||||
});
|
||||
|
||||
const loadNextPage = async () => {
|
||||
if (!nextPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { people: newPeople, hasNextPage } = await getAllPeople({ withHidden: true, page: nextPage });
|
||||
people = people.concat(newPeople);
|
||||
nextPage = hasNextPage ? nextPage + 1 : null;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_load_people'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE);
|
||||
if (getSearchedPeople !== searchName) {
|
||||
|
@ -316,18 +339,22 @@
|
|||
</svelte:fragment>
|
||||
|
||||
{#if countVisiblePeople > 0 && (!searchName || searchedPeopleLocal.length > 0)}
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
|
||||
{#each showPeople as person, index (person.id)}
|
||||
<PeopleCard
|
||||
{person}
|
||||
preload={index < 20}
|
||||
on:change-name={() => handleChangeName(person)}
|
||||
on:set-birth-date={() => handleSetBirthDate(person)}
|
||||
on:merge-people={() => handleMergePeople(person)}
|
||||
on:hide-person={() => handleHidePerson(person)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<PeopleInfiniteScroll
|
||||
people={showPeople}
|
||||
hasNextPage={!!nextPage && !searchName}
|
||||
{loadNextPage}
|
||||
let:person
|
||||
let:index
|
||||
>
|
||||
<PeopleCard
|
||||
{person}
|
||||
preload={index < 20}
|
||||
on:change-name={() => handleChangeName(person)}
|
||||
on:set-birth-date={() => handleSetBirthDate(person)}
|
||||
on:merge-people={() => handleMergePeople(person)}
|
||||
on:hide-person={() => handleHidePerson(person)}
|
||||
/>
|
||||
</PeopleInfiniteScroll>
|
||||
{:else}
|
||||
<div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white">
|
||||
<div class="flex flex-col content-center items-center text-center">
|
||||
|
@ -385,6 +412,12 @@
|
|||
aria-labelledby="manage-visibility-title"
|
||||
use:focusTrap
|
||||
>
|
||||
<ManagePeopleVisibility bind:people titleId="manage-visibility-title" onClose={() => (selectHidden = false)} />
|
||||
<ManagePeopleVisibility
|
||||
bind:people
|
||||
totalPeopleCount={data.people.total}
|
||||
titleId="manage-visibility-title"
|
||||
onClose={() => (selectHidden = false)}
|
||||
{loadNextPage}
|
||||
/>
|
||||
</section>
|
||||
{/if}
|
||||
|
|
Loading…
Reference in a new issue