@edifice.io/react 2.2.11 → 2.2.12-develop-enabling.20250716115923
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Button/Button.d.ts +1 -1
- package/dist/components/Button/Button.js +1 -1
- package/dist/components/Button/IconButton.js +2 -1
- package/dist/components/SearchBar/SearchBar.d.ts +33 -9
- package/dist/components/SearchBar/SearchBar.js +17 -2
- package/dist/components/Toolbar/Toolbar.js +1 -1
- package/dist/hooks/useDirectory/useDirectory.d.ts +1 -1
- package/dist/hooks/useDirectory/useDirectory.js +2 -1
- package/dist/hooks/useDropdown/useDropdown.js +6 -1
- package/dist/hooks/useLibraryUrl/useLibraryUrl.d.ts +1 -1
- package/dist/hooks/useLibraryUrl/useLibraryUrl.js +2 -2
- package/dist/hooks/useUploadFiles/useUploadFiles.js +9 -7
- package/dist/hooks/useZendeskGuide/useZendeskGuide.js +2 -1
- package/dist/icons-apps.js +174 -164
- package/dist/icons.js +198 -190
- package/dist/modules/editor/components/BubbleMenuEditInformationPane/BubbleMenuEditInformationPane.js +4 -4
- package/dist/modules/icons/components/IconLibrary.d.ts +7 -0
- package/dist/modules/icons/components/IconLibrary.js +12 -0
- package/dist/modules/icons/components/IconNotification.d.ts +7 -0
- package/dist/modules/icons/components/IconNotification.js +12 -0
- package/dist/modules/icons/components/IconStar.d.ts +7 -0
- package/dist/modules/icons/components/IconStar.js +13 -0
- package/dist/modules/icons/components/IconStarFavorite.d.ts +7 -0
- package/dist/modules/icons/components/IconStarFull.d.ts +7 -0
- package/dist/modules/icons/components/IconStarFull.js +13 -0
- package/dist/modules/icons/components/apps/IconAccount.js +2 -2
- package/dist/modules/icons/components/apps/IconActualites.js +2 -2
- package/dist/modules/icons/components/apps/IconAdmin.js +2 -2
- package/dist/modules/icons/components/apps/IconAgenda.js +3 -2
- package/dist/modules/icons/components/apps/IconAppointments.js +2 -2
- package/dist/modules/icons/components/apps/IconArchive.js +2 -2
- package/dist/modules/icons/components/apps/IconBlog.js +2 -2
- package/dist/modules/icons/components/apps/IconCahierDeTexte.js +2 -2
- package/dist/modules/icons/components/apps/IconCalendar.js +3 -2
- package/dist/modules/icons/components/apps/IconCollaborativeWall.js +2 -2
- package/dist/modules/icons/components/apps/IconCommunity.js +2 -2
- package/dist/modules/icons/components/apps/IconConversation.js +2 -2
- package/dist/modules/icons/components/apps/IconDirectory.js +2 -2
- package/dist/modules/icons/components/apps/IconEdt.d.ts +7 -0
- package/dist/modules/icons/components/apps/IconEdt.js +12 -0
- package/dist/modules/icons/components/apps/IconExercizer.js +2 -2
- package/dist/modules/icons/components/apps/IconForms.js +2 -6
- package/dist/modules/icons/components/apps/IconForum.js +2 -2
- package/dist/modules/icons/components/apps/IconGeogebra.d.ts +7 -0
- package/dist/modules/icons/components/apps/IconGeogebra.js +12 -0
- package/dist/modules/icons/components/apps/IconLibrary.js +2 -2
- package/dist/modules/icons/components/apps/IconMagneto.d.ts +7 -0
- package/dist/modules/icons/components/apps/IconMagneto.js +13 -0
- package/dist/modules/icons/components/apps/IconMediacentre.js +2 -2
- package/dist/modules/icons/components/apps/IconMindmap.js +2 -2
- package/dist/modules/icons/components/apps/IconMinetest.d.ts +7 -0
- package/dist/modules/icons/components/apps/IconMinetest.js +12 -0
- package/dist/modules/icons/components/apps/IconMinibadge.d.ts +7 -0
- package/dist/modules/icons/components/apps/IconMinibadge.js +12 -0
- package/dist/modules/icons/components/apps/IconMoodle.js +2 -2
- package/dist/modules/icons/components/apps/IconNabook.js +2 -4
- package/dist/modules/icons/components/apps/IconNotebook.js +2 -2
- package/dist/modules/icons/components/apps/IconPad.js +2 -2
- package/dist/modules/icons/components/apps/IconPages.js +2 -2
- package/dist/modules/icons/components/apps/IconParametrage.js +2 -2
- package/dist/modules/icons/components/apps/IconParametrages.js +2 -2
- package/dist/modules/icons/components/apps/IconPoll.js +2 -2
- package/dist/modules/icons/components/apps/IconPresences.js +2 -6
- package/dist/modules/icons/components/apps/IconRack.js +2 -2
- package/dist/modules/icons/components/apps/IconRbs.js +2 -2
- package/dist/modules/icons/components/apps/IconSchoolbook.js +2 -2
- package/dist/modules/icons/components/apps/IconScrapbook.js +2 -2
- package/dist/modules/icons/components/apps/IconSettingsClass.js +2 -2
- package/dist/modules/icons/components/apps/IconSharebigfiles.js +2 -2
- package/dist/modules/icons/components/apps/IconStatistics.js +2 -2
- package/dist/modules/icons/components/apps/IconSupport.js +2 -2
- package/dist/modules/icons/components/apps/IconTimeline.js +2 -2
- package/dist/modules/icons/components/apps/IconTimelinegenerator.js +2 -2
- package/dist/modules/icons/components/apps/IconUserbook.js +2 -2
- package/dist/modules/icons/components/apps/IconVisioconf.js +2 -2
- package/dist/modules/icons/components/apps/IconWebsite.js +2 -2
- package/dist/modules/icons/components/apps/IconWiki.js +3 -3
- package/dist/modules/icons/components/apps/IconWorkspace.js +2 -2
- package/dist/modules/icons/components/apps/index.d.ts +5 -0
- package/dist/modules/icons/components/index.d.ts +4 -0
- package/dist/modules/modals/OnboardingModal/OnboardingModal.d.ts +6 -4
- package/dist/modules/modals/OnboardingModal/OnboardingModal.js +27 -13
- package/dist/modules/modals/OnboardingModal/index.d.ts +1 -0
- package/dist/modules/modals/OnboardingModal/useOnboardingModal.js +4 -4
- package/package.json +6 -6
|
@@ -2,7 +2,7 @@ import { ReactNode } from 'react';
|
|
|
2
2
|
import { LoadingIcon, LoadingPosition } from '../Loading';
|
|
3
3
|
export type ButtonRef = HTMLButtonElement;
|
|
4
4
|
export type ButtonTypes = 'button' | 'submit' | 'reset';
|
|
5
|
-
export type ButtonColors = 'primary' | 'secondary' | 'tertiary' | 'danger';
|
|
5
|
+
export type ButtonColors = 'primary' | 'secondary' | 'tertiary' | 'danger' | 'black';
|
|
6
6
|
export type ButtonVariants = 'filled' | 'outline' | 'ghost';
|
|
7
7
|
export type ButtonSizes = 'sm' | 'md' | 'lg';
|
|
8
8
|
export interface ButtonProps extends React.ComponentPropsWithRef<'button'> {
|
|
@@ -19,7 +19,7 @@ const Button = /* @__PURE__ */ forwardRef(({
|
|
|
19
19
|
const classes = clsx("btn", {
|
|
20
20
|
[`btn-filled btn-${color}`]: variant === "filled",
|
|
21
21
|
[`btn-${variant}-${color}`]: variant === "outline" || variant === "ghost",
|
|
22
|
-
"btn-icon
|
|
22
|
+
"btn-icon": !children,
|
|
23
23
|
"btn-loading": isLoading,
|
|
24
24
|
"btn-lg": size === "lg",
|
|
25
25
|
"btn-sm": size === "sm",
|
|
@@ -9,7 +9,8 @@ const IconButton = /* @__PURE__ */ forwardRef(({
|
|
|
9
9
|
}, ref) => {
|
|
10
10
|
const buttonProps = {
|
|
11
11
|
...restProps,
|
|
12
|
-
className: clsx("btn-icon
|
|
12
|
+
className: clsx("btn-icon", className),
|
|
13
|
+
size: restProps.size || "sm"
|
|
13
14
|
};
|
|
14
15
|
return /* @__PURE__ */ jsx(Button, { ref, ...buttonProps, leftIcon: icon });
|
|
15
16
|
});
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { ChangeEvent } from 'react';
|
|
2
2
|
import { Size } from '../../types';
|
|
3
|
+
/**
|
|
4
|
+
* Base props shared by both SearchBar variants
|
|
5
|
+
*/
|
|
3
6
|
export interface BaseProps extends Omit<React.ComponentPropsWithoutRef<'input'>, 'size'> {
|
|
4
7
|
/**
|
|
5
|
-
* String or
|
|
8
|
+
* String or template literal key for i18next translation
|
|
6
9
|
*/
|
|
7
10
|
placeholder?: string;
|
|
8
11
|
/**
|
|
9
|
-
* Control SearchBar size
|
|
12
|
+
* Control SearchBar size (excluding 'sm')
|
|
10
13
|
*/
|
|
11
14
|
size?: Exclude<Size, 'sm'>;
|
|
12
15
|
/**
|
|
13
|
-
*
|
|
16
|
+
* Disable the input
|
|
14
17
|
*/
|
|
15
18
|
disabled?: boolean;
|
|
16
19
|
/**
|
|
@@ -19,37 +22,58 @@ export interface BaseProps extends Omit<React.ComponentPropsWithoutRef<'input'>,
|
|
|
19
22
|
buttonDisabled?: boolean;
|
|
20
23
|
/**
|
|
21
24
|
* Optional class for styling purpose
|
|
25
|
+
* Optional class for custom styling
|
|
22
26
|
*/
|
|
23
27
|
className?: string;
|
|
24
28
|
/**
|
|
25
|
-
*
|
|
29
|
+
* onChange handler for input changes
|
|
26
30
|
*/
|
|
27
31
|
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Current value of the input
|
|
34
|
+
*/
|
|
35
|
+
value?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Show a clear (reset) button when value is present
|
|
38
|
+
*/
|
|
39
|
+
clearable?: boolean;
|
|
28
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Default SearchBar with a submit button
|
|
43
|
+
*/
|
|
29
44
|
type DefaultSearchBar = {
|
|
30
45
|
/**
|
|
31
|
-
*
|
|
46
|
+
* Use false to render the default SearchBar (with a button)
|
|
32
47
|
*/
|
|
33
48
|
isVariant: false;
|
|
34
49
|
/**
|
|
35
|
-
*
|
|
50
|
+
* Callback when clicking the search button
|
|
36
51
|
*/
|
|
37
52
|
onClick: () => void;
|
|
38
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Dynamic SearchBar with icon and no submit button
|
|
56
|
+
*/
|
|
39
57
|
type DynamicSearchBar = {
|
|
40
58
|
/**
|
|
41
|
-
*
|
|
59
|
+
* Use true to render the dynamic SearchBar (with an icon inside input)
|
|
42
60
|
*/
|
|
43
61
|
isVariant: true;
|
|
44
62
|
/**
|
|
45
|
-
*
|
|
63
|
+
* onClick must be undefined for dynamic variant
|
|
46
64
|
*/
|
|
47
65
|
onClick?: undefined;
|
|
48
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Props for the SearchBar component
|
|
69
|
+
*/
|
|
49
70
|
export type Props = DefaultSearchBar | DynamicSearchBar;
|
|
50
71
|
export type SearchBarProps = BaseProps & Props;
|
|
72
|
+
/**
|
|
73
|
+
* SearchBar component to handle dynamic or static search input
|
|
74
|
+
*/
|
|
51
75
|
declare const SearchBar: {
|
|
52
|
-
({ isVariant, size, placeholder, className, disabled, buttonDisabled, onChange, onClick, ...restProps }: SearchBarProps): import("react/jsx-runtime").JSX.Element;
|
|
76
|
+
({ isVariant, size, placeholder, className, disabled, buttonDisabled, onChange, onClick, value, clearable, ...restProps }: SearchBarProps): import("react/jsx-runtime").JSX.Element;
|
|
53
77
|
displayName: string;
|
|
54
78
|
};
|
|
55
79
|
export default SearchBar;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import clsx from "clsx";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
|
+
import SvgIconClose from "../../modules/icons/components/IconClose.js";
|
|
4
5
|
import SvgIconSearch from "../../modules/icons/components/IconSearch.js";
|
|
5
6
|
import FormControl from "../Form/FormControl.js";
|
|
6
7
|
import SearchButton from "../Button/SearchButton.js";
|
|
@@ -13,6 +14,8 @@ const SearchBar = ({
|
|
|
13
14
|
buttonDisabled,
|
|
14
15
|
onChange,
|
|
15
16
|
onClick,
|
|
17
|
+
value,
|
|
18
|
+
clearable = !1,
|
|
16
19
|
...restProps
|
|
17
20
|
}) => {
|
|
18
21
|
const {
|
|
@@ -22,15 +25,27 @@ const SearchBar = ({
|
|
|
22
25
|
"position-relative": isVariant
|
|
23
26
|
}), input = clsx({
|
|
24
27
|
"border-end-0": !isVariant,
|
|
25
|
-
"ps-48": isVariant
|
|
28
|
+
"ps-48": isVariant,
|
|
29
|
+
"searchbar-hide-native-clear": isVariant && clearable
|
|
26
30
|
}), handleClick = () => {
|
|
27
31
|
onClick == null || onClick();
|
|
28
32
|
}, handleKeyDown = (e) => {
|
|
29
33
|
e.key === "Enter" && (e.preventDefault(), handleClick());
|
|
34
|
+
}, handleClear = () => {
|
|
35
|
+
const event = {
|
|
36
|
+
target: {
|
|
37
|
+
value: ""
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
onChange == null || onChange(event);
|
|
30
41
|
};
|
|
31
42
|
return /* @__PURE__ */ jsxs(FormControl, { id: "search-bar", className: searchbar, children: [
|
|
32
43
|
isVariant && /* @__PURE__ */ jsx("div", { className: "position-absolute z-1 top-50 start-0 translate-middle-y border-0 ps-12 bg-transparent", children: /* @__PURE__ */ jsx(SvgIconSearch, {}) }),
|
|
33
|
-
/* @__PURE__ */ jsx(FormControl.Input, { type: "search", placeholder: t(placeholder), size, noValidationIcon: !0, className: input, onChange, disabled, onKeyDown: handleKeyDown, ...restProps }),
|
|
44
|
+
/* @__PURE__ */ jsx(FormControl.Input, { type: "search", placeholder: t(placeholder), size, noValidationIcon: !0, className: input, onChange, value, disabled, onKeyDown: handleKeyDown, ...restProps }),
|
|
45
|
+
isVariant && clearable && value && onChange && /* @__PURE__ */ jsx("button", { type: "button", onClick: handleClear, className: "position-absolute end-0 top-50 translate-middle-y pe-12 bg-transparent border-0", "aria-label": t("clear"), children: /* @__PURE__ */ jsx(SvgIconClose, { className: "color-gray", style: {
|
|
46
|
+
width: 12,
|
|
47
|
+
height: 12
|
|
48
|
+
} }) }),
|
|
34
49
|
!isVariant && /* @__PURE__ */ jsx(SearchButton, { type: "submit", "aria-label": t("search"), icon: /* @__PURE__ */ jsx(SvgIconSearch, {}), className: "border-start-0", onClick: handleClick, disabled: buttonDisabled })
|
|
35
50
|
] });
|
|
36
51
|
};
|
|
@@ -66,7 +66,7 @@ const Toolbar = /* @__PURE__ */ forwardRef(({
|
|
|
66
66
|
case "divider":
|
|
67
67
|
return /* @__PURE__ */ jsx("div", { className: "toolbar-divider" }, item.name ?? index);
|
|
68
68
|
case "button":
|
|
69
|
-
return /* @__PURE__ */ jsx(Tooltip, { message: hideLabel ? renderTooltipMessage(item) : void 0, placement: renderTooltipPosition(item), children: /* @__PURE__ */ createElement(Button, { ...item.props, children: !hideLabel && item.props.children, "aria-label": item.name, key: item.name ?? index, color: item.props.color || "tertiary", variant: "ghost", tabIndex: index === firstFocusableItemIndex ? 0 : -1, onKeyDown: handleKeyDown }) }, item.name ?? index);
|
|
69
|
+
return /* @__PURE__ */ jsx(Tooltip, { message: hideLabel ? renderTooltipMessage(item) : void 0, placement: renderTooltipPosition(item), children: /* @__PURE__ */ createElement(Button, { ...item.props, children: !hideLabel && item.props.children, "aria-label": item.name, key: item.name ?? index, color: item.props.color || "tertiary", variant: "ghost", size: item.props.size || hideLabel ? "sm" : "md", tabIndex: index === firstFocusableItemIndex ? 0 : -1, onKeyDown: handleKeyDown }) }, item.name ?? index);
|
|
70
70
|
case "icon":
|
|
71
71
|
return /* @__PURE__ */ jsx(Tooltip, { message: renderTooltipMessage(item), placement: renderTooltipPosition(item), children: /* @__PURE__ */ createElement(IconButton, { ...item.props, key: item.name ?? index, color: item.props.color ? item.props.color : "tertiary", variant: item.props.variant ? item.props.variant : "ghost", tabIndex: index === firstFocusableItemIndex ? 0 : -1, onKeyDown: handleKeyDown }) }, item.name ?? index);
|
|
72
72
|
case "dropdown":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ID } from '@edifice.io/client';
|
|
2
2
|
declare const useDirectory: () => {
|
|
3
|
-
getAvatarURL: (userId: ID, type: "user" | "group") => string;
|
|
3
|
+
getAvatarURL: (userId: ID, type: "user" | "group") => string | undefined;
|
|
4
4
|
getUserbookURL: (userId: ID, type: "user" | "group") => string;
|
|
5
5
|
};
|
|
6
6
|
export default useDirectory;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { odeServices } from "@edifice.io/client";
|
|
2
2
|
const useDirectory = () => {
|
|
3
3
|
function getAvatarURL(userId, type) {
|
|
4
|
-
|
|
4
|
+
if (userId !== "")
|
|
5
|
+
return odeServices.directory().getAvatarUrl(userId, type);
|
|
5
6
|
}
|
|
6
7
|
function getUserbookURL(userId, type) {
|
|
7
8
|
return odeServices.directory().getDirectoryUrl(userId, type);
|
|
@@ -35,7 +35,12 @@ const useDropdown = (placement, extraTriggerKeyDownHandler, isTriggerHovered = !
|
|
|
35
35
|
visible && menuRef.current && focusOnVisible ? (menuRef.current.focus(), setActiveIndex(0)) : (setActiveIndex(-1), itemRefs.current = {});
|
|
36
36
|
}, [visible]), useEffect(() => {
|
|
37
37
|
if (activeIndex !== -1) {
|
|
38
|
-
const
|
|
38
|
+
const filteredItems = Object.values(itemRefs.current).filter((item) => !!item);
|
|
39
|
+
if (activeIndex < 0 || activeIndex >= filteredItems.length) {
|
|
40
|
+
setActiveIndex(-1), setIsFocused(null);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const currentItem = filteredItems[activeIndex];
|
|
39
44
|
if (currentItem) {
|
|
40
45
|
const id2 = currentItem.getAttribute("id");
|
|
41
46
|
setIsFocused(id2), currentItem.focus();
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { libraryMaps } from "@edifice.io/utilities";
|
|
2
2
|
import { useEdificeClient } from "../../providers/EdificeClientProvider/EdificeClientProvider.hook.js";
|
|
3
|
-
const useLibraryUrl = () => {
|
|
3
|
+
const useLibraryUrl = (appCodeName) => {
|
|
4
4
|
var _a;
|
|
5
5
|
const {
|
|
6
6
|
user,
|
|
7
7
|
appCode
|
|
8
|
-
} = useEdificeClient(), appName = libraryMaps[appCode], libraryApp = user == null ? void 0 : user.apps.find((app) => app.isExternal && app.address.includes("library"));
|
|
8
|
+
} = useEdificeClient(), appName = libraryMaps[appCodeName ?? appCode], libraryApp = user == null ? void 0 : user.apps.find((app) => app.isExternal && app.address.includes("library"));
|
|
9
9
|
if (!libraryApp)
|
|
10
10
|
return null;
|
|
11
11
|
const libraryUrlSplit = (_a = libraryApp.address) == null ? void 0 : _a.split("?");
|
|
@@ -11,7 +11,8 @@ const useUploadFiles = ({
|
|
|
11
11
|
const [uploadedFiles, setUploadedFiles] = useState([]), [editingImage, setEditingImage] = useState(void 0), {
|
|
12
12
|
files,
|
|
13
13
|
deleteFile,
|
|
14
|
-
replaceFileAt
|
|
14
|
+
replaceFileAt,
|
|
15
|
+
inputRef
|
|
15
16
|
} = useDropzoneContext(), {
|
|
16
17
|
remove,
|
|
17
18
|
createOrUpdate
|
|
@@ -21,18 +22,19 @@ const useUploadFiles = ({
|
|
|
21
22
|
clearUploadStatus,
|
|
22
23
|
uploadFile,
|
|
23
24
|
uploadAlternateFile
|
|
24
|
-
} = useUpload(visibility, application),
|
|
25
|
+
} = useUpload(visibility, application), resetInputValue = useCallback(() => {
|
|
26
|
+
inputRef.current && (inputRef.current.value = "");
|
|
27
|
+
}, [inputRef]), tryUploading = useCallback((files2) => {
|
|
25
28
|
files2.forEach(async (file, index) => {
|
|
26
29
|
if (file == null) return;
|
|
27
|
-
let resource;
|
|
30
|
+
let resource, replacement;
|
|
28
31
|
if (file.type.startsWith("image"))
|
|
29
32
|
try {
|
|
30
|
-
|
|
31
|
-
resource = await uploadAlternateFile(file, replacement), replaceFileAt(index, replacement);
|
|
33
|
+
replacement = await ImageResizer.resizeImageFile(file), resource = await uploadAlternateFile(file, replacement), replaceFileAt(index, replacement);
|
|
32
34
|
} catch (err) {
|
|
33
35
|
console.error(err);
|
|
34
36
|
}
|
|
35
|
-
resource
|
|
37
|
+
!resource && !replacement && (resource = await uploadFile(file)), resource ? setUploadedFiles((prevFiles) => [...prevFiles, resource]) : resetInputValue();
|
|
36
38
|
});
|
|
37
39
|
}, [uploadAlternateFile, uploadFile, replaceFileAt]);
|
|
38
40
|
useEffect(() => {
|
|
@@ -53,7 +55,7 @@ const useUploadFiles = ({
|
|
|
53
55
|
};
|
|
54
56
|
async function removeFile(file) {
|
|
55
57
|
const resource = uploadedFiles.find((uploadedFile) => uploadedFile.name === file.name);
|
|
56
|
-
resource && (await remove(resource), clearUploadStatus(file), setUploadedFiles((prevFiles) => prevFiles.filter((prevFile) => prevFile.name !== (resource == null ? void 0 : resource.name)))), deleteFile(file);
|
|
58
|
+
resource && (await remove(resource), clearUploadStatus(file), setUploadedFiles((prevFiles) => prevFiles.filter((prevFile) => prevFile.name !== (resource == null ? void 0 : resource.name)))), deleteFile(file), resetInputValue();
|
|
57
59
|
}
|
|
58
60
|
async function updateImage({
|
|
59
61
|
blob,
|