Example: Task Tracker
A complete task management app with priorities, due dates, and role-based editing.
Schema
Entity: Tasks — key: tasks
| Field | Type | Notes |
|---|---|---|
title |
text | required |
description |
textarea | |
status |
select | open / in_progress / done — required |
priority |
select | low / medium / high / critical |
due_date |
date | |
assignee_email |
Source Code
1<!DOCTYPE html>2<html lang="en">3<head>4 <meta charset="UTF-8">5 <meta name="viewport" content="width=device-width, initial-scale=1.0">6 <title>Task Tracker</title>7 <style>8 * { box-sizing: border-box; margin: 0; padding: 0; }9 body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; color: #1a1a1a; }10 h1 { margin-bottom: 20px; }11 .controls { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }12 .controls select, .controls button { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }13 .controls button { background: #2563eb; color: white; border: none; cursor: pointer; }14 .controls button:hover { background: #1d4ed8; }15 .task { border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin-bottom: 12px; }16 .task h3 { margin-bottom: 4px; }17 .task .meta { font-size: 13px; color: #6b7280; display: flex; gap: 12px; margin-top: 8px; flex-wrap: wrap; }18 .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; }19 .badge.open { background: #dbeafe; color: #1e40af; }20 .badge.in_progress { background: #fef3c7; color: #92400e; }21 .badge.done { background: #d1fae5; color: #065f46; }22 .badge.low { background: #f3f4f6; color: #374151; }23 .badge.medium { background: #fef3c7; color: #92400e; }24 .badge.high { background: #fee2e2; color: #991b1b; }25 .badge.critical { background: #991b1b; color: white; }26 .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 100; justify-content: center; align-items: center; }27 .modal-overlay.open { display: flex; }28 .modal { background: white; border-radius: 12px; padding: 24px; width: 100%; max-width: 480px; }29 .modal h2 { margin-bottom: 16px; }30 .field { margin-bottom: 12px; }31 .field label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; }32 .field input, .field select, .field textarea { width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }33 .field textarea { resize: vertical; min-height: 80px; }34 .actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }35 .actions button { padding: 8px 16px; border-radius: 6px; font-size: 14px; cursor: pointer; }36 .btn-cancel { background: white; border: 1px solid #d1d5db; }37 .btn-submit { background: #2563eb; color: white; border: none; }38 .error { color: #dc2626; font-size: 13px; margin-top: 4px; }39 .empty { text-align: center; padding: 40px; color: #9ca3af; }40 .loading { text-align: center; padding: 40px; color: #6b7280; }41 .task-actions { float: right; display: flex; gap: 4px; }42 .task-actions button { padding: 4px 8px; font-size: 12px; border: 1px solid #d1d5db; border-radius: 4px; background: white; cursor: pointer; }43 .task-actions button.delete { color: #dc2626; border-color: #fecaca; }44 </style>45</head>46<body>47 <h1>Task Tracker</h1>4849 <div class="controls">50 <select id="filter-status">51 <option value="">All statuses</option>52 <option value="open">Open</option>53 <option value="in_progress">In Progress</option>54 <option value="done">Done</option>55 </select>56 <button id="btn-create" style="display:none;">+ New Task</button>57 </div>5859 <div id="task-list"><div class="loading">Loading tasks...</div></div>6061 <div class="modal-overlay" id="modal">62 <div class="modal">63 <h2 id="modal-title">New Task</h2>64 <form id="task-form">65 <div class="field">66 <label for="title">Title *</label>67 <input type="text" id="title" name="title" required>68 <div class="error" id="error-title"></div>69 </div>70 <div class="field">71 <label for="description">Description</label>72 <textarea id="description" name="description"></textarea>73 </div>74 <div class="field">75 <label for="status">Status *</label>76 <select id="status" name="status" required>77 <option value="open">Open</option>78 <option value="in_progress">In Progress</option>79 <option value="done">Done</option>80 </select>81 </div>82 <div class="field">83 <label for="priority">Priority</label>84 <select id="priority" name="priority">85 <option value="">None</option>86 <option value="low">Low</option>87 <option value="medium">Medium</option>88 <option value="high">High</option>89 <option value="critical">Critical</option>90 </select>91 </div>92 <div class="field">93 <label for="due_date">Due Date</label>94 <input type="date" id="due_date" name="due_date">95 </div>96 <div class="field">97 <label for="assignee_email">Assignee Email</label>98 <input type="email" id="assignee_email" name="assignee_email">99 </div>100 <div class="error" id="error-general"></div>101 <div class="actions">102 <button type="button" class="btn-cancel" id="btn-cancel">Cancel</button>103 <button type="submit" class="btn-submit">Save</button>104 </div>105 </form>106 </div>107 </div>108109 <script src="https://sdk.workapps.tech/v1.js"></script>110 <script>111 const sdk = new WorkAppsSDK();112 let canEdit = false;113 let editingId = null;114115 async function init() {116 const bootstrap = await sdk.getBootstrap();117 canEdit = ['editor', 'admin'].includes(bootstrap.role);118 if (canEdit) {119 document.getElementById('btn-create').style.display = 'block';120 }121 loadTasks();122 }123124 async function loadTasks() {125 const listEl = document.getElementById('task-list');126 const filterStatus = document.getElementById('filter-status').value;127128 try {129 const filter = filterStatus ? { status: { eq: filterStatus } } : undefined;130 const result = await sdk.listRecords('tasks', { filter, sort: 'due_date:asc', pageSize: 50 });131132 if (result.data.length === 0) {133 listEl.innerHTML = '<div class="empty">No tasks found.</div>';134 return;135 }136137 listEl.innerHTML = result.data.map(task => `138 <div class="task">139 ${canEdit ? `140 <div class="task-actions">141 <button onclick="openEdit('${task.id}')">Edit</button>142 <button class="delete" onclick="deleteTask('${task.id}')">Delete</button>143 </div>144 ` : ''}145 <h3>${escapeHtml(task.title)}</h3>146 ${task.description ? `<p style="margin-top:6px;color:#374151;">${escapeHtml(task.description)}</p>` : ''}147 <div class="meta">148 <span class="badge ${task.status}">${task.status.replace('_', ' ')}</span>149 ${task.priority ? `<span class="badge ${task.priority}">${task.priority}</span>` : ''}150 ${task.due_date ? `<span>Due: ${task.due_date}</span>` : ''}151 ${task.assignee_email ? `<span>${escapeHtml(task.assignee_email)}</span>` : ''}152 </div>153 </div>154 `).join('');155 } catch (error) {156 listEl.innerHTML = `<div class="error">Failed to load tasks: ${escapeHtml(error.message)}</div>`;157 }158 }159160 document.getElementById('btn-create').addEventListener('click', () => {161 editingId = null;162 document.getElementById('modal-title').textContent = 'New Task';163 document.getElementById('task-form').reset();164 document.getElementById('modal').classList.add('open');165 });166167 document.getElementById('btn-cancel').addEventListener('click', () => {168 document.getElementById('modal').classList.remove('open');169 });170171 async function openEdit(id) {172 const result = await sdk.getRecord('tasks', id);173 const task = result.data;174 editingId = id;175 document.getElementById('modal-title').textContent = 'Edit Task';176 document.getElementById('title').value = task.title || '';177 document.getElementById('description').value = task.description || '';178 document.getElementById('status').value = task.status || 'open';179 document.getElementById('priority').value = task.priority || '';180 document.getElementById('due_date').value = task.due_date || '';181 document.getElementById('assignee_email').value = task.assignee_email || '';182 document.getElementById('modal').classList.add('open');183 }184185 document.getElementById('task-form').addEventListener('submit', async (e) => {186 e.preventDefault();187 clearErrors();188189 const data = {190 title: document.getElementById('title').value,191 description: document.getElementById('description').value || null,192 status: document.getElementById('status').value,193 priority: document.getElementById('priority').value || null,194 due_date: document.getElementById('due_date').value || null,195 assignee_email: document.getElementById('assignee_email').value || null,196 };197198 try {199 if (editingId) {200 await sdk.updateRecord('tasks', editingId, data);201 } else {202 await sdk.createRecord('tasks', data);203 }204 document.getElementById('modal').classList.remove('open');205 loadTasks();206 } catch (error) {207 const { ErrorCode } = WorkAppsSDK;208 if (error.code === ErrorCode.ValidationFailed) {209 const fieldErrors = error.getFieldErrors();210 Object.entries(fieldErrors).forEach(([field, message]) => {211 const el = document.getElementById('error-' + field);212 if (el) { el.textContent = message; }213 });214 } else {215 document.getElementById('error-general').textContent = error.message;216 }217 }218 });219220 async function deleteTask(id) {221 // In production, use a custom confirmation dialog — never window.confirm222 try {223 await sdk.deleteRecord('tasks', id);224 loadTasks();225 } catch (error) {226 console.error('Delete failed:', error.message);227 }228 }229230 document.getElementById('filter-status').addEventListener('change', loadTasks);231232 function escapeHtml(str) {233 if (str == null) return '';234 const div = document.createElement('div');235 div.textContent = String(str);236 return div.innerHTML;237 }238239 function clearErrors() {240 document.querySelectorAll('.error').forEach(el => el.textContent = '');241 }242243 init();244 </script>245</body>246</html>
Key Patterns
Delete Confirmations
Always use a custom confirmation dialog before deleting — never window.confirm:
1async function deleteTask(id) {2 // Use a custom <dialog> or modal instead of window.confirm3 // window.confirm blocks the JS thread and can't be styled4 if (!await myConfirmDialog('Delete this task?')) return;56 await sdk.deleteRecord('tasks', id);7 loadTasks();8}