Skip to content

Commit bcaf56f

Browse files
authored
feat/ handle saved image and new display when image is saved (#3675)
* add hook that handles the saved image and controllers buttons when saved
1 parent 658e9fd commit bcaf56f

File tree

5 files changed

+140
-81
lines changed

5 files changed

+140
-81
lines changed

src/layout/ImageUpload/ImageControllers.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import React from 'react';
22

3-
import { Button, Input, Label } from '@digdir/designsystemet-react';
3+
import { Button, Input, Label, Link } from '@digdir/designsystemet-react';
44
import { ArrowsSquarepathIcon, ArrowUndoIcon } from '@navikt/aksel-icons';
55

66
import classes from 'src/layout/ImageUpload/ImageControllers.module.css';
77
import { logToNormalZoom, normalToLogZoom } from 'src/layout/ImageUpload/imageUploadUtils';
8+
import { useImageFile } from 'src/layout/ImageUpload/useImageFile';
89

910
type ImageControllersProps = {
1011
zoom: number;
1112
zoomLimits: { minZoom: number; maxZoom: number };
13+
baseComponentId: string;
1214
onSave: () => void;
15+
onDelete: () => void;
1316
onCancel: () => void;
1417
updateZoom: (zoom: number) => void;
1518
onFileUploaded: (file: File) => void;
@@ -19,12 +22,16 @@ type ImageControllersProps = {
1922
export function ImageControllers({
2023
zoom,
2124
zoomLimits: { minZoom, maxZoom },
25+
baseComponentId,
2226
onSave,
27+
onDelete,
2328
onCancel,
2429
updateZoom,
2530
onFileUploaded,
2631
onReset,
2732
}: ImageControllersProps) {
33+
const { storedImageLink } = useImageFile(baseComponentId);
34+
2835
const handleSliderZoom = (e: React.ChangeEvent<HTMLInputElement>) => {
2936
const logarithmicZoomValue = normalToLogZoom({
3037
value: parseFloat(e.target.value),
@@ -43,6 +50,29 @@ export function ImageControllers({
4350
e.target.value = '';
4451
};
4552

53+
if (storedImageLink) {
54+
return (
55+
<div className={classes.actionButtons}>
56+
<Button
57+
data-size='sm'
58+
variant='secondary'
59+
data-color='accent'
60+
asChild
61+
>
62+
<Link href={storedImageLink}>Last ned bildet</Link>
63+
</Button>
64+
<Button
65+
data-size='sm'
66+
variant='secondary'
67+
data-color='danger'
68+
onClick={onDelete}
69+
>
70+
Slett bildet
71+
</Button>
72+
</div>
73+
);
74+
}
75+
4676
return (
4777
<div className={classes.controlsContainer}>
4878
<div>

src/layout/ImageUpload/ImageUpload.module.css

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,6 @@ div:has(.canvas) {
5959
line-height: 1.75rem;
6060
}
6161

62-
/* Icons */
63-
.icon {
64-
margin-right: 0.5rem;
65-
height: 1.25rem;
66-
width: 1.25rem;
67-
}
68-
6962
.dropZone {
7063
border-radius: var(--ds-border-radius-lg);
7164
border: 2px dashed var(--ds-color-border-default);

src/layout/ImageUpload/ImageUpload.tsx

Lines changed: 57 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
33
import { UploadIcon as Upload } from '@navikt/aksel-icons';
44

55
import { AppCard } from 'src/app-components/Card/Card';
6-
import { useAttachmentsUploader } from 'src/features/attachments/hooks';
76
import { useIsMobileOrTablet } from 'src/hooks/useDeviceWidths';
87
import { DropzoneComponent } from 'src/layout/FileUpload/DropZone/DropzoneComponent';
98
import { ImageCanvas } from 'src/layout/ImageUpload/ImageCanvas';
@@ -15,23 +14,20 @@ import {
1514
drawViewport,
1615
getViewport,
1716
} from 'src/layout/ImageUpload/imageUploadUtils';
18-
import { useIndexedId } from 'src/utils/layout/DataModelLocation';
19-
import type { IDataModelBindingsSimple } from 'src/layout/common.generated';
17+
import { useImageFile } from 'src/layout/ImageUpload/useImageFile';
2018
import type { Position, ViewportType } from 'src/layout/ImageUpload/imageUploadUtils';
2119

2220
interface ImageCropperProps {
23-
dataModelBindings?: IDataModelBindingsSimple;
2421
viewport?: ViewportType;
2522
baseComponentId: string;
2623
}
2724

2825
const MAX_ZOOM = 5;
2926

3027
// ImageCropper Component
31-
export function ImageCropper({ baseComponentId, viewport, dataModelBindings }: ImageCropperProps) {
28+
export function ImageCropper({ baseComponentId, viewport }: ImageCropperProps) {
3229
const mobileView = useIsMobileOrTablet();
33-
const indexedId = useIndexedId(baseComponentId);
34-
const uploadAttachment = useAttachmentsUploader();
30+
const { saveImage, deleteImage } = useImageFile(baseComponentId);
3531

3632
// Refs for canvas and image
3733
const canvasRef = useRef<HTMLCanvasElement | null>(null);
@@ -41,8 +37,6 @@ export function ImageCropper({ baseComponentId, viewport, dataModelBindings }: I
4137
const [zoom, setZoom] = useState<number>(1);
4238
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
4339
const [imageSrc, setImageSrc] = useState<File | null>(null);
44-
//bare midlertidig for å kunne laste ned resultatet som blir lagret i backend
45-
const [previewImage, setPreviewImage] = React.useState<string | null>(null);
4640

4741
const selectedViewport = getViewport(viewport);
4842

@@ -141,75 +135,67 @@ export function ImageCropper({ baseComponentId, viewport, dataModelBindings }: I
141135
const fileName = img?.name || 'cropped-image.png';
142136
const imageFile = new File([blob], fileName, { type: 'image/png' });
143137

144-
// Use the file now
145-
uploadAttachment({
146-
files: [imageFile],
147-
nodeId: indexedId,
148-
dataModelBindings,
149-
});
150-
setPreviewImage(cropCanvas.toDataURL('image/png'));
138+
saveImage(imageFile);
151139
}, 'image/png');
152140
};
153141

142+
const handleDeleteImage = () => {
143+
deleteImage();
144+
imageRef.current = null;
145+
setImageSrc(null);
146+
handleReset();
147+
};
148+
154149
return (
155-
<>
156-
<AppCard
157-
variant='default'
158-
mediaPosition='top'
159-
className={classes.imageUploadCard}
160-
media={
161-
imageSrc ? (
162-
<ImageCanvas
163-
canvasRef={canvasRef}
164-
imageRef={imageRef}
165-
zoom={zoom}
166-
position={position}
167-
viewport={selectedViewport}
168-
onPositionChange={handlePositionChange}
169-
onZoomChange={handleZoomChange}
170-
/>
171-
) : (
172-
<div className={classes.canvasSizingWrapper}>
173-
<div className={classes.placeholder}>
174-
<Upload className={classes.placeholderIcon} />
175-
<p className={classes.placeholderText}>Upload an image to start cropping</p>
176-
</div>
177-
</div>
178-
)
179-
}
180-
>
181-
{imageSrc ? (
182-
<ImageControllers
150+
<AppCard
151+
variant='default'
152+
mediaPosition='top'
153+
className={classes.imageUploadCard}
154+
media={
155+
imageSrc ? (
156+
<ImageCanvas
157+
canvasRef={canvasRef}
158+
imageRef={imageRef}
183159
zoom={zoom}
184-
zoomLimits={{ minZoom: minAllowedZoom, maxZoom: MAX_ZOOM }}
185-
updateZoom={handleZoomChange}
186-
onSave={handleSave}
187-
onCancel={() => setImageSrc(null)}
188-
onFileUploaded={handleFileUpload}
189-
onReset={handleReset}
160+
position={position}
161+
viewport={selectedViewport}
162+
onPositionChange={handlePositionChange}
163+
onZoomChange={handleZoomChange}
190164
/>
191165
) : (
192-
<DropzoneComponent
193-
id='image-upload'
194-
isMobile={mobileView}
195-
readOnly={false}
196-
onClick={(e) => e.preventDefault()}
197-
onDrop={(files) => handleFileUpload(files[0])}
198-
hasValidationMessages={false}
199-
validFileEndings={['.jpg', '.jpeg', '.png', '.gif']}
200-
className={classes.dropZone}
201-
/>
202-
)}
203-
</AppCard>
204-
{/*Fjern dette under senere*/}
205-
{previewImage && (
206-
<a
207-
href={previewImage}
208-
download='cropped-image.png'
209-
>
210-
Download Image
211-
</a>
166+
<div className={classes.canvasSizingWrapper}>
167+
<div className={classes.placeholder}>
168+
<Upload className={classes.placeholderIcon} />
169+
<p className={classes.placeholderText}>Upload an image to start cropping</p>
170+
</div>
171+
</div>
172+
)
173+
}
174+
>
175+
{imageSrc ? (
176+
<ImageControllers
177+
zoom={zoom}
178+
zoomLimits={{ minZoom: minAllowedZoom, maxZoom: MAX_ZOOM }}
179+
baseComponentId={baseComponentId}
180+
updateZoom={handleZoomChange}
181+
onSave={handleSave}
182+
onDelete={handleDeleteImage}
183+
onCancel={() => setImageSrc(null)}
184+
onFileUploaded={handleFileUpload}
185+
onReset={handleReset}
186+
/>
187+
) : (
188+
<DropzoneComponent
189+
id='image-upload'
190+
isMobile={mobileView}
191+
readOnly={false}
192+
onClick={(e) => e.preventDefault()}
193+
onDrop={(files) => handleFileUpload(files[0])}
194+
hasValidationMessages={false}
195+
validFileEndings={['.jpg', '.jpeg', '.png', '.gif']}
196+
className={classes.dropZone}
197+
/>
212198
)}
213-
</>
199+
</AppCard>
214200
);
215201
}

src/layout/ImageUpload/ImageUploadComponent.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@ import type { PropsFromGenericComponent } from 'src/layout';
77
import type { ViewportType } from 'src/layout/ImageUpload/imageUploadUtils';
88

99
export function ImageUploadComponent({ baseComponentId }: PropsFromGenericComponent<'ImageUpload'>) {
10-
const { viewport, dataModelBindings } = useItemWhenType(baseComponentId, 'ImageUpload');
10+
const { viewport } = useItemWhenType(baseComponentId, 'ImageUpload');
1111

1212
return (
1313
<ComponentStructureWrapper baseComponentId={baseComponentId}>
1414
<ImageCropper
1515
viewport={viewport as ViewportType}
1616
baseComponentId={baseComponentId}
17-
dataModelBindings={dataModelBindings}
1817
/>
1918
</ComponentStructureWrapper>
2019
);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useAttachmentsFor, useAttachmentsRemover, useAttachmentsUploader } from 'src/features/attachments/hooks';
2+
import { useLaxInstanceId } from 'src/features/instance/InstanceContext';
3+
import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
4+
import { useIndexedId } from 'src/utils/layout/DataModelLocation';
5+
import { useItemWhenType } from 'src/utils/layout/useNodeItem';
6+
import { getDataElementUrl } from 'src/utils/urls/appUrlHelper';
7+
import { makeUrlRelativeIfSameDomain } from 'src/utils/urls/urlHelper';
8+
import type { UploadedAttachment } from 'src/features/attachments';
9+
10+
type ReturnType = {
11+
storedImageLink: string | undefined;
12+
saveImage: (file: File) => void;
13+
deleteImage: () => void;
14+
};
15+
16+
export const useImageFile = (baseComponentId: string): ReturnType => {
17+
const { dataModelBindings } = useItemWhenType(baseComponentId, 'ImageUpload');
18+
const indexedId = useIndexedId(baseComponentId);
19+
const uploadImage = useAttachmentsUploader();
20+
const removeImage = useAttachmentsRemover();
21+
const storedImage = useAttachmentsFor(baseComponentId)[0] as UploadedAttachment | undefined;
22+
const language = useCurrentLanguage();
23+
const instanceId = useLaxInstanceId();
24+
25+
const storedImageLink =
26+
storedImage &&
27+
instanceId &&
28+
makeUrlRelativeIfSameDomain(getDataElementUrl(instanceId, storedImage.data.id, language));
29+
30+
const saveImage = (file: File) => {
31+
uploadImage({
32+
files: [file],
33+
nodeId: indexedId,
34+
dataModelBindings,
35+
});
36+
};
37+
38+
const deleteImage = () => {
39+
if (!storedImage?.uploaded) {
40+
return;
41+
}
42+
43+
removeImage({
44+
attachment: storedImage,
45+
nodeId: indexedId,
46+
dataModelBindings,
47+
});
48+
};
49+
50+
return { storedImageLink, saveImage, deleteImage };
51+
};

0 commit comments

Comments
 (0)