domma-cms 0.6.16 → 0.6.21

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.
Files changed (44) hide show
  1. package/admin/js/api.js +1 -1
  2. package/admin/js/app.js +4 -4
  3. package/admin/js/lib/markdown-toolbar.js +14 -14
  4. package/admin/js/views/collection-editor.js +5 -3
  5. package/admin/js/views/collections.js +1 -1
  6. package/admin/js/views/page-editor.js +27 -27
  7. package/config/plugins.json +16 -0
  8. package/config/site.json +1 -1
  9. package/package.json +2 -2
  10. package/plugins/analytics/stats.json +1 -1
  11. package/plugins/contacts/admin/templates/contacts.html +126 -0
  12. package/plugins/contacts/admin/views/contacts.js +710 -0
  13. package/plugins/contacts/config.js +6 -0
  14. package/plugins/contacts/data/contacts.json +20 -0
  15. package/plugins/contacts/plugin.js +351 -0
  16. package/plugins/contacts/plugin.json +23 -0
  17. package/plugins/docs/admin/templates/docs.html +69 -0
  18. package/plugins/docs/admin/views/docs.js +276 -0
  19. package/plugins/docs/config.js +8 -0
  20. package/plugins/docs/data/documents/452f49b7-9c93-4a67-874d-27f882891ad2.json +11 -0
  21. package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +11 -0
  22. package/plugins/docs/data/folders.json +9 -0
  23. package/plugins/docs/data/templates.json +1 -0
  24. package/plugins/docs/plugin.js +375 -0
  25. package/plugins/docs/plugin.json +23 -0
  26. package/plugins/notes/admin/templates/notes.html +92 -0
  27. package/plugins/notes/admin/views/notes.js +304 -0
  28. package/plugins/notes/config.js +6 -0
  29. package/plugins/notes/data/notes.json +1 -0
  30. package/plugins/notes/plugin.js +177 -0
  31. package/plugins/notes/plugin.json +23 -0
  32. package/plugins/todo/admin/templates/todo.html +164 -0
  33. package/plugins/todo/admin/views/todo.js +328 -0
  34. package/plugins/todo/config.js +7 -0
  35. package/plugins/todo/data/todos.json +1 -0
  36. package/plugins/todo/plugin.js +155 -0
  37. package/plugins/todo/plugin.json +23 -0
  38. package/server/routes/api/auth.js +2 -0
  39. package/server/routes/api/collections.js +55 -0
  40. package/server/routes/api/forms.js +3 -0
  41. package/server/routes/api/settings.js +16 -1
  42. package/server/routes/public.js +2 -0
  43. package/server/services/markdown.js +169 -8
  44. package/server/services/plugins.js +3 -2
@@ -0,0 +1,276 @@
1
+ import { DocEditor } from '/plugins/docs/admin/lib/editor.js';
2
+ import { FolderManager } from '/plugins/docs/admin/lib/folders.js';
3
+ import { VersionHistory } from '/plugins/docs/admin/lib/versions.js';
4
+ import { DocumentTemplates } from '/plugins/docs/admin/lib/templates.js';
5
+ import { FindReplace } from '/plugins/docs/admin/lib/find-replace.js';
6
+
7
+ async function api(url, method = 'GET', body) {
8
+ const opts = {method, headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}};
9
+ if (body !== undefined) {
10
+ opts.headers['Content-Type'] = 'application/json';
11
+ opts.body = JSON.stringify(body);
12
+ }
13
+ const res = await fetch(url, opts);
14
+ if (!res.ok) {
15
+ const err = await res.json().catch(() => ({error: res.statusText}));
16
+ throw new Error(err.error || res.statusText);
17
+ }
18
+ const text = await res.text();
19
+ return text ? JSON.parse(text) : {};
20
+ }
21
+
22
+ export const docsView = {
23
+ templateUrl: '/plugins/docs/admin/templates/docs.html',
24
+
25
+ async onMount($container) {
26
+ // 1. Get plugin config from window globals set by server
27
+ const editorMode = window.__DOCS_EDITOR_MODE__ || 'wysiwyg';
28
+ const autoSaveInterval = window.__DOCS_AUTOSAVE_INTERVAL__ || 30000;
29
+
30
+ // 2. State
31
+ let currentDocId = null;
32
+ let autosaveTimer = null;
33
+ let isDirty = false;
34
+
35
+ // 3. Init sub-modules
36
+ const folderManager = new FolderManager({
37
+ onFolderSelect: async (folderId) => {
38
+ await loadDocuments(folderId);
39
+ }
40
+ });
41
+ const versionHistory = new VersionHistory();
42
+ const templates = new DocumentTemplates();
43
+ const findReplace = new FindReplace();
44
+
45
+ // 4. Init editor
46
+ const editorContainerEl = $container.find('#doc-editor-content').get(0);
47
+ const editor = new DocEditor(editorContainerEl, { mode: editorMode });
48
+ editor.init();
49
+ findReplace.setEditor(editor);
50
+
51
+ // 5. Load initial data
52
+ await folderManager.load();
53
+ folderManager.render($container.find('#folder-sidebar').get(0));
54
+ await loadDocuments(null);
55
+
56
+ // --- Functions ---
57
+
58
+ async function loadDocuments(folderId) {
59
+ try {
60
+ const q = $container.find('#doc-search').get(0)?.value || '';
61
+ let url = '/api/plugins/docs/documents';
62
+ const params = [];
63
+ if (folderId !== null && folderId !== undefined) {
64
+ params.push(`folder=${encodeURIComponent(folderId)}`);
65
+ }
66
+ if (q) params.push(`q=${encodeURIComponent(q)}`);
67
+ if (params.length) url += '?' + params.join('&');
68
+
69
+ const docs = await api(url);
70
+ renderDocList(docs);
71
+ } catch {
72
+ E.toast('Failed to load documents', { type: 'error' });
73
+ }
74
+ }
75
+
76
+ function renderDocList(docs) {
77
+ const listEl = $container.find('#doc-list').get(0);
78
+ listEl.replaceChildren();
79
+
80
+ if (docs.length === 0) {
81
+ const empty = document.createElement('div');
82
+ empty.className = 'doc-empty';
83
+ empty.style.cssText = 'padding:1rem;color:var(--dm-text-muted);text-align:center;font-size:0.85rem;';
84
+ empty.textContent = 'No documents yet.';
85
+ listEl.appendChild(empty);
86
+ return;
87
+ }
88
+
89
+ docs.forEach(doc => {
90
+ const item = document.createElement('div');
91
+ item.className = 'doc-list-item' + (doc.id === currentDocId ? ' active' : '');
92
+ item.dataset.id = doc.id;
93
+
94
+ const title = document.createElement('div');
95
+ title.className = 'doc-title';
96
+ title.textContent = doc.title || 'Untitled';
97
+
98
+ const meta = document.createElement('div');
99
+ meta.className = 'doc-meta';
100
+ meta.style.cssText = 'font-size:0.75rem;color:var(--dm-text-muted);';
101
+ meta.textContent = D(doc.updatedAt).fromNow();
102
+
103
+ item.appendChild(title);
104
+ item.appendChild(meta);
105
+ item.addEventListener('click', () => openDocument(doc.id));
106
+ listEl.appendChild(item);
107
+ });
108
+ }
109
+
110
+ async function openDocument(id) {
111
+ if (isDirty) {
112
+ const save = await E.confirm('You have unsaved changes. Save before switching?');
113
+ if (save) await saveCurrentDoc();
114
+ }
115
+ try {
116
+ const doc = await api(`/api/plugins/docs/documents/${id}`);
117
+ currentDocId = id;
118
+ $container.find('#doc-title-input').get(0).value = doc.title || '';
119
+ editor.setValue(doc.content || '');
120
+ isDirty = false;
121
+ updateEditorUI(true);
122
+ setupAutosave();
123
+ } catch {
124
+ E.toast('Failed to open document', { type: 'error' });
125
+ }
126
+ }
127
+
128
+ async function saveCurrentDoc() {
129
+ if (!currentDocId) return;
130
+ try {
131
+ const title = $container.find('#doc-title-input').get(0).value || 'Untitled';
132
+ const content = editor.getValue();
133
+ await api(`/api/plugins/docs/documents/${currentDocId}`, 'PUT', {title, content});
134
+ isDirty = false;
135
+ E.toast('Saved', { type: 'success' });
136
+ await loadDocuments(folderManager.currentFolderId);
137
+ } catch {
138
+ E.toast('Failed to save', { type: 'error' });
139
+ }
140
+ }
141
+
142
+ function setupAutosave() {
143
+ if (!autoSaveInterval) return;
144
+ editor.onChange(() => {
145
+ isDirty = true;
146
+ clearTimeout(autosaveTimer);
147
+ autosaveTimer = setTimeout(() => saveCurrentDoc(), autoSaveInterval);
148
+ });
149
+ }
150
+
151
+ function updateEditorUI(hasDoc) {
152
+ const editorPane = $container.find('#editor-pane').get(0);
153
+ const placeholder = $container.find('#editor-placeholder').get(0);
154
+ if (hasDoc) {
155
+ editorPane.style.display = 'flex';
156
+ if (placeholder) placeholder.style.display = 'none';
157
+ } else {
158
+ editorPane.style.display = 'none';
159
+ if (placeholder) placeholder.style.display = 'flex';
160
+ }
161
+ }
162
+
163
+ // --- Event Bindings ---
164
+
165
+ // New doc button
166
+ $container.find('#new-doc-btn').get(0)?.addEventListener('click', async () => {
167
+ if (isDirty) {
168
+ const save = await E.confirm('Save current changes first?');
169
+ if (save) await saveCurrentDoc();
170
+ }
171
+ try {
172
+ const doc = await api('/api/plugins/docs/documents', 'POST', {
173
+ title: 'Untitled',
174
+ content: '',
175
+ folderId: folderManager.currentFolderId
176
+ });
177
+ await loadDocuments(folderManager.currentFolderId);
178
+ await openDocument(doc.id);
179
+ } catch {
180
+ E.toast('Failed to create document', { type: 'error' });
181
+ }
182
+ });
183
+
184
+ // New doc from template
185
+ $container.find('#new-from-template-btn').get(0)?.addEventListener('click', async () => {
186
+ await templates.showPicker(async (content) => {
187
+ try {
188
+ const doc = await api('/api/plugins/docs/documents', 'POST', {
189
+ title: 'Untitled',
190
+ content,
191
+ folderId: folderManager.currentFolderId
192
+ });
193
+ await loadDocuments(folderManager.currentFolderId);
194
+ await openDocument(doc.id);
195
+ } catch {
196
+ E.toast('Failed to create from template', { type: 'error' });
197
+ }
198
+ });
199
+ });
200
+
201
+ // Save button
202
+ $container.find('#save-doc-btn').get(0)?.addEventListener('click', () => saveCurrentDoc());
203
+
204
+ // Delete button
205
+ $container.find('#delete-doc-btn').get(0)?.addEventListener('click', async () => {
206
+ if (!currentDocId) return;
207
+ const confirmed = await E.confirm('Delete this document? This cannot be undone.');
208
+ if (!confirmed) return;
209
+ try {
210
+ await api(`/api/plugins/docs/documents/${currentDocId}`, 'DELETE');
211
+ currentDocId = null;
212
+ editor.setValue('');
213
+ updateEditorUI(false);
214
+ await loadDocuments(folderManager.currentFolderId);
215
+ E.toast('Document deleted', { type: 'success' });
216
+ } catch {
217
+ E.toast('Failed to delete', { type: 'error' });
218
+ }
219
+ });
220
+
221
+ // Version history
222
+ $container.find('#version-history-btn').get(0)?.addEventListener('click', async () => {
223
+ if (!currentDocId) return;
224
+ const modal = await versionHistory.show(currentDocId);
225
+ if (modal) {
226
+ // Reload document after modal closes in case a version was restored
227
+ modal.element.addEventListener('close', async () => {
228
+ if (currentDocId) await openDocument(currentDocId);
229
+ });
230
+ }
231
+ });
232
+
233
+ // Find & replace
234
+ $container.find('#find-replace-btn').get(0)?.addEventListener('click', () => {
235
+ const editorPane = $container.find('#editor-pane').get(0);
236
+ if (editorPane) findReplace.show(editorPane);
237
+ });
238
+
239
+ // Duplicate
240
+ $container.find('#duplicate-doc-btn').get(0)?.addEventListener('click', async () => {
241
+ if (!currentDocId) return;
242
+ try {
243
+ const copy = await api(`/api/plugins/docs/documents/${currentDocId}/duplicate`, 'POST');
244
+ await loadDocuments(folderManager.currentFolderId);
245
+ await openDocument(copy.id);
246
+ E.toast('Document duplicated', { type: 'success' });
247
+ } catch {
248
+ E.toast('Failed to duplicate', { type: 'error' });
249
+ }
250
+ });
251
+
252
+ // Search
253
+ let searchTimeout;
254
+ $container.find('#doc-search').get(0)?.addEventListener('input', () => {
255
+ clearTimeout(searchTimeout);
256
+ searchTimeout = setTimeout(() => loadDocuments(folderManager.currentFolderId), 300);
257
+ });
258
+
259
+ // New folder
260
+ $container.find('#new-folder-btn').get(0)?.addEventListener('click', async () => {
261
+ await folderManager.createFolder();
262
+ folderManager.render($container.find('#folder-sidebar').get(0));
263
+ });
264
+
265
+ // Keyboard shortcut: Ctrl+S / Cmd+S to save
266
+ document.addEventListener('keydown', (e) => {
267
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
268
+ e.preventDefault();
269
+ saveCurrentDoc();
270
+ }
271
+ });
272
+
273
+ updateEditorUI(false);
274
+ Domma.icons.scan();
275
+ }
276
+ };
@@ -0,0 +1,8 @@
1
+ export default {
2
+ scope: 'user',
3
+ editorMode: 'scribe', // 'scribe' or 'markdown'
4
+ autoSaveInterval: 30000, // ms, 0 to disable
5
+ maxVersions: 50,
6
+ // storage.adapter: 'mongodb' uses config/connections.json; 'file' uses local JSON
7
+ storage: {adapter: 'file'}
8
+ };
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "452f49b7-9c93-4a67-874d-27f882891ad2",
3
+ "title": "Untitled",
4
+ "content": "",
5
+ "folderId": null,
6
+ "tags": [],
7
+ "userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
8
+ "wordCount": 0,
9
+ "createdAt": "2026-03-24T16:48:26.074Z",
10
+ "updatedAt": "2026-03-24T16:49:05.067Z"
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "57e003f0-68f2-47dc-9c36-ed4b10ed3deb",
3
+ "title": "Untitled",
4
+ "content": "",
5
+ "folderId": null,
6
+ "tags": [],
7
+ "userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
8
+ "wordCount": 0,
9
+ "createdAt": "2026-03-24T16:41:34.006Z",
10
+ "updatedAt": "2026-03-24T16:41:34.006Z"
11
+ }
@@ -0,0 +1,9 @@
1
+ [
2
+ {
3
+ "id": "262d8f8b-452c-49b7-82ae-e74edc13249e",
4
+ "name": "Test",
5
+ "parentId": null,
6
+ "userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
7
+ "createdAt": "2026-03-24T16:49:14.093Z"
8
+ }
9
+ ]
@@ -0,0 +1 @@
1
+ []