datocms-plugin-record-comments 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/index.html DELETED
@@ -1,11 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- </head>
7
- <body>
8
- <noscript>You need to enable JavaScript to run this app.</noscript>
9
- <div id="root"></div>
10
- </body>
11
- </html>
package/public/robots.txt DELETED
@@ -1,3 +0,0 @@
1
- # https://www.robotstxt.org/robotstxt.html
2
- User-agent: *
3
- Disallow:
@@ -1,347 +0,0 @@
1
- import { RenderItemFormSidebarPanelCtx } from 'datocms-plugin-sdk';
2
- import type { SimpleSchemaTypes } from '@datocms/cma-client-node';
3
- import { Button, Canvas } from 'datocms-react-ui';
4
- import { useEffect, useState } from 'react';
5
- import Comment from './components/Comment';
6
- import styles from './styles/commentbar.module.css';
7
-
8
- type PropTypes = {
9
- ctx: RenderItemFormSidebarPanelCtx;
10
- };
11
-
12
- export type CommentType = {
13
- dateISO: string;
14
- comment: string;
15
- author: {
16
- name: string;
17
- email: string;
18
- };
19
- usersWhoUpvoted: string[];
20
- replies?: CommentType[];
21
- parentCommentISO?: string;
22
- };
23
-
24
- // Regular DatoCMS user
25
- type RegularUser = {
26
- type: 'user';
27
- id: string;
28
- attributes: {
29
- email: string;
30
- first_name: string;
31
- last_name: string;
32
- };
33
- };
34
-
35
- // SSO user from external provider
36
- type SsoUser = {
37
- type: 'sso_user';
38
- id: string;
39
- attributes: {
40
- email: string;
41
- name: string;
42
- };
43
- };
44
-
45
- // Account user (personal account)
46
- type Account = {
47
- type: 'account';
48
- id: string;
49
- attributes: {
50
- email: string;
51
- name: string;
52
- };
53
- };
54
-
55
- // Organization user
56
- type Organization = {
57
- type: 'organization';
58
- id: string;
59
- attributes: {
60
- name: string;
61
- };
62
- };
63
-
64
- const isRegularUser = (user: any): user is RegularUser => {
65
- return (
66
- user?.type === 'user' &&
67
- 'attributes' in user &&
68
- 'email' in user.attributes &&
69
- 'full_name' in user.attributes
70
- );
71
- };
72
-
73
- const isSsoUser = (user: any): user is SsoUser => {
74
- return (
75
- user?.type === 'sso_user' &&
76
- 'attributes' in user &&
77
- 'email' in user.attributes &&
78
- 'name' in user.attributes
79
- );
80
- };
81
-
82
- const isAccount = (user: any): user is Account => {
83
- return (
84
- user?.type === 'account' &&
85
- 'attributes' in user &&
86
- 'email' in user.attributes &&
87
- 'name' in user.attributes
88
- );
89
- };
90
-
91
- const isOrganization = (user: any): user is Organization => {
92
- return (
93
- user?.type === 'organization' &&
94
- 'attributes' in user &&
95
- 'name' in user.attributes
96
- );
97
- };
98
-
99
- const CommentsBar = ({ ctx }: PropTypes) => {
100
- //this component is way too big, i should split it into smaller ones
101
-
102
- const getUserEmail = (user: typeof ctx.currentUser): string => {
103
- if (isRegularUser(user)) return user.attributes.email;
104
- if (isSsoUser(user)) return user.attributes.email;
105
- if (isAccount(user)) return user.attributes.email;
106
- if (isOrganization(user)) return `organization-access`;
107
- return 'unknown@email.com';
108
- };
109
-
110
- const getUserName = (user: typeof ctx.currentUser): string => {
111
- if (isRegularUser(user)) {
112
- return user.attributes.full_name;
113
- }
114
- if (isSsoUser(user)) {
115
- return user.attributes.name;
116
- }
117
- if (isAccount(user)) {
118
- return user.attributes.name;
119
- }
120
- if (isOrganization(user)) {
121
- return user.attributes.name;
122
- }
123
- const email = getUserEmail(user);
124
- return email.split('@')[0];
125
- };
126
-
127
- const userEmail = getUserEmail(ctx.currentUser);
128
- const userName = getUserName(ctx.currentUser);
129
- let foundLog = false;
130
-
131
- for (const field in ctx.fields) {
132
- if (
133
- ctx.fields[field]?.attributes.field_type === 'json' &&
134
- 'comment_log' === ctx.fields[field]?.attributes.api_key
135
- ) {
136
- foundLog = true;
137
- break;
138
- }
139
- }
140
-
141
- if (foundLog) {
142
- ctx.disableField('comment_log', true);
143
- }
144
-
145
- const initialState =
146
- foundLog && ctx.formValues.comment_log
147
- ? (JSON.parse(ctx.formValues.comment_log as string) as CommentType[])
148
- : [];
149
-
150
- const [savedComments, setSavedComments] =
151
- useState<CommentType[]>(initialState);
152
-
153
- const handleOpenLogModal = async () => {
154
- const result = await ctx.openModal({
155
- id: 'NoLogModal',
156
- title: "Didn't find a log",
157
- width: 'l',
158
- });
159
- if (result === 'goToModelEdit') {
160
- ctx.navigateTo(`/schema/item_types/${ctx.itemType.id}`);
161
- }
162
- };
163
-
164
- const saveCommentHandler = () => {
165
- if (!foundLog) {
166
- handleOpenLogModal();
167
- return;
168
- }
169
-
170
- const newComment: CommentType = {
171
- dateISO: new Date().toISOString(),
172
- comment: '',
173
- author: {
174
- name: userName,
175
- email: userEmail,
176
- },
177
- usersWhoUpvoted: [],
178
- replies: [],
179
- };
180
-
181
- setSavedComments((oldComments: CommentType[]) => {
182
- const newComments = [...oldComments];
183
- newComments.unshift(newComment);
184
- return newComments;
185
- });
186
- };
187
-
188
- const deleteCommentHandler = (
189
- dateISO: string,
190
- parentCommentISO: string = ''
191
- ) => {
192
- if (parentCommentISO) {
193
- setSavedComments((oldComments: CommentType[]) => {
194
- const newCommentsIntermediary = [...oldComments];
195
- const parentComment = newCommentsIntermediary.find(
196
- (comment) => comment.dateISO === parentCommentISO
197
- );
198
- parentComment!.replies = parentComment!.replies!.filter(
199
- (comment) => comment.dateISO !== dateISO
200
- );
201
- const newComments = newCommentsIntermediary;
202
- return newComments;
203
- });
204
- } else {
205
- setSavedComments((oldComments: CommentType[]) => {
206
- const newCommentsIntermediary = [...oldComments];
207
- const newComments = newCommentsIntermediary.filter(
208
- (comment) => comment.dateISO !== dateISO
209
- );
210
- return newComments;
211
- });
212
- }
213
- };
214
-
215
- const editCommentHandler = (
216
- dateISO: string,
217
- newValue: string,
218
- parentCommentISO: string = ''
219
- ) => {
220
- if (parentCommentISO) {
221
- setSavedComments((oldComments: CommentType[]) => {
222
- const newComments = [...oldComments];
223
- const parentComment = newComments.find(
224
- (comment) => comment.dateISO === parentCommentISO
225
- );
226
- const reply = parentComment!.replies!.find(
227
- (comment: CommentType) => comment.dateISO === dateISO
228
- );
229
- reply!.comment = newValue;
230
- return newComments;
231
- });
232
- } else {
233
- setSavedComments((oldComments: CommentType[]) => {
234
- const newComments = [...oldComments];
235
- newComments.find((comment) => comment.dateISO === dateISO)!.comment =
236
- newValue;
237
- return newComments;
238
- });
239
- }
240
- };
241
-
242
- const upvoteCommentHandler = (
243
- dateISO: string,
244
- userUpvotedThisComment: boolean,
245
- parentCommentISO: string = ''
246
- ) => {
247
- if (parentCommentISO) {
248
- setSavedComments((oldComments: CommentType[]) => {
249
- const newComments = [...oldComments];
250
- const parentComment = newComments.find(
251
- (comment) => comment.dateISO === parentCommentISO
252
- );
253
- const reply = parentComment!.replies!.find(
254
- (comment: CommentType) => comment.dateISO === dateISO
255
- );
256
- if (userUpvotedThisComment) {
257
- reply!.usersWhoUpvoted = reply!.usersWhoUpvoted.filter(
258
- (userWhoUpvotedThisComment) =>
259
- userWhoUpvotedThisComment !== userEmail
260
- );
261
- } else {
262
- reply!.usersWhoUpvoted.push(userEmail);
263
- }
264
- return newComments;
265
- });
266
- } else {
267
- setSavedComments((oldComments: CommentType[]) => {
268
- const newComments = [...oldComments];
269
- const upvotedComment = newComments.find(
270
- (comment) => comment.dateISO === dateISO
271
- );
272
- if (userUpvotedThisComment) {
273
- upvotedComment!.usersWhoUpvoted =
274
- upvotedComment!.usersWhoUpvoted.filter(
275
- (userWhoUpvotedThisComment) =>
276
- userWhoUpvotedThisComment !== userEmail
277
- );
278
- } else {
279
- upvotedComment!.usersWhoUpvoted.push(userEmail);
280
- }
281
- return newComments;
282
- });
283
- }
284
- };
285
-
286
- const replyCommentHandler = (parentCommentISO: string) => {
287
- const newComment: CommentType = {
288
- dateISO: new Date().toISOString(),
289
- comment: '',
290
- author: {
291
- name: userName,
292
- email: userEmail,
293
- },
294
- usersWhoUpvoted: [],
295
- parentCommentISO,
296
- };
297
- setSavedComments((oldComments: CommentType[]) => {
298
- const newComments = [...oldComments];
299
- newComments
300
- .find((comment) => comment.dateISO === parentCommentISO)
301
- ?.replies?.unshift(newComment);
302
- return newComments;
303
- });
304
- };
305
-
306
- useEffect(() => {
307
- const objectIsEmpty = !Object.keys(savedComments).length;
308
- if (foundLog && objectIsEmpty) {
309
- ctx.setFieldValue('comment_log', null);
310
- } else if (foundLog) {
311
- const formatedComments = JSON.stringify(savedComments, null, 2);
312
- const stateIsEqualToLog =
313
- formatedComments === (ctx.formValues.comment_log as string);
314
- if (!stateIsEqualToLog) {
315
- ctx.setFieldValue('comment_log', formatedComments);
316
- }
317
- }
318
- }, [savedComments, ctx, foundLog]);
319
-
320
- return (
321
- <Canvas ctx={ctx}>
322
- <Button
323
- fullWidth
324
- className={styles['add-comment-button']}
325
- onClick={saveCommentHandler}
326
- >
327
- Add a new comment...
328
- </Button>
329
- {foundLog &&
330
- savedComments.map((comment) => {
331
- return (
332
- <Comment
333
- key={comment.dateISO}
334
- deleteComment={deleteCommentHandler}
335
- editComment={editCommentHandler}
336
- upvoteComment={upvoteCommentHandler}
337
- replyComment={replyCommentHandler}
338
- commentObject={comment}
339
- currentUserEmail={userEmail}
340
- />
341
- );
342
- })}
343
- </Canvas>
344
- );
345
- };
346
-
347
- export default CommentsBar;
@@ -1,43 +0,0 @@
1
- import { RenderModalCtx } from "datocms-plugin-sdk";
2
- import { Button, Canvas } from "datocms-react-ui";
3
- import styles from "./styles/modal.module.css";
4
-
5
- type PropTypes = {
6
- ctx: RenderModalCtx;
7
- };
8
-
9
- const NoLogModal = ({ ctx }: PropTypes) => {
10
- const handleCancel = () => {
11
- ctx.resolve("cancel");
12
- };
13
-
14
- const handleGoToModel = () => {
15
- ctx.resolve("goToModelEdit");
16
- };
17
-
18
- return (
19
- <Canvas ctx={ctx}>
20
- <div className={styles["error-explanation"]}>
21
- <h2>A field is missing from your model!</h2>
22
- <p>
23
- You need to create a "comment log" JSON field on this model to save
24
- comments.
25
- </p>
26
- <p className={styles["note"]}>
27
- Note that the field must be of type "JSON" and its API key must be
28
- "comment_log"
29
- </p>
30
- </div>
31
- <div className={styles["buttons"]}>
32
- <Button buttonType="negative" buttonSize="l" onClick={handleCancel}>
33
- Cancel
34
- </Button>
35
- <Button buttonType="primary" buttonSize="l" onClick={handleGoToModel}>
36
- Go to model editing
37
- </Button>
38
- </div>
39
- </Canvas>
40
- );
41
- };
42
-
43
- export default NoLogModal;
@@ -1,259 +0,0 @@
1
- import React, { useEffect, useRef, useState } from "react";
2
- import styles from "../styles/comment.module.css";
3
- import Textarea from "react-textarea-autosize";
4
- import ReactTimeAgo from "react-time-ago";
5
- import { CommentType } from "../CommentsBar";
6
- import md5 from "md5";
7
- import {
8
- faChevronUp,
9
- faPen,
10
- faReply,
11
- faSave,
12
- faTrashAlt,
13
- } from "@fortawesome/free-solid-svg-icons";
14
- import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
15
-
16
- type commentProps = {
17
- deleteComment: (dateISO: string, parentCommentISO?: string) => void;
18
- editComment: (
19
- dateISO: string,
20
- newValue: string,
21
- parentCommentISO?: string
22
- ) => void;
23
- upvoteComment: (
24
- dateISO: string,
25
- userUpvotedThisComment: boolean,
26
- parentCommentISO?: string
27
- ) => void;
28
- replyComment: (parentCommentISO: string) => void;
29
- commentObject: CommentType;
30
- currentUserEmail: string;
31
- };
32
-
33
- const Comment: React.FC<commentProps> = (props) => {
34
- const userUpvotedThisComment = !!props.commentObject.usersWhoUpvoted.find(
35
- (userWhoUpvoted) => userWhoUpvoted === props.currentUserEmail
36
- );
37
- const commentSpan = useRef<HTMLDivElement>(null);
38
- const [textAreaHeight, setTextAreaHeight] = useState(21);
39
- const [isEditing, setIsEditing] = useState(!props.commentObject.comment);
40
- const isNotAReply = "replies" in props.commentObject;
41
- const userIsAuthor =
42
- props.commentObject.author.email === props.currentUserEmail;
43
- const authorProfilePictureUrl =
44
- "https://www.gravatar.com/avatar/" +
45
- md5(props.commentObject.author.email || '') +
46
- "?d=mp";
47
-
48
- const replyClasses = isNotAReply ? "" : " " + styles.reply;
49
- const commentClasses = styles.comment + replyClasses;
50
-
51
- const [textAreaValue, setTextAreaValue] = useState(
52
- props.commentObject.comment
53
- );
54
-
55
- const toggleEdit = () => {
56
- setIsEditing(true);
57
- };
58
-
59
- const editCommentHandler = () => {
60
- if (!textAreaValue.trim()) {
61
- return;
62
- }
63
- setIsEditing(false);
64
- if (isNotAReply) {
65
- props.editComment(props.commentObject.dateISO, textAreaValue);
66
- } else {
67
- props.editComment(
68
- props.commentObject.dateISO,
69
- textAreaValue,
70
- props.commentObject.parentCommentISO
71
- );
72
- }
73
- };
74
-
75
- const deleteCommentHanlder = () => {
76
- if (isNotAReply) {
77
- props.deleteComment(props.commentObject.dateISO);
78
- } else {
79
- props.deleteComment(
80
- props.commentObject.dateISO,
81
- props.commentObject.parentCommentISO
82
- );
83
- }
84
- };
85
-
86
- const upvoteCommentHandler = () => {
87
- if (isNotAReply) {
88
- props.upvoteComment(props.commentObject.dateISO, userUpvotedThisComment);
89
- } else {
90
- props.upvoteComment(
91
- props.commentObject.dateISO,
92
- userUpvotedThisComment,
93
- props.commentObject.parentCommentISO
94
- );
95
- }
96
- };
97
-
98
- const replyCommentHandler = () => {
99
- props.replyComment(props.commentObject.dateISO);
100
- };
101
-
102
- const enterKeyHandler = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
103
- if (event.key === "Enter") {
104
- editCommentHandler();
105
- }
106
- };
107
-
108
- const heightChangeHandler = (height: number) => {
109
- setTextAreaHeight(height);
110
- };
111
-
112
- useEffect(() => {
113
- const spanHeight = commentSpan.current?.clientHeight;
114
- setTextAreaHeight(spanHeight!);
115
- }, [commentSpan]);
116
-
117
- const focousHandler = (event: React.FocusEvent<HTMLTextAreaElement>) => {
118
- event.currentTarget.setSelectionRange(
119
- event.currentTarget.value.length,
120
- event.currentTarget.value.length
121
- );
122
- };
123
-
124
- return (
125
- <div className={commentClasses}>
126
- {!isNotAReply && (
127
- <div
128
- style={{ height: 100 + textAreaHeight }}
129
- className={styles["reply-line"]}
130
- ></div>
131
- )}
132
- <div className={styles["comment-main"]}>
133
- <div className={styles["comment-body"]}>
134
- {isEditing && (
135
- <div className={styles["dialogbox"]}>
136
- <div className={styles["body"]}>
137
- <span
138
- className={styles["tip"] + " " + styles["tip-down"]}
139
- ></span>
140
- <div className={styles["message"]}>
141
- <Textarea
142
- autoFocus
143
- onFocus={focousHandler}
144
- onHeightChange={heightChangeHandler}
145
- onKeyPress={enterKeyHandler}
146
- onChange={(event) => setTextAreaValue(event.target.value)}
147
- value={textAreaValue}
148
- className={styles["comment-textarea"]}
149
- />
150
- </div>
151
- </div>
152
- </div>
153
- )}
154
- {!isEditing && (
155
- <div ref={commentSpan} className={styles["dialogbox"]}>
156
- <div className={styles["body"]}>
157
- <span
158
- className={styles["tip"] + " " + styles["tip-down"]}
159
- ></span>
160
- <div className={styles["message"]}>
161
- <span>{textAreaValue}</span>
162
- </div>
163
- </div>
164
- </div>
165
- )}
166
- <div
167
- className={`${styles["comment__footer"]} ${styles["comments__item__timestamp"]}`}
168
- >
169
- <div className={styles["comment-footer"]}>
170
- <div className={styles["author-info"]}>
171
- <img
172
- className={styles["author-profile-picture"]}
173
- src={authorProfilePictureUrl}
174
- alt={props.commentObject.author.name}
175
- />
176
- </div>
177
- {!isNotAReply && (
178
- <div className={styles["reply-profile-picture-line"]}></div>
179
- )}
180
- <div className={styles["comment__buttons"]}>
181
- <div>
182
- <span>{props.commentObject.author.name}</span>
183
- </div>
184
- <div className={styles["comment__timestamps"]}>
185
- <ReactTimeAgo date={new Date(props.commentObject.dateISO)} />
186
- </div>
187
- <div className={styles["comment__interface"]}>
188
- <span
189
- className={userUpvotedThisComment ? styles["upvoted"] : ""}
190
- >
191
- {props.commentObject.usersWhoUpvoted.length}
192
- </span>
193
- <button
194
- className={`${styles["comment__button"]} ${
195
- styles["upvotes"]
196
- } ${userUpvotedThisComment ? styles["upvoted"] : ""}`}
197
- onClick={upvoteCommentHandler}
198
- >
199
- <FontAwesomeIcon icon={faChevronUp} />
200
- </button>
201
- {userIsAuthor && props.commentObject.comment && !isEditing ? (
202
- <button
203
- className={styles["comment__button"]}
204
- onClick={toggleEdit}
205
- >
206
- <FontAwesomeIcon icon={faPen} />
207
- </button>
208
- ) : userIsAuthor ? (
209
- <button
210
- className={styles["comment__button"]}
211
- onClick={editCommentHandler}
212
- >
213
- <FontAwesomeIcon icon={faSave} />
214
- </button>
215
- ) : null}
216
- {userIsAuthor && (
217
- <button
218
- className={styles["comment__button"]}
219
- onClick={deleteCommentHanlder}
220
- >
221
- <FontAwesomeIcon icon={faTrashAlt} />
222
- </button>
223
- )}
224
- {isNotAReply && (
225
- <button
226
- className={styles["comment__button"]}
227
- onClick={replyCommentHandler}
228
- >
229
- <FontAwesomeIcon icon={faReply} />
230
- </button>
231
- )}
232
- </div>
233
- </div>
234
- </div>
235
- </div>
236
- </div>
237
- </div>
238
- {isNotAReply && (
239
- <div className={styles["comments__replies__item"]}>
240
- {props.commentObject.replies?.map((reply) => {
241
- return (
242
- <Comment
243
- key={reply.dateISO}
244
- deleteComment={props.deleteComment}
245
- editComment={props.editComment}
246
- upvoteComment={props.upvoteComment}
247
- replyComment={props.replyComment}
248
- commentObject={reply}
249
- currentUserEmail={props.currentUserEmail}
250
- />
251
- );
252
- })}
253
- </div>
254
- )}
255
- </div>
256
- );
257
- };
258
-
259
- export default Comment;