diff --git a/dist/chrome/js/components/options-app.js b/dist/chrome/js/components/options-app.js index 4a9cf31..7bb5587 100644 --- a/dist/chrome/js/components/options-app.js +++ b/dist/chrome/js/components/options-app.js @@ -20,6 +20,15 @@ const STORAGE_KEYS = { remoteHostInfo: 'remote_host_info', }; +function escapeHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + function getTemplateRegistry() { return globalThis.__THERP_TIMER_TEMPLATES__ || {}; @@ -51,14 +60,15 @@ function createOptionsAppTemplate(app, bdom, helpers) { const readMoreState = app.createComponent('ReadMore', true, false, false, ['text', 'limit']); const rootBlock = createBlock( - `


Description


This is a standalone Owl rewrite of the original cross-platform timer extension for posting work hours to Odoo timesheets.

Features


  • Support for both tasks and issues
  • Start and stop the timer for the selected item
  • Create Odoo timesheet lines against the linked analytic account
  • Show assigned items or everyone’s items
  • Add, remove, or clear remote hosts
  • Switch between remote sessions
  • Download current month or current item timesheets as CSV

Add Remote


Controls
` + `


Description


This is a standalone Owl rewrite of the original cross-platform timer extension for posting work hours to Odoo timesheets.

Features


  • Support for both tasks and issues
  • Start and stop the timer for the selected item
  • Create Odoo timesheet lines against the linked analytic account
  • Show assigned items or everyone’s items
  • Add, remove, or clear remote hosts
  • Switch between remote sessions
  • Download current month or current item timesheets as CSV

General Settings


Store timesheet locally each time you stop the timer on an item.


Add Remote


Controls
` ); const errorBlock = createBlock(`
`); const remotesTableBlock = createBlock( `
List of Available Remotes
RemoteHostDatabaseSourceState
` ); const remoteRowBlock = createBlock( - `` + ` + ` ); return function template(ctx, node, key = '') { @@ -95,6 +105,8 @@ function createOptionsAppTemplate(app, bdom, helpers) { const reloadRemotesHandler = [ctx.loadRemotes, ctx]; const toggleListHandler = [() => { ctx.state.showList = !ctx.state.showList; }, ctx]; const removeAllRemotesHandler = [ctx.removeAllRemotes, ctx]; + const autoDownloadChecked = ctx.state.autoDownloadIssueTimesheet; + const autoDownloadHandler = [(ev) => { ctx.toggleAutoDownload(ev); }]; if (ctx.state.error) { errorNode = errorBlock([ctx.state.error]); @@ -120,10 +132,11 @@ function createOptionsAppTemplate(app, bdom, helpers) { const sourceNode = readMoreSource({ text: ctx.remote.datasrc || DEFAULT_DATA_SOURCE, limit: 18 }, key + `__4__${remoteKey}`, node, this, null); const stateNode = readMoreState({ text: ctx.remote.state || 'Inactive', limit: 18 }, key + `__5__${remoteKey}`, node, this, null); const remoteItem = ctx.remote; + const editHandler = [() => ctx.editRemote(remoteItem), ctx]; const deleteHandler = [() => ctx.removeRemote(remoteItem), ctx]; remoteChildren[i] = withKey( - remoteRowBlock([deleteHandler], [nameNode, hostNode, databaseNode, sourceNode, stateNode]), + remoteRowBlock([editHandler, deleteHandler], [nameNode, hostNode, databaseNode, sourceNode, stateNode]), remoteKey ); } @@ -155,6 +168,8 @@ function createOptionsAppTemplate(app, bdom, helpers) { reloadRemotesHandler, toggleListHandler, removeAllRemotesHandler, + autoDownloadChecked, + autoDownloadHandler, ], [errorNode, remoteListNode] ); @@ -169,11 +184,15 @@ class OptionsApp extends Component { static template = 'OptionsApp'; setup() { + this.removeRemote = this.removeRemote.bind(this); + this.editRemote = this.editRemote.bind(this); + this.state = useState({ activePage: PAGE_OPTIONS, remotes: [], showList: true, error: '', + autoDownloadIssueTimesheet: false, form: { remote_host: '', remote_name: '', @@ -184,6 +203,8 @@ class OptionsApp extends Component { onWillStart(async () => { await this.loadRemotes(); + const saved = await storage.get('auto_download_issue_timesheet', false); + this.state.autoDownloadIssueTimesheet = !!saved; }); } @@ -206,6 +227,15 @@ class OptionsApp extends Component { this.state.form.remote_datasrc = DEFAULT_DATA_SOURCE; } + /** + * Toggle the auto-download timesheet preference and persist it. + * @param {Event} ev + */ + async toggleAutoDownload(ev) { + this.state.autoDownloadIssueTimesheet = ev.target.checked; + await storage.set('auto_download_issue_timesheet', !!this.state.autoDownloadIssueTimesheet); + } + /** * Validate current form fields and return normalized values. * @@ -287,11 +317,131 @@ class OptionsApp extends Component { await this.loadRemotes(); await notify(`[${remote.url}] removed successfully!`); } + /** - * Expose the About page constant to Owl XML expressions. + * Edit an existing remote configuration. * - * @returns {string} + * @param {object} remote Remote row to edit. + * @returns {Promise} */ + async editRemote(remote) { + this.state.error = ''; + + const currentHost = remote.url || ''; + const currentName = remote.name || ''; + const currentDatabase = remote.database || ''; + const currentDatasource = remote.datasrc || DEFAULT_DATA_SOURCE; + + const customAlert = globalThis.alert && typeof globalThis.alert.show === 'function' + ? globalThis.alert + : null; + + let host = currentHost; + let name = currentName; + let database = currentDatabase; + let datasrc = currentDatasource; + + if (customAlert) { + const inputSuffix = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + const hostId = `edit-remote-host-${inputSuffix}`; + const nameId = `edit-remote-name-${inputSuffix}`; + const databaseId = `edit-remote-database-${inputSuffix}`; + const datasourceId = `edit-remote-datasource-${inputSuffix}`; + const html = ` +
+
Edit Remote
+
+ + + + +
+
+ `; + + const result = await customAlert.show(html, ['Cancel', 'Save']); + if (result !== 'Save') { + return; + } + + const hostEl = document.getElementById(hostId); + const nameEl = document.getElementById(nameId); + const databaseEl = document.getElementById(databaseId); + const datasourceEl = document.getElementById(datasourceId); + + host = normalizeHost(hostEl ? hostEl.value : currentHost); + name = ((nameEl ? nameEl.value : currentName) || '').trim(); + database = ((databaseEl ? databaseEl.value : currentDatabase) || '').trim(); + datasrc = datasourceEl ? datasourceEl.value : currentDatasource; + } else { + return; + } + + if (!host || !name || !database) { + this.state.error = 'Fields cannot be empty'; + return; + } + + if (!validURL(host)) { + this.state.error = 'Invalid URL syntax'; + return; + } + + const remotes = await readRemotes(); + const duplicate = remotes.find( + (item) => !(item.url === currentHost && item.database === currentDatabase) + && item.url === host + && item.database === database + ); + if (duplicate) { + this.state.error = `${host} and ${database} already exist; duplicates are not allowed`; + return; + } + + const index = remotes.findIndex( + (item) => item.url === currentHost && item.database === currentDatabase + ); + if (index === -1) { + this.state.error = 'Remote not found'; + return; + } + + remotes[index] = { + ...remotes[index], + url: host, + name, + database, + datasrc, + }; + + await writeRemotes(remotes); + + if (host !== currentHost) { + await clearOdooSessionCookies(currentHost); + } + if (database !== currentDatabase) { + await storage.remove(currentDatabase); + } + + await this.loadRemotes(); + await notify(`Remote [${name}] updated successfully.`); + } + get PAGE_ABOUT() { return PAGE_ABOUT; } diff --git a/dist/chrome/js/components/popup-app.js b/dist/chrome/js/components/popup-app.js index 64eb316..2451f5f 100644 --- a/dist/chrome/js/components/popup-app.js +++ b/dist/chrome/js/components/popup-app.js @@ -79,7 +79,7 @@ function createPopupAppTemplate(app, bdom, helpers) { const readMoreProject = app.createComponent('ReadMore', true, false, false, ['text', 'limit']); const rootBlock = createBlock( - `
Loading current session and projects…
Please wait — or grab a cup of coffee ☕
PriorityStage
[]
Project
` + `
Loading current session and projects…
Please wait — or grab a cup of coffee ☕
PriorityStage
[]
Project
` ); const bootErrorBlock = createBlock(`

`); const noRemotesBlock = createBlock( @@ -106,8 +106,8 @@ function createPopupAppTemplate(app, bdom, helpers) { `
Host:
` ); const activeTimerDurationBlock = createBlock(``); - const hoursSpentHeaderBlock = createBlock(`Hours Spent`); - const remainingHoursHeaderBlock = createBlock(`Remaining Hours`); + const hoursSpentHeaderBlock = createBlock(`Hours Spent`); + const remainingHoursHeaderBlock = createBlock(`Hours Left`); const issueRowBlock = createBlock( `` ); @@ -119,8 +119,8 @@ function createPopupAppTemplate(app, bdom, helpers) { ); const priorityStarBlock = createBlock(``); const priorityStarOutlineBlock = createBlock(``); - const effectiveHoursCellBlock = createBlock(``); - const remainingHoursCellBlock = createBlock(``); + const effectiveHoursCellBlock = createBlock(``); + const remainingHoursCellBlock = createBlock(``); const emptyIssuesRowBlock = createBlock( `No matching items are currently available` ); @@ -250,8 +250,6 @@ function createPopupAppTemplate(app, bdom, helpers) { const limitHandler = [(ev) => { ctx.updateLimitPreference(ev.target.value); }]; - const autoDownloadChecked = ctx.state.autoDownloadIssueTimesheet; - const autoDownloadHandler = [ctx.toggleAutoDownload, ctx]; const downloadTimesheetHandler = [ctx.downloadCurrentMonthTimesheets, ctx]; const switchRemotesHandler = [ctx.switchBetweenRemotes, ctx]; const refreshHandler = [ctx.refreshAll, ctx]; @@ -400,8 +398,6 @@ function createPopupAppTemplate(app, bdom, helpers) { searchQueryHandler, limitValue, limitHandler, - autoDownloadChecked, - autoDownloadHandler, downloadTimesheetHandler, switchRemotesHandler, refreshHandler, @@ -845,15 +841,15 @@ class PopupApp extends Component { } issueLabel(issue) { - const issueName = this.normalizeText( - issue.display_name || issue.name || issue.message_summary || issue.description || '' - ); - if (this.state.dataSource === DATA_SOURCE_TASK) { const code = this.normalizeText(issue.code); + const issueName = this.normalizeText(issue.name || issue.description || ''); return [code, issueName].filter(Boolean).join(' - ') || `#${issue.id}`; } + const issueName = this.normalizeText( + issue.display_name || issue.name || issue.message_summary || issue.description || '' + ); return [`#${issue.id}`, issueName].filter(Boolean).join(' - '); } @@ -1291,6 +1287,24 @@ class PopupApp extends Component { * Download a CSV with current month timesheet rows. */ async downloadCurrentMonthTimesheets() { + const autoDownloadEnabled = await storage.get('auto_download_issue_timesheet', false); + if (!autoDownloadEnabled) { + const blockingText = + '
' + + '
⚠️ Auto-Download Not Enabled
' + + '

' + + 'Please enable Auto Download Current Item Timesheet ' + + 'in the Options menu before downloading timesheets.' + + '

' + + '

' + + 'Click the ⚙️ Options icon, then check the box under General Settings.' + + '

' + + '
'; + + await alert.show(blockingText, ['OK']); + return; // STOP - do not proceed with download + } + try { if (!this.state.user?.id) { throw new Error('Login first.'); diff --git a/dist/chrome/js/templates.js b/dist/chrome/js/templates.js index a87b1ca..25ca7f7 100644 --- a/dist/chrome/js/templates.js +++ b/dist/chrome/js/templates.js @@ -1,190 +1,80 @@ export const templates = { - "PopupApp": function PopupApp(app, bdom, helpers + "OptionsApp": function OptionsApp(app, bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { safeOutput, prepareList, withKey } = helpers; const comp1 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - const comp2 = app.createComponent(`ReadMore`, true, false, false, ["text","limit","href"]); + const comp2 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp3 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp4 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp5 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - let block1 = createBlock(`
Loading current session and projects…
Please wait — or grab a cup of coffee ☕
PriorityStage
[]
Project
`); - let block2 = createBlock(`

`); - let block4 = createBlock(`
Hello 😉, you have not configured any remotes. Open Options below and add one.
`); - let block5 = createBlock(`
`); - let block6 = createBlock(`

`); - let block8 = createBlock(``); - let block9 = createBlock(``); - let block10 = createBlock(``); - let block12 = createBlock(``); - let block14 = createBlock(``); - let block15 = createBlock(`
Host:
`); - let block18 = createBlock(` #`); - let block20 = createBlock(``); - let block25 = createBlock(`Hours Spent`); - let block26 = createBlock(`Remaining Hours`); - let block28 = createBlock(``); - let block29 = createBlock(``); - let block30 = createBlock(``); - let block32 = createBlock(``); - let block33 = createBlock(``); - let block37 = createBlock(``); - let block39 = createBlock(``); - let block42 = createBlock(` No matching items are currently available `); + let block1 = createBlock(`


Description


This is a standalone Owl rewrite of the original cross-platform timer extension for posting work hours to Odoo timesheets.

Features


  • Support for both tasks and issues
  • Start and stop the timer for the selected item
  • Create Odoo timesheet lines against the linked analytic account
  • Show assigned items or everyone's items
  • Add, remove, or clear remote hosts
  • Switch between remote sessions
  • Download current month or current item timesheets as CSV

General Settings


Store timesheet locally each time you stop the timer on an item.


Add Remote


Controls
`); + let block2 = createBlock(`
`); + let block4 = createBlock(`
List of Available Remotes
RemoteHostDatabaseSourceState
`); + let block6 = createBlock(``); return function template(ctx, node, key = "") { - let b2, b4, b5, b18, b20, b22, b23, b24, b27, b42, b43, b44, b45, b46; - let attr1 = ctx['state'].view==='loading'?'':'hide'; - let attr2 = ctx['state'].view==='login'?'login-view':'login-view hide'; - if (ctx['state'].bootError) { - const b3 = safeOutput(ctx['state'].bootError); - b2 = block2([], [b3]); - } - if (!ctx['state'].remotes.length) { - b4 = block4(); - } - if (ctx['state'].remotes.length) { - let b6, b8, b9, b10, b11, b14, b15; - let hdlr1 = ["prevent", ctx['login'], ctx]; - if (ctx['state'].loginError) { - const b7 = safeOutput(ctx['state'].loginError); - b6 = block6([], [b7]); - } - if (!ctx['state'].useExistingSession) { - let prop1 = new String((ctx['state'].username) === 0 ? 0 : ((ctx['state'].username) || "")); - const v1 = ctx['state']; - let hdlr2 = [(_ev)=>v1.username=_ev.target.value, ctx]; - b8 = block8([prop1, hdlr2]); - } - if (!ctx['state'].useExistingSession) { - let attr3 = ctx['state'].showPassword?'text':'password'; - let prop2 = new String((ctx['state'].password) === 0 ? 0 : ((ctx['state'].password) || "")); - const v2 = ctx['state']; - let hdlr3 = [(_ev)=>v2.password=_ev.target.value, ctx]; - b9 = block9([attr3, prop2, hdlr3]); - } - if (!ctx['state'].useExistingSession) { - let hdlr4 = [ctx['togglePassword'], ctx]; - let attr4 = ctx['state'].showPassword?'fa fa-eye-slash':'fa fa-eye'; - b10 = block10([hdlr4, attr4]); - } - const v3 = ctx['state']; - let hdlr5 = [(_ev)=>v3.selectedRemoteIndex=_ev.target.value, ctx]; - ctx = Object.create(ctx); - const [k_block11, v_block11, l_block11, c_block11] = prepareList(ctx['state'].remotes);; - for (let i1 = 0; i1 < l_block11; i1++) { - ctx[`remote`] = k_block11[i1]; - const key1 = ctx['remote'].database+ctx['remote'].url; - let attr5 = ctx['remote'].__index; - let prop3 = new Boolean(ctx['state'].selectedRemoteIndex===ctx['remote'].__index); - const b13 = safeOutput(ctx['remote'].name); - c_block11[i1] = withKey(block12([attr5, prop3], [b13]), key1); - } - ctx = ctx.__proto__; - b11 = list(c_block11); - let prop4 = new Boolean(ctx['state'].useExistingSession); - let hdlr6 = [ctx['toggleUseExistingSession'], ctx]; - if (ctx['state'].loginLoading) { - b14 = block14(); - } - if (ctx['currentRemote']) { - const b16 = safeOutput(ctx['currentRemote'].url); - const b17 = safeOutput(ctx['currentRemote'].datasrc||'project.issue'); - b15 = block15([], [b16, b17]); - } - b5 = block5([hdlr1, hdlr5, prop4, hdlr6], [b6, b8, b9, b10, b11, b14, b15]); - } - let attr6 = ctx['state'].view==='main'?'':'hide'; - let prop5 = new String((ctx['state'].searchQuery) === 0 ? 0 : ((ctx['state'].searchQuery) || "")); + let b2, b4; + let attr1 = ctx['state'].activePage==='about'?'selected':'notselected'; + const v1 = ctx['state']; + let hdlr1 = [()=>v1.activePage='about', ctx]; + let attr2 = ctx['state'].activePage==='options'?'selected':'notselected'; + const v2 = ctx['state']; + let hdlr2 = [()=>v2.activePage='options', ctx]; + let attr3 = ctx['state'].activePage==='about'?'active_page':'inactive_page'; + let attr4 = ctx['state'].activePage==='options'?'active_page':'inactive_page'; + let hdlr3 = ["prevent", ctx['addRemote'], ctx]; + let prop1 = new Boolean(ctx['state'].autoDownloadIssueTimesheet); + let hdlr4 = [ctx['toggleAutoDownload'], ctx]; + let prop2 = new String((ctx['state'].form.remote_host) === 0 ? 0 : ((ctx['state'].form.remote_host) || "")); + const v3 = ctx['state']; + let hdlr5 = [(_ev)=>v3.form.remote_host=_ev.target.value, ctx]; + let prop3 = new String((ctx['state'].form.remote_name) === 0 ? 0 : ((ctx['state'].form.remote_name) || "")); const v4 = ctx['state']; - let hdlr7 = [(_ev)=>v4.searchQuery=_ev.target.value, ctx]; - let prop6 = new String((ctx['state'].limitTo) === 0 ? 0 : ((ctx['state'].limitTo) || "")); - const v5 = ctx['updateLimitPreference']; - let hdlr8 = [(_ev)=>v5(_ev.target.value), ctx]; - let prop7 = new Boolean(ctx['state'].autoDownloadIssueTimesheet); - let hdlr9 = [ctx['toggleAutoDownload'], ctx]; - let hdlr10 = [ctx['downloadCurrentMonthTimesheets'], ctx]; - let hdlr11 = [ctx['switchBetweenRemotes'], ctx]; - let hdlr12 = [ctx['refreshAll'], ctx]; - let hdlr13 = [ctx['resetTimer'], ctx]; - let hdlr14 = [ctx['logout'], ctx]; - if (ctx['state'].activeTimerId&&ctx['state'].timerStartIso) { - const b19 = safeOutput(ctx['state'].activeTimerId); - b18 = block18([], [b19]); - } - if (ctx['state'].timerStartIso) { - const b21 = safeOutput(ctx['formattedTimer']); - b20 = block20([], [b21]); - } - b22 = safeOutput(ctx['itemLabelPlural']); - b23 = safeOutput(ctx['filteredIssues'].length); - let prop8 = new Boolean(ctx['state'].allIssues); - const v6 = ctx['updateShowAllPreference']; - let hdlr15 = [(_ev)=>v6(_ev.target.checked), ctx]; - if (ctx['state'].dataSource==='project.task') { - const b25 = block25(); - const b26 = block26(); - b24 = multi([b25, b26]); + let hdlr6 = [(_ev)=>v4.form.remote_name=_ev.target.value, ctx]; + let prop4 = new String((ctx['state'].form.remote_database) === 0 ? 0 : ((ctx['state'].form.remote_database) || "")); + const v5 = ctx['state']; + let hdlr7 = [(_ev)=>v5.form.remote_database=_ev.target.value, ctx]; + let prop5 = new Boolean(ctx['state'].form.remote_datasrc==='project.issue'); + const v6 = ctx['state']; + let hdlr8 = [()=>v6.form.remote_datasrc='project.issue', ctx]; + let prop6 = new Boolean(ctx['state'].form.remote_datasrc==='project.task'); + const v7 = ctx['state']; + let hdlr9 = [()=>v7.form.remote_datasrc='project.task', ctx]; + let hdlr10 = [ctx['addRemote'], ctx]; + let hdlr11 = [ctx['loadRemotes'], ctx]; + const v8 = ctx['state']; + let hdlr12 = [()=>v8.showList=!v8.showList, ctx]; + let hdlr13 = [ctx['removeAllRemotes'], ctx]; + if (ctx['state'].error) { + const b3 = safeOutput(ctx['state'].error); + b2 = block2([], [b3]); } - if (ctx['filteredIssues'].length) { + if (ctx['state'].showList&&ctx['state'].remotes.length) { ctx = Object.create(ctx); - const [k_block27, v_block27, l_block27, c_block27] = prepareList(ctx['filteredIssues']);; - for (let i1 = 0; i1 < l_block27; i1++) { - ctx[`issue`] = k_block27[i1]; - const key1 = ctx['issue'].id; - let b29, b30, b31, b33, b34, b35, b36, b41; - let attr7 = ctx['state'].activeTimerId===ctx['issue'].id?'active-row':''; - if (!ctx['state'].activeTimerId) { - const v7 = ctx['startTimer']; - const v8 = ctx['issue']; - let hdlr16 = [()=>v7(v8), ctx]; - b29 = block29([hdlr16]); - } - if (ctx['state'].activeTimerId===ctx['issue'].id) { - const v9 = ctx['stopTimer']; - const v10 = ctx['issue']; - let hdlr17 = [()=>v9(v10), ctx]; - b30 = block30([hdlr17]); - } - if (ctx['issue'].priority_level.length) { - ctx = Object.create(ctx); - const [k_block31, v_block31, l_block31, c_block31] = prepareList(ctx['issue'].priority_level);; - for (let i2 = 0; i2 < l_block31; i2++) { - ctx[`priority`] = k_block31[i2]; - const key2 = ctx['priority']+'_'+ctx['issue'].id; - c_block31[i2] = withKey(block32(), key2); - } - ctx = ctx.__proto__; - b31 = list(c_block31); - } - if (!ctx['issue'].priority_level.length) { - b33 = block33(); - } - b34 = comp1({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__1__${key1}`, node, this, null); - b35 = comp2({text: ctx['issueLabel'](ctx['issue']),limit: 70,href: ctx['issueHref'](ctx['issue'])}, key + `__2__${key1}`, node, this, null); - if (ctx['state'].dataSource==='project.task') { - const b38 = comp3({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].effective_hours)),limit: 9}, key + `__3__${key1}`, node, this, null); - const b37 = block37([], [b38]); - const b40 = comp4({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].remaining_hours)),limit: 9}, key + `__4__${key1}`, node, this, null); - const b39 = block39([], [b40]); - b36 = multi([b37, b39]); - } - b41 = comp5({text: ctx['relationLabel'](ctx['issue'].project_id),limit: 15}, key + `__5__${key1}`, node, this, null); - c_block27[i1] = withKey(block28([attr7], [b29, b30, b31, b33, b34, b35, b36, b41]), key1); + const [k_block5, v_block5, l_block5, c_block5] = prepareList(ctx['state'].remotes);; + for (let i1 = 0; i1 < l_block5; i1++) { + ctx[`remote`] = k_block5[i1]; + const key1 = ctx['remote'].url+ctx['remote'].database; + const b7 = comp1({text: ctx['remote'].name,limit: 18}, key + `__1__${key1}`, node, this, null); + const b8 = comp2({text: ctx['remote'].url,limit: 25}, key + `__2__${key1}`, node, this, null); + const b9 = comp3({text: ctx['remote'].database,limit: 18}, key + `__3__${key1}`, node, this, null); + const b10 = comp4({text: ctx['remote'].datasrc||'project.issue',limit: 18}, key + `__4__${key1}`, node, this, null); + const b11 = comp5({text: ctx['remote'].state||'Inactive',limit: 18}, key + `__5__${key1}`, node, this, null); + const v9 = ctx['editRemote']; + const v10 = ctx['remote']; + let hdlr14 = [()=>v9(v10), ctx]; + const v11 = ctx['removeRemote']; + const v12 = ctx['remote']; + let hdlr15 = [()=>v11(v12), ctx]; + c_block5[i1] = withKey(block6([hdlr14, hdlr15], [b7, b8, b9, b10, b11]), key1); } ctx = ctx.__proto__; - b27 = list(c_block27); + const b5 = list(c_block5); + b4 = block4([], [b5]); } - if (!ctx['filteredIssues'].length) { - let attr8 = ctx['state'].dataSource==='project.task'?7:5; - b42 = block42([attr8]); - } - b43 = safeOutput(ctx['state'].serverVersion||'Unknown'); - b44 = safeOutput(ctx['state'].currentHost||'-'); - b45 = safeOutput(ctx['state'].currentDatabase||'-'); - b46 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); - return block1([attr1, attr2, attr6, prop5, hdlr7, prop6, hdlr8, prop7, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, prop8, hdlr15], [b2, b4, b5, b18, b20, b22, b23, b24, b27, b42, b43, b44, b45, b46]); + return block1([attr1, hdlr1, attr2, hdlr2, attr3, attr4, hdlr3, prop1, hdlr4, prop2, hdlr5, prop3, hdlr6, prop4, hdlr7, prop5, hdlr8, prop6, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13], [b2, b4]); } }, @@ -198,7 +88,7 @@ export const templates = { const comp4 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp5 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - let block1 = createBlock(`
Loading current session and projects…
Please wait — or grab a cup of coffee ☕
PriorityStage
[]
Project
`); + let block1 = createBlock(`
Loading current session and projects…
Please wait — or grab a cup of coffee ☕
PriorityStage
[]
Project
`); let block2 = createBlock(`

`); let block4 = createBlock(`
Hello 😉, you have not configured any remotes. Open Options below and add one.
`); let block5 = createBlock(`
`); @@ -210,15 +100,15 @@ export const templates = { let block14 = createBlock(``); let block15 = createBlock(`
Host:
`); let block18 = createBlock(``); - let block23 = createBlock(`Hours Spent`); - let block24 = createBlock(`Remaining Hours`); + let block23 = createBlock(`Hours Spent`); + let block24 = createBlock(`Hours Left`); let block26 = createBlock(``); let block27 = createBlock(``); let block28 = createBlock(``); let block30 = createBlock(``); let block31 = createBlock(``); - let block35 = createBlock(``); - let block37 = createBlock(``); + let block35 = createBlock(``); + let block37 = createBlock(``); let block40 = createBlock(` No matching items are currently available `); return function template(ctx, node, key = "") { @@ -293,22 +183,20 @@ export const templates = { let hdlr8 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; const v5 = ctx['updateLimitPreference']; let hdlr9 = [(_ev)=>v5(_ev.target.value), ctx]; - let prop7 = new Boolean(ctx['state'].autoDownloadIssueTimesheet); - let hdlr10 = [ctx['toggleAutoDownload'], ctx]; - let hdlr11 = [ctx['downloadCurrentMonthTimesheets'], ctx]; - let hdlr12 = [ctx['switchBetweenRemotes'], ctx]; - let hdlr13 = [ctx['refreshAll'], ctx]; - let hdlr14 = [ctx['resetTimer'], ctx]; - let hdlr15 = [ctx['logout'], ctx]; + let hdlr10 = [ctx['downloadCurrentMonthTimesheets'], ctx]; + let hdlr11 = [ctx['switchBetweenRemotes'], ctx]; + let hdlr12 = [ctx['refreshAll'], ctx]; + let hdlr13 = [ctx['resetTimer'], ctx]; + let hdlr14 = [ctx['logout'], ctx]; if (ctx['state'].timerStartIso) { const b19 = safeOutput(ctx['formattedTimer']); b18 = block18([], [b19]); } b20 = safeOutput(ctx['itemLabelPlural']); b21 = safeOutput(ctx['filteredIssues'].length); - let prop8 = new Boolean(ctx['state'].allIssues); + let prop7 = new Boolean(ctx['state'].allIssues); const v6 = ctx['updateShowAllPreference']; - let hdlr16 = [(_ev)=>v6(_ev.target.checked), ctx]; + let hdlr15 = [(_ev)=>v6(_ev.target.checked), ctx]; if (ctx['state'].dataSource==='project.task') { const b23 = block23(); const b24 = block24(); @@ -325,14 +213,14 @@ export const templates = { if (!ctx['state'].activeTimerId) { const v7 = ctx['startTimer']; const v8 = ctx['issue']; - let hdlr17 = [()=>v7(v8), ctx]; - b27 = block27([hdlr17]); + let hdlr16 = [()=>v7(v8), ctx]; + b27 = block27([hdlr16]); } if (ctx['state'].activeTimerId===ctx['issue'].id) { const v9 = ctx['stopTimer']; const v10 = ctx['issue']; - let hdlr18 = [()=>v9(v10), ctx]; - b28 = block28([hdlr18]); + let hdlr17 = [()=>v9(v10), ctx]; + b28 = block28([hdlr17]); } if (ctx['issue'].priority_level.length) { ctx = Object.create(ctx); @@ -372,7 +260,7 @@ export const templates = { b43 = safeOutput(ctx['state'].currentHost||'-'); b44 = safeOutput(ctx['state'].currentDatabase||'-'); b45 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); - return block1([attr1, attr2, attr6, prop5, hdlr7, prop6, hdlr8, hdlr9, prop7, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15, prop8, hdlr16], [b2, b4, b5, b18, b20, b21, b22, b25, b40, b41, b42, b43, b44, b45]); + return block1([attr1, attr2, attr6, prop5, hdlr7, prop6, hdlr8, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, prop7, hdlr15], [b2, b4, b5, b18, b20, b21, b22, b25, b40, b41, b42, b43, b44, b45]); } }, diff --git a/dist/chrome/manifest.json b/dist/chrome/manifest.json index 7e98fc6..28f1757 100644 --- a/dist/chrome/manifest.json +++ b/dist/chrome/manifest.json @@ -32,7 +32,7 @@ "storage", "cookies" ], - "optional_host_permissions": [ + "host_permissions": [ "https://*/*", "http://*/*" ] diff --git a/dist/chrome/popup.html b/dist/chrome/popup.html index 17c4c28..d31ccf8 100644 --- a/dist/chrome/popup.html +++ b/dist/chrome/popup.html @@ -17,7 +17,7 @@
Loading current session and projects…
-
Please wait — or grab a cup of coffee ☕
+
Please wait or grab a cup of coffee ☕
diff --git a/dist/firefox/js/components/options-app.js b/dist/firefox/js/components/options-app.js index 4a9cf31..54f1561 100644 --- a/dist/firefox/js/components/options-app.js +++ b/dist/firefox/js/components/options-app.js @@ -20,6 +20,15 @@ const STORAGE_KEYS = { remoteHostInfo: 'remote_host_info', }; +function escapeHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + function getTemplateRegistry() { return globalThis.__THERP_TIMER_TEMPLATES__ || {}; @@ -51,14 +60,15 @@ function createOptionsAppTemplate(app, bdom, helpers) { const readMoreState = app.createComponent('ReadMore', true, false, false, ['text', 'limit']); const rootBlock = createBlock( - `


Description


This is a standalone Owl rewrite of the original cross-platform timer extension for posting work hours to Odoo timesheets.

Features


  • Support for both tasks and issues
  • Start and stop the timer for the selected item
  • Create Odoo timesheet lines against the linked analytic account
  • Show assigned items or everyone’s items
  • Add, remove, or clear remote hosts
  • Switch between remote sessions
  • Download current month or current item timesheets as CSV

Add Remote


Controls
` + `


Description


This is a standalone Owl rewrite of the original cross-platform timer extension for posting work hours to Odoo timesheets.

Features


  • Support for both tasks and issues
  • Start and stop the timer for the selected item
  • Create Odoo timesheet lines against the linked analytic account
  • Show assigned items or everyone’s items
  • Add, remove, or clear remote hosts
  • Switch between remote sessions
  • Download current month or current item timesheets as CSV

General Settings


Store timesheet locally each time you stop the timer on an item.


Add Remote


Controls
` ); const errorBlock = createBlock(`
`); const remotesTableBlock = createBlock( `
List of Available Remotes
RemoteHostDatabaseSourceState
` ); const remoteRowBlock = createBlock( - `` + ` + ` ); return function template(ctx, node, key = '') { @@ -95,6 +105,8 @@ function createOptionsAppTemplate(app, bdom, helpers) { const reloadRemotesHandler = [ctx.loadRemotes, ctx]; const toggleListHandler = [() => { ctx.state.showList = !ctx.state.showList; }, ctx]; const removeAllRemotesHandler = [ctx.removeAllRemotes, ctx]; + const autoDownloadChecked = ctx.state.autoDownloadIssueTimesheet; + const autoDownloadHandler = [(ev) => { ctx.toggleAutoDownload(ev); }]; if (ctx.state.error) { errorNode = errorBlock([ctx.state.error]); @@ -120,10 +132,11 @@ function createOptionsAppTemplate(app, bdom, helpers) { const sourceNode = readMoreSource({ text: ctx.remote.datasrc || DEFAULT_DATA_SOURCE, limit: 18 }, key + `__4__${remoteKey}`, node, this, null); const stateNode = readMoreState({ text: ctx.remote.state || 'Inactive', limit: 18 }, key + `__5__${remoteKey}`, node, this, null); const remoteItem = ctx.remote; + const editHandler = [() => ctx.editRemote(remoteItem), ctx]; const deleteHandler = [() => ctx.removeRemote(remoteItem), ctx]; remoteChildren[i] = withKey( - remoteRowBlock([deleteHandler], [nameNode, hostNode, databaseNode, sourceNode, stateNode]), + remoteRowBlock([editHandler, deleteHandler], [nameNode, hostNode, databaseNode, sourceNode, stateNode]), remoteKey ); } @@ -155,6 +168,8 @@ function createOptionsAppTemplate(app, bdom, helpers) { reloadRemotesHandler, toggleListHandler, removeAllRemotesHandler, + autoDownloadChecked, + autoDownloadHandler, ], [errorNode, remoteListNode] ); @@ -169,11 +184,15 @@ class OptionsApp extends Component { static template = 'OptionsApp'; setup() { + this.removeRemote = this.removeRemote.bind(this); + this.editRemote = this.editRemote.bind(this); + this.state = useState({ activePage: PAGE_OPTIONS, remotes: [], showList: true, error: '', + autoDownloadIssueTimesheet: false, form: { remote_host: '', remote_name: '', @@ -184,6 +203,8 @@ class OptionsApp extends Component { onWillStart(async () => { await this.loadRemotes(); + const saved = await storage.get('auto_download_issue_timesheet', false); + this.state.autoDownloadIssueTimesheet = !!saved; }); } @@ -206,6 +227,15 @@ class OptionsApp extends Component { this.state.form.remote_datasrc = DEFAULT_DATA_SOURCE; } + /** + * Toggle the auto-download timesheet preference and persist it. + * @param {Event} ev + */ + async toggleAutoDownload(ev) { + this.state.autoDownloadIssueTimesheet = ev.target.checked; + await storage.set('auto_download_issue_timesheet', !!this.state.autoDownloadIssueTimesheet); + } + /** * Validate current form fields and return normalized values. * @@ -287,11 +317,131 @@ class OptionsApp extends Component { await this.loadRemotes(); await notify(`[${remote.url}] removed successfully!`); } + /** - * Expose the About page constant to Owl XML expressions. + * Edit an existing remote configuration. * - * @returns {string} + * @param {object} remote Remote row to edit. + * @returns {Promise} */ + async editRemote(remote) { + this.state.error = ''; + + const currentHost = remote.url || ''; + const currentName = remote.name || ''; + const currentDatabase = remote.database || ''; + const currentDatasource = remote.datasrc || DEFAULT_DATA_SOURCE; + + const customAlert = globalThis.alert && typeof globalThis.alert.show === 'function' + ? globalThis.alert + : null; + + let host = currentHost; + let name = currentName; + let database = currentDatabase; + let datasrc = currentDatasource; + + if (customAlert) { + const inputSuffix = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + const hostId = `edit-remote-host-${inputSuffix}`; + const nameId = `edit-remote-name-${inputSuffix}`; + const databaseId = `edit-remote-database-${inputSuffix}`; + const datasourceId = `edit-remote-datasource-${inputSuffix}`; + const html = ` +
+
Edit Remote
+
+ + + + +
+
+ `; + + const result = await customAlert.show(html, ['Cancel', 'Save']); + if (result !== 'Save') { + return; + } + + const hostEl = document.getElementById(hostId); + const nameEl = document.getElementById(nameId); + const databaseEl = document.getElementById(databaseId); + const datasourceEl = document.getElementById(datasourceId); + + host = normalizeHost(hostEl ? hostEl.value : currentHost); + name = ((nameEl ? nameEl.value : currentName) || '').trim(); + database = ((databaseEl ? databaseEl.value : currentDatabase) || '').trim(); + datasrc = datasourceEl ? datasourceEl.value : currentDatasource; + } else { + return; + } + + if (!host || !name || !database) { + this.state.error = 'Fields cannot be empty'; + return; + } + + if (!validURL(host)) { + this.state.error = 'Invalid URL syntax'; + return; + } + + const remotes = await readRemotes(); + const duplicate = remotes.find( + (item) => !(item.url === currentHost && item.database === currentDatabase) + && item.url === host + && item.database === database + ); + if (duplicate) { + this.state.error = `${host} and ${database} already exist; duplicates are not allowed`; + return; + } + + const index = remotes.findIndex( + (item) => item.url === currentHost && item.database === currentDatabase + ); + if (index === -1) { + this.state.error = 'Remote not found'; + return; + } + + remotes[index] = { + ...remotes[index], + url: host, + name, + database, + datasrc, + }; + + await writeRemotes(remotes); + + if (host !== currentHost) { + await clearOdooSessionCookies(currentHost); + } + if (database !== currentDatabase) { + await storage.remove(currentDatabase); + } + + await this.loadRemotes(); + await notify(`Remote [${name}] updated successfully.`); + } + get PAGE_ABOUT() { return PAGE_ABOUT; } diff --git a/dist/firefox/js/components/popup-app.js b/dist/firefox/js/components/popup-app.js index 64eb316..177ea90 100644 --- a/dist/firefox/js/components/popup-app.js +++ b/dist/firefox/js/components/popup-app.js @@ -79,7 +79,7 @@ function createPopupAppTemplate(app, bdom, helpers) { const readMoreProject = app.createComponent('ReadMore', true, false, false, ['text', 'limit']); const rootBlock = createBlock( - `
Loading current session and projects…
Please wait — or grab a cup of coffee ☕
PriorityStage
[]
Project
` + `
Loading current session and projects…
Please wait — or grab a cup of coffee ☕
PriorityStage
[]
Project
` ); const bootErrorBlock = createBlock(`

`); const noRemotesBlock = createBlock( @@ -106,8 +106,8 @@ function createPopupAppTemplate(app, bdom, helpers) { `
Host:
` ); const activeTimerDurationBlock = createBlock(``); - const hoursSpentHeaderBlock = createBlock(`Hours Spent`); - const remainingHoursHeaderBlock = createBlock(`Remaining Hours`); + const hoursSpentHeaderBlock = createBlock(`Hours Spent`); + const remainingHoursHeaderBlock = createBlock(`Hours Left`); const issueRowBlock = createBlock( `` ); @@ -119,8 +119,8 @@ function createPopupAppTemplate(app, bdom, helpers) { ); const priorityStarBlock = createBlock(``); const priorityStarOutlineBlock = createBlock(``); - const effectiveHoursCellBlock = createBlock(``); - const remainingHoursCellBlock = createBlock(``); + const effectiveHoursCellBlock = createBlock(``); + const remainingHoursCellBlock = createBlock(``); const emptyIssuesRowBlock = createBlock( `No matching items are currently available` ); @@ -250,8 +250,6 @@ function createPopupAppTemplate(app, bdom, helpers) { const limitHandler = [(ev) => { ctx.updateLimitPreference(ev.target.value); }]; - const autoDownloadChecked = ctx.state.autoDownloadIssueTimesheet; - const autoDownloadHandler = [ctx.toggleAutoDownload, ctx]; const downloadTimesheetHandler = [ctx.downloadCurrentMonthTimesheets, ctx]; const switchRemotesHandler = [ctx.switchBetweenRemotes, ctx]; const refreshHandler = [ctx.refreshAll, ctx]; @@ -400,8 +398,6 @@ function createPopupAppTemplate(app, bdom, helpers) { searchQueryHandler, limitValue, limitHandler, - autoDownloadChecked, - autoDownloadHandler, downloadTimesheetHandler, switchRemotesHandler, refreshHandler, @@ -767,7 +763,6 @@ class PopupApp extends Component { ]); if (sessionInfo?.uid) { - // FIX: must return here so we don't fall through to VIEW_LOGIN below. await this.completeSession(sessionInfo, this.state.remotes[remoteIndex] || null); return; } @@ -845,15 +840,15 @@ class PopupApp extends Component { } issueLabel(issue) { - const issueName = this.normalizeText( - issue.display_name || issue.name || issue.message_summary || issue.description || '' - ); - if (this.state.dataSource === DATA_SOURCE_TASK) { const code = this.normalizeText(issue.code); + const issueName = this.normalizeText(issue.name || issue.description || ''); return [code, issueName].filter(Boolean).join(' - ') || `#${issue.id}`; } + const issueName = this.normalizeText( + issue.display_name || issue.name || issue.message_summary || issue.description || '' + ); return [`#${issue.id}`, issueName].filter(Boolean).join(' - '); } @@ -1291,6 +1286,24 @@ class PopupApp extends Component { * Download a CSV with current month timesheet rows. */ async downloadCurrentMonthTimesheets() { + const autoDownloadEnabled = await storage.get('auto_download_issue_timesheet', false); + if (!autoDownloadEnabled) { + const blockingText = + '
' + + '
⚠️ Auto-Download Not Enabled
' + + '

' + + 'Please enable Auto Download Current Item Timesheet ' + + 'in the Options menu before downloading timesheets.' + + '

' + + '

' + + 'Click the ⚙️ Options icon, then check the box under General Settings.' + + '

' + + '
'; + + await alert.show(blockingText, ['OK']); + return; // STOP - do not proceed with download + } + try { if (!this.state.user?.id) { throw new Error('Login first.'); diff --git a/dist/firefox/js/templates.js b/dist/firefox/js/templates.js index a87b1ca..25ca7f7 100644 --- a/dist/firefox/js/templates.js +++ b/dist/firefox/js/templates.js @@ -1,190 +1,80 @@ export const templates = { - "PopupApp": function PopupApp(app, bdom, helpers + "OptionsApp": function OptionsApp(app, bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { safeOutput, prepareList, withKey } = helpers; const comp1 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - const comp2 = app.createComponent(`ReadMore`, true, false, false, ["text","limit","href"]); + const comp2 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp3 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp4 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp5 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - let block1 = createBlock(`
Loading current session and projects…
Please wait — or grab a cup of coffee ☕
PriorityStage
[]
Project
`); - let block2 = createBlock(`

`); - let block4 = createBlock(`
Hello 😉, you have not configured any remotes. Open Options below and add one.
`); - let block5 = createBlock(`
`); - let block6 = createBlock(`

`); - let block8 = createBlock(``); - let block9 = createBlock(``); - let block10 = createBlock(``); - let block12 = createBlock(``); - let block14 = createBlock(``); - let block15 = createBlock(`
Host:
`); - let block18 = createBlock(` #`); - let block20 = createBlock(``); - let block25 = createBlock(`Hours Spent`); - let block26 = createBlock(`Remaining Hours`); - let block28 = createBlock(``); - let block29 = createBlock(``); - let block30 = createBlock(``); - let block32 = createBlock(``); - let block33 = createBlock(``); - let block37 = createBlock(``); - let block39 = createBlock(``); - let block42 = createBlock(` No matching items are currently available `); + let block1 = createBlock(`


Description


This is a standalone Owl rewrite of the original cross-platform timer extension for posting work hours to Odoo timesheets.

Features


  • Support for both tasks and issues
  • Start and stop the timer for the selected item
  • Create Odoo timesheet lines against the linked analytic account
  • Show assigned items or everyone's items
  • Add, remove, or clear remote hosts
  • Switch between remote sessions
  • Download current month or current item timesheets as CSV

General Settings


Store timesheet locally each time you stop the timer on an item.


Add Remote


Controls
`); + let block2 = createBlock(`
`); + let block4 = createBlock(`
List of Available Remotes
RemoteHostDatabaseSourceState
`); + let block6 = createBlock(``); return function template(ctx, node, key = "") { - let b2, b4, b5, b18, b20, b22, b23, b24, b27, b42, b43, b44, b45, b46; - let attr1 = ctx['state'].view==='loading'?'':'hide'; - let attr2 = ctx['state'].view==='login'?'login-view':'login-view hide'; - if (ctx['state'].bootError) { - const b3 = safeOutput(ctx['state'].bootError); - b2 = block2([], [b3]); - } - if (!ctx['state'].remotes.length) { - b4 = block4(); - } - if (ctx['state'].remotes.length) { - let b6, b8, b9, b10, b11, b14, b15; - let hdlr1 = ["prevent", ctx['login'], ctx]; - if (ctx['state'].loginError) { - const b7 = safeOutput(ctx['state'].loginError); - b6 = block6([], [b7]); - } - if (!ctx['state'].useExistingSession) { - let prop1 = new String((ctx['state'].username) === 0 ? 0 : ((ctx['state'].username) || "")); - const v1 = ctx['state']; - let hdlr2 = [(_ev)=>v1.username=_ev.target.value, ctx]; - b8 = block8([prop1, hdlr2]); - } - if (!ctx['state'].useExistingSession) { - let attr3 = ctx['state'].showPassword?'text':'password'; - let prop2 = new String((ctx['state'].password) === 0 ? 0 : ((ctx['state'].password) || "")); - const v2 = ctx['state']; - let hdlr3 = [(_ev)=>v2.password=_ev.target.value, ctx]; - b9 = block9([attr3, prop2, hdlr3]); - } - if (!ctx['state'].useExistingSession) { - let hdlr4 = [ctx['togglePassword'], ctx]; - let attr4 = ctx['state'].showPassword?'fa fa-eye-slash':'fa fa-eye'; - b10 = block10([hdlr4, attr4]); - } - const v3 = ctx['state']; - let hdlr5 = [(_ev)=>v3.selectedRemoteIndex=_ev.target.value, ctx]; - ctx = Object.create(ctx); - const [k_block11, v_block11, l_block11, c_block11] = prepareList(ctx['state'].remotes);; - for (let i1 = 0; i1 < l_block11; i1++) { - ctx[`remote`] = k_block11[i1]; - const key1 = ctx['remote'].database+ctx['remote'].url; - let attr5 = ctx['remote'].__index; - let prop3 = new Boolean(ctx['state'].selectedRemoteIndex===ctx['remote'].__index); - const b13 = safeOutput(ctx['remote'].name); - c_block11[i1] = withKey(block12([attr5, prop3], [b13]), key1); - } - ctx = ctx.__proto__; - b11 = list(c_block11); - let prop4 = new Boolean(ctx['state'].useExistingSession); - let hdlr6 = [ctx['toggleUseExistingSession'], ctx]; - if (ctx['state'].loginLoading) { - b14 = block14(); - } - if (ctx['currentRemote']) { - const b16 = safeOutput(ctx['currentRemote'].url); - const b17 = safeOutput(ctx['currentRemote'].datasrc||'project.issue'); - b15 = block15([], [b16, b17]); - } - b5 = block5([hdlr1, hdlr5, prop4, hdlr6], [b6, b8, b9, b10, b11, b14, b15]); - } - let attr6 = ctx['state'].view==='main'?'':'hide'; - let prop5 = new String((ctx['state'].searchQuery) === 0 ? 0 : ((ctx['state'].searchQuery) || "")); + let b2, b4; + let attr1 = ctx['state'].activePage==='about'?'selected':'notselected'; + const v1 = ctx['state']; + let hdlr1 = [()=>v1.activePage='about', ctx]; + let attr2 = ctx['state'].activePage==='options'?'selected':'notselected'; + const v2 = ctx['state']; + let hdlr2 = [()=>v2.activePage='options', ctx]; + let attr3 = ctx['state'].activePage==='about'?'active_page':'inactive_page'; + let attr4 = ctx['state'].activePage==='options'?'active_page':'inactive_page'; + let hdlr3 = ["prevent", ctx['addRemote'], ctx]; + let prop1 = new Boolean(ctx['state'].autoDownloadIssueTimesheet); + let hdlr4 = [ctx['toggleAutoDownload'], ctx]; + let prop2 = new String((ctx['state'].form.remote_host) === 0 ? 0 : ((ctx['state'].form.remote_host) || "")); + const v3 = ctx['state']; + let hdlr5 = [(_ev)=>v3.form.remote_host=_ev.target.value, ctx]; + let prop3 = new String((ctx['state'].form.remote_name) === 0 ? 0 : ((ctx['state'].form.remote_name) || "")); const v4 = ctx['state']; - let hdlr7 = [(_ev)=>v4.searchQuery=_ev.target.value, ctx]; - let prop6 = new String((ctx['state'].limitTo) === 0 ? 0 : ((ctx['state'].limitTo) || "")); - const v5 = ctx['updateLimitPreference']; - let hdlr8 = [(_ev)=>v5(_ev.target.value), ctx]; - let prop7 = new Boolean(ctx['state'].autoDownloadIssueTimesheet); - let hdlr9 = [ctx['toggleAutoDownload'], ctx]; - let hdlr10 = [ctx['downloadCurrentMonthTimesheets'], ctx]; - let hdlr11 = [ctx['switchBetweenRemotes'], ctx]; - let hdlr12 = [ctx['refreshAll'], ctx]; - let hdlr13 = [ctx['resetTimer'], ctx]; - let hdlr14 = [ctx['logout'], ctx]; - if (ctx['state'].activeTimerId&&ctx['state'].timerStartIso) { - const b19 = safeOutput(ctx['state'].activeTimerId); - b18 = block18([], [b19]); - } - if (ctx['state'].timerStartIso) { - const b21 = safeOutput(ctx['formattedTimer']); - b20 = block20([], [b21]); - } - b22 = safeOutput(ctx['itemLabelPlural']); - b23 = safeOutput(ctx['filteredIssues'].length); - let prop8 = new Boolean(ctx['state'].allIssues); - const v6 = ctx['updateShowAllPreference']; - let hdlr15 = [(_ev)=>v6(_ev.target.checked), ctx]; - if (ctx['state'].dataSource==='project.task') { - const b25 = block25(); - const b26 = block26(); - b24 = multi([b25, b26]); + let hdlr6 = [(_ev)=>v4.form.remote_name=_ev.target.value, ctx]; + let prop4 = new String((ctx['state'].form.remote_database) === 0 ? 0 : ((ctx['state'].form.remote_database) || "")); + const v5 = ctx['state']; + let hdlr7 = [(_ev)=>v5.form.remote_database=_ev.target.value, ctx]; + let prop5 = new Boolean(ctx['state'].form.remote_datasrc==='project.issue'); + const v6 = ctx['state']; + let hdlr8 = [()=>v6.form.remote_datasrc='project.issue', ctx]; + let prop6 = new Boolean(ctx['state'].form.remote_datasrc==='project.task'); + const v7 = ctx['state']; + let hdlr9 = [()=>v7.form.remote_datasrc='project.task', ctx]; + let hdlr10 = [ctx['addRemote'], ctx]; + let hdlr11 = [ctx['loadRemotes'], ctx]; + const v8 = ctx['state']; + let hdlr12 = [()=>v8.showList=!v8.showList, ctx]; + let hdlr13 = [ctx['removeAllRemotes'], ctx]; + if (ctx['state'].error) { + const b3 = safeOutput(ctx['state'].error); + b2 = block2([], [b3]); } - if (ctx['filteredIssues'].length) { + if (ctx['state'].showList&&ctx['state'].remotes.length) { ctx = Object.create(ctx); - const [k_block27, v_block27, l_block27, c_block27] = prepareList(ctx['filteredIssues']);; - for (let i1 = 0; i1 < l_block27; i1++) { - ctx[`issue`] = k_block27[i1]; - const key1 = ctx['issue'].id; - let b29, b30, b31, b33, b34, b35, b36, b41; - let attr7 = ctx['state'].activeTimerId===ctx['issue'].id?'active-row':''; - if (!ctx['state'].activeTimerId) { - const v7 = ctx['startTimer']; - const v8 = ctx['issue']; - let hdlr16 = [()=>v7(v8), ctx]; - b29 = block29([hdlr16]); - } - if (ctx['state'].activeTimerId===ctx['issue'].id) { - const v9 = ctx['stopTimer']; - const v10 = ctx['issue']; - let hdlr17 = [()=>v9(v10), ctx]; - b30 = block30([hdlr17]); - } - if (ctx['issue'].priority_level.length) { - ctx = Object.create(ctx); - const [k_block31, v_block31, l_block31, c_block31] = prepareList(ctx['issue'].priority_level);; - for (let i2 = 0; i2 < l_block31; i2++) { - ctx[`priority`] = k_block31[i2]; - const key2 = ctx['priority']+'_'+ctx['issue'].id; - c_block31[i2] = withKey(block32(), key2); - } - ctx = ctx.__proto__; - b31 = list(c_block31); - } - if (!ctx['issue'].priority_level.length) { - b33 = block33(); - } - b34 = comp1({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__1__${key1}`, node, this, null); - b35 = comp2({text: ctx['issueLabel'](ctx['issue']),limit: 70,href: ctx['issueHref'](ctx['issue'])}, key + `__2__${key1}`, node, this, null); - if (ctx['state'].dataSource==='project.task') { - const b38 = comp3({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].effective_hours)),limit: 9}, key + `__3__${key1}`, node, this, null); - const b37 = block37([], [b38]); - const b40 = comp4({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].remaining_hours)),limit: 9}, key + `__4__${key1}`, node, this, null); - const b39 = block39([], [b40]); - b36 = multi([b37, b39]); - } - b41 = comp5({text: ctx['relationLabel'](ctx['issue'].project_id),limit: 15}, key + `__5__${key1}`, node, this, null); - c_block27[i1] = withKey(block28([attr7], [b29, b30, b31, b33, b34, b35, b36, b41]), key1); + const [k_block5, v_block5, l_block5, c_block5] = prepareList(ctx['state'].remotes);; + for (let i1 = 0; i1 < l_block5; i1++) { + ctx[`remote`] = k_block5[i1]; + const key1 = ctx['remote'].url+ctx['remote'].database; + const b7 = comp1({text: ctx['remote'].name,limit: 18}, key + `__1__${key1}`, node, this, null); + const b8 = comp2({text: ctx['remote'].url,limit: 25}, key + `__2__${key1}`, node, this, null); + const b9 = comp3({text: ctx['remote'].database,limit: 18}, key + `__3__${key1}`, node, this, null); + const b10 = comp4({text: ctx['remote'].datasrc||'project.issue',limit: 18}, key + `__4__${key1}`, node, this, null); + const b11 = comp5({text: ctx['remote'].state||'Inactive',limit: 18}, key + `__5__${key1}`, node, this, null); + const v9 = ctx['editRemote']; + const v10 = ctx['remote']; + let hdlr14 = [()=>v9(v10), ctx]; + const v11 = ctx['removeRemote']; + const v12 = ctx['remote']; + let hdlr15 = [()=>v11(v12), ctx]; + c_block5[i1] = withKey(block6([hdlr14, hdlr15], [b7, b8, b9, b10, b11]), key1); } ctx = ctx.__proto__; - b27 = list(c_block27); + const b5 = list(c_block5); + b4 = block4([], [b5]); } - if (!ctx['filteredIssues'].length) { - let attr8 = ctx['state'].dataSource==='project.task'?7:5; - b42 = block42([attr8]); - } - b43 = safeOutput(ctx['state'].serverVersion||'Unknown'); - b44 = safeOutput(ctx['state'].currentHost||'-'); - b45 = safeOutput(ctx['state'].currentDatabase||'-'); - b46 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); - return block1([attr1, attr2, attr6, prop5, hdlr7, prop6, hdlr8, prop7, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, prop8, hdlr15], [b2, b4, b5, b18, b20, b22, b23, b24, b27, b42, b43, b44, b45, b46]); + return block1([attr1, hdlr1, attr2, hdlr2, attr3, attr4, hdlr3, prop1, hdlr4, prop2, hdlr5, prop3, hdlr6, prop4, hdlr7, prop5, hdlr8, prop6, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13], [b2, b4]); } }, @@ -198,7 +88,7 @@ export const templates = { const comp4 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp5 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - let block1 = createBlock(`
Loading current session and projects…
Please wait — or grab a cup of coffee ☕
PriorityStage
[]
Project
`); + let block1 = createBlock(`
Loading current session and projects…
Please wait — or grab a cup of coffee ☕
PriorityStage
[]
Project
`); let block2 = createBlock(`

`); let block4 = createBlock(`
Hello 😉, you have not configured any remotes. Open Options below and add one.
`); let block5 = createBlock(`
`); @@ -210,15 +100,15 @@ export const templates = { let block14 = createBlock(``); let block15 = createBlock(`
Host:
`); let block18 = createBlock(``); - let block23 = createBlock(`Hours Spent`); - let block24 = createBlock(`Remaining Hours`); + let block23 = createBlock(`Hours Spent`); + let block24 = createBlock(`Hours Left`); let block26 = createBlock(``); let block27 = createBlock(``); let block28 = createBlock(``); let block30 = createBlock(``); let block31 = createBlock(``); - let block35 = createBlock(``); - let block37 = createBlock(``); + let block35 = createBlock(``); + let block37 = createBlock(``); let block40 = createBlock(` No matching items are currently available `); return function template(ctx, node, key = "") { @@ -293,22 +183,20 @@ export const templates = { let hdlr8 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; const v5 = ctx['updateLimitPreference']; let hdlr9 = [(_ev)=>v5(_ev.target.value), ctx]; - let prop7 = new Boolean(ctx['state'].autoDownloadIssueTimesheet); - let hdlr10 = [ctx['toggleAutoDownload'], ctx]; - let hdlr11 = [ctx['downloadCurrentMonthTimesheets'], ctx]; - let hdlr12 = [ctx['switchBetweenRemotes'], ctx]; - let hdlr13 = [ctx['refreshAll'], ctx]; - let hdlr14 = [ctx['resetTimer'], ctx]; - let hdlr15 = [ctx['logout'], ctx]; + let hdlr10 = [ctx['downloadCurrentMonthTimesheets'], ctx]; + let hdlr11 = [ctx['switchBetweenRemotes'], ctx]; + let hdlr12 = [ctx['refreshAll'], ctx]; + let hdlr13 = [ctx['resetTimer'], ctx]; + let hdlr14 = [ctx['logout'], ctx]; if (ctx['state'].timerStartIso) { const b19 = safeOutput(ctx['formattedTimer']); b18 = block18([], [b19]); } b20 = safeOutput(ctx['itemLabelPlural']); b21 = safeOutput(ctx['filteredIssues'].length); - let prop8 = new Boolean(ctx['state'].allIssues); + let prop7 = new Boolean(ctx['state'].allIssues); const v6 = ctx['updateShowAllPreference']; - let hdlr16 = [(_ev)=>v6(_ev.target.checked), ctx]; + let hdlr15 = [(_ev)=>v6(_ev.target.checked), ctx]; if (ctx['state'].dataSource==='project.task') { const b23 = block23(); const b24 = block24(); @@ -325,14 +213,14 @@ export const templates = { if (!ctx['state'].activeTimerId) { const v7 = ctx['startTimer']; const v8 = ctx['issue']; - let hdlr17 = [()=>v7(v8), ctx]; - b27 = block27([hdlr17]); + let hdlr16 = [()=>v7(v8), ctx]; + b27 = block27([hdlr16]); } if (ctx['state'].activeTimerId===ctx['issue'].id) { const v9 = ctx['stopTimer']; const v10 = ctx['issue']; - let hdlr18 = [()=>v9(v10), ctx]; - b28 = block28([hdlr18]); + let hdlr17 = [()=>v9(v10), ctx]; + b28 = block28([hdlr17]); } if (ctx['issue'].priority_level.length) { ctx = Object.create(ctx); @@ -372,7 +260,7 @@ export const templates = { b43 = safeOutput(ctx['state'].currentHost||'-'); b44 = safeOutput(ctx['state'].currentDatabase||'-'); b45 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); - return block1([attr1, attr2, attr6, prop5, hdlr7, prop6, hdlr8, hdlr9, prop7, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15, prop8, hdlr16], [b2, b4, b5, b18, b20, b21, b22, b25, b40, b41, b42, b43, b44, b45]); + return block1([attr1, attr2, attr6, prop5, hdlr7, prop6, hdlr8, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, prop7, hdlr15], [b2, b4, b5, b18, b20, b21, b22, b25, b40, b41, b42, b43, b44, b45]); } }, diff --git a/dist/firefox/popup.html b/dist/firefox/popup.html index 17c4c28..d31ccf8 100644 --- a/dist/firefox/popup.html +++ b/dist/firefox/popup.html @@ -17,7 +17,7 @@
Loading current session and projects…
-
Please wait — or grab a cup of coffee ☕
+
Please wait or grab a cup of coffee ☕
diff --git a/src/templates/options_app.xml b/src/templates/options_app.xml index 360b0ea..0e8b4e0 100644 --- a/src/templates/options_app.xml +++ b/src/templates/options_app.xml @@ -1,378 +1,259 @@ - -
+ +
+ + + + +
-
-
- Loading current session and projects… +

+ -
- Please wait — or grab a cup of coffee ☕ +

+
+
+

Description

+
+ This is a standalone Owl rewrite of the original cross-platform timer + extension for posting work hours to Odoo timesheets. +
+

Features

+
+
+
    +
  • Support for both tasks and issues
  • +
  • Start and stop the timer for the selected item
  • +
  • Create Odoo timesheet lines against the linked analytic account
  • +
  • Show assigned items or everyone's items
  • +
  • Add, remove, or clear remote hosts
  • +
  • Switch between remote sessions
  • +
  • Download current month or current item timesheets as CSV
  • +
-
+