@@ -3,11 +3,15 @@ import app from 'flarum/admin/app';
33import SelectDropdown from 'flarum/common/components/SelectDropdown' ;
44import Button from 'flarum/common/components/Button' ;
55import abbreviateNumber from 'flarum/common/utils/abbreviateNumber' ;
6+ import extractText from 'flarum/common/utils/extractText' ;
67import LoadingIndicator from 'flarum/common/components/LoadingIndicator' ;
8+ import Placeholder from 'flarum/common/components/Placeholder' ;
79import icon from 'flarum/common/helpers/icon' ;
810
911import DashboardWidget , { IDashboardWidgetAttrs } from 'flarum/admin/components/DashboardWidget' ;
1012
13+ import StatisticsWidgetDateSelectionModal , { IDateSelection , IStatisticsWidgetDateSelectionModalAttrs } from './StatisticsWidgetDateSelectionModal' ;
14+
1115import type Mithril from 'mithril' ;
1216
1317// @ts -expect-error No typings available
@@ -25,14 +29,23 @@ export default class StatisticsWidget extends DashboardWidget {
2529
2630 chart : any ;
2731
32+ customPeriod : IDateSelection | null = null ;
33+
2834 timedData : Record < string , undefined | any > = { } ;
2935 lifetimeData : any ;
36+ customPeriodData : Record < string , undefined | any > = { } ;
37+
38+ noData : boolean = false ;
3039
3140 loadingLifetime = true ;
3241 loadingTimed : Record < string , 'unloaded' | 'loading' | 'loaded' | 'fail' > = this . entities . reduce ( ( acc , curr ) => {
3342 acc [ curr ] = 'unloaded' ;
3443 return acc ;
3544 } , { } as Record < string , 'unloaded' | 'loading' | 'loaded' | 'fail' > ) ;
45+ loadingCustom : Record < string , 'unloaded' | 'loading' | 'loaded' | 'fail' > = this . entities . reduce ( ( acc , curr ) => {
46+ acc [ curr ] = 'unloaded' ;
47+ return acc ;
48+ } , { } as Record < string , 'unloaded' | 'loading' | 'loaded' | 'fail' > ) ;
3649
3750 selectedEntity = 'users' ;
3851 selectedPeriod : undefined | string ;
@@ -105,17 +118,74 @@ export default class StatisticsWidget extends DashboardWidget {
105118 m . redraw ( ) ;
106119 }
107120
121+ async loadCustomRangeData ( model : string ) : Promise < void > {
122+ this . loadingCustom [ model ] = 'loading' ;
123+ m . redraw ( ) ;
124+
125+ // We clone so we can check that the same period is still selected
126+ // once the HTTP request is complete and the data is to be displayed
127+ const range = { ...this . customPeriod } ;
128+ try {
129+ const data = await app . request ( {
130+ method : 'GET' ,
131+ url : app . forum . attribute ( 'apiUrl' ) + '/statistics' ,
132+ params : {
133+ period : 'custom' ,
134+ model,
135+ dateRange : {
136+ start : range . start ,
137+ end : range . end ,
138+ } ,
139+ } ,
140+ } ) ;
141+
142+ if ( JSON . stringify ( range ) !== JSON . stringify ( this . customPeriod ) ) {
143+ // The range this method was called with is no longer the selected.
144+ // Bail out here.
145+ return ;
146+ }
147+
148+ this . customPeriodData [ model ] = data ;
149+ this . loadingCustom [ model ] = 'loaded' ;
150+
151+ m . redraw ( ) ;
152+ } catch ( e ) {
153+ if ( JSON . stringify ( range ) !== JSON . stringify ( this . customPeriod ) ) {
154+ // The range this method was called with is no longer the selected.
155+ // Bail out here.
156+ return ;
157+ }
158+
159+ console . error ( e ) ;
160+ this . loadingCustom [ model ] = 'fail' ;
161+ }
162+ }
163+
108164 className ( ) {
109165 return 'StatisticsWidget' ;
110166 }
111167
112168 content ( ) {
113- const loadingSelectedEntity = this . loadingTimed [ this . selectedEntity ] !== 'loaded' ;
114-
115- const thisPeriod = loadingSelectedEntity ? null : this . periods ! [ this . selectedPeriod ! ] ;
169+ const loadingSelectedEntity = ( this . selectedPeriod === 'custom' ? this . loadingCustom : this . loadingTimed ) [ this . selectedEntity ] !== 'loaded' ;
170+
171+ const thisPeriod = loadingSelectedEntity
172+ ? null
173+ : this . selectedPeriod === 'custom'
174+ ? {
175+ start : this . customPeriod ?. end ! ,
176+ end : this . customPeriod ?. end ! ,
177+ step : 86400 ,
178+ }
179+ : this . periods ! [ this . selectedPeriod ! ] ;
116180
117- if ( ! this . timedData [ this . selectedEntity ] && this . loadingTimed [ this . selectedEntity ] === 'unloaded' ) {
118- this . loadTimedData ( this . selectedEntity ) ;
181+ if ( this . selectedPeriod === 'custom' ) {
182+ if ( ! this . customPeriodData [ this . selectedEntity ] && this . loadingCustom [ this . selectedEntity ] === 'unloaded' ) {
183+ this . loadCustomRangeData ( this . selectedEntity ) ;
184+ }
185+ } else {
186+ if ( ! this . timedData [ this . selectedEntity ] && this . loadingTimed [ this . selectedEntity ] === 'unloaded' ) {
187+ this . loadTimedData ( this . selectedEntity ) ;
188+ }
119189 }
120190
121191 return (
@@ -128,16 +198,56 @@ export default class StatisticsWidget extends DashboardWidget {
128198 < LoadingIndicator size = "small" display = "inline" />
129199 ) : (
130200 < SelectDropdown disabled = { loadingSelectedEntity } buttonClassName = "Button Button--text" caretIcon = "fas fa-caret-down" >
131- { Object . keys ( this . periods ! ) . map ( ( period ) => (
132- < Button
133- key = { period }
134- active = { period === this . selectedPeriod }
135- onclick = { this . changePeriod . bind ( this , period ) }
136- icon = { period === this . selectedPeriod ? 'fas fa-check' : true }
137- >
138- { app . translator . trans ( `flarum-statistics.admin.statistics.${ period } _label` ) }
139- </ Button >
140- ) ) }
201+ { Object . keys ( this . periods ! )
202+ . map ( ( period ) => (
203+ < Button
204+ key = { period }
205+ active = { period === this . selectedPeriod }
206+ onclick = { this . changePeriod . bind ( this , period ) }
207+ icon = { period === this . selectedPeriod ? 'fas fa-check' : true }
208+ >
209+ { app . translator . trans ( `flarum-statistics.admin.statistics.${ period } _label` ) }
210+ </ Button >
211+ ) )
212+ . concat ( [
213+ < Button
214+ key = "custom"
215+ active = { this . selectedPeriod === 'custom' }
216+ onclick = { ( ) => {
217+ const attrs : IStatisticsWidgetDateSelectionModalAttrs = {
218+ onModalSubmit : ( dates : IDateSelection ) => {
219+ if ( JSON . stringify ( dates ) === JSON . stringify ( this . customPeriod ) ) {
220+ // If same period is selected, don't reload data
221+ return ;
222+ }
223+
224+ this . customPeriodData = { } ;
225+ Object . keys ( this . loadingCustom ) . forEach ( ( k ) => ( this . loadingCustom [ k ] = 'unloaded' ) ) ;
226+ this . customPeriod = dates ;
227+ this . changePeriod ( 'custom' ) ;
228+ } ,
229+ } as any ;
230+
231+ // If we have a custom period set already,
232+ // let's prefill the modal with it
233+ if ( this . customPeriod ) {
234+ attrs . value = this . customPeriod ;
235+ }
236+
237+ app . modal . show ( StatisticsWidgetDateSelectionModal as any , attrs as any ) ;
238+ } }
239+ icon = { this . selectedPeriod === 'custom' ? 'fas fa-check' : true }
240+ >
241+ { this . selectedPeriod === 'custom'
242+ ? extractText (
243+ app . translator . trans ( `flarum-statistics.admin.statistics.custom_label_specified` , {
244+ fromDate : dayjs ( this . customPeriod ! . start ! * 1000 ) . format ( 'DD MMM YYYY' ) ,
245+ toDate : dayjs ( this . customPeriod ! . end ! * 1000 ) . format ( 'DD MMM YYYY' ) ,
246+ } )
247+ )
248+ : app . translator . trans ( `flarum-statistics.admin.statistics.custom_label` ) }
249+ </ Button > ,
250+ ] ) }
141251 </ SelectDropdown >
142252 ) }
143253 </ div >
@@ -148,11 +258,14 @@ export default class StatisticsWidget extends DashboardWidget {
148258 const thisPeriodCount = loadingSelectedEntity
149259 ? app . translator . trans ( 'flarum-statistics.admin.statistics.loading' )
150260 : this . getPeriodCount ( entity , thisPeriod ! ) ;
151- const lastPeriodCount = loadingSelectedEntity
152- ? app . translator . trans ( 'flarum-statistics.admin.statistics.loading' )
153- : this . getPeriodCount ( entity , this . getLastPeriod ( thisPeriod ! ) ) ;
261+ const lastPeriodCount =
262+ this . selectedPeriod === 'custom'
263+ ? null
264+ : loadingSelectedEntity
265+ ? app . translator . trans ( 'flarum-statistics.admin.statistics.loading' )
266+ : this . getPeriodCount ( entity , this . getLastPeriod ( thisPeriod ! ) ) ;
154267 const periodChange =
155- loadingSelectedEntity || lastPeriodCount === 0
268+ loadingSelectedEntity || lastPeriodCount === 0 || lastPeriodCount === null
156269 ? 0
157270 : ( ( ( thisPeriodCount as number ) - ( lastPeriodCount as number ) ) / ( lastPeriodCount as number ) ) * 100 ;
158271
@@ -197,6 +310,8 @@ export default class StatisticsWidget extends DashboardWidget {
197310 />
198311 ) }
199312 </ >
313+
314+ { this . noData && < Placeholder text = { app . translator . trans ( `flarum-statistics.admin.statistics.no_data` ) } /> }
200315 </ div >
201316 ) ;
202317 }
@@ -206,7 +321,16 @@ export default class StatisticsWidget extends DashboardWidget {
206321 return ;
207322 }
208323
209- const period = this . periods ! [ this . selectedPeriod ! ] ;
324+ debugger ;
325+
326+ const period =
327+ this . selectedPeriod === 'custom'
328+ ? {
329+ start : this . customPeriod ?. start ! ,
330+ end : this . customPeriod ?. end ! ,
331+ step : 86400 ,
332+ }
333+ : this . periods ! [ this . selectedPeriod ! ] ;
210334 const periodLength = period . end - period . start ;
211335 const labels = [ ] ;
212336 const thisPeriod = [ ] ;
@@ -231,6 +355,15 @@ export default class StatisticsWidget extends DashboardWidget {
231355 lastPeriod . push ( this . getPeriodCount ( this . selectedEntity , { start : i - periodLength , end : i - periodLength + period . step } ) ) ;
232356 }
233357
358+ if ( thisPeriod . length === 0 ) {
359+ this . noData = true ;
360+ m . redraw ( ) ;
361+ return ;
362+ } else {
363+ this . noData = false ;
364+ m . redraw ( ) ;
365+ }
366+
234367 const datasets = [ { values : lastPeriod } , { values : thisPeriod } ] ;
235368 const data = {
236369 labels,
@@ -275,7 +408,7 @@ export default class StatisticsWidget extends DashboardWidget {
275408 }
276409
277410 getPeriodCount ( entity : string , period : { start : number ; end : number } ) {
278- const timed : Record < string , number > = this . timedData [ entity ] ;
411+ const timed : Record < string , number > = ( this . selectedPeriod === 'custom' ? this . customPeriodData : this . timedData ) [ entity ] ;
279412 let count = 0 ;
280413
281414 for ( const t in timed ) {
0 commit comments