Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement attachment viewer gallery in record sets #3363

Merged
merged 31 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
786ae34
Implement attachment viewer gallery in record sets
CarolineDenis Apr 17, 2023
c4b965b
Fix failing tests, change localized strings
CarolineDenis Apr 18, 2023
319ec82
Allow to create record set from express search results
maxpatiiuk Mar 15, 2023
0dafeaa
Lint code with ESLint and Prettier
maxpatiiuk Apr 10, 2023
23e1586
Implement attachment viewer gallery in record sets
CarolineDenis Apr 17, 2023
66f5d4c
Merge branch 'issue-2132' of https://github.com/specify/specify7 into…
CarolineDenis Apr 25, 2023
69c67d0
Change variable name
CarolineDenis Apr 25, 2023
b596831
Fix failing tests
CarolineDenis Apr 25, 2023
082e72c
Implement attachment viewer gallery in record sets
CarolineDenis Apr 26, 2023
fbf0c2c
Lint code with ESLint and Prettier
CarolineDenis Apr 26, 2023
cef641f
Merge branch 'issue-2132' of https://github.com/specify/specify7 into…
CarolineDenis Apr 26, 2023
f79e691
Implement attachment viewer gallery in record sets
CarolineDenis Apr 26, 2023
0edc050
Merge branch 'issue-2132' of https://github.com/specify/specify7 into…
CarolineDenis Apr 28, 2023
5921aa6
Lint code with ESLint and Prettier
CarolineDenis Apr 28, 2023
9e7d597
Allow to make changes to atatchment
CarolineDenis Apr 28, 2023
a3fb90a
Remove obstrusive loading dialog
CarolineDenis Apr 28, 2023
8c43fb0
Display previous fetched attachments while new ones are being fetched
CarolineDenis May 1, 2023
0b58ab2
Lint code with ESLint and Prettier
CarolineDenis May 1, 2023
bdf456b
Simplify code
CarolineDenis May 1, 2023
a277044
Lint code with ESLint and Prettier
CarolineDenis May 1, 2023
d3f995f
Change shadows in attachment gallery
CarolineDenis May 4, 2023
7053ebd
Change the minimum width value for attachment dialog
CarolineDenis May 4, 2023
772178a
Lint code with ESLint and Prettier
CarolineDenis May 4, 2023
dab39e4
Merge remote-tracking branch 'origin/production' into issue-2132
CarolineDenis May 19, 2023
86203e1
Lint code with ESLint and Prettier
CarolineDenis May 19, 2023
86b120a
Delete loading gif for attachment thumbnail
CarolineDenis May 22, 2023
9d0ac45
Merge remote-tracking branch 'origin/production' into issue-2132
CarolineDenis Sep 25, 2023
d347557
Disable attachment set until attachments are defined
CarolineDenis Sep 25, 2023
cbf4129
Lint code with ESLint and Prettier
CarolineDenis Sep 25, 2023
983a29a
Merge remote-tracking branch 'origin/production' into issue-2132
CarolineDenis Oct 30, 2023
1753216
Merge branch 'production' into issue-2132
CarolineDenis Nov 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function AttachmentDialog({
{form !== null && (
<SaveButton
form={form}
resource={resource}
resource={related[0] ?? resource}
onAdd={undefined}
onSaved={(): void => {
handleChange(serializeResource(resource));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export function AttachmentGallery({
readonly onFetchMore: (() => Promise<void>) | undefined;
readonly scale: number;
readonly isComplete: boolean;
readonly onChange: (attachments: RA<SerializedResource<Attachment>>) => void;
readonly onChange: (
attachment: SerializedResource<Attachment>,
index: number
) => void;
}): JSX.Element {
const containerRef = React.useRef<HTMLElement | null>(null);

Expand Down Expand Up @@ -61,6 +64,7 @@ export function AttachmentGallery({
const [openIndex, setOpenIndex] = React.useState<number | undefined>(
undefined
);

const [related, setRelated] = React.useState<
RA<SpecifyResource<AnySchema> | undefined>
>([]);
Expand Down Expand Up @@ -121,7 +125,7 @@ export function AttachmentGallery({
(item): void => setRelated(replaceItem(related, openIndex, item)),
]}
onChange={(newAttachment): void =>
handleChange(replaceItem(attachments, openIndex, newAttachment))
handleChange(newAttachment, openIndex)
}
onClose={(): void => setOpenIndex(undefined)}
onNext={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React from 'react';

import { useAsyncState } from '../../hooks/useAsyncState';
import { useBooleanState } from '../../hooks/useBooleanState';
import { useCachedState } from '../../hooks/useCachedState';
import { attachmentsText } from '../../localization/attachments';
import { commonText } from '../../localization/common';
import { f } from '../../utils/functools';
import type { RA } from '../../utils/types';
import { filterArray } from '../../utils/types';
import { Button } from '../Atoms/Button';
import { serializeResource } from '../DataModel/helpers';
import type { AnySchema } from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import type { CollectionObjectAttachment } from '../DataModel/types';
import { Dialog } from '../Molecules/Dialog';
import { defaultAttachmentScale } from '.';
import { AttachmentGallery } from './Gallery';

const haltIncrementSize = 300;

export function RecordSetAttachments<SCHEMA extends AnySchema>({
records,
onFetch: handleFetch,
}: {
readonly records: RA<SpecifyResource<SCHEMA> | undefined>;
readonly onFetch:
| ((index: number) => Promise<RA<number | undefined> | void>)
| undefined;
}): JSX.Element {
const fetchedCount = React.useRef<number>(0);

const [showAttachments, handleShowAttachments, handleHideAttachments] =
useBooleanState();

const [attachments] = useAsyncState(
React.useCallback(async () => {
const relatedAttachmentRecords = await Promise.all(
records.map((record) =>
record
?.rgetCollection(`${record.specifyModel.name}Attachments`)
.then(
({ models }) =>
models as RA<SpecifyResource<CollectionObjectAttachment>>
)
)
);

const fetchCount = records.findIndex(
(record) => record?.populated !== true
);

fetchedCount.current = fetchCount === -1 ? records.length : fetchCount;

const attachements = await Promise.all(
filterArray(relatedAttachmentRecords.flat()).map(
async (collectionObjectAttachment) => ({
attachment: await collectionObjectAttachment
.rgetPromise('attachment')
.then((resource) => serializeResource(resource)),
related: collectionObjectAttachment,
})
)
);

return {
attachments: attachements.map(({ attachment }) => attachment),
related: attachements.map(({ related }) => related),
};
}, [records]),
false
);
const attachmentsRef = React.useRef(attachments);

if (typeof attachments === 'object') attachmentsRef.current = attachments;

/*
* Stop fetching records if the first 300 don't have attachments
* to save computing resources. Ask the user to continue and fetch
* the next haltIncrementSize (300) if desired.
*/
const [haltValue, setHaltValue] = React.useState(300);
CarolineDenis marked this conversation as resolved.
Show resolved Hide resolved
const halt =
attachments?.attachments.length === 0 && records.length >= haltValue;

const [scale = defaultAttachmentScale] = useCachedState(
'attachments',
'scale'
);

return (
<>
<Button.Icon
icon="photos"
title="attachments"
onClick={handleShowAttachments}
/>
{showAttachments && (
<Dialog
buttons={
<Button.DialogClose>{commonText.close()}</Button.DialogClose>
}
header={
attachmentsRef.current?.attachments === undefined
? attachmentsText.attachments()
: commonText.countLine({
resource: attachmentsText.attachments(),
count: attachmentsRef.current.attachments.length,
})
}
onClose={handleHideAttachments}
>
{halt ? (
haltValue === records.length ? (
<>{attachmentsText.noAttachments()}</>
) : (
<div className="flex flex-col gap-4">
{attachmentsText.attachmentHaltLimit({ halt: haltValue })}
<Button.Orange
onClick={(): void =>
setHaltValue(
Math.min(haltValue + haltIncrementSize, records.length)
)
}
>
{attachmentsText.fetchNextAttachments()}
</Button.Orange>
</div>
)
) : (
<AttachmentGallery
attachments={attachmentsRef?.current?.attachments ?? []}
isComplete={fetchedCount.current === records.length}
scale={scale}
onChange={(attachment, index): void =>
void attachments?.related[index].set(`attachment`, attachment)
}
onFetchMore={
attachments === undefined || handleFetch === undefined || halt
? undefined
: async (): Promise<void> =>
handleFetch?.(fetchedCount.current).then(f.void)
}
/>
)}
</Dialog>
)}
</>
);
}
12 changes: 8 additions & 4 deletions specifyweb/frontend/js_src/lib/components/Attachments/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { commonText } from '../../localization/common';
import { schemaText } from '../../localization/schema';
import { f } from '../../utils/functools';
import { filterArray } from '../../utils/types';
import { replaceItem } from '../../utils/utils';
import { Container, H2 } from '../Atoms';
import { className } from '../Atoms/className';
import { Input, Label, Select } from '../Atoms/Form';
Expand Down Expand Up @@ -47,7 +48,7 @@ export const tablesWithAttachments = f.store(() =>
)
);

const defaultScale = 10;
export const defaultAttachmentScale = 10;
const minScale = 4;
const maxScale = 50;
const defaultSortOrder = '-timestampCreated';
Expand Down Expand Up @@ -124,7 +125,7 @@ function Attachments(): JSX.Element {
false
);

const [scale = defaultScale, setScale] = useCachedState(
const [scale = defaultAttachmentScale, setScale] = useCachedState(
'attachments',
'scale'
);
Expand Down Expand Up @@ -242,10 +243,13 @@ function Attachments(): JSX.Element {
}
key={`${order}_${JSON.stringify(filter)}`}
scale={scale}
onChange={(records): void =>
onChange={(attachment, index): void =>
collection === undefined
? undefined
: setCollection({ records, totalCount: collection.totalCount })
: setCollection({
records: replaceItem(collection.records, index, attachment),
totalCount: collection.totalCount,
})
}
onFetchMore={collection === undefined ? undefined : fetchMore}
/>
Expand Down
Loading