Skip to content

Commit 16dbe6b

Browse files
committed
Add support for custom rule names
1 parent c635fd2 commit 16dbe6b

File tree

4 files changed

+181
-19
lines changed

4 files changed

+181
-19
lines changed

src/components/mock/mock-rule-row.tsx

Lines changed: 105 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313

1414
import { styled, css } from '../../styles';
1515
import { Icon } from '../../icons';
16+
import { UnreachableCheck } from '../../util/error';
1617

1718
import { getMethodColor, getSummaryColour } from '../../model/events/categorization';
1819
import {
@@ -54,7 +55,7 @@ import { HandlerSelector } from './handler-selection';
5455
import { HandlerConfiguration } from './handler-config';
5556
import { DragHandle } from './mock-drag-handle';
5657
import { IconMenu, IconMenuButton } from './mock-item-menu';
57-
import { UnreachableCheck } from '../../util/error';
58+
import { RuleTitle, EditableRuleTitle } from './mock-rule-title';
5859

5960
const RowContainer = styled(LittleCard)<{
6061
deactivated?: boolean,
@@ -73,6 +74,7 @@ const RowContainer = styled(LittleCard)<{
7374
}
7475
7576
display: flex;
77+
flex-wrap: wrap;
7678
flex-direction: row;
7779
align-items: center;
7880
justify-content: space-between;
@@ -196,13 +198,15 @@ const RuleMenu = (p: {
196198
isCollapsed: boolean,
197199
isNewRule: boolean,
198200
hasUnsavedChanges: boolean,
199-
onToggleCollapse: (event: React.MouseEvent) => void,
200-
onSave: (event: React.MouseEvent) => void,
201-
onReset: (event: React.MouseEvent) => void,
202-
onClone: (event: React.MouseEvent) => void,
201+
isEditingTitle: boolean,
202+
onSetCustomTitle: (event: React.UIEvent) => void,
203+
onToggleCollapse: (event: React.UIEvent) => void,
204+
onSave: (event: React.UIEvent) => void,
205+
onReset: (event: React.UIEvent) => void,
206+
onClone: (event: React.UIEvent) => void,
203207
toggleState: boolean,
204-
onToggleActivation: (event: React.MouseEvent) => void,
205-
onDelete: (event: React.MouseEvent) => void,
208+
onToggleActivation: (event: React.UIEvent) => void,
209+
onDelete: (event: React.UIEvent) => void,
206210
}) => <RuleMenuContainer topOffset={7}>
207211
<IconMenuButton
208212
title='Delete this rule'
@@ -219,6 +223,12 @@ const RuleMenu = (p: {
219223
icon={['fas', p.toggleState ? 'toggle-on' : 'toggle-off']}
220224
onClick={p.onToggleActivation}
221225
/>
226+
<IconMenuButton
227+
title='Give this rule a custom name'
228+
icon={['fas', 'edit']}
229+
disabled={p.isEditingTitle}
230+
onClick={p.onSetCustomTitle}
231+
/>
222232
<IconMenuButton
223233
title='Revert this rule to the last saved version'
224234
icon={['fas', 'undo']}
@@ -291,6 +301,9 @@ export class RuleRow extends React.Component<{
291301
initialMatcherSelect = React.createRef<HTMLSelectElement>();
292302
containerRef: HTMLElement | null = null;
293303

304+
@observable
305+
private titleEditState: undefined | { originalTitle?: string };
306+
294307
render() {
295308
const {
296309
index,
@@ -337,6 +350,10 @@ export class RuleRow extends React.Component<{
337350
? [rule.handler]
338351
: rule.steps;
339352

353+
// We show the summary by default, but if you set a custom title, we only show it when expanded:
354+
const shouldShowSummary = !collapsed || (!rule.title && !this.titleEditState);
355+
const isEditingTitle = !!this.titleEditState && !collapsed;
356+
340357
return <Draggable
341358
draggableId={rule.id}
342359
index={index}
@@ -369,13 +386,36 @@ export class RuleRow extends React.Component<{
369386
onToggleActivation={this.toggleActivation}
370387
onClone={this.cloneRule}
371388
onDelete={this.deleteRule}
389+
isEditingTitle={isEditingTitle}
390+
onSetCustomTitle={this.startEnteringCustomTitle}
372391
/>
373392
<DragHandle {...provided.dragHandleProps} />
374393

394+
{ rule.title && !isEditingTitle &&
395+
<RuleTitle>
396+
{ rule.title }
397+
</RuleTitle>
398+
}
399+
400+
{ isEditingTitle &&
401+
<EditableRuleTitle
402+
value={rule.title || ''}
403+
onEditTitle={this.editTitle}
404+
onSave={this.saveRule}
405+
onCancel={
406+
this.titleEditState!.originalTitle !== this.props.rule.title
407+
? this.cancelEditingTitle
408+
: undefined
409+
}
410+
/>
411+
}
412+
375413
<MatcherOrHandler>
376-
<Summary collapsed={collapsed} title={summarizeMatcher(rule)}>
377-
{ summarizeMatcher(rule) }
378-
</Summary>
414+
{ shouldShowSummary &&
415+
<Summary collapsed={collapsed} title={summarizeMatcher(rule)}>
416+
{ summarizeMatcher(rule) }
417+
</Summary>
418+
}
379419

380420
{
381421
!collapsed && <Details>
@@ -411,12 +451,17 @@ export class RuleRow extends React.Component<{
411451
}
412452
</MatcherOrHandler>
413453

414-
<ArrowIcon />
454+
455+
{ shouldShowSummary &&
456+
<ArrowIcon />
457+
}
415458

416459
<MatcherOrHandler>
417-
<Summary collapsed={collapsed} title={ summarizeHandler(rule) }>
418-
{ summarizeHandler(rule) }
419-
</Summary>
460+
{ shouldShowSummary &&
461+
<Summary collapsed={collapsed} title={ summarizeHandler(rule) }>
462+
{ summarizeHandler(rule) }
463+
</Summary>
464+
}
420465

421466
{
422467
!collapsed && <Details>
@@ -443,13 +488,19 @@ export class RuleRow extends React.Component<{
443488
}</Observer>}</Draggable>;
444489
}
445490

446-
saveRule = noPropagation(() => this.props.saveRule(this.props.path));
447-
resetRule = noPropagation(() => this.props.resetRule(this.props.path));
491+
saveRule = noPropagation(() => {
492+
this.stopEditingTitle();
493+
this.props.saveRule(this.props.path);
494+
});
495+
resetRule = noPropagation(() => {
496+
this.stopEditingTitle();
497+
this.props.resetRule(this.props.path);
498+
});
448499
deleteRule = noPropagation(() => this.props.deleteRule(this.props.path));
449500
cloneRule = noPropagation(() => this.props.cloneRule(this.props.path));
450501

451502
@action.bound
452-
toggleActivation(event: React.MouseEvent) {
503+
toggleActivation(event: React.UIEvent) {
453504
const { rule } = this.props;
454505
rule.activated = !rule.activated;
455506
event.stopPropagation();
@@ -466,10 +517,13 @@ export class RuleRow extends React.Component<{
466517
}
467518
if (this.initialMatcherSelect.current) {
468519
this.initialMatcherSelect.current.focus();
520+
// Clear selection too (sometimes clicking fast selects the rule title)
521+
getSelection()?.empty();
469522
}
470523
});
471524

472525
this.props.toggleRuleCollapsed(this.props.rule.id);
526+
this.stopEditingTitle();
473527
});
474528

475529
@action.bound
@@ -554,6 +608,40 @@ export class RuleRow extends React.Component<{
554608
}
555609
}
556610
}
611+
612+
@action.bound
613+
startEnteringCustomTitle(event: React.UIEvent) {
614+
this.titleEditState = { originalTitle: this.props.rule.title };
615+
// We expand the row, but not with toggleCollapsed, because we don't want
616+
// to auto-focus the matcher like normal:
617+
if (this.props.collapsed) this.props.toggleRuleCollapsed(this.props.rule.id);
618+
event.stopPropagation();
619+
}
620+
621+
@action.bound
622+
editTitle(newTitle: string | undefined) {
623+
this.props.rule.title = newTitle || undefined;
624+
}
625+
626+
@action.bound
627+
cancelEditingTitle() {
628+
if (!this.titleEditState) return;
629+
this.editTitle(this.titleEditState.originalTitle);
630+
this.titleEditState = undefined;
631+
}
632+
633+
@action.bound
634+
stopEditingTitle() {
635+
if (!this.titleEditState) return;
636+
637+
// Trim titles on save, so that e.g. space-only titles are dropped:
638+
if (this.props.rule.title !== this.titleEditState.originalTitle) {
639+
this.props.rule.title = this.props.rule.title?.trim() || undefined;
640+
}
641+
642+
this.titleEditState = undefined;
643+
}
644+
557645
}
558646

559647
@observer
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as React from 'react';
2+
3+
import { styled } from '../../styles';
4+
import { noPropagation } from '../component-utils';
5+
6+
import { TextInput } from '../common/inputs';
7+
import { IconMenuButton } from './mock-item-menu';
8+
9+
export const RuleTitle = styled.h2`
10+
white-space: nowrap;
11+
overflow: hidden;
12+
text-overflow: ellipsis;
13+
14+
flex-basis: 100%;
15+
width: 100%;
16+
box-sizing: border-box;
17+
18+
/* Required to avoid overflow trimming hanging chars */
19+
padding: 5px;
20+
margin: -5px;
21+
22+
font-weight: bold;
23+
`;
24+
25+
const EditableTitleContainer = styled.div`
26+
flex-basis: 100%;
27+
margin: -4px;
28+
`;
29+
30+
const TitleInput = styled(TextInput)`
31+
width: 30%;
32+
margin-right: 5px;
33+
margin-bottom: 10px;
34+
`;
35+
36+
const TitleEditButton = styled(IconMenuButton)`
37+
font-size: 1em;
38+
padding: 0;
39+
vertical-align: middle;
40+
`;
41+
42+
export const EditableRuleTitle = (p: {
43+
value: string,
44+
onEditTitle: (newValue: string) => void,
45+
onSave: (event: React.UIEvent) => void,
46+
onCancel?: () => void
47+
}) => {
48+
const editTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
49+
p.onEditTitle(e.target.value)
50+
};
51+
52+
return <EditableTitleContainer>
53+
<TitleInput
54+
autoFocus
55+
value={p.value}
56+
placeholder='A custom name for this rule'
57+
onChange={editTitle}
58+
onClick={(e) => e.stopPropagation()}
59+
onKeyPress={(e) => {
60+
if (e.key === 'Enter') p.onSave(e);
61+
}}
62+
/>
63+
<TitleEditButton
64+
title="Reset changes to rule name"
65+
icon={['fas', 'undo']}
66+
disabled={!p.onCancel}
67+
onClick={noPropagation(p.onCancel ?? (() => {}))}
68+
/>
69+
</EditableTitleContainer>;
70+
};

src/model/rules/rule-serialization.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const MockRuleSerializer = serializr.custom(
7373
return {
7474
id: data.id,
7575
type: data.type,
76+
title: data.title,
7677
activated: data.activated,
7778
matchers: data.matchers.map((m) =>
7879
deserializeByType(m, MatcherLookup, context.args)

src/model/rules/rules.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,12 +434,15 @@ export const isPaidHandlerClass = (
434434

435435
/// --- Rules ---
436436

437-
export type HtkMockRule =
437+
export type HtkMockRule = (
438438
| HttpMockRule
439439
| WebSocketMockRule
440440
| EthereumMockRule
441441
| IpfsMockRule
442-
| RTCMockRule;
442+
| RTCMockRule
443+
) & {
444+
title?: string;
445+
};
443446

444447
export type RuleType = HtkMockRule['type'];
445448

0 commit comments

Comments
 (0)