clay-server 5.1.0-beta.1 → 5.1.0-beta.3
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/LICENSES/README.md +4 -0
- package/LICENSES/dicebear-avatars.md +16 -0
- package/bin/cli.js +9 -91
- package/lib/builtin-mates.js +7 -7
- package/lib/daemon-project-access.js +7 -1
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +10 -37
- package/lib/dicebear-avatar.js +62 -0
- package/lib/knowledge-db-service.js +175 -0
- package/lib/knowledge-db-validation.js +0 -0
- package/lib/knowledge-document-index.js +49 -0
- package/lib/knowledge-draft-store.js +56 -0
- package/lib/knowledge-file-store.js +199 -0
- package/lib/knowledge-markdown.js +128 -0
- package/lib/linear-store.js +35 -8
- package/lib/mate-identity-file.js +39 -0
- package/lib/mate-knowledge-mcp-server.js +29 -6
- package/lib/mate-knowledge-service.js +23 -2
- package/lib/mate-knowledge-sync.js +37 -22
- package/lib/mate-ready-creation.js +2 -1
- package/lib/mate-workspace-color.js +4 -0
- package/lib/mates-identity.js +4 -4
- package/lib/mates-prompts.js +4 -3
- package/lib/mates.js +30 -11
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +36 -9
- package/lib/project-debate-proposal.js +6 -0
- package/lib/project-debate-workspace.js +43 -0
- package/lib/project-debate.js +4 -2
- package/lib/project-filesystem.js +21 -39
- package/lib/project-knowledge.js +315 -136
- package/lib/project-mate-creation-proposal.js +1 -0
- package/lib/project-mate-interaction.js +7 -6
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +92 -182
- package/lib/project-user-knowledge.js +60 -0
- package/lib/project.js +62 -21
- package/lib/public/app.js +79 -71
- package/lib/public/clay-studio-wordmark-bold.svg +20 -0
- package/lib/public/css/clay-workspace.css +105 -0
- package/lib/public/css/debates-workbench.css +47 -0
- package/lib/public/css/home-debate-live.css +26 -0
- package/lib/public/css/home-hub.css +3 -1
- package/lib/public/css/home-mate-model-picker.css +15 -131
- package/lib/public/css/home-mate-settings.css +168 -16
- package/lib/public/css/icon-strip.css +249 -32
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/mate-conversation.css +176 -0
- package/lib/public/css/mate-knowledge-workbench.css +118 -0
- package/lib/public/css/mate-workspace-color.css +16 -0
- package/lib/public/css/mates.css +28 -28
- package/lib/public/css/menus.css +3 -0
- package/lib/public/css/messages.css +1 -114
- package/lib/public/css/mobile-nav.css +16 -16
- package/lib/public/css/overlays.css +15 -0
- package/lib/public/css/project-logs.css +7 -0
- package/lib/public/css/project-mate-creation.css +105 -0
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/right-workbench.css +3 -3
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-folders.css +80 -21
- package/lib/public/css/session-github.css +25 -16
- package/lib/public/css/settings-ui.css +247 -22
- package/lib/public/css/shared-browser.css +7 -3
- package/lib/public/css/sidebar.css +183 -107
- package/lib/public/css/title-bar.css +156 -1
- package/lib/public/css/worker-delegation.css +47 -0
- package/lib/public/css/worker-proposal.css +72 -150
- package/lib/public/css/you.css +74 -0
- package/lib/public/index.html +206 -548
- package/lib/public/modules/account-island.js +232 -0
- package/lib/public/modules/admin-settings-email.js +197 -0
- package/lib/public/modules/admin-settings-invitations.js +183 -0
- package/lib/public/modules/admin-settings-projects.js +198 -0
- package/lib/public/modules/admin-settings-ui.js +100 -0
- package/lib/public/modules/admin-settings-users.js +331 -0
- package/lib/public/modules/admin.js +19 -949
- package/lib/public/modules/app-cursors.js +19 -43
- package/lib/public/modules/app-debate-ui.js +4 -3
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-header.js +106 -0
- package/lib/public/modules/app-message-router.js +12 -3
- package/lib/public/modules/app-messages.js +47 -61
- package/lib/public/modules/app-notifications.js +4 -7
- package/lib/public/modules/app-projects.js +5 -38
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/app-rendering.js +12 -13
- package/lib/public/modules/avatar.js +30 -9
- package/lib/public/modules/chat-render-runtime.js +16 -4
- package/lib/public/modules/command-palette.js +2 -15
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/cursor-sharing-preference.js +3 -3
- package/lib/public/modules/debate-controls.js +299 -0
- package/lib/public/modules/debate.js +2 -0
- package/lib/public/modules/debates-workbench.js +339 -0
- package/lib/public/modules/default-ai.js +88 -65
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/home-debate-controls.js +18 -289
- package/lib/public/modules/home-mate-avatar-editor.js +228 -0
- package/lib/public/modules/home-mate-color-picker.js +66 -0
- package/lib/public/modules/home-mate-creation.js +1 -1
- package/lib/public/modules/home-mate-instructions.js +90 -0
- package/lib/public/modules/home-mate-model-picker.js +80 -271
- package/lib/public/modules/home-mate-profile-editor.js +72 -0
- package/lib/public/modules/home-mate-settings.js +118 -125
- package/lib/public/modules/home-shell.js +3 -0
- package/lib/public/modules/home-surface-boot.js +1 -9
- package/lib/public/modules/icon-strip-tooltip.js +95 -0
- package/lib/public/modules/knowledge-db-edit-state.js +35 -0
- package/lib/public/modules/knowledge-db-workbench.js +234 -0
- package/lib/public/modules/knowledge-history-state.js +55 -0
- package/lib/public/modules/knowledge-markdown-editor.js +57 -0
- package/lib/public/modules/knowledge-preview-links.js +70 -0
- package/lib/public/modules/knowledge-workbench-shell.js +21 -0
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/mate-knowledge-workbench-state.js +174 -0
- package/lib/public/modules/mate-knowledge-workbench.js +500 -0
- package/lib/public/modules/mate-knowledge.js +11 -3
- package/lib/public/modules/mate-management.js +54 -10
- package/lib/public/modules/mate-sidebar.js +11 -1
- package/lib/public/modules/mate-workspace-color.js +33 -0
- package/lib/public/modules/mention.js +5 -13
- package/lib/public/modules/notification-settings.js +81 -0
- package/lib/public/modules/notifications.js +44 -28
- package/lib/public/modules/personal-settings-dialog.js +51 -0
- package/lib/public/modules/profile.js +66 -24
- package/lib/public/modules/project-chat-identity.js +79 -0
- package/lib/public/modules/project-debate-planning-workspace.js +250 -0
- package/lib/public/modules/project-debate-workspace.js +185 -0
- package/lib/public/modules/project-header-identity.js +127 -0
- package/lib/public/modules/project-logs-render.js +9 -4
- package/lib/public/modules/project-mate-creation-workspace.js +414 -0
- package/lib/public/modules/project-mate-navigation.js +238 -0
- package/lib/public/modules/project-session-navigation.js +29 -0
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/project-switcher.js +6 -8
- package/lib/public/modules/projects-hub-view.js +5 -6
- package/lib/public/modules/projects-hub.js +2 -6
- package/lib/public/modules/qrcode.js +27 -8
- package/lib/public/modules/search-clay-chat.js +8 -11
- package/lib/public/modules/server-settings-controls.js +316 -0
- package/lib/public/modules/server-settings-environment.js +194 -0
- package/lib/public/modules/server-settings-navigation.js +39 -0
- package/lib/public/modules/server-settings-state.js +54 -0
- package/lib/public/modules/server-settings.js +116 -788
- package/lib/public/modules/server-storage-settings.js +84 -0
- package/lib/public/modules/session-activity-tooltip.js +12 -0
- package/lib/public/modules/session-create-form.js +86 -14
- package/lib/public/modules/session-create.js +50 -7
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-list-visibility.js +41 -0
- package/lib/public/modules/session-presence-ui.js +3 -0
- package/lib/public/modules/session-title-marquee.js +51 -0
- package/lib/public/modules/settings-env-values.js +34 -0
- package/lib/public/modules/settings-select.js +121 -0
- package/lib/public/modules/shared-browser.js +22 -12
- package/lib/public/modules/sidebar-mates.js +3 -50
- package/lib/public/modules/sidebar-mobile.js +63 -60
- package/lib/public/modules/sidebar-projects.js +84 -97
- package/lib/public/modules/sidebar-session-folders-ui.js +2 -2
- package/lib/public/modules/sidebar-session-hierarchy.js +149 -70
- package/lib/public/modules/sidebar-sessions.js +49 -50
- package/lib/public/modules/sidebar.js +11 -13
- package/lib/public/modules/stt-picker.js +14 -5
- package/lib/public/modules/terminal-prefs.js +22 -0
- package/lib/public/modules/theme.js +88 -52
- package/lib/public/modules/thinking-lifecycle.js +7 -6
- package/lib/public/modules/tool-palette-order.js +4 -3
- package/lib/public/modules/tool-palette.js +13 -3
- package/lib/public/modules/tools.js +22 -22
- package/lib/public/modules/user-settings-account.js +318 -0
- package/lib/public/modules/user-settings-appearance.js +55 -0
- package/lib/public/modules/user-settings-editors.js +24 -0
- package/lib/public/modules/user-settings-integrations.js +104 -0
- package/lib/public/modules/user-settings-layout.js +80 -0
- package/lib/public/modules/user-settings-preferences.js +48 -0
- package/lib/public/modules/user-settings-profile.js +53 -0
- package/lib/public/modules/user-settings-themes.js +38 -0
- package/lib/public/modules/user-settings-voice.js +75 -0
- package/lib/public/modules/user-settings.js +53 -480
- package/lib/public/modules/worker-proposal.js +42 -8
- package/lib/public/modules/worker-runtime-picker.js +115 -0
- package/lib/public/modules/you-activity.js +121 -0
- package/lib/public/modules/you.js +251 -0
- package/lib/public/style.css +20 -8
- package/lib/public/vendor/codemirror/LICENSE +21 -0
- package/lib/public/vendor/codemirror/codemirror.css +344 -0
- package/lib/public/vendor/codemirror/codemirror.js +9884 -0
- package/lib/public/vendor/codemirror/continuelist.js +101 -0
- package/lib/public/vendor/codemirror/markdown.js +886 -0
- package/lib/public/vendor/codemirror/show-hint.css +37 -0
- package/lib/public/vendor/codemirror/show-hint.js +523 -0
- package/lib/sdk-bridge.js +15 -3
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-admin.js +11 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-home-chat-events.js +1 -1
- package/lib/server-home-chat.js +25 -3
- package/lib/server-home-debate-planning.js +4 -1
- package/lib/server-home-debates.js +2 -0
- package/lib/server-home-mate-creation.js +4 -1
- package/lib/server-linear.js +14 -8
- package/lib/server-mate-instructions.js +44 -0
- package/lib/server-mates.js +14 -4
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-palette.js +1 -1
- package/lib/server-settings.js +69 -1
- package/lib/server-storage.js +57 -0
- package/lib/server-user-knowledge.js +119 -0
- package/lib/server.js +31 -3
- package/lib/session-visibility.js +7 -2
- package/lib/sessions.js +35 -10
- package/lib/startup-projects.js +40 -0
- package/lib/storage-native-walk.js +85 -0
- package/lib/storage-usage.js +144 -0
- package/lib/user-knowledge-service.js +167 -0
- package/lib/users-permissions.js +7 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users-project-mate-filter-preferences.js +39 -0
- package/lib/users.js +6 -4
- package/lib/workspace-query-access.js +4 -1
- package/lib/ws-schema.js +52 -4
- package/lib/yoke/acp-process-manager.js +2 -2
- package/lib/yoke/adapters/codex.js +12 -52
- package/lib/yoke/codex-app-server.js +2 -2
- package/lib/yoke/codex-model-catalog.js +52 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- package/package.json +12 -3
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { mountDefaultAi, unmountDefaultAi } from './default-ai.js';
|
|
3
|
+
import { openUserSettings } from './user-settings.js';
|
|
4
|
+
import { getProfile, saveProfileUpdates, regenerateProfileAvatar, showAvatarPositioner, uploadAvatarBlob } from './profile.js';
|
|
5
|
+
import { requestCursorSharingState, setCursorSharing } from './app-cursors.js';
|
|
6
|
+
import { registerTooltip } from './tooltip.js';
|
|
7
|
+
|
|
8
|
+
var popover = null;
|
|
9
|
+
var trigger = null;
|
|
10
|
+
var lastFocus = null;
|
|
11
|
+
|
|
12
|
+
function accountName() { return document.body.dataset.myDisplayName || 'Clay user'; }
|
|
13
|
+
|
|
14
|
+
function close(returnFocus) {
|
|
15
|
+
if (!popover) return;
|
|
16
|
+
var aiSection = popover.querySelector('#account-ai-section');
|
|
17
|
+
if (aiSection) unmountDefaultAi(aiSection);
|
|
18
|
+
popover.remove();
|
|
19
|
+
popover = null;
|
|
20
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
21
|
+
trigger.setAttribute('aria-label', 'Open account menu for ' + accountName());
|
|
22
|
+
document.removeEventListener('pointerdown', onOutside);
|
|
23
|
+
document.removeEventListener('focusin', onFocusOutside);
|
|
24
|
+
document.removeEventListener('keydown', onKeydown);
|
|
25
|
+
window.removeEventListener('resize', position);
|
|
26
|
+
store.set({ defaultAiInlineEditing: false });
|
|
27
|
+
if (returnFocus && lastFocus && document.contains(lastFocus)) lastFocus.focus({ preventScroll: true });
|
|
28
|
+
lastFocus = null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isAvatarPositionerTarget(target) { return target && target.closest && target.closest('.avatar-positioner-overlay'); }
|
|
32
|
+
function onOutside(event) { if (!popover || popover.contains(event.target) || trigger.contains(event.target) || isAvatarPositionerTarget(event.target)) return; close(false); }
|
|
33
|
+
function onFocusOutside(event) { if (!popover || popover.contains(event.target) || trigger.contains(event.target) || isAvatarPositionerTarget(event.target)) return; close(false); }
|
|
34
|
+
function onKeydown(event) {
|
|
35
|
+
if (event.key !== 'Escape') return;
|
|
36
|
+
if (isAvatarPositionerTarget(event.target) || document.querySelector('.avatar-positioner-overlay')) return;
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
event.stopPropagation();
|
|
39
|
+
close(true);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function position() {
|
|
43
|
+
if (!popover || !trigger) return;
|
|
44
|
+
var rect = trigger.getBoundingClientRect();
|
|
45
|
+
var width = Math.min(280, window.innerWidth - 24);
|
|
46
|
+
var left = Math.max(12, Math.min(window.innerWidth - width - 12, rect.left));
|
|
47
|
+
popover.style.width = width + 'px';
|
|
48
|
+
popover.style.left = left + 'px';
|
|
49
|
+
popover.style.top = Math.max(12, rect.top - popover.offsetHeight - 8) + 'px';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderNameResting(restoreFocus) {
|
|
53
|
+
var section = popover && popover.querySelector('.account-identity-copy');
|
|
54
|
+
if (!section) return;
|
|
55
|
+
section.innerHTML = '<button type="button" id="account-name-edit" class="account-name-display"><span></span><i data-lucide="pencil" aria-hidden="true"></i></button><small>Personal account</small>';
|
|
56
|
+
section.querySelector('span').textContent = accountName();
|
|
57
|
+
section.querySelector('#account-name-edit').addEventListener('click', startNameEdit);
|
|
58
|
+
if (window.lucide) window.lucide.createIcons({ nodes: [section] });
|
|
59
|
+
if (restoreFocus) section.querySelector('#account-name-edit').focus();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function startNameEdit() {
|
|
63
|
+
var section = popover && popover.querySelector('.account-identity-copy');
|
|
64
|
+
if (!section) return;
|
|
65
|
+
var input = document.createElement('input');
|
|
66
|
+
input.id = 'account-profile-name';
|
|
67
|
+
input.setAttribute('aria-label', 'Display name');
|
|
68
|
+
input.className = 'account-name-input';
|
|
69
|
+
input.type = 'text';
|
|
70
|
+
input.maxLength = 50;
|
|
71
|
+
input.autocomplete = 'name';
|
|
72
|
+
input.value = store.get('accountNameDraft') === undefined ? getProfile().name || '' : store.get('accountNameDraft');
|
|
73
|
+
input.addEventListener('input',function(){store.set({accountNameDraft:input.value});});
|
|
74
|
+
section.innerHTML = '';
|
|
75
|
+
section.appendChild(input);
|
|
76
|
+
var hint = document.createElement('small');
|
|
77
|
+
hint.textContent = 'Enter to save · Escape to cancel';
|
|
78
|
+
section.appendChild(hint);
|
|
79
|
+
input.addEventListener('keydown', function(event) {
|
|
80
|
+
if (event.key === 'Enter') {
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
input.disabled=true;hint.textContent='Saving…';
|
|
83
|
+
saveProfileUpdates({name:input.value.trim()}).then(function(){store.set({accountNameDraft:undefined});renderNameResting(true);}).catch(function(error){input.disabled=false;hint.textContent=error.message;input.focus();});
|
|
84
|
+
} else if (event.key === 'Escape') {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
store.set({accountNameDraft:undefined});renderNameResting(true);
|
|
87
|
+
}
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
});
|
|
90
|
+
input.focus();
|
|
91
|
+
input.select();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toggleAvatarEditor() {
|
|
95
|
+
var editor = popover && popover.querySelector('#account-avatar-editor');
|
|
96
|
+
if (!editor) return;
|
|
97
|
+
editor.hidden = !editor.hidden;
|
|
98
|
+
if (!editor.hidden) editor.querySelector('#account-avatar-upload').focus();
|
|
99
|
+
position();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function bindAvatarEditor() {
|
|
103
|
+
var avatar = popover.querySelector('#account-avatar-edit');
|
|
104
|
+
var editor = popover.querySelector('#account-avatar-editor');
|
|
105
|
+
var upload = editor.querySelector('#account-avatar-upload');
|
|
106
|
+
var file = editor.querySelector('#account-avatar-file');
|
|
107
|
+
var generate = editor.querySelector('#account-avatar-generate');
|
|
108
|
+
avatar.addEventListener('click', function(event) { event.stopPropagation(); toggleAvatarEditor(); });
|
|
109
|
+
editor.addEventListener('click', function(event) { event.stopPropagation(); });
|
|
110
|
+
upload.addEventListener('click', function() { file.click(); });
|
|
111
|
+
file.addEventListener('change', function() {
|
|
112
|
+
var selected = file.files && file.files[0];
|
|
113
|
+
if (!selected) return;
|
|
114
|
+
var image = new Image();
|
|
115
|
+
var objectUrl = URL.createObjectURL(selected);
|
|
116
|
+
image.onload = function() {
|
|
117
|
+
showAvatarPositioner(image, objectUrl, function(blob) {
|
|
118
|
+
URL.revokeObjectURL(objectUrl);
|
|
119
|
+
uploadAvatarBlob(blob).then(function() { editor.hidden = true; }).catch(function() { editor.dataset.error = 'true'; });
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
image.src = objectUrl;
|
|
123
|
+
});
|
|
124
|
+
generate.addEventListener('click', function() { regenerateProfileAvatar(); editor.hidden = true; });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function render() {
|
|
128
|
+
popover = document.createElement('div');
|
|
129
|
+
popover.id = 'account-popover';
|
|
130
|
+
popover.className = 'account-popover';
|
|
131
|
+
popover.setAttribute('role', 'dialog');
|
|
132
|
+
popover.setAttribute('aria-label', 'Account');
|
|
133
|
+
var profile = getProfile();
|
|
134
|
+
var identity = document.createElement('div');
|
|
135
|
+
identity.className = 'account-menu-identity';
|
|
136
|
+
identity.innerHTML = '<button type="button" id="account-avatar-edit" class="account-avatar-edit" aria-label="Edit avatar"><img src="' + (document.body.dataset.myAvatarUrl || '') + '" alt=""></button><div class="account-identity-copy"></div><button type="button" class="account-settings-button" aria-label="User Settings" title="User Settings"><i data-lucide="settings" aria-hidden="true"></i></button>';
|
|
137
|
+
identity.querySelector('#account-avatar-edit img').alt = profile.name || 'Clay user';
|
|
138
|
+
var editor = document.createElement('div');
|
|
139
|
+
editor.id = 'account-avatar-editor';
|
|
140
|
+
editor.className = 'account-avatar-editor';
|
|
141
|
+
editor.hidden = true;
|
|
142
|
+
editor.innerHTML = '<button type="button" id="account-avatar-upload">Upload photo</button><button type="button" id="account-avatar-generate">Generate avatar</button><input id="account-avatar-file" type="file" accept="image/*">';
|
|
143
|
+
popover.appendChild(identity);
|
|
144
|
+
popover.appendChild(editor);
|
|
145
|
+
var aiSection = document.createElement('section');
|
|
146
|
+
aiSection.id = 'account-ai-section';
|
|
147
|
+
aiSection.className = 'account-inline-section account-ai-section';
|
|
148
|
+
aiSection.setAttribute('aria-label', 'Default AI');
|
|
149
|
+
popover.appendChild(aiSection);
|
|
150
|
+
mountDefaultAi(aiSection);
|
|
151
|
+
identity.querySelector('.account-settings-button').addEventListener('click', onSettings);
|
|
152
|
+
document.body.appendChild(popover);
|
|
153
|
+
renderNameResting();
|
|
154
|
+
bindAvatarEditor();
|
|
155
|
+
updateCursorAction();
|
|
156
|
+
if (window.lucide) window.lucide.createIcons({ nodes: [popover] });
|
|
157
|
+
position();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function updateCursorAction() {
|
|
161
|
+
var button = document.getElementById('cursor-share-toggle');
|
|
162
|
+
if (!button) return;
|
|
163
|
+
var hydrated = store.get('cursorSharingHydrated') === true;
|
|
164
|
+
var enabled = hydrated && store.get('cursorSharingEnabled') === true;
|
|
165
|
+
var pending = store.get('cursorSharingPending') !== null && store.get('cursorSharingPending') !== undefined;
|
|
166
|
+
var connected = store.get('connected') !== false;
|
|
167
|
+
var error = store.get('cursorSharingError');
|
|
168
|
+
var canRetry = !!error && !hydrated && connected;
|
|
169
|
+
var showOff = hydrated && !enabled && !pending && !error && connected;
|
|
170
|
+
button.hidden = store.get('isMultiUserMode') !== true;
|
|
171
|
+
button.removeAttribute('role');
|
|
172
|
+
button.removeAttribute('aria-checked');
|
|
173
|
+
button.setAttribute('aria-pressed', enabled ? 'true' : 'false');
|
|
174
|
+
button.classList.toggle('on', enabled);
|
|
175
|
+
button.classList.toggle('off', showOff);
|
|
176
|
+
button.classList.toggle('pending', pending);
|
|
177
|
+
button.disabled = pending || !connected || (!hydrated && !canRetry);
|
|
178
|
+
var stateLabel = pending ? 'waiting' : !connected ? 'offline' : error ? (canRetry ? 'error; activate to retry' : 'error') : !hydrated ? 'unavailable' : enabled ? 'on' : 'off';
|
|
179
|
+
button.setAttribute('aria-label', 'Cursor sharing ' + stateLabel);
|
|
180
|
+
var tooltipText = pending ? 'Cursor sharing is waiting for confirmation.' : !connected ? 'Cursor sharing is offline.' : error ? (canRetry ? 'Cursor sharing is unavailable. Click to retry.' : 'Cursor sharing is unavailable.') : !hydrated ? 'Cursor sharing is unavailable.' : enabled ? 'Your cursor is visible to others. Click to stop sharing.' : 'Cursor sharing is off. Click to share your cursor.';
|
|
181
|
+
registerTooltip(button, tooltipText);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function onSettings(event) { event.stopPropagation(); close(true); openUserSettings(); }
|
|
185
|
+
function onCursorAction(event) {
|
|
186
|
+
event.stopPropagation();
|
|
187
|
+
if (event.currentTarget.disabled) return;
|
|
188
|
+
if (store.get('cursorSharingError') && store.get('cursorSharingHydrated') !== true) {
|
|
189
|
+
requestCursorSharingState();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
setCursorSharing(!(store.get('cursorSharingEnabled') === true));
|
|
193
|
+
}
|
|
194
|
+
function focusAi() { var control = popover && popover.querySelector('#account-ai-section select'); if (control) control.focus({ preventScroll: true }); }
|
|
195
|
+
function onOpenDefaultAi() { if (!popover) open(); store.set({ defaultAiInlineEditing: true }); focusAi(); }
|
|
196
|
+
|
|
197
|
+
function open() {
|
|
198
|
+
if (popover) { close(true); return; }
|
|
199
|
+
lastFocus = document.activeElement;
|
|
200
|
+
render();
|
|
201
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
202
|
+
trigger.setAttribute('aria-label', 'Close account menu for ' + accountName());
|
|
203
|
+
document.addEventListener('pointerdown', onOutside);
|
|
204
|
+
document.addEventListener('focusin', onFocusOutside);
|
|
205
|
+
document.addEventListener('keydown', onKeydown);
|
|
206
|
+
window.addEventListener('resize', position);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function initAccountIsland() {
|
|
210
|
+
trigger = document.getElementById('user-account-trigger');
|
|
211
|
+
if (!trigger) return;
|
|
212
|
+
trigger.addEventListener('click', open);
|
|
213
|
+
document.addEventListener('clay:open-account-default-ai', onOpenDefaultAi);
|
|
214
|
+
var cursorToggle = document.getElementById('cursor-share-toggle');
|
|
215
|
+
if (cursorToggle) cursorToggle.addEventListener('click', onCursorAction);
|
|
216
|
+
updateCursorAction();
|
|
217
|
+
trigger.setAttribute('aria-label', 'Open account menu for ' + accountName());
|
|
218
|
+
store.subscribe(function(state, previous) {
|
|
219
|
+
if(state.myUserId !== previous.myUserId && state.accountNameDraft!==undefined)store.set({accountNameDraft:undefined});
|
|
220
|
+
if (state.myUserId !== previous.myUserId || state.connected === false && previous.connected !== false) close(false);
|
|
221
|
+
updateCursorAction();
|
|
222
|
+
if (popover) position();
|
|
223
|
+
});
|
|
224
|
+
document.addEventListener('clay:profile-updated', function() {
|
|
225
|
+
if (!popover) return;
|
|
226
|
+
var identityName = popover.querySelector('.account-name-display span');
|
|
227
|
+
if (identityName) identityName.textContent = accountName();
|
|
228
|
+
var avatar = popover.querySelector('#account-avatar-edit img');
|
|
229
|
+
if (avatar) avatar.src = document.body.dataset.myAvatarUrl || avatar.src;
|
|
230
|
+
});
|
|
231
|
+
window.addEventListener('resize', position);
|
|
232
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
|
+
import { showToast, copyToClipboard, escapeHtml } from './utils.js';
|
|
4
|
+
import { apiGet, apiPost, apiPut, apiDelete, showConfirmDialog, showConfirmResetPin, prepareAdminModal } from './admin-settings-ui.js';
|
|
5
|
+
import { setSettingsDraft, clearSettingsDraft } from './server-settings-state.js';
|
|
6
|
+
// --- SMTP Configuration ---
|
|
7
|
+
export function loadSmtpTab(body) {
|
|
8
|
+
apiGet("/api/admin/smtp").then(function (data) {
|
|
9
|
+
if (!(store.get('serverSettingsDrafts') || {}).smtp) renderSmtpOverview(body, data.smtp);
|
|
10
|
+
}).catch(function () {
|
|
11
|
+
body.innerHTML = '<div class="admin-error">Failed to load SMTP settings</div>';
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderSmtpOverview(body, cfg, focus) {
|
|
16
|
+
var configured = !!(cfg && cfg.host);
|
|
17
|
+
clearSettingsDraft('smtp');
|
|
18
|
+
body.oninput = null;
|
|
19
|
+
body.onchange = null;
|
|
20
|
+
var html = '<div class="settings-section-heading"><h3>Configuration</h3><button type="button" class="settings-btn-sm" id="smtp-edit">Edit</button></div>';
|
|
21
|
+
html += '<p class="settings-hint">' + (configured ? 'SMTP configured. New email deliveries use the saved configuration.' : 'SMTP is not configured. Users sign in with a PIN. Choose Edit to set up email delivery.') + '</p>';
|
|
22
|
+
if (configured) {
|
|
23
|
+
var fields = [
|
|
24
|
+
['SMTP host', cfg.host], ['Port', String(cfg.port || 587)],
|
|
25
|
+
['Connection', cfg.secure ? 'Direct TLS/SSL' : 'STARTTLS'],
|
|
26
|
+
['Username', cfg.user || '—'], ['Password', cfg.pass ? 'Configured · hidden' : 'Not configured'],
|
|
27
|
+
['From address', cfg.from || '—'], ['Email login', cfg.emailLoginEnabled ? 'Enabled' : 'Disabled']
|
|
28
|
+
];
|
|
29
|
+
html += '<dl class="settings-summary">';
|
|
30
|
+
fields.forEach(function (field) {
|
|
31
|
+
html += '<div class="settings-field-row"><dt class="settings-label">' + escapeHtml(field[0]) + '</dt><dd class="settings-value">' + escapeHtml(field[1]) + '</dd></div>';
|
|
32
|
+
});
|
|
33
|
+
html += '</dl><p class="settings-hint">Configuration is saved; delivery is not verified here. Edit to change settings or send a test email.</p>';
|
|
34
|
+
}
|
|
35
|
+
body.innerHTML = html;
|
|
36
|
+
var edit = body.querySelector('#smtp-edit');
|
|
37
|
+
edit.onclick = function () { renderSmtpTab(body, cfg); body.querySelector('#smtp-host').focus(); };
|
|
38
|
+
if (focus) edit.focus();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderSmtpTab(body, cfg) {
|
|
42
|
+
var hasConfig = !!(cfg && cfg.host);
|
|
43
|
+
var html = '<div class="admin-smtp-form">';
|
|
44
|
+
|
|
45
|
+
var emailEnabled = !!(cfg && cfg.emailLoginEnabled);
|
|
46
|
+
if (hasConfig) {
|
|
47
|
+
html += '<div class="admin-smtp-status admin-smtp-status-ok">' + iconHtml("check-circle") + ' SMTP configured. New email deliveries use the saved configuration.</div>';
|
|
48
|
+
} else {
|
|
49
|
+
html += '<div class="admin-smtp-status admin-smtp-status-off">' + iconHtml("mail-x") + ' SMTP not configured. Users log in with a PIN instead of email codes.</div>';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
html += '<div class="admin-smtp-fields">';
|
|
53
|
+
html += '<div class="admin-smtp-row">' +
|
|
54
|
+
'<label for="smtp-host">SMTP host</label>' +
|
|
55
|
+
'<input type="text" id="smtp-host" class="admin-smtp-input" placeholder="smtp.gmail.com" value="' + escapeHtml((cfg && cfg.host) || "") + '">' +
|
|
56
|
+
'</div>';
|
|
57
|
+
html += '<div class="admin-smtp-row-half">' +
|
|
58
|
+
'<div class="admin-smtp-row">' +
|
|
59
|
+
'<label for="smtp-port">Port</label>' +
|
|
60
|
+
'<input type="number" id="smtp-port" class="admin-smtp-input" placeholder="587" value="' + ((cfg && cfg.port) || 587) + '">' +
|
|
61
|
+
'</div>' +
|
|
62
|
+
'<div class="admin-smtp-row">' +
|
|
63
|
+
'<label for="smtp-secure">Secure (TLS)</label>' +
|
|
64
|
+
'<label class="admin-smtp-toggle"><input type="checkbox" id="smtp-secure"' + (cfg && cfg.secure ? " checked" : "") + '><span>Use TLS/SSL</span></label>' +
|
|
65
|
+
'</div></div>';
|
|
66
|
+
html += '<div class="admin-smtp-row">' +
|
|
67
|
+
'<label for="smtp-user">Username</label>' +
|
|
68
|
+
'<input type="text" id="smtp-user" class="admin-smtp-input" placeholder="you@gmail.com" value="' + escapeHtml((cfg && cfg.user) || "") + '" autocomplete="off">' +
|
|
69
|
+
'</div>';
|
|
70
|
+
html += '<div class="admin-smtp-row">' +
|
|
71
|
+
'<label for="smtp-pass">Password</label>' +
|
|
72
|
+
'<input type="password" id="smtp-pass" class="admin-smtp-input" placeholder="App password" value="' + escapeHtml((cfg && cfg.pass) || "") + '" autocomplete="off">' +
|
|
73
|
+
'</div>';
|
|
74
|
+
html += '<div class="admin-smtp-row">' +
|
|
75
|
+
'<label for="smtp-from">From address</label>' +
|
|
76
|
+
'<input type="text" id="smtp-from" class="admin-smtp-input" placeholder="Clay <noreply@example.com>" value="' + escapeHtml((cfg && cfg.from) || "") + '">' +
|
|
77
|
+
'</div>';
|
|
78
|
+
html += '<div class="admin-smtp-row admin-smtp-row-otp">' +
|
|
79
|
+
'<label for="smtp-email-login">Email login (OTP)</label>' +
|
|
80
|
+
'<label class="admin-smtp-toggle"><input type="checkbox" id="smtp-email-login"' + (emailEnabled ? " checked" : "") + (hasConfig ? "" : " disabled") + '>' +
|
|
81
|
+
'<span>' + (hasConfig ? "Require email for user registration and enable OTP login" : "Configure SMTP first to enable") + '</span></label>' +
|
|
82
|
+
'</div>';
|
|
83
|
+
html += '</div>';
|
|
84
|
+
|
|
85
|
+
html += '<div class="admin-smtp-actions">';
|
|
86
|
+
html += '<button type="button" class="admin-action-btn admin-action-btn-secondary" id="smtp-cancel">Cancel</button>';
|
|
87
|
+
html += '<button class="admin-action-btn" id="smtp-save">' + iconHtml("save") + ' Save</button>';
|
|
88
|
+
html += '<button class="admin-action-btn admin-action-btn-secondary" id="smtp-test">' + iconHtml("send") + ' Send test email</button>';
|
|
89
|
+
if (hasConfig) {
|
|
90
|
+
html += '<button class="admin-action-btn admin-action-btn-danger" id="smtp-remove">' + iconHtml("trash-2") + ' Remove</button>';
|
|
91
|
+
}
|
|
92
|
+
html += '</div>';
|
|
93
|
+
html += '<div class="admin-smtp-error" id="smtp-error"></div>';
|
|
94
|
+
html += '<div class="admin-smtp-hint">For Gmail, use an <a href="https://support.google.com/accounts/answer/185833" target="_blank" rel="noopener">App Password</a>. Port 587 with TLS off uses STARTTLS. Port 465 with TLS on uses direct SSL.</div>';
|
|
95
|
+
html += '</div>';
|
|
96
|
+
|
|
97
|
+
body.innerHTML = html;
|
|
98
|
+
clearSettingsDraft('smtp');
|
|
99
|
+
body.oninput = function () { setSettingsDraft('smtp', true); };
|
|
100
|
+
body.onchange = function () { setSettingsDraft('smtp', true); };
|
|
101
|
+
refreshIcons(body);
|
|
102
|
+
|
|
103
|
+
var errorEl = body.querySelector("#smtp-error");
|
|
104
|
+
body.querySelector('#smtp-cancel').onclick = function () {
|
|
105
|
+
if (body.querySelector('#smtp-save').disabled) return;
|
|
106
|
+
function cancel() { renderSmtpOverview(body, cfg, true); }
|
|
107
|
+
if ((store.get('serverSettingsDrafts') || {}).smtp) showConfirmDialog('Discard your unsaved email delivery changes?', 'Discard changes', cancel);
|
|
108
|
+
else cancel();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function getFormData() {
|
|
112
|
+
return {
|
|
113
|
+
host: body.querySelector("#smtp-host").value.trim(),
|
|
114
|
+
port: parseInt(body.querySelector("#smtp-port").value, 10) || 587,
|
|
115
|
+
secure: body.querySelector("#smtp-secure").checked,
|
|
116
|
+
user: body.querySelector("#smtp-user").value.trim(),
|
|
117
|
+
pass: body.querySelector("#smtp-pass").value,
|
|
118
|
+
from: body.querySelector("#smtp-from").value.trim(),
|
|
119
|
+
emailLoginEnabled: body.querySelector("#smtp-email-login").checked,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Save
|
|
124
|
+
body.querySelector("#smtp-save").addEventListener("click", function () {
|
|
125
|
+
var formData = getFormData();
|
|
126
|
+
if (!formData.host || !formData.user || !formData.pass || !formData.from) {
|
|
127
|
+
errorEl.textContent = "All fields are required";
|
|
128
|
+
errorEl.className = "admin-smtp-error admin-smtp-error-visible";
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
var btn = this;
|
|
132
|
+
btn.disabled = true;
|
|
133
|
+
body.querySelectorAll("input").forEach(function (input) { input.disabled = true; });
|
|
134
|
+
errorEl.textContent = "";
|
|
135
|
+
errorEl.className = "admin-smtp-error";
|
|
136
|
+
apiPost("/api/admin/smtp", formData).then(function (data) {
|
|
137
|
+
if (data.ok) {
|
|
138
|
+
clearSettingsDraft("smtp");
|
|
139
|
+
showToast("SMTP settings saved. New email deliveries use this configuration.");
|
|
140
|
+
renderSmtpOverview(body, Object.assign({}, formData, { pass: '••••••••' }), true);
|
|
141
|
+
} else {
|
|
142
|
+
errorEl.textContent = data.error || "Failed to save";
|
|
143
|
+
errorEl.className = "admin-smtp-error admin-smtp-error-visible";
|
|
144
|
+
btn.disabled = false;
|
|
145
|
+
body.querySelectorAll("input").forEach(function (input) { input.disabled = input.id === "smtp-email-login" && !hasConfig; });
|
|
146
|
+
}
|
|
147
|
+
}).catch(function () {
|
|
148
|
+
errorEl.textContent = "Failed to save settings";
|
|
149
|
+
errorEl.className = "admin-smtp-error admin-smtp-error-visible";
|
|
150
|
+
btn.disabled = false;
|
|
151
|
+
body.querySelectorAll("input").forEach(function (input) { input.disabled = input.id === "smtp-email-login" && !hasConfig; });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Test
|
|
156
|
+
body.querySelector("#smtp-test").addEventListener("click", function () {
|
|
157
|
+
var formData = getFormData();
|
|
158
|
+
if (!formData.host || !formData.user || !formData.pass || !formData.from) {
|
|
159
|
+
errorEl.textContent = "Fill in all fields first";
|
|
160
|
+
errorEl.className = "admin-smtp-error admin-smtp-error-visible";
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
var btn = this;
|
|
164
|
+
btn.disabled = true;
|
|
165
|
+
errorEl.textContent = "";
|
|
166
|
+
errorEl.className = "admin-smtp-error";
|
|
167
|
+
apiPost("/api/admin/smtp/test", formData).then(function (data) {
|
|
168
|
+
if (data.ok) {
|
|
169
|
+
showToast(data.message || "Test email sent!");
|
|
170
|
+
errorEl.textContent = "";
|
|
171
|
+
errorEl.className = "admin-smtp-error";
|
|
172
|
+
} else {
|
|
173
|
+
errorEl.textContent = data.error || "Test failed";
|
|
174
|
+
errorEl.className = "admin-smtp-error admin-smtp-error-visible";
|
|
175
|
+
}
|
|
176
|
+
btn.disabled = false;
|
|
177
|
+
}).catch(function () {
|
|
178
|
+
errorEl.textContent = "Connection failed";
|
|
179
|
+
errorEl.className = "admin-smtp-error admin-smtp-error-visible";
|
|
180
|
+
btn.disabled = false;
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Remove
|
|
185
|
+
var removeBtn = body.querySelector("#smtp-remove");
|
|
186
|
+
if (removeBtn) {
|
|
187
|
+
removeBtn.addEventListener("click", function () {
|
|
188
|
+
showConfirmDialog("Remove SMTP configuration? Users will need to use PIN login.", "Remove SMTP", function () {
|
|
189
|
+
apiPost("/api/admin/smtp", { host: "", user: "", pass: "", from: "" }).then(function (data) {
|
|
190
|
+
if (!data.ok) throw new Error(data.error || "Could not remove SMTP configuration");
|
|
191
|
+
showToast("SMTP configuration removed");
|
|
192
|
+
renderSmtpOverview(body, null, true);
|
|
193
|
+
}).catch(function (error) { errorEl.textContent = error.message; errorEl.className = "admin-smtp-error admin-smtp-error-visible"; });
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
|
+
import { showToast, copyToClipboard, escapeHtml } from './utils.js';
|
|
4
|
+
import { apiGet, apiPost, apiPut, apiDelete, showConfirmDialog, showConfirmResetPin, prepareAdminModal } from './admin-settings-ui.js';
|
|
5
|
+
// --- Invites ---
|
|
6
|
+
export function loadInvitesTab(body) {
|
|
7
|
+
apiGet("/api/admin/invites").then(function (data) {
|
|
8
|
+
store.set({ adminInvites: (data.invites || []).filter(function (inv) { return !inv.used && inv.expiresAt > Date.now(); }) });
|
|
9
|
+
renderInvitesTab(body);
|
|
10
|
+
}).catch(function () {
|
|
11
|
+
body.innerHTML = '<div class="admin-error">Failed to load invites</div>';
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderInvitesTab(body) {
|
|
16
|
+
var smtpEnabled = store.get('adminMe') && store.get('adminMe').smtpEnabled;
|
|
17
|
+
var html = '<div class="admin-section-header">' +
|
|
18
|
+
'<div class="admin-header-btns">';
|
|
19
|
+
if (smtpEnabled) {
|
|
20
|
+
html += '<button class="admin-action-btn" id="admin-email-invite">' + iconHtml("mail") + ' Email Invite</button>';
|
|
21
|
+
}
|
|
22
|
+
html += '<button class="admin-action-btn admin-action-btn-secondary" id="admin-create-invite">' + iconHtml("plus") + ' Generate Link</button>' +
|
|
23
|
+
'</div></div>';
|
|
24
|
+
|
|
25
|
+
if (store.get('adminInvites').length === 0) {
|
|
26
|
+
html += '<div class="admin-empty">No active invites. Generate one to add a new user.</div>';
|
|
27
|
+
} else {
|
|
28
|
+
html += '<div class="admin-invite-list">';
|
|
29
|
+
for (var i = 0; i < store.get('adminInvites').length; i++) {
|
|
30
|
+
var inv = store.get('adminInvites')[i];
|
|
31
|
+
var expiresIn = Math.max(0, Math.ceil((inv.expiresAt - Date.now()) / (60 * 60 * 1000)));
|
|
32
|
+
html += '<div class="admin-invite-item">';
|
|
33
|
+
html += '<div class="admin-invite-info">';
|
|
34
|
+
html += '<code class="admin-invite-code">' + escapeHtml(inv.code.substring(0, 8)) + '...</code>';
|
|
35
|
+
if (inv.email) html += '<span class="admin-invite-email">' + escapeHtml(inv.email) + '</span>';
|
|
36
|
+
html += '<span class="admin-invite-expiry">expires in ' + expiresIn + 'h</span>';
|
|
37
|
+
html += '</div>';
|
|
38
|
+
html += '<div class="admin-invite-actions">';
|
|
39
|
+
html += '<button class="admin-copy-link-btn" data-code="' + escapeHtml(inv.code) + '" title="Copy link">' + iconHtml("copy") + '</button>';
|
|
40
|
+
html += '<button class="admin-revoke-btn" data-code="' + escapeHtml(inv.code) + '" title="Revoke">' + iconHtml("x") + '</button>';
|
|
41
|
+
html += '</div>';
|
|
42
|
+
html += '</div>';
|
|
43
|
+
}
|
|
44
|
+
html += '</div>';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
body.innerHTML = html;
|
|
48
|
+
refreshIcons(body);
|
|
49
|
+
|
|
50
|
+
// Generate invite link
|
|
51
|
+
var createBtn = body.querySelector("#admin-create-invite");
|
|
52
|
+
if (createBtn) {
|
|
53
|
+
createBtn.addEventListener("click", function () {
|
|
54
|
+
createInvite(body);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Email invite
|
|
59
|
+
var emailBtn = body.querySelector("#admin-email-invite");
|
|
60
|
+
if (emailBtn) {
|
|
61
|
+
emailBtn.addEventListener("click", function () {
|
|
62
|
+
showEmailInvitePrompt(body);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Copy link buttons
|
|
67
|
+
var copyBtns = body.querySelectorAll(".admin-copy-link-btn");
|
|
68
|
+
for (var j = 0; j < copyBtns.length; j++) {
|
|
69
|
+
copyBtns[j].addEventListener("click", function () {
|
|
70
|
+
var code = this.dataset.code;
|
|
71
|
+
var url = location.origin + "/invite/" + code;
|
|
72
|
+
copyToClipboard(url).then(function () {
|
|
73
|
+
showToast("Invite link copied");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Revoke buttons
|
|
79
|
+
var revokeBtns = body.querySelectorAll(".admin-revoke-btn");
|
|
80
|
+
for (var k = 0; k < revokeBtns.length; k++) {
|
|
81
|
+
revokeBtns[k].addEventListener("click", function () {
|
|
82
|
+
var code = this.dataset.code;
|
|
83
|
+
var btn = this;
|
|
84
|
+
showConfirmDialog("Revoke this invite? The link will no longer work.", "Revoke", function () {
|
|
85
|
+
btn.disabled = true;
|
|
86
|
+
fetch("/api/admin/invites/" + encodeURIComponent(code), { method: "DELETE" })
|
|
87
|
+
.then(function (r) { return r.json(); })
|
|
88
|
+
.then(function (d) {
|
|
89
|
+
if (d.ok) {
|
|
90
|
+
showToast("Invite revoked");
|
|
91
|
+
loadInvitesTab(body);
|
|
92
|
+
} else {
|
|
93
|
+
showToast(d.error || "Failed to revoke");
|
|
94
|
+
btn.disabled = false;
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.catch(function () {
|
|
98
|
+
showToast("Failed to revoke invite");
|
|
99
|
+
btn.disabled = false;
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createInvite(body) {
|
|
107
|
+
apiPost("/api/admin/invites").then(function (data) {
|
|
108
|
+
if (data.ok && data.url) {
|
|
109
|
+
copyToClipboard(data.url).then(function () {
|
|
110
|
+
showToast("Invite link created and copied!");
|
|
111
|
+
}).catch(function () {
|
|
112
|
+
showToast("Invite created: " + data.url);
|
|
113
|
+
});
|
|
114
|
+
loadInvitesTab(body);
|
|
115
|
+
} else {
|
|
116
|
+
showToast(data.error || "Failed to create invite");
|
|
117
|
+
}
|
|
118
|
+
}).catch(function () {
|
|
119
|
+
showToast("Failed to create invite");
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function showEmailInvitePrompt(body) {
|
|
124
|
+
var modal = document.createElement("div");
|
|
125
|
+
modal.className = "admin-modal-overlay";
|
|
126
|
+
var html = '<div class="admin-modal">' +
|
|
127
|
+
'<div class="admin-modal-header">' +
|
|
128
|
+
'<h3>Send Email Invite</h3>' +
|
|
129
|
+
'<button class="admin-modal-close">' + iconHtml("x") + '</button>' +
|
|
130
|
+
'</div>' +
|
|
131
|
+
'<div class="admin-modal-body">' +
|
|
132
|
+
'<p class="admin-modal-desc">Enter the email address to send an invitation to:</p>' +
|
|
133
|
+
'<input type="email" class="admin-smtp-input" id="admin-invite-email" placeholder="user@example.com" autocomplete="off">' +
|
|
134
|
+
'<div class="admin-smtp-error" id="admin-invite-error"></div>' +
|
|
135
|
+
'</div>' +
|
|
136
|
+
'<div class="admin-modal-footer">' +
|
|
137
|
+
'<button class="admin-modal-save" id="admin-invite-send">Send Invite</button>' +
|
|
138
|
+
'<button class="admin-modal-cancel">Cancel</button>' +
|
|
139
|
+
'</div></div>';
|
|
140
|
+
modal.innerHTML = html;
|
|
141
|
+
document.body.appendChild(modal);
|
|
142
|
+
prepareAdminModal(modal);
|
|
143
|
+
refreshIcons(modal);
|
|
144
|
+
|
|
145
|
+
var emailInput = modal.querySelector("#admin-invite-email");
|
|
146
|
+
var sendBtn = modal.querySelector("#admin-invite-send");
|
|
147
|
+
var errorEl = modal.querySelector("#admin-invite-error");
|
|
148
|
+
|
|
149
|
+
modal.querySelector(".admin-modal-close").addEventListener("click", function () { modal.remove(); });
|
|
150
|
+
modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
|
|
151
|
+
modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
|
|
152
|
+
|
|
153
|
+
emailInput.focus();
|
|
154
|
+
emailInput.addEventListener("keydown", function (e) {
|
|
155
|
+
if (e.key === "Enter") sendBtn.click();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
sendBtn.addEventListener("click", function () {
|
|
159
|
+
var email = emailInput.value.trim();
|
|
160
|
+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
161
|
+
errorEl.textContent = "Enter a valid email address";
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
sendBtn.disabled = true;
|
|
165
|
+
sendBtn.textContent = "Sending...";
|
|
166
|
+
errorEl.textContent = "";
|
|
167
|
+
apiPost("/api/admin/invites/email", { email: email }).then(function (data) {
|
|
168
|
+
if (data.ok) {
|
|
169
|
+
showToast("Invite sent to " + email);
|
|
170
|
+
modal.remove();
|
|
171
|
+
loadInvitesTab(body);
|
|
172
|
+
} else {
|
|
173
|
+
errorEl.textContent = data.error || "Failed to send invite";
|
|
174
|
+
sendBtn.disabled = false;
|
|
175
|
+
sendBtn.textContent = "Send Invite";
|
|
176
|
+
}
|
|
177
|
+
}).catch(function () {
|
|
178
|
+
errorEl.textContent = "Failed to send invite";
|
|
179
|
+
sendBtn.disabled = false;
|
|
180
|
+
sendBtn.textContent = "Send Invite";
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|