From a5163ffacbfc89fa0a66d5f0cb279c1d6ada0967 Mon Sep 17 00:00:00 2001
From: Marius Tobiassen Bungum
Date: Thu, 5 Dec 2024 08:34:44 +0100
Subject: [PATCH 1/7] :sparkles: Added new image callbacks to RichText
components to make it use images
---
src/atoms/hooks/useAmplifyKit.ts | 10 ++--
src/atoms/hooks/useRichTextImage.ts | 23 ++++++++
src/atoms/utils/richtext.ts | 6 +--
.../RichTextDisplay/RichTextDisplay.tsx | 31 +++++++++--
.../RichTextEditor/EditorProvider.tsx | 52 +++++++++++++++++--
.../RichTextEditor/MenuBar/AddImageButton.tsx | 10 ++--
.../RichTextEditor/MenuBar/MenuBar.tsx | 6 +--
.../RichTextEditor/RichTextEditor.tsx | 27 ++++++++--
.../RichTextEditor/RichTextEditor.types.ts | 14 ++++-
.../{ExtendedImage.ts => ExtendedImage.tsx} | 40 ++++++++++----
src/molecules/index.ts | 2 +-
.../ReleasePosts/hooks/useTokenReleaseNote.ts | 3 ++
12 files changed, 184 insertions(+), 40 deletions(-)
create mode 100644 src/atoms/hooks/useRichTextImage.ts
rename src/molecules/RichTextEditor/custom-extensions/{ExtendedImage.ts => ExtendedImage.tsx} (73%)
diff --git a/src/atoms/hooks/useAmplifyKit.ts b/src/atoms/hooks/useAmplifyKit.ts
index 6eda832c9..d28ea954b 100644
--- a/src/atoms/hooks/useAmplifyKit.ts
+++ b/src/atoms/hooks/useAmplifyKit.ts
@@ -2,14 +2,13 @@ import { useMemo } from 'react';
import { RichText } from 'src/molecules/RichTextEditor';
import {
- OnImageUploadFn,
+ ImageExtensionFnProps,
RichTextEditorFeatures,
} from 'src/molecules/RichTextEditor/RichTextEditor.types';
-interface AmplifyKitProps {
+interface AmplifyKitProps extends ImageExtensionFnProps {
features?: RichTextEditorFeatures[];
placeholder?: string;
- onImageUpload?: OnImageUploadFn;
}
/* c8 ignore start */
@@ -17,6 +16,7 @@ export const useAmplifyKit = ({
features,
placeholder,
onImageUpload,
+ onImageRead,
}: AmplifyKitProps) => {
// This hooks is where we can use the features API we made to turn off extensions in the new configure API
// Currently its only turning off the image extension since its the only one that needs to be turned off for the moment
@@ -40,13 +40,13 @@ export const useAmplifyKit = ({
? {}
: false,
image: features?.includes(RichTextEditorFeatures.IMAGES)
- ? { onImageUpload }
+ ? { onImageUpload, onImageRead }
: false,
heading: features?.includes(RichTextEditorFeatures.HEADERS)
? {}
: false,
}),
- [onImageUpload, placeholder, features]
+ [placeholder, features, onImageUpload, onImageRead]
);
};
/* c8 ignore stop */
diff --git a/src/atoms/hooks/useRichTextImage.ts b/src/atoms/hooks/useRichTextImage.ts
new file mode 100644
index 000000000..16a7a0bc6
--- /dev/null
+++ b/src/atoms/hooks/useRichTextImage.ts
@@ -0,0 +1,23 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { ImageExtensionFnProps } from 'src/molecules/RichTextEditor/RichTextEditor.types';
+
+export function useRichTextImage(
+ image: string,
+ onImageRead: ImageExtensionFnProps['onImageRead'] | undefined
+) {
+ return useQuery({
+ queryKey: ['image', image],
+ queryFn: async () => {
+ if (onImageRead) {
+ const response = await onImageRead(image);
+ const extension = image.split('.').at(-1);
+ return `data:image/${extension};base64,${response}`;
+ }
+
+ return image;
+ },
+ staleTime: Infinity,
+ gcTime: Infinity,
+ });
+}
diff --git a/src/atoms/utils/richtext.ts b/src/atoms/utils/richtext.ts
index c8ca0d642..1c4a502c2 100644
--- a/src/atoms/utils/richtext.ts
+++ b/src/atoms/utils/richtext.ts
@@ -1,14 +1,14 @@
import {
DEFAULT_FEATURES,
- OnImageUploadFn,
+ ImageExtensionFnProps,
RichTextEditorFeatures,
} from 'src/molecules/RichTextEditor/RichTextEditor.types';
-export interface FeaturesProps {
+export interface FeaturesProps
+ extends Pick {
features?: RichTextEditorFeatures[];
extendFeatures?: RichTextEditorFeatures[];
removeFeatures?: RichTextEditorFeatures[];
- onImageUpload?: OnImageUploadFn;
}
export function getFeatures({
diff --git a/src/molecules/RichTextDisplay/RichTextDisplay.tsx b/src/molecules/RichTextDisplay/RichTextDisplay.tsx
index 536d4bafe..9347e4022 100644
--- a/src/molecules/RichTextDisplay/RichTextDisplay.tsx
+++ b/src/molecules/RichTextDisplay/RichTextDisplay.tsx
@@ -2,14 +2,22 @@ import { FC, HTMLAttributes, useEffect, useRef } from 'react';
import { Editor, Extensions, useEditor } from '@tiptap/react';
-import { AmplifyKit } from 'src/molecules/RichTextEditor/custom-extensions/AmplifyKit';
+import { useAmplifyKit } from 'src/atoms';
import {
EditorContent,
EditorStyling,
} from 'src/molecules/RichTextEditor/RichTextEditor.styles';
+import {
+ DEFAULT_FEATURES,
+ ImageExtensionFnProps,
+} from 'src/molecules/RichTextEditor/RichTextEditor.types';
-export interface RichTextDisplayProps {
+export interface RichTextDisplayProps
+ extends Pick {
value: string | null | undefined;
+ /**
+ * @deprecated - Use OnImageRead instead
+ */
imgReadToken?: string;
lightBackground?: boolean;
padding?: 'sm' | 'md' | 'lg' | 'none';
@@ -17,19 +25,34 @@ export interface RichTextDisplayProps {
children?: (editor: Editor) => JSX.Element;
}
+/**
+ *
+ * @param value - Rich text content
+ * @param imgReadToken - Deprecated, use onImageRead instead
+ * @param onImageRead - handler used when loading images, expects b64 string to be returned
+ * @param lightBackground - if it should have a light BG color
+ * @param padding - padding in the editor
+ * @param extensions - additional extensions to be added
+ * @param children - render prop for custom rendering, provides editor via callback
+ */
export const RichTextDisplay: FC<
RichTextDisplayProps & HTMLAttributes
> = ({
value,
imgReadToken,
+ onImageRead,
lightBackground = true,
padding = 'md',
- extensions = [AmplifyKit],
+ extensions,
children,
...rest
}) => {
+ const defaultExtensions = useAmplifyKit({
+ features: DEFAULT_FEATURES,
+ onImageRead: onImageRead,
+ });
const editor = useEditor({
- extensions: extensions,
+ extensions: extensions ? extensions : [defaultExtensions],
content: imgReadToken
? value?.replaceAll(/(
)/g, `$1$2?${imgReadToken}$3`)
: value,
diff --git a/src/molecules/RichTextEditor/EditorProvider.tsx b/src/molecules/RichTextEditor/EditorProvider.tsx
index a0f236905..d08092730 100644
--- a/src/molecules/RichTextEditor/EditorProvider.tsx
+++ b/src/molecules/RichTextEditor/EditorProvider.tsx
@@ -1,20 +1,19 @@
-import { FC, useEffect } from 'react';
+import { FC, useEffect, useRef } from 'react';
import { Editor, Extensions, useEditor } from '@tiptap/react';
import {
- OnImageUploadFn,
+ ImageExtensionFnProps,
RichTextEditorFeatures,
} from './RichTextEditor.types';
import { useAmplifyKit } from 'src/atoms/hooks/useAmplifyKit';
-export interface EditorProviderProps {
+export interface EditorProviderProps extends ImageExtensionFnProps {
children: (editor: Editor) => JSX.Element;
content: string | null | undefined;
extensions?: Extensions;
onUpdate?: (html: string) => void;
placeholder?: string;
- onImageUpload?: OnImageUploadFn;
features?: RichTextEditorFeatures[];
}
@@ -25,6 +24,8 @@ export const EditorProvider: FC = ({
placeholder,
onUpdate,
onImageUpload,
+ onImageRead,
+ onRemovedImagesChange,
extensions = [],
}) => {
// Lets us apply the features API with the new amplify kit
@@ -32,12 +33,53 @@ export const EditorProvider: FC = ({
features,
placeholder,
onImageUpload,
+ onImageRead,
});
+ const addedImages = useRef([]);
+ const previousRemovedImages = useRef([]);
+
+ const handleImageCheck = (editor: Editor) => {
+ const currentImages: string[] = [];
+
+ editor.getJSON().content?.forEach((item) => {
+ if (item.type === 'image' && item.attrs?.src) {
+ currentImages.push(item.attrs.src);
+ }
+ });
+
+ for (const image of currentImages) {
+ if (!addedImages.current.includes(image)) {
+ addedImages.current.push(image);
+ }
+ }
+
+ const removedImages = addedImages.current.filter(
+ (image) => !currentImages.includes(image)
+ );
+ if (
+ previousRemovedImages.current.some(
+ (image) => !removedImages.includes(image)
+ ) ||
+ removedImages.some(
+ (image) => !previousRemovedImages.current.includes(image)
+ )
+ ) {
+ onRemovedImagesChange?.(removedImages);
+ previousRemovedImages.current = removedImages;
+ }
+ };
const editor = useEditor({
content,
extensions: [ampExtensions, ...extensions],
- onUpdate: ({ editor }) => onUpdate?.(editor.getHTML()),
+ onCreate: ({ editor }) => {
+ handleImageCheck(editor);
+ },
+ onUpdate: ({ editor }) => {
+ handleImageCheck(editor);
+
+ onUpdate?.(editor.getHTML());
+ },
});
/* c8 ignore start */
diff --git a/src/molecules/RichTextEditor/MenuBar/AddImageButton.tsx b/src/molecules/RichTextEditor/MenuBar/AddImageButton.tsx
index 0b8b5f77a..bb5111a13 100644
--- a/src/molecules/RichTextEditor/MenuBar/AddImageButton.tsx
+++ b/src/molecules/RichTextEditor/MenuBar/AddImageButton.tsx
@@ -5,13 +5,13 @@ import { camera_add_photo } from '@equinor/eds-icons';
import { EditorMenu } from './MenuBar';
import {
EditorPanel,
- OnImageUploadFn,
+ ImageExtensionFnProps,
RichTextEditorFeatures,
} from 'src/molecules/RichTextEditor/RichTextEditor.types';
-export interface AddImageProps extends EditorPanel {
- onImageUpload?: OnImageUploadFn;
-}
+export interface AddImageProps
+ extends EditorPanel,
+ Pick {}
export const AddImageButton: FC = ({
onImageUpload,
@@ -42,7 +42,7 @@ export const AddImageButton: FC = ({
const image = await onImageUpload(files[0]);
if (!image) return;
- editor?.chain().focus().setImage({ src: image.b64, alt: image.url }).run();
+ editor?.chain().focus().setImage({ src: image.src, alt: image.alt }).run();
};
/* c8 ignore end */
diff --git a/src/molecules/RichTextEditor/MenuBar/MenuBar.tsx b/src/molecules/RichTextEditor/MenuBar/MenuBar.tsx
index 108ef774b..af7059679 100644
--- a/src/molecules/RichTextEditor/MenuBar/MenuBar.tsx
+++ b/src/molecules/RichTextEditor/MenuBar/MenuBar.tsx
@@ -3,7 +3,7 @@ import { FC } from 'react';
import { Editor } from '@tiptap/react';
import {
- OnImageUploadFn,
+ ImageExtensionFnProps,
RichTextEditorFeatures,
} from '../RichTextEditor.types';
import { TextTable } from './Table/Table';
@@ -37,10 +37,10 @@ const MenuBar = styled.div`
border-bottom: 1px solid ${colors.ui.background__medium.rgba};
`;
-export interface MenuBarProps {
+export interface MenuBarProps
+ extends Pick {
editor: Editor;
features: RichTextEditorFeatures[];
- onImageUpload?: OnImageUploadFn;
}
/* c8 ignore start */
diff --git a/src/molecules/RichTextEditor/RichTextEditor.tsx b/src/molecules/RichTextEditor/RichTextEditor.tsx
index 4ddbd9032..4191de904 100644
--- a/src/molecules/RichTextEditor/RichTextEditor.tsx
+++ b/src/molecules/RichTextEditor/RichTextEditor.tsx
@@ -4,15 +4,14 @@ import { AmplifyBar } from './MenuBar/MenuBar';
import { EditorProvider } from './EditorProvider';
import { EditorContent, EditorStyling } from './RichTextEditor.styles';
import {
- OnImageUploadFn,
+ ImageExtensionFnProps,
RichTextEditorFeatures,
} from './RichTextEditor.types';
import { getFeatures } from 'src/atoms/utils/richtext';
-export interface RichTextEditorProps {
+export interface RichTextEditorProps extends ImageExtensionFnProps {
value: string | null | undefined;
onChange: (value: string) => void;
- onImageUpload?: OnImageUploadFn;
placeholder?: string;
features?: RichTextEditorFeatures[];
extendFeatures?: RichTextEditorFeatures[];
@@ -25,10 +24,30 @@ export interface RichTextEditorProps {
highlighted?: boolean;
}
+/**
+ *
+ * @param value - Rich text content
+ * @param onChange - handler for when the content changes
+ * @param onImageUpload - handler for when an image is uploaded
+ * @param onImageRead - handler used when loading images, expects b64 string to be returned
+ * @param onRemovedImagesChange - called when the list of removed images change
+ * @param placeholder - placeholder text if there is no content
+ * @param features - which features should be enabled
+ * @param extendFeatures - additional features to be added
+ * @param removeFeatures - features to exclude from default features
+ * @param padding - padding in the editor
+ * @param maxHeight - maxHeight of the text box
+ * @param minHeight - minHeight of the text box
+ * @param lightBackground - if it should have a different BG color
+ * @param border - if it should have a border
+ * @param highlighted - if it should have a highlighted border
+ */
export const RichTextEditor: FC = ({
value,
onChange,
onImageUpload,
+ onImageRead,
+ onRemovedImagesChange,
placeholder,
features,
extendFeatures,
@@ -53,6 +72,8 @@ export const RichTextEditor: FC = ({
features={usedFeatured}
placeholder={placeholder}
onImageUpload={onImageUpload}
+ onImageRead={onImageRead}
+ onRemovedImagesChange={onRemovedImagesChange}
>
{(editor) => (
Promise<{ b64: string; url: string } | undefined>;
+) => Promise<{ src: string; alt: string } | undefined>;
+
+type OnImageReadFn = (src: string) => Promise;
+
+type OnRemovedImagesChange = (images: string[]) => void;
+
+export interface ImageExtensionFnProps {
+ onImageUpload?: OnImageUploadFn;
+ onImageRead?: OnImageReadFn;
+ onRemovedImagesChange?: OnRemovedImagesChange;
+}
export interface EditorPanel {
editor: Editor;
diff --git a/src/molecules/RichTextEditor/custom-extensions/ExtendedImage.ts b/src/molecules/RichTextEditor/custom-extensions/ExtendedImage.tsx
similarity index 73%
rename from src/molecules/RichTextEditor/custom-extensions/ExtendedImage.ts
rename to src/molecules/RichTextEditor/custom-extensions/ExtendedImage.tsx
index a6f455a78..d77332dff 100644
--- a/src/molecules/RichTextEditor/custom-extensions/ExtendedImage.ts
+++ b/src/molecules/RichTextEditor/custom-extensions/ExtendedImage.tsx
@@ -1,30 +1,52 @@
import Image from '@tiptap/extension-image';
import { Plugin, PluginKey } from '@tiptap/pm/state';
+import {
+ NodeViewProps,
+ NodeViewWrapper,
+ ReactNodeViewRenderer,
+} from '@tiptap/react';
-import { OnImageUploadFn } from 'src/molecules/RichTextEditor/RichTextEditor.types';
+import { useRichTextImage } from 'src/atoms/hooks/useRichTextImage';
+import { ImageExtensionFnProps } from 'src/molecules/RichTextEditor/RichTextEditor.types';
-export interface ExtendedImageOptions {
+export interface ExtendedImageOptions extends ImageExtensionFnProps {
inline: boolean;
allowBase64: boolean;
- onImageUpload?: OnImageUploadFn;
}
declare module '@tiptap/extension-image' {
- interface ImageOptions {
+ interface ImageOptions extends ImageExtensionFnProps {
inline: boolean;
allowBase64: boolean;
- onImageUpload?: OnImageUploadFn;
}
}
+const Component = (props: NodeViewProps) => {
+ const { src, alt } = props.node.attrs;
+ const onImageRead: ImageExtensionFnProps['onImageRead'] | undefined =
+ props.extension.options.onImageRead;
+ const { data: usingSrc } = useRichTextImage(src, onImageRead);
+
+ return (
+
+
+
+ );
+};
+
+// TODO: Add 'onDelete' handler so it's easier to remove images in the apps
/* c8 ignore start */
export default Image.extend({
addOptions() {
return {
...this.parent?.(),
onImageUpload: undefined,
+ onImageRead: undefined,
};
},
+ addNodeView() {
+ return ReactNodeViewRenderer(Component);
+ },
addProseMirrorPlugins() {
const { onImageUpload } = this.options;
@@ -63,8 +85,8 @@ export default Image.extend({
.then((item) => {
if (!item) return;
const node = schema.nodes.image.create({
- src: item.b64,
- alt: item.url,
+ src: item.src,
+ alt: item.alt,
});
const transaction = view.state.tr.insert(
coordinates.pos,
@@ -101,8 +123,8 @@ export default Image.extend({
.then((item) => {
if (!item) return;
const node = schema.nodes.image.create({
- src: item.b64,
- alt: item.url,
+ src: item.src,
+ alt: item.src,
});
const transaction =
view.state.tr.replaceSelectionWith(node);
diff --git a/src/molecules/index.ts b/src/molecules/index.ts
index 7ae04907e..fb0d87e40 100644
--- a/src/molecules/index.ts
+++ b/src/molecules/index.ts
@@ -50,7 +50,7 @@ export {
} from './RichTextEditor/MenuBar/Table/TableBar';
export { DEFAULT_FEATURES } from './RichTextEditor/RichTextEditor.types';
export { RichTextEditorFeatures } from './RichTextEditor/RichTextEditor.types';
-export type { OnImageUploadFn } from './RichTextEditor/RichTextEditor.types';
+export type * from './RichTextEditor/RichTextEditor.types';
export { Search } from './Search/Search';
export { Sieve } from './Sieve/Sieve';
export type {
diff --git a/src/organisms/TopBar/Resources/ReleaseNotesDialog/ReleasePosts/hooks/useTokenReleaseNote.ts b/src/organisms/TopBar/Resources/ReleaseNotesDialog/ReleasePosts/hooks/useTokenReleaseNote.ts
index df960ebf5..ba0394581 100644
--- a/src/organisms/TopBar/Resources/ReleaseNotesDialog/ReleasePosts/hooks/useTokenReleaseNote.ts
+++ b/src/organisms/TopBar/Resources/ReleaseNotesDialog/ReleasePosts/hooks/useTokenReleaseNote.ts
@@ -1,6 +1,9 @@
import { ReleaseNotesService } from '@equinor/subsurface-app-management';
import { useQuery } from '@tanstack/react-query';
+/**
+ * @deprecated - This will change once SAM's new release note endpoints are implemented
+ */
export function useTokenReleaseNote() {
return useQuery({
queryKey: ['get-token-release-note'],
From 4b8353ab06795404eea0e4980655a5e1e0cc4555 Mon Sep 17 00:00:00 2001
From: Marius Tobiassen Bungum
Date: Thu, 5 Dec 2024 08:45:32 +0100
Subject: [PATCH 2/7] :wastebasket: Deprecate old richtext utils related to
images
---
src/atoms/utils/richtext.ts | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/src/atoms/utils/richtext.ts b/src/atoms/utils/richtext.ts
index 1c4a502c2..caaf2c2d7 100644
--- a/src/atoms/utils/richtext.ts
+++ b/src/atoms/utils/richtext.ts
@@ -35,9 +35,18 @@ export function getFeatures({
return usedFeatures;
}
+/**
+ * @deprecated - Use the onImageRead prop instead of tokens
+ */
export const IMG_WITH_SRC_AND_ALT = /(
)/g;
+/**
+ * @deprecated - Use the onImageRead prop instead of tokens
+ */
export const IMG_WITH_ALT = /(
)/g;
+/**
+ * @deprecated - Use the onImageRead prop instead of tokens
+ */
export function extractImageUrls(value: string | undefined): string[] {
if (!value) return [];
@@ -50,6 +59,9 @@ export function extractImageUrls(value: string | undefined): string[] {
return images;
}
+/**
+ * @deprecated - Use the onImageRead prop instead of tokens
+ */
export function imageToB64(file: File): Promise {
return new Promise((resolve, reject) => {
const reader = new FileReader();
@@ -61,6 +73,9 @@ export function imageToB64(file: File): Promise {
});
}
+/**
+ * @deprecated - Use the onImageRead prop instead of tokens
+ */
export function cleanRichTextValue(value: string) {
return value.replaceAll(IMG_WITH_SRC_AND_ALT, `
`);
}
From 6435ad52dd1f8f56eefd1a1befdfb81dee35919b Mon Sep 17 00:00:00 2001
From: Marius Tobiassen Bungum
Date: Thu, 5 Dec 2024 09:00:38 +0100
Subject: [PATCH 3/7] :white_check_mark: Added test for image read fn
---
.../RichTextDisplay/RichTextDisplay.test.tsx | 26 +++++++++++++++++--
.../RichTextEditor/EditorProvider.tsx | 3 +++
.../RichTextEditor/RichTextEditor.test.tsx | 9 +++++--
3 files changed, 34 insertions(+), 4 deletions(-)
diff --git a/src/molecules/RichTextDisplay/RichTextDisplay.test.tsx b/src/molecules/RichTextDisplay/RichTextDisplay.test.tsx
index 740394123..6cac46cbd 100644
--- a/src/molecules/RichTextDisplay/RichTextDisplay.test.tsx
+++ b/src/molecules/RichTextDisplay/RichTextDisplay.test.tsx
@@ -2,7 +2,9 @@ import { tokens } from '@equinor/eds-tokens';
import { faker } from '@faker-js/faker';
import { RichTextDisplay } from 'src/molecules/RichTextDisplay/RichTextDisplay';
-import { act, render, screen } from 'src/tests/test-utils';
+import { act, render, renderWithProviders, screen } from 'src/tests/test-utils';
+
+import { expect } from 'vitest';
const { spacings } = tokens;
@@ -23,7 +25,7 @@ test('Image read token prop works as expected', async () => {
'https://images.unsplash.com/photo-1682687221363-72518513620e';
const randomToken = 'sv=2023-08-03';
- render(
+ renderWithProviders(
`}
imgReadToken={randomToken}
@@ -41,6 +43,26 @@ test('Image read token prop works as expected', async () => {
);
});
+test('onImageRead fn works as expected', async () => {
+ const randomUrl =
+ 'https://images.unsplash.com/photo-1682687221363-72518513620e';
+ const handleImageRead = vi.fn();
+
+ renderWithProviders(
+ `}
+ onImageRead={handleImageRead}
+ />
+ );
+
+ // Wait for tip tap to initialize
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ });
+
+ expect(handleImageRead).toHaveBeenCalledWith(randomUrl);
+});
+
test('Shows content when sending in new value', async () => {
const content = faker.animal.dog();
const { rerender } = render();
diff --git a/src/molecules/RichTextEditor/EditorProvider.tsx b/src/molecules/RichTextEditor/EditorProvider.tsx
index d08092730..30b372e6e 100644
--- a/src/molecules/RichTextEditor/EditorProvider.tsx
+++ b/src/molecules/RichTextEditor/EditorProvider.tsx
@@ -53,6 +53,8 @@ export const EditorProvider: FC = ({
}
}
+ // TODO: Test this when we move to browser mode
+ /* c8 ignore start */
const removedImages = addedImages.current.filter(
(image) => !currentImages.includes(image)
);
@@ -67,6 +69,7 @@ export const EditorProvider: FC = ({
onRemovedImagesChange?.(removedImages);
previousRemovedImages.current = removedImages;
}
+ /* c8 ignore end */
};
const editor = useEditor({
diff --git a/src/molecules/RichTextEditor/RichTextEditor.test.tsx b/src/molecules/RichTextEditor/RichTextEditor.test.tsx
index 335383766..86325bdbc 100644
--- a/src/molecules/RichTextEditor/RichTextEditor.test.tsx
+++ b/src/molecules/RichTextEditor/RichTextEditor.test.tsx
@@ -6,7 +6,12 @@ import { RichTextEditor, RichTextEditorProps } from './RichTextEditor';
import { RichTextEditorFeatures } from './RichTextEditor.types';
import { colors } from 'src/atoms';
import type { AmplifyKitOptions } from 'src/molecules/RichTextEditor/custom-extensions/AmplifyKit';
-import { render, screen, userEvent } from 'src/tests/test-utils';
+import {
+ render,
+ renderWithProviders,
+ screen,
+ userEvent,
+} from 'src/tests/test-utils';
function fakeProps(withImage = false): RichTextEditorProps {
return {
@@ -162,7 +167,7 @@ test('Images work as expected', async () => {
const alt = faker.animal.crocodilia();
const props = fakeProps(true);
- render(
+ renderWithProviders(
`}
From a6eed3826bb90f416846b03b97656ea2d8d7ebc9 Mon Sep 17 00:00:00 2001
From: Marius Tobiassen Bungum
Date: Thu, 5 Dec 2024 12:10:13 +0100
Subject: [PATCH 4/7] :recycle: Changed how we handle deleting images in
RichText
---
src/atoms/hooks/useRichTextImage.ts | 2 +-
.../RichTextEditor/EditorProvider.tsx | 49 ++++++++++++++-----
.../RichTextEditor/RichTextEditor.tsx | 3 ++
.../RichTextEditor/RichTextEditor.types.ts | 10 ++--
.../custom-extensions/ExtendedImage.tsx | 3 +-
5 files changed, 46 insertions(+), 21 deletions(-)
diff --git a/src/atoms/hooks/useRichTextImage.ts b/src/atoms/hooks/useRichTextImage.ts
index 16a7a0bc6..983771748 100644
--- a/src/atoms/hooks/useRichTextImage.ts
+++ b/src/atoms/hooks/useRichTextImage.ts
@@ -7,7 +7,7 @@ export function useRichTextImage(
onImageRead: ImageExtensionFnProps['onImageRead'] | undefined
) {
return useQuery({
- queryKey: ['image', image],
+ queryKey: [image],
queryFn: async () => {
if (onImageRead) {
const response = await onImageRead(image);
diff --git a/src/molecules/RichTextEditor/EditorProvider.tsx b/src/molecules/RichTextEditor/EditorProvider.tsx
index 30b372e6e..e0efd9d25 100644
--- a/src/molecules/RichTextEditor/EditorProvider.tsx
+++ b/src/molecules/RichTextEditor/EditorProvider.tsx
@@ -1,5 +1,6 @@
import { FC, useEffect, useRef } from 'react';
+import { useQueryClient } from '@tanstack/react-query';
import { Editor, Extensions, useEditor } from '@tiptap/react';
import {
@@ -25,6 +26,7 @@ export const EditorProvider: FC = ({
onUpdate,
onImageUpload,
onImageRead,
+ onImageRemove,
onRemovedImagesChange,
extensions = [],
}) => {
@@ -35,10 +37,14 @@ export const EditorProvider: FC = ({
onImageUpload,
onImageRead,
});
+ const queryClient = useQueryClient();
const addedImages = useRef([]);
+ const deletedImages = useRef([]);
const previousRemovedImages = useRef([]);
const handleImageCheck = (editor: Editor) => {
+ // TODO: Test this when we move to browser mode
+ /* c8 ignore start */
const currentImages: string[] = [];
editor.getJSON().content?.forEach((item) => {
@@ -50,25 +56,44 @@ export const EditorProvider: FC = ({
for (const image of currentImages) {
if (!addedImages.current.includes(image)) {
addedImages.current.push(image);
+ } else if (addedImages.current.includes(image) && onImageRemove) {
+ // Image was recovered from undo, need to upload it again
+ const dataUrl = queryClient.getQueryData([image]);
+ if (dataUrl) {
+ const arr = dataUrl.split(',');
+ const mime = arr[0].match(/:(.*?);/)![1];
+ const byteString = atob(arr[arr.length - 1]);
+ let n = byteString.length;
+ const u8arr = new Uint8Array(n);
+ while (n--) {
+ u8arr[n] = byteString.charCodeAt(n);
+ }
+ const file = new File([u8arr], image, { type: mime });
+ onImageUpload?.(file);
+ }
}
}
- // TODO: Test this when we move to browser mode
- /* c8 ignore start */
- const removedImages = addedImages.current.filter(
- (image) => !currentImages.includes(image)
+ const imagesToDelete = addedImages.current.filter(
+ (image) =>
+ !currentImages.includes(image) && !deletedImages.current.includes(image)
);
+
+ if (onImageRemove) {
+ for (const image of imagesToDelete) {
+ onImageRemove?.(image);
+ deletedImages.current.push(image);
+ }
+ }
+
if (
- previousRemovedImages.current.some(
- (image) => !removedImages.includes(image)
- ) ||
- removedImages.some(
- (image) => !previousRemovedImages.current.includes(image)
- )
+ onRemovedImagesChange &&
+ previousRemovedImages.current.length !== imagesToDelete.length
) {
- onRemovedImagesChange?.(removedImages);
- previousRemovedImages.current = removedImages;
+ onRemovedImagesChange(deletedImages.current);
+ previousRemovedImages.current = imagesToDelete;
}
+
/* c8 ignore end */
};
diff --git a/src/molecules/RichTextEditor/RichTextEditor.tsx b/src/molecules/RichTextEditor/RichTextEditor.tsx
index 4191de904..717ce13da 100644
--- a/src/molecules/RichTextEditor/RichTextEditor.tsx
+++ b/src/molecules/RichTextEditor/RichTextEditor.tsx
@@ -30,6 +30,7 @@ export interface RichTextEditorProps extends ImageExtensionFnProps {
* @param onChange - handler for when the content changes
* @param onImageUpload - handler for when an image is uploaded
* @param onImageRead - handler used when loading images, expects b64 string to be returned
+ * @param onImageRemove - called when an image removed, use when deleting images instantly
* @param onRemovedImagesChange - called when the list of removed images change
* @param placeholder - placeholder text if there is no content
* @param features - which features should be enabled
@@ -47,6 +48,7 @@ export const RichTextEditor: FC = ({
onChange,
onImageUpload,
onImageRead,
+ onImageRemove,
onRemovedImagesChange,
placeholder,
features,
@@ -73,6 +75,7 @@ export const RichTextEditor: FC = ({
placeholder={placeholder}
onImageUpload={onImageUpload}
onImageRead={onImageRead}
+ onImageRemove={onImageRemove}
onRemovedImagesChange={onRemovedImagesChange}
>
{(editor) => (
diff --git a/src/molecules/RichTextEditor/RichTextEditor.types.ts b/src/molecules/RichTextEditor/RichTextEditor.types.ts
index 3e05046a3..aab68da78 100644
--- a/src/molecules/RichTextEditor/RichTextEditor.types.ts
+++ b/src/molecules/RichTextEditor/RichTextEditor.types.ts
@@ -32,14 +32,12 @@ type OnImageUploadFn = (
file: File
) => Promise<{ src: string; alt: string } | undefined>;
-type OnImageReadFn = (src: string) => Promise;
-
-type OnRemovedImagesChange = (images: string[]) => void;
-
+// TODO: Undo images calls onImageUpload again
export interface ImageExtensionFnProps {
onImageUpload?: OnImageUploadFn;
- onImageRead?: OnImageReadFn;
- onRemovedImagesChange?: OnRemovedImagesChange;
+ onImageRead?: (src: string) => Promise;
+ onImageRemove?: (src: string) => Promise;
+ onRemovedImagesChange?: (images: string[]) => void;
}
export interface EditorPanel {
diff --git a/src/molecules/RichTextEditor/custom-extensions/ExtendedImage.tsx b/src/molecules/RichTextEditor/custom-extensions/ExtendedImage.tsx
index d77332dff..96be56af9 100644
--- a/src/molecules/RichTextEditor/custom-extensions/ExtendedImage.tsx
+++ b/src/molecules/RichTextEditor/custom-extensions/ExtendedImage.tsx
@@ -17,7 +17,7 @@ export interface ExtendedImageOptions extends ImageExtensionFnProps {
declare module '@tiptap/extension-image' {
interface ImageOptions extends ImageExtensionFnProps {
inline: boolean;
- allowBase64: boolean;
+ allowBase64: boolean; // TODO: Try to set to false
}
}
@@ -34,7 +34,6 @@ const Component = (props: NodeViewProps) => {
);
};
-// TODO: Add 'onDelete' handler so it's easier to remove images in the apps
/* c8 ignore start */
export default Image.extend({
addOptions() {
From 9c1ab6fb634b2b216c372151b87598c9220f7c4d Mon Sep 17 00:00:00 2001
From: Marius Tobiassen Bungum
Date: Thu, 5 Dec 2024 12:11:48 +0100
Subject: [PATCH 5/7] :children_crossing: Disallow using both remove strategies
at the same time
---
.../RichTextEditor/RichTextEditor.test.tsx | 15 +++++++++++++++
src/molecules/RichTextEditor/RichTextEditor.tsx | 6 ++++++
2 files changed, 21 insertions(+)
diff --git a/src/molecules/RichTextEditor/RichTextEditor.test.tsx b/src/molecules/RichTextEditor/RichTextEditor.test.tsx
index 86325bdbc..eb5267719 100644
--- a/src/molecules/RichTextEditor/RichTextEditor.test.tsx
+++ b/src/molecules/RichTextEditor/RichTextEditor.test.tsx
@@ -182,6 +182,21 @@ test('Images work as expected', async () => {
expect(screen.getByRole('img')).toHaveAttribute('alt', alt);
});
+test('Throws error if trying to use both remove strategies', () => {
+ console.error = vi.fn();
+
+ const props = fakeProps();
+ expect(() =>
+ render(
+
+ )
+ ).toThrowError();
+});
+
describe('Editor defaults can be merged', () => {
const uniqe: Partial = {
bold: { HTMLAttributes: { class: 'bolder' } },
diff --git a/src/molecules/RichTextEditor/RichTextEditor.tsx b/src/molecules/RichTextEditor/RichTextEditor.tsx
index 717ce13da..24c4175c6 100644
--- a/src/molecules/RichTextEditor/RichTextEditor.tsx
+++ b/src/molecules/RichTextEditor/RichTextEditor.tsx
@@ -61,6 +61,12 @@ export const RichTextEditor: FC = ({
border = true,
highlighted = false,
}) => {
+ if (onImageRemove && onRemovedImagesChange) {
+ throw new Error(
+ 'onImageRemove and onRemovedImagesChange cannot be used together'
+ );
+ }
+
const usedFeatured = getFeatures({
features,
extendFeatures,
From 2f16ec82f19c6c2d1259de7bd6f4d406ad091f70 Mon Sep 17 00:00:00 2001
From: Marius Tobiassen Bungum
Date: Thu, 5 Dec 2024 12:17:27 +0100
Subject: [PATCH 6/7] :bug: Add EDS light tokens, so overriding to light theme
works as expected
---
src/atoms/style/lightTokens.ts | 76 ++++++++++++++++++++++++++++++++++
1 file changed, 76 insertions(+)
diff --git a/src/atoms/style/lightTokens.ts b/src/atoms/style/lightTokens.ts
index de392ddcf..6f5b57e67 100644
--- a/src/atoms/style/lightTokens.ts
+++ b/src/atoms/style/lightTokens.ts
@@ -53,5 +53,81 @@ export const lightTokens = css`
--amplify_dataviz_lightgray_default: rgba(144, 155, 162, 1);
--amplify_dataviz_lightgray_darker: rgba(91, 102, 108, 1);
--amplify_dataviz_lightgray_lighter: rgba(194, 200, 204, 1);
+
+ --eds_text__static_icons__default: rgba(61, 61, 61, 1);
+ --eds_text__static_icons__secondary: rgba(86, 86, 86, 1);
+ --eds_text__static_icons__tertiary: rgba(111, 111, 111, 1);
+ --eds_text__static_icons__primary_white: rgba(255, 255, 255, 1);
+ --eds_ui_background__default: rgba(255, 255, 255, 1);
+ --eds_ui_background__semitransparent: rgba(255, 255, 255, 0.2);
+ --eds_ui_background__light: rgba(247, 247, 247, 1);
+ --eds_ui_background__scrim: rgba(0, 0, 0, 0.4);
+ --eds_ui_background__overlay: rgba(0, 0, 0, 0.8);
+ --eds_ui_background__medium: rgba(220, 220, 220, 1);
+ --eds_ui_background__info: rgba(213, 234, 244, 1);
+ --eds_ui_background__warning: rgba(255, 231, 214, 1);
+ --eds_ui_background__danger: rgba(255, 193, 193, 1);
+ --eds_infographic_substitute__purple_berry: rgba(140, 17, 89, 1);
+ --eds_infographic_substitute__pink_rose: rgba(226, 73, 115, 1);
+ --eds_infographic_substitute__pink_salmon: rgba(255, 146, 168, 1);
+ --eds_infographic_substitute__green_cucumber: rgba(0, 95, 87, 1);
+ --eds_infographic_substitute__green_succulent: rgba(0, 151, 123, 1);
+ --eds_infographic_substitute__green_mint: rgba(64, 211, 143, 1);
+ --eds_infographic_substitute__blue_ocean: rgba(0, 64, 136, 1);
+ --eds_infographic_substitute__blue_overcast: rgba(0, 132, 196, 1);
+ --eds_infographic_substitute__blue_sky: rgba(82, 192, 255, 1);
+ --eds_infographic_primary__moss_green_100: rgba(0, 112, 121, 1);
+ --eds_infographic_primary__moss_green_55: rgba(115, 177, 181, 1);
+ --eds_infographic_primary__moss_green_34: rgba(168, 206, 209, 1);
+ --eds_infographic_primary__moss_green_21: rgba(201, 224, 226, 1);
+ --eds_infographic_primary__moss_green_13: rgba(222, 237, 238, 1);
+ --eds_infographic_primary__energy_red_100: rgba(235, 0, 55, 1);
+ --eds_infographic_primary__energy_red_55: rgba(255, 125, 152, 1);
+ --eds_infographic_primary__energy_red_34: rgba(255, 174, 191, 1);
+ --eds_infographic_primary__energy_red_21: rgba(255, 205, 215, 1);
+ --eds_infographic_primary__energy_red_13: rgba(255, 224, 231, 1);
+ --eds_infographic_primary__weathered_red: rgba(125, 0, 35, 1);
+ --eds_infographic_primary__slate_blue: rgba(36, 55, 70, 1);
+ --eds_infographic_primary__spruce_wood: rgba(255, 231, 214, 1);
+ --eds_infographic_primary__mist_blue: rgba(213, 234, 244, 1);
+ --eds_infographic_primary__lichen_green: rgba(230, 250, 236, 1);
+ --eds_logo__fill_positive: rgba(235, 0, 55, 1);
+ --eds_logo__fill_negative: rgba(255, 255, 255, 1);
+ --eds_interactive_primary__selected_highlight: rgba(230, 250, 236, 1);
+ --eds_interactive_primary__selected_hover: rgba(195, 243, 210, 1);
+ --eds_interactive_primary__resting: rgba(0, 112, 121, 1);
+ --eds_interactive_primary__hover: rgba(0, 79, 85, 1);
+ --eds_interactive_primary__hover_alt: rgba(222, 237, 238, 1);
+ --eds_interactive_secondary__highlight: rgba(213, 234, 244, 1);
+ --eds_interactive_secondary__resting: rgba(36, 55, 70, 1);
+ --eds_interactive_secondary__link_hover: rgba(23, 36, 47, 1);
+ --eds_interactive_danger__highlight: rgba(255, 193, 193, 1);
+ --eds_interactive_danger__resting: rgba(235, 0, 0, 1);
+ --eds_interactive_danger__hover: rgba(179, 13, 47, 1);
+ --eds_interactive_danger__text: rgba(179, 13, 47, 1);
+ --eds_interactive_warning__highlight: rgba(255, 231, 214, 1);
+ --eds_interactive_warning__resting: rgba(255, 146, 0, 1);
+ --eds_interactive_warning__hover: rgba(173, 98, 0, 1);
+ --eds_interactive_warning__text: rgba(173, 98, 0, 1);
+ --eds_interactive_success__highlight: rgba(230, 250, 236, 1);
+ --eds_interactive_success__resting: rgba(75, 183, 72, 1);
+ --eds_interactive_success__hover: rgba(53, 129, 50, 1);
+ --eds_interactive_success__text: rgba(53, 129, 50, 1);
+ --eds_interactive_table__cell__fill_resting: rgba(255, 255, 255, 1);
+ --eds_interactive_table__cell__fill_hover: rgba(234, 234, 234, 1);
+ --eds_interactive_table__cell__fill_activated: rgba(230, 250, 236, 1);
+ --eds_interactive_table__header__fill_activated: rgba(234, 234, 234, 1);
+ --eds_interactive_table__header__fill_hover: rgba(220, 220, 220, 1);
+ --eds_interactive_table__header__fill_resting: rgba(247, 247, 247, 1);
+ --eds_interactive__disabled__text: rgba(190, 190, 190, 1);
+ --eds_interactive__text_highlight: rgba(213, 234, 244, 1);
+ --eds_interactive__focus: rgba(0, 112, 121, 1);
+ --eds_interactive__disabled__border: rgba(220, 220, 220, 1);
+ --eds_interactive__disabled__fill: rgba(234, 234, 234, 1);
+ --eds_interactive__link_on_interactive_colors: rgba(255, 255, 255, 1);
+ --eds_interactive__icon_on_interactive_colors: rgba(255, 255, 255, 1);
+ --eds_interactive__link_in_snackbars: rgba(151, 202, 206, 1);
+ --eds_interactive__pressed_overlay_dark: rgba(0, 0, 0, 0.2);
+ --eds_interactive__pressed_overlay_light: rgba(255, 255, 255, 0.2);
}
`;
From a2c7401eac8590479c76eaca5bacdb21570b9c90 Mon Sep 17 00:00:00 2001
From: Marius Tobiassen Bungum
Date: Thu, 5 Dec 2024 12:55:08 +0100
Subject: [PATCH 7/7] :white_check_mark: Fix tests related to RichTextEditor
now expecting a query provider
---
.../RichTextDisplay/RichTextDisplay.tsx | 1 +
.../RichTextEditor/RichTextEditor.test.tsx | 25 ++++++++-----------
2 files changed, 11 insertions(+), 15 deletions(-)
diff --git a/src/molecules/RichTextDisplay/RichTextDisplay.tsx b/src/molecules/RichTextDisplay/RichTextDisplay.tsx
index 9347e4022..5e72f2b2e 100644
--- a/src/molecules/RichTextDisplay/RichTextDisplay.tsx
+++ b/src/molecules/RichTextDisplay/RichTextDisplay.tsx
@@ -52,6 +52,7 @@ export const RichTextDisplay: FC<
onImageRead: onImageRead,
});
const editor = useEditor({
+ /* c8 ignore next */
extensions: extensions ? extensions : [defaultExtensions],
content: imgReadToken
? value?.replaceAll(/(
)/g, `$1$2?${imgReadToken}$3`)
diff --git a/src/molecules/RichTextEditor/RichTextEditor.test.tsx b/src/molecules/RichTextEditor/RichTextEditor.test.tsx
index eb5267719..97ad30ef3 100644
--- a/src/molecules/RichTextEditor/RichTextEditor.test.tsx
+++ b/src/molecules/RichTextEditor/RichTextEditor.test.tsx
@@ -6,12 +6,7 @@ import { RichTextEditor, RichTextEditorProps } from './RichTextEditor';
import { RichTextEditorFeatures } from './RichTextEditor.types';
import { colors } from 'src/atoms';
import type { AmplifyKitOptions } from 'src/molecules/RichTextEditor/custom-extensions/AmplifyKit';
-import {
- render,
- renderWithProviders,
- screen,
- userEvent,
-} from 'src/tests/test-utils';
+import { renderWithProviders, screen, userEvent } from 'src/tests/test-utils';
function fakeProps(withImage = false): RichTextEditorProps {
return {
@@ -24,7 +19,7 @@ function fakeProps(withImage = false): RichTextEditorProps {
test('Shows text that is input', async () => {
const props = fakeProps(true);
const fish = faker.animal.fish();
- render(${fish}
`} />);
+ renderWithProviders(${fish}`} />);
await waitFor(() => expect(screen.getByText(fish)).toBeInTheDocument(), {
timeout: 1000,
@@ -36,7 +31,7 @@ test('Throws error if providing RichTextEditorFeature.IMAGES but not an image ha
const props = fakeProps();
expect(() =>
- render(
+ renderWithProviders(
{
const props = fakeProps();
- const { container } = render(
+ const { container } = renderWithProviders(
{
test('Setting color works as expected', async () => {
const props = fakeProps();
- const { container } = render(
+ const { container } = renderWithProviders(
);
@@ -89,7 +84,7 @@ test('Setting color works as expected', async () => {
test('Calls onImageUpload as expected', async () => {
const props = fakeProps(true);
- const { container } = render(
+ const { container } = renderWithProviders(
);
const user = userEvent.setup();
@@ -112,7 +107,7 @@ test('Calls onImageUpload as expected', async () => {
test('Open file dialog', async () => {
const props = fakeProps(true);
- render(
+ renderWithProviders(
);
const user = userEvent.setup();
@@ -127,7 +122,7 @@ test('Open file dialog', async () => {
test('Creating table works as expected', async () => {
const props = fakeProps();
- render(
+ renderWithProviders(
);
const user = userEvent.setup();
@@ -144,7 +139,7 @@ test('Creating table works as expected', async () => {
test('Creating table with highlight works as expected', async () => {
const props = fakeProps();
- render(
+ renderWithProviders(
{
const props = fakeProps();
expect(() =>
- render(
+ renderWithProviders(