Skip to content

Commit 02fcfa2

Browse files
Fix some submit button issues (#2487)
* Prevent PostForm submit button spam * Keep CreatePost PostForm visible during submission * Keep PostListing PostForm visible during submission * Keep PostForm navigation warning enabled during submission * Remove `finished` from MarkdownTextAreaProps * Handle CommentForm submission failures * Keep CommentForm navigation warning enabled during submission * Handle PrivateMessageForm submission failures * Bypass navigation warning for successful CreatePrivateMessage * Fix absolute import, add eslint rule * Cleaner handleCommentSubmit --------- Co-authored-by: SleeplessOne1917 <[email protected]>
1 parent 14ae45f commit 02fcfa2

19 files changed

+270
-251
lines changed

eslint.config.mjs

+11
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ export default [
7979
"unicorn/filename-case": 0,
8080
"jsx-a11y/media-has-caption": 0,
8181
"jsx-a11y/label-has-associated-control": 0,
82+
"no-restricted-imports": [
83+
"error",
84+
{
85+
patterns: [
86+
{
87+
group: ["assets/*", "client/*", "server/*", "shared/*"],
88+
message: "Use relative import instead.",
89+
},
90+
],
91+
},
92+
],
8293
},
8394
},
8495
];

src/shared/components/comment/comment-form.tsx

+34-23
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,33 @@ import { capitalizeFirstLetter } from "@utils/helpers";
22
import { Component } from "inferno";
33
import { T } from "inferno-i18next-dess";
44
import { Link } from "inferno-router";
5-
import { CreateComment, EditComment, Language } from "lemmy-js-client";
5+
import {
6+
CommentResponse,
7+
CreateComment,
8+
EditComment,
9+
Language,
10+
} from "lemmy-js-client";
611
import { CommentNodeI } from "../../interfaces";
712
import { I18NextService, UserService } from "../../services";
813
import { Icon } from "../common/icon";
914
import { MarkdownTextArea } from "../common/markdown-textarea";
15+
import { RequestState } from "../../services/HttpService";
1016

1117
interface CommentFormProps {
1218
/**
1319
* Can either be the parent, or the editable comment. The right side is a postId.
1420
*/
1521
node: CommentNodeI | number;
16-
finished?: boolean;
1722
edit?: boolean;
1823
disabled?: boolean;
1924
focus?: boolean;
2025
onReplyCancel?(): void;
2126
allLanguages: Language[];
2227
siteLanguages: number[];
2328
containerClass?: string;
24-
onUpsertComment(form: EditComment | CreateComment): void;
29+
onUpsertComment(
30+
form: EditComment | CreateComment,
31+
): Promise<RequestState<CommentResponse>>;
2532
}
2633

2734
export class CommentForm extends Component<CommentFormProps, any> {
@@ -50,7 +57,6 @@ export class CommentForm extends Component<CommentFormProps, any> {
5057
initialContent={initialContent}
5158
showLanguage
5259
buttonTitle={this.buttonTitle}
53-
finished={this.props.finished}
5460
replyType={typeof this.props.node !== "number"}
5561
focus={this.props.focus}
5662
disabled={this.props.disabled}
@@ -83,33 +89,38 @@ export class CommentForm extends Component<CommentFormProps, any> {
8389
: capitalizeFirstLetter(I18NextService.i18n.t("reply"));
8490
}
8591

86-
handleCommentSubmit(content: string, language_id?: number) {
92+
async handleCommentSubmit(
93+
content: string,
94+
language_id?: number,
95+
): Promise<boolean> {
8796
const { node, onUpsertComment, edit } = this.props;
97+
let response: RequestState<CommentResponse>;
98+
8899
if (typeof node === "number") {
89100
const post_id = node;
90-
onUpsertComment({
101+
response = await onUpsertComment({
91102
content,
92103
post_id,
93104
language_id,
94105
});
106+
} else if (edit) {
107+
const comment_id = node.comment_view.comment.id;
108+
response = await onUpsertComment({
109+
content,
110+
comment_id,
111+
language_id,
112+
});
95113
} else {
96-
if (edit) {
97-
const comment_id = node.comment_view.comment.id;
98-
onUpsertComment({
99-
content,
100-
comment_id,
101-
language_id,
102-
});
103-
} else {
104-
const post_id = node.comment_view.post.id;
105-
const parent_id = node.comment_view.comment.id;
106-
this.props.onUpsertComment({
107-
content,
108-
parent_id,
109-
post_id,
110-
language_id,
111-
});
112-
}
114+
const post_id = node.comment_view.post.id;
115+
const parent_id = node.comment_view.comment.id;
116+
response = await onUpsertComment({
117+
content,
118+
parent_id,
119+
post_id,
120+
language_id,
121+
});
113122
}
123+
124+
return response.state !== "failed";
114125
}
115126
}

src/shared/components/comment/comment-node.tsx

+25-24
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { colorList, getCommentParentId } from "@utils/app";
22
import { futureDaysToUnixTime, numToSI } from "@utils/helpers";
33
import classNames from "classnames";
44
import { isBefore, parseISO, subMinutes } from "date-fns";
5-
import { Component, InfernoNode, linkEvent } from "inferno";
5+
import { Component, linkEvent } from "inferno";
66
import { Link } from "inferno-router";
77
import {
88
AddAdmin,
@@ -33,7 +33,6 @@ import {
3333
SaveComment,
3434
TransferCommunity,
3535
} from "lemmy-js-client";
36-
import deepEqual from "lodash.isequal";
3736
import { commentTreeMaxDepth } from "../../config";
3837
import {
3938
CommentNodeI,
@@ -87,7 +86,6 @@ interface CommentNodeProps {
8786
allLanguages: Language[];
8887
siteLanguages: number[];
8988
hideImages?: boolean;
90-
finished: Map<CommentId, boolean | undefined>;
9189
onSaveComment(form: SaveComment): Promise<void>;
9290
onCommentReplyRead(form: MarkCommentReplyAsRead): void;
9391
onPersonMentionRead(form: MarkPersonMentionAsRead): void;
@@ -139,6 +137,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
139137
super(props, context);
140138

141139
this.handleReplyCancel = this.handleReplyCancel.bind(this);
140+
this.handleCreateComment = this.handleCreateComment.bind(this);
141+
this.handleEditComment = this.handleEditComment.bind(this);
142142
this.handleReportComment = this.handleReportComment.bind(this);
143143
this.handleRemoveComment = this.handleRemoveComment.bind(this);
144144
this.handleReplyClick = this.handleReplyClick.bind(this);
@@ -164,22 +164,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
164164
return this.commentView.comment.id;
165165
}
166166

167-
componentWillReceiveProps(
168-
nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>,
169-
): void {
170-
if (!deepEqual(this.props, nextProps)) {
171-
this.setState({
172-
showEdit: false,
173-
showAdvanced: false,
174-
createOrEditCommentLoading: false,
175-
upvoteLoading: false,
176-
downvoteLoading: false,
177-
readLoading: false,
178-
fetchChildrenLoading: false,
179-
});
180-
}
181-
}
182-
183167
render() {
184168
const node = this.props.node;
185169
const cv = this.commentView;
@@ -283,12 +267,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
283267
edit
284268
onReplyCancel={this.handleReplyCancel}
285269
disabled={this.props.locked}
286-
finished={this.props.finished.get(id)}
287270
focus
288271
allLanguages={this.props.allLanguages}
289272
siteLanguages={this.props.siteLanguages}
290273
containerClass="comment-comment-container"
291-
onUpsertComment={this.props.onEditComment}
274+
onUpsertComment={this.handleEditComment}
292275
/>
293276
)}
294277
{!this.state.showEdit && !this.state.collapsed && (
@@ -425,12 +408,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
425408
node={node}
426409
onReplyCancel={this.handleReplyCancel}
427410
disabled={this.props.locked}
428-
finished={this.props.finished.get(id)}
429411
focus
430412
allLanguages={this.props.allLanguages}
431413
siteLanguages={this.props.siteLanguages}
432414
containerClass="comment-comment-container"
433-
onUpsertComment={this.props.onCreateComment}
415+
onUpsertComment={this.handleCreateComment}
434416
/>
435417
)}
436418
{!this.state.collapsed && node.children.length > 0 && (
@@ -447,7 +429,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
447429
hideImages={this.props.hideImages}
448430
isChild={!this.props.isTopLevel}
449431
depth={this.props.node.depth + 1}
450-
finished={this.props.finished}
451432
onCommentReplyRead={this.props.onCommentReplyRead}
452433
onPersonMentionRead={this.props.onPersonMentionRead}
453434
onCreateComment={this.props.onCreateComment}
@@ -559,6 +540,26 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
559540
this.setState({ showReply: false, showEdit: false });
560541
}
561542

543+
async handleCreateComment(
544+
form: CreateComment,
545+
): Promise<RequestState<CommentResponse>> {
546+
const res = await this.props.onCreateComment(form);
547+
if (res.state !== "failed") {
548+
this.setState({ showReply: false, showEdit: false });
549+
}
550+
return res;
551+
}
552+
553+
async handleEditComment(
554+
form: EditComment,
555+
): Promise<RequestState<CommentResponse>> {
556+
const res = await this.props.onEditComment(form);
557+
if (res.state !== "failed") {
558+
this.setState({ showReply: false, showEdit: false });
559+
}
560+
return res;
561+
}
562+
562563
isPersonMentionType(item: CommentNodeView): item is PersonMentionView {
563564
return item.person_mention?.id !== undefined;
564565
}

src/shared/components/comment/comment-nodes.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
BanFromCommunity,
88
BanPerson,
99
BlockPerson,
10-
CommentId,
1110
CommentResponse,
1211
CommunityModeratorView,
1312
CreateComment,
@@ -52,7 +51,6 @@ interface CommentNodesProps {
5251
hideImages?: boolean;
5352
isChild?: boolean;
5453
depth?: number;
55-
finished: Map<CommentId, boolean | undefined>;
5654
onSaveComment(form: SaveComment): Promise<void>;
5755
onCommentReplyRead(form: MarkCommentReplyAsRead): void;
5856
onPersonMentionRead(form: MarkPersonMentionAsRead): void;
@@ -124,7 +122,6 @@ export class CommentNodes extends Component<CommentNodesProps, any> {
124122
hideImages={this.props.hideImages}
125123
onCommentReplyRead={this.props.onCommentReplyRead}
126124
onPersonMentionRead={this.props.onPersonMentionRead}
127-
finished={this.props.finished}
128125
onCreateComment={this.props.onCreateComment}
129126
onEditComment={this.props.onEditComment}
130127
onCommentVote={this.props.onCommentVote}

src/shared/components/comment/comment-report.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ export class CommentReport extends Component<
9090
siteLanguages={[]}
9191
hideImages
9292
// All of these are unused, since its viewonly
93-
finished={new Map()}
9493
onSaveComment={async () => {}}
9594
onBlockPerson={async () => {}}
9695
onDeleteComment={async () => {}}

src/shared/components/common/markdown-textarea.tsx

+10-28
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { numToSI, randomStr } from "@utils/helpers";
33
import autosize from "autosize";
44
import classNames from "classnames";
55
import { NoOptionI18nKeys } from "i18next";
6-
import { Component, InfernoNode, linkEvent } from "inferno";
6+
import { Component, linkEvent } from "inferno";
77
import { Prompt } from "inferno-router";
88
import { Language } from "lemmy-js-client";
99
import {
@@ -41,15 +41,14 @@ interface MarkdownTextAreaProps {
4141
replyType?: boolean;
4242
focus?: boolean;
4343
disabled?: boolean;
44-
finished?: boolean;
4544
/**
4645
* Whether to show the language selector
4746
*/
4847
showLanguage?: boolean;
4948
hideNavigationWarnings?: boolean;
5049
onContentChange?(val: string): void;
5150
onReplyCancel?(): void;
52-
onSubmit?(content: string, languageId?: number): void;
51+
onSubmit?(content: string, languageId?: number): Promise<boolean>;
5352
allLanguages: Language[]; // TODO should probably be nullable
5453
siteLanguages: number[]; // TODO same
5554
}
@@ -115,27 +114,6 @@ export class MarkdownTextArea extends Component<
115114
}
116115
}
117116

118-
componentWillReceiveProps(
119-
nextProps: MarkdownTextAreaProps & { children?: InfernoNode },
120-
) {
121-
if (nextProps.finished) {
122-
this.setState({
123-
previewMode: false,
124-
imageUploadStatus: undefined,
125-
loading: false,
126-
content: undefined,
127-
});
128-
if (this.props.replyType) {
129-
this.props.onReplyCancel?.();
130-
}
131-
132-
const textarea: any = document.getElementById(this.id);
133-
const form: any = document.getElementById(this.formId);
134-
form.reset();
135-
setTimeout(() => autosize.update(textarea), 10);
136-
}
137-
}
138-
139117
render() {
140118
const languageId = this.state.languageId;
141119

@@ -149,8 +127,8 @@ export class MarkdownTextArea extends Component<
149127
message={I18NextService.i18n.t("block_leaving")}
150128
when={
151129
!this.props.hideNavigationWarnings &&
152-
!!this.state.content &&
153-
!this.state.submitted
130+
((!!this.state.content && !this.state.submitted) ||
131+
this.state.loading)
154132
}
155133
/>
156134
<div className="mb-3 row">
@@ -575,11 +553,15 @@ export class MarkdownTextArea extends Component<
575553
this.setState({ languageId: val[0] });
576554
}
577555

578-
handleSubmit(i: MarkdownTextArea, event: any) {
556+
async handleSubmit(i: MarkdownTextArea, event: any) {
579557
event.preventDefault();
580558
if (i.state.content) {
581559
i.setState({ loading: true, submitted: true });
582-
i.props.onSubmit?.(i.state.content, i.state.languageId);
560+
const success = await i.props.onSubmit?.(
561+
i.state.content,
562+
i.state.languageId,
563+
);
564+
i.setState({ loading: false, submitted: success ?? true });
583565
}
584566
}
585567

0 commit comments

Comments
 (0)