Skip to content

Commit 13318cd

Browse files
[STT-1580] - As a user I want to filter Events and Planning items by their created date (#2798)
* [STT-1580] wire created-date filtering through combined search and backend query normalization * [STT-1580] add regression coverage for created-date search serialization and query normalization * [STT-1580] restore existing search date parsing * [STT-1580] stabilize playwright next-week search fixtures * [STT-1580] address Copilot review comments * [STT-1580] drop playwright next-week fixture stabilization * [STT-1580] handle created-date timezone boundaries * simplify end date handling * updated test
1 parent defdf65 commit 13318cd

File tree

19 files changed

+539
-96
lines changed

19 files changed

+539
-96
lines changed

client/actions/eventsPlanning/tests/api_test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,25 @@ describe('actions.eventsplanning.api', () => {
6363
})
6464
.catch(done.fail);
6565
});
66+
67+
it('created date search', (done) => {
68+
const params = {
69+
created_start_date: moment('2026-04-09T00:00:00+0000'),
70+
created_end_date: moment('2026-04-09T23:59:59+0000'),
71+
};
72+
73+
store.test(done, eventsPlanningApi.query(params))
74+
.then(() => {
75+
expect(planningApis.combined.search.callCount).toBe(1);
76+
expect(planningApis.combined.search.args[0]).toEqual([jasmine.objectContaining({
77+
created_start_date: params.created_start_date,
78+
created_end_date: params.created_end_date,
79+
})]);
80+
81+
done();
82+
})
83+
.catch(done.fail);
84+
});
6685
});
6786

6887
describe('refetch', () => {

client/api/search.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import {IEventOrPlanningItem, ISearchAPIParams, ISearchParams} from '../interfaces';
22
import {superdeskApi, planningApi as sdPlanningApi} from '../superdeskApi';
33
import {IRestApiResponse} from 'superdesk-api';
4-
import {getDateTimeElasticFormat, getTimeZoneOffset} from '../utils';
4+
import {
5+
getCreatedEndDateElasticFormat,
6+
getCreatedStartDateElasticFormat,
7+
getDateTimeElasticFormat,
8+
getTimeZoneOffset,
9+
} from '../utils';
510
import {default as timeUtils} from '../utils/time';
611
import {appConfig} from 'appConfig';
712
import planningApi from '../actions/planning/api';
@@ -26,6 +31,9 @@ export function arrayToString(items?: Array<string | number>): string {
2631
}
2732

2833
export function convertCommonParams(params: ISearchParams): Partial<ISearchAPIParams> {
34+
const createdStartDate = params.created_start_date ?? params.advancedSearch?.created_start_date;
35+
const createdEndDate = params.created_end_date ?? params.advancedSearch?.created_end_date;
36+
2937
return {
3038
item_ids: arrayToString(params.item_ids),
3139
name: params.name,
@@ -44,6 +52,12 @@ export function convertCommonParams(params: ISearchParams): Partial<ISearchAPIPa
4452
end_date: params.end_date == null ?
4553
null :
4654
getDateTimeElasticFormat(params.end_date, params.date_filter != 'for_date'),
55+
created_start_date: createdStartDate == null ?
56+
null :
57+
getCreatedStartDateElasticFormat(createdStartDate),
58+
created_end_date: createdEndDate == null ?
59+
null :
60+
getCreatedEndDateElasticFormat(createdEndDate),
4761
start_of_week: appConfig.start_of_week,
4862
slugline: params.slugline,
4963
lock_state: params.lock_state,

client/components/AdvancedSearch/index.tsx

Lines changed: 108 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {SEARCH_SPIKE_STATE, IDateRange, ISearchParams, IAssignmentSearchParams,
77

88
import {getUserInterfaceLanguageFromCV} from '../../utils/users';
99
import {renderGroupedFieldsForPanel} from '../fields';
10+
import {DatesGroup} from '../fields/editor/DatesGroup';
1011

1112
interface ICommonParams {
1213
searchProfile: any;
@@ -124,107 +125,120 @@ export class AdvancedSearch extends React.PureComponent<IProps> {
124125
}
125126

126127
render() {
128+
const {
129+
start_date,
130+
end_date,
131+
date_filter,
132+
creation_date,
133+
...profileWithoutDates
134+
} = this.props.searchProfile;
127135
const customTextFields = Object.keys(this.props.searchProfile)
128136
.filter((field) => superdeskApi.entities.vocabulary.getVocabulary(field)?.field_type === 'text')
129137
.reduce((prev, field) => ({
130138
...prev,
131139
[field]: {storageField: `customText.${field}`},
132140
}), {});
133141

134-
return renderGroupedFieldsForPanel(
135-
'editor',
136-
this.props.searchProfile,
137-
{
138-
onChange: this.props.onChange,
139-
popupContainer: this.props.popupContainer,
140-
language: getUserInterfaceLanguageFromCV(),
141-
item: this.props.params,
142-
schema: {},
143-
},
144-
{
145-
...customTextFields,
146-
user: {
147-
field: 'userIds',
148-
valueStoredAsArray: true,
149-
deskId: this.props.filterUsersByDesk,
150-
},
151-
content_type: {
152-
field: 'contentType',
153-
},
154-
multiple_content: {
155-
field: 'multipleContent',
156-
},
157-
genre: {
158-
clearable: true,
159-
},
160-
assignment_priority: {
161-
field: 'assignmentPriority',
162-
noTitleInPopup: true,
163-
clearable: true,
164-
},
165-
anpa_category: {
166-
field: this.props.type === 'assignments' ? 'anpaCategory' : 'anpa_category',
167-
singleSelect: false,
168-
},
169-
start_date: {
170-
canClear: true,
171-
onChange: this.onStartDateTimeChange,
172-
},
173-
end_date: {
174-
canClear: true,
175-
onChange: this.onEndDateTimeChange,
176-
},
177-
date_filter: {
178-
onChange: this.onRelativeDateTimeChange,
179-
},
180-
spike_state: {
181-
enabled: (
182-
this.props.type === 'event_planning'
183-
&& !this.props.params.posted
184-
&& !this.props.params.state?.length
185-
),
186-
},
187-
agendas: {
188-
enabled: (
189-
this.props.type === 'event_planning'
190-
&& !this.props.params.no_agenda_assigned
191-
&& this.props.enabledField !== 'search_enabled'
192-
),
193-
},
194-
g2_content_type: {
195-
enabled: this.props.type === 'assignments' || !this.props.params.no_coverage,
196-
},
197-
calendars: {
198-
enabled: (
199-
this.props.type === 'event_planning'
200-
&& !this.props.params.no_calendar_assigned
201-
&& this.props.enabledField !== 'search_enabled'
202-
),
203-
},
204-
include_killed: {
205-
enabled: this.props.type === 'event_planning' && !this.props.params.state?.length,
206-
},
207-
exclude_rescheduled_and_cancelled: {
208-
enabled: this.props.type === 'event_planning' && !this.props.params.state?.length,
209-
},
210-
state: {
211-
excludeOptions: this.props.type === 'event_planning' && !this.props.params.posted ?
212-
[] :
213-
NON_PUBLISHED_STATES,
214-
},
215-
location: {
216-
disableAddLocation: false,
217-
},
218-
priority: {
219-
multiple: true,
220-
defaultValue: [],
221-
},
222-
urgency: {
223-
valueAsString: false,
224-
},
225-
},
226-
null,
227-
this.props.enabledField
142+
return (
143+
<React.Fragment>
144+
{renderGroupedFieldsForPanel(
145+
'editor',
146+
profileWithoutDates,
147+
{
148+
onChange: this.props.onChange,
149+
popupContainer: this.props.popupContainer,
150+
language: getUserInterfaceLanguageFromCV(),
151+
item: this.props.params,
152+
onChangeMultiple: this.props.onChangeMultiple,
153+
schema: {},
154+
},
155+
{
156+
...customTextFields,
157+
user: {
158+
field: 'userIds',
159+
valueStoredAsArray: true,
160+
deskId: this.props.filterUsersByDesk,
161+
},
162+
content_type: {
163+
field: 'contentType',
164+
},
165+
multiple_content: {
166+
field: 'multipleContent',
167+
},
168+
genre: {
169+
clearable: true,
170+
},
171+
assignment_priority: {
172+
field: 'assignmentPriority',
173+
noTitleInPopup: true,
174+
clearable: true,
175+
},
176+
anpa_category: {
177+
field: this.props.type === 'assignments' ? 'anpaCategory' : 'anpa_category',
178+
singleSelect: false,
179+
},
180+
spike_state: {
181+
enabled: (
182+
this.props.type === 'event_planning'
183+
&& !this.props.params.posted
184+
&& !this.props.params.state?.length
185+
),
186+
},
187+
agendas: {
188+
enabled: (
189+
this.props.type === 'event_planning'
190+
&& !this.props.params.no_agenda_assigned
191+
&& this.props.enabledField !== 'search_enabled'
192+
),
193+
},
194+
g2_content_type: {
195+
enabled: this.props.type === 'assignments' || !this.props.params.no_coverage,
196+
},
197+
calendars: {
198+
enabled: (
199+
this.props.type === 'event_planning'
200+
&& !this.props.params.no_calendar_assigned
201+
&& this.props.enabledField !== 'search_enabled'
202+
),
203+
},
204+
include_killed: {
205+
enabled: this.props.type === 'event_planning' && !this.props.params.state?.length,
206+
},
207+
exclude_rescheduled_and_cancelled: {
208+
enabled: this.props.type === 'event_planning' && !this.props.params.state?.length,
209+
},
210+
state: {
211+
excludeOptions: this.props.type === 'event_planning' && !this.props.params.posted ?
212+
[] :
213+
NON_PUBLISHED_STATES,
214+
},
215+
location: {
216+
disableAddLocation: false,
217+
},
218+
priority: {
219+
multiple: true,
220+
defaultValue: [],
221+
},
222+
urgency: {
223+
valueAsString: false,
224+
},
225+
},
226+
null,
227+
this.props.enabledField
228+
)}
229+
230+
<DatesGroup
231+
searchProfile={{start_date, end_date, date_filter, creation_date}}
232+
params={this.props.params}
233+
onChange={this.props.onChange}
234+
onChangeMultiple={this.props.onChangeMultiple}
235+
popupContainer={this.props.popupContainer}
236+
enabledField={this.props.enabledField}
237+
onStartDateTimeChange={this.onStartDateTimeChange}
238+
onEndDateTimeChange={this.onEndDateTimeChange}
239+
onRelativeDateTimeChange={this.onRelativeDateTimeChange}
240+
/>
241+
</React.Fragment>
228242
);
229243
}
230244
}

client/components/EventsPlanningFilters/PreviewFilter.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,26 @@ export class PreviewFilter extends React.PureComponent<IEventsPlanningContentPan
125125
)}
126126
</ul>
127127

128+
<Label
129+
text={gettext('Creation Date')}
130+
row={true}
131+
/>
132+
<ul className="simple-list simple-list--dotted">
133+
{renderFieldsForPanel(
134+
'simple-preview',
135+
{
136+
created_start_date: {enabled: true, index: 1},
137+
created_end_date: {enabled: true, index: 2},
138+
},
139+
{
140+
item: this.props.filter.params,
141+
language: language,
142+
},
143+
{
144+
}
145+
)}
146+
</ul>
147+
128148
{!this.props.filter.schedules?.length ? null : (
129149
<React.Fragment>
130150
<Label
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as React from 'react';
2+
import moment from 'moment';
3+
4+
import {superdeskApi} from '../../../superdeskApi';
5+
import {IEditorFieldProps} from '../../../interfaces';
6+
7+
import {EditorFieldDateOnly} from './DateOnly';
8+
9+
interface IProps extends IEditorFieldProps {
10+
onChangeMultiple(updates: {[key: string]: any}): void;
11+
}
12+
13+
export class EditorFieldCreationDate extends React.PureComponent<IProps> {
14+
constructor(props: IProps) {
15+
super(props);
16+
17+
this.onStartDateChange = this.onStartDateChange.bind(this);
18+
this.onEndDateChange = this.onEndDateChange.bind(this);
19+
}
20+
21+
onStartDateChange(_field: string, value: any) {
22+
this.props.onChangeMultiple({
23+
created_start_date: value == null ? value : moment(value).startOf('day'),
24+
});
25+
}
26+
27+
onEndDateChange(_field: string, value: any) {
28+
this.props.onChangeMultiple({
29+
created_end_date: value == null ? value : moment(value).endOf('day'),
30+
});
31+
}
32+
33+
render() {
34+
const {gettext} = superdeskApi.localization;
35+
const props = this.props;
36+
37+
return (
38+
<div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12}}>
39+
<EditorFieldDateOnly
40+
{...props}
41+
field="created_start_date"
42+
label={gettext('From')}
43+
onChange={this.onStartDateChange}
44+
canClear={true}
45+
/>
46+
<EditorFieldDateOnly
47+
{...props}
48+
field="created_end_date"
49+
label={gettext('To')}
50+
onChange={this.onEndDateChange}
51+
canClear={true}
52+
/>
53+
</div>
54+
);
55+
}
56+
}

0 commit comments

Comments
 (0)