-
Notifications
You must be signed in to change notification settings - Fork 4
.438081291297676:be4ed675800a877ee7d371ea48991a9f_69ef6d0fdf20d71b25c740e2.69ef6d2cdf20d71b25c7411d.69ef6d2b0d279a6ad9dc4247:Trae CN.T(2026/4/27 22:05:40) #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <link rel="icon" href="/favicon.ico" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <meta name="theme-color" content="#000000" /> | ||
| <meta | ||
| name="description" | ||
| content="refine | Build your React-based CRUD applications, without constraints." | ||
| /> | ||
| <meta | ||
| data-rh="true" | ||
| property="og:image" | ||
| content="https://refine.dev/img/refine_social.png" | ||
| /> | ||
| <meta | ||
| data-rh="true" | ||
| name="twitter:image" | ||
| content="https://refine.dev/img/refine_social.png" | ||
| /> | ||
| <title> | ||
| refine - Build your React-based CRUD applications, without constraints. | ||
| </title> | ||
| <script type="module" crossorigin src="/assets/index-813fbcca.js"></script> | ||
| <link rel="stylesheet" href="/assets/index-5380a742.css"> | ||
| </head> | ||
| <body> | ||
| <noscript>You need to enable JavaScript to run this app.</noscript> | ||
| <div id="root"></div> | ||
|
|
||
| <!-- | ||
| This HTML file is a template. | ||
| If you open it directly in the browser, you will see an empty page. | ||
|
|
||
| You can add webfonts, meta tags, or analytics to this file. | ||
| The build step will place the bundled scripts into the <body> tag. | ||
|
|
||
| To begin the development, run `npm dev` or `yarn start`. | ||
| To create a production bundle, use `npm run build` or `yarn build`. | ||
| --> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,166 @@ | ||||||||
| import React, { useMemo } from 'react' | ||||||||
| import { Select, DatePicker, Button, Space, Card } from 'antd' | ||||||||
| import { FilterOutlined, ClearOutlined } from '@ant-design/icons' | ||||||||
| import { useSelect } from '@refinedev/antd' | ||||||||
| import { GetFieldsFromList } from '@refinedev/nestjs-query' | ||||||||
| import { UsersSelectQuery, TaskStagesSelectQuery } from '@/graphql/types' | ||||||||
| import { USERS_SELECT_QUERY, TASK_STAGES_SELECT_QUERY } from '@/graphql/queries' | ||||||||
| import SelectOptionWithAvatar from '@/components/select-option-with-avatar' | ||||||||
| import dayjs, { Dayjs } from 'dayjs' | ||||||||
| import { RangePickerProps } from 'antd/es/date-picker' | ||||||||
|
|
||||||||
| export interface TaskFilterState { | ||||||||
| userIds: string[] | ||||||||
| stageIds: string[] | ||||||||
| dueDateRange: [Dayjs | null, Dayjs | null] | null | ||||||||
| } | ||||||||
|
|
||||||||
| interface TaskFilterProps { | ||||||||
| filters: TaskFilterState | ||||||||
| onFiltersChange: (filters: TaskFilterState) => void | ||||||||
| } | ||||||||
|
|
||||||||
| const { RangePicker } = DatePicker | ||||||||
|
|
||||||||
| type UserOption = { | ||||||||
| value: string | ||||||||
| label: string | ||||||||
| avatarUrl?: string | null | ||||||||
| } | ||||||||
|
|
||||||||
| export const TaskFilter: React.FC<TaskFilterProps> = ({ filters, onFiltersChange }) => { | ||||||||
| const { selectProps: usersSelectProps } = useSelect<GetFieldsFromList<UsersSelectQuery>>({ | ||||||||
| resource: 'users', | ||||||||
| meta: { | ||||||||
| gqlQuery: USERS_SELECT_QUERY, | ||||||||
| }, | ||||||||
| optionLabel: 'name', | ||||||||
| }) | ||||||||
|
|
||||||||
| const { selectProps: stagesSelectProps } = useSelect<GetFieldsFromList<TaskStagesSelectQuery>>({ | ||||||||
| resource: 'taskStages', | ||||||||
| meta: { | ||||||||
| gqlQuery: TASK_STAGES_SELECT_QUERY, | ||||||||
| }, | ||||||||
| optionLabel: 'title', | ||||||||
| }) | ||||||||
|
|
||||||||
| const userOptions = useMemo(() => { | ||||||||
| if (!usersSelectProps?.options) { | ||||||||
| return [] | ||||||||
| } | ||||||||
| return usersSelectProps.options.map((user) => ({ | ||||||||
| value: user.value, | ||||||||
| label: ( | ||||||||
| <SelectOptionWithAvatar | ||||||||
| name={user.label as string} | ||||||||
| avatarUrl={(user as UserOption).avatarUrl ?? undefined} | ||||||||
| /> | ||||||||
|
Comment on lines
+52
to
+58
|
||||||||
| ), | ||||||||
| })) | ||||||||
| }, [usersSelectProps]) | ||||||||
|
|
||||||||
| const handleUserChange = (value: string[]) => { | ||||||||
| onFiltersChange({ | ||||||||
| ...filters, | ||||||||
| userIds: value, | ||||||||
| }) | ||||||||
| } | ||||||||
|
|
||||||||
| const handleStageChange = (value: string[]) => { | ||||||||
| onFiltersChange({ | ||||||||
| ...filters, | ||||||||
| stageIds: value, | ||||||||
| }) | ||||||||
| } | ||||||||
|
|
||||||||
| const handleDateRangeChange: RangePickerProps['onChange'] = (dates) => { | ||||||||
| onFiltersChange({ | ||||||||
| ...filters, | ||||||||
| dueDateRange: dates as [Dayjs | null, Dayjs | null] | null, | ||||||||
| }) | ||||||||
| } | ||||||||
|
|
||||||||
| const handleClearFilters = () => { | ||||||||
| onFiltersChange({ | ||||||||
| userIds: [], | ||||||||
| stageIds: [], | ||||||||
| dueDateRange: null, | ||||||||
| }) | ||||||||
| } | ||||||||
|
|
||||||||
| const hasActiveFilters = filters.userIds.length > 0 || | ||||||||
| filters.stageIds.length > 0 || | ||||||||
| filters.dueDateRange !== null | ||||||||
|
|
||||||||
| return ( | ||||||||
| <Card | ||||||||
| size="small" | ||||||||
| style={{ marginBottom: 16 }} | ||||||||
| title={ | ||||||||
| <Space> | ||||||||
| <FilterOutlined /> | ||||||||
| <span>筛选条件</span> | ||||||||
| </Space> | ||||||||
| } | ||||||||
| extra={ | ||||||||
| hasActiveFilters ? ( | ||||||||
| <Button | ||||||||
| type="text" | ||||||||
| icon={<ClearOutlined />} | ||||||||
| onClick={handleClearFilters} | ||||||||
| > | ||||||||
| 清空筛选 | ||||||||
| </Button> | ||||||||
| ) : null | ||||||||
| } | ||||||||
| > | ||||||||
| <Space wrap size="middle" style={{ width: '100%' }}> | ||||||||
| <div style={{ minWidth: 200 }}> | ||||||||
| <label style={{ display: 'block', marginBottom: 4, fontSize: 12, color: '#666' }}> | ||||||||
| 负责人 | ||||||||
| </label> | ||||||||
| <Select | ||||||||
| mode="multiple" | ||||||||
| placeholder="选择负责人" | ||||||||
| style={{ width: '100%' }} | ||||||||
| value={filters.userIds} | ||||||||
| onChange={handleUserChange} | ||||||||
|
||||||||
| onChange={handleUserChange} | |
| onChange={handleUserChange} | |
| {...usersSelectProps} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { KanbanColumnSkeleton, ProjectCardSkeleton } from '@/components' | ||
| import TaskFilter, { TaskFilterState } from '@/components/tasks/filter' | ||
| import { KanbanAddCardButton } from '@/components/tasks/kanban/add-card-button' | ||
| import { KanbanBoardContainer, KanbanBoard } from '@/components/tasks/kanban/board' | ||
| import { ProjectCardMemo } from '@/components/tasks/kanban/card' | ||
|
|
@@ -10,6 +11,7 @@ import { TaskStagesQuery, TasksQuery } from '@/graphql/types' | |
| import { DragEndEvent } from '@dnd-kit/core' | ||
| import { useList, useNavigation, useUpdate } from '@refinedev/core' | ||
| import { GetFieldsFromList } from '@refinedev/nestjs-query' | ||
| import dayjs from 'dayjs' | ||
| import React from 'react' | ||
|
|
||
| type Task = GetFieldsFromList<TasksQuery> | ||
|
|
@@ -18,6 +20,12 @@ type TaskStage = GetFieldsFromList<TaskStagesQuery> & { tasks: Task[] } | |
| const List = ({ children }: React.PropsWithChildren) => { | ||
| const { replace } = useNavigation() | ||
|
|
||
| const [filters, setFilters] = React.useState<TaskFilterState>({ | ||
| userIds: [], | ||
| stageIds: [], | ||
| dueDateRange: null, | ||
| }) | ||
|
|
||
| const { data: stages, isLoading: isLoadingStages } = useList<TaskStage>({ | ||
| resource: 'taskStages', | ||
| filters: [ | ||
|
|
@@ -58,26 +66,54 @@ const List = ({ children }: React.PropsWithChildren) => { | |
|
|
||
| const { mutate: updateTask } = useUpdate(); | ||
|
|
||
| const filteredTasks = React.useMemo(() => { | ||
| if (!tasks?.data) return [] | ||
|
|
||
| return tasks.data.filter((task) => { | ||
| if (filters.userIds.length > 0) { | ||
| const taskUserIds = task.users?.map((user) => user.id) || [] | ||
| const hasMatchingUser = filters.userIds.some((userId) => taskUserIds.includes(userId)) | ||
| if (!hasMatchingUser) return false | ||
| } | ||
|
|
||
| if (filters.stageIds.length > 0) { | ||
| const taskStageId = task.stageId?.toString() || 'unassigned' | ||
| if (!filters.stageIds.includes(taskStageId)) return false | ||
| } | ||
|
|
||
| if (filters.dueDateRange) { | ||
| const [startDate, endDate] = filters.dueDateRange | ||
| if (!task.dueDate) return false | ||
|
|
||
| const taskDueDate = dayjs(task.dueDate) | ||
| if (startDate && taskDueDate.isBefore(startDate, 'day')) return false | ||
| if (endDate && taskDueDate.isAfter(endDate, 'day')) return false | ||
| } | ||
|
|
||
| return true | ||
| }) | ||
| }, [tasks, filters]) | ||
|
|
||
| const taskStages = React.useMemo(() => { | ||
| if (!tasks?.data || !stages?.data) { | ||
| if (!filteredTasks.length || !stages?.data) { | ||
| return { | ||
| unassignedStage: [], | ||
| stages: [] | ||
| } | ||
| } | ||
|
Comment on lines
+98
to
103
|
||
|
|
||
| const unassignedStage = tasks.data.filter((task) => task.stageId === null) | ||
| const unassignedStage = filteredTasks.filter((task) => task.stageId === null) | ||
|
|
||
| const grouped: TaskStage[] = stages.data.map((stage) => ({ | ||
| ...stage, | ||
| tasks: tasks.data.filter((task) => task.stageId?.toString() === stage.id) | ||
| tasks: filteredTasks.filter((task) => task.stageId?.toString() === stage.id) | ||
| })) | ||
|
|
||
| return { | ||
| unassignedStage, | ||
| columns: grouped | ||
| } | ||
| }, [stages, tasks]) | ||
| }, [stages, filteredTasks]) | ||
|
|
||
| const handleAddCard = (args: { stageId: string}) => { | ||
| const path = args.stageId === 'unassigned' | ||
|
|
@@ -118,6 +154,10 @@ const List = ({ children }: React.PropsWithChildren) => { | |
|
|
||
| return ( | ||
| <> | ||
| <TaskFilter | ||
| filters={filters} | ||
| onFiltersChange={setFilters} | ||
| /> | ||
| <KanbanBoardContainer> | ||
| <KanbanBoard onDragEnd={handleOnDragEnd}> | ||
| <KanbanColumn | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dayjsis imported but never used in this component (onlyDayjsis used as a type). Remove the unused default import to avoid lint/TS warnings (or switch toimport type { Dayjs } from 'dayjs'if your tooling supports it).