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.
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/markdown-toolbar.js +14 -14
- package/admin/js/views/collection-editor.js +5 -3
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/page-editor.js +27 -27
- package/config/plugins.json +16 -0
- package/config/site.json +1 -1
- package/package.json +2 -2
- package/plugins/analytics/stats.json +1 -1
- package/plugins/contacts/admin/templates/contacts.html +126 -0
- package/plugins/contacts/admin/views/contacts.js +710 -0
- package/plugins/contacts/config.js +6 -0
- package/plugins/contacts/data/contacts.json +20 -0
- package/plugins/contacts/plugin.js +351 -0
- package/plugins/contacts/plugin.json +23 -0
- package/plugins/docs/admin/templates/docs.html +69 -0
- package/plugins/docs/admin/views/docs.js +276 -0
- package/plugins/docs/config.js +8 -0
- package/plugins/docs/data/documents/452f49b7-9c93-4a67-874d-27f882891ad2.json +11 -0
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +11 -0
- package/plugins/docs/data/folders.json +9 -0
- package/plugins/docs/data/templates.json +1 -0
- package/plugins/docs/plugin.js +375 -0
- package/plugins/docs/plugin.json +23 -0
- package/plugins/notes/admin/templates/notes.html +92 -0
- package/plugins/notes/admin/views/notes.js +304 -0
- package/plugins/notes/config.js +6 -0
- package/plugins/notes/data/notes.json +1 -0
- package/plugins/notes/plugin.js +177 -0
- package/plugins/notes/plugin.json +23 -0
- package/plugins/todo/admin/templates/todo.html +164 -0
- package/plugins/todo/admin/views/todo.js +328 -0
- package/plugins/todo/config.js +7 -0
- package/plugins/todo/data/todos.json +1 -0
- package/plugins/todo/plugin.js +155 -0
- package/plugins/todo/plugin.json +23 -0
- package/server/routes/api/auth.js +2 -0
- package/server/routes/api/collections.js +55 -0
- package/server/routes/api/forms.js +3 -0
- package/server/routes/api/settings.js +16 -1
- package/server/routes/public.js +2 -0
- package/server/services/markdown.js +169 -8
- 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 @@
|
|
|
1
|
+
[]
|