@firecms/core 3.0.0-canary.77 → 3.0.0-canary.79

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.
@@ -37,4 +37,9 @@ export interface PropertyPreviewProps<T extends CMSType = any, CustomProps = any
37
37
  * Additional properties set by the developer
38
38
  */
39
39
  customProps?: CustomProps;
40
+ /**
41
+ * If the preview should be interactive or not.
42
+ * This applies only to videos.
43
+ */
44
+ interactive?: boolean;
40
45
  }
@@ -4,10 +4,11 @@ type StorageThumbnailProps = {
4
4
  storagePathOrDownloadUrl: string;
5
5
  storeUrl: boolean;
6
6
  size: PreviewSize;
7
+ interactive?: boolean;
7
8
  };
8
9
  /**
9
10
  * @group Preview components
10
11
  */
11
12
  export declare const StorageThumbnail: React.FunctionComponent<StorageThumbnailProps>;
12
- export declare function StorageThumbnailInternal({ storeUrl, storagePathOrDownloadUrl, size }: StorageThumbnailProps): import("react/jsx-runtime").JSX.Element | null;
13
+ export declare function StorageThumbnailInternal({ storeUrl, interactive, storagePathOrDownloadUrl, size }: StorageThumbnailProps): import("react/jsx-runtime").JSX.Element | null;
13
14
  export {};
@@ -4,9 +4,10 @@ import { PreviewSize } from "../PropertyPreviewProps";
4
4
  /**
5
5
  * @group Preview components
6
6
  */
7
- export declare function UrlComponentPreview({ url, previewType, size, hint }: {
7
+ export declare function UrlComponentPreview({ url, previewType, size, hint, interactive }: {
8
8
  url: string;
9
9
  previewType?: PreviewType;
10
10
  size: PreviewSize;
11
11
  hint?: string;
12
+ interactive?: boolean;
12
13
  }): React.ReactElement;
@@ -32,7 +32,7 @@ export type AuthController<UserType extends User = any, ExtraData extends any =
32
32
  /**
33
33
  * Sign out
34
34
  */
35
- signOut: () => void;
35
+ signOut: () => Promise<void>;
36
36
  /**
37
37
  * Error initializing the authentication
38
38
  */
@@ -80,6 +80,12 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
80
80
  collectionActionsProps?: COL_ACTIONS_PROPS;
81
81
  CollectionActionsStart?: React.ComponentType<CollectionActionsProps<any, any, EC> & COL_ACTIONS_START__PROPS> | React.ComponentType<CollectionActionsProps<any, any, EC> & COL_ACTIONS_START__PROPS>[];
82
82
  collectionActionsStartProps?: COL_ACTIONS_START__PROPS;
83
+ blockSearch?: (props: {
84
+ context: FireCMSContext;
85
+ path: string;
86
+ collection: EC;
87
+ parentCollectionIds?: string[];
88
+ }) => boolean;
83
89
  showTextSearchBar?: (props: {
84
90
  context: FireCMSContext;
85
91
  path: string;
@@ -628,6 +628,11 @@ export type StorageConfig = {
628
628
  * after it has been resolved.
629
629
  */
630
630
  postProcess?: (pathOrUrl: string) => Promise<string>;
631
+ /**
632
+ * You can use this prop in order to provide a custom preview URL.
633
+ * Useful when the file's path is different from the original field value
634
+ */
635
+ previewUrl?: (fileName: string) => string;
631
636
  };
632
637
  /**
633
638
  * @group Entity properties
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.0.0-canary.77",
4
+ "version": "3.0.0-canary.79",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -46,8 +46,8 @@
46
46
  "./package.json": "./package.json"
47
47
  },
48
48
  "dependencies": {
49
- "@firecms/formex": "^3.0.0-canary.77",
50
- "@firecms/ui": "^3.0.0-canary.77",
49
+ "@firecms/formex": "^3.0.0-canary.79",
50
+ "@firecms/ui": "^3.0.0-canary.79",
51
51
  "@fontsource/jetbrains-mono": "^5.0.20",
52
52
  "@hello-pangea/dnd": "^16.6.0",
53
53
  "@radix-ui/react-portal": "^1.1.1",
@@ -85,17 +85,8 @@
85
85
  "@types/react": "^18.3.3",
86
86
  "@types/react-dom": "^18.3.0",
87
87
  "@types/react-measure": "^2.0.12",
88
- "@typescript-eslint/eslint-plugin": "^7.15.0",
89
- "@typescript-eslint/parser": "^7.15.0",
90
88
  "@vitejs/plugin-react": "^4.3.1",
91
89
  "cross-env": "^7.0.3",
92
- "eslint": "^9.6.0",
93
- "eslint-config-standard": "^17.1.0",
94
- "eslint-plugin-import": "^2.29.1",
95
- "eslint-plugin-n": "^16.6.2",
96
- "eslint-plugin-promise": "^6.4.0",
97
- "eslint-plugin-react": "^7.34.3",
98
- "eslint-plugin-react-hooks": "^4.6.2",
99
90
  "firebase": "^10.12.2",
100
91
  "jest": "^29.7.0",
101
92
  "npm-run-all": "^4.1.5",
@@ -111,7 +102,7 @@
111
102
  "dist",
112
103
  "src"
113
104
  ],
114
- "gitHead": "2eb2e7d18eb036aa68f2c6c633093fc67417cba5",
105
+ "gitHead": "1007170266579ae963305dff219dfd57c5e97bc7",
115
106
  "publishConfig": {
116
107
  "access": "public"
117
108
  },
@@ -1,7 +1,7 @@
1
1
  import React, { MouseEvent, useCallback } from "react";
2
2
 
3
3
  import { CollectionSize, Entity, EntityAction, EntityCollection, SelectionController } from "../../types";
4
- import { Checkbox, cls, IconButton, Menu, MenuItem, MoreVertIcon, Skeleton, Tooltip, Typography } from "@firecms/ui";
4
+ import { Checkbox, cls, IconButton, Menu, MenuItem, MoreVertIcon, Skeleton, Tooltip } from "@firecms/ui";
5
5
  import { useFireCMSContext, useLargeLayout } from "../../hooks";
6
6
 
7
7
  /**
@@ -149,16 +149,14 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
149
149
  </div>}
150
150
 
151
151
  {!hideId && size !== "xs" && (
152
- <div className="w-[138px] text-center overflow-hidden truncate">
152
+ <div
153
+ className="w-[138px] text-center overflow-hidden truncate font-mono text-xs text-text-secondary dark:text-text-secondary-dark max-w-full text-ellipsis px-2"
154
+ onClick={(event) => {
155
+ event.stopPropagation();
156
+ }}>
153
157
 
154
158
  {entity
155
- ? <Typography
156
- onClick={(event) => {
157
- event.stopPropagation();
158
- }}
159
- className={"font-mono select-all"}
160
- variant={"caption"}
161
- color={"secondary"}> {entity.id} </Typography>
159
+ ? entity.id
162
160
  : <Skeleton/>
163
161
  }
164
162
  </div>
@@ -21,33 +21,43 @@ export function useTableSearchHelper<M extends Record<string, any>>({
21
21
 
22
22
  const [textSearchLoading, setTextSearchLoading] = useState<boolean>(false);
23
23
  const [textSearchInitialised, setTextSearchInitialised] = useState<boolean>(false);
24
+
24
25
  let onTextSearchClick: (() => void) | undefined;
25
26
  let textSearchEnabled = Boolean(collection.textSearchEnabled);
26
- if (customizationController?.plugins) {
27
- const addTextSearchClickListener = dataSource?.initTextSearch || customizationController.plugins?.find(p => Boolean(p.collectionView?.onTextSearchClick));
27
+
28
+ const props = {
29
+ context,
30
+ path: fullPath,
31
+ collection,
32
+ parentCollectionIds
33
+ };
34
+
35
+ const searchBlocked = customizationController.plugins?.find(p => {
36
+ return p.collectionView?.blockSearch?.(props);
37
+ });
38
+
39
+ const addTextSearchClickListener = Boolean(dataSource?.initTextSearch) || customizationController.plugins?.find(p => Boolean(p.collectionView?.onTextSearchClick));
40
+
41
+ if (addTextSearchClickListener) {
28
42
 
29
43
  onTextSearchClick = addTextSearchClickListener
30
44
  ? () => {
31
45
  setTextSearchLoading(true);
32
46
  const promises: Promise<boolean>[] = [];
33
- if (dataSource?.initTextSearch) {
34
- promises.push(dataSource.initTextSearch({
35
- context,
36
- path: fullPath,
37
- collection,
38
- parentCollectionIds
39
- }));
47
+ if (dataSource?.initTextSearch && !searchBlocked) {
48
+ promises.push(dataSource.initTextSearch(props));
49
+ }
50
+ if (searchBlocked) {
51
+ customizationController.plugins?.forEach(p => {
52
+ if (p.collectionView?.onTextSearchClick)
53
+ promises.push(p.collectionView.onTextSearchClick({
54
+ context,
55
+ path: fullPath,
56
+ collection,
57
+ parentCollectionIds
58
+ }));
59
+ })
40
60
  }
41
- customizationController.plugins?.forEach(p => {
42
- if (p.collectionView?.onTextSearchClick)
43
- promises.push(p.collectionView.onTextSearchClick({
44
- context,
45
- path: fullPath,
46
- collection,
47
- parentCollectionIds
48
- }));
49
- return Promise.resolve(true);
50
- })
51
61
  return Promise.all(promises)
52
62
  .then((res: boolean[]) => {
53
63
  if (res.every(Boolean)) setTextSearchInitialised(true);
@@ -18,14 +18,14 @@ export const DialogsProvider: React.FC<PropsWithChildren<{}>> = ({ children }) =
18
18
  if (dialogEntries.length === 0)
19
19
  return;
20
20
 
21
- const updatedPanels = [...dialogEntries.slice(0, -1)];
21
+ const updatedPanels = [...dialogEntriesRef.current.slice(0, -1)];
22
22
  updateDialogEntries(updatedPanels);
23
23
 
24
24
  }, [dialogEntries]);
25
25
 
26
26
  const open = useCallback((dialogEntry: DialogControllerEntryProps) => {
27
27
 
28
- const updatedPanels = [...dialogEntries, dialogEntry];
28
+ const updatedPanels = [...dialogEntriesRef.current, dialogEntry];
29
29
  updateDialogEntries(updatedPanels);
30
30
 
31
31
  return {
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { Link as ReactLink } from "react-router-dom";
3
+ import { Link as ReactLink, useNavigate } from "react-router-dom";
4
4
  import { ErrorBoundary, FireCMSLogo } from "../components";
5
5
  import {
6
6
  Avatar,
@@ -71,6 +71,8 @@ export const DefaultAppBar = function DefaultAppBar({
71
71
  toggleMode
72
72
  } = useModeController();
73
73
 
74
+ const navigate = useNavigate();
75
+
74
76
  const largeLayout = useLargeLayout();
75
77
 
76
78
  const user = userProp ?? authController.user;
@@ -161,7 +163,11 @@ export const DefaultAppBar = function DefaultAppBar({
161
163
 
162
164
  {dropDownActions}
163
165
 
164
- {!dropDownActions && <MenuItem onClick={authController.signOut}>
166
+ {!dropDownActions && <MenuItem onClick={async () => {
167
+ await authController.signOut();
168
+ // replace current route with home
169
+ navigate("/");
170
+ }}>
165
171
  <LogoutIcon/>
166
172
  Log Out
167
173
  </MenuItem>}
@@ -1,9 +1,9 @@
1
1
  import React from "react";
2
2
 
3
- import { Entity, EntityCollection, ResolvedStringProperty } from "../../types";
3
+ import { ResolvedStringProperty } from "../../types";
4
4
  import { PreviewSize, PropertyPreview } from "../../preview";
5
5
 
6
- import { cls, IconButton, paperMixin, RemoveIcon, Tooltip } from "@firecms/ui";
6
+ import { cls, DescriptionIcon, IconButton, paperMixin, RemoveIcon, Tooltip } from "@firecms/ui";
7
7
  import { ErrorBoundary } from "../../components";
8
8
 
9
9
  interface StorageItemPreviewProps {
@@ -13,6 +13,8 @@ interface StorageItemPreviewProps {
13
13
  onRemove: (value: string) => void;
14
14
  size: PreviewSize;
15
15
  disabled: boolean;
16
+ placeholder?: boolean;
17
+ className?: string;
16
18
  }
17
19
 
18
20
  export function StorageItemPreview({
@@ -22,14 +24,17 @@ export function StorageItemPreview({
22
24
  onRemove,
23
25
  disabled,
24
26
  size,
27
+ placeholder,
28
+ className
25
29
  }: StorageItemPreviewProps) {
26
30
 
27
31
  return (
28
32
  <div className={cls(paperMixin,
29
33
  "relative m-4 border-box flex items-center justify-center",
30
- size === "medium" ? "min-w-[220px] min-h-[220px] max-w-[220px]" : "min-w-[118px] min-h-[118px] max-w-[118px]")}>
34
+ size === "medium" ? "min-w-[220px] min-h-[220px] max-w-[220px]" : "min-w-[118px] min-h-[118px] max-w-[118px]",
35
+ className)}>
31
36
 
32
- {!disabled &&
37
+ {!placeholder && !disabled &&
33
38
  <div
34
39
  className="absolute rounded-full -top-2 -right-2 z-10 bg-white dark:bg-gray-900">
35
40
 
@@ -47,16 +52,24 @@ export function StorageItemPreview({
47
52
  </div>
48
53
  }
49
54
 
50
- {value &&
55
+ {!placeholder && value &&
51
56
  <ErrorBoundary>
52
57
  <PropertyPreview propertyKey={name}
53
58
  value={value}
54
59
  property={property}
55
- // entity={entity}
60
+ interactive={false}
56
61
  size={size}/>
57
62
  </ErrorBoundary>
58
63
  }
59
64
 
65
+ {placeholder &&
66
+ <div
67
+ onClick={(e) => e.stopPropagation()}
68
+ className="flex flex-col items-center justify-center w-full h-full">
69
+ <DescriptionIcon className="text-gray-700 dark:text-gray-300"/>
70
+ </div>
71
+ }
72
+
60
73
 
61
74
  </div>
62
75
  );
@@ -21,6 +21,7 @@ import {
21
21
  } from "@firecms/ui";
22
22
  import { getDefaultValueForDataType, getIconForProperty } from "../../util";
23
23
  import { useCustomizationController } from "../../hooks";
24
+ import { getIn } from "@firecms/formex";
24
25
 
25
26
  type MapEditViewRowState = [number, {
26
27
  key: string,
@@ -52,9 +53,13 @@ export function KeyValueFieldBinding({
52
53
  if (!property.keyValue) {
53
54
  throw Error(`Your property ${propertyKey} needs to have the 'keyValue' prop in order to use this field binding`);
54
55
  }
56
+
57
+ const initialValues = getIn(context.formex.initialValues, propertyKey);
58
+
55
59
  const mapFormView = <MapEditView value={value}
56
60
  setValue={setValue}
57
61
  disabled={disabled}
62
+ initialValue={initialValues}
58
63
  fieldName={property.name ?? propertyKey}/>;
59
64
 
60
65
  const title = <LabelWithIcon
@@ -84,6 +89,7 @@ export function KeyValueFieldBinding({
84
89
 
85
90
  interface MapEditViewParams<T extends Record<string, any>> {
86
91
  value?: T;
92
+ initialValue?: T;
87
93
  setValue: (value: (T | null)) => void;
88
94
  fieldName?: string,
89
95
  disabled?: boolean
@@ -91,14 +97,15 @@ interface MapEditViewParams<T extends Record<string, any>> {
91
97
 
92
98
  function MapEditView<T extends Record<string, any>>({
93
99
  value,
100
+ initialValue,
94
101
  setValue,
95
102
  fieldName,
96
103
  disabled
97
104
  }: MapEditViewParams<T>) {
98
105
  const [internalState, setInternalState] = React.useState<MapEditViewRowState[]>(
99
- Object.keys(value ?? {}).map((key) => [getRandomId(), {
106
+ Object.keys(initialValue ?? {}).map((key) => [getRandomId(), {
100
107
  key,
101
- dataType: getDataType(value?.[key]) ?? "string"
108
+ dataType: getDataType(initialValue?.[key]) ?? "string"
102
109
  }])
103
110
  );
104
111
 
@@ -121,8 +128,6 @@ function MapEditView<T extends Record<string, any>>({
121
128
  setInternalState(newRowIds);
122
129
  }, [value]);
123
130
 
124
- const originalValue = React.useRef<T>(value ?? {} as T);
125
-
126
131
  const updateDataType = (rowId: number, dataType: DataType) => {
127
132
  if (!rowId) {
128
133
  console.warn("No key selected for data type update");
@@ -168,7 +173,7 @@ function MapEditView<T extends Record<string, any>>({
168
173
  }
169
174
 
170
175
  const newValue = { ...(value ?? {}) } as T;
171
- if (typeof originalValue.current === "object" && fieldKey in originalValue.current) {
176
+ if (typeof initialValue === "object" && fieldKey in initialValue) {
172
177
  // @ts-ignore
173
178
  newValue[fieldKey] = undefined; // set to undefined to remove from the object, the datasource will remove it from the backend
174
179
  } else {
@@ -186,7 +191,7 @@ function MapEditView<T extends Record<string, any>>({
186
191
  value={value ?? {} as T}
187
192
  onDeleteClick={() => {
188
193
  const newValue = { ...(value ?? {}) as T };
189
- if (originalValue.current && fieldKey in originalValue.current) {
194
+ if (initialValue && fieldKey in initialValue) {
190
195
  // @ts-ignore
191
196
  newValue[fieldKey] = undefined;
192
197
  } else {
@@ -305,7 +310,7 @@ function MapKeyValueRow<T extends Record<string, any>>({
305
310
  }}/>;
306
311
  } else if (dataType === "boolean") {
307
312
  return <BooleanSwitchWithLabel value={entryValue}
308
- size={"small"}
313
+ size={"medium"}
309
314
  position={"start"}
310
315
  disabled={disabled || !fieldKey}
311
316
  onValueChange={(newValue) => {
@@ -375,7 +380,7 @@ function MapKeyValueRow<T extends Record<string, any>>({
375
380
  <Typography key={rowId.toString()}
376
381
  component={"div"}
377
382
  className="font-mono flex flex-row gap-1">
378
- <div className="w-[200px] max-w-[25%]">
383
+ <div className="w-[300px] max-w-[30%]">
379
384
  <TextField
380
385
  value={fieldKey}
381
386
  placeholder={"key"}
@@ -389,32 +394,32 @@ function MapKeyValueRow<T extends Record<string, any>>({
389
394
  <div className="flex-grow">
390
395
  {(dataType !== "map" && dataType !== "array") && buildInput(entryValue, fieldKey, dataType)}
391
396
  </div>
392
- <Menu
393
- trigger={<IconButton size={"small"}
394
- className="h-7 w-7">
395
- <ArrowDropDownIcon/>
396
- </IconButton>}
397
- >
398
- <MenuItem dense
399
- onClick={() => doUpdateDataType("string")}>string</MenuItem>
400
- <MenuItem dense
401
- onClick={() => doUpdateDataType("number")}>number</MenuItem>
402
- <MenuItem dense
403
- onClick={() => doUpdateDataType("boolean")}>boolean</MenuItem>
404
- <MenuItem dense
405
- onClick={() => doUpdateDataType("date")}>date</MenuItem>
406
- <MenuItem dense
407
- onClick={() => doUpdateDataType("map")}>map</MenuItem>
408
- <MenuItem dense
409
- onClick={() => doUpdateDataType("array")}>array</MenuItem>
410
- </Menu>
411
-
412
- <IconButton aria-label="delete"
413
- size={"small"}
414
- onClick={onDeleteClick}
415
- className="h-7 w-7">
416
- <RemoveIcon size={"small"}/>
417
- </IconButton>
397
+ <div className={"flex flex-col"}>
398
+ <Menu
399
+ trigger={<IconButton size={"smallest"}>
400
+ <ArrowDropDownIcon size={"small"}/>
401
+ </IconButton>}
402
+ >
403
+ <MenuItem dense
404
+ onClick={() => doUpdateDataType("string")}>string</MenuItem>
405
+ <MenuItem dense
406
+ onClick={() => doUpdateDataType("number")}>number</MenuItem>
407
+ <MenuItem dense
408
+ onClick={() => doUpdateDataType("boolean")}>boolean</MenuItem>
409
+ <MenuItem dense
410
+ onClick={() => doUpdateDataType("date")}>date</MenuItem>
411
+ <MenuItem dense
412
+ onClick={() => doUpdateDataType("map")}>map</MenuItem>
413
+ <MenuItem dense
414
+ onClick={() => doUpdateDataType("array")}>array</MenuItem>
415
+ </Menu>
416
+
417
+ <IconButton aria-label="delete"
418
+ size={"smallest"}
419
+ onClick={onDeleteClick}>
420
+ <RemoveIcon size={"smallest"}/>
421
+ </IconButton>
422
+ </div>
418
423
  </Typography>
419
424
 
420
425
  {(dataType === "map" || dataType === "array") && buildInput(entryValue, fieldKey, dataType)}
@@ -472,7 +477,7 @@ function ArrayKeyValueRow<T>({
472
477
  }}/>;
473
478
  } else if (dataType === "boolean") {
474
479
  return <BooleanSwitchWithLabel value={entryValue}
475
- size={"small"}
480
+ size={"medium"}
476
481
  position={"start"}
477
482
  onValueChange={(v) => {
478
483
  setValue(v as T);
@@ -383,6 +383,7 @@ export function StorageUpload({
383
383
  >
384
384
  <StorageItemPreview
385
385
  name={`storage_preview_${entry.storagePathOrDownloadUrl}`}
386
+ placeholder={true}
386
387
  property={renderProperty}
387
388
  disabled={true}
388
389
  value={entry.storagePathOrDownloadUrl as string}
@@ -46,7 +46,7 @@ export const SwitchFieldBinding = React.forwardRef(function SwitchFieldBinding({
46
46
  title={property.name}/>}
47
47
  disabled={disabled}
48
48
  autoFocus={autoFocus}
49
- size={"medium"}
49
+ size={"large"}
50
50
  />
51
51
 
52
52
  <FieldHelperText includeDescription={includeDescription}
@@ -47,7 +47,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<T extends CMS
47
47
  size,
48
48
  height,
49
49
  width,
50
- // entity
50
+ interactive
51
51
  } = props;
52
52
 
53
53
  const property = resolveProperty({
@@ -84,12 +84,15 @@ export const PropertyPreview = React.memo(function PropertyPreview<T extends CMS
84
84
  content =
85
85
  <UrlComponentPreview size={props.size}
86
86
  url={value}
87
+ interactive={interactive}
87
88
  previewType={stringProperty.url}/>;
88
89
  } else if (stringProperty.storage) {
90
+ const filePath = stringProperty.storage.previewUrl ? stringProperty.storage.previewUrl(value) : value;
89
91
  content = <StorageThumbnail
92
+ interactive={interactive}
90
93
  storeUrl={property.storage?.storeUrl ?? false}
91
94
  size={props.size}
92
- storagePathOrDownloadUrl={value}/>;
95
+ storagePathOrDownloadUrl={filePath}/>;
93
96
  } else if (stringProperty.markdown) {
94
97
  content = <Markdown source={value} size={"small"}/>;
95
98
  } else {
@@ -46,4 +46,10 @@ export interface PropertyPreviewProps<T extends CMSType = any, CustomProps = any
46
46
  */
47
47
  customProps?: CustomProps;
48
48
 
49
+ /**
50
+ * If the preview should be interactive or not.
51
+ * This applies only to videos.
52
+ */
53
+ interactive?: boolean;
54
+
49
55
  }
@@ -1,4 +1,4 @@
1
- import React, { CSSProperties, useMemo, useState } from "react";
1
+ import React, { CSSProperties, useMemo } from "react";
2
2
 
3
3
  import { getThumbnailMeasure } from "../util";
4
4
  import { PreviewSize } from "../PropertyPreviewProps";
@@ -20,8 +20,6 @@ export function ImagePreview({
20
20
  url
21
21
  }: ImagePreviewProps) {
22
22
 
23
- const [onHover, setOnHover] = useState(false);
24
-
25
23
  const imageSize = useMemo(() => getThumbnailMeasure(size), [size]);
26
24
 
27
25
  if (size === "tiny") {
@@ -47,60 +45,50 @@ export function ImagePreview({
47
45
 
48
46
  return (
49
47
  <div
50
- className="relative flex items-center justify-center max-w-full max-h-full"
48
+ className="relative flex items-center justify-center max-w-full max-h-full group"
51
49
  style={{
52
50
  width: imageSize,
53
51
  height: imageSize
54
52
  }}
55
- key={"image_preview_" + url}
56
- onMouseEnter={() => setOnHover(true)}
57
- onMouseMove={() => setOnHover(true)}
58
- onMouseLeave={() => setOnHover(false)}>
53
+ key={"image_preview_" + url}>
59
54
 
60
55
  <img src={url}
61
56
  className={"rounded-md"}
62
57
  style={imageStyle}/>
63
58
 
64
- {onHover && <>
65
59
 
66
- {navigator && <Tooltip title="Copy url to clipboard">
67
- <div
68
- className="rounded-full absolute bottom-[-4px] right-8">
69
- <IconButton
70
- variant={"filled"}
71
- size={"small"}
72
- onClick={(e) => {
73
- e.stopPropagation();
74
- e.preventDefault();
75
- return navigator.clipboard.writeText(url);
76
- }}>
77
- <ContentCopyIcon className={"text-gray-500"}
78
- size={"small"}/>
79
- </IconButton>
80
- </div>
60
+ <div className={"flex flex-row gap-2 absolute bottom-[-4px] right-[-4px] invisible group-hover:visible"}>
61
+ {navigator && <Tooltip title="Copy url to clipboard" side={"bottom"}>
62
+ <IconButton
63
+ variant={"filled"}
64
+ size={"small"}
65
+ onClick={(e) => {
66
+ e.stopPropagation();
67
+ e.preventDefault();
68
+ return navigator.clipboard.writeText(url);
69
+ }}>
70
+ <ContentCopyIcon className={"text-gray-700 dark:text-gray-300"}
71
+ size={"small"}/>
72
+ </IconButton>
81
73
  </Tooltip>}
82
74
 
83
- <Tooltip title="Open image in new tab">
75
+ <Tooltip title="Open image in new tab" side={"bottom"}>
84
76
  <IconButton
77
+ className="invisible group-hover:visible"
85
78
  variant={"filled"}
86
79
  component={"a" as React.ElementType}
87
- style={{
88
- position: "absolute",
89
- bottom: -4,
90
- right: -4
91
- }}
92
80
  href={url}
93
81
  rel="noopener noreferrer"
94
82
  target="_blank"
95
83
  size={"small"}
96
84
  onClick={(e: any) => e.stopPropagation()}
97
85
  >
98
- <OpenInNewIcon className={"text-gray-500"}
86
+ <OpenInNewIcon className={"text-gray-700 dark:text-gray-300"}
99
87
  size={"small"}/>
100
88
  </IconButton>
101
89
  </Tooltip>
102
- </>
103
- }
90
+ </div>
91
+
104
92
  </div>
105
93
  );
106
94
  }