@lowdefy/blocks-tiptap 5.6.0 → 6.1.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.
@@ -15,18 +15,18 @@
15
15
  */ import React, { useEffect } from 'react';
16
16
  import { withBlockDefaults } from '@lowdefy/block-utils';
17
17
  import { type } from '@lowdefy/helpers';
18
- import { useEditor, EditorContent } from '@tiptap/react';
18
+ import { EditorContent } from '@tiptap/react';
19
19
  import { mergeAttributes } from '@tiptap/core';
20
20
  import Mention from '@tiptap/extension-mention';
21
21
  import Label from '@lowdefy/blocks-antd/blocks/Label/Label.js';
22
22
  import PopoverMenu from '../utils/PopoverMenu.js';
23
- import buildExtensions from '../utils/buildExtensions.js';
24
23
  import computeHeightStyle from '../utils/computeHeightStyle.js';
25
24
  import statusClass from '../utils/statusClass.js';
25
+ import useTiptapEditor from '../utils/useTiptapEditor.js';
26
26
  import suggestion from './suggestion.js';
27
27
  import useGroupMembersPopover from './useGroupMembersPopover.js';
28
28
  import useTiptapMentionState from './useTiptapMentionState.js';
29
- import './style.module.css';
29
+ import './style.css';
30
30
  // A mention node's `id` attribute holds whatever option the user picked — a
31
31
  // plain string, or the option object ({ label, value, tag }) — so the label
32
32
  // and the DOM identifier are both derived from it. Content loaded back from
@@ -92,12 +92,15 @@ const LowdefyMention = Mention.extend({
92
92
  }
93
93
  });
94
94
  const TiptapMentionInput = ({ blockId, components: { Icon, Link }, events, loading, methods, properties, required, validation, value })=>{
95
+ const uploadPolicyRequestId = properties.uploadPolicyRequestId ?? properties.s3PostPolicyRequestId;
96
+ const downloadPolicyRequestId = properties.downloadPolicyRequestId;
95
97
  const { emit, insertImage } = useTiptapMentionState({
96
98
  value,
97
- methods
99
+ methods,
100
+ hasDownloadRequest: !type.isNone(downloadPolicyRequestId)
98
101
  });
99
102
  const disabled = properties.disabled === true || loading;
100
- const uploadEnabled = !type.isNone(properties.s3PostPolicyRequestId);
103
+ const uploadEnabled = !type.isNone(uploadPolicyRequestId);
101
104
  const char = properties.mentions?.char ?? '@';
102
105
  const allowSpaces = properties.mentions?.allowSpaces !== false;
103
106
  const limit = properties.mentions?.limit ?? 5;
@@ -144,29 +147,22 @@ const TiptapMentionInput = ({ blockId, components: { Icon, Link }, events, loadi
144
147
  limit
145
148
  })
146
149
  });
147
- const extensions = buildExtensions({
148
- properties,
149
- insertImage,
150
- mentionExtension,
151
- uploadEnabled
152
- });
153
150
  const heightStyle = computeHeightStyle({
154
151
  rows: properties.rows,
155
152
  autoSize: properties.autoSize
156
153
  });
157
- const editor = useEditor({
154
+ const editor = useTiptapEditor({
155
+ disabled,
158
156
  editorProps: {
159
157
  items: type.isArray(properties.mentions?.options) ? properties.mentions.options : []
160
158
  },
161
- extensions,
162
- content: value?.html || '',
163
- editable: ()=>!disabled,
164
- onUpdate: ({ editor })=>{
165
- emit(editor);
166
- methods.triggerEvent({
167
- name: 'onChange'
168
- });
169
- }
159
+ emit,
160
+ insertImage,
161
+ mentionExtension,
162
+ methods,
163
+ properties,
164
+ uploadEnabled,
165
+ value
170
166
  });
171
167
  useGroupMembersPopover({
172
168
  editor,
@@ -186,18 +182,35 @@ const TiptapMentionInput = ({ blockId, components: { Icon, Link }, events, loadi
186
182
  ]);
187
183
  useEffect(()=>{
188
184
  if (uploadEnabled) {
185
+ if (!type.isNone(properties.s3PostPolicyRequestId)) {
186
+ console.warn('TiptapMentionInput property "s3PostPolicyRequestId" is deprecated. Use "uploadPolicyRequestId" instead.');
187
+ }
189
188
  methods.registerEvent({
190
- name: '__getS3PostPolicy',
189
+ name: '__getUploadPolicy',
191
190
  actions: [
192
191
  {
193
- id: '__getS3PostPolicy',
192
+ id: '__getUploadPolicy',
194
193
  type: 'Request',
195
194
  params: [
196
- properties.s3PostPolicyRequestId
195
+ uploadPolicyRequestId
197
196
  ]
198
197
  }
199
198
  ]
200
199
  });
200
+ if (!type.isNone(downloadPolicyRequestId)) {
201
+ methods.registerEvent({
202
+ name: '__getDownloadPolicy',
203
+ actions: [
204
+ {
205
+ id: '__getDownloadPolicy',
206
+ type: 'Request',
207
+ params: [
208
+ downloadPolicyRequestId
209
+ ]
210
+ }
211
+ ]
212
+ });
213
+ }
201
214
  }
202
215
  if (!type.isNone(properties.mentionsRequestId)) {
203
216
  methods.registerEvent({
@@ -214,62 +227,6 @@ const TiptapMentionInput = ({ blockId, components: { Icon, Link }, events, loadi
214
227
  });
215
228
  }
216
229
  }, []);
217
- useEffect(()=>{
218
- if (!editor) return;
219
- methods.registerMethod('clear', ()=>{
220
- editor.commands.clearContent();
221
- emit(editor);
222
- });
223
- methods.registerMethod('setContent', (args)=>{
224
- editor.commands.setContent(args?.html ?? '');
225
- emit(editor);
226
- });
227
- methods.registerMethod('focus', ()=>{
228
- editor.commands.focus();
229
- });
230
- }, [
231
- editor
232
- ]);
233
- // External value.html → editor sync. One-way only: we read value.html and
234
- // push it into the editor with setContent(..., false) so tiptap's onUpdate
235
- // does not fire. No write-back via emit() — that would race with concurrent
236
- // SetState calls (child effects fire before parent effects, so a sibling's
237
- // onMount SetState could be overwritten by our derived emit). Derived
238
- // fields (text/markdown/fileList/mentions) are populated on user interaction
239
- // via onUpdate; downstream consumers of seeded content should read
240
- // value.html directly, or include the fields they need in their SetState
241
- // payload.
242
- useEffect(()=>{
243
- if (!editor) return;
244
- const next = value?.html ?? '';
245
- const current = editor.getHTML();
246
- if (next !== current) {
247
- editor.commands.setContent(next, false);
248
- }
249
- }, [
250
- value?.html,
251
- editor
252
- ]);
253
- useEffect(()=>{
254
- if (!editor) return;
255
- editor.setOptions({
256
- editable: !disabled
257
- });
258
- }, [
259
- editor,
260
- disabled
261
- ]);
262
- useEffect(()=>{
263
- if (!editor) return;
264
- const placeholderExt = editor.extensionManager.extensions.find((extension)=>extension.name === 'placeholder');
265
- if (placeholderExt) {
266
- placeholderExt.options.placeholder = properties.placeholder ?? '';
267
- editor.view.dispatch(editor.state.tr);
268
- }
269
- }, [
270
- editor,
271
- properties.placeholder
272
- ]);
273
230
  const wrapperClass = [
274
231
  'tiptap-wrapper',
275
232
  properties.bordered === false ? 'tiptap-wrapper-borderless' : '',
@@ -0,0 +1,296 @@
1
+ # Copyright 2020-2026 Lowdefy, Inc
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ - title: Default with static mentions
16
+ fullWidth: true
17
+ blocks:
18
+ - id: tiptap_mention_default
19
+ type: TiptapMentionInput
20
+ properties:
21
+ title: Ticket note
22
+ placeholder: 'Type @ to mention someone.'
23
+ mentions:
24
+ options:
25
+ - label: Alice
26
+ value:
27
+ _id: user_1
28
+ name: Alice
29
+ - label: Bob
30
+ value:
31
+ _id: user_2
32
+ name: Bob
33
+ - label: Carol
34
+ value:
35
+ _id: user_3
36
+ name: Carol
37
+
38
+ - title: Pre-populated content via SetState on onMount
39
+ fullWidth: true
40
+ blocks:
41
+ - id: tiptap_mention_prepopulated
42
+ type: TiptapMentionInput
43
+ events:
44
+ onMount:
45
+ - id: tiptap_mention_prepopulated_set
46
+ type: SetState
47
+ params:
48
+ tiptap_mention_prepopulated:
49
+ html: >
50
+ <p>Thanks <span class="tiptap-mention">@Alice</span> for the review.
51
+ Assigning follow-up to <span class="tiptap-mention">@Bob</span>.</p>
52
+ properties:
53
+ title: Ticket summary
54
+ mentions:
55
+ options:
56
+ - label: Alice
57
+ value:
58
+ _id: user_1
59
+ - label: Bob
60
+ value:
61
+ _id: user_2
62
+
63
+ - title: Custom trigger character (hashtags)
64
+ fullWidth: true
65
+ blocks:
66
+ - id: tiptap_mention_hashtags
67
+ type: TiptapMentionInput
68
+ properties:
69
+ title: Tag the ticket
70
+ placeholder: 'Type # to add a tag.'
71
+ mentions:
72
+ char: '#'
73
+ allowSpaces: false
74
+ options:
75
+ - label: bug
76
+ value:
77
+ _id: tag_bug
78
+ - label: feature
79
+ value:
80
+ _id: tag_feature
81
+ - label: needs-triage
82
+ value:
83
+ _id: tag_triage
84
+
85
+ - title: Mentions rendered as links
86
+ fullWidth: true
87
+ blocks:
88
+ - id: tiptap_mention_links
89
+ type: TiptapMentionInput
90
+ properties:
91
+ title: Link mentions to profile pages
92
+ placeholder: 'Type @ — selections render as <a>.'
93
+ mentions:
94
+ options:
95
+ - label: Alice
96
+ value:
97
+ _id: user_1
98
+ - label: Bob
99
+ value:
100
+ _id: user_2
101
+ getHref:
102
+ _function:
103
+ __string.concat:
104
+ - '/users/'
105
+ - __args: 0.value._id
106
+
107
+ - title: autoSize — minRows 2, maxRows 5
108
+ fullWidth: true
109
+ blocks:
110
+ - id: tiptap_mention_autosize
111
+ type: TiptapMentionInput
112
+ properties:
113
+ title: Grows between 2 and 5 rows
114
+ placeholder: 'Type @ to mention. Scrolls past 5 rows.'
115
+ autoSize:
116
+ minRows: 2
117
+ maxRows: 5
118
+ mentions:
119
+ options:
120
+ - label: Alice
121
+ value:
122
+ _id: user_1
123
+ - label: Bob
124
+ value:
125
+ _id: user_2
126
+
127
+ - title: Validation — required, with error state
128
+ fullWidth: true
129
+ blocks:
130
+ - id: tiptap_mention_validation
131
+ type: TiptapMentionInput
132
+ required: true
133
+ properties:
134
+ title: Mention a teammate (required)
135
+ placeholder: 'Type @ and pick someone, then click Validate.'
136
+ rows: 3
137
+ mentions:
138
+ options:
139
+ - label: Alice
140
+ value:
141
+ _id: user_1
142
+ - label: Bob
143
+ value:
144
+ _id: user_2
145
+ validate:
146
+ - status: error
147
+ message: 'Please mention at least one teammate.'
148
+ pass:
149
+ _gt:
150
+ - _string.length:
151
+ _state: tiptap_mention_validation.text
152
+ - 0
153
+ - id: tiptap_mention_validation_button
154
+ type: Button
155
+ properties:
156
+ title: Validate
157
+ events:
158
+ onClick:
159
+ - id: tiptap_mention_validation_run
160
+ type: Validate
161
+ params:
162
+ - tiptap_mention_validation
163
+
164
+ - title: Disabled (read-only)
165
+ fullWidth: true
166
+ blocks:
167
+ - id: tiptap_mention_disabled
168
+ type: TiptapMentionInput
169
+ events:
170
+ onMount:
171
+ - id: tiptap_mention_disabled_set
172
+ type: SetState
173
+ params:
174
+ tiptap_mention_disabled:
175
+ html: '<p>Mentions render inline: <span class="tiptap-mention">@Alice</span>.</p>'
176
+ properties:
177
+ title: Read only
178
+ disabled: true
179
+
180
+ - title: Sectioned menu
181
+ fullWidth: true
182
+ blocks:
183
+ - id: tiptap_mention_sectioned
184
+ type: TiptapMentionInput
185
+ properties:
186
+ title: Mention people or roles
187
+ placeholder: 'Type @ — options are grouped, each section capped at 3.'
188
+ mentions:
189
+ limit: 3
190
+ options:
191
+ - label: Alice
192
+ value:
193
+ _id: user_1
194
+ tag:
195
+ section: People
196
+ - label: Bob
197
+ value:
198
+ _id: user_2
199
+ tag:
200
+ section: People
201
+ - label: Carol
202
+ value:
203
+ _id: user_3
204
+ tag:
205
+ section: People
206
+ - label: Dave
207
+ value:
208
+ _id: user_4
209
+ tag:
210
+ section: People
211
+ - label: Finance
212
+ value:
213
+ _id: role_finance
214
+ tag:
215
+ section: Roles
216
+ - label: Developers
217
+ value:
218
+ _id: role_devs
219
+ tag:
220
+ section: Roles
221
+
222
+ - title: Group mentions
223
+ fullWidth: true
224
+ blocks:
225
+ - id: tiptap_mention_groups
226
+ type: TiptapMentionInput
227
+ properties:
228
+ title: Mention people or groups
229
+ placeholder: 'Type @ — groups render as coloured chips, people as links.'
230
+ mentions:
231
+ # shown on hover of a group chip, keyed by tag.group; may load from a request
232
+ groupMembers:
233
+ finance:
234
+ - name: Jane Doe
235
+ email: jane@example.com
236
+ - name: John Smith
237
+ email: john@example.com
238
+ devs:
239
+ - name: Ada Lovelace
240
+ email: ada@example.com
241
+ - name: Alan Turing
242
+ email: alan@example.com
243
+ # people carry value.href → <a>; groups omit it → nullish → <span>
244
+ getHref:
245
+ _function:
246
+ __args: 0.value.href
247
+ options:
248
+ - label: Alice
249
+ value:
250
+ name: Alice
251
+ href: '/contacts?_id=user_1'
252
+ tag:
253
+ section: People
254
+ - label: Bob
255
+ value:
256
+ name: Bob
257
+ href: '/contacts?_id=user_2'
258
+ tag:
259
+ section: People
260
+ - label: Finance
261
+ value:
262
+ role: finance-admin
263
+ tag:
264
+ section: Roles
265
+ group: finance
266
+ color: '#722ed1'
267
+ - label: Developers
268
+ value:
269
+ role: developer
270
+ tag:
271
+ section: Roles
272
+ group: devs
273
+ color: '#13c2c2'
274
+
275
+ - title: Minimal — tables and images disabled
276
+ fullWidth: true
277
+ blocks:
278
+ - id: tiptap_mention_minimal
279
+ type: TiptapMentionInput
280
+ properties:
281
+ title: Chat-style
282
+ placeholder: 'Type @ to mention.'
283
+ image:
284
+ disabled: true
285
+ table:
286
+ disabled: true
287
+ highlight:
288
+ disabled: true
289
+ mentions:
290
+ options:
291
+ - label: Alice
292
+ value:
293
+ _id: user_1
294
+ - label: Bob
295
+ value:
296
+ _id: user_2
@@ -35,7 +35,7 @@
35
35
  items: {
36
36
  type: 'string'
37
37
  },
38
- description: 'Mime-types accepted by the drag/drop and paste file handler. Defaults to common image types. Only used when s3PostPolicyRequestId is set.'
38
+ description: 'Mime-types accepted by the drag/drop and paste file handler. Defaults to common image types. Only used when uploadPolicyRequestId is set.'
39
39
  },
40
40
  autoSize: {
41
41
  oneOf: [
@@ -211,9 +211,17 @@
211
211
  minimum: 1,
212
212
  description: 'Fix the editor height to exactly this many rows. Takes precedence over autoSize.'
213
213
  },
214
+ downloadPolicyRequestId: {
215
+ type: 'string',
216
+ description: 'Id of a download request (e.g. AwsS3PresignedGetObject) used to resolve the inserted image URL after upload. Inline images persist in saved content, so the request should set public: true (and the upload request the provider public-read acl) to return a stable, non-expiring URL. When unset, a legacy unsigned object URL is constructed from the upload response (S3-shaped, deprecated).'
217
+ },
214
218
  s3PostPolicyRequestId: {
215
219
  type: 'string',
216
- description: 'Id of a request that returns an S3 presigned POST policy. When set, images dragged or pasted into the editor are uploaded via that policy.'
220
+ description: 'Deprecated — use uploadPolicyRequestId instead.'
221
+ },
222
+ uploadPolicyRequestId: {
223
+ type: 'string',
224
+ description: 'Id of an upload-policy request (e.g. AwsS3PresignedPostPolicy, GcsSignedPostPolicy, AzureBlobUploadSas). When set, images dragged or pasted into the editor are uploaded via that request.'
217
225
  },
218
226
  size: {
219
227
  type: 'string',
@@ -1,17 +1,17 @@
1
- @import '../utils/style.module.css';
1
+ @import '../utils/style.css';
2
2
 
3
- :global(.tiptap-mention) {
3
+ .tiptap-mention {
4
4
  box-decoration-break: clone;
5
5
  font-weight: 600;
6
6
  color: var(--ant-color-link);
7
7
  }
8
8
 
9
- :global(.tiptap-mention-group) {
9
+ .tiptap-mention-group {
10
10
  box-decoration-break: clone;
11
11
  font-weight: 600;
12
12
  }
13
13
 
14
- :global(.tiptap-mention-items) {
14
+ .tiptap-mention-items {
15
15
  background: var(--ant-color-bg-container);
16
16
  border-radius: var(--ant-border-radius, 8px);
17
17
  box-shadow:
@@ -24,7 +24,7 @@
24
24
  position: relative;
25
25
  }
26
26
 
27
- :global(.tiptap-mention-item) {
27
+ .tiptap-mention-item {
28
28
  background: transparent;
29
29
  border: 1px solid transparent;
30
30
  border-radius: var(--ant-border-radius, 8px);
@@ -36,12 +36,12 @@
36
36
  width: 100%;
37
37
  }
38
38
 
39
- :global(.tiptap-mention-item.is-selected) {
39
+ .tiptap-mention-item.is-selected {
40
40
  border-color: var(--ant-color-primary);
41
41
  background: var(--ant-control-item-bg-hover);
42
42
  }
43
43
 
44
- :global(.tiptap-mention-section) {
44
+ .tiptap-mention-section {
45
45
  color: var(--ant-color-text-secondary);
46
46
  font-size: var(--ant-font-size-sm, 12px);
47
47
  font-weight: 600;
@@ -50,66 +50,66 @@
50
50
  padding: 0.3rem 0.4rem 0.1rem;
51
51
  }
52
52
 
53
- :global(.tableWrapper),
54
- :global(.tiptap-table) {
53
+ .tableWrapper,
54
+ .tiptap-table {
55
55
  display: block;
56
56
  overflow-x: auto;
57
57
  max-width: 80%;
58
58
  }
59
59
 
60
- :global(.tableWrapper) :global(table),
61
- :global(.tiptap-table) :global(table) {
60
+ .tableWrapper table,
61
+ .tiptap-table table {
62
62
  border-collapse: collapse;
63
63
  width: 100%;
64
64
  min-width: 75px;
65
65
  }
66
66
 
67
- :global(.tableWrapper) :global(colgroup) :global(col),
68
- :global(.tiptap-table) :global(colgroup) :global(col) {
67
+ .tableWrapper colgroup col,
68
+ .tiptap-table colgroup col {
69
69
  width: auto;
70
70
  }
71
71
 
72
- :global(.tableWrapper) :global(table),
73
- :global(.tableWrapper) :global(th),
74
- :global(.tableWrapper) :global(td),
75
- :global(.tiptap-table) :global(table),
76
- :global(.tiptap-table) :global(th),
77
- :global(.tiptap-table) :global(td) {
72
+ .tableWrapper table,
73
+ .tableWrapper th,
74
+ .tableWrapper td,
75
+ .tiptap-table table,
76
+ .tiptap-table th,
77
+ .tiptap-table td {
78
78
  border: 1px solid var(--ant-color-border);
79
79
  }
80
80
 
81
- :global(.tableWrapper) :global(th),
82
- :global(.tableWrapper) :global(td),
83
- :global(.tiptap-table) :global(th),
84
- :global(.tiptap-table) :global(td) {
81
+ .tableWrapper th,
82
+ .tableWrapper td,
83
+ .tiptap-table th,
84
+ .tiptap-table td {
85
85
  padding: 2px;
86
86
  text-align: left;
87
87
  }
88
88
 
89
- :global(.tableWrapper) :global(th),
90
- :global(.tiptap-table) :global(th) {
89
+ .tableWrapper th,
90
+ .tiptap-table th {
91
91
  background-color: var(--ant-color-fill-tertiary);
92
92
  font-weight: bold;
93
93
  }
94
94
 
95
- :global(.tableWrapper) :global(tr):nth-child(even),
96
- :global(.tiptap-table) :global(tr):nth-child(even) {
95
+ .tableWrapper tr:nth-child(even),
96
+ .tiptap-table tr:nth-child(even) {
97
97
  background-color: var(--ant-color-fill-quaternary);
98
98
  }
99
99
 
100
- :global(.tableWrapper) :global(tr):hover,
101
- :global(.tiptap-table) :global(tr):hover {
100
+ .tableWrapper tr:hover,
101
+ .tiptap-table tr:hover {
102
102
  background-color: var(--ant-color-primary-bg);
103
103
  }
104
104
 
105
- :global(.tableWrapper) :global(td) :global(p),
106
- :global(.tableWrapper) :global(th) :global(p),
107
- :global(.tiptap-table) :global(td) :global(p),
108
- :global(.tiptap-table) :global(th) :global(p) {
105
+ .tableWrapper td p,
106
+ .tableWrapper th p,
107
+ .tiptap-table td p,
108
+ .tiptap-table th p {
109
109
  margin: 0;
110
110
  }
111
111
 
112
- :global(.tiptap-mention-group-popover) {
112
+ .tiptap-mention-group-popover {
113
113
  background: var(--ant-color-bg-container);
114
114
  border-radius: var(--ant-border-radius, 8px);
115
115
  box-shadow:
@@ -121,28 +121,28 @@
121
121
  padding: 0.3rem 0.6rem;
122
122
  }
123
123
 
124
- :global(.tiptap-mention-group-popover-header) {
124
+ .tiptap-mention-group-popover-header {
125
125
  font-weight: 600;
126
126
  padding: 0.1rem 0;
127
127
  }
128
128
 
129
- :global(.tiptap-mention-group-popover-item) {
129
+ .tiptap-mention-group-popover-item {
130
130
  padding: 0.1rem 0;
131
131
  }
132
132
 
133
- :global(.tiptap-mention-group-popover-item) > div {
133
+ .tiptap-mention-group-popover-item > div {
134
134
  overflow: hidden;
135
135
  text-overflow: ellipsis;
136
136
  white-space: nowrap;
137
137
  }
138
138
 
139
- :global(.tiptap-mention-group-popover-email) {
139
+ .tiptap-mention-group-popover-email {
140
140
  color: var(--ant-color-text-secondary);
141
141
  font-size: var(--ant-font-size-sm, 12px);
142
142
  line-height: 1.3;
143
143
  }
144
144
 
145
- :global(.tiptap-mention-group-popover-empty) {
145
+ .tiptap-mention-group-popover-empty {
146
146
  color: var(--ant-color-text-secondary);
147
147
  padding: 0.1rem 0;
148
148
  }