1
- import * as React from 'react' ;
1
+ import React , { useState , useEffect } from 'react' ;
2
+ import {
3
+ closestCenter ,
4
+ DndContext ,
5
+ DragOverlay ,
6
+ KeyboardSensor ,
7
+ PointerSensor ,
8
+ useSensor ,
9
+ useSensors ,
10
+ } from '@dnd-kit/core' ;
11
+ import {
12
+ arrayMove ,
13
+ SortableContext ,
14
+ sortableKeyboardCoordinates ,
15
+ verticalListSortingStrategy ,
16
+ } from '@dnd-kit/sortable' ;
17
+ import { useSortable } from '@dnd-kit/sortable' ;
18
+ import { CSS } from '@dnd-kit/utilities' ;
2
19
import { useActiveGroup } from '../../../hooks/groups/useActiveGroup' ;
3
- import { sortPostsComparator } from '../../../models/groups/posts' ;
20
+ import { PostData , sortPostsComparator } from '../../../models/groups/posts' ;
4
21
import Tabs from '../../Tabs' ;
5
22
import FeedItem from './FeedItem' ;
23
+ import { MenuIcon } from '@heroicons/react/solid' ;
24
+ import { GroupData } from '../../../models/groups/groups' ;
25
+ import { useGroupActions } from '../../../hooks/groups/useGroupActions' ;
6
26
7
- export default function Feed ( ) {
27
+ function SortableItem ( props : {
28
+ id : string ;
29
+ group : GroupData ;
30
+ post : PostData ;
31
+ isBeingDragged : boolean ;
32
+ } ) {
33
+ // probably post was just deleted before items updated
34
+ if ( ! props . post ) return null ;
35
+
36
+ const {
37
+ attributes,
38
+ listeners,
39
+ setNodeRef,
40
+ transform,
41
+ transition,
42
+ } = useSortable ( { id : props . id } ) ;
43
+
44
+ const style = {
45
+ transform : CSS . Transform . toString ( transform ) ,
46
+ transition,
47
+ } ;
48
+
49
+ return (
50
+ < div ref = { setNodeRef } style = { style } >
51
+ < FeedItem
52
+ group = { props . group }
53
+ post = { props . post }
54
+ isBeingDragged = { props . isBeingDragged }
55
+ dragHandle = {
56
+ < div
57
+ className = "self-stretch flex items-center px-2"
58
+ { ...attributes }
59
+ { ...listeners }
60
+ >
61
+ < MenuIcon className = "h-5 w-5 text-gray-300" />
62
+ </ div >
63
+ }
64
+ />
65
+ </ div >
66
+ ) ;
67
+ }
68
+
69
+ export default function Feed ( ) : JSX . Element {
8
70
const feedTabs = [ 'all' , 'assignments' , 'announcements' ] ;
9
71
const [ currentFeed , setCurrentFeed ] = React . useState < string > ( 'all' ) ;
10
72
const group = useActiveGroup ( ) ;
73
+ const { updatePostOrdering } = useGroupActions ( ) ;
11
74
12
- const feedPosts = group . posts
13
- ?. filter ( post => {
14
- if ( ! group . showAdminView && ! post . isPublished ) return false ;
15
- if ( currentFeed === 'all' ) return true ;
16
- if ( currentFeed === 'assignments' ) return post . type === 'assignment' ;
17
- if ( currentFeed === 'announcements' ) return post . type === 'announcement' ;
18
- throw 'unknown feed ' + this . currentFeed ;
75
+ const [ activeId , setActiveId ] = useState ( null ) ;
76
+ const [ items , setItems ] = useState ( [ ] ) ;
77
+ const sensors = useSensors (
78
+ useSensor ( PointerSensor ) ,
79
+ useSensor ( KeyboardSensor , {
80
+ coordinateGetter : sortableKeyboardCoordinates ,
19
81
} )
20
- . sort ( sortPostsComparator )
21
- . reverse ( ) ;
82
+ ) ;
83
+
84
+ useEffect ( ( ) => {
85
+ if ( ! group . groupData || ! group . posts ) return ;
86
+ if ( group . groupData . postOrdering ?. length !== group . posts . length ) {
87
+ // This shouldn't happen. But maybe this is from an old group
88
+ // that was created before post ordering was implemented
89
+
90
+ // Note: There's a small bug here and in PostProblems.tsx
91
+ // where immediately after creating a new post the post ordering and post length will be off...
92
+ setItems (
93
+ group . posts . filter ( post => {
94
+ if ( ! group . showAdminView && ! post . isPublished ) return false ;
95
+ if ( currentFeed === 'all' ) return true ;
96
+ if ( currentFeed === 'assignments' ) return post . type === 'assignment' ;
97
+ if ( currentFeed === 'announcements' ) return post . type === 'announcement' ;
98
+ throw 'unknown feed ' + this . currentFeed ;
99
+ } )
100
+ . sort ( sortPostsComparator )
101
+ . reverse ( )
102
+ . map ( x => x . id )
103
+ ) ;
104
+ } else {
105
+ setItems ( group . groupData . postOrdering ) ;
106
+ }
107
+ } , [ group . groupData ?. postOrdering , group . posts ] ) ;
108
+
109
+ const handleDragStart = ( event ) => {
110
+ const { active} = event ;
111
+
112
+ setActiveId ( active . id ) ;
113
+ }
114
+
115
+ const handleDragEnd = ( event ) => {
116
+ const { active, over} = event ;
117
+
118
+ if ( active . id !== over . id ) {
119
+ setItems ( ( items ) => {
120
+ const oldIndex = items . indexOf ( active . id ) ;
121
+ const newIndex = items . indexOf ( over . id ) ;
122
+
123
+ const newArr = arrayMove ( items , oldIndex , newIndex ) ;
124
+ updatePostOrdering ( group . activeGroupId , newArr ) ;
125
+ return newArr ;
126
+ } ) ;
127
+ }
128
+
129
+ setActiveId ( null ) ;
130
+ }
22
131
23
132
return (
24
133
< >
@@ -36,15 +145,43 @@ export default function Feed() {
36
145
</ div >
37
146
< div className = "mt-4" >
38
147
{ group . isLoading && 'Loading posts...' }
39
- { ! group . isLoading && (
40
- < ul className = "divide-y divide-solid divide-gray-200 dark:divide-gray-600 sm:divide-none sm:space-y-4" >
41
- { feedPosts . map ( post => (
42
- < li key = { post . id } >
43
- < FeedItem group = { group . groupData } post = { post } />
44
- </ li >
45
- ) ) }
46
- </ ul >
47
- ) }
148
+ { ! group . isLoading &&
149
+ ( group . showAdminView ? (
150
+ < DndContext
151
+ sensors = { sensors }
152
+ collisionDetection = { closestCenter }
153
+ onDragStart = { handleDragStart }
154
+ onDragEnd = { handleDragEnd }
155
+ >
156
+ < SortableContext
157
+ items = { items }
158
+ strategy = { verticalListSortingStrategy }
159
+ >
160
+ < div className = "divide-y divide-solid divide-gray-200 dark:divide-gray-600 sm:divide-none sm:space-y-4" >
161
+ { items . map ( id => < SortableItem key = { id } id = { id } group = { group . groupData } post = { group . posts . find ( x => x . id === id ) } isBeingDragged = { activeId === id } /> ) }
162
+ </ div >
163
+ </ SortableContext >
164
+ < DragOverlay >
165
+ { activeId ? < FeedItem
166
+ group = { group . groupData }
167
+ post = { group . posts . find ( x => x . id === activeId ) }
168
+ dragHandle = { (
169
+ < div
170
+ className = "self-stretch flex items-center px-2"
171
+ >
172
+ < MenuIcon className = "h-5 w-5 text-gray-300" />
173
+ </ div >
174
+ ) }
175
+ /> : null }
176
+ </ DragOverlay >
177
+ </ DndContext >
178
+ ) : (
179
+ < div className = "divide-y divide-solid divide-gray-200 dark:divide-gray-600 sm:divide-none sm:space-y-4" >
180
+ { items . map ( id => (
181
+ < FeedItem group = { group . groupData } post = { group . posts . find ( x => x . id === id ) } key = { id } />
182
+ ) ) }
183
+ </ div >
184
+ ) ) }
48
185
</ div >
49
186
</ >
50
187
) ;
0 commit comments