Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -62,7 +62,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 @@ -29,7 +29,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;
readonly onClick?: (attachment: SerializedResource<Attachment>) => void;
}): JSX.Element {
const containerRef = React.useRef<HTMLElement | null>(null);
Expand Down Expand Up @@ -63,6 +66,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 @@ -129,7 +133,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
Expand Up @@ -3,7 +3,6 @@ import React from 'react';
import { useAsyncState } from '../../hooks/useAsyncState';
import type { SerializedResource } from '../DataModel/helperTypes';
import type { Attachment } from '../DataModel/types';
import { loadingGif } from '../Molecules';
import type { AttachmentThumbnail } from './attachments';
import { fetchThumbnail } from './attachments';

Expand Down Expand Up @@ -37,23 +36,23 @@ export function AttachmentPreview({
export function Thumbnail({
attachment,
thumbnail,
className,
}: {
readonly attachment: SerializedResource<Attachment>;
readonly thumbnail: AttachmentThumbnail | undefined;
}): JSX.Element {
return thumbnail === undefined ? (
loadingGif
) : (
readonly className?: string;
}): JSX.Element | null {
return thumbnail === undefined ? null : (
<img
alt={
typeof attachment.title === 'string' && attachment.title.length > 0
? attachment.title
: thumbnail.alt
}
className={`
max-h-full max-w-full border-2 border-white object-contain
dark:border-black
`}
${className}
max-h-full max-w-full border-2 border-white object-contain
dark:border-black`}
src={thumbnail.src}
style={{
width: `${thumbnail.width}px`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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, dialogClassNames } 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(async (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 attachments = await Promise.all(
filterArray(relatedAttachmentRecords.flat()).map(
async (collectionObjectAttachment) => ({
attachment: await collectionObjectAttachment
.rgetPromise('attachment')
.then((resource) => serializeResource(resource)),
related: collectionObjectAttachment,
})
)
);

return {
attachments: attachments.map(({ attachment }) => attachment),
related: attachments.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(39);
const halt =
attachments?.attachments.length === 0 && records.length >= haltValue;

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

return (
<>
<Button.Icon
disabled={attachments === undefined}
icon="photos"
title="attachments"
onClick={handleShowAttachments}
/>
{showAttachments && (
<Dialog
buttons={
<Button.DialogClose>{commonText.close()}</Button.DialogClose>
}
className={{
container: dialogClassNames.wideContainer,
}}
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.Warning
onClick={(): void =>
setHaltValue(
Math.min(haltValue + haltIncrementSize, records.length)
)
}
>
{attachmentsText.fetchNextAttachments()}
</Button.Warning>
</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 { DialogContext } from '../Atoms/Button';
import { className } from '../Atoms/className';
Expand Down Expand Up @@ -49,7 +50,7 @@ export const tablesWithAttachments = f.store(() =>
)
);

export const defaultScale = 10;
export const defaultAttachmentScale = 10;
const minScale = 4;
const maxScale = 50;
const defaultSortOrder = '-timestampCreated';
Expand Down Expand Up @@ -136,7 +137,7 @@ function Attachments({
false
);

const [scale = defaultScale, setScale] = useCachedState(
const [scale = defaultAttachmentScale, setScale] = useCachedState(
'attachments',
'scale'
);
Expand Down Expand Up @@ -257,10 +258,13 @@ function Attachments({
}
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,
})
}
onClick={onClick}
onFetchMore={collection === undefined ? undefined : fetchMore}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { RA } from '../../utils/types';
import { filterArray } from '../../utils/types';
import { Button } from '../Atoms/Button';
import { icons } from '../Atoms/Icons';
import { defaultScale } from '../Attachments';
import { defaultAttachmentScale } from '../Attachments';
import { AttachmentGallery } from '../Attachments/Gallery';
import { serializeResource } from '../DataModel/helpers';
import type { AnySchema, SerializedResource } from '../DataModel/helperTypes';
Expand All @@ -27,7 +27,10 @@ export function AttachmentsCollection({
const [showAllAttachments, handleOpenAttachments, handleCloseAttachments] =
useBooleanState();

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

const attachments: RA<SerializedResource<Attachment>> = filterArray(
Array.from(collection.models, (model) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type { RA } from '../../utils/types';
import { removeItem } from '../../utils/utils';
import { Button } from '../Atoms/Button';
import { DataEntry } from '../Atoms/DataEntry';
import { tablesWithAttachments } from '../Attachments';
import { RecordSetAttachments } from '../Attachments/RecordSetAttachment';
import type { AnySchema } from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import { schema } from '../DataModel/schema';
Expand Down Expand Up @@ -46,6 +48,7 @@ export function RecordSelectorFromIds<SCHEMA extends AnySchema>({
onAdd: handleAdd,
onClone: handleClone,
onDelete: handleDelete,
onFetch: handleFetch,
...rest
}: Omit<RecordSelectorProps<SCHEMA>, 'index' | 'records'> & {
/*
Expand All @@ -70,6 +73,9 @@ export function RecordSelectorFromIds<SCHEMA extends AnySchema>({
readonly onClone:
| ((newResource: SpecifyResource<SCHEMA>) => void)
| undefined;
readonly onFetch?: (
index: number
) => Promise<RA<number | undefined> | undefined>;
}): JSX.Element | null {
const [records, setRecords] = React.useState<
RA<SpecifyResource<SCHEMA> | undefined>
Expand Down Expand Up @@ -176,6 +182,8 @@ export function RecordSelectorFromIds<SCHEMA extends AnySchema>({
})
: commonText.delete();

const hasAttachments = tablesWithAttachments().includes(model);

return (
<>
<ResourceView
Expand All @@ -189,7 +197,6 @@ export function RecordSelectorFromIds<SCHEMA extends AnySchema>({
!isDependent && dialog !== false ? resource : undefined
}
/>

{hasTablePermission(
model.name,
isDependent ? 'create' : 'read'
Expand All @@ -201,7 +208,6 @@ export function RecordSelectorFromIds<SCHEMA extends AnySchema>({
onClick={handleAdding}
/>
) : undefined}

{typeof handleRemove === 'function' && canRemove ? (
<DataEntry.Remove
aria-label={removeLabel}
Expand All @@ -210,15 +216,16 @@ export function RecordSelectorFromIds<SCHEMA extends AnySchema>({
onClick={(): void => handleRemove('minusButton')}
/>
) : undefined}

{typeof newResource === 'object' && handleAdd !== undefined ? (
<p className="flex-1">{formsText.creatingNewRecord()}</p>
) : (
<span
className={`flex-1 ${dialog === false ? '-ml-2' : '-ml-4'}`}
/>
)}

{hasAttachments && (
<RecordSetAttachments records={records} onFetch={handleFetch} />
)}
{specifyNetworkBadge}
</div>
{totalCount > 1 && <div>{slider}</div>}
Expand Down
Loading