Skip to content
This repository was archived by the owner on Dec 18, 2024. It is now read-only.

Commit e77883b

Browse files
Feat: add delete functionality to Image component (#570)
* Feat: add delete functionality to Image component - add delete option to image component - add tests * update changelog * Force resolution of postcss to patched version * changes from code review Co-authored-by: Nicolas Seydoux <[email protected]>
1 parent aace993 commit e77883b

File tree

7 files changed

+23187
-23006
lines changed

7 files changed

+23187
-23006
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
88

99
### New features
1010

11+
- The `Image` component now takes an optional prop `allowDelete`, which renders a default delete button that will remove the value from the dataset. It is also possible to pass a `deleteComponent` to render a custom delete button in place of the default.
12+
1113
- The `Image` component now allows for a new image to be saved even if the property is not found in the dataset, by passing a `solidDataset` where the Thing should be set after updating, and a `saveLocation` where the image file should be stored.
1214

1315
### Bugfixes

package-lock.json

Lines changed: 23021 additions & 23004 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,8 @@
100100
"react-table": "^7.6.3",
101101
"stream": "0.0.2",
102102
"swr": "^0.5.7"
103+
},
104+
"resolutions": {
105+
"postcss": "8.4.5"
103106
}
104107
}

src/components/image/__snapshots__/index.test.tsx.snap

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,32 @@ exports[`Image component Image snapshots matches snapshot with standard props 1`
3434
</DocumentFragment>
3535
`;
3636

37+
exports[`Image component Image snapshots renders a a custom delete button if passed and allowDelete is true 1`] = `
38+
<DocumentFragment>
39+
<span>
40+
Error: No value found for property.
41+
</span>
42+
<button
43+
type="button"
44+
>
45+
Custom Delete Component
46+
</button>
47+
</DocumentFragment>
48+
`;
49+
50+
exports[`Image component Image snapshots renders a a default delete button if allowDelete is true 1`] = `
51+
<DocumentFragment>
52+
<span>
53+
Error: No value found for property.
54+
</span>
55+
<button
56+
type="button"
57+
>
58+
Delete
59+
</button>
60+
</DocumentFragment>
61+
`;
62+
3763
exports[`Image component Image snapshots renders an error message if an errorComponent is provided 1`] = `
3864
<DocumentFragment>
3965
<span

src/components/image/index.test.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
import React from "react";
2323
import { render, waitFor, fireEvent } from "@testing-library/react";
2424
import * as SolidFns from "@inrupt/solid-client";
25+
import type {
26+
SolidDataset,
27+
WithChangeLog,
28+
WithServerResourceInfo,
29+
} from "@inrupt/solid-client";
2530
import { Image } from ".";
2631
import * as helpers from "../../helpers";
2732

@@ -156,6 +161,33 @@ describe("Image component", () => {
156161

157162
expect(asFragment()).toMatchSnapshot();
158163
});
164+
it("renders a a default delete button if allowDelete is true", () => {
165+
const emptyThing = SolidFns.createThing();
166+
const { asFragment } = render(
167+
<Image
168+
thing={emptyThing}
169+
allowDelete
170+
property="https://example.com/url"
171+
/>
172+
);
173+
expect(asFragment()).toMatchSnapshot();
174+
});
175+
it("renders a a custom delete button if passed and allowDelete is true", () => {
176+
const emptyThing = SolidFns.createThing();
177+
const { asFragment } = render(
178+
<Image
179+
thing={emptyThing}
180+
allowDelete
181+
property="https://example.com/url"
182+
deleteComponent={({ onClick }) => (
183+
<button type="button" onClick={onClick}>
184+
Custom Delete Component
185+
</button>
186+
)}
187+
/>
188+
);
189+
expect(asFragment()).toMatchSnapshot();
190+
});
159191
});
160192

161193
describe("Image functional tests", () => {
@@ -267,7 +299,9 @@ describe("Image component", () => {
267299
jest.spyOn(SolidFns, "getUrl").mockImplementationOnce(() => null);
268300
jest
269301
.spyOn(SolidFns, "saveSolidDatasetAt")
270-
.mockResolvedValueOnce(mockDataset as any);
302+
.mockResolvedValueOnce(
303+
mockDataset as SolidDataset & WithServerResourceInfo & WithChangeLog
304+
);
271305
jest
272306
.spyOn(SolidFns, "saveFileInContainer")
273307
.mockResolvedValueOnce(mockFile);
@@ -309,6 +343,32 @@ describe("Image component", () => {
309343
);
310344
});
311345
});
346+
it("Should call saveSolidDatasetAt when clicking delete button", async () => {
347+
jest
348+
.spyOn(SolidFns, "saveSolidDatasetAt")
349+
.mockResolvedValue(
350+
mockDataset as SolidDataset & WithServerResourceInfo & WithChangeLog
351+
);
352+
const { getByAltText, getByText } = render(
353+
<Image
354+
thing={mockThing}
355+
solidDataset={mockDataset}
356+
edit
357+
autosave
358+
allowDelete
359+
property={mockProperty}
360+
alt={mockAlt}
361+
/>
362+
);
363+
await waitFor(() =>
364+
expect(getByAltText(mockAlt).getAttribute("src")).toBe(mockObjectUrl)
365+
);
366+
const deleteButton = getByText("Delete");
367+
fireEvent.click(deleteButton);
368+
await waitFor(() => {
369+
expect(SolidFns.saveSolidDatasetAt).toHaveBeenCalled();
370+
});
371+
});
312372

313373
test.skip("Should not call overwriteFile on change if file size > maxSize", async () => {
314374
const { getByAltText } = render(

src/components/image/index.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import React, { ReactElement, useState, useEffect, useContext } from "react";
2323
import {
2424
addUrl,
2525
getSourceUrl,
26+
removeUrl,
2627
saveFileInContainer,
2728
saveSolidDatasetAt,
2829
setThing,
@@ -36,10 +37,12 @@ import useFile from "../../hooks/useFile";
3637
export type Props = {
3738
maxSize?: number;
3839
saveLocation?: string;
40+
allowDelete?: boolean;
3941
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
4042
solidDataset?: SolidDataset;
4143
errorComponent?: React.ComponentType<{ error: Error }>;
4244
loadingComponent?: React.ComponentType | null;
45+
deleteComponent?: React.ComponentType<{ onClick: () => void }> | null;
4346
} & CommonProperties &
4447
React.ImgHTMLAttributes<HTMLImageElement>;
4548

@@ -52,13 +55,15 @@ export function Image({
5255
properties: propProperties,
5356
edit,
5457
autosave,
58+
allowDelete,
5559
onSave,
5660
onError,
5761
maxSize,
5862
alt,
5963
inputProps,
6064
errorComponent: ErrorComponent,
6165
loadingComponent: LoadingComponent,
66+
deleteComponent: DeleteComponent,
6267
saveLocation,
6368
solidDataset,
6469
...imgOptions
@@ -112,6 +117,28 @@ export function Image({
112117
}
113118
}, [data, fetchingFileInProgress, imgError]);
114119

120+
const handleDelete = async () => {
121+
if (
122+
!propThing ||
123+
!solidDataset ||
124+
!propProperty ||
125+
typeof value !== "string" ||
126+
!autosave
127+
)
128+
return;
129+
try {
130+
const updatedThing = removeUrl(propThing, propProperty, value);
131+
const updatedDataset = setThing(solidDataset, updatedThing);
132+
const datasetSourceUrl = getSourceUrl(solidDataset);
133+
if (!datasetSourceUrl) return;
134+
await saveSolidDatasetAt(datasetSourceUrl, updatedDataset, {
135+
fetch,
136+
});
137+
} catch (e) {
138+
setError(e as Error);
139+
}
140+
};
141+
115142
const handleChange = async (input: EventTarget & HTMLInputElement) => {
116143
const fileList = input.files;
117144
if (autosave && fileList && fileList.length > 0) {
@@ -187,6 +214,14 @@ export function Image({
187214
onChange={(e) => handleChange(e.target)}
188215
/>
189216
)}
217+
{allowDelete &&
218+
(DeleteComponent ? (
219+
<DeleteComponent onClick={handleDelete} />
220+
) : (
221+
<button type="button" onClick={handleDelete}>
222+
Delete
223+
</button>
224+
))}
190225
</>
191226
);
192227
}

stories/components/image.stories.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020
*/
2121

2222
import React, { ReactElement } from "react";
23-
import { addUrl, createThing } from "@inrupt/solid-client";
23+
import {
24+
addUrl,
25+
createSolidDataset,
26+
createThing,
27+
setThing,
28+
} from "@inrupt/solid-client";
2429
import { Image } from "../../src/components/image";
2530
import CombinedDataProvider from "../../src/context/combinedDataContext";
2631
import config from "../config";
@@ -84,12 +89,14 @@ interface IWithBasicData {
8489
property: string;
8590
properties: Array<string>;
8691
edit: boolean;
92+
allowDelete: boolean;
8793
maxSize: number;
8894
}
8995
export function BasicExample({
9096
property,
9197
properties,
9298
edit,
99+
allowDelete,
93100
maxSize,
94101
}: IWithBasicData): ReactElement {
95102
const thing = addUrl(createThing(), property, `${host}/example.jpg`);
@@ -101,13 +108,15 @@ export function BasicExample({
101108
properties={properties}
102109
edit={edit}
103110
maxSize={maxSize}
111+
allowDelete={allowDelete}
104112
/>
105113
);
106114
}
107115

108116
BasicExample.args = {
109117
property: "http://schema.org/contentUrl",
110118
edit: false,
119+
allowDelete: false,
111120
maxSize: 100000000,
112121
};
113122

@@ -119,6 +128,7 @@ export function PropertyArrayExample({
119128
properties,
120129
edit,
121130
maxSize,
131+
allowDelete,
122132
}: IWithBasicData): ReactElement {
123133
const thing = addUrl(createThing(), property, `${host}/example.jpg`);
124134

@@ -128,6 +138,7 @@ export function PropertyArrayExample({
128138
properties={properties}
129139
edit={edit}
130140
maxSize={maxSize}
141+
allowDelete={allowDelete}
131142
/>
132143
);
133144
}
@@ -194,3 +205,30 @@ export function ErrorComponent(): ReactElement {
194205
ErrorComponent.parameters = {
195206
actions: { disable: true },
196207
};
208+
209+
export function DeleteComponent(): ReactElement {
210+
const property = "http://schema.org/contentUrl";
211+
const thing = addUrl(createThing(), property, `${host}/example.jpg`);
212+
const dataset = setThing(createSolidDataset(), thing);
213+
214+
return (
215+
<Image
216+
thing={thing}
217+
solidDataset={dataset}
218+
edit
219+
autosave
220+
saveLocation={`${host}/`}
221+
property={property}
222+
allowDelete
223+
deleteComponent={({ onClick }) => (
224+
<button type="button" onClick={onClick}>
225+
Custom Delete Component
226+
</button>
227+
)}
228+
/>
229+
);
230+
}
231+
232+
DeleteComponent.parameters = {
233+
actions: { disable: true },
234+
};

0 commit comments

Comments
 (0)