feat: beautify item card

This commit is contained in:
linonetwo 2023-07-15 01:07:54 +08:00 committed by lin onetwo
parent 491044747f
commit 2f1b6c3be3
6 changed files with 141 additions and 45 deletions

View file

@ -461,9 +461,13 @@
"AddNewWorkflowDescription": "Create a new automated workflow and save it to the selected workspace wiki to backup.", "AddNewWorkflowDescription": "Create a new automated workflow and save it to the selected workspace wiki to backup.",
"BelongsToWorkspace": "Belongs to workspace", "BelongsToWorkspace": "Belongs to workspace",
"AddNewWorkflowDoneMessage": "Add successfully", "AddNewWorkflowDoneMessage": "Add successfully",
"AddTagsDescription": "in-wiki tags, press Enter to add more" "AddTagsDescription": "in-wiki tags, press Enter to add more",
"DeleteWorkflow": "Delete Workflow",
"DeleteWorkflowDescription": "The workflow will be completely deleted from the Wiki workspace it belongs to, should it really be deleted?"
}, },
"Description": "Description", "Description": "Description",
"Tags": "Tags", "Tags": "Tags",
"Title": "Title" "Title": "Title",
"Delete": "Delete",
"Open": "Open"
} }

View file

@ -453,6 +453,7 @@
"Title": "标题", "Title": "标题",
"Description": "描述", "Description": "描述",
"Tags": "标签", "Tags": "标签",
"Open": "打开",
"LanguageModel": { "LanguageModel": {
"ModelNotExist": "找不到模型", "ModelNotExist": "找不到模型",
"ModelNotExistDescription": "尝试使用你给的这个路径加载模型,但在这个位置其实没有所需要的模型", "ModelNotExistDescription": "尝试使用你给的这个路径加载模型,但在这个位置其实没有所需要的模型",
@ -464,6 +465,8 @@
"AddNewWorkflow": "添加新的工作流", "AddNewWorkflow": "添加新的工作流",
"AddNewWorkflowDescription": "创建新的自动化工作流并保存到所选的工作区Wiki里备份。", "AddNewWorkflowDescription": "创建新的自动化工作流并保存到所选的工作区Wiki里备份。",
"AddNewWorkflowDoneMessage": "添加成功", "AddNewWorkflowDoneMessage": "添加成功",
"DeleteWorkflow": "删除工作流",
"DeleteWorkflowDescription": "将从所属的Wiki工作区里彻底删除该工作流是否真的要删除",
"BelongsToWorkspace": "所属工作区", "BelongsToWorkspace": "所属工作区",
"AddTagsDescription": "Wiki内的标签回车可以添加更多" "AddTagsDescription": "Wiki内的标签回车可以添加更多"
} }

View file

@ -0,0 +1,28 @@
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
interface DeleteConfirmationDialogProps {
onCancel: () => void;
onConfirm: () => void;
open: boolean;
}
export const DeleteConfirmationDialog = ({
open,
onCancel,
onConfirm,
}: DeleteConfirmationDialogProps) => {
const { t } = useTranslation();
return (
<Dialog open={open} onClose={onCancel}>
<DialogTitle>{t('Workflow.DeleteWorkflow')}</DialogTitle>
<DialogContent>
<Typography>{t('Workflow.DeleteWorkflowDescription')}</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{t('No')}</Button>
<Button onClick={onConfirm}>{t('Yes')} {t('Delete')}</Button>
</DialogActions>
</Dialog>
);
};

View file

@ -1,24 +1,34 @@
/* eslint-disable unicorn/no-null */ /* eslint-disable unicorn/no-null, @typescript-eslint/strict-boolean-expressions, unicorn/no-useless-undefined */
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import MenuOpenIcon from '@mui/icons-material/MenuOpen'; import MenuOpenIcon from '@mui/icons-material/MenuOpen';
import { Box, Button, Card, CardActionArea, CardContent, Fade, Menu, MenuItem, Typography } from '@mui/material'; import { Box, Button, Card, CardActionArea, CardActions, CardContent, CardMedia, Chip, Fade, Menu, MenuItem, Stack, Typography } from '@mui/material';
import type { IWorkspaceWithMetadata } from '@services/workspaces/interface'; import type { IWorkspaceWithMetadata } from '@services/workspaces/interface';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import styled from 'styled-components'; import styled from 'styled-components';
import is from 'typescript-styled-is'; import { DeleteConfirmationDialog } from './DeleteConfirmationDialog';
import type { IWorkflowTiddler } from './useWorkflowDataSource'; import type { IWorkflowTiddler } from './useWorkflowDataSource';
const WorkflowCard = styled(Card)<{ $backgroundImage?: string }>` const WorkflowListContainer = styled(Box)`
${is('$backgroundImage')` display: flex;
background-image: url(${(props: { $backgroundImage: string }) => props.$backgroundImage}); flex-direction: row;
`} flex-wrap: wrap;
background-size: cover; justify-content: flex-start;
background-blend-mode: darken; align-items: flex-start;
`; `;
const WorkflowCard = styled(Card)`
const WorkflowTitle = styled(Typography)` display: flex;
color: white; flex-direction: column;
position: relative;
width: 300px;
justify-content: space-between;
margin-right: 1em;
margin-bottom: 1em;
`;
const ItemMenuCardActions = styled(CardActions)`
display: flex;
flex-direction: row;
justify-content: space-between;
`; `;
interface IWorkflowListItemProps { interface IWorkflowListItemProps {
@ -43,15 +53,31 @@ export function WorkflowListItem(props: IWorkflowListItemProps) {
}, [item, onDeleteWorkflow]); }, [item, onDeleteWorkflow]);
const menuID = `workflow-list-item-menu-${item.id}`; const menuID = `workflow-list-item-menu-${item.id}`;
return ( return (
<WorkflowCard $backgroundImage={item.image}> <WorkflowCard>
<CardActionArea> <CardActionArea>
{item.image && (
<CardMedia
component='img'
height='140'
image={item.image}
alt={`screenshot of workflow ${item.title}`}
/>
)}
<CardContent> <CardContent>
<WorkflowTitle variant='h5'>{item.title}</WorkflowTitle> <Typography gutterBottom variant='h5' component='div'>{item.title}</Typography>
{item.tags && (
<Stack direction='row' spacing={1} flexWrap='wrap'>
{item.tags.map(tag => <Chip key={tag} label={tag} style={{ marginBottom: '0.3em' }} clickable />)}
</Stack>
)}
</CardContent> </CardContent>
</CardActionArea> </CardActionArea>
<ItemMenuCardActions>
<Button>{t('Open')}</Button>
<Button aria-controls={menuID} aria-haspopup='true' onClick={handleOpenItemMenu}> <Button aria-controls={menuID} aria-haspopup='true' onClick={handleOpenItemMenu}>
{anchorElement === null ? <MenuIcon /> : <MenuOpenIcon />} {anchorElement === null ? <MenuIcon /> : <MenuOpenIcon />}
</Button> </Button>
</ItemMenuCardActions>
<Menu <Menu
id={menuID} id={menuID}
anchorEl={anchorElement} anchorEl={anchorElement}
@ -85,9 +111,30 @@ interface IWorkflowListProps {
} }
export const WorkflowList: React.FC<IWorkflowListProps> = ({ workflows, onDeleteWorkflow }) => { export const WorkflowList: React.FC<IWorkflowListProps> = ({ workflows, onDeleteWorkflow }) => {
const [itemToDelete, setDeleteItem] = useState<IWorkflowListItem | undefined>();
const handleDeleteConfirmed = useCallback(() => {
if (itemToDelete) {
onDeleteWorkflow(itemToDelete);
setDeleteItem(undefined);
}
}, [itemToDelete, onDeleteWorkflow]);
const handleDeleteWithConfirmation = useCallback((item: IWorkflowListItem) => {
setDeleteItem(item);
}, []);
const handleDeleteCancel = useCallback(() => {
setDeleteItem(undefined);
}, []);
return ( return (
<Box> <>
{workflows.map((workflow) => <WorkflowListItem key={workflow.id} item={workflow} onDeleteWorkflow={onDeleteWorkflow} />)} <WorkflowListContainer>
</Box> {workflows.map((workflow) => <WorkflowListItem key={workflow.id} item={workflow} onDeleteWorkflow={handleDeleteWithConfirmation} />)}
</WorkflowListContainer>
<DeleteConfirmationDialog
open={itemToDelete !== undefined}
onCancel={handleDeleteCancel}
onConfirm={handleDeleteConfirmed}
/>
</>
); );
}; };

View file

@ -8,6 +8,16 @@ import { AddItemDialog } from './AddItemDialog';
import { useAvailableFilterTags, useWorkflows } from './useWorkflowDataSource'; import { useAvailableFilterTags, useWorkflows } from './useWorkflowDataSource';
import { IWorkflowListItem, WorkflowList } from './WorkflowList'; import { IWorkflowListItem, WorkflowList } from './WorkflowList';
const WorkflowManageContainer = styled(Box)`
display: flex;
flex-direction: column;
margin: 1em;
`;
const SearchRegionContainer = styled(Box)`
display: flex;
flex-direction: column;
margin-bottom: 1em;
`;
const AddNewItemFloatingButton = styled(Fab)` const AddNewItemFloatingButton = styled(Fab)`
position: absolute; position: absolute;
bottom: 1em; bottom: 1em;
@ -44,7 +54,8 @@ export const WorkflowManage: React.FC = () => {
.filter(workflow => selectedTags.length > 0 ? selectedTags.some(tag => workflow.tags.includes(tag)) : workflow); .filter(workflow => selectedTags.length > 0 ? selectedTags.some(tag => workflow.tags.includes(tag)) : workflow);
return ( return (
<Box> <WorkflowManageContainer>
<SearchRegionContainer>
<TextField <TextField
label='Search' label='Search'
value={search} value={search}
@ -65,6 +76,7 @@ export const WorkflowManage: React.FC = () => {
/> />
))} ))}
</Stack> </Stack>
</SearchRegionContainer>
<WorkflowList workflows={filteredWorkflows} onDeleteWorkflow={onDeleteWorkflow} /> <WorkflowList workflows={filteredWorkflows} onDeleteWorkflow={onDeleteWorkflow} />
<AddNewItemFloatingButton color='primary' aria-label='add' onClick={handleOpenDialog}> <AddNewItemFloatingButton color='primary' aria-label='add' onClick={handleOpenDialog}>
<AddIcon /> <AddIcon />
@ -76,6 +88,6 @@ export const WorkflowManage: React.FC = () => {
availableFilterTags={availableFilterTags} availableFilterTags={availableFilterTags}
workspacesList={workspacesList} workspacesList={workspacesList}
/> />
</Box> </WorkflowManageContainer>
); );
}; };

View file

@ -80,11 +80,13 @@ export function useWorkflowFromWiki(workspacesList: IWorkspaceWithMetadata[] | u
return workspacesList?.map?.((workspace, workspaceIndex) => { return workspacesList?.map?.((workspace, workspaceIndex) => {
const workflowTiddlersInWorkspace = workflowsByWorkspace[workspaceIndex]; const workflowTiddlersInWorkspace = workflowsByWorkspace[workspaceIndex];
return workflowTiddlersInWorkspace.map((tiddler) => { return workflowTiddlersInWorkspace.map((tiddler) => {
// DEBUG: console tiddler
console.log(`tiddler`, tiddler);
const workflowItem: IWorkflowListItem = { const workflowItem: IWorkflowListItem = {
id: `${workspace.id}:${tiddler.title}`, id: `${workspace.id}:${tiddler.title}`,
title: tiddler.title, title: tiddler.title,
description: tiddler.description, description: tiddler.description,
tags: tiddler.tags, tags: typeof tiddler.tags === 'string' ? (tiddler.tags as string).split(' ') : tiddler.tags,
workspaceID: workspace.id, workspaceID: workspace.id,
image: tiddler['page-cover'], image: tiddler['page-cover'],
metadata: { metadata: {