@adminforth/markdown 1.2.10 → 1.4.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.
@@ -10,10 +10,10 @@
10
10
  "author": "",
11
11
  "license": "ISC",
12
12
  "dependencies": {
13
- "@milkdown/kit": "^7.18.0",
14
- "@milkdown/theme-nord": "^7.18.0",
15
- "@milkdown/vue": "^7.18.0",
16
13
  "dompurify": "^3.2.4",
17
- "marked": "^15.0.7"
14
+ "monaco-editor": "^0.45.0",
15
+ "marked": "^15.0.7",
16
+ "turndown": "^7.2.2",
17
+ "turndown-plugin-gfm": "^1.0.2"
18
18
  }
19
19
  }
package/dist/index.js CHANGED
@@ -54,6 +54,18 @@ export default class MarkdownPlugin extends AdminForthPlugin {
54
54
  if (!field) {
55
55
  throw new Error(`Field '${this.options.attachments.attachmentFieldName}' not found in resource '${this.options.attachments.attachmentResource}'`);
56
56
  }
57
+ if (this.options.attachments.attachmentTitleFieldName) {
58
+ const titleField = yield resource.columns.find(c => c.name === this.options.attachments.attachmentTitleFieldName);
59
+ if (!titleField) {
60
+ throw new Error(`Field '${this.options.attachments.attachmentTitleFieldName}' not found in resource '${this.options.attachments.attachmentResource}'`);
61
+ }
62
+ }
63
+ if (this.options.attachments.attachmentAltFieldName) {
64
+ const altField = yield resource.columns.find(c => c.name === this.options.attachments.attachmentAltFieldName);
65
+ if (!altField) {
66
+ throw new Error(`Field '${this.options.attachments.attachmentAltFieldName}' not found in resource '${this.options.attachments.attachmentResource}'`);
67
+ }
68
+ }
57
69
  const plugin = yield adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.options.attachments.attachmentResource &&
58
70
  p.pluginOptions.pathColumnName === this.options.attachments.attachmentFieldName);
59
71
  if (!plugin) {
@@ -86,7 +98,6 @@ export default class MarkdownPlugin extends AdminForthPlugin {
86
98
  meta: {
87
99
  pluginInstanceId: this.pluginInstanceId,
88
100
  columnName: fieldName,
89
- pluginType: 'crepe',
90
101
  uploadPluginInstanceId: (_a = this.uploadPlugin) === null || _a === void 0 ? void 0 : _a.pluginInstanceId,
91
102
  },
92
103
  };
@@ -95,43 +106,65 @@ export default class MarkdownPlugin extends AdminForthPlugin {
95
106
  meta: {
96
107
  pluginInstanceId: this.pluginInstanceId,
97
108
  columnName: fieldName,
98
- pluginType: 'crepe',
99
109
  uploadPluginInstanceId: (_b = this.uploadPlugin) === null || _b === void 0 ? void 0 : _b.pluginInstanceId,
100
110
  },
101
111
  };
102
112
  const editorRecordPkField = resourceConfig.columns.find(c => c.primaryKey);
103
113
  if (this.options.attachments) {
104
- function getAttachmentPathes(markdown) {
114
+ const extractKeyFromUrl = (url) => url.replace(/^https:\/\/[^\/]+\/+/, '');
115
+ function getAttachmentMetas(markdown) {
116
+ var _a, _b, _c;
105
117
  if (!markdown) {
106
118
  return [];
107
119
  }
108
- const s3PathRegex = /!\[.*?\]\((https:\/\/.*?\/.*?)(\?.*)?\)/g;
109
- const matches = [...markdown.matchAll(s3PathRegex)];
110
- return matches
111
- .map(match => match[1])
112
- .filter(src => src.includes("s3") || src.includes("amazonaws"));
120
+ // Minimal image syntax: ![alt](src) or ![alt](src "title") or ![alt](src 'title')
121
+ // We only track https URLs and only those that look like S3/AWS public URLs.
122
+ const imageRegex = /!\[([^\]]*)\]\(\s*(https:\/\/[^\s)]+)\s*(?:\s+(?:"([^"]*)"|'([^']*)'))?\s*\)/g;
123
+ const byKey = new Map();
124
+ for (const match of markdown.matchAll(imageRegex)) {
125
+ const altRaw = (_a = match[1]) !== null && _a !== void 0 ? _a : '';
126
+ const srcRaw = match[2];
127
+ const titleRaw = (_c = ((_b = match[3]) !== null && _b !== void 0 ? _b : match[4])) !== null && _c !== void 0 ? _c : null;
128
+ const srcNoQuery = srcRaw.split('?')[0];
129
+ if (!srcNoQuery.includes('s3') && !srcNoQuery.includes('amazonaws')) {
130
+ continue;
131
+ }
132
+ const key = extractKeyFromUrl(srcNoQuery);
133
+ byKey.set(key, {
134
+ key,
135
+ alt: altRaw,
136
+ title: titleRaw,
137
+ });
138
+ }
139
+ return [...byKey.values()];
113
140
  }
114
- const createAttachmentRecords = (adminforth, options, recordId, s3Paths, adminUser) => __awaiter(this, void 0, void 0, function* () {
115
- const extractKey = (s3Paths) => s3Paths.replace(/^https:\/\/[^\/]+\/+/, '');
141
+ const createAttachmentRecords = (adminforth, options, recordId, metas, adminUser) => __awaiter(this, void 0, void 0, function* () {
142
+ if (!metas.length) {
143
+ return;
144
+ }
116
145
  process.env.HEAVY_DEBUG && console.log('📸 Creating attachment records', JSON.stringify(recordId));
117
146
  try {
118
- yield Promise.all(s3Paths.map((s3Path) => __awaiter(this, void 0, void 0, function* () {
119
- console.log('Processing path:', s3Path);
147
+ yield Promise.all(metas.map((meta) => __awaiter(this, void 0, void 0, function* () {
120
148
  try {
149
+ const recordToCreate = {
150
+ [options.attachments.attachmentFieldName]: meta.key,
151
+ [options.attachments.attachmentRecordIdFieldName]: recordId,
152
+ [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
153
+ };
154
+ if (options.attachments.attachmentTitleFieldName) {
155
+ recordToCreate[options.attachments.attachmentTitleFieldName] = meta.title;
156
+ }
157
+ if (options.attachments.attachmentAltFieldName) {
158
+ recordToCreate[options.attachments.attachmentAltFieldName] = meta.alt;
159
+ }
121
160
  yield adminforth.createResourceRecord({
122
161
  resource: this.attachmentResource,
123
- record: {
124
- [options.attachments.attachmentFieldName]: extractKey(s3Path),
125
- [options.attachments.attachmentRecordIdFieldName]: recordId,
126
- [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
127
- },
162
+ record: recordToCreate,
128
163
  adminUser,
129
- response: {}
130
164
  });
131
- console.log('Successfully created record for:', s3Path);
132
165
  }
133
166
  catch (err) {
134
- console.error('Error creating record for', s3Path, err);
167
+ console.error('Error creating record for', meta.key, err);
135
168
  }
136
169
  })));
137
170
  }
@@ -139,28 +172,77 @@ export default class MarkdownPlugin extends AdminForthPlugin {
139
172
  console.error('Error in Promise.all', err);
140
173
  }
141
174
  });
142
- const deleteAttachmentRecords = (adminforth, options, s3Paths, adminUser) => __awaiter(this, void 0, void 0, function* () {
143
- if (!s3Paths.length) {
175
+ const deleteAttachmentRecords = (adminforth, options, recordId, keys, adminUser) => __awaiter(this, void 0, void 0, function* () {
176
+ if (!keys.length) {
144
177
  return;
145
178
  }
146
179
  const attachmentPrimaryKeyField = this.attachmentResource.columns.find(c => c.primaryKey);
147
- const attachments = yield adminforth.resource(options.attachments.attachmentResource).list(Filters.IN(options.attachments.attachmentFieldName, s3Paths));
180
+ const attachments = yield adminforth.resource(options.attachments.attachmentResource).list([
181
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
182
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId),
183
+ Filters.IN(options.attachments.attachmentFieldName, keys),
184
+ ]);
148
185
  yield Promise.all(attachments.map((a) => __awaiter(this, void 0, void 0, function* () {
149
186
  yield adminforth.deleteResourceRecord({
150
187
  resource: this.attachmentResource,
151
188
  recordId: a[attachmentPrimaryKeyField.name],
152
189
  adminUser,
153
190
  record: a,
154
- response: {}
191
+ });
192
+ })));
193
+ });
194
+ const updateAttachmentRecordsMetadata = (adminforth, options, recordId, metas, adminUser) => __awaiter(this, void 0, void 0, function* () {
195
+ if (!metas.length) {
196
+ return;
197
+ }
198
+ if (!options.attachments.attachmentTitleFieldName && !options.attachments.attachmentAltFieldName) {
199
+ return;
200
+ }
201
+ const attachmentPrimaryKeyField = this.attachmentResource.columns.find(c => c.primaryKey);
202
+ const metaByKey = new Map(metas.map(m => [m.key, m]));
203
+ const existingAparts = yield adminforth.resource(options.attachments.attachmentResource).list([
204
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
205
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
206
+ ]);
207
+ yield Promise.all(existingAparts.map((a) => __awaiter(this, void 0, void 0, function* () {
208
+ var _a, _b, _c, _d;
209
+ const key = a[options.attachments.attachmentFieldName];
210
+ const meta = metaByKey.get(key);
211
+ if (!meta) {
212
+ return;
213
+ }
214
+ const patch = {};
215
+ if (options.attachments.attachmentTitleFieldName) {
216
+ const field = options.attachments.attachmentTitleFieldName;
217
+ if (((_a = a[field]) !== null && _a !== void 0 ? _a : null) !== ((_b = meta.title) !== null && _b !== void 0 ? _b : null)) {
218
+ patch[field] = meta.title;
219
+ }
220
+ }
221
+ if (options.attachments.attachmentAltFieldName) {
222
+ const field = options.attachments.attachmentAltFieldName;
223
+ if (((_c = a[field]) !== null && _c !== void 0 ? _c : null) !== ((_d = meta.alt) !== null && _d !== void 0 ? _d : null)) {
224
+ patch[field] = meta.alt;
225
+ }
226
+ }
227
+ if (!Object.keys(patch).length) {
228
+ return;
229
+ }
230
+ yield adminforth.updateResourceRecord({
231
+ resource: this.attachmentResource,
232
+ recordId: a[attachmentPrimaryKeyField.name],
233
+ record: patch,
234
+ oldRecord: a,
235
+ adminUser,
155
236
  });
156
237
  })));
157
238
  });
158
239
  (resourceConfig.hooks.create.afterSave).push((_a) => __awaiter(this, [_a], void 0, function* ({ record, adminUser }) {
159
240
  // find all s3Paths in the html
160
- const s3Paths = getAttachmentPathes(record[this.options.fieldName]);
161
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths', s3Paths);
241
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
242
+ const keys = metas.map(m => m.key);
243
+ process.env.HEAVY_DEBUG && console.log('📸 Found attachment keys', keys);
162
244
  // create attachment records
163
- yield createAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], s3Paths, adminUser);
245
+ yield createAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], metas, adminUser);
164
246
  return { ok: true };
165
247
  }));
166
248
  // after edit we need to delete attachments that are not in the html anymore
@@ -176,18 +258,22 @@ export default class MarkdownPlugin extends AdminForthPlugin {
176
258
  Filters.EQ(this.options.attachments.attachmentRecordIdFieldName, recordId),
177
259
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
178
260
  ]);
179
- const existingS3Paths = existingAparts.map((a) => a[this.options.attachments.attachmentFieldName]);
180
- const newS3Paths = getAttachmentPathes(record[this.options.fieldName]);
181
- process.env.HEAVY_DEBUG && console.log('📸 Existing s3Paths (from db)', existingS3Paths);
182
- process.env.HEAVY_DEBUG && console.log('📸 Found new s3Paths (from text)', newS3Paths);
183
- const toDelete = existingS3Paths.filter(s3Path => !newS3Paths.includes(s3Path));
184
- const toAdd = newS3Paths.filter(s3Path => !existingS3Paths.includes(s3Path));
185
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to delete', toDelete);
186
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to add', toAdd);
261
+ const existingKeys = existingAparts.map((a) => a[this.options.attachments.attachmentFieldName]);
262
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
263
+ const newKeys = metas.map(m => m.key);
264
+ process.env.HEAVY_DEBUG && console.log('📸 Existing keys (from db)', existingKeys);
265
+ process.env.HEAVY_DEBUG && console.log('📸 Found new keys (from text)', newKeys);
266
+ const toDelete = existingKeys.filter(key => !newKeys.includes(key));
267
+ const toAdd = newKeys.filter(key => !existingKeys.includes(key));
268
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', toDelete);
269
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to add', toAdd);
270
+ const metasToAdd = metas.filter(m => toAdd.includes(m.key));
187
271
  yield Promise.all([
188
- deleteAttachmentRecords(adminforth, this.options, toDelete, adminUser),
189
- createAttachmentRecords(adminforth, this.options, recordId, toAdd, adminUser)
272
+ deleteAttachmentRecords(adminforth, this.options, recordId, toDelete, adminUser),
273
+ createAttachmentRecords(adminforth, this.options, recordId, metasToAdd, adminUser)
190
274
  ]);
275
+ // Keep alt/title in sync for existing attachments too
276
+ yield updateAttachmentRecordsMetadata(adminforth, this.options, recordId, metas, adminUser);
191
277
  return { ok: true };
192
278
  }));
193
279
  // after delete we need to delete all attachments
@@ -196,9 +282,9 @@ export default class MarkdownPlugin extends AdminForthPlugin {
196
282
  Filters.EQ(this.options.attachments.attachmentRecordIdFieldName, record[editorRecordPkField.name]),
197
283
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
198
284
  ]);
199
- const existingS3Paths = existingAparts.map((a) => a[this.options.attachments.attachmentFieldName]);
200
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to delete', existingS3Paths);
201
- yield deleteAttachmentRecords(adminforth, this.options, existingS3Paths, adminUser);
285
+ const existingKeys = existingAparts.map((a) => a[this.options.attachments.attachmentFieldName]);
286
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', existingKeys);
287
+ yield deleteAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], existingKeys, adminUser);
202
288
  return { ok: true };
203
289
  }));
204
290
  }
package/index.ts CHANGED
@@ -52,6 +52,20 @@ export default class MarkdownPlugin extends AdminForthPlugin {
52
52
  if (!field) {
53
53
  throw new Error(`Field '${this.options.attachments!.attachmentFieldName}' not found in resource '${this.options.attachments!.attachmentResource}'`);
54
54
  }
55
+
56
+ if (this.options.attachments.attachmentTitleFieldName) {
57
+ const titleField = await resource.columns.find(c => c.name === this.options.attachments!.attachmentTitleFieldName);
58
+ if (!titleField) {
59
+ throw new Error(`Field '${this.options.attachments!.attachmentTitleFieldName}' not found in resource '${this.options.attachments!.attachmentResource}'`);
60
+ }
61
+ }
62
+
63
+ if (this.options.attachments.attachmentAltFieldName) {
64
+ const altField = await resource.columns.find(c => c.name === this.options.attachments!.attachmentAltFieldName);
65
+ if (!altField) {
66
+ throw new Error(`Field '${this.options.attachments!.attachmentAltFieldName}' not found in resource '${this.options.attachments!.attachmentResource}'`);
67
+ }
68
+ }
55
69
 
56
70
  const plugin = await adminforth.activatedPlugins.find(p =>
57
71
  p.resourceConfig!.resourceId === this.options.attachments!.attachmentResource &&
@@ -91,7 +105,6 @@ export default class MarkdownPlugin extends AdminForthPlugin {
91
105
  meta: {
92
106
  pluginInstanceId: this.pluginInstanceId,
93
107
  columnName: fieldName,
94
- pluginType: 'crepe',
95
108
  uploadPluginInstanceId: this.uploadPlugin?.pluginInstanceId,
96
109
  },
97
110
  };
@@ -101,51 +114,80 @@ export default class MarkdownPlugin extends AdminForthPlugin {
101
114
  meta: {
102
115
  pluginInstanceId: this.pluginInstanceId,
103
116
  columnName: fieldName,
104
- pluginType: 'crepe',
105
117
  uploadPluginInstanceId: this.uploadPlugin?.pluginInstanceId,
106
118
  },
107
119
  };
108
120
  const editorRecordPkField = resourceConfig.columns.find(c => c.primaryKey);
109
121
  if (this.options.attachments) {
110
122
 
111
- function getAttachmentPathes(markdown: string): string[] {
123
+ type AttachmentMeta = { key: string; alt: string | null; title: string | null };
124
+
125
+ const extractKeyFromUrl = (url: string) => url.replace(/^https:\/\/[^\/]+\/+/, '');
126
+
127
+ function getAttachmentMetas(markdown: string): AttachmentMeta[] {
112
128
  if (!markdown) {
113
129
  return [];
114
130
  }
115
131
 
116
- const s3PathRegex = /!\[.*?\]\((https:\/\/.*?\/.*?)(\?.*)?\)/g;
117
-
118
- const matches = [...markdown.matchAll(s3PathRegex)];
132
+ // Minimal image syntax: ![alt](src) or ![alt](src "title") or ![alt](src 'title')
133
+ // We only track https URLs and only those that look like S3/AWS public URLs.
134
+ const imageRegex = /!\[([^\]]*)\]\(\s*(https:\/\/[^\s)]+)\s*(?:\s+(?:"([^"]*)"|'([^']*)'))?\s*\)/g;
135
+
136
+ const byKey = new Map<string, AttachmentMeta>();
137
+ for (const match of markdown.matchAll(imageRegex)) {
138
+ const altRaw = match[1] ?? '';
139
+ const srcRaw = match[2];
140
+ const titleRaw = (match[3] ?? match[4]) ?? null;
141
+
142
+ const srcNoQuery = srcRaw.split('?')[0];
143
+ if (!srcNoQuery.includes('s3') && !srcNoQuery.includes('amazonaws')) {
144
+ continue;
145
+ }
119
146
 
120
- return matches
121
- .map(match => match[1])
122
- .filter(src => src.includes("s3") || src.includes("amazonaws"));
147
+ const key = extractKeyFromUrl(srcNoQuery);
148
+ byKey.set(key, {
149
+ key,
150
+ alt: altRaw,
151
+ title: titleRaw,
152
+ });
153
+ }
154
+ return [...byKey.values()];
123
155
  }
124
156
 
125
157
  const createAttachmentRecords = async (
126
- adminforth: IAdminForth, options: PluginOptions, recordId: any, s3Paths: string[], adminUser: AdminUser
158
+ adminforth: IAdminForth,
159
+ options: PluginOptions,
160
+ recordId: any,
161
+ metas: AttachmentMeta[],
162
+ adminUser: AdminUser
127
163
  ) => {
128
- const extractKey = (s3Paths: string) => s3Paths.replace(/^https:\/\/[^\/]+\/+/, '');
164
+ if (!metas.length) {
165
+ return;
166
+ }
129
167
  process.env.HEAVY_DEBUG && console.log('📸 Creating attachment records', JSON.stringify(recordId))
130
168
  try {
131
- await Promise.all(s3Paths.map(async (s3Path) => {
132
- console.log('Processing path:', s3Path);
169
+ await Promise.all(metas.map(async (meta) => {
133
170
  try {
134
- await adminforth.createResourceRecord(
135
- {
136
- resource: this.attachmentResource,
137
- record: {
138
- [options.attachments.attachmentFieldName]: extractKey(s3Path),
139
- [options.attachments.attachmentRecordIdFieldName]: recordId,
140
- [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
141
- },
142
- adminUser,
143
- response: {} as any
144
- }
145
- );
146
- console.log('Successfully created record for:', s3Path);
171
+ const recordToCreate: any = {
172
+ [options.attachments.attachmentFieldName]: meta.key,
173
+ [options.attachments.attachmentRecordIdFieldName]: recordId,
174
+ [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
175
+ };
176
+
177
+ if (options.attachments.attachmentTitleFieldName) {
178
+ recordToCreate[options.attachments.attachmentTitleFieldName] = meta.title;
179
+ }
180
+ if (options.attachments.attachmentAltFieldName) {
181
+ recordToCreate[options.attachments.attachmentAltFieldName] = meta.alt;
182
+ }
183
+
184
+ await adminforth.createResourceRecord({
185
+ resource: this.attachmentResource,
186
+ record: recordToCreate,
187
+ adminUser,
188
+ });
147
189
  } catch (err) {
148
- console.error('Error creating record for', s3Path, err);
190
+ console.error('Error creating record for', meta.key, err);
149
191
  }
150
192
  }));
151
193
  } catch (err) {
@@ -154,36 +196,95 @@ export default class MarkdownPlugin extends AdminForthPlugin {
154
196
  }
155
197
 
156
198
  const deleteAttachmentRecords = async (
157
- adminforth: IAdminForth, options: PluginOptions, s3Paths: string[], adminUser: AdminUser
199
+ adminforth: IAdminForth,
200
+ options: PluginOptions,
201
+ recordId: any,
202
+ keys: string[],
203
+ adminUser: AdminUser
158
204
  ) => {
159
- if (!s3Paths.length) {
205
+ if (!keys.length) {
160
206
  return;
161
207
  }
162
208
  const attachmentPrimaryKeyField = this.attachmentResource.columns.find(c => c.primaryKey);
163
- const attachments = await adminforth.resource(options.attachments.attachmentResource).list(
164
- Filters.IN(options.attachments.attachmentFieldName, s3Paths)
165
- );
209
+ const attachments = await adminforth.resource(options.attachments.attachmentResource).list([
210
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
211
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId),
212
+ Filters.IN(options.attachments.attachmentFieldName, keys),
213
+ ]);
214
+
166
215
  await Promise.all(attachments.map(async (a: any) => {
167
- await adminforth.deleteResourceRecord(
168
- {
169
- resource: this.attachmentResource,
170
- recordId: a[attachmentPrimaryKeyField.name],
171
- adminUser,
172
- record: a,
173
- response: {} as any
174
- }
175
- )
216
+ await adminforth.deleteResourceRecord({
217
+ resource: this.attachmentResource,
218
+ recordId: a[attachmentPrimaryKeyField.name],
219
+ adminUser,
220
+ record: a,
221
+ })
176
222
  }))
177
223
  }
224
+
225
+ const updateAttachmentRecordsMetadata = async (
226
+ adminforth: IAdminForth,
227
+ options: PluginOptions,
228
+ recordId: any,
229
+ metas: AttachmentMeta[],
230
+ adminUser: AdminUser
231
+ ) => {
232
+ if (!metas.length) {
233
+ return;
234
+ }
235
+ if (!options.attachments.attachmentTitleFieldName && !options.attachments.attachmentAltFieldName) {
236
+ return;
237
+ }
238
+ const attachmentPrimaryKeyField = this.attachmentResource.columns.find(c => c.primaryKey);
239
+ const metaByKey = new Map(metas.map(m => [m.key, m] as const));
240
+
241
+ const existingAparts = await adminforth.resource(options.attachments.attachmentResource).list([
242
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
243
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
244
+ ]);
245
+
246
+ await Promise.all(existingAparts.map(async (a: any) => {
247
+ const key = a[options.attachments.attachmentFieldName];
248
+ const meta = metaByKey.get(key);
249
+ if (!meta) {
250
+ return;
251
+ }
252
+
253
+ const patch: any = {};
254
+ if (options.attachments.attachmentTitleFieldName) {
255
+ const field = options.attachments.attachmentTitleFieldName;
256
+ if ((a[field] ?? null) !== (meta.title ?? null)) {
257
+ patch[field] = meta.title;
258
+ }
259
+ }
260
+ if (options.attachments.attachmentAltFieldName) {
261
+ const field = options.attachments.attachmentAltFieldName;
262
+ if ((a[field] ?? null) !== (meta.alt ?? null)) {
263
+ patch[field] = meta.alt;
264
+ }
265
+ }
266
+ if (!Object.keys(patch).length) {
267
+ return;
268
+ }
269
+
270
+ await adminforth.updateResourceRecord({
271
+ resource: this.attachmentResource,
272
+ recordId: a[attachmentPrimaryKeyField.name],
273
+ record: patch,
274
+ oldRecord: a,
275
+ adminUser,
276
+ });
277
+ }));
278
+ }
178
279
 
179
280
  (resourceConfig.hooks.create.afterSave).push(async ({ record, adminUser }: { record: any, adminUser: AdminUser }) => {
180
281
  // find all s3Paths in the html
181
- const s3Paths = getAttachmentPathes(record[this.options.fieldName])
182
-
183
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths', s3Paths);
282
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
283
+ const keys = metas.map(m => m.key);
284
+ process.env.HEAVY_DEBUG && console.log('📸 Found attachment keys', keys);
184
285
  // create attachment records
185
286
  await createAttachmentRecords(
186
- adminforth, this.options, record[editorRecordPkField.name], s3Paths, adminUser);
287
+ adminforth, this.options, record[editorRecordPkField.name], metas, adminUser);
187
288
 
188
289
  return { ok: true };
189
290
  });
@@ -202,19 +303,29 @@ export default class MarkdownPlugin extends AdminForthPlugin {
202
303
  Filters.EQ(this.options.attachments.attachmentRecordIdFieldName, recordId),
203
304
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
204
305
  ]);
205
- const existingS3Paths = existingAparts.map((a: any) => a[this.options.attachments.attachmentFieldName]);
206
- const newS3Paths = getAttachmentPathes(record[this.options.fieldName]);
207
- process.env.HEAVY_DEBUG && console.log('📸 Existing s3Paths (from db)', existingS3Paths)
208
- process.env.HEAVY_DEBUG && console.log('📸 Found new s3Paths (from text)', newS3Paths);
209
- const toDelete = existingS3Paths.filter(s3Path => !newS3Paths.includes(s3Path));
210
- const toAdd = newS3Paths.filter(s3Path => !existingS3Paths.includes(s3Path));
211
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to delete', toDelete)
212
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to add', toAdd);
306
+ const existingKeys = existingAparts.map((a: any) => a[this.options.attachments.attachmentFieldName]);
307
+
308
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
309
+ const newKeys = metas.map(m => m.key);
310
+
311
+ process.env.HEAVY_DEBUG && console.log('📸 Existing keys (from db)', existingKeys)
312
+ process.env.HEAVY_DEBUG && console.log('📸 Found new keys (from text)', newKeys);
313
+
314
+ const toDelete = existingKeys.filter(key => !newKeys.includes(key));
315
+ const toAdd = newKeys.filter(key => !existingKeys.includes(key));
316
+
317
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', toDelete)
318
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to add', toAdd);
319
+
320
+ const metasToAdd = metas.filter(m => toAdd.includes(m.key));
213
321
  await Promise.all([
214
- deleteAttachmentRecords(adminforth, this.options, toDelete, adminUser),
215
- createAttachmentRecords(adminforth, this.options, recordId, toAdd, adminUser)
322
+ deleteAttachmentRecords(adminforth, this.options, recordId, toDelete, adminUser),
323
+ createAttachmentRecords(adminforth, this.options, recordId, metasToAdd, adminUser)
216
324
  ]);
217
325
 
326
+ // Keep alt/title in sync for existing attachments too
327
+ await updateAttachmentRecordsMetadata(adminforth, this.options, recordId, metas, adminUser);
328
+
218
329
  return { ok: true };
219
330
 
220
331
  }
@@ -229,9 +340,9 @@ export default class MarkdownPlugin extends AdminForthPlugin {
229
340
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
230
341
  ]
231
342
  );
232
- const existingS3Paths = existingAparts.map((a: any) => a[this.options.attachments.attachmentFieldName]);
233
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to delete', existingS3Paths);
234
- await deleteAttachmentRecords(adminforth, this.options, existingS3Paths, adminUser);
343
+ const existingKeys = existingAparts.map((a: any) => a[this.options.attachments.attachmentFieldName]);
344
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', existingKeys);
345
+ await deleteAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], existingKeys, adminUser);
235
346
 
236
347
  return { ok: true };
237
348
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/markdown",
3
- "version": "1.2.10",
3
+ "version": "1.4.0",
4
4
  "description": "Markdown plugin for adminforth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/types.ts CHANGED
@@ -32,8 +32,25 @@ export interface PluginOptions {
32
32
 
33
33
  /**
34
34
  * When attachment is created, it will be linked to the resource, by storing id of the resource with editor in attachment resource.
35
+ * Here you define the field name where this id will be stored.
35
36
  * For example when RichEditor installed on description field of apartment resource, it will store id of apartment resource.
37
+ *
38
+ * Why we force to store and ask for resource id if we already have record id? Because in amny use cases attachments resource is shared between multiple resources, and record id might be not be unique across resources, but resource id + record id will be always unique.
36
39
  */
37
40
  attachmentResourceIdFieldName: string; // e.g. 'apartment_resource_id',
41
+
42
+ /**
43
+ * Optional: field name in attachment resource where title of image will be stored.
44
+ * When in markdown title of image is mentioned e.g. ![alt](image.jpg "title"), it will be parsed and stored in attachment resource.
45
+ * If you will update title in markdown, it will be updated in attachment resource as well.
46
+ */
47
+ attachmentTitleFieldName?: string; // e.g. 'title',
48
+
49
+ /**
50
+ * Optional: field name in attachment resource where alt of image will be stored.
51
+ * When in markdown alt of image is mentioned e.g. ![alt](image.jpg), it will be parsed and stored in attachment resource.
52
+ * If you will update alt in markdown, it will be updated in attachment resource as well.
53
+ */
54
+ attachmentAltFieldName?: string; // e.g. 'alt',
38
55
  },
39
56
  }