From 369aebb35f158c6725e9236ba50a9505718908a2 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 25 Mar 2026 19:18:01 +1100 Subject: [PATCH 01/25] add debug --- src/debug.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/debug.js diff --git a/src/debug.js b/src/debug.js new file mode 100644 index 0000000..00648ef --- /dev/null +++ b/src/debug.js @@ -0,0 +1,15 @@ +export function log (...args) { + console.log(...args) +} + +export function warn (...args) { + console.warn(...args) +} + +export function error (...args) { + console.error(...args) +} + +export function trace (...args) { + console.trace(...args) +} From 5a81d8318766fb2b048d9e07767a1210413b78d7 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 25 Mar 2026 19:18:37 +1100 Subject: [PATCH 02/25] error handling --- src/localUtils.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/localUtils.js diff --git a/src/localUtils.js b/src/localUtils.js new file mode 100644 index 0000000..078fd69 --- /dev/null +++ b/src/localUtils.js @@ -0,0 +1,11 @@ +export function alertDialog (message, title = 'Information') { + return openModal({ + title, + message, + buttons: [{ label: 'OK', value: true, primary: true }] + }) +} + +export function complain (div, d, message) { + div.appendChild(UI.widgets.errorMessageBlock(d, message, 'pink')) +} From ca12d090fe9f993a683b0fa48103eb9e677429e5 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 25 Mar 2026 19:18:51 +1100 Subject: [PATCH 03/25] styles for details sec --- src/issuePane.js | 112 +++++++++++++++++++++++++++++---------- src/styles/issuePane.css | 12 +++++ 2 files changed, 96 insertions(+), 28 deletions(-) diff --git a/src/issuePane.js b/src/issuePane.js index 4ff24d1..55224d3 100644 --- a/src/issuePane.js +++ b/src/issuePane.js @@ -12,8 +12,10 @@ import { renderIssue, renderIssueCard, getState, exposeOverlay } from './issue' import { newTrackerButton } from './newTracker' import { newIssueForm } from './newIssue' import { csvButton } from './csvButton' +import { complain } from './localUtils' import { trackerSettingsFormText } from './ontology/trackerSettingsForm.ttl' import * as $rdf from 'rdflib' +import * as debug from './debug' import './styles/issue.css' import './styles/board.css' import './styles/newTracker.css' @@ -100,7 +102,8 @@ export default { try { await updateMany([], ins) } catch (err) { - return widgets.complain(context, 'Error writing tracker configuration: ' + err) + debug.error('Error writing tracker configuration: ' + err) + return complain(options.div, context.dom, 'Error creating the tracker configuration') } /* try { @@ -130,14 +133,30 @@ export default { context.paneDiv = paneDiv paneDiv.setAttribute('class', 'issuePane') - function complain (message) { - console.warn(message) - paneDiv.appendChild(widgets.errorMessageBlock(dom, message)) + const detailsSection = dom.createElement('section') + detailsSection.classList.add('detailSection') + detailsSection.setAttribute('role', 'region') + detailsSection.setAttribute('aria-label', 'Details section') + detailsSection.hidden = true + + const detailsSectionContent = dom.createElement('div') + detailsSectionContent.classList.add('detailsSectionContent') + detailsSectionContent.setAttribute('aria-live', 'polite') + detailsSection.appendChild(detailsSectionContent) + + function showDetailsSection () { + detailsSection.hidden = false + paneDiv.appendChild(detailsSection) // keep details at the bottom + } + + function complainInDetails (message) { + showDetailsSection() + complain(detailsSectionContent, dom, message) } function complainIfBad (ok, message) { if (!ok) { - complain(message) + complainInDetails(message) } } @@ -149,8 +168,14 @@ export default { async function fixSubClasses (kb, tracker) { // 20220228 async function checkOneSuperclass (klass) { const collection = kb.any(klass, ns.owl('disjointUnionOf'), null, doc) - if (!collection) throw new Error(`Classification ${klass} has no disjointUnionOf`) - if (!collection.elements) throw new Error(`Classification ${klass} has no array`) + if (!collection) { + debug.error(`Classification ${klass} has no disjointUnionOf`) + throw new Error(`Tracker configuration error: ${utils.label(klass)} is missing its allowed state list (owl:disjointUnionOf)`) + } + if (!collection.elements) { + debug.error(`Classification ${klass} has no array`) + throw new Error(`Tracker configuration error: ${utils.label(klass)} has no array`) + } const needed = new Set(collection.elements.map(x => x.uri)) const existing = new Set(kb.each(null, ns.rdfs('subClassOf'), klass, doc).map(x => x.uri)) @@ -166,13 +191,18 @@ export default { const cats = kb.each(tracker, ns.wf('issueCategory')).concat([states]) let damage = [] // to make totally functionaly need to deal with map over async. for (const klass of cats) { - damage = damage.concat(await checkOneSuperclass(klass)) + try { + damage = damage.concat(await checkOneSuperclass(klass)) + } catch (err) { + debug.error('Error checking subclasses of ' + utils.label(klass) + ': ' + err) + complainInDetails('Tracker settings need an update for "' + utils.label(klass) + '". Open Settings and make sure this status/category has a list of allowed values (for example Open, In Progress, Closed), then save and refresh.') + } + } if (damage.length) { const insertables = damage.filter(fix => fix.action === 'insert').map(fix => fix.st) const deletables = damage.filter(fix => fix.action === 'delete').map(fix => fix.st) - // alert(`Internal error: s${damage} subclasses inconsistences!`) - console.log('Damage:', damage) + debug.warn('Damage:', damage) if (confirm(`Fix ${damage} inconsistent subclasses in tracker config?`)) { await kb.updater.update(deletables, insertables) } @@ -190,7 +220,8 @@ export default { const stateArray = kb.any(klass, ns.owl('disjointUnionOf')) if (!stateArray) { - return complain(`Configuration error: state ${states} does not have substates`) + debug.error(`Configuration error: state ${states} does not have a disjointUnionOf`) + return complainInDetails(`Configuration error: state ${states} does not have substates`) } let columnValues = stateArray.elements if (doingStates && columnValues.length > 2 // and there are more than two @@ -212,7 +243,8 @@ export default { [$rdf.st(issue, ns.rdf('type'), currentState, stateStore)], [$rdf.st(issue, ns.rdf('type'), newState, stateStore)]) } catch (err) { - widgets.complain(context, 'Unable to change issue state: ' + err) + debug.error('Unable to change issue state: ' + err) + complainInDetails('Unable to change issue state') } boardDiv.refresh() // reorganize board to match the new reality } @@ -433,18 +465,20 @@ export default { // eslint-disable-next-line no-unused-vars const _xhrs = await context.session.store.fetcher.load(tracker.doc()) } catch (err) { - const msg = 'Failed to load tracker config ' + tracker.doc() + ': ' + err - return complain(msg) + debug.error(`Failed to load tracker config: ${tracker.doc()}: ${err}`) + return complainInDetails('Failed to load tracker config') } const stateStore = kb.any(tracker, ns.wf('stateStore')) if (!stateStore) { - return complain('Tracker has no state store: ' + tracker) + debug.error('Tracker has no state store: ' + tracker) + return complainInDetails('Tracker has no state store: ' + tracker) } try { await context.session.store.fetcher.load(subject) } catch (err) { - return complain('Failed to load issue state ' + stateStore + ': ' + err) + debug.error(`Failed to load issue state: ${stateStore}: ${err}`) + return complainInDetails('Failed to load issue state') } paneDiv.appendChild(renderIssue(subject, context)) updater.addDownstreamChangeListener(stateStore, function () { @@ -463,13 +497,22 @@ export default { try { await fixSubClasses(kb, tracker) } catch (err) { - console.log('@@@ Error fixing subclasses in config: ' + err) + debug.error('Error fixing subclasses in config: ' + err) + complainInDetails('Error fixing subclasses in config') } const states = kb.any(subject, ns.wf('issueClass')) - if (!states) throw new Error('This tracker has no issueClass') + if (!states) { + debug.error('This tracker has no issueClass') + complainInDetails('Tracker settings are incomplete: missing Issue Class. Open Settings, choose an Issue Class, then refresh.') + return + } const stateStore = kb.any(subject, ns.wf('stateStore')) - if (!stateStore) throw new Error('This tracker has no stateStore') + if (!stateStore) { + debug.error('This tracker has no stateStore') + complainInDetails('Tracker settings are incomplete: missing State Store. Open Settings, save the tracker state settings, then refresh.') + return + } // const me = await authn.currentUser() @@ -517,13 +560,15 @@ export default { if (tableDiv.refresh) { // Refresh function } else { - console.log('No table refresh function?!') + debug.warn('No refresh function on the tableDiv?!') + complainInDetails('There is no way to refresh the table view. Please refresh the whole page to see updates to the issues.') } paneDiv.appendChild(newTrackerButton(subject, context)) updater.addDownstreamChangeListener(stateStore, tableDiv.refresh) // Live update }) .catch(function (err) { - return console.log('Cannot load state store: ' + err) + debug.error('Cannot load state store: ' + err) + complainInDetails('Cannot load state store') }) // end of Tracker instance } // render tracker @@ -556,16 +601,27 @@ export default { t['http://www.w3.org/2005/01/wf/flow#Task'] || kb.holds(subject, ns.wf('tracker')) ) { - renderSingleIssue().then(() => console.log('Single issue rendered')) + renderSingleIssue() + .then(() => debug.log('Single issue rendered')) + .catch(err => { + debug.error('Single issue render failed: ' + err) + complainInDetails('Could not load this issue view. Please refresh, and if this continues, check tracker settings.') + }) } else if (t['http://www.w3.org/2005/01/wf/flow#Tracker']) { // Render a Tracker instance - renderTracker().then(() => console.log('Tracker rendered')) + renderTracker() + .then(() => debug.log('Tracker rendered')) + .catch(err => { + debug.error('Tracker render failed: ' + err) + complainInDetails('Could not load this tracker. Open Settings to verify Issue Class, then refresh.') + }) } else { - console.log( + debug.error( 'Error: Issue pane: No evidence that ' + subject + ' is either a bug or a tracker.' ) + complainInDetails('This item is not recognized as an issue or tracker.') } let loginOutButton @@ -575,7 +631,6 @@ export default { authn.checkUser().then(webId => { if (webId) { - console.log('Web ID set already: ' + webId) context.me = webId // @@ enable things return @@ -584,7 +639,6 @@ export default { loginOutButton = login.loginStatusBox(dom, webIdUri => { if (webIdUri) { context.me = kb.sym(webIdUri) - console.log('Web ID set from login button: ' + webIdUri) paneDiv.removeChild(loginOutButton) // enable things } else { @@ -593,12 +647,14 @@ export default { }) loginOutButton.classList.add('trackerIssuePaneLoginButton') - paneDiv.appendChild(loginOutButton) + paneDiv.insertBefore(loginOutButton, detailsSection) if (!context.statusArea) { - context.statusArea = paneDiv.appendChild(dom.createElement('div')) + context.statusArea = paneDiv.insertBefore(dom.createElement('div'), detailsSection) } }) + paneDiv.appendChild(detailsSection) + return paneDiv } } diff --git a/src/styles/issuePane.css b/src/styles/issuePane.css index 2babab9..3bee327 100644 --- a/src/styles/issuePane.css +++ b/src/styles/issuePane.css @@ -68,3 +68,15 @@ using the GPT-5.3-Codex model in Github. The prompts used are below: .trackerIssuePaneLoginButton { margin: var(--spacing-sm) var(--spacing-md); } + +.detailSection { + margin: var(--spacing-sm, 0.75em); + padding: var(--spacing-sm, 0.75em); + border-top: 1px solid #d0d0d0; +} + +.detailsSectionContent { + display: flex; + flex-direction: column; + gap: var(--spacing-sm, 0.75em); +} From 00be28fe5ba07027a057e2d39d0532749dda1ccf Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 25 Mar 2026 19:30:59 +1100 Subject: [PATCH 04/25] attach details sooner --- src/issuePane.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/issuePane.js b/src/issuePane.js index 55224d3..e70df2b 100644 --- a/src/issuePane.js +++ b/src/issuePane.js @@ -138,6 +138,7 @@ export default { detailsSection.setAttribute('role', 'region') detailsSection.setAttribute('aria-label', 'Details section') detailsSection.hidden = true + paneDiv.appendChild(detailsSection) const detailsSectionContent = dom.createElement('div') detailsSectionContent.classList.add('detailsSectionContent') @@ -653,8 +654,6 @@ export default { } }) - paneDiv.appendChild(detailsSection) - return paneDiv } } From aa5c206a00a2cc2d9a19a3857d0e317973c5b4c7 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 25 Mar 2026 19:31:19 +1100 Subject: [PATCH 05/25] Update src/issuePane.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/issuePane.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/issuePane.js b/src/issuePane.js index 55224d3..a4aa55d 100644 --- a/src/issuePane.js +++ b/src/issuePane.js @@ -193,11 +193,10 @@ export default { for (const klass of cats) { try { damage = damage.concat(await checkOneSuperclass(klass)) - } catch (err) { + } catch (err) { debug.error('Error checking subclasses of ' + utils.label(klass) + ': ' + err) complainInDetails('Tracker settings need an update for "' + utils.label(klass) + '". Open Settings and make sure this status/category has a list of allowed values (for example Open, In Progress, Closed), then save and refresh.') } - } if (damage.length) { const insertables = damage.filter(fix => fix.action === 'insert').map(fix => fix.st) From 4fc5a7f81ae95aa2cb67a72017c5d8254dee4d94 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 25 Mar 2026 19:34:31 +1100 Subject: [PATCH 06/25] add openmodal function --- src/localUtils.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/localUtils.js b/src/localUtils.js index 078fd69..cd4fbe0 100644 --- a/src/localUtils.js +++ b/src/localUtils.js @@ -1,3 +1,41 @@ +function openModal ({ title, message, buttons }) { + const overlay = ensureModalOverlay() + previousFocus = document.activeElement + hideSiblings(true) + overlay.classList.remove('hidden') + + overlay.querySelector('#modal-title').textContent = title || '' + const descEl = overlay.querySelector('#modal-desc') + if (typeof message === 'string') { + descEl.textContent = message + } else { + // allow passing nodes + descEl.innerHTML = '' + descEl.appendChild(message) + } + + const btnContainer = overlay.querySelector('#modal-buttons') + btnContainer.innerHTML = '' + + return new Promise(resolve => { + buttons.forEach(btn => { + const b = dom.createElement('button') + b.setAttribute('type', 'button') + b.textContent = btn.label + if (btn.primary) b.classList.add('btn-primary') + if (btn.cancel) b.setAttribute('data-cancel', 'true') + b.addEventListener('click', () => { + closeModal(btn.value) + resolve(btn.value) + }) + btnContainer.appendChild(b) + }) + // focus first button + const first = btnContainer.querySelector('button') + if (first) first.focus() + }) +} + export function alertDialog (message, title = 'Information') { return openModal({ title, From cb5ea3d7bad701887856f7a0c1a85c3ff5437744 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 25 Mar 2026 19:43:12 +1100 Subject: [PATCH 07/25] add missing functions --- src/localUtils.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/localUtils.js b/src/localUtils.js index cd4fbe0..370e85f 100644 --- a/src/localUtils.js +++ b/src/localUtils.js @@ -1,5 +1,8 @@ -function openModal ({ title, message, buttons }) { +import { widgets } from "solid-ui" + +function openModal ({ title, message, buttons, dom }) { const overlay = ensureModalOverlay() + const modalDom = dom || overlay.ownerDocument previousFocus = document.activeElement hideSiblings(true) overlay.classList.remove('hidden') @@ -19,7 +22,7 @@ function openModal ({ title, message, buttons }) { return new Promise(resolve => { buttons.forEach(btn => { - const b = dom.createElement('button') + const b = modalDom.createElement('button') b.setAttribute('type', 'button') b.textContent = btn.label if (btn.primary) b.classList.add('btn-primary') @@ -36,14 +39,23 @@ function openModal ({ title, message, buttons }) { }) } -export function alertDialog (message, title = 'Information') { +function closeModal (result) { + if (modalOverlay) { + modalOverlay.classList.add('hidden') + hideSiblings(false) + if (previousFocus && previousFocus.focus) previousFocus.focus() + } +} + +export function alertDialog (message, title = 'Information', dom = null) { return openModal({ title, message, - buttons: [{ label: 'OK', value: true, primary: true }] + buttons: [{ label: 'OK', value: true, primary: true }], + dom }) } export function complain (div, d, message) { - div.appendChild(UI.widgets.errorMessageBlock(d, message, 'pink')) + div.appendChild(widgets.errorMessageBlock(d, message, 'pink')) } From 1a6642b9e47335986069a4701b221b3fb2056415 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 25 Mar 2026 20:26:31 +1100 Subject: [PATCH 08/25] csvButton error handling --- src/csvButton.js | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/csvButton.js b/src/csvButton.js index e545f2c..8241aaa 100644 --- a/src/csvButton.js +++ b/src/csvButton.js @@ -6,6 +6,8 @@ import { icons, ns, utils, widgets } from 'solid-ui' import { store } from 'solid-logic' +import { alertDialog } from './localUtils' +import debug from './debug' export function quoteString (value) { // https://www.rfc-editor.org/rfc/rfc4180 @@ -14,9 +16,11 @@ export function quoteString (value) { return stripped } // If contains comma then put in quotes and double up internal quotes const quoted = '"' + stripped.replaceAll('"', '""') + '"' - console.log('Quoted: >>>' + quoted + '<<<') const check = quoted.slice(1, -1).replaceAll('""', '') - if (check.includes('"')) throw new Error('CSV inconsistecy') + if (check.includes('"')) { + debug.error(`quoteString failed to quote properly, value: ${value}, quoted: ${quoted}, check: ${check}`) + throw new Error('CSV inconsistecy') + } return quoted } @@ -29,7 +33,6 @@ export function csvText (store, tracker) { } else if (column.category) { const types = store.each(task, ns.rdf('type')) for (const t of types) { - // console.log('@@ checking subclass type: ', t, ' category: ', column.category ) if (store.holds(t, ns.rdfs('subClassOf'), column.category)) { thing = t } @@ -37,7 +40,8 @@ export function csvText (store, tracker) { if (!thing) return '?' + utils.label(column.category) // Missing cat OK // if (!thing) throw new Error('wot no class of category ', column.category) } else { - throw new Error('wot no pred or cat', column) + debug.error('column has no predicate or category', column) + throw new Error('Column has no predicate or category.') } return utils.label(thing) } @@ -49,7 +53,6 @@ export function csvText (store, tracker) { } const stateStore = store.any(tracker, ns.wf('stateStore')) const tasks = store.each(null, ns.wf('tracker'), tracker, stateStore) - console.log(' CSV: Tasks:', tasks.length) const columns = [ @@ -60,49 +63,37 @@ export function csvText (store, tracker) { */ ] const states = store.any(tracker, ns.wf('issueClass')) // Main states are subclasses of this class - console.log(' CSV: States - main superclass:', states) const stateColumn = { label: 'State', category: states } // better than 'task' - console.log(' CSV: found column from state', stateColumn) columns.push(stateColumn) const categories = store.each(tracker, ns.wf('issueCategory')) - console.log(' CSV: Categories : ', categories) - console.log(' CSV: Categories : length: ', categories.length) - console.log(' CSV: Categories : first: ', categories[0]) const classifications = categories for (const c of classifications) { const column = { label: utils.label(c), category: c } - console.log(' CSV: found column from classifications', column) columns.push(column) // Classes are different } // const propertyList = ns.wf('propertyList') const form = store.any(tracker, ns.wf('extrasEntryForm'), null, null) - console.log(' CSV: Form : ', form) if (form) { const parts = store.any(form, ns.ui('parts'), null, form.doc()) - console.log(' CSV: parts : ', parts) const fields = parts.elements - console.log(' CSV: fields : ', fields) for (const field of fields) { const prop = store.any(field, ns.ui('property')) if (prop) { const lab = utils.label(prop) const column = { label: lab, predicate: prop } - console.log(' CSV: found column from form', column) columns.push(column) } } } // Put description on the end as it can be long columns.push({ label: 'Description', predicate: ns.wf('description') }) - console.log('Columns: ', columns.length) const header = columns.map(col => col.label).join(',') + '\n' - console.log('CSV: Header= ', header) // Order tasks?? By Creation date? By Status? const body = tasks.map(taskLine).join('') return header + body @@ -114,14 +105,20 @@ export function csvButton (dom, tracker) { const button = widgets.button(dom, icons.iconBase + 'noun_Document_998605.svg', 'Copy as CSV', async _event => { const div = button.parentNode.parentNode - console.log('button gparent div', div) div.addEventListener('copy', event => { // alert ('Copy caught'); - const csv = csvText(store, tracker) + let csv + try { + csv = csvText(store, tracker) + } catch (err) { + alertDialog('Could not generate CSV. Please check tracker data and try again.', 'CSV export error', dom) + event.preventDefault() + return + } + event.preventDefault() event.clipboardData.setData('text/plain', csv) event.clipboardData.setData('text/csv', csv) - alert('Copy data: ' + csv) - event.preventDefault() + alertDialog('CSV data copied to clipboard.', 'CSV export', dom) }) }) From baba6e0003cdcc713244cd6341d28e1b72f3383e Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 25 Mar 2026 20:36:50 +1100 Subject: [PATCH 09/25] board errors --- src/board.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/board.js b/src/board.js index fb66180..2c8e027 100644 --- a/src/board.js +++ b/src/board.js @@ -21,6 +21,7 @@ export function board (dom, columnValues, renderItem, options) { const table = board.appendChild(dom.createElement('table')) table.classList.add('trackerBoardTable') + // build table header and columns const headerRow = table.appendChild(dom.createElement('tr')) headerRow.classList.add('trackerBoardHeader') const mainRow = table.appendChild(dom.createElement('tr')) @@ -37,7 +38,6 @@ export function board (dom, columnValues, renderItem, options) { function droppedURIHandler (uris) { uris.forEach(function (u) { - console.log('Dropped on column: ' + u) const item = store.sym(u) options.columnDropHandler(item, x) }) From 3624234ac51f2c70e5fe6d527cec66e2f9d22236 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Wed, 25 Mar 2026 20:40:59 +1100 Subject: [PATCH 10/25] new Issue --- src/newIssue.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/newIssue.js b/src/newIssue.js index 59f1021..be466c8 100644 --- a/src/newIssue.js +++ b/src/newIssue.js @@ -2,6 +2,7 @@ // import { ns, utils } from 'solid-ui' import * as $rdf from 'rdflib' +import { alertDialog } from './localUtils' import './styles/newIssue.css' export function newIssueForm (dom, kb, tracker, superIssue, showNewIssue, onCancel) { @@ -78,7 +79,10 @@ export function newIssueForm (dom, kb, tracker, superIssue, showNewIssue, onCanc const sendComplete = function (uri, success, body) { if (!success) { - console.log('Error: can\'t save new issue:' + body) + titlefield.classList.remove('pendingedit') + titlefield.disabled = false + titlefield.focus() + alertDialog('Could not save the new issue. Please try again.', 'Save issue failed', dom) } else { form.parentNode.removeChild(form) showNewIssue(issue) From ee3e49ade7369b7135a63914cca86e8144def123 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 11:24:37 +1100 Subject: [PATCH 11/25] newTracker error handling --- src/newTracker.js | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/newTracker.js b/src/newTracker.js index e452236..41a3d90 100644 --- a/src/newTracker.js +++ b/src/newTracker.js @@ -2,6 +2,8 @@ import * as UI from 'solid-ui' import * as $rdf from 'rdflib' import { store } from 'solid-logic' import './styles/newTracker.css' +import { alertDialog } from './localUtils' +import debug from './debug' const ns = UI.ns const updater = store.updater @@ -34,16 +36,13 @@ export function newTrackerButton (thisTracker, context) { } const appPathSegment = 'issuetracker.w3.org' // how to allocate this string and connect to - // console.log("Ready to make new instance at "+ws) const sp = UI.ns.space const kb = context.session.store if (!base) { base = kb.any(ws, sp('uriPrefix')).value if (base.slice(-1) !== '/') { - $rdf.log.error( - appPathSegment + ': No / at end of uriPrefix ' + base - ) + debug.error(`${appPathSegment}: No / at end of uriPrefix ${base}`) base = base + '/' } base += appPathSegment + '/' + timestring() + '/' // unique id @@ -84,8 +83,6 @@ export function newTrackerButton (thisTracker, context) { kb.add(newTracker, UI.ns.space('inspiration'), thisTracker, there) - // $rdf.log.debug("\n Ready to put " + kb.statementsMatching(undefined, undefined, undefined, there)); //@@ - updater.put( there, kb.statementsMatching(undefined, undefined, undefined, there), @@ -98,24 +95,15 @@ export function newTrackerButton (thisTracker, context) { message ) { if (ok) { - console.info( - 'Ok The tracker created OK at: ' + - newTracker.uri + - '\nMake a note of it, bookmark it. ' - ) + debug.log(`Ok The tracker created OK at: ${newTracker.uri}\nMake a note of it, bookmark it.`) } else { - console.log( - 'FAILED to set up new store at: ' + - newStore.uri + - ' : ' + - message - ) + debug.error(`Failed to set up new store at: ${newStore.uri} : ${message}`) + alertDialog(`Failed to set up new store at: ${newStore.uri}`) } }) } else { - console.log( - 'FAILED to save new tracker at: ' + there.uri + ' : ' + message - ) + debug.error(`Failed to save new tracker at: ${there.uri} : ${message}`) + alertDialog(`Failed to save new tracker at: ${there.uri}`) } } ) From 38b0525ef51d31784b76ba8298ee29d5ea067b17 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 11:36:07 +1100 Subject: [PATCH 12/25] missing localUtils functions and css --- src/localUtils.js | 69 +++++++++++++++++++++++++++++++++++++-- src/styles/localUtils.css | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 src/styles/localUtils.css diff --git a/src/localUtils.js b/src/localUtils.js index 370e85f..e946118 100644 --- a/src/localUtils.js +++ b/src/localUtils.js @@ -1,7 +1,72 @@ -import { widgets } from "solid-ui" +import { widgets } from 'solid-ui' +import '../styles/localUtils.css' + +let modalOverlay = null +let previousFocus = null + +function ensureModalOverlay (dom) { + // if we previously created an overlay but it was removed from the document + // (tests clear body), rebuild it. Checking presence ensures our reference + // doesn't point at a detached element. + if (modalOverlay && document.body.contains(modalOverlay)) return modalOverlay + // otherwise drop stale reference and create a new element + modalOverlay = null + // overlay container + modalOverlay = dom.createElement('div') + modalOverlay.id = 'contacts-modal' + modalOverlay.className = 'focus-trap hidden' + modalOverlay.setAttribute('role', 'presentation') + + modalOverlay.innerHTML = ` + + ` + + document.body.appendChild(modalOverlay) + + // keyboard handling (esc/tab) + modalOverlay.addEventListener('keydown', e => { + if (e.key === 'Escape') { + e.stopPropagation() + // simulate cancel if available + const cancelBtn = modalOverlay.querySelector('button[data-cancel]') + if (cancelBtn) cancelBtn.click() + else closeModal(false) + } else if (e.key === 'Tab') { + // simple focus trap: cycle through focusable elements inside overlay + const focusable = Array.from(modalOverlay.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter(el => !el.hasAttribute('disabled')) + if (focusable.length === 0) return + const idx = focusable.indexOf(document.activeElement) + if (e.shiftKey) { + if (idx === 0) { + focusable[focusable.length - 1].focus() + e.preventDefault() + } + } else { + if (idx === focusable.length - 1) { + focusable[0].focus() + e.preventDefault() + } + } + } + }) + + return modalOverlay +} + +function hideSiblings (hide) { + const siblings = Array.from(document.body.children).filter(c => c !== modalOverlay) + siblings.forEach(el => { + if (hide) el.setAttribute('aria-hidden', 'true') + else el.removeAttribute('aria-hidden') + }) +} function openModal ({ title, message, buttons, dom }) { - const overlay = ensureModalOverlay() + const overlay = ensureModalOverlay(dom) const modalDom = dom || overlay.ownerDocument previousFocus = document.activeElement hideSiblings(true) diff --git a/src/styles/localUtils.css b/src/styles/localUtils.css new file mode 100644 index 0000000..54c1473 --- /dev/null +++ b/src/styles/localUtils.css @@ -0,0 +1,67 @@ +/* CSS for the accessible modal dialogs created by localUtils.js */ + +/* backdrop / focus trap container */ +.focus-trap { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 9999; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; +} + +.focus-trap.hidden { + display: none; +} + +/* inner dialog box */ +.focus-trap .modal { + background: var(--color-background); + padding: var(--spacing-lg); + border-radius: var(--border-radius-base); + max-width: 90%; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); +} + +/* button container: center buttons horizontally (uses id in markup) */ +#contacts-modal #modal-buttons { + margin-top: var(--spacing-md); + display: flex; + justify-content: center; + gap: var(--spacing-sm); +} + +/* buttons themselves use the shared btn-primary rules */ +#contacts-modal .modal button { + min-height: var(--min-touch-target); + padding: var(--spacing-sm) var(--spacing-md); + border: 1px solid var(--color-primary); + border-radius: var(--border-radius-base); + font-weight: 600; + cursor: pointer; + transition: all var(--animation-duration) ease; +} + +#contacts-modal .modal button.btn-primary { + background: var(--color-primary); + color: var(--color-background); +} + +#contacts-modal .modal button.btn-primary:hover { + background: color-mix(in srgb, var(--color-primary) 90%, black); + box-shadow: 0 2px 4px rgba(124, 77, 255, 0.2); +} + +#contacts-modal .modal button.btn-primary:active { + box-shadow: 0 1px 2px rgba(124, 77, 255, 0.2); +} + +#contacts-modal .modal button:disabled { + opacity: var(--opacity-disabled, 0.6); + cursor: not-allowed; + transform: none; +} From 49a3e486e0ad4e3d08870ebf1e6b338b9257957a Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 11:36:37 +1100 Subject: [PATCH 13/25] fix new tracker --- src/newTracker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/newTracker.js b/src/newTracker.js index 41a3d90..068799a 100644 --- a/src/newTracker.js +++ b/src/newTracker.js @@ -1,5 +1,4 @@ import * as UI from 'solid-ui' -import * as $rdf from 'rdflib' import { store } from 'solid-logic' import './styles/newTracker.css' import { alertDialog } from './localUtils' From 9f285324e47cb7001e7d435fb9a33e57f03cdf8b Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 11:36:53 +1100 Subject: [PATCH 14/25] fix issue pane --- src/issuePane.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/issuePane.js b/src/issuePane.js index e70df2b..158e72f 100644 --- a/src/issuePane.js +++ b/src/issuePane.js @@ -194,11 +194,10 @@ export default { for (const klass of cats) { try { damage = damage.concat(await checkOneSuperclass(klass)) - } catch (err) { + } catch (err) { debug.error('Error checking subclasses of ' + utils.label(klass) + ': ' + err) complainInDetails('Tracker settings need an update for "' + utils.label(klass) + '". Open Settings and make sure this status/category has a list of allowed values (for example Open, In Progress, Closed), then save and refresh.') } - } if (damage.length) { const insertables = damage.filter(fix => fix.action === 'insert').map(fix => fix.st) From 1dcb6a6ceed7e9af5423c83501d42e4b2091db2f Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 12:23:53 +1100 Subject: [PATCH 15/25] issue error handling --- src/issue.js | 61 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/src/issue.js b/src/issue.js index 810b7f4..4f54f00 100644 --- a/src/issue.js +++ b/src/issue.js @@ -4,6 +4,8 @@ import { icons, messageArea, ns, utils, widgets } from 'solid-ui' import { authn, store } from 'solid-logic' import { newIssueForm } from './newIssue' import * as $rdf from 'rdflib' +import debug from './debug' +import { alertDialog, complain } from './localUtils' import './styles/issue.css' import './styles/utilites.css' @@ -15,11 +17,6 @@ export const TASK_ICON = icons.iconBase + 'noun_17020_gray-tick.svg' export const OPEN_TASK_ICON = icons.iconBase + 'noun_17020_sans-tick.svg' export const CLOSED_TASK_ICON = icons.iconBase + 'noun_17020.svg' -function complain (message, context) { - console.warn(message) - context.paneDiv.appendChild(widgets.errorMessageBlock(context.dom, message)) -} - export function isOpen (issue) { const types = kb.findTypeURIs(issue) return !!types[ns.wf('Open').uri] @@ -83,12 +80,12 @@ export function renderIssueCard (issue, context) { try { await kb.updater.update(kb.connectedStatements(issue)) } catch (err) { - complain(`Unable to delete issue: ${err}`, context) + debug.error('Unable to delete issue: ' + err) + alertDialog('Unable to delete issue', 'Delete failed', dom) + return } - console.log('User deleted issue ' + issue) card.parentNode.removeChild(card) // refresh doesn't work yet because it is not passed though tabs so short cut widgets.refreshTree(context.paneDiv) // Should delete the card if nec when tabs pass it though - // complain('DELETED OK', context) }) buttonsCell.appendChild(deleteButton) } @@ -123,6 +120,8 @@ function renderSpacer (dom, backgroundColor) { } export function renderIssue (issue, context) { + const dom = context.dom + // Don't bother changing the last modified dates of things: save time function setModifiedDate (subj, kb, doc) { if (SET_MODIFIED_DATES) { @@ -150,17 +149,14 @@ export function renderIssue (issue, context) { // http://www.w3schools.com/jsref/jsref_obj_date.asp } - function complain (message) { - console.warn(message) - issueDiv.appendChild(widgets.errorMessageBlock(dom, message)) + function complainInIssue (message) { + complain(issueDiv, dom, message) } function complainIfBad (ok, body) { if (!ok) { - complain( - 'Sorry, failed to save your change:\n' + body, - 'background-color: pink;', context - ) + debug.error('Failed to save change:\n' + body) + complainInIssue('Failed to save change') } } function getOption (tracker, option) { @@ -176,8 +172,6 @@ export function renderIssue (issue, context) { /// ////////////// Body of renderIssue - const dom = context.dom - const tracker = kb.the(issue, ns.wf('tracker'), null, issue.doc()) if (!tracker) throw new Error('No tracker') @@ -198,7 +192,15 @@ export function renderIssue (issue, context) { widgets.makeDraggable(iconButton, issue) // Drag me wherever you need to do stuff with this issue const states = kb.any(tracker, ns.wf('issueClass')) - if (!states) { throw new Error('This tracker ' + tracker + ' has no issueClass') } + if (!states) { + debug.error('Tracker ' + utils.label(tracker) + ' has no issueClass') + alertDialog('Tracker ' + utils.label(tracker) + ' has no issueClass. Please open Settings and make sure there is a state class with allowed values (for example Open, In Progress, Closed), then save and refresh.', 'Tracker configuration error', dom) + const p = dom.createElement('p') + p.textContent = 'Sorry, failed to save your change.' + issueDiv.appendChild(p) + return issueDiv + } + const select = widgets.makeSelectForCategory( dom, kb, @@ -210,7 +212,8 @@ export function renderIssue (issue, context) { setModifiedDate(store, kb, store) widgets.refreshTree(issueDiv) } else { - console.log('Failed to change state:\n' + body) + debug.warn('Failed to change state:\n' + body) + alertDialog('Failed to change state', 'State Update Failed', dom) } } ) @@ -230,7 +233,8 @@ export function renderIssue (issue, context) { setModifiedDate(store, kb, store) widgets.refreshTree(issueDiv) } else { - console.log('Failed to change category:\n' + body) + debug.warn('Failed to change category:\n' + body) + alertDialog('Failed to change category', 'Category Update Failed', dom) } } ) @@ -289,13 +293,14 @@ export function renderIssue (issue, context) { const assignments = kb.statementsMatching(issue, ns.wf('assignee')) if (assignments.length > 1) { - say('Weird, was assigned to more than one person. Fixing ..') + alertDialog('Weird, was assigned to more than one person. Fixing ..', 'Assignment Error', dom) const deletions = assignments.slice(1) kb.updater.update(deletions, [], function (uri, ok, body) { if (ok) { - say('Now fixed.') + alertDialog('Now fixed.', 'Assignment Fixed', dom) } else { - complain('Fixed failed: ' + body, context) + debug.error(`Failed to fix multiple assignees:\n${body}`) + alertDialog('Failed to fix multiple assignees', 'Assignment Fix Failed', dom) } }) } @@ -348,7 +353,10 @@ export function renderIssue (issue, context) { store, function (ok, body) { if (ok) setModifiedDate(store, kb, store) - else console.log('Failed to change assignee:\n' + body) + else { + debug.error('Failed to change assignee:\n' + body) + alertDialog('Failed to change assignee', 'Assignee Update Failed', dom) + } } ) ) @@ -478,10 +486,11 @@ export function renderIssue (issue, context) { try { await kb.updater.update(kb.connectedStatements(issue)) } catch (err) { - complain(`Unable to delete issue: ${err}`, context) + debug.error('Unable to delete issue: ' + err) + alertDialog('Unable to delete issue', 'Issue Deletion Failed', dom) } // @@ refreshTree - complain('DELETED OK', context) + alertDialog('Your issue has been deleted', 'Issue Deleted', dom) // This code was generated by Generative AI (GPT-5.3-Codex in GitHub Copilot) based on the following prompt: // Could you move the color changes after // @@refreshTree into css? issueDiv.classList.add('trackerIssueDeleted') From 6e39af7b7bbfd72e9f8c762bb05dfcb481b20311 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 12:41:46 +1100 Subject: [PATCH 16/25] fixed debug error --- src/csvButton.js | 2 +- src/issue.js | 2 +- src/issuePane.js | 20 ++++++++++++++++++++ src/localUtils.js | 2 +- src/newTracker.js | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/csvButton.js b/src/csvButton.js index 8241aaa..99d8e40 100644 --- a/src/csvButton.js +++ b/src/csvButton.js @@ -7,7 +7,7 @@ import { icons, ns, utils, widgets } from 'solid-ui' import { store } from 'solid-logic' import { alertDialog } from './localUtils' -import debug from './debug' +import * as debug from './debug' export function quoteString (value) { // https://www.rfc-editor.org/rfc/rfc4180 diff --git a/src/issue.js b/src/issue.js index 4f54f00..e9cc1e0 100644 --- a/src/issue.js +++ b/src/issue.js @@ -4,7 +4,7 @@ import { icons, messageArea, ns, utils, widgets } from 'solid-ui' import { authn, store } from 'solid-logic' import { newIssueForm } from './newIssue' import * as $rdf from 'rdflib' -import debug from './debug' +import * as debug from './debug' import { alertDialog, complain } from './localUtils' import './styles/issue.css' import './styles/utilites.css' diff --git a/src/issuePane.js b/src/issuePane.js index 158e72f..736bc7e 100644 --- a/src/issuePane.js +++ b/src/issuePane.js @@ -155,6 +155,26 @@ export default { complain(detailsSectionContent, dom, message) } + function shouldInjectDetailsTestErrors () { + const view = dom.defaultView + if (!view || !view.location) return false + try { + const params = new view.URLSearchParams(view.location.search || '') + return params.get('testDetailsSectionErrors') === '1' + } catch (_e) { + return false + } + } + + function injectDetailsTestErrorsIfEnabled () { + if (!shouldInjectDetailsTestErrors()) return + complainInDetails('Test error: unable to load optional tracker metadata.') + complainInDetails('Test warning: one issue is missing a category value.') + complainInDetails('Test info: this is a sample detailsSection message for visual QA.') + } + + injectDetailsTestErrorsIfEnabled() + function complainIfBad (ok, message) { if (!ok) { complainInDetails(message) diff --git a/src/localUtils.js b/src/localUtils.js index e946118..2a048ea 100644 --- a/src/localUtils.js +++ b/src/localUtils.js @@ -1,5 +1,5 @@ import { widgets } from 'solid-ui' -import '../styles/localUtils.css' +import './styles/localUtils.css' let modalOverlay = null let previousFocus = null diff --git a/src/newTracker.js b/src/newTracker.js index 068799a..59a0c35 100644 --- a/src/newTracker.js +++ b/src/newTracker.js @@ -2,7 +2,7 @@ import * as UI from 'solid-ui' import { store } from 'solid-logic' import './styles/newTracker.css' import { alertDialog } from './localUtils' -import debug from './debug' +import * as debug from './debug' const ns = UI.ns const updater = store.updater From 24725a2c5eb5221afc2b4deed51f6e1a73f12f68 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 12:53:42 +1100 Subject: [PATCH 17/25] place the detailsSection below the table --- src/issuePane.js | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/issuePane.js b/src/issuePane.js index 736bc7e..e559d01 100644 --- a/src/issuePane.js +++ b/src/issuePane.js @@ -145,9 +145,18 @@ export default { detailsSectionContent.setAttribute('aria-live', 'polite') detailsSection.appendChild(detailsSectionContent) + function placeDetailsSection () { + const newTrackerControl = paneDiv.querySelector('.trackerNewTrackerButton') + if (newTrackerControl && newTrackerControl.parentNode === paneDiv) { + paneDiv.insertBefore(detailsSection, newTrackerControl) + } else { + paneDiv.appendChild(detailsSection) + } + } + function showDetailsSection () { detailsSection.hidden = false - paneDiv.appendChild(detailsSection) // keep details at the bottom + placeDetailsSection() } function complainInDetails (message) { @@ -155,26 +164,6 @@ export default { complain(detailsSectionContent, dom, message) } - function shouldInjectDetailsTestErrors () { - const view = dom.defaultView - if (!view || !view.location) return false - try { - const params = new view.URLSearchParams(view.location.search || '') - return params.get('testDetailsSectionErrors') === '1' - } catch (_e) { - return false - } - } - - function injectDetailsTestErrorsIfEnabled () { - if (!shouldInjectDetailsTestErrors()) return - complainInDetails('Test error: unable to load optional tracker metadata.') - complainInDetails('Test warning: one issue is missing a category value.') - complainInDetails('Test info: this is a sample detailsSection message for visual QA.') - } - - injectDetailsTestErrorsIfEnabled() - function complainIfBad (ok, message) { if (!ok) { complainInDetails(message) From 2fe099bc186e804e8df815588abaeb6dabcf12c0 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 12:54:15 +1100 Subject: [PATCH 18/25] configuration for debug --- src/debug.js | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/debug.js b/src/debug.js index 00648ef..cea6bf3 100644 --- a/src/debug.js +++ b/src/debug.js @@ -1,15 +1,48 @@ +const LEVELS = { + trace: 10, + log: 20, + warn: 30, + error: 40, + silent: 99 +} + +let currentLevel = LEVELS.silent + +function defaultWriter (level, ...args) { + const c = globalThis && globalThis['console'] + if (!c) return + if (level === 'warn' && typeof c.warn === 'function') return c.warn(...args) + if (level === 'error' && typeof c.error === 'function') return c.error(...args) + if (typeof c.log === 'function') return c.log(...args) +} + +let sink = defaultWriter + +currentLevel = LEVELS.warn + +export function configureDebug ({ level = 'warn', writer } = {}) { + currentLevel = LEVELS[level] ?? LEVELS.warn + sink = writer === undefined ? defaultWriter : writer +} + +function emit (level, args) { + if (!sink) return + if ((LEVELS[level] ?? LEVELS.silent) < currentLevel) return + sink(level, ...args) +} + export function log (...args) { - console.log(...args) + emit('log', args) } export function warn (...args) { - console.warn(...args) + emit('warn', args) } export function error (...args) { - console.error(...args) + emit('error', args) } export function trace (...args) { - console.trace(...args) + emit('trace', args) } From 0086bc3933f690a4f5e0e3761c3980fbcdc62b43 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 13:09:08 +1100 Subject: [PATCH 19/25] generative ai --- src/issuePane.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/issuePane.js b/src/issuePane.js index e559d01..169961a 100644 --- a/src/issuePane.js +++ b/src/issuePane.js @@ -145,6 +145,7 @@ export default { detailsSectionContent.setAttribute('aria-live', 'polite') detailsSection.appendChild(detailsSectionContent) + /* Generative AI - Model: GPT-5.3-Codex see ReadMe for details */ function placeDetailsSection () { const newTrackerControl = paneDiv.querySelector('.trackerNewTrackerButton') if (newTrackerControl && newTrackerControl.parentNode === paneDiv) { From c213869fa6ef44e2bf4d8055d7bcd0474a8f5b64 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 13:13:01 +1100 Subject: [PATCH 20/25] generative ai --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ceee280..a072617 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,4 @@ Example: * Some code was generated by the GPT-5.3-Codex model in GitHub Copilot based on the following prompt: * can you style my table and make it look nice. * can you handle the visibility in exposeOverlay with css? +* move details section to be before the newTrackerButton From b4d421798e343172228659f6bccbb6767988da74 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 13:14:10 +1100 Subject: [PATCH 21/25] comment local utils --- src/localUtils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/localUtils.js b/src/localUtils.js index 2a048ea..0df0e1f 100644 --- a/src/localUtils.js +++ b/src/localUtils.js @@ -1,6 +1,7 @@ import { widgets } from 'solid-ui' import './styles/localUtils.css' +/* Copied from contacts-pane, made minor adjustments */ let modalOverlay = null let previousFocus = null From 9e6ccce23a365a5578697a4842bcfdd16bd972a4 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 13:16:19 +1100 Subject: [PATCH 22/25] generative ai messenging --- README.md | 1 + src/debug.js | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index a072617..a162a99 100644 --- a/README.md +++ b/README.md @@ -37,3 +37,4 @@ Example: * can you style my table and make it look nice. * can you handle the visibility in exposeOverlay with css? * move details section to be before the newTrackerButton +* Configure debug so that it can be turned on and off diff --git a/src/debug.js b/src/debug.js index cea6bf3..0639458 100644 --- a/src/debug.js +++ b/src/debug.js @@ -1,3 +1,4 @@ +/* Generative AI - Model: GPT-5.3-Codex used for configuration logic */ const LEVELS = { trace: 10, log: 20, From 58a491677e384c0d15f597a9c8d04a9b277d35a5 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 13:37:21 +1100 Subject: [PATCH 23/25] Update src/csvButton.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/csvButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/csvButton.js b/src/csvButton.js index 99d8e40..c3018e1 100644 --- a/src/csvButton.js +++ b/src/csvButton.js @@ -19,7 +19,7 @@ export function quoteString (value) { const check = quoted.slice(1, -1).replaceAll('""', '') if (check.includes('"')) { debug.error(`quoteString failed to quote properly, value: ${value}, quoted: ${quoted}, check: ${check}`) - throw new Error('CSV inconsistecy') + throw new Error('CSV inconsistency') } return quoted } From 8a34839abde802260bea390b7f94cdbe32604190 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 13:39:38 +1100 Subject: [PATCH 24/25] fixed PR review comments --- src/csvButton.js | 47 ++++++++++++++++++++++++++++++---------------- src/issue.js | 48 +++++++++++++++++++++++++++++++---------------- src/issuePane.js | 3 ++- src/localUtils.js | 23 ++++++++++++----------- src/newIssue.js | 6 +++++- src/newTracker.js | 12 ++++++++++-- 6 files changed, 92 insertions(+), 47 deletions(-) diff --git a/src/csvButton.js b/src/csvButton.js index 99d8e40..6ea2367 100644 --- a/src/csvButton.js +++ b/src/csvButton.js @@ -101,25 +101,40 @@ export function csvText (store, tracker) { export function csvButton (dom, tracker) { const wrapper = dom.createElement('div') + let pendingCsvCopy = false + + wrapper.addEventListener('copy', event => { + if (!pendingCsvCopy) return + pendingCsvCopy = false + + let csv + try { + csv = csvText(store, tracker) + } catch (err) { + alertDialog( + `Could not generate CSV. Please check tracker data and try again.\n\nDetails: ${err?.message || String(err)}`, + 'CSV export error', + dom + ) + event.preventDefault() + return + } + + event.preventDefault() + event.clipboardData.setData('text/plain', csv) + event.clipboardData.setData('text/csv', csv) + alertDialog('CSV data copied to clipboard.', 'CSV export', dom) + }) + // Add a button const button = widgets.button(dom, icons.iconBase + 'noun_Document_998605.svg', 'Copy as CSV', async _event => { - const div = button.parentNode.parentNode - div.addEventListener('copy', event => { - // alert ('Copy caught'); - let csv - try { - csv = csvText(store, tracker) - } catch (err) { - alertDialog('Could not generate CSV. Please check tracker data and try again.', 'CSV export error', dom) - event.preventDefault() - return - } - event.preventDefault() - event.clipboardData.setData('text/plain', csv) - event.clipboardData.setData('text/csv', csv) - alertDialog('CSV data copied to clipboard.', 'CSV export', dom) - }) + pendingCsvCopy = true + const copied = dom.execCommand('copy') + if (!copied) { + pendingCsvCopy = false + alertDialog('Could not copy CSV to clipboard. Please try again.', 'CSV export error', dom) + } }) wrapper.appendChild(button) diff --git a/src/issue.js b/src/issue.js index e9cc1e0..5cac364 100644 --- a/src/issue.js +++ b/src/issue.js @@ -81,7 +81,11 @@ export function renderIssueCard (issue, context) { await kb.updater.update(kb.connectedStatements(issue)) } catch (err) { debug.error('Unable to delete issue: ' + err) - alertDialog('Unable to delete issue', 'Delete failed', dom) + alertDialog( + `Unable to delete issue.\n\nDetails: ${err?.message || String(err)}`, + 'Delete failed', + dom + ) return } card.parentNode.removeChild(card) // refresh doesn't work yet because it is not passed though tabs so short cut @@ -134,15 +138,6 @@ export function renderIssue (issue, context) { kb.updater.update(deletions, insertions, function (_uri, _ok, _body) {}) } } - /* no longer pass in style it is not used */ - function say (message) { - const pre = dom.createElement('pre') - pre.classList.add('trackerIssueMessage') - issueDiv.appendChild(pre) - pre.appendChild(dom.createTextNode(message)) - return pre - } - function timestring () { const now = new Date() return '' + now.getTime() @@ -192,7 +187,7 @@ export function renderIssue (issue, context) { widgets.makeDraggable(iconButton, issue) // Drag me wherever you need to do stuff with this issue const states = kb.any(tracker, ns.wf('issueClass')) - if (!states) { + if (!states) { debug.error('Tracker ' + utils.label(tracker) + ' has no issueClass') alertDialog('Tracker ' + utils.label(tracker) + ' has no issueClass. Please open Settings and make sure there is a state class with allowed values (for example Open, In Progress, Closed), then save and refresh.', 'Tracker configuration error', dom) const p = dom.createElement('p') @@ -213,7 +208,11 @@ export function renderIssue (issue, context) { widgets.refreshTree(issueDiv) } else { debug.warn('Failed to change state:\n' + body) - alertDialog('Failed to change state', 'State Update Failed', dom) + alertDialog( + `Failed to change state.\n\nDetails: ${body || 'No additional error details provided.'}`, + 'State update failed', + dom + ) } } ) @@ -234,7 +233,11 @@ export function renderIssue (issue, context) { widgets.refreshTree(issueDiv) } else { debug.warn('Failed to change category:\n' + body) - alertDialog('Failed to change category', 'Category Update Failed', dom) + alertDialog( + `Failed to change category.\n\nDetails: ${body || 'No additional error details provided.'}`, + 'Category update failed', + dom + ) } } ) @@ -300,7 +303,11 @@ export function renderIssue (issue, context) { alertDialog('Now fixed.', 'Assignment Fixed', dom) } else { debug.error(`Failed to fix multiple assignees:\n${body}`) - alertDialog('Failed to fix multiple assignees', 'Assignment Fix Failed', dom) + alertDialog( + `Failed to fix multiple assignees.\n\nDetails: ${body || 'No additional error details provided.'}`, + 'Assignment fix failed', + dom + ) } }) } @@ -355,7 +362,11 @@ export function renderIssue (issue, context) { if (ok) setModifiedDate(store, kb, store) else { debug.error('Failed to change assignee:\n' + body) - alertDialog('Failed to change assignee', 'Assignee Update Failed', dom) + alertDialog( + `Failed to change assignee.\n\nDetails: ${body || 'No additional error details provided.'}`, + 'Assignee update failed', + dom + ) } } ) @@ -487,7 +498,12 @@ export function renderIssue (issue, context) { await kb.updater.update(kb.connectedStatements(issue)) } catch (err) { debug.error('Unable to delete issue: ' + err) - alertDialog('Unable to delete issue', 'Issue Deletion Failed', dom) + alertDialog( + `Unable to delete issue.\n\nDetails: ${err?.message || String(err)}`, + 'Issue deletion failed', + dom + ) + return } // @@ refreshTree alertDialog('Your issue has been deleted', 'Issue Deleted', dom) diff --git a/src/issuePane.js b/src/issuePane.js index 169961a..51482e3 100644 --- a/src/issuePane.js +++ b/src/issuePane.js @@ -213,7 +213,8 @@ export default { const insertables = damage.filter(fix => fix.action === 'insert').map(fix => fix.st) const deletables = damage.filter(fix => fix.action === 'delete').map(fix => fix.st) debug.warn('Damage:', damage) - if (confirm(`Fix ${damage} inconsistent subclasses in tracker config?`)) { + const fixSummary = `${damage.length} total (${insertables.length} add, ${deletables.length} remove)` + if (confirm(`Apply subclass fixes in tracker config? ${fixSummary}.`)) { await kb.updater.update(deletables, insertables) } } diff --git a/src/localUtils.js b/src/localUtils.js index 0df0e1f..9bdf6ab 100644 --- a/src/localUtils.js +++ b/src/localUtils.js @@ -9,7 +9,7 @@ function ensureModalOverlay (dom) { // if we previously created an overlay but it was removed from the document // (tests clear body), rebuild it. Checking presence ensures our reference // doesn't point at a detached element. - if (modalOverlay && document.body.contains(modalOverlay)) return modalOverlay + if (modalOverlay && dom.body.contains(modalOverlay)) return modalOverlay // otherwise drop stale reference and create a new element modalOverlay = null // overlay container @@ -26,7 +26,7 @@ function ensureModalOverlay (dom) { ` - document.body.appendChild(modalOverlay) + dom.body.appendChild(modalOverlay) // keyboard handling (esc/tab) modalOverlay.addEventListener('keydown', e => { @@ -40,7 +40,7 @@ function ensureModalOverlay (dom) { // simple focus trap: cycle through focusable elements inside overlay const focusable = Array.from(modalOverlay.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter(el => !el.hasAttribute('disabled')) if (focusable.length === 0) return - const idx = focusable.indexOf(document.activeElement) + const idx = focusable.indexOf(dom.activeElement) if (e.shiftKey) { if (idx === 0) { focusable[focusable.length - 1].focus() @@ -58,8 +58,8 @@ function ensureModalOverlay (dom) { return modalOverlay } -function hideSiblings (hide) { - const siblings = Array.from(document.body.children).filter(c => c !== modalOverlay) +function hideSiblings (hide, dom) { + const siblings = Array.from(dom.body.children).filter(c => c !== modalOverlay) siblings.forEach(el => { if (hide) el.setAttribute('aria-hidden', 'true') else el.removeAttribute('aria-hidden') @@ -68,9 +68,9 @@ function hideSiblings (hide) { function openModal ({ title, message, buttons, dom }) { const overlay = ensureModalOverlay(dom) - const modalDom = dom || overlay.ownerDocument - previousFocus = document.activeElement - hideSiblings(true) + + previousFocus = dom.activeElement + hideSiblings(true, dom) overlay.classList.remove('hidden') overlay.querySelector('#modal-title').textContent = title || '' @@ -88,7 +88,7 @@ function openModal ({ title, message, buttons, dom }) { return new Promise(resolve => { buttons.forEach(btn => { - const b = modalDom.createElement('button') + const b = dom.createElement('button') b.setAttribute('type', 'button') b.textContent = btn.label if (btn.primary) b.classList.add('btn-primary') @@ -107,13 +107,14 @@ function openModal ({ title, message, buttons, dom }) { function closeModal (result) { if (modalOverlay) { + const modalDom = modalOverlay.ownerDocument modalOverlay.classList.add('hidden') - hideSiblings(false) + hideSiblings(false, modalDom) if (previousFocus && previousFocus.focus) previousFocus.focus() } } -export function alertDialog (message, title = 'Information', dom = null) { +export function alertDialog (message, title = 'Information', dom) { return openModal({ title, message, diff --git a/src/newIssue.js b/src/newIssue.js index be466c8..99b04bf 100644 --- a/src/newIssue.js +++ b/src/newIssue.js @@ -82,7 +82,11 @@ export function newIssueForm (dom, kb, tracker, superIssue, showNewIssue, onCanc titlefield.classList.remove('pendingedit') titlefield.disabled = false titlefield.focus() - alertDialog('Could not save the new issue. Please try again.', 'Save issue failed', dom) + alertDialog( + `Could not save the new issue. Please try again.\n\nDetails: ${body || 'No additional error details provided.'}`, + 'Save issue failed', + dom + ) } else { form.parentNode.removeChild(form) showNewIssue(issue) diff --git a/src/newTracker.js b/src/newTracker.js index 59a0c35..49837fe 100644 --- a/src/newTracker.js +++ b/src/newTracker.js @@ -97,12 +97,20 @@ export function newTrackerButton (thisTracker, context) { debug.log(`Ok The tracker created OK at: ${newTracker.uri}\nMake a note of it, bookmark it.`) } else { debug.error(`Failed to set up new store at: ${newStore.uri} : ${message}`) - alertDialog(`Failed to set up new store at: ${newStore.uri}`) + alertDialog( + `Failed to set up new store at: ${newStore.uri}\n\nDetails: ${message || 'No additional error details provided.'}`, + 'Create tracker failed', + context.dom + ) } }) } else { debug.error(`Failed to save new tracker at: ${there.uri} : ${message}`) - alertDialog(`Failed to save new tracker at: ${there.uri}`) + alertDialog( + `Failed to save new tracker at: ${there.uri}\n\nDetails: ${message || 'No additional error details provided.'}`, + 'Save tracker failed', + context.dom + ) } } ) From e8d33408cc18fb4c6f46f5bce153819cda6e45db Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Tue, 31 Mar 2026 13:09:52 +1100 Subject: [PATCH 25/25] changed contacts to issue in localUtils --- src/localUtils.js | 4 ++-- src/styles/localUtils.css | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/localUtils.js b/src/localUtils.js index 9bdf6ab..d2ad665 100644 --- a/src/localUtils.js +++ b/src/localUtils.js @@ -1,7 +1,7 @@ import { widgets } from 'solid-ui' import './styles/localUtils.css' -/* Copied from contacts-pane, made minor adjustments */ +/* Copied from issue-pane, made minor adjustments */ let modalOverlay = null let previousFocus = null @@ -14,7 +14,7 @@ function ensureModalOverlay (dom) { modalOverlay = null // overlay container modalOverlay = dom.createElement('div') - modalOverlay.id = 'contacts-modal' + modalOverlay.id = 'issue-modal' modalOverlay.className = 'focus-trap hidden' modalOverlay.setAttribute('role', 'presentation') diff --git a/src/styles/localUtils.css b/src/styles/localUtils.css index 54c1473..fda361c 100644 --- a/src/styles/localUtils.css +++ b/src/styles/localUtils.css @@ -28,7 +28,7 @@ } /* button container: center buttons horizontally (uses id in markup) */ -#contacts-modal #modal-buttons { +#issue-modal #modal-buttons { margin-top: var(--spacing-md); display: flex; justify-content: center; @@ -36,7 +36,7 @@ } /* buttons themselves use the shared btn-primary rules */ -#contacts-modal .modal button { +#issue-modal .modal button { min-height: var(--min-touch-target); padding: var(--spacing-sm) var(--spacing-md); border: 1px solid var(--color-primary); @@ -46,21 +46,21 @@ transition: all var(--animation-duration) ease; } -#contacts-modal .modal button.btn-primary { +#issue-modal .modal button.btn-primary { background: var(--color-primary); color: var(--color-background); } -#contacts-modal .modal button.btn-primary:hover { +#issue-modal .modal button.btn-primary:hover { background: color-mix(in srgb, var(--color-primary) 90%, black); box-shadow: 0 2px 4px rgba(124, 77, 255, 0.2); } -#contacts-modal .modal button.btn-primary:active { +#issue-modal .modal button.btn-primary:active { box-shadow: 0 1px 2px rgba(124, 77, 255, 0.2); } -#contacts-modal .modal button:disabled { +#issue-modal .modal button:disabled { opacity: var(--opacity-disabled, 0.6); cursor: not-allowed; transform: none;