@sqlrooms/discuss 0.27.0 → 0.28.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/README.md +71 -304
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,74 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
Threaded discussion slice and UI components for SQLRooms.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use this package to add comments/replies linked to data points or UI anchors.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **Real-time state management**: Built on Zustand for reactive updates
|
|
10
|
-
- **Customizable rendering**: Flexible component system for custom UI implementations
|
|
11
|
-
- **Delete confirmation**: Built-in confirmation dialogs for safe content removal
|
|
12
|
-
- **Highlighting**: Visual highlighting of specific discussions
|
|
13
|
-
|
|
14
|
-
## Main Components
|
|
15
|
-
|
|
16
|
-
### Core Components
|
|
17
|
-
|
|
18
|
-
#### `DiscussionList`
|
|
19
|
-
|
|
20
|
-
The main container component that renders all discussions with built-in forms for adding, editing, and replying to content.
|
|
21
|
-
|
|
22
|
-
```tsx
|
|
23
|
-
import {DiscussionList} from '@sqlrooms/discuss';
|
|
24
|
-
|
|
25
|
-
<DiscussionList
|
|
26
|
-
className="flex flex-col gap-4"
|
|
27
|
-
renderComment={(props) => <CustomCommentRenderer {...props} />}
|
|
28
|
-
renderDiscussion={(props) => <CustomDiscussionRenderer {...props} />}
|
|
29
|
-
/>;
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
#### `CommentItem`
|
|
33
|
-
|
|
34
|
-
Individual comment renderer with built-in edit/delete actions.
|
|
35
|
-
|
|
36
|
-
```tsx
|
|
37
|
-
import {CommentItem} from '@sqlrooms/discuss';
|
|
38
|
-
|
|
39
|
-
<CommentItem comment={comment} discussion={discussion}>
|
|
40
|
-
<div className="flex flex-col gap-1">
|
|
41
|
-
<div className="text-muted-foreground text-xs">
|
|
42
|
-
{comment.userId} - {formatTimeRelative(comment.timestamp)}
|
|
43
|
-
</div>
|
|
44
|
-
<div className="whitespace-pre-wrap text-sm">{comment.text}</div>
|
|
45
|
-
</div>
|
|
46
|
-
</CommentItem>;
|
|
7
|
+
```bash
|
|
8
|
+
npm install @sqlrooms/discuss @sqlrooms/room-shell @sqlrooms/ui @sqlrooms/utils
|
|
47
9
|
```
|
|
48
10
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Container for a complete discussion thread including root comment and replies.
|
|
52
|
-
|
|
53
|
-
```tsx
|
|
54
|
-
import {DiscussionItem} from '@sqlrooms/discuss';
|
|
55
|
-
|
|
56
|
-
<DiscussionItem
|
|
57
|
-
discussion={discussion}
|
|
58
|
-
renderComment={customCommentRenderer}
|
|
59
|
-
className="rounded border p-4"
|
|
60
|
-
/>;
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### Store Integration
|
|
64
|
-
|
|
65
|
-
#### Store Setup with `createDiscussSlice`
|
|
66
|
-
|
|
67
|
-
To use the discussion system, you need to integrate it with your room store using `createDiscussSlice`:
|
|
11
|
+
## Store setup
|
|
68
12
|
|
|
69
13
|
```tsx
|
|
70
14
|
import {
|
|
71
|
-
createDefaultDiscussConfig,
|
|
72
15
|
createDiscussSlice,
|
|
73
16
|
DiscussSliceConfig,
|
|
74
17
|
DiscussSliceState,
|
|
@@ -77,284 +20,108 @@ import {
|
|
|
77
20
|
BaseRoomConfig,
|
|
78
21
|
createRoomShellSlice,
|
|
79
22
|
createRoomStore,
|
|
23
|
+
LayoutConfig,
|
|
80
24
|
RoomShellSliceState,
|
|
25
|
+
persistSliceConfigs,
|
|
81
26
|
} from '@sqlrooms/room-shell';
|
|
82
|
-
import {z} from 'zod';
|
|
83
|
-
|
|
84
|
-
// 1. Extend your app config with DiscussSliceConfig
|
|
85
|
-
export const RoomConfig = BaseRoomConfig.merge(DiscussSliceConfig);
|
|
86
|
-
export type RoomConfig = z.infer<typeof RoomConfig>;
|
|
87
27
|
|
|
88
|
-
|
|
89
|
-
export type RoomState = RoomShellSliceState<RoomConfig> & DiscussSliceState;
|
|
28
|
+
type RoomState = RoomShellSliceState & DiscussSliceState;
|
|
90
29
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
connector: yourDatabaseConnector,
|
|
100
|
-
config: {
|
|
101
|
-
// Include default discuss config
|
|
102
|
-
...createDefaultDiscussConfig(),
|
|
103
|
-
// Your other config...
|
|
104
|
-
layout: {
|
|
105
|
-
/* your layout */
|
|
106
|
-
},
|
|
107
|
-
dataSources: [
|
|
108
|
-
/* your data sources */
|
|
109
|
-
],
|
|
110
|
-
},
|
|
111
|
-
room: {
|
|
112
|
-
// Your room configuration
|
|
30
|
+
export const {roomStore, useRoomStore} = createRoomStore<RoomState>(
|
|
31
|
+
persistSliceConfigs(
|
|
32
|
+
{
|
|
33
|
+
name: 'discuss-demo-storage',
|
|
34
|
+
sliceConfigSchemas: {
|
|
35
|
+
room: BaseRoomConfig,
|
|
36
|
+
layout: LayoutConfig,
|
|
37
|
+
discuss: DiscussSliceConfig,
|
|
113
38
|
},
|
|
114
|
-
}
|
|
115
|
-
|
|
39
|
+
},
|
|
40
|
+
(set, get, store) => ({
|
|
41
|
+
...createRoomShellSlice({
|
|
42
|
+
config: {title: 'Discuss Demo', dataSources: []},
|
|
43
|
+
})(set, get, store),
|
|
44
|
+
...createDiscussSlice({userId: 'user-1'})(set, get, store),
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
116
47
|
);
|
|
117
48
|
```
|
|
118
49
|
|
|
119
|
-
|
|
50
|
+
## Initialization
|
|
120
51
|
|
|
121
|
-
|
|
52
|
+
Call `room.initialize()` after store creation so slice lifecycle hooks run:
|
|
122
53
|
|
|
123
54
|
```tsx
|
|
124
|
-
import {
|
|
55
|
+
import {useEffect} from 'react';
|
|
56
|
+
import {roomStore} from './store';
|
|
125
57
|
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
58
|
+
function App() {
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
void roomStore.getState().room.initialize();
|
|
61
|
+
}, []);
|
|
131
62
|
|
|
132
|
-
|
|
133
|
-
const addDiscussion = useStoreWithDiscussion(
|
|
134
|
-
(state) => state.discuss.addDiscussion,
|
|
135
|
-
);
|
|
136
|
-
const setReplyToItem = useStoreWithDiscussion(
|
|
137
|
-
(state) => state.discuss.setReplyToItem,
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
// Add a new discussion
|
|
141
|
-
const handleAddDiscussion = (text: string, anchorId?: string) => {
|
|
142
|
-
addDiscussion(text, anchorId);
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// Start replying to a discussion
|
|
146
|
-
const handleReply = (discussionId: string) => {
|
|
147
|
-
setReplyToItem({discussionId});
|
|
148
|
-
};
|
|
63
|
+
return null;
|
|
149
64
|
}
|
|
150
65
|
```
|
|
151
66
|
|
|
152
|
-
##
|
|
153
|
-
|
|
154
|
-
### Basic Discussion Panel
|
|
67
|
+
## Render discussions
|
|
155
68
|
|
|
156
69
|
```tsx
|
|
157
|
-
import {
|
|
158
|
-
import {useStoreWithDiscussion} from '@sqlrooms/discuss';
|
|
70
|
+
import {CommentItem, DiscussionList} from '@sqlrooms/discuss';
|
|
159
71
|
import {formatTimeRelative} from '@sqlrooms/utils';
|
|
72
|
+
import {useRoomStore} from './store';
|
|
160
73
|
|
|
161
|
-
|
|
162
|
-
const discussions =
|
|
163
|
-
(state) => state.discuss.config.discussions,
|
|
164
|
-
);
|
|
74
|
+
export function DiscussionPanel() {
|
|
75
|
+
const discussions = useRoomStore((state) => state.discuss.config.discussions);
|
|
165
76
|
|
|
166
|
-
|
|
167
|
-
<div className="
|
|
168
|
-
|
|
169
|
-
<div className="py-10 text-center text-gray-400">
|
|
170
|
-
<p>No comments yet. Start a discussion!</p>
|
|
171
|
-
</div>
|
|
172
|
-
) : (
|
|
173
|
-
<DiscussionList
|
|
174
|
-
className="flex flex-col gap-4"
|
|
175
|
-
renderComment={(props) => {
|
|
176
|
-
const {comment, discussion} = props;
|
|
177
|
-
const {anchorId} = discussion;
|
|
178
|
-
const isRootComment = comment.id === discussion.rootComment.id;
|
|
77
|
+
if (discussions.length === 0) {
|
|
78
|
+
return <div className="p-4 text-sm text-muted-foreground">No discussions yet.</div>;
|
|
79
|
+
}
|
|
179
80
|
|
|
180
|
-
return (
|
|
181
|
-
<CommentItem {...props}>
|
|
182
|
-
<div className="flex flex-col gap-1">
|
|
183
|
-
{anchorId && isRootComment && (
|
|
184
|
-
<div className="text-md flex items-center gap-2 font-medium">
|
|
185
|
-
📍 Linked to: {anchorId}
|
|
186
|
-
</div>
|
|
187
|
-
)}
|
|
188
|
-
<div className="text-muted-foreground text-xs">
|
|
189
|
-
{comment.userId} - {formatTimeRelative(comment.timestamp)}
|
|
190
|
-
</div>
|
|
191
|
-
<div className="whitespace-pre-wrap text-sm">
|
|
192
|
-
{comment.text}
|
|
193
|
-
</div>
|
|
194
|
-
</div>
|
|
195
|
-
</CommentItem>
|
|
196
|
-
);
|
|
197
|
-
}}
|
|
198
|
-
/>
|
|
199
|
-
)}
|
|
200
|
-
</div>
|
|
201
|
-
);
|
|
202
|
-
};
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### Anchor-Based Discussions
|
|
206
|
-
|
|
207
|
-
Link discussions to specific data points or UI elements:
|
|
208
|
-
|
|
209
|
-
```tsx
|
|
210
|
-
import {useStoreWithDiscussion} from '@sqlrooms/discuss';
|
|
211
|
-
|
|
212
|
-
function DataVisualization() {
|
|
213
|
-
const addDiscussion = useStoreWithDiscussion(
|
|
214
|
-
(state) => state.discuss.addDiscussion,
|
|
215
|
-
);
|
|
216
|
-
const setHighlightedDiscussionId = useStoreWithDiscussion(
|
|
217
|
-
(state) => state.discuss.setHighlightedDiscussionId,
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
// Handle clicking on a data point
|
|
221
|
-
const handleDataPointClick = (dataId: string) => {
|
|
222
|
-
// Add a discussion linked to this data point
|
|
223
|
-
addDiscussion('What do you think about this data point?', dataId);
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
// Highlight related discussion when hovering over data
|
|
227
|
-
const handleDataPointHover = (dataId: string) => {
|
|
228
|
-
const discussions =
|
|
229
|
-
useStoreWithDiscussion.getState().discuss.config.discussions;
|
|
230
|
-
const relatedDiscussion = discussions.find((d) => d.anchorId === dataId);
|
|
231
|
-
if (relatedDiscussion) {
|
|
232
|
-
setHighlightedDiscussionId(relatedDiscussion.id);
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
return (
|
|
237
|
-
<div>
|
|
238
|
-
{/* Your data visualization with clickable elements */}
|
|
239
|
-
<div
|
|
240
|
-
onClick={() => handleDataPointClick('airport-LAX')}
|
|
241
|
-
onMouseEnter={() => handleDataPointHover('airport-LAX')}
|
|
242
|
-
className="cursor-pointer hover:bg-blue-100"
|
|
243
|
-
>
|
|
244
|
-
LAX Airport
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### Custom Comment Rendering
|
|
252
|
-
|
|
253
|
-
```tsx
|
|
254
|
-
import {DiscussionList, CommentItem} from '@sqlrooms/discuss';
|
|
255
|
-
import {Avatar, Badge} from '@sqlrooms/ui';
|
|
256
|
-
|
|
257
|
-
const CustomDiscussionPanel = () => {
|
|
258
81
|
return (
|
|
259
82
|
<DiscussionList
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
<div className="flex-1">
|
|
270
|
-
<div className="mb-1 flex items-center gap-2">
|
|
271
|
-
<span className="font-medium">{comment.userId}</span>
|
|
272
|
-
<Badge variant="secondary" className="text-xs">
|
|
273
|
-
{formatTimeRelative(comment.timestamp)}
|
|
274
|
-
</Badge>
|
|
275
|
-
</div>
|
|
276
|
-
<div className="text-sm leading-relaxed">{comment.text}</div>
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
</CommentItem>
|
|
280
|
-
);
|
|
281
|
-
}}
|
|
83
|
+
className="flex flex-col gap-4"
|
|
84
|
+
renderComment={(props) => (
|
|
85
|
+
<CommentItem {...props}>
|
|
86
|
+
<div className="text-xs text-muted-foreground">
|
|
87
|
+
{props.comment.userId} • {formatTimeRelative(props.comment.timestamp)}
|
|
88
|
+
</div>
|
|
89
|
+
<div className="whitespace-pre-wrap text-sm">{props.comment.text}</div>
|
|
90
|
+
</CommentItem>
|
|
91
|
+
)}
|
|
282
92
|
/>
|
|
283
93
|
);
|
|
284
|
-
}
|
|
94
|
+
}
|
|
285
95
|
```
|
|
286
96
|
|
|
287
|
-
|
|
97
|
+
## Actions
|
|
288
98
|
|
|
289
99
|
```tsx
|
|
290
|
-
import {
|
|
291
|
-
|
|
292
|
-
function DiscussionManager() {
|
|
293
|
-
const {
|
|
294
|
-
addDiscussion,
|
|
295
|
-
removeDiscussion,
|
|
296
|
-
addComment,
|
|
297
|
-
setReplyToItem,
|
|
298
|
-
setEditingItem,
|
|
299
|
-
submitEdit,
|
|
300
|
-
} = useStoreWithDiscussion((state) => state.discuss);
|
|
301
|
-
|
|
302
|
-
// Add a new discussion
|
|
303
|
-
const createDiscussion = () => {
|
|
304
|
-
addDiscussion('New discussion topic', 'optional-anchor-id');
|
|
305
|
-
};
|
|
100
|
+
import {useRoomStore} from './store';
|
|
101
|
+
import {Button} from '@sqlrooms/ui';
|
|
306
102
|
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// Edit a comment
|
|
314
|
-
const editComment = (discussionId: string, commentId: string) => {
|
|
315
|
-
setEditingItem({discussionId, commentId});
|
|
316
|
-
// User can now edit in the form and call submitEdit
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
// Direct comment addition (bypassing UI state)
|
|
320
|
-
const addDirectComment = (discussionId: string, text: string) => {
|
|
321
|
-
addComment(discussionId, text);
|
|
322
|
-
};
|
|
103
|
+
function DiscussionActions() {
|
|
104
|
+
const addDiscussion = useRoomStore((state) => state.discuss.addDiscussion);
|
|
105
|
+
const setReplyToItem = useRoomStore((state) => state.discuss.setReplyToItem);
|
|
106
|
+
const submitEdit = useRoomStore((state) => state.discuss.submitEdit);
|
|
323
107
|
|
|
324
108
|
return (
|
|
325
109
|
<div className="flex gap-2">
|
|
326
|
-
<
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
110
|
+
<Button onClick={() => addDiscussion('Initial note', 'data-point-id')}>
|
|
111
|
+
Add discussion
|
|
112
|
+
</Button>
|
|
113
|
+
<Button onClick={() => setReplyToItem({discussionId: 'some-id'})}>
|
|
114
|
+
Reply mode
|
|
115
|
+
</Button>
|
|
116
|
+
<Button onClick={() => submitEdit('Reply text')}>Submit</Button>
|
|
331
117
|
</div>
|
|
332
118
|
);
|
|
333
119
|
}
|
|
334
120
|
```
|
|
335
121
|
|
|
336
|
-
##
|
|
337
|
-
|
|
338
|
-
### Types
|
|
339
|
-
|
|
340
|
-
- `Comment`: Individual comment with id, userId, text, timestamp, and optional parentId
|
|
341
|
-
- `Discussion`: Container with id, optional anchorId, rootComment, and array of reply comments
|
|
342
|
-
- `DiscussSliceConfig`: Configuration type for the discuss slice
|
|
343
|
-
- `DiscussSliceState`: State type including all discussion actions and UI state
|
|
344
|
-
|
|
345
|
-
### Key Functions
|
|
346
|
-
|
|
347
|
-
- `createDiscussSlice({ userId })`: Creates the discussion slice for your store
|
|
348
|
-
- `createDefaultDiscussConfig()`: Returns default configuration for discussions
|
|
349
|
-
- `useStoreWithDiscussion(selector)`: Hook to access discussion state and actions
|
|
350
|
-
|
|
351
|
-
### Main Actions
|
|
352
|
-
|
|
353
|
-
- `submitEdit(text)`: Submit based on current UI state (add/reply/edit)
|
|
354
|
-
- `addDiscussion(text, anchorId?)`: Add new discussion
|
|
355
|
-
- `addComment(discussionId, text, parentId?)`: Add reply to discussion
|
|
356
|
-
- `setReplyToItem(item)`: Set reply context for UI
|
|
357
|
-
- `setEditingItem(item)`: Set editing context for UI
|
|
358
|
-
- `setHighlightedDiscussionId(id)`: Highlight specific discussion
|
|
122
|
+
## Main exports
|
|
359
123
|
|
|
360
|
-
|
|
124
|
+
- components: `DiscussionList`, `DiscussionItem`, `CommentItem`, `DeleteConfirmDialog`
|
|
125
|
+
- slice: `createDiscussSlice`, `createDefaultDiscussConfig`
|
|
126
|
+
- config/types: `DiscussSliceConfig`, `DiscussSliceState`, `Discussion`, `Comment`
|
|
127
|
+
- hook: `useStoreWithDiscussion`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqlrooms/discuss",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@paralleldrive/cuid2": "^3.0.0",
|
|
19
|
-
"@sqlrooms/room-shell": "0.
|
|
20
|
-
"@sqlrooms/ui": "0.
|
|
21
|
-
"@sqlrooms/utils": "0.
|
|
19
|
+
"@sqlrooms/room-shell": "0.28.0",
|
|
20
|
+
"@sqlrooms/ui": "0.28.0",
|
|
21
|
+
"@sqlrooms/utils": "0.28.0",
|
|
22
22
|
"immer": "^11.0.1",
|
|
23
23
|
"lucide-react": "^0.556.0",
|
|
24
24
|
"zod": "^4.1.8"
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
"lint": "eslint .",
|
|
34
34
|
"typedoc": "typedoc"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "dcac54f8adf77240e293c93d224a0ce9fd8142a9"
|
|
37
37
|
}
|