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

WIP: Variants Editor #177

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
16 changes: 16 additions & 0 deletions Classes/GraphQL/Resolver/Type/MutationResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -706,4 +706,20 @@ public function deleteTag($_, array $variables): bool

return true;
}

/**
* @throws Exception|IllegalObjectTypeException
*/
public function updateVariant($_, array $variables): ?Tag
{
[
'id' => $id,
'cropInformation' => $cropInformation,
] = $variables;

$variant = $this->assetRepository->findByIdentifier($id);


return $variant;
}
}
9 changes: 9 additions & 0 deletions Resources/Private/GraphQL/schema.root.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ type Mutation {
updateAssetCollection(id: AssetCollectionId!, title: String, tagIds: [TagId]): AssetCollection!

updateTag(id: TagId!, label: String): Tag!

updateVariant(id: AssetId!, cropInformation: CropInformationInput!): AssetVariant!
}

"""
Expand Down Expand Up @@ -211,6 +213,13 @@ type CropInformation {
y: Int
}

input CropInformationInput {
width: Int
height: Int
x: Int
y: Int
}

type UsageDetailsGroup {
serviceId: ServiceId!
label: String!
Expand Down
3 changes: 2 additions & 1 deletion Resources/Private/JavaScript/asset-variants/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"private": true,
"main": "src/index.ts",
"dependencies": {
"@media-ui/core": "*"
"@media-ui/core": "*",
"react-image-crop": "^10.0.7"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { FC } from 'react';
import ReactCrop, { PercentCrop, PixelCrop } from 'react-image-crop';

interface ImageCropPros {
aspectRatio: number;
currentCrop: any;
onCropChange(crop: PixelCrop, percentageCrop: PercentCrop): void;
onCropComplete(crop: PixelCrop, percentageCrop: PercentCrop): void;
originalPreviewUrl: string;
}

const ImageCrop: FC<ImageCropPros> = ({
aspectRatio,
currentCrop,
onCropChange,
onCropComplete,
originalPreviewUrl,
}: ImageCropPros) => {
return (
<ReactCrop
aspect={aspectRatio}
crop={currentCrop}
onChange={onCropChange}
onComplete={onCropComplete}
keepSelection={true}
>
<img src={originalPreviewUrl} />
</ReactCrop>
);
};

export default React.memo(ImageCrop);
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React from 'react';
import { createUseMediaUiStyles, MediaUiTheme } from '@media-ui/core/src';
import AssetVariant from '../interfaces/AssetVariant';
import useSelectVariant from '../hooks/useSelectVariant';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface VariantProps extends AssetVariant {}

const useStyles = createUseMediaUiStyles((theme: MediaUiTheme) => ({
variantContainer: {
variantContainer: ({ isCroppable }) => ({
backgroundColor: theme.colors.assetBackground,
},
cursor: isCroppable ? 'pointer' : 'default',
}),
picture: {
height: 200,
display: 'flex',
Expand Down Expand Up @@ -54,10 +56,18 @@ const Variant: React.FC<VariantProps> = ({
width,
height,
previewUrl,
id,
hasCrop,
}: VariantProps) => {
const classes = useStyles();
// TODO: Find out why we need to check both
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you wondering why there can exist variants without an preset identifier in a Neos installation?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess thats because custom crops (e.g. in Fusion) are also stored as variants and have no preset identifier?

I took that code from the current media browser version, and I am wondering why a variant with preset identifier should not be croppable, and more important: why does checking for an existing crop area ensure that the image is croppable. If I have a 16:9 image and a 16:9 preset, technically there is no need for a crop area over the whole image, but I should still be able to choose a smaller area

const isCroppable = presetIdentifier && hasCrop;
const classes = useStyles({ isCroppable });
const selectVariant = useSelectVariant();
const handleVariantClick = () => {
selectVariant(id);
};
return (
<div className={classes.variantContainer}>
<div className={classes.variantContainer} onClick={isCroppable && handleVariantClick}>
<picture className={classes.picture}>
<img className={classes.image} src={previewUrl} alt={variantName} />
</picture>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { FC, useEffect, useRef, useState } from 'react';
import { useCallback } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import 'react-image-crop/dist/ReactCrop.css';
import { Button, Dialog } from '@neos-project/react-ui-components';

import { useIntl, createUseMediaUiStyles, MediaUiTheme } from '@media-ui/core/src';
import { useSelectedAsset } from '@media-ui/core/src/hooks';

import assetVariantModalState from '../state/assetVariantModalState';
import useSelectedAssetVariant from '../hooks/useSelectedAssetVariant';
import selectedVariantIdState from '../state/selectedVariantIdState';
import ImageCrop from './ImageCrop';
import { PixelCrop } from 'react-image-crop';

const useStyles = createUseMediaUiStyles((theme: MediaUiTheme) => ({
cropContainer: {
display: 'flex',
justifyContent: 'center',
},
}));

const VariantModal: FC = () => {
const classes = useStyles();
const { translate } = useIntl();
const [isOpen, setIsOpen] = useRecoilState(assetVariantModalState);
const setSelectedVariantId = useSetRecoilState(selectedVariantIdState);
const asset = useSelectedAsset();
const assetVariant = useSelectedAssetVariant();
const [currentCrop, setCurrentCrop] = useState<PixelCrop>();
const [hasChanged, setHasChanged] = useState(false);
const aspectRatio = useRef<number>();
const handleRequestClose = useCallback(() => {
setSelectedVariantId(undefined);
setIsOpen(false);
}, [setIsOpen, setSelectedVariantId]);
const handleCropComplete = (pxCrop: PixelCrop) => {
const savedCropInformation = assetVariant.cropInformation;
setHasChanged(
savedCropInformation?.x !== pxCrop.x ||
savedCropInformation?.y !== pxCrop.y ||
savedCropInformation?.width !== pxCrop.width ||
savedCropInformation?.height !== pxCrop.height
);
};

const cropHasChanged = () => {
const savedCropInformation = assetVariant.cropInformation;
return (
savedCropInformation?.x !== currentCrop.x ||
savedCropInformation?.y !== currentCrop.y ||
savedCropInformation?.width !== currentCrop.width ||
savedCropInformation?.height !== currentCrop.height
);
};

const handleSave = () => {
console.log('saving', currentCrop);
};

useEffect(() => {
setCurrentCrop({ unit: 'px', ...assetVariant.cropInformation });
aspectRatio.current = assetVariant.width / assetVariant.height;
}, [assetVariant]);

return (
<Dialog
isOpen={isOpen}
title={translate(
'assetVariantModal.title',
`Variant details for ${assetVariant.presetIdentifier}: ${assetVariant.variantName}`,
{
assetVariant: assetVariant,
}
)}
onRequestClose={handleRequestClose}
style="wide"
actions={[
<Button key="cancel" style="neutral" hoverStyle="darken" onClick={handleRequestClose}>
{translate('assetVariantModal.cancel', 'Cancel')}
</Button>,
<Button key="save" style="success" hoverStyle="success" disabled={!hasChanged} onClick={handleSave}>
{translate('assetVariantModal.save', 'Save')}
</Button>,
]}
>
<div className={classes.cropContainer}>
{assetVariant ? (
<ImageCrop
currentCrop={currentCrop}
onCropChange={setCurrentCrop}
onCropComplete={handleCropComplete}
originalPreviewUrl={asset.previewUrl}
aspectRatio={aspectRatio.current}
/>
) : (
<span>{translate('assetVariantModal.loading', 'Loading...')}</span>
)}
</div>
</Dialog>
);
};

export default React.memo(VariantModal);
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';

import selectedVariantIdState from '../state/selectedVariantIdState';
import assetVariantModalState from '../state/assetVariantModalState';

const useSelectVariant = () => {
const setSelectedVariantId = useSetRecoilState(selectedVariantIdState);
const setAssetVariantModalState = useSetRecoilState(assetVariantModalState);

return useCallback(
(variantId: string) => {
if (!variantId) return;
setSelectedVariantId(variantId);
setAssetVariantModalState(true);
},
[setSelectedVariantId, setAssetVariantModalState]
);
};
export default useSelectVariant;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useRecoilValue } from 'recoil';

import { useSelectedAsset } from '@media-ui/core/src/hooks';
import AssetVariant from '../interfaces/AssetVariant';
import selectedVariantIdState from '../state/selectedVariantIdState';
import useAssetVariants from './useAssetVariants';

export default function useSelectedAssetVariant(): AssetVariant {
const selectedVariantId = useRecoilValue(selectedVariantIdState);
const selectedAsset = useSelectedAsset();
const assetVariants = useAssetVariants({ assetId: selectedAsset.id, assetSourceId: selectedAsset.assetSource.id });

return assetVariants.variants?.find((variant) => variant.id === selectedVariantId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// import { useMutation } from '@apollo/client';

// import { Asset } from '@media-ui/core/src/interfaces';

// import { REPLACE_ASSET } from '../mutations';
// import { FileUploadResult } from '../interfaces';

// export interface AssetReplacementOptions {
// generateRedirects: boolean;
// keepOriginalFilename: boolean;
// }

// interface ReplaceAssetProps {
// asset: Asset;
// file: File;
// options: AssetReplacementOptions;
// }

// export default function useReplaceAsset() {
// const [action, { error, data, loading }] = useMutation<{ replaceAsset: FileUploadResult }>(REPLACE_ASSET);

// const replaceAsset = ({ asset, file, options }: ReplaceAssetProps) => {
// return action({
// variables: {
// id: asset.id,
// assetSourceId: asset.assetSource.id,
// file,
// options,
// },
// });
// };

// return { replaceAsset, uploadState: data?.replaceAsset || null, error, loading };
// }
3 changes: 3 additions & 0 deletions Resources/Private/JavaScript/asset-variants/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export { default as ASSET_VARIANTS } from './queries/assetVariants';
export { default as CROP_INFORMATION_FRAGMENT } from './fragments/cropInformation';

export { default as VariantModal } from './components/VariantModal';
export { default as VariantsInspector } from './components/VariantsInspector';

export { default as assetVariantModalState } from './state/assetVariantModalState';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { atom, useSetRecoilState } from 'recoil';
import selectedVariantIdState from './selectedVariantIdState';

const assetVariantModalState = atom({
key: 'assetVariantModalState',
default: false,
});

export default assetVariantModalState;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { atom } from 'recoil';

const selectedVariantIdState = atom<string>({
key: 'selectedVariantIdState',
default: null,
});

export default selectedVariantIdState;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SimilarAssetsModal, similarAssetsModalState } from '@media-ui/feature-s
import { uploadDialogVisibleState } from '@media-ui/feature-asset-upload/src/state';
import { UploadDialog } from '@media-ui/feature-asset-upload/src/components';
import { AssetPreview } from '@media-ui/feature-asset-preview/src';
import { assetVariantModalState, VariantModal } from '@media-ui/feature-asset-variants/src';

import { SideBarLeft } from './SideBarLeft';
import { SideBarRight } from './SideBarRight';
Expand Down Expand Up @@ -100,6 +101,7 @@ const App = () => {
const { visible: showCreateAssetCollectionDialog } = useRecoilValue(createAssetCollectionDialogState);
const showAssetUsagesModal = useRecoilValue(assetUsageDetailsModalState);
const showSimilarAssetsModal = useRecoilValue(similarAssetsModalState);
const showVariantModal = useRecoilValue(assetVariantModalState);
const searchTerm = useRecoilValue(searchTermState);
const selectAsset = useSelectAsset();
const [, selectAssetSource] = useSelectAssetSource();
Expand Down Expand Up @@ -151,6 +153,7 @@ const App = () => {
{showCreateTagDialog && <CreateTagDialog />}
{showCreateAssetCollectionDialog && <CreateAssetCollectionDialog />}
{showSimilarAssetsModal && <SimilarAssetsModal />}
{showVariantModal && <VariantModal />}

<InteractionDialogRenderer />
<ClipboardWatcher />
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4975,6 +4975,11 @@ clone@^2.1.1:
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=

clsx@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==

coa@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
Expand Down Expand Up @@ -12745,6 +12750,13 @@ react-hot-loader@^4.13.0:
shallowequal "^1.1.0"
source-map "^0.7.3"

react-image-crop@^10.0.7:
version "10.0.7"
resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-10.0.7.tgz#79ae04ac8886f93c5945c9378bfc38ef3b29df2d"
integrity sha512-doV3sz101q9IaTWlx+DlErJ+BUknJ5CMVIRV72thWC3Fn5bg2XWoft7FbbrFGIr46zYrKKS9QuWNEzNkaqRvfQ==
dependencies:
clsx "^1.2.1"

react-image-lightbox@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-image-lightbox/-/react-image-lightbox-5.1.1.tgz#872d1a4336b5a6410ea7909b767cf59014081004"
Expand Down