Skip to content

Commit 1ffe26f

Browse files
committed
fix: Fix create audience dialog
1 parent 1948f96 commit 1ffe26f

4 files changed

Lines changed: 206 additions & 6 deletions

File tree

src/components/PushAudienceDialog/PushAudienceDialog.react.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ export default class PushAudienceDialog extends React.Component {
7272
stateSettings.platforms = deviceType.$in || [];
7373
}
7474
if (audienceInfo.filters) {
75-
stateSettings.filters = audienceInfo.filters;
75+
stateSettings.filters = audienceInfo.filters.map(filter =>
76+
filter.get('class') ? filter : filter.set('class', '_Installation')
77+
);
7678
}
7779
if (audienceInfo.name) {
7880
stateSettings.audienceName = audienceInfo.name;
@@ -100,10 +102,22 @@ export default class PushAudienceDialog extends React.Component {
100102
return;
101103
}
102104
const available = Filters.availableFilters(this.props.schema, this.state.filters);
103-
const field = Object.keys(available)[0];
105+
106+
const keys = Object.keys(available);
107+
if (keys.length === 0) {
108+
this.setState({
109+
errorMessage: 'No condition available.',
110+
});
111+
return;
112+
}
113+
114+
const field = keys[0];
104115
this.setState(
105116
({ filters }) => ({
106-
filters: filters.push(new Map({ field: field, constraint: available[field][0] })),
117+
filters: filters.push(
118+
new Map({ class: '_Installation', field: field, constraint: available[field][0] })
119+
),
120+
errorMessage: undefined,
107121
}),
108122
this.fetchAudienceSize.bind(this)
109123
);
@@ -284,11 +298,19 @@ export default class PushAudienceDialog extends React.Component {
284298
/>
285299
<div className={styles.filter}>
286300
<Filter
287-
schema={this.props.schema}
301+
className="_Installation"
302+
schema={{ _Installation: this.props.schema }}
303+
allClasses={{ _Installation: this.props.schema }}
288304
filters={this.state.filters}
289305
onChange={filters => {
290-
this.setState({ filters }, this.fetchAudienceSize.bind(this));
306+
this.setState(
307+
{ filters, errorMessage: undefined },
308+
this.fetchAudienceSize.bind(this)
309+
);
291310
}}
311+
onSearch={() =>
312+
this.setState({ errorMessage: undefined }, this.fetchAudienceSize.bind(this))
313+
}
292314
renderRow={props => <InstallationCondition {...props} />}
293315
/>
294316
</div>
@@ -307,7 +329,7 @@ export default class PushAudienceDialog extends React.Component {
307329
<FormNote
308330
show={Boolean(
309331
(this.props.errorMessage && this.props.errorMessage.length > 0) ||
310-
(this.state.errorMessage && this.state.errorMessage.length > 0)
332+
(this.state.errorMessage && this.state.errorMessage.length > 0)
311333
)}
312334
color="red"
313335
>

src/lib/Filters.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ export function availableFilters(schema, currentFilters, blacklist) {
260260

261261
export function findRelatedClasses(referClass, allClasses, blacklist, currentFilters) {
262262
const relatedClasses = {};
263+
if (!allClasses) {
264+
return relatedClasses;
265+
}
263266
if (allClasses[referClass]) {
264267
const availableForRefer = availableFilters(allClasses[referClass], currentFilters, blacklist);
265268
if (Object.keys(availableForRefer).length > 0) {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
const { findRelatedClasses } = require('../Filters');
9+
10+
describe('findRelatedClasses', () => {
11+
it('returns an empty object when all classes are undefined', () => {
12+
expect(findRelatedClasses('_Installation', undefined, [], undefined)).toEqual({});
13+
});
14+
15+
it('returns the available filters for the referenced class', () => {
16+
const allClasses = { _Installation: { deviceType: { type: 'String' } } };
17+
const result = findRelatedClasses('_Installation', allClasses, [], undefined);
18+
19+
expect(result._Installation).toBeDefined();
20+
expect(result._Installation.deviceType).toContain('exists');
21+
});
22+
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
jest.dontMock('../../components/PushAudienceDialog/PushAudienceDialog.react');
9+
jest.mock('../../components/Filter/Filter.react');
10+
jest.mock('../../components/MultiSelect/MultiSelect.react');
11+
jest.mock('../../components/Popover/Popover.react', () => 'div');
12+
jest.mock('context/currentApp', () => require('../../context/currentApp'), { virtual: true });
13+
14+
const Filter = require('../../components/Filter/Filter.react').default;
15+
const PushAudienceDialog =
16+
require('../../components/PushAudienceDialog/PushAudienceDialog.react').default;
17+
const React = require('react');
18+
const { act } = React;
19+
const { List, Map } = require('immutable');
20+
const { renderComponent } = require('./renderWithAct');
21+
22+
const defaultProps = {
23+
availableDevices: [],
24+
primaryAction: jest.fn(),
25+
secondaryAction: jest.fn(),
26+
};
27+
28+
function renderDialog(schema, audienceInfo) {
29+
return renderComponent(
30+
<PushAudienceDialog {...defaultProps} audienceInfo={audienceInfo} schema={schema} />
31+
);
32+
}
33+
34+
describe('PushAudienceDialog', () => {
35+
beforeEach(() => {
36+
jest.spyOn(PushAudienceDialog.prototype, 'fetchAudienceSize').mockImplementation(() => {});
37+
});
38+
39+
afterEach(() => {
40+
jest.restoreAllMocks();
41+
});
42+
43+
it('configures the shared filter for the Installation class', () => {
44+
const schema = {
45+
deviceType: { type: 'String' },
46+
};
47+
const component = renderDialog(schema);
48+
const filter = component.root.findByType(Filter);
49+
50+
expect(filter.props.className).toBe('_Installation');
51+
expect(filter.props.schema).toEqual({ _Installation: schema });
52+
expect(filter.props.allClasses).toEqual({ _Installation: schema });
53+
});
54+
55+
it('adds an available audience condition for the Installation class', () => {
56+
const component = renderDialog({
57+
deviceType: { type: 'String' },
58+
});
59+
const dialog = component.getInstance();
60+
61+
act(() => {
62+
dialog.setState({ errorMessage: 'No condition available.' });
63+
});
64+
act(() => {
65+
dialog.handleAddCondition();
66+
});
67+
68+
expect(dialog.state.filters.size).toBe(1);
69+
expect(dialog.state.filters.getIn([0, 'class'])).toBe('_Installation');
70+
expect(dialog.state.filters.getIn([0, 'field'])).toBe('deviceType');
71+
expect(dialog.state.filters.getIn([0, 'constraint'])).toBe('exists');
72+
expect(dialog.state.errorMessage).toBeUndefined();
73+
expect(dialog.fetchAudienceSize).toHaveBeenCalledTimes(1);
74+
});
75+
76+
it('shows an error when no audience condition is available', () => {
77+
const component = renderDialog({
78+
unsupported: { type: 'File' },
79+
});
80+
const dialog = component.getInstance();
81+
82+
act(() => {
83+
dialog.handleAddCondition();
84+
});
85+
86+
expect(dialog.state.filters.size).toBe(0);
87+
expect(dialog.state.errorMessage).toBe('No condition available.');
88+
});
89+
90+
it('normalizes persisted audience filters for the Installation class', () => {
91+
const filters = new List([
92+
new Map({
93+
field: 'deviceType',
94+
constraint: 'exists',
95+
}),
96+
]);
97+
const component = renderDialog(
98+
{ deviceType: { type: 'String' } },
99+
{
100+
filters,
101+
}
102+
);
103+
const dialog = component.getInstance();
104+
105+
expect(dialog.state.filters.getIn([0, 'class'])).toBe('_Installation');
106+
expect(dialog.fetchAudienceSize).toHaveBeenCalledTimes(1);
107+
});
108+
109+
it('clears stale errors when filters change', () => {
110+
const schema = {
111+
deviceType: { type: 'String' },
112+
};
113+
const component = renderDialog(schema);
114+
const dialog = component.getInstance();
115+
const filters = new List([
116+
new Map({
117+
class: '_Installation',
118+
field: 'deviceType',
119+
constraint: 'exists',
120+
}),
121+
]);
122+
act(() => {
123+
dialog.setState({ errorMessage: 'No condition available.' });
124+
});
125+
const filter = component.root.findByType(Filter);
126+
127+
act(() => {
128+
filter.props.onChange(filters);
129+
});
130+
131+
expect(dialog.state.filters).toBe(filters);
132+
expect(dialog.state.errorMessage).toBeUndefined();
133+
expect(dialog.fetchAudienceSize).toHaveBeenCalledTimes(1);
134+
});
135+
136+
it('clears stale errors when filters are searched', () => {
137+
const component = renderDialog({
138+
deviceType: { type: 'String' },
139+
});
140+
const dialog = component.getInstance();
141+
act(() => {
142+
dialog.setState({ errorMessage: 'No condition available.' });
143+
});
144+
const filter = component.root.findByType(Filter);
145+
146+
act(() => {
147+
filter.props.onSearch();
148+
});
149+
150+
expect(dialog.state.errorMessage).toBeUndefined();
151+
expect(dialog.fetchAudienceSize).toHaveBeenCalledTimes(1);
152+
});
153+
});

0 commit comments

Comments
 (0)