Skip to content

Commit

Permalink
Merge pull request #29 from Bill2015/develop-attribute-modify
Browse files Browse the repository at this point in the history
Implement the attribute modify feature
  • Loading branch information
Bill2015 authored Mar 9, 2024
2 parents 1815dcb + e9b1159 commit db86e07
Show file tree
Hide file tree
Showing 13 changed files with 341 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ use serde::Deserialize;

use crate::command_from_dto;
use crate::modules::resource::application::dto::ResourceTaggingAttrPayloadDto;
use crate::modules::resource::domain::entities::TaggingAttrPayload;
use crate::modules::resource::domain::{ResourceGenericError, ResourceID};
use crate::modules::resource::repository::ResourceRepository;
use crate::modules::common::application::ICommandHandler;
use crate::modules::tag::domain::TagID;
use crate::modules::tag::repository::TagRepository;

mod dto;
Expand Down
2 changes: 0 additions & 2 deletions src-tauri/src/modules/resource/application/dto/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ pub enum ResourceTaggingAttrPayloadDto {
None,
Number(i64),
Text(String),
Date(String),
Bool(bool),
}
impl Into<TaggingAttrPayload> for ResourceTaggingAttrPayloadDto {
Expand All @@ -17,7 +16,6 @@ impl Into<TaggingAttrPayload> for ResourceTaggingAttrPayloadDto {
Self::None => TaggingAttrPayload::None,
Self::Number(val) => TaggingAttrPayload::Number(val),
Self::Text(val) => TaggingAttrPayload::Text(val),
Self::Date(val) => TaggingAttrPayload::Date(val),
Self::Bool(val) => TaggingAttrPayload::Bool(val),
}
}
Expand Down
6 changes: 2 additions & 4 deletions src-tauri/src/modules/resource/domain/entities/restag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ pub enum TaggingAttrPayload {

Text(String),

Date(String),

Bool(bool),
}

Expand Down Expand Up @@ -132,8 +130,8 @@ impl ResourceTaggingEntity {
Err(ResourceGenericError::InvalidTaggingAttribute())
}
TagAttrVO::Date { .. } => {
if let TaggingAttrPayload::Date(val) = payload {
if let Ok(date) = dateutils::parse(val) {
if let TaggingAttrPayload::Text(val) = payload {
if let Ok(date) = dateutils::parse(val.clone()) {
return Ok(ResourceTaggingAttrVO::Date(date.and_utc()));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}

.carouselViewPort, .carouselContainer {
Expand Down
18 changes: 13 additions & 5 deletions src/pages/resource-detail/ResourceDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { useActiveCategoryRedux } from '@store/global';
import { ModalName, useModelConfirmAction } from '@store/modal';
import { ResourceMutation, ResourceQuery, ResourceUpdateDto } from '@api/resource';
import { ErrorResBody } from '@api/common';
import { ResourceDetailParam } from '@router/params';
import { SubjectQuery } from '@api/subject';
import { EditableText } from '@components/display';
Expand Down Expand Up @@ -148,11 +149,18 @@ export default function ResourcesDetailPage() {
resourceRefetch();
}}
onUpdateTag={async (tag, attrVal) => {
await updateResourceTag.mutateAsync({
id: resourceData.id,
tag_id: tag.id,
attrval: attrVal,
});
try {
await updateResourceTag.mutateAsync({
id: resourceData.id,
tag_id: tag.id,
attrval: attrVal,
});
showNotification('Update Resource Successful', '', 'success');
}
catch (e) {
const error = e as ErrorResBody;
showNotification('Update Attribute Error', error.message, 'error');
}
resourceRefetch();
}}
/>
Expand Down
81 changes: 77 additions & 4 deletions src/pages/resource-detail/components/ResourceTagPill.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
/* eslint-disable react/jsx-props-no-spreading */
import { useCallback, useState } from 'react';
import millify from 'millify';
import { ResourceTagAttrValDto, ResourceTagDto } from '@api/resource';
import { EditableText } from '@components/display';
import { Group, Pill, Text } from '@mantine/core';
import millify from 'millify';
import { TagAttrPayload } from '@api/tag';
import { BoxProps, Group, Pill, Text } from '@mantine/core';
import { EditableBool, EditableDate, EditableNumber } from './attributes';

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

Expand All @@ -15,6 +19,75 @@ export interface ResourceTagPillProps {

export function ResourceTagPill(props: ResourceTagPillProps) {
const { tag, onRemoveTag, onUpdateTag } = props;
const [value, setValue] = useState<ResourceTagAttrValDto>(tag.attrval);

const renderAttributeField = useCallback(() => {
const baseProps: BoxProps & { name: string } = {
c: 'teal',
fz: '0.8rem',
name: tag.name,
};

switch (tag.tag_type) {
case 'number': {
const attr = tag.attr as TagAttrPayload.Number;
return (
<EditableNumber
{...baseProps}
value={value as number}
min={attr.start}
max={attr.end}
onChange={setValue}
onEditFinished={(val, isEdited) => {
if (isEdited) {
onUpdateTag({ id: tag.id, name: tag.name }, val);
}
}}
/>
);
}
case 'text': {
return (
<EditableText
{...baseProps}
value={value as string}
onChange={setValue}
onEditFinished={(val, isEdited) => {
if (isEdited) {
onUpdateTag({ id: tag.id, name: tag.name }, val);
}
}}
/>
);
}
case 'date':
return (
<EditableDate
{...baseProps}
value={value as string}
onChange={setValue}
onEditFinished={(val, isEdited) => {
if (isEdited) {
onUpdateTag({ id: tag.id, name: tag.name }, val);
}
}}
/>
);
case 'bool':
return (
<EditableBool
{...baseProps}
value={value as boolean}
onChange={(val) => {
setValue(val);
onUpdateTag({ id: tag.id, name: tag.name }, val);
}}
/>
);
default:
break;
}
}, [onUpdateTag, tag, value]);

return (
<Pill
Expand All @@ -26,9 +99,9 @@ export function ResourceTagPill(props: ResourceTagPillProps) {
<Group gap={3} align="baseline">
{tag.name}
{tag.attrval !== null && (
<Group gap={0} align="baseline">
<Group gap={0} align="baseline" style={{ cursor: 'pointer' }}>
<Text c="teal" fz="0.8rem">[</Text>
<EditableText c="teal" fz="0.8rem" value={tag.attrval?.toString()} name="attr" onChange={(val) => onUpdateTag({ id: tag.id, name: tag.name }, val)} />
{renderAttributeField()}
<Text c="teal" fz="0.8rem">]</Text>
</Group>
)}
Expand Down
15 changes: 13 additions & 2 deletions src/pages/resource-detail/components/ResourceTagStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ export interface ResourceTagGroupProps {
onRemoveTag: (tag: Pick<ResourceTagDto, 'id'|'name'>) => void;

onUpdateTag: (tag: Pick<ResourceTagDto, 'id'|'name'>, attrVal: ResourceTagAttrValDto) => void;

}

export function ResourceTagGroup(props: ResourceTagGroupProps) {
const { subjectName, autoFocus, subjectId, tags, onSelectNewTag, onRemoveTag, onUpdateTag } = props;
const {
subjectName, autoFocus, subjectId, tags,
onSelectNewTag, onRemoveTag, onUpdateTag,
} = props;
const selectRef = useRef<HTMLInputElement>(null);
const [searchValue, setSearchValue] = useState<string>('');
const [selectValue, setSelectValue] = useState<string>('');
Expand All @@ -48,7 +52,14 @@ export function ResourceTagGroup(props: ResourceTagGroupProps) {
}
};

const itemChip = tags.map((val) => <ResourceTagPill key={val.id} tag={val} onRemoveTag={onRemoveTag} onUpdateTag={onUpdateTag} />);
const itemChip = tags.map((val) => (
<ResourceTagPill
key={val.id + (val.attrval?.toString() ?? '')}
tag={val}
onRemoveTag={onRemoveTag}
onUpdateTag={onUpdateTag}
/>
));

const selectableTags = useMemo(() => subjectTags
.filter((tag) => !tags.find((obj) => obj.id === tag.id)), [tags, subjectTags]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.root {
align-self: center;
margin-top: 3px;
}

.input {
border: none;

&:not(:checked) {
border: 1px solid teal;
border-radius: 100%;
}
}
40 changes: 40 additions & 0 deletions src/pages/resource-detail/components/attributes/EditableBool.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable react/jsx-props-no-spreading */
import { Box, BoxProps, Checkbox } from '@mantine/core';
import { MdCircle } from 'react-icons/md';

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

export interface EditableBoolProps extends BoxProps {
value: boolean;

/** display name */
name: string;

/** when text change vent
* @param newValue new value of text */
onChange: (newValue: boolean) => void;
}

export function EditableBool(props: EditableBoolProps) {
const { value, name, onChange, ...boxProps } = props;

return (
<Checkbox
{...boxProps}
color="teal"
size="0.9rem"
name={name}
variant="outline"
// eslint-disable-next-line react/no-unstable-nested-components
icon={({ className }) => (
// eslint-disable-next-line object-curly-newline
<Box className={className} style={{ display: 'flex', alignItems: 'center', width: '13px' }}>
<MdCircle />
</Box>
)}
checked={value}
classNames={{ root: classes.root, input: classes.input }}
onChange={(e) => onChange(e.currentTarget.checked)}
/>
);
}
72 changes: 72 additions & 0 deletions src/pages/resource-detail/components/attributes/EditableDate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint-disable react/jsx-props-no-spreading */
import { useCallback, useRef, useState } from 'react';
import { BoxProps, Popover, Text } from '@mantine/core';

import { DatePicker } from '@mantine/dates';
import { formatDate, toDateTime } from '@utils/date';

export interface EditableDateProps extends BoxProps {
value: string;

/** display name */
name: string;

/** when text change vent
* @param newValue new value of text */
onChange: (newValue: string) => void;

onEdit?: () => void;

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

export function EditableDate(props: EditableDateProps) {
const { value, name, onChange, onEdit, onEditFinished, ...boxProps } = props;
const [inEdited, setInEdited] = useState<boolean>(false);
const edited = useRef<boolean>(false);

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

const handleBlur = useCallback((newVal: string) => {
setInEdited(false);
if (onEditFinished && inEdited) {
onEditFinished(newVal, edited.current);
}
edited.current = false;
}, [onEditFinished, inEdited]);

// display value
return (
<Popover
position="bottom"
withArrow
arrowSize={10}
shadow="md"
opened={inEdited}
onClose={() => handleBlur(value)}
>
<Popover.Target>
<Text title="double click to edit" onDoubleClick={handleClick} {...boxProps}>
{formatDate(value)}
</Text>
</Popover.Target>
<Popover.Dropdown p={10} pt={5} pb={5}>
<Text>{name}</Text>
<DatePicker
value={toDateTime(value)}
onChange={(e) => {
if (e) {
onChange(e.toISOString());
edited.current = true;
}
}}
/>
</Popover.Dropdown>
</Popover>
);
}
Loading

0 comments on commit db86e07

Please sign in to comment.