codeapp-js 0.2.1 → 0.3.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.
- package/codeApp/.power/schemas/appschemas/dataSourcesInfo.ts +6275 -0
- package/codeApp/.power/schemas/jira/jira.Schema.json +6903 -0
- package/codeApp/.power/schemas/keyvault/keyvault.Schema.json +1600 -0
- package/codeApp/.power/schemas/teams/teams.Schema.json +11112 -0
- package/codeApp/dist/codeapp.js +992 -4
- package/codeApp/dist/power-apps-data.js +145 -31
- package/codeApp/src/generated/index.ts +12 -0
- package/codeApp/src/generated/models/AzureKeyVaultModel.ts +107 -0
- package/codeApp/src/generated/models/JiraModel.ts +501 -0
- package/codeApp/src/generated/models/Office365GroupsModel.ts +363 -0
- package/codeApp/src/generated/models/Office365OutlookModel.ts +2046 -0
- package/codeApp/src/generated/models/Office365UsersModel.ts +254 -0
- package/codeApp/src/generated/services/AzureKeyVaultService.ts +257 -0
- package/codeApp/src/generated/services/JiraService.ts +1124 -0
- package/codeApp/src/generated/services/Office365GroupsService.ts +326 -0
- package/codeApp/src/generated/services/Office365OutlookService.ts +2476 -0
- package/codeApp/src/generated/services/Office365UsersService.ts +358 -0
- package/dev files/outlook.js +218 -9
- package/examples/combined demo/dist/codeapp.js +1098 -1098
- package/examples/combined demo/dist/index.js +470 -470
- package/examples/combined demo/dist/power-apps-data.js +3006 -3006
- package/examples/combined demo/power.config.json +42 -42
- package/examples/dataverse Demo/dist/codeapp.js +1085 -1085
- package/examples/dataverse Demo/dist/index.html +54 -54
- package/examples/dataverse Demo/dist/index.js +82 -82
- package/examples/dataverse Demo/dist/power-apps-data.js +2911 -2911
- package/examples/dataverse Demo/power.config.json +34 -34
- package/examples/dataverse Demo/readme.md +79 -79
- package/examples/groups Demo/dist/codeapp.js +1085 -1085
- package/examples/groups Demo/dist/index.js +113 -113
- package/examples/groups Demo/dist/power-apps-data.js +2911 -2911
- package/examples/kanban/dist/dataverse.js +94 -94
- package/examples/kanban/dist/environmentVar.js +55 -55
- package/examples/kanban/dist/office365groups.js +97 -97
- package/examples/kanban/dist/office365users.js +169 -169
- package/examples/kanban/dist/outlook.js +162 -162
- package/examples/kanban/dist/power-apps-data.js +2953 -2953
- package/examples/kanban/dist/sharepoint.js +339 -339
- package/examples/myProfile/dist/index.html +184 -184
- package/examples/myProfile/dist/index.js +141 -141
- package/examples/myProfile/dist/office365users.js +169 -169
- package/examples/myProfile/dist/power-apps-data.js +2953 -2953
- package/examples/myProfile/power.config.json +22 -22
- package/examples/myProfile/readme.md +79 -79
- package/examples/outlook Demo/dist/codeapp.js +1085 -1085
- package/examples/outlook Demo/dist/index.html +35 -35
- package/examples/outlook Demo/dist/index.js +170 -170
- package/examples/outlook Demo/dist/outlook.js +121 -121
- package/examples/outlook Demo/dist/power-apps-data.js +2911 -2911
- package/examples/outlook Demo/dist/styles.css +84 -84
- package/examples/outlook Demo/readme.md +82 -82
- package/examples/outlook Demo2/OutlookDemo_1_0_0_1.zip +0 -0
- package/examples/outlook Demo2/agent/decision-log.md +7 -0
- package/examples/{solution explorer → outlook Demo2}/dist/codeapp.js +245 -9
- package/examples/outlook Demo2/dist/index.html +98 -0
- package/examples/outlook Demo2/dist/index.js +272 -0
- package/examples/outlook Demo2/dist/styles.css +639 -0
- package/examples/outlook Demo2/power.config.json +23 -0
- package/examples/outlook Demo2/src/generated/index.ts +14 -0
- package/examples/outlook Demo2/src/generated/models/Office365GroupsModel.ts +363 -0
- package/examples/outlook Demo2/src/generated/models/Office365OutlookModel.ts +2046 -0
- package/examples/outlook Demo2/src/generated/models/Office365UsersModel.ts +254 -0
- package/examples/outlook Demo2/src/generated/services/Office365GroupsService.ts +326 -0
- package/examples/outlook Demo2/src/generated/services/Office365OutlookService.ts +2476 -0
- package/examples/outlook Demo2/src/generated/services/Office365UsersService.ts +358 -0
- package/examples/planning Poker/.vscode/settings.json +4 -4
- package/examples/planning Poker/additional files/customizations (tables).xml +6428 -6428
- package/examples/planning Poker/additional files/dataverse-tables.json +165 -165
- package/examples/planning Poker/additional files/readme.md +122 -122
- package/examples/planning Poker/dist/dataverse.js +78 -78
- package/examples/planning Poker/dist/index.html +198 -198
- package/examples/planning Poker/dist/index.js +954 -954
- package/examples/planning Poker/dist/power-apps-data.js +2953 -2953
- package/examples/planning Poker/dist/styles.css +815 -815
- package/examples/sharePoint Demo/agent/decision-log.md +5 -5
- package/examples/sharePoint Demo/dist/codeapp.js +1085 -1085
- package/examples/sharePoint Demo/dist/index.js +262 -262
- package/examples/sharePoint Demo/dist/power-apps-data.js +2911 -2911
- package/examples/sharePoint Demo/power.config.json +22 -22
- package/examples/todo/dist/dataverse.js +64 -64
- package/examples/todo/dist/index.html +75 -75
- package/examples/todo/dist/index.js +8 -8
- package/examples/todo/dist/power-apps-data.js +2953 -2953
- package/examples/todo/dist/renderer.js +375 -375
- package/examples/todo/dist/styles.css +691 -691
- package/examples/todo/power.config.json +34 -34
- package/package.json +1 -1
- package/readme.md +33 -4
- package/examples/solution explorer/agent/decision-log.md +0 -27
- package/examples/solution explorer/agent/mockup-01-swiss-grid.html +0 -452
- package/examples/solution explorer/agent/mockup-02-dark-glass.html +0 -496
- package/examples/solution explorer/agent/mockup-03-paper-console.html +0 -510
- package/examples/solution explorer/agent/mockup-04-neon-noir.html +0 -546
- package/examples/solution explorer/agent/mockup-05-zen-garden.html +0 -534
- package/examples/solution explorer/dist/index.html +0 -80
- package/examples/solution explorer/dist/index.js +0 -735
- package/examples/solution explorer/dist/styles.css +0 -571
- package/examples/solution explorer/power.config.json +0 -151
- /package/examples/{solution explorer → outlook Demo2}/dist/icon-512.png +0 -0
- /package/examples/{solution explorer → outlook Demo2}/dist/power-apps-data.js +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Outlook Mail</title>
|
|
8
|
+
<link rel="stylesheet" href="styles.css" />
|
|
9
|
+
<script type="module" src="power-apps-data.js"></script>
|
|
10
|
+
<script type="module" src="codeapp.js"></script>
|
|
11
|
+
<script type="module" src="index.js"></script>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="root">
|
|
15
|
+
<!-- Header -->
|
|
16
|
+
<header class="app-header">
|
|
17
|
+
<div class="app-header__brand">
|
|
18
|
+
<div class="app-header__icon">M</div>
|
|
19
|
+
<h1 class="app-header__title">Mail</h1>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="app-header__actions">
|
|
22
|
+
<button class="btn btn--secondary" id="btnRefresh" title="Refresh inbox">
|
|
23
|
+
↻ Refresh
|
|
24
|
+
</button>
|
|
25
|
+
<button class="btn btn--primary" id="btnCompose" title="Compose new email">
|
|
26
|
+
✎ Compose
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</header>
|
|
30
|
+
|
|
31
|
+
<!-- Body -->
|
|
32
|
+
<div class="app-body" id="appBody">
|
|
33
|
+
<!-- Inbox List -->
|
|
34
|
+
<aside class="inbox-panel">
|
|
35
|
+
<div class="inbox-panel__header">
|
|
36
|
+
<span class="inbox-panel__title">Inbox</span>
|
|
37
|
+
<span class="inbox-panel__count" id="emailCount">—</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="inbox-panel__list" id="emailList">
|
|
40
|
+
<!-- Skeleton loader -->
|
|
41
|
+
<div class="loading-skeleton" id="loadingSkeleton">
|
|
42
|
+
<div class="skeleton-item"><div class="skeleton-circle"></div><div class="skeleton-lines"><div class="skeleton-line skeleton-line--short"></div><div class="skeleton-line skeleton-line--medium"></div><div class="skeleton-line"></div></div></div>
|
|
43
|
+
<div class="skeleton-item"><div class="skeleton-circle"></div><div class="skeleton-lines"><div class="skeleton-line skeleton-line--short"></div><div class="skeleton-line skeleton-line--medium"></div><div class="skeleton-line"></div></div></div>
|
|
44
|
+
<div class="skeleton-item"><div class="skeleton-circle"></div><div class="skeleton-lines"><div class="skeleton-line skeleton-line--short"></div><div class="skeleton-line skeleton-line--medium"></div><div class="skeleton-line"></div></div></div>
|
|
45
|
+
<div class="skeleton-item"><div class="skeleton-circle"></div><div class="skeleton-lines"><div class="skeleton-line skeleton-line--short"></div><div class="skeleton-line skeleton-line--medium"></div><div class="skeleton-line"></div></div></div>
|
|
46
|
+
<div class="skeleton-item"><div class="skeleton-circle"></div><div class="skeleton-lines"><div class="skeleton-line skeleton-line--short"></div><div class="skeleton-line skeleton-line--medium"></div><div class="skeleton-line"></div></div></div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</aside>
|
|
50
|
+
|
|
51
|
+
<!-- Detail Panel -->
|
|
52
|
+
<main class="detail-panel" id="detailPanel">
|
|
53
|
+
<div class="detail-panel__empty" id="detailEmpty">
|
|
54
|
+
<div class="detail-panel__empty-icon">✉</div>
|
|
55
|
+
<div class="detail-panel__empty-text">Select an email to read</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="detail-panel__content" id="detailContent" style="display:none;"></div>
|
|
58
|
+
</main>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Status Bar -->
|
|
62
|
+
<div class="status-bar" id="statusBar">
|
|
63
|
+
<div class="status-bar__dot"></div>
|
|
64
|
+
<span id="statusText">Ready</span>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Compose Modal -->
|
|
68
|
+
<div class="modal-overlay" id="composeOverlay">
|
|
69
|
+
<div class="compose-modal">
|
|
70
|
+
<div class="compose-modal__header">
|
|
71
|
+
<h2 class="compose-modal__title">New Message</h2>
|
|
72
|
+
<button class="compose-modal__close" id="btnCloseCompose">✕</button>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="compose-modal__body">
|
|
75
|
+
<div class="form-group">
|
|
76
|
+
<label class="form-group__label" for="composeTo">To</label>
|
|
77
|
+
<input class="form-group__input" type="email" id="composeTo" placeholder="recipient@example.com" />
|
|
78
|
+
</div>
|
|
79
|
+
<div class="form-group">
|
|
80
|
+
<label class="form-group__label" for="composeSubject">Subject</label>
|
|
81
|
+
<input class="form-group__input" type="text" id="composeSubject" placeholder="What is this about?" />
|
|
82
|
+
</div>
|
|
83
|
+
<div class="form-group">
|
|
84
|
+
<label class="form-group__label" for="composeBody">Message</label>
|
|
85
|
+
<textarea class="form-group__input form-group__input--textarea" id="composeBody" placeholder="Write your message…"></textarea>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="compose-modal__footer">
|
|
89
|
+
<button class="btn btn--ghost" id="btnDiscardCompose">Discard</button>
|
|
90
|
+
<button class="btn btn--primary" id="btnSendEmail">
|
|
91
|
+
Send ➤
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</body>
|
|
98
|
+
</html>
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { listEmails, sendEmail } from './codeapp.js';
|
|
2
|
+
|
|
3
|
+
// ===== STATE =====
|
|
4
|
+
let emails = [];
|
|
5
|
+
let selectedEmailId = null;
|
|
6
|
+
|
|
7
|
+
// ===== DOM REFS =====
|
|
8
|
+
const $ = (sel) => document.querySelector(sel);
|
|
9
|
+
const emailListEl = () => $('#emailList');
|
|
10
|
+
const emailCountEl = () => $('#emailCount');
|
|
11
|
+
const loadingEl = () => $('#loadingSkeleton');
|
|
12
|
+
const detailEmpty = () => $('#detailEmpty');
|
|
13
|
+
const detailContent = () => $('#detailContent');
|
|
14
|
+
const statusBar = () => $('#statusBar');
|
|
15
|
+
const statusText = () => $('#statusText');
|
|
16
|
+
const composeOverlay = () => $('#composeOverlay');
|
|
17
|
+
|
|
18
|
+
// ===== STATUS HELPERS =====
|
|
19
|
+
const setStatus = (text, type = 'ok') => {
|
|
20
|
+
const bar = statusBar();
|
|
21
|
+
const txt = statusText();
|
|
22
|
+
if (!bar || !txt) return;
|
|
23
|
+
bar.className = 'status-bar' + (type === 'error' ? ' status-bar--error' : type === 'loading' ? ' status-bar--loading' : '');
|
|
24
|
+
txt.textContent = text;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ===== FORMAT HELPERS =====
|
|
28
|
+
const getInitials = (name) => {
|
|
29
|
+
if (!name) return '?';
|
|
30
|
+
const parts = name.trim().split(/\s+/);
|
|
31
|
+
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
32
|
+
return name.slice(0, 2).toUpperCase();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const formatTime = (dateStr) => {
|
|
36
|
+
if (!dateStr) return '';
|
|
37
|
+
try {
|
|
38
|
+
const d = new Date(dateStr);
|
|
39
|
+
const now = new Date();
|
|
40
|
+
const diff = now - d;
|
|
41
|
+
const oneDay = 86400000;
|
|
42
|
+
if (diff < oneDay && d.getDate() === now.getDate()) {
|
|
43
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
44
|
+
}
|
|
45
|
+
if (diff < 7 * oneDay) {
|
|
46
|
+
return d.toLocaleDateString([], { weekday: 'short' });
|
|
47
|
+
}
|
|
48
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
49
|
+
} catch {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const formatFullDate = (dateStr) => {
|
|
55
|
+
if (!dateStr) return '';
|
|
56
|
+
try {
|
|
57
|
+
return new Date(dateStr).toLocaleString([], {
|
|
58
|
+
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
|
59
|
+
hour: '2-digit', minute: '2-digit'
|
|
60
|
+
});
|
|
61
|
+
} catch {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const stripHtml = (html) => {
|
|
67
|
+
if (!html) return '';
|
|
68
|
+
const tmp = document.createElement('div');
|
|
69
|
+
tmp.innerHTML = html;
|
|
70
|
+
return tmp.textContent || tmp.innerText || '';
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ===== RENDER INBOX =====
|
|
74
|
+
const renderEmailList = () => {
|
|
75
|
+
const list = emailListEl();
|
|
76
|
+
if (!list) return;
|
|
77
|
+
|
|
78
|
+
const skeleton = loadingEl();
|
|
79
|
+
if (skeleton) skeleton.remove();
|
|
80
|
+
|
|
81
|
+
if (emails.length === 0) {
|
|
82
|
+
list.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--ink-muted);font-size:14px;">No emails found</div>';
|
|
83
|
+
emailCountEl().textContent = '0';
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
emailCountEl().textContent = `${emails.length}`;
|
|
88
|
+
|
|
89
|
+
list.innerHTML = emails.map((email) => {
|
|
90
|
+
const id = email.Id || email.id || '';
|
|
91
|
+
const from = email.From || email.from;
|
|
92
|
+
const senderName = from?.EmailAddress?.Name || from?.emailAddress?.name || from?.Name || 'Unknown';
|
|
93
|
+
const subject = email.Subject || email.subject || '(No subject)';
|
|
94
|
+
const preview = stripHtml(email.BodyPreview || email.bodyPreview || email.Body?.Content || '');
|
|
95
|
+
const dateStr = email.DateTimeReceived || email.receivedDateTime || email.ReceivedDateTime || '';
|
|
96
|
+
const isRead = email.IsRead ?? email.isRead ?? true;
|
|
97
|
+
const isActive = id === selectedEmailId;
|
|
98
|
+
|
|
99
|
+
return `
|
|
100
|
+
<div class="email-item ${!isRead ? 'email-item--unread' : ''} ${isActive ? 'email-item--active' : ''}"
|
|
101
|
+
data-id="${id}" role="button" tabindex="0">
|
|
102
|
+
<div class="email-item__avatar">${getInitials(senderName)}</div>
|
|
103
|
+
<div class="email-item__content">
|
|
104
|
+
<div class="email-item__row">
|
|
105
|
+
<span class="email-item__sender">${escapeHtml(senderName)}</span>
|
|
106
|
+
<span class="email-item__time">${formatTime(dateStr)}</span>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="email-item__subject">${escapeHtml(subject)}</div>
|
|
109
|
+
<div class="email-item__preview">${escapeHtml(preview.slice(0, 100))}</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
`;
|
|
113
|
+
}).join('');
|
|
114
|
+
|
|
115
|
+
// Attach click handlers
|
|
116
|
+
list.querySelectorAll('.email-item').forEach((el) => {
|
|
117
|
+
el.addEventListener('click', () => selectEmail(el.dataset.id));
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const escapeHtml = (str) => {
|
|
122
|
+
const div = document.createElement('div');
|
|
123
|
+
div.appendChild(document.createTextNode(str || ''));
|
|
124
|
+
return div.innerHTML;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ===== SELECT EMAIL =====
|
|
128
|
+
const selectEmail = (id) => {
|
|
129
|
+
selectedEmailId = id;
|
|
130
|
+
const email = emails.find((e) => (e.Id || e.id) === id);
|
|
131
|
+
if (!email) return;
|
|
132
|
+
|
|
133
|
+
// Re-render list to update active state
|
|
134
|
+
renderEmailList();
|
|
135
|
+
|
|
136
|
+
// Show detail
|
|
137
|
+
const empty = detailEmpty();
|
|
138
|
+
const content = detailContent();
|
|
139
|
+
if (empty) empty.style.display = 'none';
|
|
140
|
+
if (!content) return;
|
|
141
|
+
content.style.display = 'block';
|
|
142
|
+
|
|
143
|
+
const from = email.From || email.from;
|
|
144
|
+
const senderName = from?.EmailAddress?.Name || from?.emailAddress?.name || from?.Name || 'Unknown';
|
|
145
|
+
const senderEmail = from?.EmailAddress?.Address || from?.emailAddress?.address || from?.Address || '';
|
|
146
|
+
const subject = email.Subject || email.subject || '(No subject)';
|
|
147
|
+
const dateStr = email.DateTimeReceived || email.receivedDateTime || email.ReceivedDateTime || '';
|
|
148
|
+
const body = email.Body?.Content || email.body?.content || email.BodyPreview || email.bodyPreview || '';
|
|
149
|
+
|
|
150
|
+
content.innerHTML = `
|
|
151
|
+
<h2 class="detail-panel__subject">${escapeHtml(subject)}</h2>
|
|
152
|
+
<div class="detail-panel__meta">
|
|
153
|
+
<div class="detail-panel__meta-avatar">${getInitials(senderName)}</div>
|
|
154
|
+
<div class="detail-panel__meta-info">
|
|
155
|
+
<div class="detail-panel__meta-sender">${escapeHtml(senderName)}</div>
|
|
156
|
+
<div class="detail-panel__meta-email">${escapeHtml(senderEmail)}</div>
|
|
157
|
+
<div class="detail-panel__meta-date">${formatFullDate(dateStr)}</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="detail-panel__body">${body}</div>
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
// Mobile: show detail
|
|
164
|
+
const appBody = $('#appBody');
|
|
165
|
+
if (appBody) appBody.classList.add('app-body--detail-open');
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// ===== FETCH INBOX =====
|
|
169
|
+
const fetchInbox = async () => {
|
|
170
|
+
setStatus('Loading inbox…', 'loading');
|
|
171
|
+
try {
|
|
172
|
+
const result = await listEmails({ folderId: 'Inbox', top: 25 });
|
|
173
|
+
emails = Array.isArray(result) ? result : [];
|
|
174
|
+
if (result && Array.isArray(result.value)) emails = result.value;
|
|
175
|
+
renderEmailList();
|
|
176
|
+
setStatus(`${emails.length} email${emails.length !== 1 ? 's' : ''} loaded`);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error('Failed to fetch inbox:', err);
|
|
179
|
+
setStatus(`Error: ${err.message}`, 'error');
|
|
180
|
+
const list = emailListEl();
|
|
181
|
+
const skeleton = loadingEl();
|
|
182
|
+
if (skeleton) skeleton.remove();
|
|
183
|
+
if (list) {
|
|
184
|
+
list.innerHTML = `<div style="padding:40px 20px;text-align:center;color:var(--accent);font-size:14px;">
|
|
185
|
+
Failed to load emails.<br><span style="color:var(--ink-muted);font-size:12px;">${escapeHtml(err.message)}</span>
|
|
186
|
+
</div>`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// ===== SEND EMAIL =====
|
|
192
|
+
const doSendEmail = async () => {
|
|
193
|
+
const toEl = $('#composeTo');
|
|
194
|
+
const subjectEl = $('#composeSubject');
|
|
195
|
+
const bodyEl = $('#composeBody');
|
|
196
|
+
const sendBtn = $('#btnSendEmail');
|
|
197
|
+
|
|
198
|
+
const to = toEl?.value?.trim();
|
|
199
|
+
const subject = subjectEl?.value?.trim();
|
|
200
|
+
const body = bodyEl?.value?.trim();
|
|
201
|
+
|
|
202
|
+
if (!to) {
|
|
203
|
+
toEl?.focus();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
sendBtn.disabled = true;
|
|
208
|
+
sendBtn.textContent = 'Sending…';
|
|
209
|
+
setStatus('Sending email…', 'loading');
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
await sendEmail({
|
|
213
|
+
to: to,
|
|
214
|
+
subject: subject || '(No subject)',
|
|
215
|
+
body: '<p>' + escapeHtml(body || '') + '</p>',
|
|
216
|
+
importance: 'Normal'
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
setStatus('Email sent successfully');
|
|
220
|
+
closeCompose();
|
|
221
|
+
|
|
222
|
+
// Clear form
|
|
223
|
+
if (toEl) toEl.value = '';
|
|
224
|
+
if (subjectEl) subjectEl.value = '';
|
|
225
|
+
if (bodyEl) bodyEl.value = '';
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error('Failed to send email:', err);
|
|
228
|
+
setStatus(`Send failed: ${err.message}`, 'error');
|
|
229
|
+
} finally {
|
|
230
|
+
sendBtn.disabled = false;
|
|
231
|
+
sendBtn.textContent = 'Send ➤';
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// ===== COMPOSE MODAL =====
|
|
236
|
+
const openCompose = () => {
|
|
237
|
+
const overlay = composeOverlay();
|
|
238
|
+
if (overlay) overlay.classList.add('modal-overlay--visible');
|
|
239
|
+
setTimeout(() => $('#composeTo')?.focus(), 200);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const closeCompose = () => {
|
|
243
|
+
const overlay = composeOverlay();
|
|
244
|
+
if (overlay) overlay.classList.remove('modal-overlay--visible');
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// ===== EVENT BINDINGS =====
|
|
248
|
+
const bindEvents = () => {
|
|
249
|
+
$('#btnCompose')?.addEventListener('click', openCompose);
|
|
250
|
+
$('#btnCloseCompose')?.addEventListener('click', closeCompose);
|
|
251
|
+
$('#btnDiscardCompose')?.addEventListener('click', closeCompose);
|
|
252
|
+
$('#btnSendEmail')?.addEventListener('click', doSendEmail);
|
|
253
|
+
$('#btnRefresh')?.addEventListener('click', fetchInbox);
|
|
254
|
+
|
|
255
|
+
// Close modal on overlay click
|
|
256
|
+
composeOverlay()?.addEventListener('click', (e) => {
|
|
257
|
+
if (e.target === composeOverlay()) closeCompose();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Escape key closes modal
|
|
261
|
+
document.addEventListener('keydown', (e) => {
|
|
262
|
+
if (e.key === 'Escape') closeCompose();
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// ===== BOOT =====
|
|
267
|
+
async function boot() {
|
|
268
|
+
bindEvents();
|
|
269
|
+
await fetchInbox();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
boot();
|