Skip to content

Commit d013500

Browse files
authored
feat(editor): ✨ add title section clear buttons (#23)
Add clear buttons for type, scope and gitmoji dropdowns so respective sections can be removed easily. Resolves #18
1 parent a2b4319 commit d013500

9 files changed

+320
-80
lines changed

components/common/searchable-menu.component.tsx

+44-34
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Col, Input, Row, Space, Typography } from 'antd';
1+
import { Col, Input, Row, Typography } from 'antd';
22
import classNames from 'classnames';
33
import React from 'react';
44
import { AiFillCloseCircle } from 'react-icons/ai';
@@ -33,6 +33,7 @@ const styles = (theme: CommitComposerTheme) => ({
3333
description: {
3434
fontSize: 12,
3535
paddingRight: 9,
36+
marginBottom: '0 !important',
3637
},
3738
searchRow: {},
3839
itemRow: {
@@ -61,6 +62,7 @@ type Props = WithStylesProps<typeof styles> & OwnProps & ReduxProps & DispatchPr
6162
export interface State {
6263
searchInputRef: React.RefObject<Input>;
6364
visibleItems: RenderedItem[];
65+
query: string;
6466
}
6567

6668
class SearchableMenuComponent extends React.Component<Props, State> {
@@ -69,14 +71,15 @@ class SearchableMenuComponent extends React.Component<Props, State> {
6971
this.state = {
7072
searchInputRef: React.createRef(),
7173
visibleItems: this.renderItems(props.items),
74+
query: '',
7275
};
7376
}
7477

7578
componentDidMount(): void {
7679
this.focus();
7780
}
7881

79-
componentDidUpdate(prevProps: Readonly<Props>): void {
82+
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): void {
8083
const { focus, items } = this.props;
8184

8285
if (!prevProps.focus && focus) {
@@ -85,7 +88,7 @@ class SearchableMenuComponent extends React.Component<Props, State> {
8588

8689
if (prevProps.items !== items) {
8790
this.setState({
88-
visibleItems: this.renderItems(items),
91+
visibleItems: this.renderItems(items, prevState.query),
8992
});
9093
}
9194
}
@@ -120,38 +123,40 @@ class SearchableMenuComponent extends React.Component<Props, State> {
120123
onSearch(query?: string): void {
121124
const { items } = this.props;
122125
const visibleItems = this.renderItems(items, query);
123-
this.setState({ visibleItems });
126+
this.setState({ visibleItems, query });
124127
}
125128

126129
onSelect(item: string): void {
127130
this.clear();
128131
this.props.onClick?.(item);
129132
}
130133

131-
renderItems(items: SearchableItem[], query?: string): RenderedItem[] {
134+
highlight(input: string, regex?: RegExp): { elem: JSX.Element; found: boolean } {
132135
const { classes } = this.props;
133136

134-
const highlight = (input: string, regex?: RegExp): { elem: JSX.Element; found: boolean } => {
135-
if (input === '' || input === undefined) {
136-
return;
137-
}
137+
if (input === '' || input === undefined) {
138+
return;
139+
}
138140

139-
const parts: React.ReactNode[] = input.split(regex);
140-
let found = false;
141+
const parts: React.ReactNode[] = input.split(regex);
142+
let found = false;
141143

142-
for (let i = 1; i < parts.length; i += 2) {
143-
parts[i] = (
144-
<span key={i} className={classes.highlight}>
145-
{parts[i]}
146-
</span>
147-
);
148-
found = true;
149-
}
150-
return {
151-
elem: <Typography.Text>{parts}</Typography.Text>,
152-
found,
153-
};
144+
for (let i = 1; i < parts.length; i += 2) {
145+
parts[i] = (
146+
<span key={i} className={classes.highlight}>
147+
{parts[i]}
148+
</span>
149+
);
150+
found = true;
151+
}
152+
return {
153+
elem: <>{parts}</>,
154+
found,
154155
};
156+
}
157+
158+
renderItems(items: SearchableItem[], query?: string): RenderedItem[] {
159+
const { classes } = this.props;
155160

156161
const escapeRegExp = (str: string): string => {
157162
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -160,18 +165,23 @@ class SearchableMenuComponent extends React.Component<Props, State> {
160165
return items
161166
.map((x) => {
162167
const titleRegex = query ? new RegExp(`(${escapeRegExp(query)})`, 'gi') : undefined;
163-
const title = highlight(x.title, titleRegex);
168+
const title = this.highlight(x.title, titleRegex);
164169
const descriptionRegex = query ? new RegExp(`(${escapeRegExp(query)})`, 'gi') : undefined;
165-
const description = highlight(x.description, descriptionRegex);
170+
const description = this.highlight(x.description, descriptionRegex);
166171

167172
const found =
168173
query === undefined || query === '' || title.found || (description && description.found);
169174

170175
if (description) {
171-
description.elem = React.cloneElement(description.elem, {
172-
type: 'secondary',
173-
className: classes.description,
174-
});
176+
description.elem = (
177+
<Typography.Paragraph type="secondary" className={classes.description} ellipsis>
178+
{description.elem}
179+
</Typography.Paragraph>
180+
);
181+
}
182+
183+
if (title) {
184+
title.elem = <Typography.Text>{title.elem}</Typography.Text>;
175185
}
176186

177187
return {
@@ -187,7 +197,7 @@ class SearchableMenuComponent extends React.Component<Props, State> {
187197

188198
render(): JSX.Element {
189199
const { classes, className, searchBarClassName, children } = this.props;
190-
const { visibleItems } = this.state;
200+
const { visibleItems, query } = this.state;
191201

192202
return (
193203
<div className={classNames(classes.root, className)}>
@@ -202,7 +212,7 @@ class SearchableMenuComponent extends React.Component<Props, State> {
202212
/>
203213
<span
204214
className={classNames('ant-input-suffix', {
205-
[classes.hidden]: !this.state.searchInputRef?.current?.input.value,
215+
[classes.hidden]: !query,
206216
})}>
207217
<span
208218
tabIndex={-1}
@@ -224,15 +234,15 @@ class SearchableMenuComponent extends React.Component<Props, State> {
224234
key={x.item}
225235
item={x.item}
226236
icon={x.icon}>
227-
<Space size={1} direction="vertical">
237+
<>
228238
{x.title}
229239
{x.description}
230-
</Space>
240+
</>
231241
</SearchableMenuItemComponent>
232242
))}
233243
</Col>
234244
</Row>
235-
<Row onClick={(e) => e.stopPropagation()}>
245+
<Row>
236246
<Col flex="auto">{children}</Col>
237247
</Row>
238248
</div>

components/editor/state/editor.epic.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { GitmojiSelectAction, TypeSelectAction } from 'components/preset/state/preset.action';
1+
import {
2+
GitmojiSelectAction,
3+
ScopeSelectAction,
4+
TypeSelectAction,
5+
} from 'components/preset/state/preset.action';
26
import { Epic } from 'redux-observable';
37
import { PlainAction } from 'redux-typed-actions';
48
import { concat, of } from 'rxjs';
@@ -15,6 +19,7 @@ export const updateValidation: Epic<PlainAction, PlainAction, AppState> = (actio
1519
EditorFormatAction.type,
1620
GitmojiSelectAction.type,
1721
TypeSelectAction.type,
22+
ScopeSelectAction.type,
1823
)
1924
.pipe(
2025
debounceTime(250),

components/preset/gitmoji-picker.component.tsx

+28-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button, Dropdown, Space, Switch, Typography } from 'antd';
1+
import { Button, Col, Dropdown, Row, Space, Switch, Typography } from 'antd';
22
import classNames from 'classnames';
33
import RecentListComponent, { RecentItem } from 'components/common/recent-list.component';
44
import SearchableMenuComponent from 'components/common/searchable-menu.component';
@@ -16,7 +16,7 @@ const styles = (theme: CommitComposerTheme) => ({
1616
border: `1px solid ${theme.lighter}`,
1717
display: 'block',
1818
[`@media only screen and (min-width: ${theme.screenMD})`]: {
19-
maxWidth: 380,
19+
maxWidth: 400,
2020
},
2121
},
2222
items: {
@@ -79,7 +79,6 @@ const styles = (theme: CommitComposerTheme) => ({
7979
},
8080
actionContainer: {
8181
padding: '4px 9px',
82-
justifyContent: 'flex-end',
8382
width: '100%',
8483
backgroundColor: theme.lighter,
8584
},
@@ -93,7 +92,7 @@ export interface ReduxProps {
9392
preset: PresetState;
9493
}
9594
export interface DispatchProps {
96-
gitmojiSelected: (gitmoji: GitmojiDefinition) => void;
95+
gitmojiSelected: (gitmoji: GitmojiDefinition | null) => void;
9796
toggleShortcode: (value: boolean) => void;
9897
}
9998
type Props = WithStylesProps<typeof styles> & OwnProps & ReduxProps & DispatchProps;
@@ -111,6 +110,11 @@ class GitmojiPickerComponent extends React.Component<Props, State> {
111110
};
112111
}
113112

113+
handleClear(): void {
114+
const { gitmojiSelected } = this.props;
115+
gitmojiSelected(null);
116+
}
117+
114118
handleClick(key: string): void {
115119
setTimeout(() => {
116120
const { gitmojiSelected } = this.props;
@@ -168,16 +172,25 @@ class GitmojiPickerComponent extends React.Component<Props, State> {
168172
</span>
169173
),
170174
}))}>
171-
<Space className={classes.actionContainer}>
172-
<Typography.Text className={classes.actionText}>Shortcode:</Typography.Text>
173-
<Switch
174-
size="small"
175-
defaultChecked={preset.useShortcode}
176-
checkedChildren={<AiOutlineCheck />}
177-
unCheckedChildren={<AiOutlineClose />}
178-
onChange={(x) => toggleShortcode(x)}
179-
/>
180-
</Space>
175+
<Row justify="space-between" className={classes.actionContainer}>
176+
<Col>
177+
<Button size="small" type="link" onClick={() => this.handleClear()}>
178+
Clear
179+
</Button>
180+
</Col>
181+
<Col>
182+
<Space>
183+
<Typography.Text className={classes.actionText}>Shortcode:</Typography.Text>
184+
<Switch
185+
size="small"
186+
defaultChecked={preset.useShortcode}
187+
checkedChildren={<AiOutlineCheck />}
188+
unCheckedChildren={<AiOutlineClose />}
189+
onChange={(x) => toggleShortcode(x)}
190+
/>
191+
</Space>
192+
</Col>
193+
</Row>
181194
</SearchableMenuComponent>
182195
</span>
183196
);
@@ -186,6 +199,7 @@ class GitmojiPickerComponent extends React.Component<Props, State> {
186199
<Dropdown
187200
overlayClassName={classes.overlay}
188201
overlay={menu}
202+
visible={visible}
189203
onVisibleChange={(visible) => this.handleVisibilityChange(visible)}
190204
trigger={['click']}
191205
placement="bottomRight">

components/preset/scope-picker.component.tsx

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button, Dropdown } from 'antd';
1+
import { Button, Col, Dropdown, Row } from 'antd';
22
import classNames from 'classnames';
33
import RecentListComponent, { RecentItem } from 'components/common/recent-list.component';
44
import SearchableMenuComponent from 'components/common/searchable-menu.component';
@@ -16,7 +16,7 @@ const styles = (theme: CommitComposerTheme) => ({
1616
border: `1px solid ${theme.lighter}`,
1717
display: 'block',
1818
[`@media only screen and (min-width: ${theme.screenMD})`]: {
19-
maxWidth: 585,
19+
maxWidth: 400,
2020
},
2121
},
2222
items: {
@@ -64,6 +64,11 @@ const styles = (theme: CommitComposerTheme) => ({
6464
backgroundColor: theme.itemHoverBG,
6565
},
6666
},
67+
actionContainer: {
68+
padding: '4px 9px',
69+
width: '100%',
70+
backgroundColor: theme.lighter,
71+
},
6772
});
6873

6974
export interface OwnProps {}
@@ -88,6 +93,11 @@ class ScopePickerComponent extends React.Component<Props, State> {
8893
};
8994
}
9095

96+
handleClear(): void {
97+
const { scopeSelected } = this.props;
98+
scopeSelected(null);
99+
}
100+
91101
handleClick(key: string): void {
92102
setTimeout(() => {
93103
const { scopeSelected } = this.props;
@@ -132,15 +142,23 @@ class ScopePickerComponent extends React.Component<Props, State> {
132142
items={preset.scopes.map((x) => ({
133143
item: x,
134144
title: `(${x})`,
135-
}))}
136-
/>
145+
}))}>
146+
<Row justify="space-between" className={classes.actionContainer}>
147+
<Col>
148+
<Button size="small" type="link" onClick={() => this.handleClear()}>
149+
Clear
150+
</Button>
151+
</Col>
152+
</Row>
153+
</SearchableMenuComponent>
137154
</span>
138155
);
139156

140157
return preset.scopes.length || preset.recentScopes.length ? (
141158
<Dropdown
142159
overlayClassName={classes.overlay}
143160
overlay={menu}
161+
visible={visible}
144162
onVisibleChange={(visible) => this.handleVisibilityChange(visible)}
145163
trigger={['click']}
146164
placement="bottomRight">

components/preset/state/preset.action.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { defineAction } from 'redux-typed-actions';
22
import { GitmojiDefinition } from 'shared/presets/gitmojis';
33
import { TypeDefinition } from 'shared/presets/types';
44

5-
export const GitmojiSelectAction = defineAction<GitmojiDefinition>('GitmojiSelectAction');
6-
export const TypeSelectAction = defineAction<TypeDefinition>('TypeSelectAction');
5+
export const GitmojiSelectAction = defineAction<GitmojiDefinition | null>('GitmojiSelectAction');
6+
export const TypeSelectAction = defineAction<TypeDefinition | null>('TypeSelectAction');
77
export const ToggleShortcodeAction = defineAction<boolean>('ToggleShortcodeAction');
8-
export const ScopeSelectAction = defineAction<string>('ScopeSelectAction');
8+
export const ScopeSelectAction = defineAction<string | null>('ScopeSelectAction');

components/preset/state/preset.reducer.ts

+18-12
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,28 @@ const presetReducer = (
1818
): PresetState => {
1919
if (GitmojiSelectAction.is(action)) {
2020
const { payload } = action;
21-
const map = state.recentGitmojis.map((x) => ({ key: x.shortcode, value: x }));
22-
const cache = new LRUCache<GitmojiDefinition>(map, 20);
23-
cache.write(payload.shortcode, payload);
24-
state.recentGitmojis = cache.toArray();
21+
if (payload) {
22+
const map = state.recentGitmojis.map((x) => ({ key: x.shortcode, value: x }));
23+
const cache = new LRUCache<GitmojiDefinition>(map, 20);
24+
cache.write(payload.shortcode, payload);
25+
state.recentGitmojis = cache.toArray();
26+
}
2527
} else if (TypeSelectAction.is(action)) {
2628
const { payload } = action;
27-
const map = state.recentTypes.map((x) => ({ key: x.key, value: x }));
28-
const cache = new LRUCache<TypeDefinition>(map, 20);
29-
cache.write(payload.key, payload);
30-
state.recentTypes = cache.toArray();
29+
if (payload) {
30+
const map = state.recentTypes.map((x) => ({ key: x.key, value: x }));
31+
const cache = new LRUCache<TypeDefinition>(map, 20);
32+
cache.write(payload.key, payload);
33+
state.recentTypes = cache.toArray();
34+
}
3135
} else if (ScopeSelectAction.is(action)) {
3236
const { payload } = action;
33-
const map = state.recentScopes.map((x) => ({ key: x, value: x }));
34-
const cache = new LRUCache<string>(map, 20);
35-
cache.write(payload, payload);
36-
state.recentScopes = cache.toArray();
37+
if (payload) {
38+
const map = state.recentScopes.map((x) => ({ key: x, value: x }));
39+
const cache = new LRUCache<string>(map, 20);
40+
cache.write(payload, payload);
41+
state.recentScopes = cache.toArray();
42+
}
3743
} else if (ToggleShortcodeAction.is(action)) {
3844
state.useShortcode = action.payload;
3945
} else if (RulesetParseAsync.success.is(action)) {

0 commit comments

Comments
 (0)