Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 31 additions & 1 deletion src/layout/ImageUpload/ImageControllers.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React from 'react';

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

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

type ImageControllersProps = {
zoom: number;
zoomLimits: { minZoom: number; maxZoom: number };
baseComponentId: string;
onSave: () => void;
onDelete: () => void;
onCancel: () => void;
updateZoom: (zoom: number) => void;
onFileUploaded: (file: File) => void;
Expand All @@ -19,12 +22,16 @@ type ImageControllersProps = {
export function ImageControllers({
zoom,
zoomLimits: { minZoom, maxZoom },
baseComponentId,
onSave,
onDelete,
onCancel,
updateZoom,
onFileUploaded,
onReset,
}: ImageControllersProps) {
const { storedImageLink } = useImageFile(baseComponentId);

const handleSliderZoom = (e: React.ChangeEvent<HTMLInputElement>) => {
const logarithmicZoomValue = normalToLogZoom({
value: parseFloat(e.target.value),
Expand All @@ -43,6 +50,29 @@ export function ImageControllers({
e.target.value = '';
};

if (storedImageLink) {
return (
<div className={classes.actionButtons}>
<Button
data-size='sm'
variant='secondary'
data-color='accent'
asChild
>
<Link href={storedImageLink}>Last ned bildet</Link>
</Button>
<Button
data-size='sm'
variant='secondary'
data-color='danger'
onClick={onDelete}
>
Slett bildet
</Button>
</div>
);
}

return (
<div className={classes.controlsContainer}>
<div>
Expand Down
7 changes: 0 additions & 7 deletions src/layout/ImageUpload/ImageUpload.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,6 @@ div:has(.canvas) {
line-height: 1.75rem;
}

/* Icons */
.icon {
margin-right: 0.5rem;
height: 1.25rem;
width: 1.25rem;
}

.dropZone {
border-radius: var(--ds-border-radius-lg);
border: 2px dashed var(--ds-color-border-default);
Expand Down
128 changes: 57 additions & 71 deletions src/layout/ImageUpload/ImageUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import { UploadIcon as Upload } from '@navikt/aksel-icons';

import { AppCard } from 'src/app-components/Card/Card';
import { useAttachmentsUploader } from 'src/features/attachments/hooks';
import { useIsMobileOrTablet } from 'src/hooks/useDeviceWidths';
import { DropzoneComponent } from 'src/layout/FileUpload/DropZone/DropzoneComponent';
import { ImageCanvas } from 'src/layout/ImageUpload/ImageCanvas';
Expand All @@ -15,23 +14,20 @@ import {
drawViewport,
getViewport,
} from 'src/layout/ImageUpload/imageUploadUtils';
import { useIndexedId } from 'src/utils/layout/DataModelLocation';
import type { IDataModelBindingsSimple } from 'src/layout/common.generated';
import { useImageFile } from 'src/layout/ImageUpload/useImageFile';
import type { Position, ViewportType } from 'src/layout/ImageUpload/imageUploadUtils';

interface ImageCropperProps {
dataModelBindings?: IDataModelBindingsSimple;
viewport?: ViewportType;
baseComponentId: string;
}

const MAX_ZOOM = 5;

// ImageCropper Component
export function ImageCropper({ baseComponentId, viewport, dataModelBindings }: ImageCropperProps) {
export function ImageCropper({ baseComponentId, viewport }: ImageCropperProps) {
const mobileView = useIsMobileOrTablet();
const indexedId = useIndexedId(baseComponentId);
const uploadAttachment = useAttachmentsUploader();
const { saveImage, deleteImage } = useImageFile(baseComponentId);

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

const selectedViewport = getViewport(viewport);

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

// Use the file now
uploadAttachment({
files: [imageFile],
nodeId: indexedId,
dataModelBindings,
});
setPreviewImage(cropCanvas.toDataURL('image/png'));
saveImage(imageFile);
}, 'image/png');
};

const handleDeleteImage = () => {
deleteImage();
imageRef.current = null;
setImageSrc(null);
handleReset();
};

return (
<>
<AppCard
variant='default'
mediaPosition='top'
className={classes.imageUploadCard}
media={
imageSrc ? (
<ImageCanvas
canvasRef={canvasRef}
imageRef={imageRef}
zoom={zoom}
position={position}
viewport={selectedViewport}
onPositionChange={handlePositionChange}
onZoomChange={handleZoomChange}
/>
) : (
<div className={classes.canvasSizingWrapper}>
<div className={classes.placeholder}>
<Upload className={classes.placeholderIcon} />
<p className={classes.placeholderText}>Upload an image to start cropping</p>
</div>
</div>
)
}
>
{imageSrc ? (
<ImageControllers
<AppCard
variant='default'
mediaPosition='top'
className={classes.imageUploadCard}
media={
imageSrc ? (
<ImageCanvas
canvasRef={canvasRef}
imageRef={imageRef}
zoom={zoom}
zoomLimits={{ minZoom: minAllowedZoom, maxZoom: MAX_ZOOM }}
updateZoom={handleZoomChange}
onSave={handleSave}
onCancel={() => setImageSrc(null)}
onFileUploaded={handleFileUpload}
onReset={handleReset}
position={position}
viewport={selectedViewport}
onPositionChange={handlePositionChange}
onZoomChange={handleZoomChange}
/>
) : (
<DropzoneComponent
id='image-upload'
isMobile={mobileView}
readOnly={false}
onClick={(e) => e.preventDefault()}
onDrop={(files) => handleFileUpload(files[0])}
hasValidationMessages={false}
validFileEndings={['.jpg', '.jpeg', '.png', '.gif']}
className={classes.dropZone}
/>
)}
</AppCard>
{/*Fjern dette under senere*/}
{previewImage && (
<a
href={previewImage}
download='cropped-image.png'
>
Download Image
</a>
<div className={classes.canvasSizingWrapper}>
<div className={classes.placeholder}>
<Upload className={classes.placeholderIcon} />
<p className={classes.placeholderText}>Upload an image to start cropping</p>
</div>
</div>
)
}
>
{imageSrc ? (
<ImageControllers
zoom={zoom}
zoomLimits={{ minZoom: minAllowedZoom, maxZoom: MAX_ZOOM }}
baseComponentId={baseComponentId}
updateZoom={handleZoomChange}
onSave={handleSave}
onDelete={handleDeleteImage}
onCancel={() => setImageSrc(null)}
onFileUploaded={handleFileUpload}
onReset={handleReset}
/>
) : (
<DropzoneComponent
id='image-upload'
isMobile={mobileView}
readOnly={false}
onClick={(e) => e.preventDefault()}
onDrop={(files) => handleFileUpload(files[0])}
hasValidationMessages={false}
validFileEndings={['.jpg', '.jpeg', '.png', '.gif']}
className={classes.dropZone}
/>
)}
</>
</AppCard>
);
}
3 changes: 1 addition & 2 deletions src/layout/ImageUpload/ImageUploadComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import type { PropsFromGenericComponent } from 'src/layout';
import type { ViewportType } from 'src/layout/ImageUpload/imageUploadUtils';

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

return (
<ComponentStructureWrapper baseComponentId={baseComponentId}>
<ImageCropper
viewport={viewport as ViewportType}
baseComponentId={baseComponentId}
dataModelBindings={dataModelBindings}
/>
</ComponentStructureWrapper>
);
Expand Down
51 changes: 51 additions & 0 deletions src/layout/ImageUpload/useImageFile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useAttachmentsFor, useAttachmentsRemover, useAttachmentsUploader } from 'src/features/attachments/hooks';
import { useLaxInstanceId } from 'src/features/instance/InstanceContext';
import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
import { useIndexedId } from 'src/utils/layout/DataModelLocation';
import { useItemWhenType } from 'src/utils/layout/useNodeItem';
import { getDataElementUrl } from 'src/utils/urls/appUrlHelper';
import { makeUrlRelativeIfSameDomain } from 'src/utils/urls/urlHelper';
import type { UploadedAttachment } from 'src/features/attachments';

type ReturnType = {
storedImageLink: string | undefined;
saveImage: (file: File) => void;
deleteImage: () => void;
};

export const useImageFile = (baseComponentId: string): ReturnType => {
const { dataModelBindings } = useItemWhenType(baseComponentId, 'ImageUpload');
const indexedId = useIndexedId(baseComponentId);
const uploadImage = useAttachmentsUploader();
const removeImage = useAttachmentsRemover();
const storedImage = useAttachmentsFor(baseComponentId)[0] as UploadedAttachment | undefined;
const language = useCurrentLanguage();
const instanceId = useLaxInstanceId();

const storedImageLink =
storedImage &&
instanceId &&
makeUrlRelativeIfSameDomain(getDataElementUrl(instanceId, storedImage.data.id, language));

const saveImage = (file: File) => {
uploadImage({
files: [file],
nodeId: indexedId,
dataModelBindings,
});
};

const deleteImage = () => {
if (!storedImage?.uploaded) {
return;
}

removeImage({
attachment: storedImage,
nodeId: indexedId,
dataModelBindings,
});
};

return { storedImageLink, saveImage, deleteImage };
};
Loading