@@ -6,13 +6,14 @@
import { useMemo , useRef , useState } from "react" ;
import { observer } from "mobx-react" ;
import { ArrowDownUp , Check , Filter } from "lucide-react" ;
import useSWR from "swr" ;
import { useTranslation } from "@plane/i18n" ;
// plane types
import { PageIcon , ProjectIcon , WorkItemsIcon } from "@plane/propel/icons" ;
import { Avatar } from "@plane/propel/avatar" ;
import type { IIssueActivity , TActivityEntityData , THomeWidgetProps , TRecentActivityFilterKeys } from "@plane/types" ;
import { calculateTimeAgo , generateWorkItemLink , getFileURL , renderFormattedDate } from "@plane/utils" ;
import { calculateTimeAgo , cn , generateWorkItemLink , getFileURL , renderFormattedDate } from "@plane/utils" ;
import { ActivityIcon } from "@/components/core/activity" ;
// plane web services
import { WorkspaceService } from "@/services/workspace.service" ;
@@ -34,6 +35,35 @@ const filters: { name: TRecentActivityFilterKeys; icon?: React.ReactNode; i18n_k
{ name : "project" , icon : < ProjectIcon height = { 16 } width = { 16 } / > , i18n_key : "home.recents.filters.projects" } ,
] ;
type TActivityTraceFilterKey =
| "archive"
| "assignees"
| "content"
| "created"
| "dates"
| "labels"
| "mine"
| "priority"
| "state" ;
type TActivityTraceSortMode = "newest" | "oldest" ;
const ACTIVITY_TRACE_FILTER_OPTIONS : { key : TActivityTraceFilterKey ; label : string } [ ] = [
{ key : "created" , label : "Новые задачи" } ,
{ key : "assignees" , label : "Исполнители" } ,
{ key : "state" , label : "Статусы" } ,
{ key : "priority" , label : "Приоритеты" } ,
{ key : "dates" , label : "Сроки и даты" } ,
{ key : "content" , label : "Название и описание" } ,
{ key : "labels" , label : "Метки" } ,
{ key : "archive" , label : "Архив и восстановление" } ,
{ key : "mine" , label : "Только мои действия" } ,
] ;
const ACTIVITY_TRACE_SORT_OPTIONS : { key : TActivityTraceSortMode ; label : string } [ ] = [
{ key : "newest" , label : "Новые сверху" } ,
{ key : "oldest" , label : "Старые сверху" } ,
] ;
const PRIORITY_LABELS : Record < string , string > = {
urgent : "Срочный" ,
high : "Высокий" ,
@@ -83,6 +113,39 @@ const activityMessage = (activity: IIssueActivity) => {
}
} ;
const getActivityTraceFilterKeys = ( activity : IIssueActivity ) : TActivityTraceFilterKey [ ] = > {
if ( ! activity . field && activity . verb === "created" ) return [ "created" ] ;
switch ( activity . field ) {
case "assignees" :
return [ "assignees" ] ;
case "state" :
return [ "state" ] ;
case "priority" :
return [ "priority" ] ;
case "start_date" :
case "target_date" :
return [ "dates" ] ;
case "name" :
case "description" :
return [ "content" ] ;
case "labels" :
return [ "labels" ] ;
case "archived_at" :
return [ "archive" ] ;
default :
return [ ] ;
}
} ;
const sortActivityTrace = ( activities : IIssueActivity [ ] , sortMode : TActivityTraceSortMode ) = >
[ . . . activities ] . sort ( ( firstActivity , secondActivity ) = > {
const firstTime = new Date ( firstActivity . created_at ) . getTime ( ) ;
const secondTime = new Date ( secondActivity . created_at ) . getTime ( ) ;
return sortMode === "oldest" ? firstTime - secondTime : secondTime - firstTime ;
} ) ;
type TRecentWidgetProps = THomeWidgetProps & {
presetFilter? : TRecentActivityFilterKeys ;
showFilterSelect? : boolean ;
@@ -158,6 +221,9 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
const { presetFilter , showFilterSelect = true , workspaceSlug , projectId , recents : preloadedRecents } = props ;
// states
const [ filter , setFilter ] = useState < TRecentActivityFilterKeys > ( presetFilter ? ? filters [ 0 ] . name ) ;
const [ activeTraceFilters , setActiveTraceFilters ] = useState < TActivityTraceFilterKey [ ] > ( [ ] ) ;
const [ isTraceFilterOpen , setIsTraceFilterOpen ] = useState ( false ) ;
const [ traceSortMode , setTraceSortMode ] = useState < TActivityTraceSortMode > ( "newest" ) ;
const { t } = useTranslation ( ) ;
const { data : currentUser } = useUser ( ) ;
// ref
@@ -171,7 +237,7 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
workspaceSlug
? ( ) = >
workspaceService . fetchWorkspaceActivity ( workspaceSlug . toString ( ) , {
per_page : 20 ,
per_page : 60 ,
. . . ( projectId ? { project : projectId } : { } ) ,
} )
: null ,
@@ -200,7 +266,20 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
}
) ;
const activityTrace = useMemo ( ( ) = > fetchedActivity ? . results ? ? [ ] , [ fetchedActivity ? . results ] ) ;
const activityTrace = useMemo ( ( ) = > {
const source = fetchedActivity ? . results ? ? [ ] ;
const onlyMine = activeTraceFilters . includes ( "mine" ) ;
const fieldFilters = activeTraceFilters . filter ( ( filterKey ) = > filterKey !== "mine" ) ;
const filteredTrace = source . filter ( ( activity ) = > {
if ( onlyMine && currentUser ? . id !== activity . actor_detail ? . id ) return false ;
if ( fieldFilters . length === 0 ) return true ;
const activityFilterKeys = getActivityTraceFilterKeys ( activity ) ;
return fieldFilters . some ( ( filterKey ) = > activityFilterKeys . includes ( filterKey ) ) ;
} ) ;
return sortActivityTrace ( filteredTrace , traceSortMode ) ;
} , [ activeTraceFilters , currentUser ? . id , fetchedActivity ? . results , traceSortMode ] ) ;
const recents = useMemo ( ( ) = > {
const source = preloadedRecents ? ? fetchedRecents ? ? [ ] ;
@@ -217,6 +296,106 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
const isWidgetLoading = shouldUseActivityTrace ? isActivityLoading : isLoading ;
const isWidgetEmpty = shouldUseActivityTrace ? activityTrace . length === 0 : recents.length === 0 ;
const activeTraceFilterCount = activeTraceFilters . length + ( traceSortMode === "newest" ? 0 : 1 ) ;
const toggleTraceFilter = ( filterKey : TActivityTraceFilterKey ) = >
setActiveTraceFilters ( ( currentFilters ) = >
currentFilters . includes ( filterKey )
? currentFilters . filter ( ( currentFilter ) = > currentFilter !== filterKey )
: [ . . . currentFilters , filterKey ]
) ;
const resetTraceFilters = ( ) = > {
setActiveTraceFilters ( [ ] ) ;
setTraceSortMode ( "newest" ) ;
} ;
const headerActions = (
< div className = "flex items-center gap-2" >
{ showFilterSelect && < FiltersDropdown filters = { filters } activeFilter = { filter } setActiveFilter = { setFilter } / > }
{ shouldUseActivityTrace && (
< div className = "nodedc-home-gantt-action-group" >
< button
type = "button"
className = { cn ( "nodedc-home-gantt-round-button" , {
"nodedc-home-gantt-round-button-active" : activeTraceFilterCount > 0 || isTraceFilterOpen ,
"nodedc-home-gantt-filter-button-has-count" : activeTraceFilterCount > 0 ,
} ) }
aria-expanded = { isTraceFilterOpen }
aria-label = "Фильтры недавних действий"
aria-pressed = { activeTraceFilterCount > 0 }
onClick = { ( ) = > setIsTraceFilterOpen ( ( isOpen ) = > ! isOpen ) }
>
< Filter className = "size-4" / >
{ activeTraceFilterCount > 0 && (
< span className = "nodedc-home-gantt-filter-count" > { activeTraceFilterCount } < / span >
) }
< / button >
{ isTraceFilterOpen && (
< div className = "nodedc-home-gantt-popover nodedc-home-gantt-popover-wide" >
< div className = "nodedc-home-gantt-popover-section" >
< div className = "nodedc-home-gantt-popover-title" > Т и п д е й с т в и я < / div >
{ ACTIVITY_TRACE_FILTER_OPTIONS . map ( ( option ) = > {
const isActive = activeTraceFilters . includes ( option . key ) ;
return (
< button
key = { option . key }
type = "button"
className = { cn ( "nodedc-home-gantt-popover-option" , {
"nodedc-home-gantt-popover-option-active" : isActive ,
} ) }
onClick = { ( ) = > toggleTraceFilter ( option . key ) }
>
< span className = "nodedc-home-gantt-popover-option-left" >
< span className = "nodedc-home-gantt-popover-check" >
{ isActive && < Check className = "size-3" / > }
< / span >
< span > { option . label } < / span >
< / span >
< / button >
) ;
} ) }
< / div >
< div className = "nodedc-home-gantt-popover-section" >
< div className = "nodedc-home-gantt-popover-title" > С о р т и р о в к а < / div >
{ ACTIVITY_TRACE_SORT_OPTIONS . map ( ( option ) = > {
const isActive = traceSortMode === option . key ;
return (
< button
key = { option . key }
type = "button"
className = { cn ( "nodedc-home-gantt-popover-option" , {
"nodedc-home-gantt-popover-option-active" : isActive ,
} ) }
onClick = { ( ) = > setTraceSortMode ( option . key ) }
>
< span className = "nodedc-home-gantt-popover-option-left" >
< span className = "nodedc-home-gantt-popover-check" >
{ isActive && < Check className = "size-3" / > }
< / span >
< span > { option . label } < / span >
< / span >
< ArrowDownUp className = "size-3 text-tertiary" / >
< / button >
) ;
} ) }
< / div >
{ activeTraceFilterCount > 0 && (
< button type = "button" className = "nodedc-home-gantt-popover-reset" onClick = { resetTraceFilters } >
С б р о с и т ь ф и л ь т р ы
< / button >
) }
< / div >
) }
< / div >
) }
< / div >
) ;
const resolveRecent = ( activity : TActivityEntityData ) = > {
switch ( activity . entity_name ) {
@@ -237,7 +416,7 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
< div ref = { ref } className = "max-h-[500px] overflow-y-scroll" >
< div className = "mb-4 flex items-center justify-between" >
< div className = "text-14 font-semibold text-tertiary" > { t ( "home.recents.title" ) } < / div >
{ showFilterSelect && < FiltersDropdown filters = { filters } activeFilter = { filter } setActiveFilter = { setFilter } / > }
{ headerActions }
< / div >
< div className = "flex flex-col items-center justify-center" >
< RecentsEmptyState type = { filter } / >
@@ -249,7 +428,7 @@ export const RecentActivityWidget = observer(function RecentActivityWidget(props
< div className = "box-border min-h-[250px]" >
< div className = "mb-2 flex items-center justify-between" >
< div className = "text-14 font-semibold text-tertiary" > { t ( "home.recents.title" ) } < / div >
{ showFilterSelect && < FiltersDropdown filters = { filters } activeFilter = { filter } setActiveFilter = { setFilter } / > }
{ headerActions }
< / div >
< div className = "flex max-h-[415px] min-h-[250px] flex-col overflow-y-auto" >
{ isWidgetLoading && < WidgetLoader widgetKey = { WIDGET_KEY } / > }