From 70a6bbcbb361a6840440bef6a534665041f14fa5 Mon Sep 17 00:00:00 2001 From: "ASBSECURITYBV\\bko" Date: Fri, 15 May 2026 21:13:37 +0200 Subject: [PATCH 1/5] add edit functionality --- src/time-entry.css | 44 ++++++++++-- src/time-entry.html | 112 +++++++++++++++++++++++++++++- src/time-report.css | 47 +++++++++++++ src/time-report.html | 154 ++++++++++++++++++++++++++++++++++++++++- vss-extension.dev.json | 2 +- 5 files changed, 347 insertions(+), 12 deletions(-) diff --git a/src/time-entry.css b/src/time-entry.css index d5582e1..3432375 100644 --- a/src/time-entry.css +++ b/src/time-entry.css @@ -7,7 +7,7 @@ body { padding: 10px; } .form-group { display: flex; flex-direction: column; gap: 4px; } -label { font-weight: 600; color: var(--text-primary); } +label { font-weight: 600; color: var(--text-primary) !important; } input, textarea { padding: 6px 8px; font-size: 14px; } @@ -43,12 +43,44 @@ input, textarea { padding: 6px 8px; font-size: 14px; } color: var(--text-primary); } -.delete-btn { - background: #d13438; - padding: 4px 8px; - font-size: 12px; +.icon-btn { + background: transparent !important; + border: none; + padding: 0; + border-radius: 3px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + width: 28px; + height: 28px; + cursor: pointer; +} +.icon-btn:hover { background: var(--bg-row-hover) !important; } +.edit-btn:hover { color: var(--accent) !important; } +.delete-btn:hover { color: #d13438 !important; } + +.edit-modal-backdrop { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; +} +.edit-modal-content { + background: var(--bg-card); + padding: 24px; + border-radius: 4px; + min-width: 300px; + max-width: 400px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + gap: 12px; } -.delete-btn:hover { background: #a4262c; } +.edit-modal-actions { display: flex; gap: 8px; } .message { padding: 8px; border-radius: 4px; margin-bottom: 12px; } .message.success { background: var(--success-bg); color: var(--success-text); } diff --git a/src/time-entry.html b/src/time-entry.html index b0ea699..e8ec4ca 100644 --- a/src/time-entry.html +++ b/src/time-entry.html @@ -55,6 +55,28 @@

Time Entries

+ + diff --git a/src/time-report.css b/src/time-report.css index a75b767..4337710 100644 --- a/src/time-report.css +++ b/src/time-report.css @@ -100,3 +100,50 @@ td { padding: 12px 16px; color: var(--text-primary); } color: var(--accent); font-weight: 600; } + +.icon-btn { + background: transparent !important; + border: none; + padding: 0; + border-radius: 3px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + width: 28px; + height: 28px; + cursor: pointer; +} +.icon-btn:hover { background: var(--bg-row-hover) !important; } +.edit-btn:hover { color: var(--accent); } +.delete-btn:hover { color: #d13438; } + +.actions-cell { white-space: nowrap; padding: 8px 12px !important; } + +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +.edit-modal-backdrop { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; +} +.edit-modal-content { + background: var(--bg-card); + padding: 24px; + border-radius: 4px; + min-width: 320px; + max-width: 440px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + gap: 12px; +} +.edit-modal-content h3 { margin: 0; } +.edit-modal-content label { font-weight: 600; color: var(--text-primary) !important; font-size: 13px; } +.edit-modal-content input, .edit-modal-content textarea { width: 100%; box-sizing: border-box; } +.edit-modal-actions { display: flex; gap: 8px; } diff --git a/src/time-report.html b/src/time-report.html index 2d46794..3968070 100644 --- a/src/time-report.html +++ b/src/time-report.html @@ -137,6 +137,13 @@

Work Items

let filteredEntries = []; let currentView = "entries"; var pendingFilters = null; + let currentUser = null; + var collectionUri = ""; + var currentProjectName = ""; + var EDIT_ICON = ''; + var DELETE_ICON = ''; + var editingEntryId = null; + var editingEntryOriginalDate = null; function parseHashString(hash) { if (!hash) return null; @@ -223,6 +230,12 @@

Work Items

VSS.getService(VSS.ServiceIds.ExtensionData).then(function(dataService) { console.log("[TimeReport] Got extension data service"); window.dataService = dataService; + try { + var ctx = VSS.getWebContext(); + currentUser = ctx.user; + collectionUri = ((ctx.collection || {}).uri || "").replace(/\/$/, ""); + currentProjectName = (ctx.project || {}).name || ""; + } catch(e) {} try { dataService.setValue('user-theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light', { scopeType: 'User' }); } catch(e) {} // Set default date range (last 30 days) @@ -546,7 +559,7 @@

Work Items

var thead = document.getElementById("tableHead"); var tbody = document.getElementById("tableBody"); - thead.innerHTML = "DateUserWork ItemParentEpicTagsHoursDescription"; + thead.innerHTML = "DateUserWork ItemParentEpicTagsHoursDescription"; var sorted = [...filteredEntries].sort((a, b) => new Date(b.date) - new Date(a.date)); @@ -561,15 +574,27 @@

Work Items

parentDisplay = escapeHtml(e.parentTitle) + " (" + escapeHtml(e.parentType) + ")"; } + var canModify = currentUser && e.userId === currentUser.id; + var actionBtns = canModify + ? '' + + '' + : ''; + + var wiUrl = getWorkItemUrl(e); + var wiIdLabel = wiUrl + ? '#' + e.workItemId + '' + : '#' + e.workItemId; + return "" + "" + e.date + "" + "" + escapeHtml(e.userName) + "" + - "" + escapeHtml(e.workItemTitle) + " (#" + e.workItemId + ")" + + "" + escapeHtml(e.workItemTitle) + " (" + wiIdLabel + ")" + "" + parentDisplay + "" + "" + (e.epicTitle ? escapeHtml(e.epicTitle) : "-") + "" + "" + tags + "" + "" + e.hours + "h" + "" + (e.description ? escapeHtml(e.description) : "-") + "" + + "" + actionBtns + "" + ""; }).join(""); } @@ -1089,6 +1114,109 @@

Work Items

link.click(); } + function getWorkItemUrl(entry) { + var base = collectionUri.replace(/\/$/, ""); + var proj = entry.teamProject || currentProjectName || ""; + if (!base || !proj) return null; + return base + "/" + encodeURIComponent(proj) + "/_workitems/edit/" + entry.workItemId; + } + + function deleteEntryById(entryId, entryDate) { + var storageKey = getStorageKeyForDate(entryDate); + return window.dataService.getValue(storageKey, { scopeType: "Default" }).then(function(data) { + if (!data || data.length === 0) { + throw new Error("No entries found for this month"); + } + var filtered = data.filter(function(e) { return e.id !== entryId; }); + return window.dataService.setValue(storageKey, filtered, { scopeType: "Default" }); + }); + } + + function deleteEntry(entryId) { + if (!confirm("Delete this time entry?")) return; + var entry = allEntries.find(function(e) { return e.id === entryId; }); + if (!entry) { alert("Entry not found"); return; } + if (!currentUser || entry.userId !== currentUser.id) { alert("You can only delete your own entries"); return; } + deleteEntryById(entryId, entry.date).then(function() { + loadAllEntries(); + }, function() { + alert("Failed to delete entry"); + }); + } + + function editEntry(entryId) { + var entry = allEntries.find(function(e) { return e.id === entryId; }); + if (!entry) { alert("Entry not found"); return; } + editingEntryId = entryId; + editingEntryOriginalDate = entry.date; + document.getElementById("editHours").value = entry.hours; + document.getElementById("editDate").value = entry.date; + document.getElementById("editDescription").value = entry.description || ""; + document.getElementById("editModal").style.display = "flex"; + } + + function closeEditModal() { + document.getElementById("editModal").style.display = "none"; + editingEntryId = null; + editingEntryOriginalDate = null; + } + + function saveEditEntry() { + var hours = parseFloat(document.getElementById("editHours").value); + var date = document.getElementById("editDate").value; + var description = document.getElementById("editDescription").value; + + if (!hours || hours <= 0 || hours > 24) { + alert("Please enter valid hours (0.25 - 24)"); + return; + } + if (!date) { + alert("Please select a date"); + return; + } + + document.getElementById("editSaveBtn").disabled = true; + + var originalKey = getStorageKeyForDate(editingEntryOriginalDate); + var newKey = getStorageKeyForDate(date); + + if (originalKey === newKey) { + window.dataService.getValue(originalKey, { scopeType: "Default" }).then(function(data) { + var updated = (data || []).map(function(e) { + if (e.id === editingEntryId) { + return Object.assign({}, e, { hours: hours, date: date, description: description }); + } + return e; + }); + return window.dataService.setValue(originalKey, updated, { scopeType: "Default" }); + }).then(function() { + closeEditModal(); + loadAllEntries(); + }, function() { + alert("Failed to update entry"); + document.getElementById("editSaveBtn").disabled = false; + }); + } else { + var entryToMove = null; + window.dataService.getValue(originalKey, { scopeType: "Default" }).then(function(data) { + var oldEntries = data || []; + entryToMove = oldEntries.find(function(e) { return e.id === editingEntryId; }); + return window.dataService.setValue(originalKey, oldEntries.filter(function(e) { return e.id !== editingEntryId; }), { scopeType: "Default" }); + }).then(function() { + return window.dataService.getValue(newKey, { scopeType: "Default" }); + }).then(function(data) { + var updatedEntry = Object.assign({}, entryToMove, { hours: hours, date: date, description: description }); + return window.dataService.setValue(newKey, (data || []).concat([updatedEntry]), { scopeType: "Default" }); + }).then(function() { + closeEditModal(); + loadAllEntries(); + }, function() { + alert("Failed to update entry"); + document.getElementById("editSaveBtn").disabled = false; + }); + } + } + function escapeHtml(text) { if (!text) return ""; var div = document.createElement("div"); @@ -1096,5 +1224,27 @@

Work Items

return div.innerHTML; } + + diff --git a/vss-extension.dev.json b/vss-extension.dev.json index 3a2ac9c..96ec48a 100644 --- a/vss-extension.dev.json +++ b/vss-extension.dev.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/vss-extension", "manifestVersion": 1, "id": "timetracker-extension-dev", - "version": "1.4.26", + "version": "1.4.28", "name": "Time Tracker (DEV)", "description": "[DEVELOPMENT] Simple time tracking for user stories with reporting by user, epic, tag and date range", "publisher": "miguelnicolas", From 8a81f770a91a871951534db3527ea2b54a2cf3b9 Mon Sep 17 00:00:00 2001 From: "ASBSECURITYBV\\bko" Date: Fri, 15 May 2026 21:14:06 +0200 Subject: [PATCH 2/5] version --- vss-extension.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vss-extension.json b/vss-extension.json index 29bc61c..e02569c 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/vss-extension", "manifestVersion": 1, "id": "timetracker-extension", - "version": "1.5.10", + "version": "1.5.11", "name": "Time Tracker", "description": "Simple time tracking for user stories with reporting by user, epic, tag and date range", "publisher": "miguelnicolas", From f9eeaca94fd908b602df1e5f26be3fbc17541e5d Mon Sep 17 00:00:00 2001 From: "ASBSECURITYBV\\bko" Date: Mon, 18 May 2026 14:38:27 +0200 Subject: [PATCH 3/5] resolve comments --- src/time-entry.html | 26 +++++++++++++++++--------- src/time-report.css | 4 ++-- src/time-report.html | 22 +++++++++++++--------- vss-extension.dev.json | 2 +- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/time-entry.html b/src/time-entry.html index e8ec4ca..d378148 100644 --- a/src/time-entry.html +++ b/src/time-entry.html @@ -575,6 +575,10 @@

Edit Time Entry

showMessage("Entry not found", "error"); return; } + if (!currentUser || entry.userId !== currentUser.id) { + showMessage("You can only delete your own entries", "error"); + return; + } return deleteEntryById(entryId, entry.date); }).then(function() { @@ -684,6 +688,7 @@

Edit Time Entry

getAllEntries().then(function(entries) { var entry = entries.find(function(e) { return e.id === entryId; }); if (!entry) { showMessage("Entry not found", "error"); return; } + if (!currentUser || entry.userId !== currentUser.id) { showMessage("You can only edit your own entries", "error"); return; } editingEntryId = entryId; editingEntryOriginalDate = entry.date; document.getElementById("editHours").value = entry.hours; @@ -736,16 +741,19 @@

Edit Time Entry

document.getElementById("editSaveBtn").disabled = false; }); } else { - var entryToMove = null; - window.dataService.getValue(originalKey, { scopeType: "Default" }).then(function(data) { - var oldEntries = data || []; - entryToMove = oldEntries.find(function(e) { return e.id === editingEntryId; }); - return window.dataService.setValue(originalKey, oldEntries.filter(function(e) { return e.id !== editingEntryId; }), { scopeType: "Default" }); - }).then(function() { - return window.dataService.getValue(newKey, { scopeType: "Default" }); - }).then(function(data) { + Promise.all([ + window.dataService.getValue(originalKey, { scopeType: "Default" }), + window.dataService.getValue(newKey, { scopeType: "Default" }) + ]).then(function(results) { + var oldEntries = results[0] || []; + var newEntries = results[1] || []; + var entryToMove = oldEntries.find(function(e) { return e.id === editingEntryId; }); + if (!entryToMove) { throw new Error("Entry not found in original month"); } var updatedEntry = Object.assign({}, entryToMove, { hours: hours, date: date, description: description }); - return window.dataService.setValue(newKey, (data || []).concat([updatedEntry]), { scopeType: "Default" }); + // Write to new month first; only remove from old month after that succeeds + return window.dataService.setValue(newKey, newEntries.concat([updatedEntry]), { scopeType: "Default" }).then(function() { + return window.dataService.setValue(originalKey, oldEntries.filter(function(e) { return e.id !== editingEntryId; }), { scopeType: "Default" }); + }); }).then(function() { showMessage("Entry updated", "success"); closeEditModal(); diff --git a/src/time-report.css b/src/time-report.css index 4337710..a53d7ee 100644 --- a/src/time-report.css +++ b/src/time-report.css @@ -120,8 +120,8 @@ td { padding: 12px 16px; color: var(--text-primary); } .actions-cell { white-space: nowrap; padding: 8px 12px !important; } -a { color: var(--accent); text-decoration: none; } -a:hover { text-decoration: underline; } +td a { color: var(--accent); text-decoration: none; } +td a:hover { text-decoration: underline; } .edit-modal-backdrop { position: fixed; diff --git a/src/time-report.html b/src/time-report.html index 3968070..f6e311e 100644 --- a/src/time-report.html +++ b/src/time-report.html @@ -1147,6 +1147,7 @@

Work Items

function editEntry(entryId) { var entry = allEntries.find(function(e) { return e.id === entryId; }); if (!entry) { alert("Entry not found"); return; } + if (!currentUser || entry.userId !== currentUser.id) { alert("You can only edit your own entries"); return; } editingEntryId = entryId; editingEntryOriginalDate = entry.date; document.getElementById("editHours").value = entry.hours; @@ -1197,16 +1198,19 @@

Work Items

document.getElementById("editSaveBtn").disabled = false; }); } else { - var entryToMove = null; - window.dataService.getValue(originalKey, { scopeType: "Default" }).then(function(data) { - var oldEntries = data || []; - entryToMove = oldEntries.find(function(e) { return e.id === editingEntryId; }); - return window.dataService.setValue(originalKey, oldEntries.filter(function(e) { return e.id !== editingEntryId; }), { scopeType: "Default" }); - }).then(function() { - return window.dataService.getValue(newKey, { scopeType: "Default" }); - }).then(function(data) { + Promise.all([ + window.dataService.getValue(originalKey, { scopeType: "Default" }), + window.dataService.getValue(newKey, { scopeType: "Default" }) + ]).then(function(results) { + var oldEntries = results[0] || []; + var newEntries = results[1] || []; + var entryToMove = oldEntries.find(function(e) { return e.id === editingEntryId; }); + if (!entryToMove) { throw new Error("Entry not found in original month"); } var updatedEntry = Object.assign({}, entryToMove, { hours: hours, date: date, description: description }); - return window.dataService.setValue(newKey, (data || []).concat([updatedEntry]), { scopeType: "Default" }); + // Write to new month first; only remove from old month after that succeeds + return window.dataService.setValue(newKey, newEntries.concat([updatedEntry]), { scopeType: "Default" }).then(function() { + return window.dataService.setValue(originalKey, oldEntries.filter(function(e) { return e.id !== editingEntryId; }), { scopeType: "Default" }); + }); }).then(function() { closeEditModal(); loadAllEntries(); diff --git a/vss-extension.dev.json b/vss-extension.dev.json index 96ec48a..b17918b 100644 --- a/vss-extension.dev.json +++ b/vss-extension.dev.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/vss-extension", "manifestVersion": 1, "id": "timetracker-extension-dev", - "version": "1.4.28", + "version": "1.4.29", "name": "Time Tracker (DEV)", "description": "[DEVELOPMENT] Simple time tracking for user stories with reporting by user, epic, tag and date range", "publisher": "miguelnicolas", From afd2708d36e8ad750faaab7af524d3fd465887a7 Mon Sep 17 00:00:00 2001 From: "ASBSECURITYBV\\bko" Date: Fri, 22 May 2026 14:04:22 +0200 Subject: [PATCH 4/5] Ui improvements --- src/my-time.css | 10 ++- src/my-time.html | 155 +++++++++++++++++++++++++++++---- src/notification-settings.html | 13 ++- src/theme.css | 54 ++++++++++++ src/time-core.js | 41 +++++++++ src/time-entry.css | 44 +--------- src/time-entry.html | 102 +++++----------------- src/time-report.css | 46 ---------- src/time-report.html | 75 ++-------------- vss-extension.dev.json | 2 +- 10 files changed, 287 insertions(+), 255 deletions(-) diff --git a/src/my-time.css b/src/my-time.css index e2c56eb..492d7b2 100644 --- a/src/my-time.css +++ b/src/my-time.css @@ -1,5 +1,6 @@ -html, body { height: 100vh; overflow: auto; } +html, body { height: 100vh; overflow: hidden; } body { padding: 20px; display: flex; flex-direction: column; } +.page-scroll { flex: 1; min-height: 0; overflow-y: auto; } .page-header { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; flex-shrink: 0; } .page-header h1 { margin: 0; font-weight: 600; } @@ -96,8 +97,11 @@ body { padding: 20px; display: flex; flex-direction: column; } .quicklog input[type="date"] { width: 140px; } .quicklog button { padding: 5px 12px; font-size: 13px; } -.delete-btn { background-color: var(--error-border) !important; padding: 4px 10px; font-size: 12px; } -.delete-btn:hover { opacity: 0.85; } +/* week navigation header */ +.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } +.section-header .section-title { margin: 0; } +.week-nav { display: flex; gap: 4px; } +.week-nav-btn { padding: 2px 10px; font-size: 16px; line-height: 1; } .empty { padding: 32px; text-align: center; color: var(--text-secondary); } .loading { padding: 40px; text-align: center; color: var(--text-secondary); } diff --git a/src/my-time.html b/src/my-time.html index 20350fe..2ce12a4 100644 --- a/src/my-time.html +++ b/src/my-time.html @@ -32,6 +32,7 @@

My Time

+
Loading…
-
This Week
+
+
This Week
+
+ + +
+
@@ -94,14 +101,22 @@

Untracked This Week

+ + + diff --git a/src/notification-settings.html b/src/notification-settings.html index 73efe21..28d4c9b 100644 --- a/src/notification-settings.html +++ b/src/notification-settings.html @@ -87,9 +87,16 @@

User Notification Configuration

function applyDevOpsTheme() { function applyIfExplicit(t) { if (!t) return false; - // A non-empty theme that isn't a dark variant (e.g. "Default") means light. - document.documentElement.classList.toggle('dark', String(t).toLowerCase().indexOf('dark') !== -1); - return true; + var theme = String(t).toLowerCase(); + if (theme.indexOf('dark') !== -1) { + document.documentElement.classList.add('dark'); + return true; + } + if (theme === 'default' || theme === 'light') { + document.documentElement.classList.remove('dark'); + return true; + } + return false; } var t = ''; try { if (typeof VSS.getPageContext === 'function') t = (VSS.getPageContext().globalization.theme || '').toLowerCase(); } catch(e) {} diff --git a/src/theme.css b/src/theme.css index f39d98d..74f78c2 100644 --- a/src/theme.css +++ b/src/theme.css @@ -241,3 +241,57 @@ tr:hover td { .status { font-size: 13px; } .status.success { color: var(--success-text); } .status.error { color: var(--error-text); } + +/* ================================================================ + Icon action buttons — shared across all pages + ================================================================ */ +.icon-btn { + background: transparent !important; + border: none; + padding: 0; + border-radius: 3px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--text-secondary) !important; + width: 28px; + height: 28px; + cursor: pointer; +} +.icon-btn:hover { background: var(--bg-row-hover) !important; } +.edit-btn:hover { color: var(--accent) !important; } +.delete-btn:hover { color: #d13438 !important; } +.actions-cell { white-space: nowrap; padding: 8px 12px !important; } + +/* ================================================================ + Edit entry modal — shared across all pages + ================================================================ */ +.edit-modal-backdrop { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; +} +.edit-modal-content { + background: var(--bg-card); + padding: 24px; + border-radius: 4px; + min-width: 300px; + max-width: 440px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + gap: 12px; +} +.edit-modal-content h3 { margin: 0; } +.edit-modal-content label { font-weight: 600; color: var(--text-primary) !important; font-size: 13px; } +.edit-modal-content input, +.edit-modal-content textarea { width: 100%; box-sizing: border-box; } +.edit-modal-actions { display: flex; gap: 8px; } + +/* Table links */ +td a { color: var(--accent); text-decoration: none; } +td a:hover { text-decoration: underline; } diff --git a/src/time-core.js b/src/time-core.js index 004ef88..7f5bdb5 100644 --- a/src/time-core.js +++ b/src/time-core.js @@ -151,6 +151,43 @@ }); } + var EDIT_ICON = ''; + var DELETE_ICON = ''; + + function escapeHtml(text) { + if (text == null) return ""; + var div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + function updateEntry(dataService, entryId, originalDate, updates) { + var originalKey = getStorageKeyForDate(originalDate); + var newKey = getStorageKeyForDate(updates.date); + if (originalKey === newKey) { + return dataService.getValue(originalKey, { scopeType: "Default" }).then(function(data) { + var updated = (data || []).map(function(e) { + if (e.id !== entryId) return e; + return Object.assign({}, e, updates); + }); + return dataService.setValue(originalKey, updated, { scopeType: "Default" }); + }); + } + return Promise.all([ + dataService.getValue(originalKey, { scopeType: "Default" }), + dataService.getValue(newKey, { scopeType: "Default" }) + ]).then(function(results) { + var oldEntries = results[0] || []; + var newEntries = results[1] || []; + var entryToMove = oldEntries.find(function(e) { return e.id === entryId; }); + if (!entryToMove) throw new Error("Entry not found in original month"); + var updatedEntry = Object.assign({}, entryToMove, updates); + return dataService.setValue(newKey, newEntries.concat([updatedEntry]), { scopeType: "Default" }).then(function() { + return dataService.setValue(originalKey, oldEntries.filter(function(e) { return e.id !== entryId; }), { scopeType: "Default" }); + }); + }); + } + // ---- Entry construction with parent/epic inheritance ------------- // Mirrors processWorkItem() in time-entry.html. Resolves with the // built entry; the caller is responsible for persisting it. @@ -267,6 +304,10 @@ loadEntriesForKeys: loadEntriesForKeys, saveEntry: saveEntry, deleteEntryById: deleteEntryById, + EDIT_ICON: EDIT_ICON, + DELETE_ICON: DELETE_ICON, + escapeHtml: escapeHtml, + updateEntry: updateEntry, buildEntryForWorkItem: buildEntryForWorkItem }; })(window); diff --git a/src/time-entry.css b/src/time-entry.css index 3432375..1cf057c 100644 --- a/src/time-entry.css +++ b/src/time-entry.css @@ -1,5 +1,6 @@ -html, body { height: 100%; overflow: auto; } -body { padding: 10px; } +html, body { height: 100vh; overflow: hidden; } +body { padding: 0; display: flex; flex-direction: column; } +#app { flex: 1; min-height: 0; overflow-y: auto; padding: 10px; } #app { min-width: fit-content; } @@ -43,45 +44,6 @@ input, textarea { padding: 6px 8px; font-size: 14px; } color: var(--text-primary); } -.icon-btn { - background: transparent !important; - border: none; - padding: 0; - border-radius: 3px; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--text-secondary); - width: 28px; - height: 28px; - cursor: pointer; -} -.icon-btn:hover { background: var(--bg-row-hover) !important; } -.edit-btn:hover { color: var(--accent) !important; } -.delete-btn:hover { color: #d13438 !important; } - -.edit-modal-backdrop { - position: fixed; - top: 0; left: 0; - width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.5); - z-index: 1000; - align-items: center; - justify-content: center; -} -.edit-modal-content { - background: var(--bg-card); - padding: 24px; - border-radius: 4px; - min-width: 300px; - max-width: 400px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - display: flex; - flex-direction: column; - gap: 12px; -} -.edit-modal-actions { display: flex; gap: 8px; } - .message { padding: 8px; border-radius: 4px; margin-bottom: 12px; } .message.success { background: var(--success-bg); color: var(--success-text); } .message.error { background: var(--error-bg); color: var(--error-text); } diff --git a/src/time-entry.html b/src/time-entry.html index af8efa6..332d2c9 100644 --- a/src/time-entry.html +++ b/src/time-entry.html @@ -5,6 +5,7 @@ Time Entry + @@ -81,7 +82,19 @@

Edit Time Entry

function applyDevOpsTheme() { function applyIfExplicit(t) { if (!t) return false; - // A non-empty theme that isn't a dark variant (e.g. "Default") means light. + // IMPORTANT — keep this simple toggle, do NOT copy the hub-page version here. + // + // This page runs as a sandboxed work-item-form contribution (cross-origin + // iframe). VSS context theme values are unreliable in this context, so the + // best-effort toggle below may be wrong. It does not matter: the async + // dataService.getValue('user-theme') call further down always overwrites it + // once it resolves. That async read is the real source of truth here. + // + // The hub-page version of applyIfExplicit returns false for unrecognised + // theme strings, letting execution fall through to the luminance-based + // parent-page background read. That fallback throws (cross-origin) in a + // sandboxed iframe, causing a flash to the wrong theme. Always returning + // true here prevents that fallback from running at all. document.documentElement.classList.toggle('dark', String(t).toLowerCase().indexOf('dark') !== -1); return true; } @@ -116,19 +129,13 @@

Edit Time Entry

let currentUser = null; const STORAGE_KEY_PREFIX = "timetracker_"; const LEGACY_STORAGE_KEY = "timetracker_entries"; - var EDIT_ICON = ''; - var DELETE_ICON = ''; + var TC = window.TimeCore; + var EDIT_ICON = TC.EDIT_ICON; + var DELETE_ICON = TC.DELETE_ICON; + var escapeHtml = TC.escapeHtml; var editingEntryId = null; var editingEntryOriginalDate = null; - // Helper function to get storage key for a given date - function getStorageKeyForDate(dateString) { - var date = new Date(dateString); - var year = date.getFullYear(); - var month = String(date.getMonth() + 1).padStart(2, '0'); - return STORAGE_KEY_PREFIX + year + "_" + month; - } - // Wait for VSS SDK to be available function initializeExtension() { console.log("[TimeTracker] Checking for VSS...", typeof VSS); @@ -236,7 +243,7 @@

Edit Time Entry

// Group entries by month var entriesByMonth = {}; legacyData.forEach(function(entry) { - var key = getStorageKeyForDate(entry.date); + var key = TC.getStorageKeyForDate(entry.date); if (!entriesByMonth[key]) { entriesByMonth[key] = []; } @@ -301,7 +308,7 @@

Edit Time Entry

// Save a new entry to the appropriate monthly partition function saveEntry(entry) { - var storageKey = getStorageKeyForDate(entry.date); + var storageKey = TC.getStorageKeyForDate(entry.date); console.log("[TimeTracker] Saving entry to partition:", storageKey, "Entry:", entry); return window.dataService.getValue(storageKey, { scopeType: "Default" }).then(function(data) { @@ -319,21 +326,6 @@

Edit Time Entry

}); } - // Delete an entry from its monthly partition - function deleteEntryById(entryId, entryDate) { - var storageKey = getStorageKeyForDate(entryDate); - - return window.dataService.getValue(storageKey, { scopeType: "Default" }).then(function(data) { - if (!data || data.length === 0) { - throw new Error("No entries found for this month — aborting delete to prevent data loss"); - } - var filtered = data.filter(function(e) { - return e.id !== entryId; - }); - return window.dataService.setValue(storageKey, filtered, { scopeType: "Default" }); - }); - } - // Possible field reference names for Client (varies by where field was created in Azure DevOps) var CLIENT_FIELD_REFS = ["Custom.Client", "Custom.Planning_Client", "Planning.Client"]; var PROJECT_FIELD_REFS = ["Custom.Project", "Custom.Planning_Project", "Planning.Project"]; @@ -580,19 +572,13 @@

Edit Time Entry

return; } - return deleteEntryById(entryId, entry.date); + return TC.deleteEntryById(window.dataService, entryId, entry.date); }).then(function() { showMessage("Entry deleted", "success"); loadTimeEntries(); }); } - function escapeHtml(text) { - var div = document.createElement("div"); - div.textContent = text; - return div.innerHTML; - } - // Setup field observer for state changes function setupFieldObserver() { VSS.require(["VSS/Service", "TFS/WorkItemTracking/Services"], function(VSS_Service, WIT_Services) { @@ -642,11 +628,8 @@

Edit Time Entry

// Set default date to today nudgeDate.valueAsDate = new Date(); - // Show the banner with animation banner.style.display = "block"; - - // Scroll to top to make it visible - window.scrollTo({ top: 0, behavior: "smooth" }); + document.getElementById("app").scrollTo({ top: 0, behavior: "smooth" }); } // Dismiss the nudge banner @@ -708,7 +691,6 @@

Edit Time Entry

var hours = parseFloat(document.getElementById("editHours").value); var date = document.getElementById("editDate").value; var description = document.getElementById("editDescription").value; - if (!hours || hours <= 0 || hours > 24) { showMessage("Please enter valid hours (0.25 - 24)", "error"); return; @@ -717,22 +699,9 @@

Edit Time Entry

showMessage("Please select a date", "error"); return; } - document.getElementById("editSaveBtn").disabled = true; - - var originalKey = getStorageKeyForDate(editingEntryOriginalDate); - var newKey = getStorageKeyForDate(date); - - if (originalKey === newKey) { - window.dataService.getValue(originalKey, { scopeType: "Default" }).then(function(data) { - var updated = (data || []).map(function(e) { - if (e.id === editingEntryId) { - return Object.assign({}, e, { hours: hours, date: date, description: description }); - } - return e; - }); - return window.dataService.setValue(originalKey, updated, { scopeType: "Default" }); - }).then(function() { + TC.updateEntry(window.dataService, editingEntryId, editingEntryOriginalDate, { hours: hours, date: date, description: description }) + .then(function() { showMessage("Entry updated", "success"); closeEditModal(); loadTimeEntries(); @@ -740,29 +709,6 @@

Edit Time Entry

showMessage("Failed to update entry", "error"); document.getElementById("editSaveBtn").disabled = false; }); - } else { - Promise.all([ - window.dataService.getValue(originalKey, { scopeType: "Default" }), - window.dataService.getValue(newKey, { scopeType: "Default" }) - ]).then(function(results) { - var oldEntries = results[0] || []; - var newEntries = results[1] || []; - var entryToMove = oldEntries.find(function(e) { return e.id === editingEntryId; }); - if (!entryToMove) { throw new Error("Entry not found in original month"); } - var updatedEntry = Object.assign({}, entryToMove, { hours: hours, date: date, description: description }); - // Write to new month first; only remove from old month after that succeeds - return window.dataService.setValue(newKey, newEntries.concat([updatedEntry]), { scopeType: "Default" }).then(function() { - return window.dataService.setValue(originalKey, oldEntries.filter(function(e) { return e.id !== editingEntryId; }), { scopeType: "Default" }); - }); - }).then(function() { - showMessage("Entry updated", "success"); - closeEditModal(); - loadTimeEntries(); - }, function() { - showMessage("Failed to update entry", "error"); - document.getElementById("editSaveBtn").disabled = false; - }); - } } diff --git a/src/time-report.css b/src/time-report.css index a53d7ee..ca817aa 100644 --- a/src/time-report.css +++ b/src/time-report.css @@ -101,49 +101,3 @@ td { padding: 12px 16px; color: var(--text-primary); } font-weight: 600; } -.icon-btn { - background: transparent !important; - border: none; - padding: 0; - border-radius: 3px; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--text-secondary); - width: 28px; - height: 28px; - cursor: pointer; -} -.icon-btn:hover { background: var(--bg-row-hover) !important; } -.edit-btn:hover { color: var(--accent); } -.delete-btn:hover { color: #d13438; } - -.actions-cell { white-space: nowrap; padding: 8px 12px !important; } - -td a { color: var(--accent); text-decoration: none; } -td a:hover { text-decoration: underline; } - -.edit-modal-backdrop { - position: fixed; - top: 0; left: 0; - width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.5); - z-index: 1000; - align-items: center; - justify-content: center; -} -.edit-modal-content { - background: var(--bg-card); - padding: 24px; - border-radius: 4px; - min-width: 320px; - max-width: 440px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - display: flex; - flex-direction: column; - gap: 12px; -} -.edit-modal-content h3 { margin: 0; } -.edit-modal-content label { font-weight: 600; color: var(--text-primary) !important; font-size: 13px; } -.edit-modal-content input, .edit-modal-content textarea { width: 100%; box-sizing: border-box; } -.edit-modal-actions { display: flex; gap: 8px; } diff --git a/src/time-report.html b/src/time-report.html index 4903ed6..28f80de 100644 --- a/src/time-report.html +++ b/src/time-report.html @@ -7,6 +7,7 @@ +

Time Reports

@@ -150,8 +151,10 @@

Work Items

let currentUser = null; var collectionUri = ""; var currentProjectName = ""; - var EDIT_ICON = ''; - var DELETE_ICON = ''; + var TC = window.TimeCore; + var EDIT_ICON = TC.EDIT_ICON; + var DELETE_ICON = TC.DELETE_ICON; + var escapeHtml = TC.escapeHtml; var editingEntryId = null; var editingEntryOriginalDate = null; @@ -191,14 +194,6 @@

Work Items

}); } - // Helper function to get storage key for a given date - function getStorageKeyForDate(dateString) { - var date = new Date(dateString); - var year = date.getFullYear(); - var month = String(date.getMonth() + 1).padStart(2, '0'); - return STORAGE_KEY_PREFIX + year + "_" + month; - } - // Get all months between two dates function getMonthsBetween(startDate, endDate) { var months = []; @@ -1131,23 +1126,12 @@

Work Items

return base + "/" + encodeURIComponent(proj) + "/_workitems/edit/" + entry.workItemId; } - function deleteEntryById(entryId, entryDate) { - var storageKey = getStorageKeyForDate(entryDate); - return window.dataService.getValue(storageKey, { scopeType: "Default" }).then(function(data) { - if (!data || data.length === 0) { - throw new Error("No entries found for this month"); - } - var filtered = data.filter(function(e) { return e.id !== entryId; }); - return window.dataService.setValue(storageKey, filtered, { scopeType: "Default" }); - }); - } - function deleteEntry(entryId) { if (!confirm("Delete this time entry?")) return; var entry = allEntries.find(function(e) { return e.id === entryId; }); if (!entry) { alert("Entry not found"); return; } if (!currentUser || entry.userId !== currentUser.id) { alert("You can only delete your own entries"); return; } - deleteEntryById(entryId, entry.date).then(function() { + TC.deleteEntryById(window.dataService, entryId, entry.date).then(function() { loadAllEntries(); }, function() { alert("Failed to delete entry"); @@ -1176,7 +1160,6 @@

Work Items

var hours = parseFloat(document.getElementById("editHours").value); var date = document.getElementById("editDate").value; var description = document.getElementById("editDescription").value; - if (!hours || hours <= 0 || hours > 24) { alert("Please enter valid hours (0.25 - 24)"); return; @@ -1185,57 +1168,15 @@

Work Items

alert("Please select a date"); return; } - document.getElementById("editSaveBtn").disabled = true; - - var originalKey = getStorageKeyForDate(editingEntryOriginalDate); - var newKey = getStorageKeyForDate(date); - - if (originalKey === newKey) { - window.dataService.getValue(originalKey, { scopeType: "Default" }).then(function(data) { - var updated = (data || []).map(function(e) { - if (e.id === editingEntryId) { - return Object.assign({}, e, { hours: hours, date: date, description: description }); - } - return e; - }); - return window.dataService.setValue(originalKey, updated, { scopeType: "Default" }); - }).then(function() { - closeEditModal(); - loadAllEntries(); - }, function() { - alert("Failed to update entry"); - document.getElementById("editSaveBtn").disabled = false; - }); - } else { - Promise.all([ - window.dataService.getValue(originalKey, { scopeType: "Default" }), - window.dataService.getValue(newKey, { scopeType: "Default" }) - ]).then(function(results) { - var oldEntries = results[0] || []; - var newEntries = results[1] || []; - var entryToMove = oldEntries.find(function(e) { return e.id === editingEntryId; }); - if (!entryToMove) { throw new Error("Entry not found in original month"); } - var updatedEntry = Object.assign({}, entryToMove, { hours: hours, date: date, description: description }); - // Write to new month first; only remove from old month after that succeeds - return window.dataService.setValue(newKey, newEntries.concat([updatedEntry]), { scopeType: "Default" }).then(function() { - return window.dataService.setValue(originalKey, oldEntries.filter(function(e) { return e.id !== editingEntryId; }), { scopeType: "Default" }); - }); - }).then(function() { + TC.updateEntry(window.dataService, editingEntryId, editingEntryOriginalDate, { hours: hours, date: date, description: description }) + .then(function() { closeEditModal(); loadAllEntries(); }, function() { alert("Failed to update entry"); document.getElementById("editSaveBtn").disabled = false; }); - } - } - - function escapeHtml(text) { - if (!text) return ""; - var div = document.createElement("div"); - div.textContent = text; - return div.innerHTML; } diff --git a/vss-extension.dev.json b/vss-extension.dev.json index 7c3aa93..b0de433 100644 --- a/vss-extension.dev.json +++ b/vss-extension.dev.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/vss-extension", "manifestVersion": 1, "id": "timetracker-extension-dev", - "version": "1.5.5", + "version": "1.5.8", "name": "Time Tracker (DEV)", "description": "[DEVELOPMENT] Simple time tracking for user stories with reporting by user, epic, tag and date range", "publisher": "miguelnicolas", From 14faaf131db8b171974b4e0a4c138b11499b0b92 Mon Sep 17 00:00:00 2001 From: "ASBSECURITYBV\\bko" Date: Fri, 22 May 2026 15:06:10 +0200 Subject: [PATCH 5/5] add entry --- src/my-time.html | 30 +++++ src/theme.css | 133 +++++++++++++++++++++ src/time-core.js | 256 ++++++++++++++++++++++++++++++++++++++++- src/time-report.html | 38 ++++++ vss-extension.dev.json | 2 +- vss-extension.json | 2 +- 6 files changed, 457 insertions(+), 4 deletions(-) diff --git a/src/my-time.html b/src/my-time.html index 2ce12a4..3b2bbca 100644 --- a/src/my-time.html +++ b/src/my-time.html @@ -30,6 +30,12 @@

My Time

+
+ +
@@ -675,6 +681,30 @@

Untracked This Week

}); } + function openAddEntry() { + var seen = {}, recent = []; + // Items from recent entries first (most recently logged date first), + // then active assigned items not yet in the list. + allMyEntries.slice().sort(function(a, b) { + return b.date > a.date ? -1 : b.date < a.date ? 1 : 0; + }).forEach(function(e) { + var k = String(e.workItemId); + if (!seen[k]) { seen[k] = true; recent.push({ id: e.workItemId, title: e.workItemTitle || '(untitled)', type: null }); } + }); + assignedItems.forEach(function(wi) { + var k = String(wi.id); + if (!seen[k]) { seen[k] = true; recent.push({ id: wi.id, title: wi.title, type: wi.type }); } + }); + TC.openAddEntryModal({ + dataService: window.dataService, + witClientGetter: getWitClient, + currentUser: currentUser, + projectName: window.__projectName, + recentItems: recent.slice(0, 20), + onSaved: function() { showMessage('Time logged!', 'success'); reloadAll(); } + }); + } + function showMessage(text, type) { var msg = document.getElementById("message"); msg.textContent = text; diff --git a/src/theme.css b/src/theme.css index 74f78c2..77bcaf0 100644 --- a/src/theme.css +++ b/src/theme.css @@ -295,3 +295,136 @@ tr:hover td { /* Table links */ td a { color: var(--accent); text-decoration: none; } td a:hover { text-decoration: underline; } + +/* ================================================================ + Add-entry button (filter bar, far right) + ================================================================ */ +.filters-add { margin-left: auto; display: flex; align-items: flex-end; } + +.add-entry-btn { + display: inline-flex !important; + align-items: center; + gap: 6px; + background: transparent !important; + color: var(--text-secondary) !important; + border: 1px solid var(--border-input) !important; + padding: 5px 12px !important; + border-radius: 3px; + font-size: 13px; + white-space: nowrap; + cursor: pointer; +} +.add-entry-btn:hover { + color: var(--accent) !important; + border-color: var(--accent) !important; + background: var(--bg-accent-light) !important; +} + +/* ================================================================ + Add-entry modal: work item picker + ================================================================ */ +.tc-add-modal { max-width: 520px; width: 100%; } + +.tc-add-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} +.tc-add-header h3 { margin: 0; } + +.tc-modal-close { + background: transparent !important; + border: none; + color: var(--text-secondary) !important; + font-size: 18px; + line-height: 1; + padding: 2px 6px; + cursor: pointer; + border-radius: 3px; +} +.tc-modal-close:hover { background: var(--bg-row-hover) !important; color: var(--text-primary) !important; } + +.tc-search-input { width: 100%; box-sizing: border-box; } + +.tc-search-status { + font-size: 12px; + color: var(--text-secondary); + min-height: 18px; + margin: 4px 0; +} + +.tc-item-list { + border: 1px solid var(--border); + border-radius: 2px; + max-height: 180px; + overflow-y: auto; +} + +.tc-item-group { + padding: 5px 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + color: var(--text-secondary); + background: var(--bg-header); + border-bottom: 1px solid var(--border); +} + +.tc-item-row { + display: flex !important; + align-items: center; + gap: 8px; + width: 100%; + text-align: left; + background: transparent !important; + color: var(--text-primary) !important; + border: none; + border-bottom: 1px solid var(--border); + border-radius: 0 !important; + padding: 8px 12px !important; + cursor: pointer; + font-size: 13px; +} +.tc-item-row:last-child { border-bottom: none; } +.tc-item-row:hover { background: var(--bg-row-hover) !important; } + +.tc-item-type { + display: inline-block; + padding: 2px 7px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; + background: var(--bg-accent-light); + color: var(--accent); + flex-shrink: 0; + white-space: nowrap; +} + +.tc-item-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.tc-item-empty { padding: 20px 12px; color: var(--text-secondary); font-size: 13px; text-align: center; } + +/* Selected work item chip — a clickable button that reopens the search */ +.tc-selected-chip { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + text-align: left; + background: var(--bg-header) !important; + color: var(--text-primary) !important; + border: 1px solid var(--border) !important; + border-radius: 4px; + padding: 8px 12px !important; + margin-bottom: 4px; + font-size: 13px; + cursor: pointer; +} +.tc-selected-chip:hover { + border-color: var(--accent) !important; + background: var(--bg-accent-light) !important; +} +/* Pencil icon: faintly visible at rest, full opacity on hover */ +.tc-chip-pencil { margin-left: auto; flex-shrink: 0; opacity: 0.25; } +.tc-selected-chip:hover .tc-chip-pencil { opacity: 0.8; } diff --git a/src/time-core.js b/src/time-core.js index 7f5bdb5..3f68e7f 100644 --- a/src/time-core.js +++ b/src/time-core.js @@ -1,4 +1,4 @@ -/* ================================================================ +/* ================================================================ time-core.js — shared storage + entry logic Loaded as a plain