Skip to content

Commit

Permalink
Merge pull request #23 from Bill2015/develop-resource-list-ui-change
Browse files Browse the repository at this point in the history
Change the Resources display view UI
  • Loading branch information
Bill2015 authored Feb 24, 2024
2 parents eeeed6c + d71a409 commit ad61ede
Show file tree
Hide file tree
Showing 14 changed files with 305 additions and 151 deletions.
80 changes: 57 additions & 23 deletions src/components/display/EditableText.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Badge, Box, BoxProps, Text } from '@mantine/core';
import { Badge, Box, BoxProps, ElementProps, Text } from '@mantine/core';

import classes from './EditableText.module.scss';

export interface ContentEditableProps extends BoxProps, Omit<ElementProps<'div', keyof BoxProps>, 'onChange'> {
value: string;

onChange: (value: string) => void;
}

export function ContentEditable(props: ContentEditableProps) {
const { value, onChange, ...boxProps } = props;
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
if (ref && ref.current) {
ref.current.textContent = value;
}
}, [value]);

return (
<Box
// eslint-disable-next-line react/jsx-props-no-spreading
{...boxProps}
ref={ref}
onInput={(e) => onChange(e.currentTarget.textContent ?? '')}
contentEditable
suppressContentEditableWarning
/>
);
}

export interface EditableTextProps extends BoxProps {
value: string;

Expand All @@ -13,6 +41,10 @@ export interface EditableTextProps extends BoxProps {
/** when text change vent
* @param newValue new value of text */
onChange: (newValue: string) => void;

onEdit?: () => void;

onEditFinished?: (newValue: string, isEdited: boolean) => void;
}

/**
Expand All @@ -24,36 +56,44 @@ export interface EditableTextProps extends BoxProps {
* Because input can't warp the text when too long \
* And textarea can input newline, which is unnecessary in some fields */
export function EditableText(props: EditableTextProps) {
const { value, name, onChange, ...boxProps } = props;
const { value, name, onChange, onEdit, onEditFinished, ...boxProps } = props;
const { t } = useTranslation('common', { keyPrefix: 'Display.EditableText' });
const [inEdited, setInEdited] = useState<boolean>(false);
const [newValue, setNewValue] = useState<string>(value);
const edited = useRef<boolean>(false);

const handleClick = () => {
setInEdited(true);
setNewValue(value);
if (onEdit) {
onEdit();
}
};

const handleBlur = useCallback((newVal: string) => {
setNewValue(newVal);
setInEdited(false);
if (newVal !== value) {
onChange(newVal);
if (onEditFinished) {
onEditFinished(newVal, edited.current);
}
}, [onChange, value]);
edited.current = false;
}, [onEditFinished]);

if (inEdited) {
return (
<Box pos="relative">
<Box
contentEditable
<ContentEditable
value={value}
className={classes.input}
suppressContentEditableWarning
onMouseEnter={(e) => e.currentTarget.focus()}
onChange={(e) => {
onChange(e);
edited.current = true;
}}
onFocus={(e) => {
const textNode = e.currentTarget.firstChild!;
// prevent blur not being triggered
setTimeout(() => {
if (!textNode || !textNode.textContent) {
return;
}
const range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, textNode.textContent!.length);
Expand All @@ -64,8 +104,8 @@ export function EditableText(props: EditableTextProps) {
}, 1);
}}
onBlur={(e) => {
const child = e.currentTarget.lastChild;
handleBlur(child ? child.textContent! : '');
const text = e.currentTarget.textContent;
handleBlur(text ?? '');
}}
onKeyDown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter') {
Expand All @@ -74,15 +114,9 @@ export function EditableText(props: EditableTextProps) {
handleBlur(child ? child.textContent! : '');
}
}}
onPaste={(e) => {
e.preventDefault();
setNewValue(e.clipboardData.getData('text/plain'));
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...boxProps}
>
{newValue}
</Box>
/>
<Badge color="indigo" pos="absolute" right={0} variant="outline" style={{ zIndex: 99 }}>
{t('modifying')}
</Badge>
Expand All @@ -91,7 +125,7 @@ export function EditableText(props: EditableTextProps) {
}

// value is empty
if ((!value && !newValue) || ((value && !newValue))) {
if (!value) {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<Text className={classes.text} title={t('double_click_to_edit')} onDoubleClick={handleClick} {...boxProps}>
Expand All @@ -104,7 +138,7 @@ export function EditableText(props: EditableTextProps) {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<Text className={classes.text} title="double click to edit" onDoubleClick={handleClick} {...boxProps}>
{(value === newValue) ? value : newValue}
{value}
</Text>
);
}
60 changes: 0 additions & 60 deletions src/components/display/LinkIcon.tsx

This file was deleted.

38 changes: 38 additions & 0 deletions src/components/display/icons/ActionFileIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-disable react/jsx-props-no-spreading */
import { useCallback } from 'react';
import { FcOpenedFolder } from 'react-icons/fc';

import { ResourceMutation } from '@api/resource';

import { TooltipActionIcon, TooltipActionIconProps } from './TooltipActionIcon';

export interface ActionFileIconProps extends Omit<TooltipActionIconProps, 'label'> {
filePath: string | null;
}

/**
* According URL host to determin which icon will be showing */
export function ActionFileIcon(props: ActionFileIconProps) {
const {
filePath,
...actionIconProps
} = props;

const exporeFile = ResourceMutation.useExporeFile();

const handleExporeClick = useCallback(() => {
if (filePath) {
exporeFile.mutateAsync(filePath);
}
}, [exporeFile, filePath]);

return (
<TooltipActionIcon
label={`↖️ ${filePath}`}
onClick={handleExporeClick}
{...actionIconProps}
>
<FcOpenedFolder />
</TooltipActionIcon>
);
}
File renamed without changes.
57 changes: 57 additions & 0 deletions src/components/display/icons/ActionLinkIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* eslint-disable react/jsx-props-no-spreading */
import { open } from '@tauri-apps/api/shell';

import { IconType } from 'react-icons';
import { FaYoutube } from 'react-icons/fa';
import { FaLink } from 'react-icons/fa6';

import { showNotification } from '@components/notification';
import { UrlHost } from '@declares/variables';

import classes from './ActionLinkIcon.module.scss';
import { TooltipActionIcon, TooltipActionIconProps } from './TooltipActionIcon';

const URL_ICON_MAPPER = new Map<string, IconType>();
URL_ICON_MAPPER.set(UrlHost.Youtube, FaYoutube);

export interface ActionLinkIconProps extends Omit<TooltipActionIconProps, 'label'> {
/** URLs */
url: {
full: string;

host: string;
};
}

/**
* According URL host to determin which icon will be showing */
export function ActionLinkIcon(props: ActionLinkIconProps) {
const {
url,
...actionIconProps
} = props;

const IconElement = (() => {
if (URL_ICON_MAPPER.has(url.host)) {
return URL_ICON_MAPPER.get(url.host)!;
}
// defualt icon
return FaLink;
})();

return (
<TooltipActionIcon
label={`↖️ ${url.full}`}
classNames={{ root: classes.ActionIconRoot }}
onClick={() => {
open(url.full)
.catch(() => {
showNotification('Invalid URL', url.full, 'error');
});
}}
{...actionIconProps}
>
<IconElement />
</TooltipActionIcon>
);
}
4 changes: 4 additions & 0 deletions src/components/display/icons/TooltipActionIcon.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.tooltip {
background-color: var(--mantine-color-default-border);
color: var(--mantine-color-text)
}
45 changes: 45 additions & 0 deletions src/components/display/icons/TooltipActionIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-disable react/jsx-props-no-spreading */
import { PropsWithChildren } from 'react';

import { ActionIcon, ActionIconProps, ElementProps, Tooltip, TooltipProps } from '@mantine/core';

import classes from './TooltipActionIcon.module.scss';

export interface TooltipActionIconProps extends ActionIconProps, ElementProps<'button', keyof ActionIconProps>, PropsWithChildren {
tooltipProps?: TooltipProps & ElementProps<'div', keyof TooltipProps>;

label: string;

onTooltipChange?: (opened: boolean) => void;
}

/**
* According URL host to determin which icon will be showing */
export function TooltipActionIcon(props: TooltipActionIconProps) {
const {
label,
children,
tooltipProps,
onTooltipChange,
...actionIconProps
} = props;

return (
<Tooltip
withArrow
label={label}
classNames={{ tooltip: classes.tooltip }}
offset={10}
{...tooltipProps}
>
<ActionIcon
classNames={{ root: classes.ActionIconRoot }}
{...actionIconProps}
onMouseEnter={() => onTooltipChange && onTooltipChange(true)}
onMouseLeave={() => onTooltipChange && onTooltipChange(false)}
>
{children}
</ActionIcon>
</Tooltip>
);
}
3 changes: 3 additions & 0 deletions src/components/display/icons/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './TooltipActionIcon';
export * from './ActionLinkIcon';
export * from './ActionFileIcon';
2 changes: 1 addition & 1 deletion src/components/display/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './icons';
export * from './ResponsiveImage';
export * from './YoutubeThunbnail';
export * from './LinkIcon';
export * from './EditableText';
export * from './ResourceThumbnailDisplayer';
export * from './DateTimeDisplayer';
Expand Down
11 changes: 9 additions & 2 deletions src/modals/tag/createTagModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Button, Group, Input, SegmentedControl, Stack, Title } from '@mantine/core';
import { TagAttrPayload, TagCreateDto, TagMutation } from '@api/tag';
import { useActiveCategoryRedux } from '@store/global';
Expand Down Expand Up @@ -27,7 +27,14 @@ export function CreateTagModal() {
const { tv } = useValueTranslation('AttributeType');
const { activeCategory } = useActiveCategoryRedux();
const [opened, { close, confirmClose, cancelClose }] = useCreateTagModal();
const { data: subjectData } = SubjectQuery.useGetByCategory(activeCategory && activeCategory.id);
const { data: subjectData, refetch: refetchSubject } = SubjectQuery.useGetByCategory(activeCategory && activeCategory.id);

// refetch the subject
useEffect(() => {
if (opened === true) {
refetchSubject();
}
}, [opened, refetchSubject]);

const [data, setData] = useState<TagCreateDto>(DEFAULT_VALUE);
const [belongSubject, setBelongSubject] = useState<{ value: string, id: string } | null>(null);
Expand Down
Loading

0 comments on commit ad61ede

Please sign in to comment.