tycho-components 0.40.9 → 0.41.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.
@@ -3,9 +3,8 @@ import CookieStorage from '../CookieStorage';
3
3
  import { resolveAccessDeniedMessage } from './resolveAccessDeniedMessage';
4
4
  import { logged, message } from '../store/actions';
5
5
  import { getCommonDispatch } from '../store/commonDispatchBridge';
6
- const isStorybook = () => typeof window !== 'undefined' &&
7
- (window.location.port === '6006' ||
8
- window.__STORYBOOK__);
6
+ import StorybookUtils from '../../functions/StorybookUtils';
7
+ const { isStorybook } = StorybookUtils;
9
8
  function defaultRedirectUrls() {
10
9
  const publicUrl = import.meta.env.VITE_APP_PUBLIC_URL;
11
10
  return {
@@ -8,6 +8,7 @@ export declare const CommentsTexts: {
8
8
  "label.button.update": string;
9
9
  "label.requested.to": string;
10
10
  "label.posted.on": string;
11
+ "label.on": string;
11
12
  "placeholder.comments.empty": string;
12
13
  "tooltip.edit": string;
13
14
  "tooltip.delete": string;
@@ -29,6 +30,9 @@ export declare const CommentsTexts: {
29
30
  "notification.empty": string;
30
31
  "notification.text": string;
31
32
  "label.edited": string;
33
+ "tab.write": string;
34
+ "tab.preview": string;
35
+ "placeholder.preview.empty": string;
32
36
  };
33
37
  "pt-BR": {
34
38
  "label.title.comments": string;
@@ -39,6 +43,7 @@ export declare const CommentsTexts: {
39
43
  "label.button.update": string;
40
44
  "label.requested.to": string;
41
45
  "label.posted.on": string;
46
+ "label.on": string;
42
47
  "placeholder.comments.empty": string;
43
48
  "tooltip.edit": string;
44
49
  "tooltip.delete": string;
@@ -60,6 +65,9 @@ export declare const CommentsTexts: {
60
65
  "notification.empty": string;
61
66
  "notification.text": string;
62
67
  "label.edited": string;
68
+ "tab.write": string;
69
+ "tab.preview": string;
70
+ "placeholder.preview.empty": string;
63
71
  };
64
72
  it: {
65
73
  "label.title.comments": string;
@@ -70,6 +78,7 @@ export declare const CommentsTexts: {
70
78
  "label.button.update": string;
71
79
  "label.requested.to": string;
72
80
  "label.posted.on": string;
81
+ "label.on": string;
73
82
  "placeholder.comments.empty": string;
74
83
  "tooltip.edit": string;
75
84
  "tooltip.delete": string;
@@ -91,5 +100,8 @@ export declare const CommentsTexts: {
91
100
  "notification.empty": string;
92
101
  "notification.text": string;
93
102
  "label.edited": string;
103
+ "tab.write": string;
104
+ "tab.preview": string;
105
+ "placeholder.preview.empty": string;
94
106
  };
95
107
  };
@@ -8,6 +8,7 @@ export const CommentsTexts = {
8
8
  "label.button.update": "Update comment",
9
9
  "label.requested.to": "Sent to:",
10
10
  "label.posted.on": "Posted on:",
11
+ "label.on": "on",
11
12
  "placeholder.comments.empty": "No comments available.",
12
13
  "tooltip.edit": "Edit comment",
13
14
  "tooltip.delete": "Remove comment",
@@ -29,6 +30,9 @@ export const CommentsTexts = {
29
30
  "notification.empty": "No more notifications",
30
31
  "notification.text": "{{user}} added a new comment.",
31
32
  "label.edited": "edited",
33
+ "tab.write": "Write",
34
+ "tab.preview": "Preview",
35
+ "placeholder.preview.empty": "Nothing to preview",
32
36
  },
33
37
  "pt-BR": {
34
38
  "label.title.comments": "Comentários",
@@ -39,6 +43,7 @@ export const CommentsTexts = {
39
43
  "label.button.update": "Atualizar comentário",
40
44
  "label.requested.to": "Enviado para:",
41
45
  "label.posted.on": "Postado em:",
46
+ "label.on": "em",
42
47
  "placeholder.comments.empty": "Nenhum comentário disponível.",
43
48
  "tooltip.edit": "Editar comentário",
44
49
  "tooltip.delete": "Remover comentário",
@@ -60,6 +65,9 @@ export const CommentsTexts = {
60
65
  "notification.empty": "Não há notificações",
61
66
  "notification.text": "{{user}} adicionou um novo comentário.",
62
67
  "label.edited": "editado",
68
+ "tab.write": "Escrever",
69
+ "tab.preview": "Visualizar",
70
+ "placeholder.preview.empty": "Nada para visualizar",
63
71
  },
64
72
  it: {
65
73
  "label.title.comments": "Commenti",
@@ -70,6 +78,7 @@ export const CommentsTexts = {
70
78
  "label.button.update": "Aggiorna commento",
71
79
  "label.requested.to": "Inviato a:",
72
80
  "label.posted.on": "Pubblicato il:",
81
+ "label.on": "il",
73
82
  "placeholder.comments.empty": "Nessun commento disponibile.",
74
83
  "tooltip.edit": "Modifica commento",
75
84
  "tooltip.delete": "Rimuovi commento",
@@ -91,5 +100,8 @@ export const CommentsTexts = {
91
100
  "notification.empty": "Nessun'altra notifica",
92
101
  "notification.text": "{{user}} ha aggiunto un commento all'inserimento {{entry}}.",
93
102
  "label.edited": "modificato",
103
+ "tab.write": "Scrivi",
104
+ "tab.preview": "Anteprima",
105
+ "placeholder.preview.empty": "Niente da visualizzare",
94
106
  },
95
107
  };
@@ -1,9 +1,11 @@
1
- import { User } from "../../configs/types/User";
2
- import "./style.scss";
3
- import { Comment } from "./types/Comment";
1
+ import '@uiw/react-markdown-preview/markdown.css';
2
+ import '@uiw/react-md-editor/markdown-editor.css';
3
+ import { User } from '../../../configs/types/User';
4
+ import { Comment } from '../types/Comment';
5
+ import './style.scss';
4
6
  type Props = {
5
7
  uid: string;
6
- mode: "lexicon" | "corpus" | "parser";
8
+ mode: 'lexicon' | 'corpus' | 'parser';
7
9
  users: User[];
8
10
  references: Record<string, string | number | boolean>;
9
11
  keywords?: Record<string, string | number | boolean>;
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { yupResolver } from '@hookform/resolvers/yup';
3
+ import Tab from '@mui/material/Tab';
4
+ import Tabs from '@mui/material/Tabs';
5
+ import '@uiw/react-markdown-preview/markdown.css';
6
+ import MDEditor from '@uiw/react-md-editor';
7
+ import '@uiw/react-md-editor/markdown-editor.css';
8
+ import { useState } from 'react';
9
+ import { useForm } from 'react-hook-form';
10
+ import { useTranslation } from 'react-i18next';
11
+ import { Button, IconButton, SelectField } from 'tycho-storybook';
12
+ import * as yup from 'yup';
13
+ import { useMessageUtils } from '../../../configs/useMessageUtils';
14
+ import FormUtils from '../../../functions/FormUtils';
15
+ import CommentService from '../CommentService';
16
+ import { EMPTY_COMMENT_REQUEST, } from '../types/Comment';
17
+ import './style.scss';
18
+ const EMPTY_COMMENT_ADD_FORM = {
19
+ ...EMPTY_COMMENT_REQUEST,
20
+ requested: [],
21
+ userPicker: '',
22
+ };
23
+ export default function CommentAdd({ uid, mode, users, references, keywords, comment, reply, onClose, onChange, }) {
24
+ const { t } = useTranslation('comments');
25
+ const { isLoading, dispatchLoading } = useMessageUtils();
26
+ const [activeTab, setActiveTab] = useState('write');
27
+ const getFormDefaultValues = () => {
28
+ if (comment)
29
+ return {
30
+ ...EMPTY_COMMENT_ADD_FORM,
31
+ value: comment.value,
32
+ requested: comment.requested?.map((r) => r.uid) ?? [],
33
+ };
34
+ if (reply) {
35
+ return {
36
+ ...EMPTY_COMMENT_ADD_FORM,
37
+ reply: reply.id,
38
+ requested: [reply.user],
39
+ };
40
+ }
41
+ return EMPTY_COMMENT_ADD_FORM;
42
+ };
43
+ const createdForm = useForm({
44
+ resolver: yupResolver(getFormSchema(t)),
45
+ mode: 'onChange',
46
+ defaultValues: getFormDefaultValues(),
47
+ });
48
+ const value = createdForm.watch('value') ?? '';
49
+ const requested = createdForm.watch('requested') ?? [];
50
+ const toCommentRequest = (form) => {
51
+ const { userPicker: _userPicker, ...request } = form;
52
+ return request;
53
+ };
54
+ const handleAdd = () => {
55
+ if (isLoading())
56
+ return;
57
+ dispatchLoading(true);
58
+ CommentService.add(uid, mode, toCommentRequest(createdForm.getValues()), references, keywords).then((r) => {
59
+ dispatchLoading(false);
60
+ onClose();
61
+ onChange(r.data);
62
+ createdForm.reset(EMPTY_COMMENT_ADD_FORM);
63
+ });
64
+ };
65
+ const handleUpdate = () => {
66
+ if (!comment)
67
+ return;
68
+ CommentService.update(comment.id, toCommentRequest(createdForm.getValues())).then((r) => {
69
+ onClose();
70
+ onChange(r.data);
71
+ createdForm.reset(EMPTY_COMMENT_ADD_FORM);
72
+ });
73
+ };
74
+ const handleUserPick = (_attr, nextValue) => {
75
+ if (!nextValue)
76
+ return;
77
+ const current = createdForm.getValues('requested') ?? [];
78
+ if (!current.includes(nextValue)) {
79
+ createdForm.setValue('requested', [...current, nextValue], {
80
+ shouldDirty: true,
81
+ });
82
+ }
83
+ createdForm.setValue('userPicker', '');
84
+ };
85
+ const handleRemoveUser = (uidToRemove) => {
86
+ const current = createdForm.getValues('requested') ?? [];
87
+ createdForm.setValue('requested', current.filter((id) => id !== uidToRemove), { shouldDirty: true });
88
+ };
89
+ const availableUsers = users.filter((user) => !requested.includes(user.uid));
90
+ return (_jsxs("div", { className: "comment-add-container", children: [_jsx("div", { className: "form-tabs", children: _jsxs(Tabs, { value: activeTab, onChange: (_, next) => setActiveTab(next), children: [_jsx(Tab, { label: t('tab.write'), value: "write" }), _jsx(Tab, { label: t('tab.preview'), value: "preview" })] }) }), activeTab === 'write' ? (_jsx("div", { className: "form-editor", "data-color-mode": "light", children: _jsx(MDEditor, { height: 220, value: value, preview: "edit", onChange: (next) => createdForm.setValue('value', next || '', {
91
+ shouldValidate: true,
92
+ shouldDirty: true,
93
+ }), textareaProps: {
94
+ placeholder: t('placeholder.comment.text'),
95
+ } }) })) : (_jsx("div", { className: "form-preview", "data-color-mode": "light", children: value.trim() ? (_jsx(MDEditor.Markdown, { source: value })) : (_jsx("span", { className: "preview-empty", children: t('placeholder.preview.empty') })) })), _jsxs("div", { className: "form-footer", children: [_jsx(SelectField, { label: reply ? t('input.user.reply') : t('input.user'), attr: "userPicker", createdForm: createdForm, placeholder: t('common:generic.placeholder.select'), options: FormUtils.convertList(availableUsers, 'name', 'uid'), showEndAdornment: false, onChange: handleUserPick }), requested.length > 0 && (_jsx("div", { className: "selected-users", children: requested.map((userUid) => {
96
+ const user = users.find((u) => u.uid === userUid);
97
+ return (_jsxs("div", { className: "selected-user", children: [_jsx("span", { className: "name", children: user?.name || userUid }), _jsx(IconButton, { name: "close", size: "x-small", mode: "ghost", onClick: () => handleRemoveUser(userUid) })] }, userUid));
98
+ }) })), _jsxs("div", { className: "buttons", children: [_jsx(Button, { onClick: onClose, text: t('button.discard'), color: "danger", size: "small" }), _jsx(Button, { onClick: comment ? handleUpdate : handleAdd, text: comment ? t('label.button.update') : t('label.button.add'), disabled: !createdForm.formState.isValid, size: "small" })] })] })] }));
99
+ }
100
+ const getFormSchema = (t) => yup.object().shape({
101
+ id: yup.string().optional(),
102
+ value: yup.string().required(t('common:validation.required')),
103
+ requested: yup.array().of(yup.string().defined()).optional(),
104
+ coordinates: yup.array().of(yup.string().defined()).optional(),
105
+ userPicker: yup.string().optional(),
106
+ reply: yup.string().optional(),
107
+ });
@@ -0,0 +1,2 @@
1
+ import CommentAdd from './CommentAdd';
2
+ export default CommentAdd;
@@ -0,0 +1,2 @@
1
+ import CommentAdd from './CommentAdd';
2
+ export default CommentAdd;
@@ -0,0 +1,124 @@
1
+ .comment-add-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ margin-bottom: var(--spacing-200);
5
+ border: 1px solid var(--border-subtle-1);
6
+ border-radius: var(--radius-200);
7
+ overflow: hidden;
8
+ background-color: var(--background-default);
9
+ margin-top: 16px;
10
+
11
+ .form-tabs {
12
+ padding: 0 var(--spacing-150);
13
+ background-color: var(--layer-hover-2);
14
+ border-bottom: 1px solid var(--border-subtle-1);
15
+
16
+ .MuiTabs-root {
17
+ min-height: unset;
18
+ }
19
+
20
+ .MuiTabs-indicator {
21
+ background-color: var(--border-hover);
22
+ }
23
+
24
+ .MuiTab-root {
25
+ @include label-medium-1;
26
+ min-height: unset;
27
+ min-width: unset;
28
+ padding: var(--spacing-100) var(--spacing-150);
29
+ text-transform: none;
30
+ color: var(--text-secondary);
31
+
32
+ &.Mui-selected {
33
+ color: var(--text-primary);
34
+ }
35
+ }
36
+ }
37
+
38
+ .form-editor {
39
+ .w-md-editor {
40
+ border: none;
41
+ border-radius: 0;
42
+ box-shadow: none;
43
+
44
+ .w-md-editor-toolbar {
45
+ border-bottom: 1px solid var(--border-subtle-1);
46
+ background-color: var(--background-default);
47
+ padding: var(--spacing-050) var(--spacing-100);
48
+
49
+ li button {
50
+ padding: 0 var(--spacing-050);
51
+
52
+ > svg {
53
+ width: 16px;
54
+ height: 16px;
55
+ }
56
+ }
57
+ }
58
+
59
+ .w-md-editor-content {
60
+ background-color: var(--background-default);
61
+ }
62
+ }
63
+ }
64
+
65
+ .form-preview {
66
+ min-height: 220px;
67
+ padding: var(--spacing-150);
68
+ background-color: var(--background-default);
69
+
70
+ .preview-empty {
71
+ @include helper-small-1;
72
+ color: var(--text-tertiary);
73
+ }
74
+
75
+ .wmde-markdown {
76
+ background: transparent;
77
+ }
78
+ }
79
+
80
+ .form-footer {
81
+ display: flex;
82
+ flex-direction: column;
83
+ gap: var(--spacing-100);
84
+ padding: var(--spacing-150);
85
+ border-top: 1px solid var(--border-subtle-1);
86
+ background-color: var(--background-default);
87
+
88
+ > .ds-select-text {
89
+ margin-bottom: 0;
90
+ }
91
+
92
+ .selected-users {
93
+ display: flex;
94
+ flex-wrap: wrap;
95
+ gap: var(--spacing-100);
96
+ }
97
+
98
+ .selected-user {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ gap: var(--spacing-050);
102
+ padding: 4px 8px;
103
+ border: 1px solid var(--border-subtle-1);
104
+ border-radius: var(--radius-100);
105
+ background-color: var(--layer-hover-2);
106
+
107
+ .name {
108
+ @include label-small-1;
109
+ color: var(--text-primary);
110
+ }
111
+
112
+ .ds-icon-button.x-small {
113
+ width: 16px;
114
+ height: 16px;
115
+ }
116
+ }
117
+
118
+ > .buttons {
119
+ display: flex;
120
+ justify-content: flex-end;
121
+ gap: var(--spacing-100);
122
+ }
123
+ }
124
+ }
@@ -1,3 +1,4 @@
1
+ import '@uiw/react-markdown-preview/markdown.css';
1
2
  import './style.scss';
2
3
  import { Comment } from './types/Comment';
3
4
  type Props = {
@@ -1,11 +1,13 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import MDEditor from '@uiw/react-md-editor';
3
+ import '@uiw/react-markdown-preview/markdown.css';
2
4
  import { useContext } from 'react';
3
5
  import { useTranslation } from 'react-i18next';
4
- import { Avatar, IconButton } from 'tycho-storybook';
6
+ import { Avatar, IconButton, Tag } from 'tycho-storybook';
5
7
  import CommonContext from '../../configs/CommonContext';
6
8
  import DateUtils from '../../functions/DateUtils';
7
- import './style.scss';
8
9
  import CommentService from './CommentService';
10
+ import './style.scss';
9
11
  export default function CommentInfo({ comment, hasEditAccess = true, onRemove, onUpdate, onEdit, onReply, }) {
10
12
  const { t } = useTranslation('comments');
11
13
  const { state } = useContext(CommonContext);
@@ -27,5 +29,7 @@ export default function CommentInfo({ comment, hasEditAccess = true, onRemove, o
27
29
  return false;
28
30
  return !requested.read;
29
31
  };
30
- return (_jsxs("div", { className: "comment", children: [_jsxs("div", { className: "top", children: [_jsx(Avatar, { title: comment.name, src: comment.picture, size: comment.reply ? 'small' : 'medium' }), _jsx("div", { className: "name", children: comment.name }), _jsxs("div", { className: "buttons", children: [hasEditAccess && !comment.reply && (_jsx(IconButton, { size: "small", onClick: onReply, title: t('tooltip.reply'), name: "reply" })), hasEditAccess && state.logged?.uid === comment.user && (_jsxs(_Fragment, { children: [_jsx(IconButton, { size: comment.reply ? 'x-small' : 'small', onClick: onEdit, title: t('tooltip.edit'), name: "edit" }), _jsx(IconButton, { size: comment.reply ? 'x-small' : 'small', onClick: handleRemove, title: t('tooltip.delete'), name: "delete" })] })), isNotRead(comment) && (_jsx(IconButton, { size: comment.reply ? 'x-small' : 'small', onClick: handleMarkRead, title: t('tooltip.read'), name: "check_circle" }))] })] }), _jsxs("div", { className: "content", children: [comment.title && _jsx("div", { className: "title", children: comment.title }), _jsx("div", { className: "text", children: comment.value }), comment.requested && (_jsxs("div", { className: "date", children: [_jsx("span", { className: "me-1", children: t('label.requested.to') }), _jsx("span", { children: comment.requested.map((e) => e.name).join(', ') })] })), _jsxs("div", { className: "date", children: [_jsx("span", { className: "me-1", children: t('label.posted.on') }), _jsx("span", { className: "me-1", children: DateUtils.formatDateTime(comment.edited ? comment.edited : comment.date, 'MM/dd/yyyy - HH:mm') }), comment.edited && _jsx("i", { children: t('label.edited') })] })] })] }));
32
+ const displayDate = comment.edited || comment.date;
33
+ const fullDateTitle = DateUtils.formatDateTime(displayDate, 'MM/dd/yyyy - HH:mm');
34
+ return (_jsxs("div", { className: "comment", children: [_jsxs("div", { className: "comment-header", children: [_jsx(Avatar, { title: comment.name, src: comment.picture, size: "x-small" }), _jsxs("div", { className: "meta", children: [_jsx("span", { className: "name", children: comment.name }), _jsxs("span", { className: "timestamp", title: fullDateTitle, children: [t('label.on'), ' ', DateUtils.formatDateTime(displayDate, 'MMM d')] })] }), _jsxs("div", { className: "buttons", children: [comment.edited && (_jsx(Tag, { text: t('label.edited'), color: "blue", size: "small" })), hasEditAccess && !comment.reply && (_jsx(IconButton, { size: "x-small", onClick: onReply, title: t('tooltip.reply'), name: "reply" })), hasEditAccess && state.logged?.uid === comment.user && (_jsxs(_Fragment, { children: [_jsx(IconButton, { size: "x-small", onClick: onEdit, title: t('tooltip.edit'), name: "edit" }), _jsx(IconButton, { size: "x-small", onClick: handleRemove, title: t('tooltip.delete'), name: "delete" })] })), isNotRead(comment) && (_jsx(IconButton, { size: "x-small", onClick: handleMarkRead, title: t('tooltip.read'), name: "check_circle" }))] })] }), _jsxs("div", { className: "comment-body", children: [_jsx("div", { className: "text", "data-color-mode": "light", children: _jsx(MDEditor.Markdown, { source: comment.value }) }), comment.requested && (_jsxs("div", { className: "meta-line", children: [_jsx("span", { className: "me-1", children: t('label.requested.to') }), _jsx("span", { children: comment.requested.map((e) => e.name).join(', ') })] }))] })] }));
31
35
  }
@@ -1,13 +1,14 @@
1
+ import { AxiosResponse } from 'axios';
1
2
  import { User } from '../../configs/types/User';
2
3
  import { Comment, CommentRequest } from './types/Comment';
3
- declare function findNotifications(uid: string, mode: string): Promise<import("axios").AxiosResponse<Comment[], any, {}, any>>;
4
- declare function findReadNotifications(uid: string, mode: string): Promise<import("axios").AxiosResponse<Comment[], any, {}, any>>;
5
- declare function markRead(id: string): Promise<import("axios").AxiosResponse<Comment, any, {}, any>>;
6
- declare function update(id: string, comment: CommentRequest): Promise<import("axios").AxiosResponse<Comment, any, {}, any>>;
7
- declare function remove(id: string): Promise<import("axios").AxiosResponse<Comment, any, {}, any>>;
8
- declare function add(uid: string, mode: string, comment: CommentRequest, references: Record<string, string | number | boolean>, keywords?: Record<string, string | number | boolean>): Promise<import("axios").AxiosResponse<Comment, any, {}, any>>;
9
- declare function find(uid: string, mode: string, request: Record<string, string | number | boolean>): Promise<import("axios").AxiosResponse<Comment[], any, {}, any>>;
10
- declare function findAvailableUsers(uid: string, mode: string): Promise<import("axios").AxiosResponse<User[], any, {}, any>>;
4
+ declare function findNotifications(uid: string, mode: string): Promise<AxiosResponse<Comment[], any, {}, any>>;
5
+ declare function findReadNotifications(uid: string, mode: string): Promise<AxiosResponse<Comment[], any, {}, any>>;
6
+ declare function markRead(id: string): Promise<AxiosResponse<Comment, any, {}, any>>;
7
+ declare function update(id: string, comment: CommentRequest): Promise<AxiosResponse<Comment, any, {}, any>>;
8
+ declare function remove(id: string): Promise<AxiosResponse<Comment, any, {}, any>>;
9
+ declare function add(uid: string, mode: string, comment: CommentRequest, references: Record<string, string | number | boolean>, keywords?: Record<string, string | number | boolean>): Promise<AxiosResponse<Comment, any, {}, any>>;
10
+ declare function find(uid: string, mode: string, request: Record<string, string | number | boolean>): Promise<AxiosResponse<Comment[], any, {}, any>>;
11
+ declare function findAvailableUsers(uid: string, mode: string): Promise<AxiosResponse<User[], any, {}, any>>;
11
12
  declare const CommentService: {
12
13
  add: typeof add;
13
14
  remove: typeof remove;
@@ -1,4 +1,30 @@
1
1
  import api from '../../configs/api';
2
+ import StorybookUtils from '../../functions/StorybookUtils';
3
+ import { mockCommentUsers, mockComments } from './CommentService.mock';
4
+ let storybookComments = mockComments.map((c) => ({ ...c }));
5
+ function mockResponse(data) {
6
+ return Promise.resolve({
7
+ data,
8
+ status: 200,
9
+ statusText: 'OK',
10
+ headers: {},
11
+ config: {},
12
+ });
13
+ }
14
+ function resolveRequestedUsers(uids) {
15
+ if (!uids)
16
+ return undefined;
17
+ return uids.map((uid) => {
18
+ const user = mockCommentUsers.find((u) => u.uid === uid);
19
+ return {
20
+ uid,
21
+ name: user?.name || uid,
22
+ picture: user?.picture || '',
23
+ email: user?.sub || '',
24
+ read: false,
25
+ };
26
+ });
27
+ }
2
28
  function findNotifications(uid, mode) {
3
29
  return api.get(`${import.meta.env.VITE_APP_COMMENT_API_URL}/${mode}/notifications/${uid}`);
4
30
  }
@@ -6,15 +32,57 @@ function findReadNotifications(uid, mode) {
6
32
  return api.get(`${import.meta.env.VITE_APP_COMMENT_API_URL}/${mode}/notifications/read/${uid}`);
7
33
  }
8
34
  function markRead(id) {
35
+ if (StorybookUtils.isStorybook()) {
36
+ storybookComments = storybookComments.map((c) => c.id === id
37
+ ? { ...c, requested: c.requested?.map((r) => ({ ...r, read: true })) }
38
+ : c);
39
+ return mockResponse(storybookComments.find((c) => c.id === id));
40
+ }
9
41
  return api.put(`${import.meta.env.VITE_APP_COMMENT_API_URL}/read/${id}`);
10
42
  }
11
43
  function update(id, comment) {
44
+ if (StorybookUtils.isStorybook()) {
45
+ storybookComments = storybookComments.map((c) => {
46
+ if (c.id !== id)
47
+ return c;
48
+ return {
49
+ ...c,
50
+ value: comment.value ?? c.value,
51
+ requested: comment.requested
52
+ ? resolveRequestedUsers(comment.requested)
53
+ : c.requested,
54
+ coordinates: comment.coordinates ?? c.coordinates,
55
+ edited: new Date().toISOString(),
56
+ };
57
+ });
58
+ return mockResponse(storybookComments.find((c) => c.id === id));
59
+ }
12
60
  return api.patch(`${import.meta.env.VITE_APP_COMMENT_API_URL}/${id}`, { ...comment });
13
61
  }
14
62
  function remove(id) {
63
+ if (StorybookUtils.isStorybook()) {
64
+ storybookComments = storybookComments.filter((c) => c.id !== id);
65
+ return mockResponse(undefined);
66
+ }
15
67
  return api.delete(`${import.meta.env.VITE_APP_COMMENT_API_URL}/${id}`);
16
68
  }
17
69
  function add(uid, mode, comment, references, keywords) {
70
+ if (StorybookUtils.isStorybook()) {
71
+ const replyId = comment.reply;
72
+ const newComment = {
73
+ id: `mock-comment-${Date.now()}`,
74
+ value: comment.value || '',
75
+ date: new Date().toISOString(),
76
+ name: 'Storybook User',
77
+ user: 'storybook-current-user',
78
+ picture: '',
79
+ reply: replyId,
80
+ requested: resolveRequestedUsers(comment.requested),
81
+ coordinates: comment.coordinates,
82
+ };
83
+ storybookComments = [...storybookComments, newComment];
84
+ return mockResponse(newComment);
85
+ }
18
86
  return api.post(`${import.meta.env.VITE_APP_COMMENT_API_URL}/${mode}/${uid}`, {
19
87
  ...comment,
20
88
  ...references,
@@ -22,9 +90,15 @@ function add(uid, mode, comment, references, keywords) {
22
90
  });
23
91
  }
24
92
  function find(uid, mode, request) {
93
+ if (StorybookUtils.isStorybook()) {
94
+ return mockResponse(storybookComments);
95
+ }
25
96
  return api.post(`${import.meta.env.VITE_APP_COMMENT_API_URL}/${mode}/find/${uid}`, request);
26
97
  }
27
98
  function findAvailableUsers(uid, mode) {
99
+ if (StorybookUtils.isStorybook()) {
100
+ return mockResponse(mockCommentUsers);
101
+ }
28
102
  return api.get(`${import.meta.env.VITE_APP_AUTH_API}/${mode}/${uid}`);
29
103
  }
30
104
  const CommentService = {
@@ -0,0 +1,4 @@
1
+ import { User } from '../../configs/types/User';
2
+ import { Comment } from './types/Comment';
3
+ export declare const mockCommentUsers: User[];
4
+ export declare const mockComments: Comment[];
@@ -0,0 +1,67 @@
1
+ import { UserStatus } from '../../configs/types/User';
2
+ export const mockCommentUsers = [
3
+ {
4
+ uid: 'storybook-user-ana',
5
+ name: 'Ana Silva',
6
+ sub: 'ana.silva@example.com',
7
+ picture: '',
8
+ lang: 'en',
9
+ status: UserStatus.ACTIVE,
10
+ permissions: [],
11
+ },
12
+ {
13
+ uid: 'storybook-user-john',
14
+ name: 'John Doe',
15
+ sub: 'john.doe@example.com',
16
+ picture: '',
17
+ lang: 'en',
18
+ status: UserStatus.ACTIVE,
19
+ permissions: [],
20
+ },
21
+ {
22
+ uid: 'storybook-user-maria',
23
+ name: 'Maria Souza',
24
+ sub: 'maria.souza@example.com',
25
+ picture: '',
26
+ lang: 'en',
27
+ status: UserStatus.ACTIVE,
28
+ permissions: [],
29
+ },
30
+ ];
31
+ export const mockComments = [
32
+ {
33
+ id: 'mock-comment-1',
34
+ value: 'This passage needs a **second review** before publishing.\n\n```\ncheck coordinates\n```',
35
+ date: new Date(Date.now() - 2 * 86400000).toISOString(),
36
+ name: 'Ana Silva',
37
+ user: 'storybook-user-ana',
38
+ picture: '',
39
+ edited: new Date(Date.now() - 86400000).toISOString(),
40
+ requested: [
41
+ {
42
+ uid: 'storybook-user-john',
43
+ name: 'John Doe',
44
+ picture: '',
45
+ email: 'john.doe@example.com',
46
+ read: false,
47
+ },
48
+ ],
49
+ },
50
+ {
51
+ id: 'mock-comment-1-reply',
52
+ value: 'Agreed, I will take a look today.',
53
+ date: new Date(Date.now() - 86400000).toISOString(),
54
+ name: 'John Doe',
55
+ user: 'storybook-user-john',
56
+ picture: '',
57
+ reply: 'mock-comment-1',
58
+ },
59
+ {
60
+ id: 'mock-comment-2',
61
+ value: 'Great work on this entry!',
62
+ date: new Date(Date.now() - 3 * 3600000).toISOString(),
63
+ name: 'Maria Souza',
64
+ user: 'storybook-user-maria',
65
+ picture: '',
66
+ },
67
+ ];
@@ -0,0 +1,11 @@
1
+ import { Comment } from './types/Comment';
2
+ type Props = {
3
+ comments: Comment[];
4
+ hasEditAccess: boolean;
5
+ onRemove: (id: Comment['id']) => void;
6
+ onUpdate: (comment: Comment) => void;
7
+ onEdit: (comment: Comment) => void;
8
+ onReply: (comment: Comment) => void;
9
+ };
10
+ export default function CommentThread({ comments, hasEditAccess, onRemove, onUpdate, onEdit, onReply, }: Props): import("react").JSX.Element;
11
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import UsabilityUtils from '../../functions/UsabilityUtils';
3
+ import CommentInfo from './CommentInfo';
4
+ export default function CommentThread({ comments, hasEditAccess, onRemove, onUpdate, onEdit, onReply, }) {
5
+ const hasReplies = (thisComment) => comments.some((c) => c.reply === thisComment.id);
6
+ const getReplies = (thisComment) => comments.filter((c) => c.reply === thisComment.id);
7
+ return (_jsx(_Fragment, { children: comments.map((el, idx) => (_jsxs("div", { className: "thread", children: [!el.reply && (_jsx(CommentInfo, { comment: el, hasEditAccess: hasEditAccess, onRemove: () => onRemove(el.id), onUpdate: onUpdate, onEdit: () => {
8
+ onEdit(el);
9
+ UsabilityUtils.goToAnchor('comment-top');
10
+ }, onReply: () => {
11
+ onReply(el);
12
+ UsabilityUtils.goToAnchor('comment-top');
13
+ } })), hasReplies(el) && (_jsx("div", { className: "replies", children: getReplies(el).map((rel, idy) => (_jsx(CommentInfo, { comment: rel, hasEditAccess: hasEditAccess, onRemove: () => onRemove(rel.id), onUpdate: onUpdate, onEdit: () => onEdit(rel), onReply: () => onReply(rel) }, `${idx}_${idy}`))) }))] }, idx.valueOf()))) }));
14
+ }
@@ -7,6 +7,8 @@ type Props = {
7
7
  onClose: () => void;
8
8
  mode: 'lexicon' | 'corpus' | 'parser';
9
9
  onChange?: (a: Comment[]) => void;
10
+ /** Opens the add-comment panel on mount (useful in Storybook). */
11
+ initialOpenAdd?: boolean;
10
12
  };
11
- export default function Comments({ uid, keywords, references, mode, onClose, onChange, }: Props): import("react").JSX.Element;
13
+ export default function Comments({ uid, keywords, references, mode, onClose, onChange, initialOpenAdd, }: Props): import("react").JSX.Element;
12
14
  export {};
@@ -1,23 +1,57 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Drawer } from '@mui/material';
3
- import { useEffect, useMemo, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Button, IconButton } from 'tycho-storybook';
6
6
  import AppLoading from '../../common/AppLoading';
7
7
  import AppPlaceholder from '../../common/AppPlaceholder';
8
8
  import SecurityUtils from '../../functions/SecurityUtils';
9
- import UsabilityUtils from '../../functions/UsabilityUtils';
10
9
  import CommentAdd from './CommentAdd';
11
- import CommentInfo from './CommentInfo';
12
10
  import CommentService from './CommentService';
11
+ import CommentThread from './CommentThread';
13
12
  import './style.scss';
14
- export default function Comments({ uid, keywords, references, mode, onClose, onChange, }) {
13
+ const DRAWER_WIDTH_STORAGE_KEY = 'tycho-comments-drawer-width';
14
+ const DRAWER_MIN_WIDTH = 320;
15
+ const getDefaultDrawerWidth = () => {
16
+ if (typeof window === 'undefined')
17
+ return 480;
18
+ const { innerWidth } = window;
19
+ if (innerWidth <= 767)
20
+ return Math.round(innerWidth * 0.9);
21
+ if (innerWidth <= 768)
22
+ return Math.round(innerWidth * 0.6);
23
+ return Math.round(innerWidth * 0.4);
24
+ };
25
+ const clampDrawerWidth = (width) => {
26
+ if (typeof window === 'undefined')
27
+ return width;
28
+ const maxWidth = Math.min(Math.round(window.innerWidth * 0.9), window.innerWidth - 48);
29
+ return Math.min(Math.max(width, DRAWER_MIN_WIDTH), maxWidth);
30
+ };
31
+ const readStoredDrawerWidth = () => {
32
+ try {
33
+ const stored = localStorage.getItem(DRAWER_WIDTH_STORAGE_KEY);
34
+ if (stored) {
35
+ const parsed = Number(stored);
36
+ if (!Number.isNaN(parsed) && parsed > 0) {
37
+ return clampDrawerWidth(parsed);
38
+ }
39
+ }
40
+ }
41
+ catch {
42
+ // ignore storage access errors
43
+ }
44
+ return clampDrawerWidth(getDefaultDrawerWidth());
45
+ };
46
+ export default function Comments({ uid, keywords, references, mode, onClose, onChange, initialOpenAdd = false, }) {
15
47
  const { t } = useTranslation('comments');
16
- const [openAddComment, setOpenAddComment] = useState(false);
48
+ const [openAddComment, setOpenAddComment] = useState(initialOpenAdd);
17
49
  const [comment, setComment] = useState();
18
50
  const [comments, setComments] = useState();
19
51
  const [users, setUsers] = useState([]);
20
52
  const [reply, setReply] = useState();
53
+ const [drawerWidth, setDrawerWidth] = useState(readStoredDrawerWidth);
54
+ const commentAddRef = useRef(null);
21
55
  const load = async () => {
22
56
  try {
23
57
  const [commentsResponse, usersResponse] = await Promise.all([
@@ -35,61 +69,86 @@ export default function Comments({ uid, keywords, references, mode, onClose, onC
35
69
  const hasEditAccess = useMemo(() => {
36
70
  return SecurityUtils.hasAccess(uid, ['ADMIN', 'EDITOR'], mode);
37
71
  }, [uid, mode]);
38
- const hasReplies = (thisComment) => comments && comments.some((c) => c.reply === thisComment.id);
39
- const getReplies = (thisComment) => comments?.filter((c) => c.reply === thisComment.id) || [];
40
72
  useEffect(() => {
41
73
  load();
42
74
  }, []);
43
- return (_jsx(Drawer, { anchor: "right", open: true, onClose: onClose, children: !comments ? (_jsx(AppLoading, {})) : (_jsxs("div", { className: "comments-container", id: "comment-top", children: [_jsxs("div", { className: "header", children: [_jsx(IconButton, { name: "close", size: "small", mode: "ghost", onClick: onClose }), _jsx("span", { className: "title", children: t('label.title.comments') }), _jsx("div", { className: "actions", children: hasEditAccess && (_jsx(Button, { text: t('button.add'), icon: "add", mode: "outlined", size: "small", className: "edit-button", onClick: () => {
44
- setComment(undefined);
45
- setOpenAddComment(!openAddComment);
46
- } })) })] }), _jsxs("div", { className: "body", children: [hasEditAccess && openAddComment && (_jsx(CommentAdd, { uid: uid, mode: mode, users: users, references: references, keywords: keywords, comment: comment, reply: reply, onClose: () => {
47
- setComment(undefined);
48
- setReply(undefined);
49
- setOpenAddComment(false);
50
- }, onChange: (el) => {
51
- if (!comment) {
52
- const updated = [...(comments || []), el];
53
- setComments(updated);
54
- onChange?.(updated);
55
- }
56
- else {
57
- const updated = comments?.map((c) => (c.id === comment.id ? el : c)) ||
58
- [];
59
- setComments(updated);
60
- onChange?.(updated);
61
- }
62
- } })), comments?.length === 0 && (_jsx(AppPlaceholder, { text: t('placeholder.comments.empty') })), comments?.map((el, idx) => (_jsxs("div", { className: "thread", children: [!el.reply && (_jsx(CommentInfo, { comment: el, hasEditAccess: hasEditAccess, onRemove: () => {
63
- const updated = comments?.filter((c) => c.id !== el.id);
75
+ useEffect(() => {
76
+ if (!openAddComment || !comments)
77
+ return;
78
+ const frame = requestAnimationFrame(() => {
79
+ commentAddRef.current?.scrollIntoView({
80
+ behavior: 'smooth',
81
+ block: 'nearest',
82
+ });
83
+ });
84
+ return () => cancelAnimationFrame(frame);
85
+ }, [openAddComment, comments, comment, reply]);
86
+ const handleResizePointerDown = useCallback((event) => {
87
+ event.preventDefault();
88
+ const handle = event.currentTarget;
89
+ handle.setPointerCapture(event.pointerId);
90
+ const previousUserSelect = document.body.style.userSelect;
91
+ const previousCursor = document.body.style.cursor;
92
+ document.body.style.userSelect = 'none';
93
+ document.body.style.cursor = 'col-resize';
94
+ const onPointerMove = (moveEvent) => {
95
+ const nextWidth = clampDrawerWidth(window.innerWidth - moveEvent.clientX);
96
+ setDrawerWidth(nextWidth);
97
+ };
98
+ const onPointerUp = (upEvent) => {
99
+ const nextWidth = clampDrawerWidth(window.innerWidth - upEvent.clientX);
100
+ setDrawerWidth(nextWidth);
101
+ try {
102
+ localStorage.setItem(DRAWER_WIDTH_STORAGE_KEY, String(nextWidth));
103
+ }
104
+ catch {
105
+ // ignore storage access errors
106
+ }
107
+ document.body.style.userSelect = previousUserSelect;
108
+ document.body.style.cursor = previousCursor;
109
+ handle.releasePointerCapture(upEvent.pointerId);
110
+ handle.removeEventListener('pointermove', onPointerMove);
111
+ handle.removeEventListener('pointerup', onPointerUp);
112
+ handle.removeEventListener('pointercancel', onPointerUp);
113
+ };
114
+ handle.addEventListener('pointermove', onPointerMove);
115
+ handle.addEventListener('pointerup', onPointerUp);
116
+ handle.addEventListener('pointercancel', onPointerUp);
117
+ }, []);
118
+ return (_jsx(Drawer, { anchor: "right", open: true, onClose: onClose, PaperProps: {
119
+ className: 'comments-drawer-paper',
120
+ style: { width: drawerWidth },
121
+ }, children: _jsxs("div", { className: "comments-container", id: "comment-top", children: [_jsx("div", { className: "comments-resize-handle", onPointerDown: handleResizePointerDown, role: "separator", "aria-orientation": "vertical", "aria-label": "Resize comments panel" }), !comments ? (_jsx(AppLoading, { pageLoading: true })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "header", children: [_jsx(IconButton, { name: "close", size: "small", mode: "ghost", onClick: onClose }), _jsx("span", { className: "title", children: t('label.title.comments') }), _jsx("div", { className: "actions", children: hasEditAccess && (_jsx(Button, { text: t('button.add'), icon: "add", mode: "outlined", size: "small", className: "edit-button", onClick: () => {
122
+ setComment(undefined);
123
+ setOpenAddComment(!openAddComment);
124
+ } })) })] }), _jsxs("div", { className: "body", children: [comments?.length === 0 && (_jsx(AppPlaceholder, { text: t('placeholder.comments.empty'), useMarginTop: true })), _jsx(CommentThread, { comments: comments, hasEditAccess: hasEditAccess, onRemove: (id) => {
125
+ const updated = comments.filter((c) => c.id !== id);
64
126
  setComments(updated);
65
- onChange && onChange(updated || []);
127
+ onChange?.(updated);
66
128
  }, onUpdate: (thisComment) => {
67
- const updated = comments?.map((c) => c.id === thisComment.id ? { ...c, ...thisComment } : c) || [];
129
+ const updated = comments.map((c) => c.id === thisComment.id ? { ...c, ...thisComment } : c);
68
130
  setComments(updated);
69
131
  onChange?.(updated);
70
- }, onEdit: () => {
132
+ }, onEdit: (el) => {
71
133
  setComment(el);
72
134
  setOpenAddComment(true);
73
- UsabilityUtils.goToAnchor('comment-top');
74
- }, onReply: () => {
135
+ }, onReply: (el) => {
75
136
  setReply(el);
76
137
  setOpenAddComment(true);
77
- UsabilityUtils.goToAnchor('comment-top');
78
- } })), hasReplies(el) && (_jsx("div", { className: "replies", children: getReplies(el).map((rel, idy) => (_jsx(CommentInfo, { comment: rel, hasEditAccess: hasEditAccess, onRemove: () => {
79
- const updated = comments?.filter((c) => c.id !== rel.id);
80
- setComments(updated);
81
- onChange && onChange(updated || []);
82
- }, onUpdate: (thisComment) => {
83
- const updated = comments?.map((c) => c.id === thisComment.id
84
- ? { ...c, ...thisComment }
85
- : c) || [];
86
- setComments(updated);
87
- onChange?.(updated);
88
- }, onEdit: () => {
89
- setComment(rel);
90
- setOpenAddComment(true);
91
- }, onReply: () => {
92
- setReply(rel);
93
- setOpenAddComment(true);
94
- } }, `${idx}_${idy}`))) }))] }, idx.valueOf())))] })] })) }));
138
+ } }), hasEditAccess && openAddComment && (_jsx("div", { ref: commentAddRef, children: _jsx(CommentAdd, { uid: uid, mode: mode, users: users, references: references, keywords: keywords, comment: comment, reply: reply, onClose: () => {
139
+ setComment(undefined);
140
+ setReply(undefined);
141
+ setOpenAddComment(false);
142
+ }, onChange: (el) => {
143
+ if (!comment) {
144
+ const updated = [...(comments || []), el];
145
+ setComments(updated);
146
+ onChange?.(updated);
147
+ }
148
+ else {
149
+ const updated = comments?.map((c) => c.id === comment.id ? el : c) || [];
150
+ setComments(updated);
151
+ onChange?.(updated);
152
+ }
153
+ } }) }))] })] }))] }) }));
95
154
  }
@@ -1,7 +1,44 @@
1
+ .comments-drawer-paper {
2
+ width: 40vw;
3
+ max-width: 90vw;
4
+ min-width: 320px;
5
+ overflow: hidden;
6
+ }
7
+
1
8
  .comments-container {
9
+ position: relative;
2
10
  display: flex;
3
11
  flex-direction: column;
4
- width: 25vw;
12
+ width: 100%;
13
+ height: 100%;
14
+ overflow: hidden;
15
+
16
+ > .comments-resize-handle {
17
+ position: absolute;
18
+ top: 0;
19
+ left: 0;
20
+ width: 6px;
21
+ height: 100%;
22
+ cursor: col-resize;
23
+ z-index: 2;
24
+ touch-action: none;
25
+
26
+ &::after {
27
+ content: '';
28
+ position: absolute;
29
+ top: 0;
30
+ left: 0;
31
+ width: 2px;
32
+ height: 100%;
33
+ background-color: transparent;
34
+ transition: background-color 0.15s ease;
35
+ }
36
+
37
+ &:hover::after,
38
+ &:active::after {
39
+ background-color: var(--border-hover);
40
+ }
41
+ }
5
42
 
6
43
  > .header {
7
44
  display: flex;
@@ -12,7 +49,7 @@
12
49
  > .title {
13
50
  @include tag-medium-1;
14
51
  color: var(--text-secondary);
15
- margin-left: 8px;
52
+ margin-left: var(--spacing-100);
16
53
  }
17
54
 
18
55
  > .actions {
@@ -26,118 +63,137 @@
26
63
  }
27
64
 
28
65
  > .body {
29
- padding: var(--spacing-0) var(--spacing-250);
30
- background-color: #fff;
31
- padding: 16px;
66
+ flex: 1;
67
+ overflow-y: auto;
68
+ padding: var(--spacing-200);
69
+ background-color: var(--background-default);
32
70
 
33
71
  .thread {
34
- border-bottom: 1px solid var(--border-subtle-1);
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: var(--spacing-200);
75
+
76
+ & + .thread {
77
+ margin-top: var(--spacing-100);
78
+ }
35
79
 
36
80
  .comment {
37
81
  display: flex;
38
82
  flex-direction: column;
39
- padding: var(--spacing-150) var(--spacing-100);
83
+ border: 1px solid var(--border-subtle-1);
84
+ border-radius: var(--radius-200);
85
+ overflow: hidden;
86
+ background-color: var(--background-default);
40
87
 
41
- .top {
88
+ .comment-header {
42
89
  display: flex;
43
90
  align-items: center;
91
+ gap: var(--spacing-100);
92
+ padding: 4px 8px;
93
+ background-color: var(--layer-hover-2);
94
+ border-bottom: 1px solid var(--border-subtle-1);
44
95
 
45
96
  > .ds-avatar {
46
- margin-right: 8px;
97
+ flex-shrink: 0;
98
+ }
99
+
100
+ .meta {
101
+ display: flex;
102
+ align-items: baseline;
103
+ flex-wrap: wrap;
104
+ gap: var(--spacing-050) var(--spacing-100);
105
+ min-width: 0;
106
+
107
+ .name {
108
+ @include label-medium-1;
109
+ color: var(--text-primary);
110
+ }
111
+
112
+ .timestamp {
113
+ @include helper-small-1;
114
+ color: var(--text-tertiary);
115
+ }
47
116
  }
48
117
 
49
118
  .buttons {
50
119
  display: flex;
51
120
  margin-left: auto;
52
- gap: 8px;
121
+ flex-shrink: 0;
122
+ gap: var(--spacing-100);
53
123
  }
54
124
  }
55
125
 
56
- .content {
57
- margin-top: 16px;
58
-
59
- .title {
60
- @include tag-medium-1;
61
- color: var(--text-accent);
62
- text-transform: none;
63
- margin-bottom: 4px;
64
- }
126
+ .comment-body {
127
+ padding: var(--spacing-200) var(--spacing-150);
128
+ background-color: var(--background-default);
65
129
 
66
130
  .text {
67
131
  @include body-medium-1;
68
132
  color: var(--text-primary);
133
+ word-break: break-word;
134
+
135
+ .wmde-markdown {
136
+ background: transparent;
137
+ font-size: inherit;
138
+ color: inherit;
139
+
140
+ p {
141
+ margin: 0 0 var(--spacing-100);
142
+
143
+ &:last-child {
144
+ margin-bottom: 0;
145
+ }
146
+ }
147
+
148
+ pre,
149
+ code {
150
+ border-radius: var(--radius-100);
151
+ }
152
+
153
+ pre {
154
+ overflow-x: auto;
155
+ }
156
+
157
+ ul,
158
+ ol {
159
+ margin: 0 0 var(--spacing-100);
160
+ padding-left: var(--spacing-250);
161
+ }
162
+ }
69
163
  }
70
164
 
71
- .date {
165
+ .meta-line {
72
166
  @include helper-small-1;
73
167
  color: var(--text-tertiary);
74
- margin-top: 8px;
168
+ margin-top: var(--spacing-100);
75
169
  }
76
170
  }
77
171
  }
78
172
 
79
173
  .replies {
80
- margin-left: 32px;
81
-
82
- .comment {
83
- margin-top: 8px;
84
-
85
- .top {
86
- .name {
87
- font-size: 14px !important;
88
- }
89
- }
90
-
91
- .content {
92
- .title {
93
- font-size: 13px !important;
94
- }
95
-
96
- .text {
97
- font-size: 13px !important;
98
- }
99
- }
100
- }
101
- }
102
- }
103
-
104
- .form {
105
- display: flex;
106
- flex-direction: column;
107
- padding-bottom: 16px;
108
- border-bottom: 1px solid var(--border-subtle-1);
109
-
110
- > .ds-input-text,
111
- > .ds-select-text {
112
- margin-bottom: 8px;
113
- }
114
-
115
- > .buttons {
174
+ margin-left: var(--spacing-400);
116
175
  display: flex;
117
- gap: 8px;
118
-
119
- > .ds-button {
120
- width: 100%;
121
- }
176
+ flex-direction: column;
177
+ gap: var(--spacing-100);
122
178
  }
123
179
  }
124
180
  }
125
181
  }
126
182
 
127
183
  @media (max-width: 1366px) {
128
- .comments-container {
184
+ .comments-drawer-paper {
129
185
  width: 40vw;
130
186
  }
131
187
  }
132
188
 
133
189
  @media (max-width: 768px) {
134
- .comments-container {
190
+ .comments-drawer-paper {
135
191
  width: 60vw;
136
192
  }
137
193
  }
138
194
 
139
195
  @media (max-width: 767px) {
140
- .comments-container {
196
+ .comments-drawer-paper {
141
197
  width: 90vw;
142
198
  }
143
199
  }
@@ -1,13 +1,11 @@
1
1
  export type CommentRequest = {
2
2
  id?: string;
3
- title?: string;
4
3
  value?: string;
5
4
  requested?: string[];
6
5
  coordinates?: string[];
7
6
  };
8
7
  export type Comment = {
9
8
  id: string;
10
- title?: string;
11
9
  value: string;
12
10
  date: string;
13
11
  edited?: string;
@@ -0,0 +1,4 @@
1
+ declare const StorybookUtils: {
2
+ isStorybook: () => boolean;
3
+ };
4
+ export default StorybookUtils;
@@ -0,0 +1,8 @@
1
+ /** True when running inside Storybook (dev server on port 6006, or the `__STORYBOOK__` flag set in .storybook/preview.tsx). */
2
+ const isStorybook = () => typeof window !== 'undefined' &&
3
+ Boolean(window.location.port === '6006' ||
4
+ window.__STORYBOOK__);
5
+ const StorybookUtils = {
6
+ isStorybook,
7
+ };
8
+ export default StorybookUtils;
@@ -5,6 +5,7 @@ export { default as FormUtils } from './FormUtils';
5
5
  export { default as ImageUtils } from './ImageUtils';
6
6
  export { default as SecurityUtils } from './SecurityUtils';
7
7
  export { default as SentenceUtils } from './SentenceUtils';
8
+ export { default as StorybookUtils } from './StorybookUtils';
8
9
  export { EDITION_TOOLS, default as ToolsUtils, type PlatformTools, } from './ToolsUtils';
9
10
  export type { OpenDocumentParams, RedirectPageParams } from './ToolsUtils';
10
11
  export { default as UsabilityUtils } from './UsabilityUtils';
@@ -5,6 +5,7 @@ export { default as FormUtils } from './FormUtils';
5
5
  export { default as ImageUtils } from './ImageUtils';
6
6
  export { default as SecurityUtils } from './SecurityUtils';
7
7
  export { default as SentenceUtils } from './SentenceUtils';
8
+ export { default as StorybookUtils } from './StorybookUtils';
8
9
  export { EDITION_TOOLS, default as ToolsUtils, } from './ToolsUtils';
9
10
  export { default as UsabilityUtils } from './UsabilityUtils';
10
11
  export { conlluViewerConfigurations } from './conllu/ConlluViewerConstants';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tycho-components",
3
3
  "private": false,
4
- "version": "0.40.9",
4
+ "version": "0.41.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -51,6 +51,7 @@
51
51
  "@svgdotjs/svg.js": "^3.2.4",
52
52
  "@svgdotjs/svg.panzoom.js": "^2.1.2",
53
53
  "@tanstack/react-table": "^8.20.6",
54
+ "@uiw/react-md-editor": "^4.1.2",
54
55
  "classnames": "^2.5.1",
55
56
  "cytoscape": "^3.28.1",
56
57
  "cytoscape-dagre": "^2.5.0",
@@ -84,7 +85,7 @@
84
85
  "react-i18next": "^13.0.2",
85
86
  "react-router-dom": "^6.14.2",
86
87
  "react-toastify": "^9.1.3",
87
- "tycho-storybook": "0.11.0",
88
+ "tycho-storybook": "0.12.0",
88
89
  "wavesurfer-react": "^2.2.2",
89
90
  "wavesurfer.js": "^6.6.3"
90
91
  },
@@ -114,7 +115,7 @@
114
115
  "react-toastify": "^9.1.3",
115
116
  "sass-embedded": "^1.97.2",
116
117
  "storybook": "^10.1.11",
117
- "tycho-storybook": "^0.11.0",
118
+ "tycho-storybook": "^0.12.0",
118
119
  "typescript": "^5.7.3",
119
120
  "vite": "^7.0.0",
120
121
  "vitest": "^3.2.6",
@@ -1,70 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { yupResolver } from "@hookform/resolvers/yup";
3
- import { useForm } from "react-hook-form";
4
- import { useTranslation } from "react-i18next";
5
- import { Button, SelectMultipleField, TextField, } from "tycho-storybook";
6
- import * as yup from "yup";
7
- import { useMessageUtils } from "../../configs/useMessageUtils";
8
- import FormUtils from "../../functions/FormUtils";
9
- import "./style.scss";
10
- import { EMPTY_COMMENT_REQUEST, } from "./types/Comment";
11
- import CommentService from "./CommentService";
12
- export default function CommentAdd({ uid, mode, users, references, keywords, comment, reply, onClose, onChange, }) {
13
- const { t } = useTranslation("comments");
14
- const { isLoading, dispatchLoading } = useMessageUtils();
15
- const getFormDefaultValues = () => {
16
- if (comment)
17
- return {
18
- value: comment.value,
19
- title: comment.title,
20
- requested: comment.requested?.map((r) => r.uid),
21
- };
22
- if (reply) {
23
- return {
24
- ...EMPTY_COMMENT_REQUEST,
25
- reply: reply.id,
26
- requested: [reply.user],
27
- };
28
- }
29
- return EMPTY_COMMENT_REQUEST;
30
- };
31
- const createdForm = useForm({
32
- resolver: yupResolver(getFormSchema(t)),
33
- mode: "onChange",
34
- defaultValues: getFormDefaultValues(),
35
- });
36
- const handleAdd = () => {
37
- if (isLoading())
38
- return;
39
- dispatchLoading(true);
40
- CommentService.add(uid, mode, createdForm.getValues(), references, keywords).then((r) => {
41
- dispatchLoading(false);
42
- onClose();
43
- onChange(r.data);
44
- createdForm.reset(EMPTY_COMMENT_REQUEST);
45
- });
46
- };
47
- const handleUpdate = () => {
48
- if (!comment)
49
- return;
50
- CommentService.update(comment.id, createdForm.getValues()).then((r) => {
51
- onClose();
52
- onChange(r.data);
53
- createdForm.reset(EMPTY_COMMENT_REQUEST);
54
- });
55
- };
56
- return (_jsxs("div", { className: "form", children: [_jsx(TextField, { label: t("input.value"), attr: "value", createdForm: createdForm, showEndAdornment: false, placeholder: t("common:generic.placeholder"), required: true, multiline: true }), _jsx(TextField, { label: t("input.title"), attr: "title", createdForm: createdForm, showEndAdornment: false, placeholder: t("common:generic.placeholder") }), _jsx(SelectMultipleField, { label: reply ? t("input.user.reply") : t("input.user"), attr: "requested", createdForm: createdForm, options: [
57
- {
58
- label: t("common:generic.placeholder.select"),
59
- value: "",
60
- },
61
- ...FormUtils.convertList(users, "name", "uid"),
62
- ], showEndAdornment: false }), _jsxs("div", { className: "buttons", children: [_jsx(Button, { onClick: onClose, text: t("button.discard"), color: "danger" }), _jsx(Button, { onClick: comment ? handleUpdate : handleAdd, text: comment ? t("label.button.update") : t("label.button.add"), disabled: !createdForm.formState.isValid })] })] }));
63
- }
64
- const getFormSchema = (t) => yup.object().shape({
65
- id: yup.string().optional(),
66
- title: yup.string().optional(),
67
- value: yup.string().required(t("common:validation.required")),
68
- requested: yup.array().of(yup.string().defined()).optional(),
69
- coordinates: yup.array().of(yup.string().defined()).optional(),
70
- });