Skip to content

Commit 9f6c7bf

Browse files
committed
feat: use MessageComposer's PollComposer to handle poll creation
1 parent 7d4ed43 commit 9f6c7bf

File tree

7 files changed

+232
-335
lines changed

7 files changed

+232
-335
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import clsx from 'clsx';
2+
import React from 'react';
3+
import { SimpleSwitchField } from '../../Form/SwitchField';
4+
import { FieldError } from '../../Form/FieldError';
5+
import { useTranslationContext } from '../../../context';
6+
import { useMessageComposer } from '../../MessageInput/hooks/messageComposer/useMessageComposer';
7+
import { useStateStore } from '../../../store';
8+
import type { PollComposerState } from 'stream-chat';
9+
10+
const pollComposerStateSelector = (state: PollComposerState) => ({
11+
enforce_unique_vote: state.data.enforce_unique_vote,
12+
error: state.errors.max_votes_allowed,
13+
max_votes_allowed: state.data.max_votes_allowed,
14+
});
15+
16+
export const MultipleAnswersField = () => {
17+
const { t } = useTranslationContext();
18+
const { pollComposer } = useMessageComposer();
19+
const { enforce_unique_vote, error, max_votes_allowed } = useStateStore(
20+
pollComposer.state,
21+
pollComposerStateSelector,
22+
);
23+
return (
24+
<div
25+
className={clsx('str-chat__form__expandable-field', {
26+
'str-chat__form__expandable-field--expanded': !enforce_unique_vote,
27+
})}
28+
>
29+
<SimpleSwitchField
30+
checked={!enforce_unique_vote}
31+
id='enforce_unique_vote'
32+
labelText={t<string>('Multiple answers')}
33+
onChange={(e) => {
34+
pollComposer.updateFields({ enforce_unique_vote: !e.target.checked });
35+
}}
36+
/>
37+
{!enforce_unique_vote && (
38+
<div
39+
className={clsx('str-chat__form__input-field', {
40+
'str-chat__form__input-field--has-error': error,
41+
})}
42+
>
43+
<div className={clsx('str-chat__form__input-field__value')}>
44+
<FieldError
45+
className='str-chat__form__input-field__error'
46+
data-testid={'poll-max-votes-allowed-input-field-error'}
47+
text={error && t(error)}
48+
/>
49+
<input
50+
id='max_votes_allowed'
51+
onChange={(e) => {
52+
pollComposer.updateFields({
53+
max_votes_allowed: e.target.value,
54+
});
55+
}}
56+
placeholder={t<string>('Maximum number of votes (from 2 to 10)')}
57+
type='number'
58+
value={max_votes_allowed}
59+
/>
60+
</div>
61+
</div>
62+
)}
63+
</div>
64+
);
65+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
import clsx from 'clsx';
3+
import { FieldError } from '../../Form/FieldError';
4+
import { useTranslationContext } from '../../../context';
5+
import { useMessageComposer } from '../../MessageInput/hooks/messageComposer/useMessageComposer';
6+
import { useStateStore } from '../../../store';
7+
import type { PollComposerState } from 'stream-chat';
8+
9+
const pollComposerStateSelector = (state: PollComposerState) => ({
10+
error: state.errors.name,
11+
name: state.data.name,
12+
});
13+
14+
export const NameField = () => {
15+
const { t } = useTranslationContext();
16+
const { pollComposer } = useMessageComposer();
17+
const { error, name } = useStateStore(pollComposer.state, pollComposerStateSelector);
18+
return (
19+
<div
20+
className={clsx(
21+
'str-chat__form__field str-chat__form__input-field str-chat__form__input-field--with-label',
22+
{
23+
'str-chat__form__input-field--has-error': error,
24+
},
25+
)}
26+
>
27+
<label className='str-chat__form__field-label' htmlFor='name'>
28+
{t<string>('Question')}
29+
</label>
30+
<div className={clsx('str-chat__form__input-field__value')}>
31+
<FieldError
32+
className='str-chat__form__input-field__error'
33+
data-testid={'poll-name-input-field-error'}
34+
text={error && t(error)}
35+
/>
36+
<input
37+
id='name'
38+
onBlur={() => {
39+
pollComposer.handleFieldBlur('name');
40+
}}
41+
onChange={(e) => {
42+
pollComposer.updateFields({ name: e.target.value });
43+
}}
44+
placeholder={t<string>('Ask a question')}
45+
type='text'
46+
value={name}
47+
/>
48+
</div>
49+
</div>
50+
);
51+
};

src/components/Poll/PollCreationDialog/OptionFieldSet.tsx

Lines changed: 55 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,31 @@
11
import clsx from 'clsx';
2-
import { MAX_POLL_OPTIONS } from '../constants';
3-
import { nanoid } from 'nanoid';
42
import React, { useCallback } from 'react';
53
import { FieldError } from '../../Form/FieldError';
64
import { DragAndDropContainer } from '../../DragAndDrop/DragAndDropContainer';
75
import { useTranslationContext } from '../../../context';
8-
import type { OptionErrors, PollFormState, PollOptionFormData } from './types';
6+
import { useMessageComposer } from '../../MessageInput/hooks/messageComposer/useMessageComposer';
7+
import { useStateStore } from '../../../store';
8+
import type { PollComposerState } from 'stream-chat';
99

10-
const VALIDATION_ERRORS = { 'Option already exists': true } as const as Record<
11-
string,
12-
boolean
13-
>;
10+
const pollComposerStateSelector = (state: PollComposerState) => ({
11+
errors: state.errors.options,
12+
options: state.data.options,
13+
});
1414

15-
export type OptionFieldSetProps = {
16-
errors: OptionErrors;
17-
options: PollFormState['options'];
18-
setErrors: (fn: (prev: OptionErrors) => OptionErrors) => void;
19-
setState: (fn: (prev: PollFormState) => PollFormState) => void;
20-
};
21-
22-
export const OptionFieldSet = ({
23-
errors,
24-
options,
25-
setErrors,
26-
setState,
27-
}: OptionFieldSetProps) => {
15+
export const OptionFieldSet = () => {
16+
const { pollComposer } = useMessageComposer();
17+
const { errors, options } = useStateStore(
18+
pollComposer.state,
19+
pollComposerStateSelector,
20+
);
2821
const { t } = useTranslationContext('OptionFieldSet');
2922

30-
const findOptionDuplicate = (sourceOption: PollOptionFormData) => {
31-
const isDuplicateFilter = (option: PollOptionFormData) =>
32-
!!sourceOption.text.trim() && // do not include empty options into consideration
33-
option.id !== sourceOption.id &&
34-
option.text.trim() === sourceOption.text.trim();
35-
36-
return options.find(isDuplicateFilter);
37-
};
38-
3923
const onSetNewOrder = useCallback(
4024
(newOrder: number[]) => {
41-
setState((prev) => ({
42-
...prev,
43-
options: newOrder.map((index) => prev.options[index]),
44-
}));
25+
const prevOptions = pollComposer.options;
26+
pollComposer.updateFields({ options: newOrder.map((index) => prevOptions[index]) });
4527
},
46-
[setState],
28+
[pollComposer],
4729
);
4830

4931
const draggable = options.length > 1;
@@ -56,83 +38,47 @@ export const OptionFieldSet = ({
5638
draggable={draggable}
5739
onSetNewOrder={onSetNewOrder}
5840
>
59-
{options.map((option, i) => (
60-
<div
61-
className={clsx('str-chat__form__input-field', {
62-
'str-chat__form__input-field--draggable': draggable,
63-
'str-chat__form__input-field--has-error': errors[option.id],
64-
})}
65-
key={`new-poll-option-${i}`}
66-
>
67-
<div className='str-chat__form__input-field__value'>
68-
<FieldError
69-
className='str-chat__form__input-field__error'
70-
data-testid={'poll-option-input-field-error'}
71-
text={errors[option.id]}
72-
/>
73-
<input
74-
id={option.id}
75-
onBlur={(e) => {
76-
if (findOptionDuplicate({ id: e.target.id, text: e.target.value })) {
77-
setErrors((prev) => ({
78-
...prev,
79-
[e.target.id]: t<string>('Option already exists'),
80-
}));
81-
}
82-
}}
83-
onChange={(e) => {
84-
setState((prev) => {
85-
const shouldAddEmptyOption =
86-
prev.options.length < MAX_POLL_OPTIONS &&
87-
(!prev.options ||
88-
(prev.options.slice(i + 1).length === 0 && !!e.target.value));
89-
const shouldRemoveOption =
90-
prev.options &&
91-
prev.options.slice(i + 1).length > 0 &&
92-
!e.target.value;
93-
94-
const optionListHead = prev.options ? prev.options.slice(0, i) : [];
95-
const optionListTail = shouldAddEmptyOption
96-
? [{ id: nanoid(), text: '' }]
97-
: (prev.options || []).slice(i + 1);
98-
99-
if (
100-
(errors[option.id] && !e.target.value) ||
101-
(VALIDATION_ERRORS[errors[option.id]] &&
102-
!findOptionDuplicate({ id: e.target.id, text: e.target.value }))
103-
) {
104-
setErrors((prev) => {
105-
delete prev[option.id];
106-
return prev;
107-
});
41+
{options.map((option, i) => {
42+
const error = errors?.[option.id];
43+
return (
44+
<div
45+
className={clsx('str-chat__form__input-field', {
46+
'str-chat__form__input-field--draggable': draggable,
47+
'str-chat__form__input-field--has-error': error,
48+
})}
49+
key={`new-poll-option-${i}`}
50+
>
51+
<div className='str-chat__form__input-field__value'>
52+
<FieldError
53+
className='str-chat__form__input-field__error'
54+
data-testid={'poll-option-input-field-error'}
55+
text={error && t(error)}
56+
/>
57+
<input
58+
id={option.id}
59+
onBlur={() => {
60+
pollComposer.handleFieldBlur('options');
61+
}}
62+
onChange={(e) => {
63+
pollComposer.updateFields({
64+
options: { index: i, text: e.target.value },
65+
});
66+
}}
67+
onKeyUp={(event) => {
68+
if (event.key === 'Enter') {
69+
const nextInputId = options[i + 1].id;
70+
document.getElementById(nextInputId)?.focus();
10871
}
109-
110-
return {
111-
...prev,
112-
options: [
113-
...optionListHead,
114-
...(shouldRemoveOption
115-
? []
116-
: [{ ...option, text: e.target.value }]),
117-
...optionListTail,
118-
],
119-
};
120-
});
121-
}}
122-
onKeyUp={(event) => {
123-
if (event.key === 'Enter') {
124-
const nextInputId = options[i + 1].id;
125-
document.getElementById(nextInputId)?.focus();
126-
}
127-
}}
128-
placeholder={t<string>('Add an option')}
129-
type='text'
130-
value={option.text}
131-
/>
72+
}}
73+
placeholder={t<string>('Add an option')}
74+
type='text'
75+
value={option.text}
76+
/>
77+
</div>
78+
{draggable && <div className='str-chat__drag-handle' />}
13279
</div>
133-
{draggable && <div className='str-chat__drag-handle' />}
134-
</div>
135-
))}
80+
);
81+
})}
13682
</DragAndDropContainer>
13783
</fieldset>
13884
);

0 commit comments

Comments
 (0)