From 5f379cc7a34467cc89e68bd4e4b84fb5703ac464 Mon Sep 17 00:00:00 2001 From: KKamaa Date: Thu, 23 Apr 2026 04:46:29 +0300 Subject: [PATCH 01/15] [FIX] issues #36,#37,#38 &39 --- dist/chrome/js/components/options-app.js | 19 +- dist/chrome/js/components/popup-app.js | 24 +- dist/chrome/js/templates.js | 263 +++-------- dist/chrome/manifest.json | 3 + dist/firefox/js/components/options-app.js | 19 +- dist/firefox/js/components/popup-app.js | 24 +- dist/firefox/js/templates.js | 263 +++-------- src/templates/options_app.xml | 546 +++++++++------------- src/templates/popup_app.xml | 21 +- 9 files changed, 423 insertions(+), 759 deletions(-) diff --git a/dist/chrome/js/components/options-app.js b/dist/chrome/js/components/options-app.js index 4a9cf31..3156572 100644 --- a/dist/chrome/js/components/options-app.js +++ b/dist/chrome/js/components/options-app.js @@ -51,7 +51,7 @@ 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( @@ -95,6 +95,9 @@ function createOptionsAppTemplate(app, bdom, helpers) { const reloadRemotesHandler = [ctx.loadRemotes, ctx]; const toggleListHandler = [() => { ctx.state.showList = !ctx.state.showList; }, ctx]; const removeAllRemotesHandler = [ctx.removeAllRemotes, ctx]; + // [FIX #38] Auto-download preference + const autoDownloadChecked = ctx.state.autoDownloadIssueTimesheet; + const autoDownloadHandler = [(ev) => { ctx.toggleAutoDownload(ev); }]; if (ctx.state.error) { errorNode = errorBlock([ctx.state.error]); @@ -155,6 +158,8 @@ function createOptionsAppTemplate(app, bdom, helpers) { reloadRemotesHandler, toggleListHandler, removeAllRemotesHandler, + autoDownloadChecked, // 21 [FIX #38] + autoDownloadHandler, // 22 [FIX #38] ], [errorNode, remoteListNode] ); @@ -174,6 +179,7 @@ class OptionsApp extends Component { remotes: [], showList: true, error: '', + autoDownloadIssueTimesheet: false, // [FIX #38] form: { remote_host: '', remote_name: '', @@ -184,6 +190,8 @@ class OptionsApp extends Component { onWillStart(async () => { await this.loadRemotes(); + const saved = await storage.get('auto_download_issue_timesheet', false); + this.state.autoDownloadIssueTimesheet = !!saved; // [FIX #38] }); } @@ -206,6 +214,15 @@ class OptionsApp extends Component { this.state.form.remote_datasrc = DEFAULT_DATA_SOURCE; } + /** + * [FIX #38] 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. * diff --git a/dist/chrome/js/components/popup-app.js b/dist/chrome/js/components/popup-app.js index 64eb316..1fc8fd3 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,17 @@ class PopupApp extends Component { } issueLabel(issue) { - const issueName = this.normalizeText( - issue.display_name || issue.name || issue.message_summary || issue.description || '' - ); - + // [FIX #36] For tasks, use issue.name only — display_name already + // contains the code prefix in Odoo, causing it to appear twice. 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(' - '); } diff --git a/dist/chrome/js/templates.js b/dist/chrome/js/templates.js index a87b1ca..8f38a04 100644 --- a/dist/chrome/js/templates.js +++ b/dist/chrome/js/templates.js @@ -1,190 +1,77 @@ 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['removeRemote']; + const v10 = ctx['remote']; + let hdlr14 = [()=>v9(v10), ctx]; + c_block5[i1] = withKey(block6([hdlr14], [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 +85,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 +97,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 +180,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 +210,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 +257,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..04482f0 100644 --- a/dist/chrome/manifest.json +++ b/dist/chrome/manifest.json @@ -32,6 +32,9 @@ "storage", "cookies" ], + "host_permissions": [ + "" + ], "optional_host_permissions": [ "https://*/*", "http://*/*" diff --git a/dist/firefox/js/components/options-app.js b/dist/firefox/js/components/options-app.js index 4a9cf31..3156572 100644 --- a/dist/firefox/js/components/options-app.js +++ b/dist/firefox/js/components/options-app.js @@ -51,7 +51,7 @@ 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( @@ -95,6 +95,9 @@ function createOptionsAppTemplate(app, bdom, helpers) { const reloadRemotesHandler = [ctx.loadRemotes, ctx]; const toggleListHandler = [() => { ctx.state.showList = !ctx.state.showList; }, ctx]; const removeAllRemotesHandler = [ctx.removeAllRemotes, ctx]; + // [FIX #38] Auto-download preference + const autoDownloadChecked = ctx.state.autoDownloadIssueTimesheet; + const autoDownloadHandler = [(ev) => { ctx.toggleAutoDownload(ev); }]; if (ctx.state.error) { errorNode = errorBlock([ctx.state.error]); @@ -155,6 +158,8 @@ function createOptionsAppTemplate(app, bdom, helpers) { reloadRemotesHandler, toggleListHandler, removeAllRemotesHandler, + autoDownloadChecked, // 21 [FIX #38] + autoDownloadHandler, // 22 [FIX #38] ], [errorNode, remoteListNode] ); @@ -174,6 +179,7 @@ class OptionsApp extends Component { remotes: [], showList: true, error: '', + autoDownloadIssueTimesheet: false, // [FIX #38] form: { remote_host: '', remote_name: '', @@ -184,6 +190,8 @@ class OptionsApp extends Component { onWillStart(async () => { await this.loadRemotes(); + const saved = await storage.get('auto_download_issue_timesheet', false); + this.state.autoDownloadIssueTimesheet = !!saved; // [FIX #38] }); } @@ -206,6 +214,15 @@ class OptionsApp extends Component { this.state.form.remote_datasrc = DEFAULT_DATA_SOURCE; } + /** + * [FIX #38] 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. * diff --git a/dist/firefox/js/components/popup-app.js b/dist/firefox/js/components/popup-app.js index 64eb316..1fc8fd3 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, @@ -845,15 +841,17 @@ class PopupApp extends Component { } issueLabel(issue) { - const issueName = this.normalizeText( - issue.display_name || issue.name || issue.message_summary || issue.description || '' - ); - + // [FIX #36] For tasks, use issue.name only — display_name already + // contains the code prefix in Odoo, causing it to appear twice. 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(' - '); } diff --git a/dist/firefox/js/templates.js b/dist/firefox/js/templates.js index a87b1ca..8f38a04 100644 --- a/dist/firefox/js/templates.js +++ b/dist/firefox/js/templates.js @@ -1,190 +1,77 @@ 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['removeRemote']; + const v10 = ctx['remote']; + let hdlr14 = [()=>v9(v10), ctx]; + c_block5[i1] = withKey(block6([hdlr14], [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 +85,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 +97,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 +180,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 +210,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 +257,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/src/templates/options_app.xml b/src/templates/options_app.xml index 360b0ea..6f04763 100644 --- a/src/templates/options_app.xml +++ b/src/templates/options_app.xml @@ -1,378 +1,252 @@ - -
+ +
+ + + + +
-
-
- 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
  • +
-
+
PriorityStage
[]
`); - 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 block22 = createBlock(``); - let block24 = createBlock(``); - let block29 = createBlock(`Hours Spent`); - let block30 = createBlock(`Hours Left`); - let block32 = createBlock(`Assigned To`); - let block33 = createBlock(`Helpdesk Team`); - let block34 = createBlock(`Description`); - let block35 = createBlock(`Therp link`); - let block36 = createBlock(`Project`); - let block37 = createBlock(`Time Spent`); - let block38 = createBlock(``); - let block41 = createBlock(``); - let block42 = createBlock(``); - let block43 = createBlock(``); - let block45 = createBlock(``); - let block46 = createBlock(``); - let block50 = createBlock(``); - let block52 = createBlock(``); - let block55 = createBlock(``); - let block57 = createBlock(``); - let block59 = createBlock(``); - let block61 = createBlock(``); - let block63 = createBlock(``); - let block65 = createBlock(``); - let block67 = createBlock(``); - let block69 = createBlock(` No matching items are currently available `); + let block1 = createBlock(`
Please wait — or grab a cup of coffee ☕
`); + let block3 = createBlock(`

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

`); + let block9 = createBlock(``); + let block10 = createBlock(``); + let block11 = createBlock(``); + let block13 = createBlock(``); + let block15 = createBlock(``); + let block16 = createBlock(`
Host:
`); + let block19 = createBlock(`
`); + let block21 = createBlock(``); + let block23 = createBlock(``); + let block25 = createBlock(``); + let block28 = createBlock(`Therp Task`); + let block29 = createBlock(`Priority`); + let block30 = createBlock(`
[]
`); + let block33 = createBlock(`Stage`); + let block34 = createBlock(`Assigned To`); + let block35 = createBlock(`Time Spent`); + let block37 = createBlock(`Priority`); + let block38 = createBlock(`Stage`); + let block39 = createBlock(`
[]
`); + let block43 = createBlock(`Hours Spent`); + let block44 = createBlock(`Hours Left`); + let block45 = createBlock(``); + let block48 = createBlock(``); + let block49 = createBlock(``); + let block50 = createBlock(``); + let block52 = createBlock(``); + let block53 = createBlock(``); + let block54 = createBlock(``); + let block56 = createBlock(``); + let block57 = createBlock(``); + let block58 = createBlock(``); + let block60 = createBlock(`
`); + let block62 = createBlock(``); + let block64 = createBlock(``); + let block66 = createBlock(``); + let block69 = createBlock(``); + let block71 = createBlock(``); + let block72 = createBlock(``); + let block73 = createBlock(``); + let block75 = createBlock(``); + let block78 = createBlock(``); + let block80 = createBlock(``); + let block82 = createBlock(``); + let block84 = createBlock(` No matching items are currently available `); return function template(ctx, node, key = "") { - let b2, b4, b5, b18, b20, b24, b26, b27, b28, b31, b38, b40, b69, b70, b71, b72, b73, b74; + let b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b84, b85, b86, b87, b88, b89; let attr1 = ctx['state'].view==='loading'?'':'hide'; + b2 = safeOutput(ctx['state'].busyMessage); let attr2 = ctx['state'].view==='login'?'login-view':'login-view hide'; if (ctx['state'].bootError) { - const b3 = safeOutput(ctx['state'].bootError); - b2 = block2([], [b3]); + const b4 = safeOutput(ctx['state'].bootError); + b3 = block3([], [b4]); } if (!ctx['state'].remotes.length) { - b4 = block4(); + b5 = block5(); } if (ctx['state'].remotes.length) { - let b6, b8, b9, b10, b11, b14, b15; + let b7, b9, b10, b11, b12, b15, b16; let hdlr1 = ["prevent", ctx['login'], ctx]; if (ctx['state'].loginError) { - const b7 = safeOutput(ctx['state'].loginError); - b6 = block6([], [b7]); + const b8 = safeOutput(ctx['state'].loginError); + b7 = block7([], [b8]); } 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]); + b9 = block9([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]); + b10 = block10([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]); + b11 = block11([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 [k_block12, v_block12, l_block12, c_block12] = prepareList(ctx['state'].remotes);; + for (let i1 = 0; i1 < l_block12; i1++) { + ctx[`remote`] = k_block12[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); + const b14 = safeOutput(ctx['remote'].name); + c_block12[i1] = withKey(block13([attr5, prop3], [b14]), key1); } ctx = ctx.__proto__; - b11 = list(c_block11); + b12 = list(c_block12); let prop4 = new Boolean(ctx['state'].useExistingSession); let hdlr6 = [ctx['toggleUseExistingSession'], ctx]; if (ctx['state'].loginLoading) { - b14 = block14(); + b15 = block15(); } if (ctx['currentRemote']) { - const b16 = safeOutput(ctx['currentRemote'].url); - const b17 = safeOutput(ctx['currentRemote'].datasrc||'project.issue'); - b15 = block15([], [b16, b17]); + const b17 = safeOutput(ctx['currentRemote'].url); + const b18 = safeOutput(ctx['currentRemote'].datasrc||'project.issue'); + b16 = block16([], [b17, b18]); } - b5 = block5([hdlr1, hdlr5, prop4, hdlr6], [b6, b8, b9, b10, b11, b14, b15]); + b6 = block6([hdlr1, hdlr5, prop4, hdlr6], [b7, b9, b10, b11, b12, b15, b16]); } let attr6 = ctx['state'].view==='main'?'':'hide'; if (ctx['state'].sourceError) { - const b19 = safeOutput(ctx['state'].sourceError); - b18 = block18([], [b19]); + const b20 = safeOutput(ctx['state'].sourceError); + b19 = block19([], [b20]); } let prop5 = new String((ctx['state'].searchQuery) === 0 ? 0 : ((ctx['state'].searchQuery) || "")); const v4 = ctx['state']; @@ -212,18 +222,18 @@ export const templates = { const v5 = ctx['updateHelpdeskStageFilter']; let hdlr8 = [(_ev)=>v5(_ev.target.value), ctx]; ctx = Object.create(ctx); - const [k_block21, v_block21, l_block21, c_block21] = prepareList(ctx['helpdeskStageOptions']);; - for (let i1 = 0; i1 < l_block21; i1++) { - ctx[`stage`] = k_block21[i1]; + const [k_block22, v_block22, l_block22, c_block22] = prepareList(ctx['helpdeskStageOptions']);; + for (let i1 = 0; i1 < l_block22; i1++) { + ctx[`stage`] = k_block22[i1]; const key1 = ctx['stage'].id; let attr7 = ''+ctx['stage'].id; let prop7 = new Boolean(ctx['state'].helpdeskStageFilter===''+ctx['stage'].id); - const b23 = safeOutput(ctx['stage'].name); - c_block21[i1] = withKey(block22([attr7, prop7], [b23]), key1); + const b24 = safeOutput(ctx['stage'].name); + c_block22[i1] = withKey(block23([attr7, prop7], [b24]), key1); } ctx = ctx.__proto__; - const b21 = list(c_block21); - b20 = block20([prop6, hdlr8], [b21]); + const b22 = list(c_block22); + b21 = block21([prop6, hdlr8], [b22]); } const bExpr1 = ctx['state']; const expr1 = 'limitTo'; @@ -237,133 +247,160 @@ export const templates = { let hdlr14 = [ctx['resetTimer'], ctx]; let hdlr15 = [ctx['logout'], ctx]; if (ctx['state'].timerStartIso) { - const b25 = safeOutput(ctx['formattedTimer']); - b24 = block24([], [b25]); - } - b26 = safeOutput(ctx['itemLabelPlural']); - b27 = safeOutput(ctx['filteredIssues'].length); - let prop9 = new Boolean(ctx['state'].allIssues); - const v7 = ctx['updateShowAllPreference']; - let hdlr16 = [(_ev)=>v7(_ev.target.checked), ctx]; - if (ctx['state'].dataSource==='project.task') { - const b29 = block29(); - const b30 = block30(); - b28 = multi([b29, b30]); + const b26 = safeOutput(ctx['formattedTimer']); + b25 = block25([], [b26]); } if (ctx['isHelpdeskSource']) { - let b32, b33, b34, b35, b36, b37; - if (ctx['showHelpdeskAssignee']) { - b32 = block32(); - } - if (ctx['showHelpdeskTeam']) { - b33 = block33(); - } - if (ctx['showHelpdeskDescription']) { - b34 = block34(); - } + let b28, b29, b30, b33, b34, b35; if (ctx['showHelpdeskTherpLink']) { - b35 = block35(); + b28 = block28(); } - if (ctx['showHelpdeskProject']) { - b36 = block36(); + b29 = block29(); + const b31 = safeOutput(ctx['itemLabelPlural']); + const b32 = safeOutput(ctx['filteredIssues'].length); + let prop9 = new Boolean(ctx['state'].allIssues); + const v7 = ctx['updateShowAllPreference']; + let hdlr16 = [(_ev)=>v7(_ev.target.checked), ctx]; + b30 = block30([prop9, hdlr16], [b31, b32]); + b33 = block33(); + if (ctx['showHelpdeskAssignee']) { + b34 = block34(); } if (ctx['showHelpdeskHours']) { - b37 = block37(); + b35 = block35(); } - b31 = multi([b32, b33, b34, b35, b36, b37]); + b27 = multi([b28, b29, b30, b33, b34, b35]); } else { - const b39 = safeOutput(ctx['relationHeaderLabel']); - b38 = block38([], [b39]); + let b37, b38, b39, b42, b45; + b37 = block37(); + b38 = block38(); + const b40 = safeOutput(ctx['itemLabelPlural']); + const b41 = safeOutput(ctx['filteredIssues'].length); + let prop10 = new Boolean(ctx['state'].allIssues); + const v8 = ctx['updateShowAllPreference']; + let hdlr17 = [(_ev)=>v8(_ev.target.checked), ctx]; + b39 = block39([prop10, hdlr17], [b40, b41]); + if (ctx['state'].dataSource==='project.task') { + const b43 = block43(); + const b44 = block44(); + b42 = multi([b43, b44]); + } + const b46 = safeOutput(ctx['relationHeaderLabel']); + b45 = block45([], [b46]); + b36 = multi([b37, b38, b39, b42, b45]); } if (ctx['filteredIssues'].length) { ctx = Object.create(ctx); - const [k_block40, v_block40, l_block40, c_block40] = prepareList(ctx['filteredIssues']);; - for (let i1 = 0; i1 < l_block40; i1++) { - ctx[`issue`] = k_block40[i1]; + const [k_block47, v_block47, l_block47, c_block47] = prepareList(ctx['filteredIssues']);; + for (let i1 = 0; i1 < l_block47; i1++) { + ctx[`issue`] = k_block47[i1]; const key1 = ctx['issue'].id; - let b42, b43, b44, b46, b47, b48, b49, b54, b67; + let b49, b50, b51, b68; let attr8 = ctx['isActiveTimerItem'](ctx['issue'])?'active-row':''; if (!ctx['state'].activeTimerId) { - const v8 = ctx['startTimer']; - const v9 = ctx['issue']; - let hdlr17 = [()=>v8(v9), ctx]; - b42 = block42([hdlr17]); + const v9 = ctx['startTimer']; + const v10 = ctx['issue']; + let hdlr18 = [()=>v9(v10), ctx]; + b49 = block49([hdlr18]); } if (ctx['isActiveTimerItem'](ctx['issue'])) { - const v10 = ctx['stopTimer']; - const v11 = ctx['issue']; - let hdlr18 = [()=>v10(v11), ctx]; - b43 = block43([hdlr18]); - } - if (ctx['issue'].priority_level.length) { - ctx = Object.create(ctx); - const [k_block44, v_block44, l_block44, c_block44] = prepareList(ctx['issue'].priority_level);; - for (let i2 = 0; i2 < l_block44; i2++) { - ctx[`priority`] = k_block44[i2]; - const key2 = ctx['priority']+'_'+ctx['issue'].id; - c_block44[i2] = withKey(block45(), key2); - } - ctx = ctx.__proto__; - b44 = list(c_block44); - } - if (!ctx['issue'].priority_level.length) { - b46 = block46(); - } - b47 = comp1({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__1__${key1}`, node, this, null); - b48 = 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 b51 = comp3({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].effective_hours)),limit: 9}, key + `__3__${key1}`, node, this, null); - const b50 = block50([], [b51]); - const b53 = comp4({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].remaining_hours)),limit: 9}, key + `__4__${key1}`, node, this, null); - const b52 = block52([], [b53]); - b49 = multi([b50, b52]); + const v11 = ctx['stopTimer']; + const v12 = ctx['issue']; + let hdlr19 = [()=>v11(v12), ctx]; + b50 = block50([hdlr19]); } if (ctx['isHelpdeskSource']) { - let b55, b57, b59, b61, b63, b65; - if (ctx['showHelpdeskAssignee']) { - const b56 = comp5({text: ctx['relationLabel'](ctx['helpdeskAssigneeValue'](ctx['issue'])),limit: 22}, key + `__5__${key1}`, node, this, null); - b55 = block55([], [b56]); + let b52, b54, b58, b62, b64, b66; + if (ctx['showHelpdeskTherpLink']) { + let b53; + if (ctx['helpdeskTherpLinkHref'](ctx['issue'])) { + let attr9 = ctx['helpdeskTherpLinkHref'](ctx['issue']); + b53 = block53([attr9]); + } + b52 = block52([], [b53]); } - if (ctx['showHelpdeskTeam']) { - const b58 = comp6({text: ctx['relationLabel'](ctx['helpdeskTeamValue'](ctx['issue'])),limit: 22}, key + `__6__${key1}`, node, this, null); - b57 = block57([], [b58]); + let b55, b57; + if (ctx['issue'].priority_level.length) { + ctx = Object.create(ctx); + const [k_block55, v_block55, l_block55, c_block55] = prepareList(ctx['issue'].priority_level);; + for (let i2 = 0; i2 < l_block55; i2++) { + ctx[`priority`] = k_block55[i2]; + const key2 = ctx['priority']+'_'+ctx['issue'].id; + c_block55[i2] = withKey(block56(), key2); + } + ctx = ctx.__proto__; + b55 = list(c_block55); } - if (ctx['showHelpdeskDescription']) { - const b60 = comp7({text: ctx['helpdeskDescriptionText'](ctx['issue']),limit: 80}, key + `__7__${key1}`, node, this, null); - b59 = block59([], [b60]); + if (!ctx['issue'].priority_level.length) { + b57 = block57(); } - if (ctx['showHelpdeskTherpLink']) { - const b62 = comp8({text: ctx['helpdeskTherpLinkValue'](ctx['issue']),limit: 45,href: ctx['helpdeskTherpLinkHref'](ctx['issue'])}, key + `__8__${key1}`, node, this, null); - b61 = block61([], [b62]); + b54 = block54([], [b55, b57]); + let b59, b60; + b59 = comp1({text: ctx['issueLabel'](ctx['issue']),limit: 55,href: ctx['issueHref'](ctx['issue'])}, key + `__1__${key1}`, node, this, null); + if (ctx['showHelpdeskDescription']&&ctx['helpdeskDescriptionText'](ctx['issue'])) { + const b61 = comp2({text: ctx['helpdeskDescriptionText'](ctx['issue']),limit: 70}, key + `__2__${key1}`, node, this, null); + b60 = block60([], [b61]); } - if (ctx['showHelpdeskProject']) { - const b64 = comp9({text: ctx['relationLabel'](ctx['helpdeskProjectValue'](ctx['issue'])),limit: 22}, key + `__9__${key1}`, node, this, null); - b63 = block63([], [b64]); + b58 = block58([], [b59, b60]); + const b63 = comp3({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__3__${key1}`, node, this, null); + b62 = block62([], [b63]); + if (ctx['showHelpdeskAssignee']) { + const b65 = comp4({text: ctx['relationLabel'](ctx['helpdeskAssigneeValue'](ctx['issue'])),limit: 22}, key + `__4__${key1}`, node, this, null); + b64 = block64([], [b65]); } if (ctx['showHelpdeskHours']) { - const b66 = comp10({text: ctx['formatHours'](ctx['helpdeskHoursValue'](ctx['issue'])),limit: 10}, key + `__10__${key1}`, node, this, null); - b65 = block65([], [b66]); + const b67 = comp5({text: ctx['formatHours'](ctx['helpdeskHoursValue'](ctx['issue'])),limit: 10}, key + `__5__${key1}`, node, this, null); + b66 = block66([], [b67]); } - b54 = multi([b55, b57, b59, b61, b63, b65]); + b51 = multi([b52, b54, b58, b62, b64, b66]); } else { - const b68 = comp11({text: ctx['relationLabel'](ctx['resourceRelationValue'](ctx['issue'])),limit: 22}, key + `__11__${key1}`, node, this, null); - b67 = block67([], [b68]); + let b69, b73, b75, b77, b82; + let b70, b72; + if (ctx['issue'].priority_level.length) { + ctx = Object.create(ctx); + const [k_block70, v_block70, l_block70, c_block70] = prepareList(ctx['issue'].priority_level);; + for (let i2 = 0; i2 < l_block70; i2++) { + ctx[`priority`] = k_block70[i2]; + const key2 = ctx['priority']+'_'+ctx['issue'].id; + c_block70[i2] = withKey(block71(), key2); + } + ctx = ctx.__proto__; + b70 = list(c_block70); + } + if (!ctx['issue'].priority_level.length) { + b72 = block72(); + } + b69 = block69([], [b70, b72]); + const b74 = comp6({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__6__${key1}`, node, this, null); + b73 = block73([], [b74]); + const b76 = comp7({text: ctx['issueLabel'](ctx['issue']),limit: 70,href: ctx['issueHref'](ctx['issue'])}, key + `__7__${key1}`, node, this, null); + b75 = block75([], [b76]); + if (ctx['state'].dataSource==='project.task') { + const b79 = comp8({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].effective_hours)),limit: 9}, key + `__8__${key1}`, node, this, null); + const b78 = block78([], [b79]); + const b81 = comp9({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].remaining_hours)),limit: 9}, key + `__9__${key1}`, node, this, null); + const b80 = block80([], [b81]); + b77 = multi([b78, b80]); + } + const b83 = comp10({text: ctx['relationLabel'](ctx['resourceRelationValue'](ctx['issue'])),limit: 22}, key + `__10__${key1}`, node, this, null); + b82 = block82([], [b83]); + b68 = multi([b69, b73, b75, b77, b82]); } - c_block40[i1] = withKey(block41([attr8], [b42, b43, b44, b46, b47, b48, b49, b54, b67]), key1); + c_block47[i1] = withKey(block48([attr8], [b49, b50, b51, b68]), key1); } ctx = ctx.__proto__; - b40 = list(c_block40); + b47 = list(c_block47); } if (!ctx['filteredIssues'].length) { - let attr9 = ctx['tableColumnCount']; - b69 = block69([attr9]); + let attr10 = ctx['tableColumnCount']; + b84 = block84([attr10]); } - b70 = safeOutput(ctx['state'].serverVersion||'Unknown'); - b71 = safeOutput(ctx['state'].odooOWLVersion||'Unknown'); - b72 = safeOutput(ctx['state'].currentHost||'-'); - b73 = safeOutput(ctx['state'].currentDatabase||'-'); - b74 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); - return block1([attr1, attr2, attr6, prop5, hdlr7, prop8, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15, prop9, hdlr16], [b2, b4, b5, b18, b20, b24, b26, b27, b28, b31, b38, b40, b69, b70, b71, b72, b73, b74]); + b85 = safeOutput(ctx['state'].serverVersion||'Unknown'); + b86 = safeOutput(ctx['state'].odooOWLVersion||'Unknown'); + b87 = safeOutput(ctx['state'].currentHost||'-'); + b88 = safeOutput(ctx['state'].currentDatabase||'-'); + b89 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); + return block1([attr1, attr2, attr6, prop5, hdlr7, prop8, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15], [b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b84, b85, b86, b87, b88, b89]); } }, diff --git a/dist/firefox/js/components/popup-app.js b/dist/firefox/js/components/popup-app.js index 67f8261..8534301 100644 --- a/dist/firefox/js/components/popup-app.js +++ b/dist/firefox/js/components/popup-app.js @@ -61,7 +61,7 @@ const TIMEOUTS = { const DEFAULTS = { selectedRemoteIndex: '0', searchLimit: '10', - busyMessage: 'Loading current session and projects…', + busyMessage: 'Loading current session…', dataSource: DATA_SOURCE_ISSUE, }; @@ -474,6 +474,8 @@ class PopupApp extends Component { currentHost: '', currentDatabase: '', dataSource: DEFAULTS.dataSource, + currentCompanyId: null, + allowedCompanyIds: [], serverVersion: '', odooOWLVersion: owl.__info__.version, supportedFields: {}, @@ -539,7 +541,10 @@ class PopupApp extends Component { get itemLabelSingular() { return resourceLabels(this.state.dataSource).singular; } get itemLabelPlural() { return resourceLabels(this.state.dataSource).plural; } get isHelpdeskSource() { return this.state.dataSource === DATA_SOURCE_HELPDESK; } - get showHelpdeskAssignee() { return this.isHelpdeskSource && Boolean(this.state.sourceCapabilities.assignmentField); } + get showHelpdeskAssignee() { + return this.isHelpdeskSource && this.state.allIssues && + Boolean(this.state.sourceCapabilities.assignmentField); + } get showHelpdeskTeam() { return this.isHelpdeskSource && Boolean(this.state.sourceCapabilities.teamField); } get showHelpdeskDescription() { return this.isHelpdeskSource && Boolean(this.state.sourceCapabilities.descriptionField); } get showHelpdeskTherpLink() { return this.isHelpdeskSource && Boolean(this.state.sourceCapabilities.therpLinkField); } @@ -573,15 +578,60 @@ class PopupApp extends Component { get tableColumnCount() { if (this.state.dataSource === DATA_SOURCE_TASK) return 7; if (!this.isHelpdeskSource) return 5; + // Compact Helpdesk layout: action, optional Therp link, priority, ticket, + // stage, optional assignee (only in Show for everyone), optional hours. return 4 + - Number(this.showHelpdeskAssignee) + - Number(this.showHelpdeskTeam) + - Number(this.showHelpdeskDescription) + Number(this.showHelpdeskTherpLink) + - Number(this.showHelpdeskProject) + + Number(this.showHelpdeskAssignee) + Number(this.showHelpdeskHours); } + loadingMessage(includeSession = false) { + const noun = this.state.dataSource === DATA_SOURCE_HELPDESK + ? 'Helpdesk tickets' + : this.state.dataSource === DATA_SOURCE_TASK ? 'tasks' : 'issues'; + return includeSession ? `Loading current session and ${noun}…` : `Loading ${noun}…`; + } + + relationId(value) { + if (!value) return null; + if (Array.isArray(value)) return Number(value[0]) || null; + if (typeof value === 'object') return Number(value.id) || null; + return Number(value) || null; + } + + captureSessionCompanies(sessionInfo) { + const contextIds = Array.isArray(sessionInfo?.user_context?.allowed_company_ids) + ? sessionInfo.user_context.allowed_company_ids.map(Number).filter(Boolean) + : []; + const current = Number( + sessionInfo?.user_companies?.current_company || contextIds[0] || 0 + ) || null; + this.state.currentCompanyId = current; + this.state.allowedCompanyIds = contextIds.length ? contextIds : (current ? [current] : []); + } + + helpdeskTimesheetCompanyId(issue) { + // hr_timesheet derives the timesheet company from project_id first, so + // mirror that choice when building the RPC company context. + const projectId = this.relationId(issue?.project_id); + const project = this.state.projects.find((item) => Number(item.id) === Number(projectId)); + const projectCompany = this.relationId(project?.company_id); + if (projectCompany) return projectCompany; + const field = this.state.sourceCapabilities.companyField; + const ticketCompany = this.relationId(field ? issue?.[field] : issue?.company_id); + return ticketCompany || this.state.currentCompanyId || null; + } + + helpdeskTimesheetContext(companyId) { + if (!companyId) return {}; + const allowed = [ + companyId, + ...(this.state.allowedCompanyIds || []).filter((id) => Number(id) !== Number(companyId)), + ]; + return {allowed_company_ids: allowed}; + } + async updateLimitPreference(value) { this.state.limitTo = value; await storage.set(STORAGE_KEYS.searchLimit, value); @@ -693,6 +743,8 @@ class PopupApp extends Component { this.state.user = null; this.state.projects = []; this.state.issues = []; + this.state.currentCompanyId = null; + this.state.allowedCompanyIds = []; this.state.serverVersion = ''; this.state.odooOWLVersion = owl.__info__.version; this.state.supportedFields = {}; @@ -796,6 +848,7 @@ class PopupApp extends Component { ); this.state.limitTo = searchLimit ?? DEFAULTS.searchLimit; this.state.allIssues = !!showAllItems; + this.state.busyMessage = this.loadingMessage(true); } /** @@ -1074,7 +1127,6 @@ class PopupApp extends Component { * to the login screen. */ async completeSession(sessionInfo, remote) { - this.state.busyMessage = 'Loading tasks…'; this.state.loadingTable = true; const remoteInfo = remote || this.currentRemote || null; @@ -1085,6 +1137,8 @@ class PopupApp extends Component { remoteInfo?.url || sessionInfo['web.base.url'] || this.state.currentHost; this.state.dataSource = remoteInfo?.datasrc || this.state.dataSource || DEFAULTS.dataSource; + this.captureSessionCompanies(sessionInfo); + this.state.busyMessage = this.loadingMessage(true); try { await storage.set(this.state.currentDatabase, JSON.stringify(sessionInfo)); @@ -1163,7 +1217,7 @@ class PopupApp extends Component { this.state.projects = []; return; } - const fields = ['name', 'analytic_account_id', 'account_id'].filter((field) => + const fields = ['name', 'analytic_account_id', 'account_id', 'company_id'].filter((field) => Object.prototype.hasOwnProperty.call(projectFields, field) ); const result = await this.rpc.searchRead('project.project', [], fields); @@ -1268,7 +1322,7 @@ class PopupApp extends Component { async loadIssues() { const model = this.state.dataSource; this.state.loadingTable = true; - this.state.busyMessage = 'Loading tasks…'; + this.state.busyMessage = this.loadingMessage(false); try { if (!(await this.inspectSelectedSource())) return; @@ -1316,6 +1370,7 @@ class PopupApp extends Component { 'name', 'user_id', 'project_id', + 'company_id', 'stage_id', 'priority', 'create_date', @@ -1332,6 +1387,7 @@ class PopupApp extends Component { 'partner_id', capabilities.assignmentField, capabilities.projectField, + capabilities.companyField, capabilities.teamField, capabilities.descriptionField, capabilities.therpLinkField, @@ -1516,14 +1572,30 @@ class PopupApp extends Component { const lineFields = capabilities.timesheetFields || {}; const projectId = params.issue.project_id?.[0]; const analyticAccountId = params.analyticAccount?.[0]; + const companyId = this.helpdeskTimesheetCompanyId(params.issue); if (projectId && Object.prototype.hasOwnProperty.call(lineFields, 'project_id')) { values.project_id = projectId; } if (analyticAccountId && Object.prototype.hasOwnProperty.call(lineFields, 'account_id')) { values.account_id = analyticAccountId; } + if (companyId && Object.prototype.hasOwnProperty.call(lineFields, 'company_id')) { + values.company_id = companyId; + } - await this.rpc.call(binding.model, 'create', [values], {}); + try { + await this.rpc.call(binding.model, 'create', [values], { + context: this.helpdeskTimesheetContext(companyId), + }); + } catch (err) { + if (String(err?.message || '').includes('active employee in the selected companies')) { + throw new Error( + 'Odoo could not match your user to an active employee in the ticket/project company. ' + + 'Check the employee company and allowed companies, then reconnect.' + ); + } + throw err; + } } /** @@ -1540,7 +1612,10 @@ class PopupApp extends Component { if (this.isHelpdeskSource && !isHelpdeskTimesheetEnabled(issue, this.state.sourceCapabilities)) { throw new Error('Timesheets are disabled for this ticket team/project in Odoo.'); } - const issueDescription = (await promptDialog(`${this.itemLabelSingular.charAt(0).toUpperCase() + this.itemLabelSingular.slice(1)} #${issue.id} Description`, issue.name)) || ''; + const promptTitle = this.isHelpdeskSource + ? `Timesheet Description for Ticket #${issue.id}` + : `${this.itemLabelSingular.charAt(0).toUpperCase() + this.itemLabelSingular.slice(1)} #${issue.id} Description`; + const issueDescription = (await promptDialog(promptTitle, issue.name)) || ''; const startIso = this.state.timerStartIso || (await storage.get(STORAGE_KEYS.timerStartIso, null)); @@ -1766,6 +1841,8 @@ class PopupApp extends Component { this.state.user = null; this.state.issues = []; this.state.projects = []; + this.state.currentCompanyId = null; + this.state.allowedCompanyIds = []; this.state.view = VIEW_LOGIN; this.state.useExistingSession = true; } diff --git a/dist/firefox/js/lib/resource-adapters.js b/dist/firefox/js/lib/resource-adapters.js index 182f5d8..e593a3a 100644 --- a/dist/firefox/js/lib/resource-adapters.js +++ b/dist/firefox/js/lib/resource-adapters.js @@ -37,6 +37,7 @@ export function inspectHelpdeskCapabilities(ticketFields, timesheetFields) { 'user_ids', ]); const projectField = firstField(ticketFields, ['project_id']); + const companyField = firstField(ticketFields, ['company_id']); const teamField = firstField(ticketFields, ['team_id']); const descriptionField = firstField(ticketFields, ['description']); const therpLinkField = firstFieldByLabel(ticketFields, ['Therp link']) || @@ -55,6 +56,7 @@ export function inspectHelpdeskCapabilities(ticketFields, timesheetFields) { assignmentField, assignmentType: assignmentField ? ticketFields?.[assignmentField]?.type || null : null, projectField, + companyField, teamField, descriptionField, therpLinkField, diff --git a/dist/firefox/js/templates.js b/dist/firefox/js/templates.js index f73b0b5..c0f2a20 100644 --- a/dist/firefox/js/templates.js +++ b/dist/firefox/js/templates.js @@ -85,124 +85,134 @@ export const templates = { ) { let { text, createBlock, list, multi, html, toggler, comment } = bdom; let { safeOutput, prepareList, withKey, toNumber } = helpers; - const comp1 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - const comp2 = app.createComponent(`ReadMore`, true, false, false, ["text","limit","href"]); + const comp1 = 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"]); const comp6 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - const comp7 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - const comp8 = app.createComponent(`ReadMore`, true, false, false, ["text","limit","href"]); + const comp7 = app.createComponent(`ReadMore`, true, false, false, ["text","limit","href"]); + const comp8 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp9 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp10 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - const comp11 = 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
[]
`); - 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 block22 = createBlock(``); - let block24 = createBlock(``); - let block29 = createBlock(`Hours Spent`); - let block30 = createBlock(`Hours Left`); - let block32 = createBlock(`Assigned To`); - let block33 = createBlock(`Helpdesk Team`); - let block34 = createBlock(`Description`); - let block35 = createBlock(`Therp link`); - let block36 = createBlock(`Project`); - let block37 = createBlock(`Time Spent`); - let block38 = createBlock(``); - let block41 = createBlock(``); - let block42 = createBlock(``); - let block43 = createBlock(``); - let block45 = createBlock(``); - let block46 = createBlock(``); - let block50 = createBlock(``); - let block52 = createBlock(``); - let block55 = createBlock(``); - let block57 = createBlock(``); - let block59 = createBlock(``); - let block61 = createBlock(``); - let block63 = createBlock(``); - let block65 = createBlock(``); - let block67 = createBlock(``); - let block69 = createBlock(` No matching items are currently available `); + let block1 = createBlock(`
Please wait — or grab a cup of coffee ☕
`); + let block3 = createBlock(`

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

`); + let block9 = createBlock(``); + let block10 = createBlock(``); + let block11 = createBlock(``); + let block13 = createBlock(``); + let block15 = createBlock(``); + let block16 = createBlock(`
Host:
`); + let block19 = createBlock(`
`); + let block21 = createBlock(``); + let block23 = createBlock(``); + let block25 = createBlock(``); + let block28 = createBlock(`Therp Task`); + let block29 = createBlock(`Priority`); + let block30 = createBlock(`
[]
`); + let block33 = createBlock(`Stage`); + let block34 = createBlock(`Assigned To`); + let block35 = createBlock(`Time Spent`); + let block37 = createBlock(`Priority`); + let block38 = createBlock(`Stage`); + let block39 = createBlock(`
[]
`); + let block43 = createBlock(`Hours Spent`); + let block44 = createBlock(`Hours Left`); + let block45 = createBlock(``); + let block48 = createBlock(``); + let block49 = createBlock(``); + let block50 = createBlock(``); + let block52 = createBlock(``); + let block53 = createBlock(``); + let block54 = createBlock(``); + let block56 = createBlock(``); + let block57 = createBlock(``); + let block58 = createBlock(``); + let block60 = createBlock(`
`); + let block62 = createBlock(``); + let block64 = createBlock(``); + let block66 = createBlock(``); + let block69 = createBlock(``); + let block71 = createBlock(``); + let block72 = createBlock(``); + let block73 = createBlock(``); + let block75 = createBlock(``); + let block78 = createBlock(``); + let block80 = createBlock(``); + let block82 = createBlock(``); + let block84 = createBlock(` No matching items are currently available `); return function template(ctx, node, key = "") { - let b2, b4, b5, b18, b20, b24, b26, b27, b28, b31, b38, b40, b69, b70, b71, b72, b73, b74; + let b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b84, b85, b86, b87, b88, b89; let attr1 = ctx['state'].view==='loading'?'':'hide'; + b2 = safeOutput(ctx['state'].busyMessage); let attr2 = ctx['state'].view==='login'?'login-view':'login-view hide'; if (ctx['state'].bootError) { - const b3 = safeOutput(ctx['state'].bootError); - b2 = block2([], [b3]); + const b4 = safeOutput(ctx['state'].bootError); + b3 = block3([], [b4]); } if (!ctx['state'].remotes.length) { - b4 = block4(); + b5 = block5(); } if (ctx['state'].remotes.length) { - let b6, b8, b9, b10, b11, b14, b15; + let b7, b9, b10, b11, b12, b15, b16; let hdlr1 = ["prevent", ctx['login'], ctx]; if (ctx['state'].loginError) { - const b7 = safeOutput(ctx['state'].loginError); - b6 = block6([], [b7]); + const b8 = safeOutput(ctx['state'].loginError); + b7 = block7([], [b8]); } 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]); + b9 = block9([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]); + b10 = block10([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]); + b11 = block11([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 [k_block12, v_block12, l_block12, c_block12] = prepareList(ctx['state'].remotes);; + for (let i1 = 0; i1 < l_block12; i1++) { + ctx[`remote`] = k_block12[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); + const b14 = safeOutput(ctx['remote'].name); + c_block12[i1] = withKey(block13([attr5, prop3], [b14]), key1); } ctx = ctx.__proto__; - b11 = list(c_block11); + b12 = list(c_block12); let prop4 = new Boolean(ctx['state'].useExistingSession); let hdlr6 = [ctx['toggleUseExistingSession'], ctx]; if (ctx['state'].loginLoading) { - b14 = block14(); + b15 = block15(); } if (ctx['currentRemote']) { - const b16 = safeOutput(ctx['currentRemote'].url); - const b17 = safeOutput(ctx['currentRemote'].datasrc||'project.issue'); - b15 = block15([], [b16, b17]); + const b17 = safeOutput(ctx['currentRemote'].url); + const b18 = safeOutput(ctx['currentRemote'].datasrc||'project.issue'); + b16 = block16([], [b17, b18]); } - b5 = block5([hdlr1, hdlr5, prop4, hdlr6], [b6, b8, b9, b10, b11, b14, b15]); + b6 = block6([hdlr1, hdlr5, prop4, hdlr6], [b7, b9, b10, b11, b12, b15, b16]); } let attr6 = ctx['state'].view==='main'?'':'hide'; if (ctx['state'].sourceError) { - const b19 = safeOutput(ctx['state'].sourceError); - b18 = block18([], [b19]); + const b20 = safeOutput(ctx['state'].sourceError); + b19 = block19([], [b20]); } let prop5 = new String((ctx['state'].searchQuery) === 0 ? 0 : ((ctx['state'].searchQuery) || "")); const v4 = ctx['state']; @@ -212,18 +222,18 @@ export const templates = { const v5 = ctx['updateHelpdeskStageFilter']; let hdlr8 = [(_ev)=>v5(_ev.target.value), ctx]; ctx = Object.create(ctx); - const [k_block21, v_block21, l_block21, c_block21] = prepareList(ctx['helpdeskStageOptions']);; - for (let i1 = 0; i1 < l_block21; i1++) { - ctx[`stage`] = k_block21[i1]; + const [k_block22, v_block22, l_block22, c_block22] = prepareList(ctx['helpdeskStageOptions']);; + for (let i1 = 0; i1 < l_block22; i1++) { + ctx[`stage`] = k_block22[i1]; const key1 = ctx['stage'].id; let attr7 = ''+ctx['stage'].id; let prop7 = new Boolean(ctx['state'].helpdeskStageFilter===''+ctx['stage'].id); - const b23 = safeOutput(ctx['stage'].name); - c_block21[i1] = withKey(block22([attr7, prop7], [b23]), key1); + const b24 = safeOutput(ctx['stage'].name); + c_block22[i1] = withKey(block23([attr7, prop7], [b24]), key1); } ctx = ctx.__proto__; - const b21 = list(c_block21); - b20 = block20([prop6, hdlr8], [b21]); + const b22 = list(c_block22); + b21 = block21([prop6, hdlr8], [b22]); } const bExpr1 = ctx['state']; const expr1 = 'limitTo'; @@ -237,133 +247,160 @@ export const templates = { let hdlr14 = [ctx['resetTimer'], ctx]; let hdlr15 = [ctx['logout'], ctx]; if (ctx['state'].timerStartIso) { - const b25 = safeOutput(ctx['formattedTimer']); - b24 = block24([], [b25]); - } - b26 = safeOutput(ctx['itemLabelPlural']); - b27 = safeOutput(ctx['filteredIssues'].length); - let prop9 = new Boolean(ctx['state'].allIssues); - const v7 = ctx['updateShowAllPreference']; - let hdlr16 = [(_ev)=>v7(_ev.target.checked), ctx]; - if (ctx['state'].dataSource==='project.task') { - const b29 = block29(); - const b30 = block30(); - b28 = multi([b29, b30]); + const b26 = safeOutput(ctx['formattedTimer']); + b25 = block25([], [b26]); } if (ctx['isHelpdeskSource']) { - let b32, b33, b34, b35, b36, b37; - if (ctx['showHelpdeskAssignee']) { - b32 = block32(); - } - if (ctx['showHelpdeskTeam']) { - b33 = block33(); - } - if (ctx['showHelpdeskDescription']) { - b34 = block34(); - } + let b28, b29, b30, b33, b34, b35; if (ctx['showHelpdeskTherpLink']) { - b35 = block35(); + b28 = block28(); } - if (ctx['showHelpdeskProject']) { - b36 = block36(); + b29 = block29(); + const b31 = safeOutput(ctx['itemLabelPlural']); + const b32 = safeOutput(ctx['filteredIssues'].length); + let prop9 = new Boolean(ctx['state'].allIssues); + const v7 = ctx['updateShowAllPreference']; + let hdlr16 = [(_ev)=>v7(_ev.target.checked), ctx]; + b30 = block30([prop9, hdlr16], [b31, b32]); + b33 = block33(); + if (ctx['showHelpdeskAssignee']) { + b34 = block34(); } if (ctx['showHelpdeskHours']) { - b37 = block37(); + b35 = block35(); } - b31 = multi([b32, b33, b34, b35, b36, b37]); + b27 = multi([b28, b29, b30, b33, b34, b35]); } else { - const b39 = safeOutput(ctx['relationHeaderLabel']); - b38 = block38([], [b39]); + let b37, b38, b39, b42, b45; + b37 = block37(); + b38 = block38(); + const b40 = safeOutput(ctx['itemLabelPlural']); + const b41 = safeOutput(ctx['filteredIssues'].length); + let prop10 = new Boolean(ctx['state'].allIssues); + const v8 = ctx['updateShowAllPreference']; + let hdlr17 = [(_ev)=>v8(_ev.target.checked), ctx]; + b39 = block39([prop10, hdlr17], [b40, b41]); + if (ctx['state'].dataSource==='project.task') { + const b43 = block43(); + const b44 = block44(); + b42 = multi([b43, b44]); + } + const b46 = safeOutput(ctx['relationHeaderLabel']); + b45 = block45([], [b46]); + b36 = multi([b37, b38, b39, b42, b45]); } if (ctx['filteredIssues'].length) { ctx = Object.create(ctx); - const [k_block40, v_block40, l_block40, c_block40] = prepareList(ctx['filteredIssues']);; - for (let i1 = 0; i1 < l_block40; i1++) { - ctx[`issue`] = k_block40[i1]; + const [k_block47, v_block47, l_block47, c_block47] = prepareList(ctx['filteredIssues']);; + for (let i1 = 0; i1 < l_block47; i1++) { + ctx[`issue`] = k_block47[i1]; const key1 = ctx['issue'].id; - let b42, b43, b44, b46, b47, b48, b49, b54, b67; + let b49, b50, b51, b68; let attr8 = ctx['isActiveTimerItem'](ctx['issue'])?'active-row':''; if (!ctx['state'].activeTimerId) { - const v8 = ctx['startTimer']; - const v9 = ctx['issue']; - let hdlr17 = [()=>v8(v9), ctx]; - b42 = block42([hdlr17]); + const v9 = ctx['startTimer']; + const v10 = ctx['issue']; + let hdlr18 = [()=>v9(v10), ctx]; + b49 = block49([hdlr18]); } if (ctx['isActiveTimerItem'](ctx['issue'])) { - const v10 = ctx['stopTimer']; - const v11 = ctx['issue']; - let hdlr18 = [()=>v10(v11), ctx]; - b43 = block43([hdlr18]); - } - if (ctx['issue'].priority_level.length) { - ctx = Object.create(ctx); - const [k_block44, v_block44, l_block44, c_block44] = prepareList(ctx['issue'].priority_level);; - for (let i2 = 0; i2 < l_block44; i2++) { - ctx[`priority`] = k_block44[i2]; - const key2 = ctx['priority']+'_'+ctx['issue'].id; - c_block44[i2] = withKey(block45(), key2); - } - ctx = ctx.__proto__; - b44 = list(c_block44); - } - if (!ctx['issue'].priority_level.length) { - b46 = block46(); - } - b47 = comp1({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__1__${key1}`, node, this, null); - b48 = 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 b51 = comp3({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].effective_hours)),limit: 9}, key + `__3__${key1}`, node, this, null); - const b50 = block50([], [b51]); - const b53 = comp4({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].remaining_hours)),limit: 9}, key + `__4__${key1}`, node, this, null); - const b52 = block52([], [b53]); - b49 = multi([b50, b52]); + const v11 = ctx['stopTimer']; + const v12 = ctx['issue']; + let hdlr19 = [()=>v11(v12), ctx]; + b50 = block50([hdlr19]); } if (ctx['isHelpdeskSource']) { - let b55, b57, b59, b61, b63, b65; - if (ctx['showHelpdeskAssignee']) { - const b56 = comp5({text: ctx['relationLabel'](ctx['helpdeskAssigneeValue'](ctx['issue'])),limit: 22}, key + `__5__${key1}`, node, this, null); - b55 = block55([], [b56]); + let b52, b54, b58, b62, b64, b66; + if (ctx['showHelpdeskTherpLink']) { + let b53; + if (ctx['helpdeskTherpLinkHref'](ctx['issue'])) { + let attr9 = ctx['helpdeskTherpLinkHref'](ctx['issue']); + b53 = block53([attr9]); + } + b52 = block52([], [b53]); } - if (ctx['showHelpdeskTeam']) { - const b58 = comp6({text: ctx['relationLabel'](ctx['helpdeskTeamValue'](ctx['issue'])),limit: 22}, key + `__6__${key1}`, node, this, null); - b57 = block57([], [b58]); + let b55, b57; + if (ctx['issue'].priority_level.length) { + ctx = Object.create(ctx); + const [k_block55, v_block55, l_block55, c_block55] = prepareList(ctx['issue'].priority_level);; + for (let i2 = 0; i2 < l_block55; i2++) { + ctx[`priority`] = k_block55[i2]; + const key2 = ctx['priority']+'_'+ctx['issue'].id; + c_block55[i2] = withKey(block56(), key2); + } + ctx = ctx.__proto__; + b55 = list(c_block55); } - if (ctx['showHelpdeskDescription']) { - const b60 = comp7({text: ctx['helpdeskDescriptionText'](ctx['issue']),limit: 80}, key + `__7__${key1}`, node, this, null); - b59 = block59([], [b60]); + if (!ctx['issue'].priority_level.length) { + b57 = block57(); } - if (ctx['showHelpdeskTherpLink']) { - const b62 = comp8({text: ctx['helpdeskTherpLinkValue'](ctx['issue']),limit: 45,href: ctx['helpdeskTherpLinkHref'](ctx['issue'])}, key + `__8__${key1}`, node, this, null); - b61 = block61([], [b62]); + b54 = block54([], [b55, b57]); + let b59, b60; + b59 = comp1({text: ctx['issueLabel'](ctx['issue']),limit: 55,href: ctx['issueHref'](ctx['issue'])}, key + `__1__${key1}`, node, this, null); + if (ctx['showHelpdeskDescription']&&ctx['helpdeskDescriptionText'](ctx['issue'])) { + const b61 = comp2({text: ctx['helpdeskDescriptionText'](ctx['issue']),limit: 70}, key + `__2__${key1}`, node, this, null); + b60 = block60([], [b61]); } - if (ctx['showHelpdeskProject']) { - const b64 = comp9({text: ctx['relationLabel'](ctx['helpdeskProjectValue'](ctx['issue'])),limit: 22}, key + `__9__${key1}`, node, this, null); - b63 = block63([], [b64]); + b58 = block58([], [b59, b60]); + const b63 = comp3({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__3__${key1}`, node, this, null); + b62 = block62([], [b63]); + if (ctx['showHelpdeskAssignee']) { + const b65 = comp4({text: ctx['relationLabel'](ctx['helpdeskAssigneeValue'](ctx['issue'])),limit: 22}, key + `__4__${key1}`, node, this, null); + b64 = block64([], [b65]); } if (ctx['showHelpdeskHours']) { - const b66 = comp10({text: ctx['formatHours'](ctx['helpdeskHoursValue'](ctx['issue'])),limit: 10}, key + `__10__${key1}`, node, this, null); - b65 = block65([], [b66]); + const b67 = comp5({text: ctx['formatHours'](ctx['helpdeskHoursValue'](ctx['issue'])),limit: 10}, key + `__5__${key1}`, node, this, null); + b66 = block66([], [b67]); } - b54 = multi([b55, b57, b59, b61, b63, b65]); + b51 = multi([b52, b54, b58, b62, b64, b66]); } else { - const b68 = comp11({text: ctx['relationLabel'](ctx['resourceRelationValue'](ctx['issue'])),limit: 22}, key + `__11__${key1}`, node, this, null); - b67 = block67([], [b68]); + let b69, b73, b75, b77, b82; + let b70, b72; + if (ctx['issue'].priority_level.length) { + ctx = Object.create(ctx); + const [k_block70, v_block70, l_block70, c_block70] = prepareList(ctx['issue'].priority_level);; + for (let i2 = 0; i2 < l_block70; i2++) { + ctx[`priority`] = k_block70[i2]; + const key2 = ctx['priority']+'_'+ctx['issue'].id; + c_block70[i2] = withKey(block71(), key2); + } + ctx = ctx.__proto__; + b70 = list(c_block70); + } + if (!ctx['issue'].priority_level.length) { + b72 = block72(); + } + b69 = block69([], [b70, b72]); + const b74 = comp6({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__6__${key1}`, node, this, null); + b73 = block73([], [b74]); + const b76 = comp7({text: ctx['issueLabel'](ctx['issue']),limit: 70,href: ctx['issueHref'](ctx['issue'])}, key + `__7__${key1}`, node, this, null); + b75 = block75([], [b76]); + if (ctx['state'].dataSource==='project.task') { + const b79 = comp8({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].effective_hours)),limit: 9}, key + `__8__${key1}`, node, this, null); + const b78 = block78([], [b79]); + const b81 = comp9({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].remaining_hours)),limit: 9}, key + `__9__${key1}`, node, this, null); + const b80 = block80([], [b81]); + b77 = multi([b78, b80]); + } + const b83 = comp10({text: ctx['relationLabel'](ctx['resourceRelationValue'](ctx['issue'])),limit: 22}, key + `__10__${key1}`, node, this, null); + b82 = block82([], [b83]); + b68 = multi([b69, b73, b75, b77, b82]); } - c_block40[i1] = withKey(block41([attr8], [b42, b43, b44, b46, b47, b48, b49, b54, b67]), key1); + c_block47[i1] = withKey(block48([attr8], [b49, b50, b51, b68]), key1); } ctx = ctx.__proto__; - b40 = list(c_block40); + b47 = list(c_block47); } if (!ctx['filteredIssues'].length) { - let attr9 = ctx['tableColumnCount']; - b69 = block69([attr9]); + let attr10 = ctx['tableColumnCount']; + b84 = block84([attr10]); } - b70 = safeOutput(ctx['state'].serverVersion||'Unknown'); - b71 = safeOutput(ctx['state'].odooOWLVersion||'Unknown'); - b72 = safeOutput(ctx['state'].currentHost||'-'); - b73 = safeOutput(ctx['state'].currentDatabase||'-'); - b74 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); - return block1([attr1, attr2, attr6, prop5, hdlr7, prop8, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15, prop9, hdlr16], [b2, b4, b5, b18, b20, b24, b26, b27, b28, b31, b38, b40, b69, b70, b71, b72, b73, b74]); + b85 = safeOutput(ctx['state'].serverVersion||'Unknown'); + b86 = safeOutput(ctx['state'].odooOWLVersion||'Unknown'); + b87 = safeOutput(ctx['state'].currentHost||'-'); + b88 = safeOutput(ctx['state'].currentDatabase||'-'); + b89 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); + return block1([attr1, attr2, attr6, prop5, hdlr7, prop8, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15], [b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b84, b85, b86, b87, b88, b89]); } }, diff --git a/src/templates/popup_app.xml b/src/templates/popup_app.xml index 7bfe891..6e7b539 100644 --- a/src/templates/popup_app.xml +++ b/src/templates/popup_app.xml @@ -6,9 +6,7 @@ t-att-class="state.view === 'loading' ? '' : 'hide'" >
-
- Loading current session and projects… -
+
Please wait — or grab a cup of coffee ☕
@@ -230,44 +228,60 @@
- - Priority - Stage - - -
- [] -
- - - - - Hours Spent - Hours Left - - - Assigned To - Helpdesk Team - Description - Therp link - Project - Time Spent + + Therp Task + + Priority + +
+ [] +
+ + + Stage + + Assigned To + + + Time Spent +
+ + Priority + Stage + +
+ [] +
+ + + + Hours Spent + Hours Left +
@@ -275,14 +289,8 @@ - - + + - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + + + - - + + +
+ +
+
- - + + + -
- - - - - - - - - - - - - - - - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -388,10 +409,7 @@ - + No matching items are currently available diff --git a/tests/resource-adapters.test.mjs b/tests/resource-adapters.test.mjs index c9d55e9..93acb4c 100644 --- a/tests/resource-adapters.test.mjs +++ b/tests/resource-adapters.test.mjs @@ -13,6 +13,7 @@ import { const enterpriseTicketFields = { user_id: { type: 'many2one', relation: 'res.users' }, project_id: { type: 'many2one', relation: 'project.project' }, + company_id: { type: 'many2one', relation: 'res.company' }, team_id: { type: 'many2one', relation: 'helpdesk.team' }, description: { type: 'html', string: 'Description' }, x_therp_url: { type: 'char', string: 'Therp link' }, From a40f149c17db56370bebdc7fe767cab00390e299 Mon Sep 17 00:00:00 2001 From: KKamaa Date: Thu, 3 Sep 2026 07:50:41 +0300 Subject: [PATCH 08/15] [FIX] helpdesk flow, create/edit host instances, logo --- dist/chrome/js/components/options-app.js | 125 ++++++++++-- dist/chrome/js/components/popup-app.js | 217 ++++++++++++++++++-- dist/chrome/js/lib/common.js | 16 ++ dist/chrome/js/templates.js | 232 ++++++++++++---------- dist/firefox/js/components/options-app.js | 125 ++++++++++-- dist/firefox/js/components/popup-app.js | 217 ++++++++++++++++++-- dist/firefox/js/lib/common.js | 16 ++ dist/firefox/js/templates.js | 232 ++++++++++++---------- src/templates/options_app.xml | 24 ++- src/templates/popup_app.xml | 25 ++- tests/resource-adapters.test.mjs | 18 ++ 11 files changed, 942 insertions(+), 305 deletions(-) diff --git a/dist/chrome/js/components/options-app.js b/dist/chrome/js/components/options-app.js index 9c72c27..c7f7581 100644 --- a/dist/chrome/js/components/options-app.js +++ b/dist/chrome/js/components/options-app.js @@ -3,6 +3,7 @@ import { writeRemotes, validURL, normalizeHost, + remoteIdentity, storage, clearOdooSessionCookies, notify, @@ -15,6 +16,9 @@ const { Component, mount, useState, onWillStart } = owl; const PAGE_ABOUT = 'about'; const PAGE_OPTIONS = 'options'; const DEFAULT_DATA_SOURCE = 'project.issue'; +const DEFAULT_LOGO_SRC = '/img/logo.png'; +const MAX_REMOTE_LOGO_BYTES = 512 * 1024; +const ALLOWED_REMOTE_LOGO_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']); const STORAGE_KEYS = { remoteHostInfo: 'remote_host_info', @@ -119,7 +123,7 @@ function createOptionsAppTemplate(app, bdom, helpers) { for (let i = 0; i < remoteCount; i++) { ctx.remote = remoteItems[i]; - const remoteKey = ctx.remote.url + ctx.remote.database; + const remoteKey = ctx.remoteKey(ctx.remote); if (seenRemoteKeys.has(String(remoteKey))) { throw new OwlError(`Got duplicate key in t-foreach: ${remoteKey}`); @@ -198,6 +202,7 @@ class OptionsApp extends Component { remote_name: '', remote_database: '', remote_datasrc: DEFAULT_DATA_SOURCE, + remote_logo: '', }, }); @@ -217,6 +222,50 @@ class OptionsApp extends Component { this.state.remotes = await readRemotes(); } + + remoteKey(remote) { return remoteIdentity(remote); } + remoteLogoSrc(remote) { return remote?.logoDataUrl || DEFAULT_LOGO_SRC; } + get formLogoSrc() { return this.state.form.remote_logo || DEFAULT_LOGO_SRC; } + + async readLogoFile(file) { + if (!file) return ''; + if (!ALLOWED_REMOTE_LOGO_TYPES.has(file.type)) { + throw new Error('Company logo must be PNG, JPEG, WebP, or GIF.'); + } + if (file.size > MAX_REMOTE_LOGO_BYTES) { + throw new Error('Company logo is too large. Maximum size is 512 KB.'); + } + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result || '')); + reader.onerror = () => reject(new Error('Could not read the selected company logo.')); + reader.readAsDataURL(file); + }); + } + + async onRemoteLogoChange(ev) { + const input = ev?.target; + const file = input?.files?.[0]; + if (!file) return; + try { + this.state.form.remote_logo = await this.readLogoFile(file); + } catch (err) { + if (input) input.value = ''; + await notify(err.message || 'Could not use the selected company logo.'); + } + } + + clearRemoteLogo() { + this.state.form.remote_logo = ''; + const input = document.getElementById('remote-logo'); + if (input) input.value = ''; + } + + duplicateRemoteMessage(remote) { + return `A remote for ${remote.url} / ${remote.database} / ${remote.datasrc || DEFAULT_DATA_SOURCE} already exists. ` + + 'Change the host, database, or data source.'; + } + /** * Reset the add-remote form back to defaults. */ @@ -225,6 +274,7 @@ class OptionsApp extends Component { this.state.form.remote_name = ''; this.state.form.remote_database = ''; this.state.form.remote_datasrc = DEFAULT_DATA_SOURCE; + this.state.form.remote_logo = ''; } /** @@ -276,8 +326,9 @@ class OptionsApp extends Component { const { host, name, database, datasrc } = validated; const remotes = await readRemotes(); - if (remotes.some((remote) => remote.url === host && remote.database === database)) { - this.state.error = `${host} and ${database} already exist; duplicates are not allowed`; + const candidate = {url: host, database, datasrc}; + if (remotes.some((remote) => remoteIdentity(remote) === remoteIdentity(candidate))) { + await notify(this.duplicateRemoteMessage(candidate)); return; } @@ -286,6 +337,7 @@ class OptionsApp extends Component { name, database, datasrc, + logoDataUrl: this.state.form.remote_logo || '', state: 'Inactive', }); @@ -307,13 +359,18 @@ class OptionsApp extends Component { return; } - await clearOdooSessionCookies(remote.url); + const targetKey = remoteIdentity(remote); const remotes = (await readRemotes()).filter( - (currentRemote) => !(currentRemote.url === remote.url && currentRemote.database === remote.database) + (currentRemote) => remoteIdentity(currentRemote) !== targetKey ); await writeRemotes(remotes); - await storage.remove(remote.database); + if (!remotes.some((item) => normalizeHost(item.url || '') === normalizeHost(remote.url || ''))) { + await clearOdooSessionCookies(remote.url); + } + if (!remotes.some((item) => String(item.database || '').trim() === String(remote.database || '').trim())) { + await storage.remove(remote.database); + } await this.loadRemotes(); await notify(`[${remote.url}] removed successfully!`); } @@ -340,6 +397,7 @@ class OptionsApp extends Component { let name = currentName; let database = currentDatabase; let datasrc = currentDatasource; + let logoDataUrl = remote.logoDataUrl || ''; if (customAlert) { const inputSuffix = `${Date.now()}-${Math.random().toString(36).slice(2)}`; @@ -371,6 +429,17 @@ class OptionsApp extends Component { +
+ Remote logo +
+
Company Logo (optional)
+ + +
PNG, JPEG, WebP or GIF; maximum 512 KB.
+
+
`; @@ -384,11 +453,19 @@ class OptionsApp extends Component { const nameEl = document.getElementById(nameId); const databaseEl = document.getElementById(databaseId); const datasourceEl = document.getElementById(datasourceId); + const logoEl = document.getElementById(`edit-remote-logo-${inputSuffix}`); + const removeLogoEl = document.getElementById(`edit-remote-remove-logo-${inputSuffix}`); host = normalizeHost(hostEl ? hostEl.value : currentHost); name = ((nameEl ? nameEl.value : currentName) || '').trim(); database = ((databaseEl ? databaseEl.value : currentDatabase) || '').trim(); datasrc = datasourceEl ? datasourceEl.value : currentDatasource; + if (removeLogoEl?.checked) { + logoDataUrl = ''; + } else if (logoEl?.files?.[0]) { + try { logoDataUrl = await this.readLogoFile(logoEl.files[0]); } + catch (err) { await notify(err.message || 'Could not use the selected company logo.'); return; } + } } else { return; } @@ -404,39 +481,41 @@ class OptionsApp extends Component { } const remotes = await readRemotes(); - const duplicate = remotes.find( - (item) => !(item.url === currentHost && item.database === currentDatabase) - && item.url === host - && item.database === database + const originalKey = remoteIdentity(remote); + const candidate = {url: host, database, datasrc}; + const duplicate = remotes.some( + (item) => remoteIdentity(item) !== originalKey && remoteIdentity(item) === remoteIdentity(candidate) ); if (duplicate) { - this.state.error = `${host} and ${database} already exist; duplicates are not allowed`; + await notify(this.duplicateRemoteMessage(candidate)); return; } - const index = remotes.findIndex( - (item) => item.url === currentHost && item.database === currentDatabase - ); + const index = remotes.findIndex((item) => remoteIdentity(item) === originalKey); if (index === -1) { - this.state.error = 'Remote not found'; + await notify('Remote not found. Refresh the list and try again.'); return; } + const oldHost = normalizeHost(remotes[index].url || ''); + const oldDb = String(remotes[index].database || '').trim(); + remotes[index] = { ...remotes[index], url: host, name, database, datasrc, + logoDataUrl, }; await writeRemotes(remotes); - if (host !== currentHost) { - await clearOdooSessionCookies(currentHost); + if (oldHost !== host && !remotes.some((item) => normalizeHost(item.url || '') === oldHost)) { + await clearOdooSessionCookies(oldHost); } - if (database !== currentDatabase) { - await storage.remove(currentDatabase); + if (oldDb !== database && !remotes.some((item) => String(item.database || '').trim() === oldDb)) { + await storage.remove(oldDb); } await this.loadRemotes(); @@ -557,9 +636,11 @@ class OptionsApp extends Component { } const remotes = await readRemotes(); - for (const remote of remotes) { - await clearOdooSessionCookies(remote.url); - await storage.remove(remote.database); + for (const host of [...new Set(remotes.map((remote) => normalizeHost(remote.url || '')).filter(Boolean))]) { + await clearOdooSessionCookies(host); + } + for (const database of [...new Set(remotes.map((remote) => String(remote.database || '').trim()).filter(Boolean))]) { + await storage.remove(database); } await writeRemotes([]); diff --git a/dist/chrome/js/components/popup-app.js b/dist/chrome/js/components/popup-app.js index 8534301..088a419 100644 --- a/dist/chrome/js/components/popup-app.js +++ b/dist/chrome/js/components/popup-app.js @@ -12,6 +12,7 @@ import { priorityStars, matchesIssue, extractMessageSummary, + remoteIdentity, notify, confirmDialog, promptDialog, @@ -212,14 +213,14 @@ function createPopupAppTemplate(app, bdom, helpers) { for (let i = 0; i < remoteCount; i++) { ctx.remote = remoteItems[i]; - const remoteKey = ctx.remote.database + ctx.remote.url; + const remoteKey = ctx.remoteKey(ctx.remote); if (seenRemoteKeys.has(String(remoteKey))) { throw new OwlError(`Got duplicate key in t-foreach: ${remoteKey}`); } seenRemoteKeys.add(String(remoteKey)); const optionValue = String(ctx.remote.__index); const optionSelected = selectedRemoteIndex === String(ctx.remote.__index); - const optionText = ctx.remote.name; + const optionText = ctx.remoteOptionLabel(ctx.remote); remoteChildren[i] = withKey(remoteOptionBlock([optionValue, optionSelected, optionText]), remoteKey); } @@ -476,6 +477,9 @@ class PopupApp extends Component { dataSource: DEFAULTS.dataSource, currentCompanyId: null, allowedCompanyIds: [], + availableCompanyIds: [], + currentUserId: null, + companyNames: {}, serverVersion: '', odooOWLVersion: owl.__info__.version, supportedFields: {}, @@ -528,6 +532,17 @@ class PopupApp extends Component { return this.state.remotes[idx] || null; } + + remoteKey(remote) { return remoteIdentity(remote); } + remoteOptionLabel(remote) { + const source = remote?.datasrc || DATA_SOURCE_ISSUE; + const sourceLabel = source === DATA_SOURCE_HELPDESK + ? 'Helpdesk Tickets' + : source === DATA_SOURCE_TASK ? 'Tasks' : 'Issues'; + return `${remote?.name || remote?.database || 'Remote'} — ${sourceLabel}`; + } + get currentRemoteLogoSrc() { return this.currentRemote?.logoDataUrl || '/img/logo.png'; } + /** * Formatted active timer duration. */ @@ -604,11 +619,34 @@ class PopupApp extends Component { const contextIds = Array.isArray(sessionInfo?.user_context?.allowed_company_ids) ? sessionInfo.user_context.allowed_company_ids.map(Number).filter(Boolean) : []; + const rawAllowed = sessionInfo?.user_companies?.allowed_companies; + let availableIds = []; + const companyNames = {}; + if (Array.isArray(rawAllowed)) { + for (const item of rawAllowed) { + const id = Number(typeof item === 'object' ? item?.id : item); + if (!id) continue; + availableIds.push(id); + if (typeof item === 'object' && item?.name) companyNames[id] = String(item.name); + } + } else if (rawAllowed && typeof rawAllowed === 'object') { + availableIds = Object.keys(rawAllowed).map(Number).filter(Boolean); + for (const [id, item] of Object.entries(rawAllowed)) { + if (item?.name) companyNames[Number(id)] = String(item.name); + } + } const current = Number( - sessionInfo?.user_companies?.current_company || contextIds[0] || 0 + sessionInfo?.user_companies?.current_company || contextIds[0] || availableIds[0] || 0 ) || null; + this.state.currentUserId = Number(sessionInfo?.uid || 0) || null; this.state.currentCompanyId = current; - this.state.allowedCompanyIds = contextIds.length ? contextIds : (current ? [current] : []); + this.state.availableCompanyIds = availableIds.length + ? [...new Set(availableIds)] + : [...new Set(contextIds.length ? contextIds : (current ? [current] : []))]; + this.state.allowedCompanyIds = contextIds.length + ? [...new Set(contextIds)] + : [...this.state.availableCompanyIds]; + this.state.companyNames = companyNames; } helpdeskTimesheetCompanyId(issue) { @@ -632,6 +670,130 @@ class PopupApp extends Component { return {allowed_company_ids: allowed}; } + + helpdeskCompanyLabel(companyId) { + if (!companyId) return 'the ticket/project company'; + return this.state.companyNames?.[companyId] || `company #${companyId}`; + } + + helpdeskTimesheetBlocked(issue) { + return this.isHelpdeskSource && issue?.__timesheetWritable === false; + } + + helpdeskTimesheetBlockMessage(issue) { + return issue?.__timesheetBlockMessage || 'Odoo is not currently able to create a timesheet for this ticket.'; + } + + async showHelpdeskTimesheetInfo(issue) { + await notify(this.helpdeskTimesheetBlockMessage(issue)); + } + + staticHelpdeskTimesheetReadiness(issue) { + if (!this.state.sourceCapabilities.canRecordTime) { + return {writable: false, message: helpdeskTimesheetUnavailableMessage()}; + } + if (!isHelpdeskTimesheetEnabled(issue, this.state.sourceCapabilities)) { + return { + writable: false, + message: 'Timesheets are disabled for this ticket team/project. Enable Helpdesk Timesheets in Odoo before starting the timer.', + }; + } + const projectId = this.relationId(issue?.project_id); + const analyticAccount = this.resolveAnalyticAccount(issue); + if (!projectId && !analyticAccount) { + return { + writable: false, + message: 'This Helpdesk ticket has no timesheet project or analytic account. Configure its Helpdesk team/project in Odoo before starting the timer.', + }; + } + const companyId = this.helpdeskTimesheetCompanyId(issue); + const accessible = this.state.availableCompanyIds || []; + if (companyId && accessible.length && !accessible.map(Number).includes(Number(companyId))) { + return { + writable: false, + message: `The ticket/project belongs to ${this.helpdeskCompanyLabel(companyId)}, which is not an allowed company for this Odoo user.`, + }; + } + return {writable: null, message: '', companyId}; + } + + async annotateHelpdeskTimesheetReadiness(records) { + if (!this.isHelpdeskSource) return records; + const annotated = records.map((issue) => { + const check = this.staticHelpdeskTimesheetReadiness(issue); + return { + ...issue, + __timesheetWritable: check.writable, + __timesheetBlockMessage: check.message || '', + __timesheetCompanyId: check.companyId || this.helpdeskTimesheetCompanyId(issue), + }; + }); + + const pending = annotated.filter((issue) => issue.__timesheetWritable === null); + if (!pending.length || !this.state.currentUserId) return annotated; + + const companyIds = [...new Set(pending.map((issue) => Number(issue.__timesheetCompanyId || 0)).filter(Boolean))]; + const domain = [ + ['user_id', '=', this.state.currentUserId], + ['active', '=', true], + ]; + if (companyIds.length) domain.push(['company_id', 'in', companyIds]); + else if (this.state.allowedCompanyIds?.length) domain.push(['company_id', 'in', this.state.allowedCompanyIds]); + + let employees; + try { + const contextIds = companyIds.length + ? [...new Set([...companyIds, ...(this.state.allowedCompanyIds || [])])] + : (this.state.allowedCompanyIds || []); + const result = await this.rpc.searchRead('hr.employee', domain, ['id', 'company_id'], { + limit: 100, + context: contextIds.length ? {allowed_company_ids: contextIds} : {}, + }); + employees = result.records || []; + } catch (err) { + // Some users cannot read hr.employee even though account.analytic.line + // create() can resolve the employee with sudo. In that case the check + // is inconclusive, so do not disable a timer that Odoo may accept. + console.warn('Helpdesk timesheet employee preflight could not be verified', err); + return annotated; + } + + const employeeCompanies = new Set( + employees.map((employee) => this.relationId(employee.company_id)).filter(Boolean) + ); + for (const issue of annotated) { + if (issue.__timesheetWritable !== null) continue; + const companyId = Number(issue.__timesheetCompanyId || 0) || null; + if (companyId) { + if (employeeCompanies.has(companyId)) { + issue.__timesheetWritable = true; + } else { + issue.__timesheetWritable = false; + issue.__timesheetBlockMessage = + `Odoo cannot create this timesheet because your user has no active employee in ${this.helpdeskCompanyLabel(companyId)}. ` + + 'Check the employee company and allowed companies, then reconnect.'; + } + } else if (!employees.length) { + issue.__timesheetWritable = false; + issue.__timesheetBlockMessage = + 'Odoo cannot create this timesheet because your user has no active employee in the selected companies. ' + + 'Check the employee company and allowed companies, then reconnect.'; + } else if (employees.length === 1) { + issue.__timesheetWritable = true; + } + } + return annotated; + } + + async refreshHelpdeskTimesheetReadiness(issue) { + if (!this.isHelpdeskSource) return issue; + const [checked] = await this.annotateHelpdeskTimesheetReadiness([{...issue}]); + if (!checked) return issue; + const index = this.state.issues.findIndex((item) => Number(item.id) === Number(issue.id)); + if (index >= 0) this.state.issues[index] = {...this.state.issues[index], ...checked}; + return checked; + } + async updateLimitPreference(value) { this.state.limitTo = value; await storage.set(STORAGE_KEYS.searchLimit, value); @@ -745,6 +907,9 @@ class PopupApp extends Component { this.state.issues = []; this.state.currentCompanyId = null; this.state.allowedCompanyIds = []; + this.state.availableCompanyIds = []; + this.state.currentUserId = null; + this.state.companyNames = {}; this.state.serverVersion = ''; this.state.odooOWLVersion = owl.__info__.version; this.state.supportedFields = {}; @@ -877,11 +1042,19 @@ class PopupApp extends Component { this.rpc.setHost(this.state.currentHost); - const remoteIndex = this.state.remotes.findIndex( - (remote) => - remote.url === this.state.currentHost && - remote.database === this.state.currentDatabase + const storedRemoteKey = remoteIdentity({ + url: this.state.currentHost, + database: this.state.currentDatabase, + datasrc: this.state.dataSource, + }); + let remoteIndex = this.state.remotes.findIndex( + (remote) => remoteIdentity(remote) === storedRemoteKey ); + if (remoteIndex < 0) { + remoteIndex = this.state.remotes.findIndex( + (remote) => remote.url === this.state.currentHost && remote.database === this.state.currentDatabase + ); + } if (remoteIndex >= 0) { this.state.selectedRemoteIndex = String(remoteIndex); @@ -1184,8 +1357,10 @@ class PopupApp extends Component { const [userResult, serverInfo] = await Promise.all([ userPromise, serverInfoPromise, - this.loadProjects().catch((err) => console.warn('loadProjects failed', err)), - this.loadIssues().catch((err) => console.warn('loadIssues failed', err)), + (async () => { + await this.loadProjects().catch((err) => console.warn('loadProjects failed', err)); + await this.loadIssues().catch((err) => console.warn('loadIssues failed', err)); + })(), ]); this.state.user = userResult.records?.[0] || { @@ -1436,7 +1611,7 @@ class PopupApp extends Component { } } - this.state.issues = records.map((issue) => ({ + let preparedRecords = records.map((issue) => ({ ...issue, message_summary: extractMessageSummary( issue.message_summary || issue.description || issue.display_name || issue.name || '' @@ -1444,6 +1619,10 @@ class PopupApp extends Component { priority_level: priorityStars(issue.priority), stage_sequence: stageSequenceById.get(issue.stage_id?.[0]) ?? 9999, })); + if (model === DATA_SOURCE_HELPDESK) { + preparedRecords = await this.annotateHelpdeskTimesheetReadiness(preparedRecords); + } + this.state.issues = preparedRecords; } catch (err) { if (model === DATA_SOURCE_HELPDESK) { this.state.issues = []; @@ -1476,13 +1655,12 @@ class PopupApp extends Component { await notify(`Stop the active ${this.itemLabelSingular} timer before starting another one.`); return; } - if (this.isHelpdeskSource && !this.state.sourceCapabilities.canRecordTime) { - await notify(helpdeskTimesheetUnavailableMessage()); - return; - } - if (this.isHelpdeskSource && !isHelpdeskTimesheetEnabled(issue, this.state.sourceCapabilities)) { - await notify('Timesheets are disabled for this ticket team/project. Enable Helpdesk Timesheets in Odoo before starting the timer.'); - return; + if (this.isHelpdeskSource) { + const checkedIssue = await this.refreshHelpdeskTimesheetReadiness(issue); + if (this.helpdeskTimesheetBlocked(checkedIssue)) { + await this.showHelpdeskTimesheetInfo(checkedIssue); + return; + } } const now = new Date().toISOString(); @@ -1843,6 +2021,9 @@ class PopupApp extends Component { this.state.projects = []; this.state.currentCompanyId = null; this.state.allowedCompanyIds = []; + this.state.availableCompanyIds = []; + this.state.currentUserId = null; + this.state.companyNames = {}; this.state.view = VIEW_LOGIN; this.state.useExistingSession = true; } diff --git a/dist/chrome/js/lib/common.js b/dist/chrome/js/lib/common.js index 2b70962..e13c320 100644 --- a/dist/chrome/js/lib/common.js +++ b/dist/chrome/js/lib/common.js @@ -136,6 +136,22 @@ export function normalizeHost(host) { return out.replace(/\/$/, ''); } +/** + * Return the functional identity of a saved remote. + * + * A single Odoo database can legitimately be configured more than once when + * each entry targets a different timer resource (for example Tasks and + * Helpdesk Tickets). Display name, logo and UI state are intentionally not + * part of this identity. + */ +export function remoteIdentity(remote) { + const host = normalizeHost(remote?.url || ''); + const database = String(remote?.database || '').trim(); + const dataSource = String(remote?.datasrc || 'project.issue').trim() || 'project.issue'; + return JSON.stringify([host, database, dataSource]); +} + + export function toCSV(rows) { if (!rows || !rows.length) return ''; const headers = Array.from(new Set(rows.flatMap((r) => Object.keys(r)))); diff --git a/dist/chrome/js/templates.js b/dist/chrome/js/templates.js index c0f2a20..de42e91 100644 --- a/dist/chrome/js/templates.js +++ b/dist/chrome/js/templates.js @@ -9,10 +9,10 @@ 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(`


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 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


Remote logo preview
PNG, JPEG, WebP or GIF; maximum 512 KB.
Controls
`); let block2 = createBlock(`
`); - let block4 = createBlock(`
List of Available Remotes
RemoteHostDatabaseSourceState
`); - let block6 = createBlock(``); + let block4 = createBlock(`
List of Available Remotes
LogoRemoteHostDatabaseSourceState
`); + let block6 = createBlock(`Remote logo`); return function template(ctx, node, key = "") { let b2, b4; @@ -45,11 +45,14 @@ export const templates = { let prop7 = new Boolean(ctx['state'].form.remote_datasrc==='helpdesk.ticket'); const v8 = ctx['state']; let hdlr10 = [()=>v8.form.remote_datasrc='helpdesk.ticket', ctx]; - let hdlr11 = [ctx['addRemote'], ctx]; - let hdlr12 = [ctx['loadRemotes'], ctx]; + let attr5 = ctx['formLogoSrc']; + let hdlr11 = [ctx['onRemoteLogoChange'], ctx]; + let hdlr12 = [ctx['clearRemoteLogo'], ctx]; + let hdlr13 = [ctx['addRemote'], ctx]; + let hdlr14 = [ctx['loadRemotes'], ctx]; const v9 = ctx['state']; - let hdlr13 = [()=>v9.showList=!v9.showList, ctx]; - let hdlr14 = [ctx['removeAllRemotes'], ctx]; + let hdlr15 = [()=>v9.showList=!v9.showList, ctx]; + let hdlr16 = [ctx['removeAllRemotes'], ctx]; if (ctx['state'].error) { const b3 = safeOutput(ctx['state'].error); b2 = block2([], [b3]); @@ -59,7 +62,8 @@ export const templates = { 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 key1 = ctx['remoteKey'](ctx['remote']); + let attr6 = ctx['remoteLogoSrc'](ctx['remote']); 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); @@ -67,17 +71,17 @@ export const templates = { const b11 = comp5({text: ctx['remote'].state||'Inactive',limit: 18}, key + `__5__${key1}`, node, this, null); const v10 = ctx['editRemote']; const v11 = ctx['remote']; - let hdlr15 = [()=>v10(v11), ctx]; + let hdlr17 = [()=>v10(v11), ctx]; const v12 = ctx['removeRemote']; const v13 = ctx['remote']; - let hdlr16 = [()=>v12(v13), ctx]; - c_block5[i1] = withKey(block6([hdlr15, hdlr16], [b7, b8, b9, b10, b11]), key1); + let hdlr18 = [()=>v12(v13), ctx]; + c_block5[i1] = withKey(block6([attr6, hdlr17, hdlr18], [b7, b8, b9, b10, b11]), key1); } ctx = ctx.__proto__; const b5 = list(c_block5); b4 = block4([], [b5]); } - return block1([attr1, hdlr1, attr2, hdlr2, attr3, attr4, hdlr3, prop1, hdlr4, prop2, hdlr5, prop3, hdlr6, prop4, hdlr7, prop5, hdlr8, prop6, hdlr9, prop7, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14], [b2, b4]); + return block1([attr1, hdlr1, attr2, hdlr2, attr3, attr4, hdlr3, prop1, hdlr4, prop2, hdlr5, prop3, hdlr6, prop4, hdlr7, prop5, hdlr8, prop6, hdlr9, prop7, hdlr10, attr5, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15, hdlr16], [b2, b4]); } }, @@ -96,7 +100,7 @@ export const templates = { const comp9 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp10 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - let block1 = createBlock(`
Please wait — or grab a cup of coffee ☕
`); + let block1 = createBlock(`
Please wait — or grab a cup of coffee ☕
`); let block3 = createBlock(`

`); let block5 = createBlock(`
Hello 😉, you have not configured any remotes. Open Options below and add one.
`); let block6 = createBlock(`
`); @@ -124,33 +128,35 @@ export const templates = { let block44 = createBlock(`Hours Left`); let block45 = createBlock(``); let block48 = createBlock(``); - let block49 = createBlock(``); - let block50 = createBlock(``); - let block52 = createBlock(``); - let block53 = createBlock(``); - let block54 = createBlock(``); - let block56 = createBlock(``); - let block57 = createBlock(``); - let block58 = createBlock(``); - let block60 = createBlock(`
`); - let block62 = createBlock(``); - let block64 = createBlock(``); - let block66 = createBlock(``); - let block69 = createBlock(``); - let block71 = createBlock(``); - let block72 = createBlock(``); - let block73 = createBlock(``); - let block75 = createBlock(``); - let block78 = createBlock(``); + let block50 = createBlock(``); + let block51 = createBlock(``); + let block52 = createBlock(``); + let block54 = createBlock(``); + let block55 = createBlock(``); + let block56 = createBlock(``); + let block58 = createBlock(``); + let block59 = createBlock(``); + let block60 = createBlock(``); + let block62 = createBlock(`
`); + let block64 = createBlock(``); + let block66 = createBlock(``); + let block68 = createBlock(``); + let block71 = createBlock(``); + let block73 = createBlock(``); + let block74 = createBlock(``); + let block75 = createBlock(``); + let block77 = createBlock(``); let block80 = createBlock(``); - let block82 = createBlock(``); - let block84 = createBlock(` No matching items are currently available `); + let block82 = createBlock(``); + let block84 = createBlock(``); + let block86 = createBlock(` No matching items are currently available `); return function template(ctx, node, key = "") { - let b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b84, b85, b86, b87, b88, b89; + let b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b86, b87, b88, b89, b90, b91; let attr1 = ctx['state'].view==='loading'?'':'hide'; b2 = safeOutput(ctx['state'].busyMessage); let attr2 = ctx['state'].view==='login'?'login-view':'login-view hide'; + let attr3 = ctx['currentRemoteLogoSrc']; if (ctx['state'].bootError) { const b4 = safeOutput(ctx['state'].bootError); b3 = block3([], [b4]); @@ -172,16 +178,16 @@ export const templates = { b9 = block9([prop1, hdlr2]); } if (!ctx['state'].useExistingSession) { - let attr3 = ctx['state'].showPassword?'text':'password'; + let attr4 = 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]; - b10 = block10([attr3, prop2, hdlr3]); + b10 = block10([attr4, prop2, hdlr3]); } if (!ctx['state'].useExistingSession) { let hdlr4 = [ctx['togglePassword'], ctx]; - let attr4 = ctx['state'].showPassword?'fa fa-eye-slash':'fa fa-eye'; - b11 = block11([hdlr4, attr4]); + let attr5 = ctx['state'].showPassword?'fa fa-eye-slash':'fa fa-eye'; + b11 = block11([hdlr4, attr5]); } const v3 = ctx['state']; let hdlr5 = [(_ev)=>v3.selectedRemoteIndex=_ev.target.value, ctx]; @@ -189,11 +195,11 @@ export const templates = { const [k_block12, v_block12, l_block12, c_block12] = prepareList(ctx['state'].remotes);; for (let i1 = 0; i1 < l_block12; i1++) { ctx[`remote`] = k_block12[i1]; - const key1 = ctx['remote'].database+ctx['remote'].url; - let attr5 = ctx['remote'].__index; + const key1 = ctx['remoteKey'](ctx['remote']); + let attr6 = ctx['remote'].__index; let prop3 = new Boolean(ctx['state'].selectedRemoteIndex===ctx['remote'].__index); - const b14 = safeOutput(ctx['remote'].name); - c_block12[i1] = withKey(block13([attr5, prop3], [b14]), key1); + const b14 = safeOutput(ctx['remoteOptionLabel'](ctx['remote'])); + c_block12[i1] = withKey(block13([attr6, prop3], [b14]), key1); } ctx = ctx.__proto__; b12 = list(c_block12); @@ -209,7 +215,7 @@ export const templates = { } b6 = block6([hdlr1, hdlr5, prop4, hdlr6], [b7, b9, b10, b11, b12, b15, b16]); } - let attr6 = ctx['state'].view==='main'?'':'hide'; + let attr7 = ctx['state'].view==='main'?'':'hide'; if (ctx['state'].sourceError) { const b20 = safeOutput(ctx['state'].sourceError); b19 = block19([], [b20]); @@ -226,10 +232,10 @@ export const templates = { for (let i1 = 0; i1 < l_block22; i1++) { ctx[`stage`] = k_block22[i1]; const key1 = ctx['stage'].id; - let attr7 = ''+ctx['stage'].id; + let attr8 = ''+ctx['stage'].id; let prop7 = new Boolean(ctx['state'].helpdeskStageFilter===''+ctx['stage'].id); const b24 = safeOutput(ctx['stage'].name); - c_block22[i1] = withKey(block23([attr7, prop7], [b24]), key1); + c_block22[i1] = withKey(block23([attr8, prop7], [b24]), key1); } ctx = ctx.__proto__; const b22 = list(c_block22); @@ -295,112 +301,122 @@ export const templates = { for (let i1 = 0; i1 < l_block47; i1++) { ctx[`issue`] = k_block47[i1]; const key1 = ctx['issue'].id; - let b49, b50, b51, b68; - let attr8 = ctx['isActiveTimerItem'](ctx['issue'])?'active-row':''; + let b49, b52, b53, b70; + let attr9 = ctx['isActiveTimerItem'](ctx['issue'])?'active-row':''; if (!ctx['state'].activeTimerId) { - const v9 = ctx['startTimer']; - const v10 = ctx['issue']; - let hdlr18 = [()=>v9(v10), ctx]; - b49 = block49([hdlr18]); + let b50, b51; + if (ctx['isHelpdeskSource']&&ctx['helpdeskTimesheetBlocked'](ctx['issue'])) { + let attr10 = ctx['helpdeskTimesheetBlockMessage'](ctx['issue']); + const v9 = ctx['showHelpdeskTimesheetInfo']; + const v10 = ctx['issue']; + let hdlr18 = [()=>v9(v10), ctx]; + b50 = block50([attr10, hdlr18]); + } else { + const v11 = ctx['startTimer']; + const v12 = ctx['issue']; + let hdlr19 = [()=>v11(v12), ctx]; + b51 = block51([hdlr19]); + } + b49 = multi([b50, b51]); } if (ctx['isActiveTimerItem'](ctx['issue'])) { - const v11 = ctx['stopTimer']; - const v12 = ctx['issue']; - let hdlr19 = [()=>v11(v12), ctx]; - b50 = block50([hdlr19]); + const v13 = ctx['stopTimer']; + const v14 = ctx['issue']; + let hdlr20 = [()=>v13(v14), ctx]; + b52 = block52([hdlr20]); } if (ctx['isHelpdeskSource']) { - let b52, b54, b58, b62, b64, b66; + let b54, b56, b60, b64, b66, b68; if (ctx['showHelpdeskTherpLink']) { - let b53; + let b55; if (ctx['helpdeskTherpLinkHref'](ctx['issue'])) { - let attr9 = ctx['helpdeskTherpLinkHref'](ctx['issue']); - b53 = block53([attr9]); + let attr11 = ctx['helpdeskTherpLinkHref'](ctx['issue']); + b55 = block55([attr11]); } - b52 = block52([], [b53]); + b54 = block54([], [b55]); } - let b55, b57; + let b57, b59; if (ctx['issue'].priority_level.length) { ctx = Object.create(ctx); - const [k_block55, v_block55, l_block55, c_block55] = prepareList(ctx['issue'].priority_level);; - for (let i2 = 0; i2 < l_block55; i2++) { - ctx[`priority`] = k_block55[i2]; + const [k_block57, v_block57, l_block57, c_block57] = prepareList(ctx['issue'].priority_level);; + for (let i2 = 0; i2 < l_block57; i2++) { + ctx[`priority`] = k_block57[i2]; const key2 = ctx['priority']+'_'+ctx['issue'].id; - c_block55[i2] = withKey(block56(), key2); + c_block57[i2] = withKey(block58(), key2); } ctx = ctx.__proto__; - b55 = list(c_block55); + b57 = list(c_block57); } if (!ctx['issue'].priority_level.length) { - b57 = block57(); + b59 = block59(); } - b54 = block54([], [b55, b57]); - let b59, b60; - b59 = comp1({text: ctx['issueLabel'](ctx['issue']),limit: 55,href: ctx['issueHref'](ctx['issue'])}, key + `__1__${key1}`, node, this, null); + b56 = block56([], [b57, b59]); + let b61, b62; + b61 = comp1({text: ctx['issueLabel'](ctx['issue']),limit: 55,href: ctx['issueHref'](ctx['issue'])}, key + `__1__${key1}`, node, this, null); if (ctx['showHelpdeskDescription']&&ctx['helpdeskDescriptionText'](ctx['issue'])) { - const b61 = comp2({text: ctx['helpdeskDescriptionText'](ctx['issue']),limit: 70}, key + `__2__${key1}`, node, this, null); - b60 = block60([], [b61]); + const b63 = comp2({text: ctx['helpdeskDescriptionText'](ctx['issue']),limit: 70}, key + `__2__${key1}`, node, this, null); + b62 = block62([], [b63]); } - b58 = block58([], [b59, b60]); - const b63 = comp3({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__3__${key1}`, node, this, null); - b62 = block62([], [b63]); + b60 = block60([], [b61, b62]); + const b65 = comp3({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__3__${key1}`, node, this, null); + b64 = block64([], [b65]); if (ctx['showHelpdeskAssignee']) { - const b65 = comp4({text: ctx['relationLabel'](ctx['helpdeskAssigneeValue'](ctx['issue'])),limit: 22}, key + `__4__${key1}`, node, this, null); - b64 = block64([], [b65]); + const b67 = comp4({text: ctx['relationLabel'](ctx['helpdeskAssigneeValue'](ctx['issue'])),limit: 22}, key + `__4__${key1}`, node, this, null); + b66 = block66([], [b67]); } if (ctx['showHelpdeskHours']) { - const b67 = comp5({text: ctx['formatHours'](ctx['helpdeskHoursValue'](ctx['issue'])),limit: 10}, key + `__5__${key1}`, node, this, null); - b66 = block66([], [b67]); + const b69 = comp5({text: ctx['formatHours'](ctx['helpdeskHoursValue'](ctx['issue'])),limit: 10}, key + `__5__${key1}`, node, this, null); + b68 = block68([], [b69]); } - b51 = multi([b52, b54, b58, b62, b64, b66]); + b53 = multi([b54, b56, b60, b64, b66, b68]); } else { - let b69, b73, b75, b77, b82; - let b70, b72; + let b71, b75, b77, b79, b84; + let b72, b74; if (ctx['issue'].priority_level.length) { ctx = Object.create(ctx); - const [k_block70, v_block70, l_block70, c_block70] = prepareList(ctx['issue'].priority_level);; - for (let i2 = 0; i2 < l_block70; i2++) { - ctx[`priority`] = k_block70[i2]; + const [k_block72, v_block72, l_block72, c_block72] = prepareList(ctx['issue'].priority_level);; + for (let i2 = 0; i2 < l_block72; i2++) { + ctx[`priority`] = k_block72[i2]; const key2 = ctx['priority']+'_'+ctx['issue'].id; - c_block70[i2] = withKey(block71(), key2); + c_block72[i2] = withKey(block73(), key2); } ctx = ctx.__proto__; - b70 = list(c_block70); + b72 = list(c_block72); } if (!ctx['issue'].priority_level.length) { - b72 = block72(); + b74 = block74(); } - b69 = block69([], [b70, b72]); - const b74 = comp6({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__6__${key1}`, node, this, null); - b73 = block73([], [b74]); - const b76 = comp7({text: ctx['issueLabel'](ctx['issue']),limit: 70,href: ctx['issueHref'](ctx['issue'])}, key + `__7__${key1}`, node, this, null); + b71 = block71([], [b72, b74]); + const b76 = comp6({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__6__${key1}`, node, this, null); b75 = block75([], [b76]); + const b78 = comp7({text: ctx['issueLabel'](ctx['issue']),limit: 70,href: ctx['issueHref'](ctx['issue'])}, key + `__7__${key1}`, node, this, null); + b77 = block77([], [b78]); if (ctx['state'].dataSource==='project.task') { - const b79 = comp8({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].effective_hours)),limit: 9}, key + `__8__${key1}`, node, this, null); - const b78 = block78([], [b79]); - const b81 = comp9({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].remaining_hours)),limit: 9}, key + `__9__${key1}`, node, this, null); + const b81 = comp8({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].effective_hours)),limit: 9}, key + `__8__${key1}`, node, this, null); const b80 = block80([], [b81]); - b77 = multi([b78, b80]); + const b83 = comp9({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].remaining_hours)),limit: 9}, key + `__9__${key1}`, node, this, null); + const b82 = block82([], [b83]); + b79 = multi([b80, b82]); } - const b83 = comp10({text: ctx['relationLabel'](ctx['resourceRelationValue'](ctx['issue'])),limit: 22}, key + `__10__${key1}`, node, this, null); - b82 = block82([], [b83]); - b68 = multi([b69, b73, b75, b77, b82]); + const b85 = comp10({text: ctx['relationLabel'](ctx['resourceRelationValue'](ctx['issue'])),limit: 22}, key + `__10__${key1}`, node, this, null); + b84 = block84([], [b85]); + b70 = multi([b71, b75, b77, b79, b84]); } - c_block47[i1] = withKey(block48([attr8], [b49, b50, b51, b68]), key1); + c_block47[i1] = withKey(block48([attr9], [b49, b52, b53, b70]), key1); } ctx = ctx.__proto__; b47 = list(c_block47); } if (!ctx['filteredIssues'].length) { - let attr10 = ctx['tableColumnCount']; - b84 = block84([attr10]); + let attr12 = ctx['tableColumnCount']; + b86 = block86([attr12]); } - b85 = safeOutput(ctx['state'].serverVersion||'Unknown'); - b86 = safeOutput(ctx['state'].odooOWLVersion||'Unknown'); - b87 = safeOutput(ctx['state'].currentHost||'-'); - b88 = safeOutput(ctx['state'].currentDatabase||'-'); - b89 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); - return block1([attr1, attr2, attr6, prop5, hdlr7, prop8, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15], [b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b84, b85, b86, b87, b88, b89]); + b87 = safeOutput(ctx['state'].serverVersion||'Unknown'); + b88 = safeOutput(ctx['state'].odooOWLVersion||'Unknown'); + b89 = safeOutput(ctx['state'].currentHost||'-'); + b90 = safeOutput(ctx['state'].currentDatabase||'-'); + b91 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); + return block1([attr1, attr2, attr3, attr7, prop5, hdlr7, prop8, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15], [b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b86, b87, b88, b89, b90, b91]); } }, diff --git a/dist/firefox/js/components/options-app.js b/dist/firefox/js/components/options-app.js index 9c72c27..c7f7581 100644 --- a/dist/firefox/js/components/options-app.js +++ b/dist/firefox/js/components/options-app.js @@ -3,6 +3,7 @@ import { writeRemotes, validURL, normalizeHost, + remoteIdentity, storage, clearOdooSessionCookies, notify, @@ -15,6 +16,9 @@ const { Component, mount, useState, onWillStart } = owl; const PAGE_ABOUT = 'about'; const PAGE_OPTIONS = 'options'; const DEFAULT_DATA_SOURCE = 'project.issue'; +const DEFAULT_LOGO_SRC = '/img/logo.png'; +const MAX_REMOTE_LOGO_BYTES = 512 * 1024; +const ALLOWED_REMOTE_LOGO_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']); const STORAGE_KEYS = { remoteHostInfo: 'remote_host_info', @@ -119,7 +123,7 @@ function createOptionsAppTemplate(app, bdom, helpers) { for (let i = 0; i < remoteCount; i++) { ctx.remote = remoteItems[i]; - const remoteKey = ctx.remote.url + ctx.remote.database; + const remoteKey = ctx.remoteKey(ctx.remote); if (seenRemoteKeys.has(String(remoteKey))) { throw new OwlError(`Got duplicate key in t-foreach: ${remoteKey}`); @@ -198,6 +202,7 @@ class OptionsApp extends Component { remote_name: '', remote_database: '', remote_datasrc: DEFAULT_DATA_SOURCE, + remote_logo: '', }, }); @@ -217,6 +222,50 @@ class OptionsApp extends Component { this.state.remotes = await readRemotes(); } + + remoteKey(remote) { return remoteIdentity(remote); } + remoteLogoSrc(remote) { return remote?.logoDataUrl || DEFAULT_LOGO_SRC; } + get formLogoSrc() { return this.state.form.remote_logo || DEFAULT_LOGO_SRC; } + + async readLogoFile(file) { + if (!file) return ''; + if (!ALLOWED_REMOTE_LOGO_TYPES.has(file.type)) { + throw new Error('Company logo must be PNG, JPEG, WebP, or GIF.'); + } + if (file.size > MAX_REMOTE_LOGO_BYTES) { + throw new Error('Company logo is too large. Maximum size is 512 KB.'); + } + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result || '')); + reader.onerror = () => reject(new Error('Could not read the selected company logo.')); + reader.readAsDataURL(file); + }); + } + + async onRemoteLogoChange(ev) { + const input = ev?.target; + const file = input?.files?.[0]; + if (!file) return; + try { + this.state.form.remote_logo = await this.readLogoFile(file); + } catch (err) { + if (input) input.value = ''; + await notify(err.message || 'Could not use the selected company logo.'); + } + } + + clearRemoteLogo() { + this.state.form.remote_logo = ''; + const input = document.getElementById('remote-logo'); + if (input) input.value = ''; + } + + duplicateRemoteMessage(remote) { + return `A remote for ${remote.url} / ${remote.database} / ${remote.datasrc || DEFAULT_DATA_SOURCE} already exists. ` + + 'Change the host, database, or data source.'; + } + /** * Reset the add-remote form back to defaults. */ @@ -225,6 +274,7 @@ class OptionsApp extends Component { this.state.form.remote_name = ''; this.state.form.remote_database = ''; this.state.form.remote_datasrc = DEFAULT_DATA_SOURCE; + this.state.form.remote_logo = ''; } /** @@ -276,8 +326,9 @@ class OptionsApp extends Component { const { host, name, database, datasrc } = validated; const remotes = await readRemotes(); - if (remotes.some((remote) => remote.url === host && remote.database === database)) { - this.state.error = `${host} and ${database} already exist; duplicates are not allowed`; + const candidate = {url: host, database, datasrc}; + if (remotes.some((remote) => remoteIdentity(remote) === remoteIdentity(candidate))) { + await notify(this.duplicateRemoteMessage(candidate)); return; } @@ -286,6 +337,7 @@ class OptionsApp extends Component { name, database, datasrc, + logoDataUrl: this.state.form.remote_logo || '', state: 'Inactive', }); @@ -307,13 +359,18 @@ class OptionsApp extends Component { return; } - await clearOdooSessionCookies(remote.url); + const targetKey = remoteIdentity(remote); const remotes = (await readRemotes()).filter( - (currentRemote) => !(currentRemote.url === remote.url && currentRemote.database === remote.database) + (currentRemote) => remoteIdentity(currentRemote) !== targetKey ); await writeRemotes(remotes); - await storage.remove(remote.database); + if (!remotes.some((item) => normalizeHost(item.url || '') === normalizeHost(remote.url || ''))) { + await clearOdooSessionCookies(remote.url); + } + if (!remotes.some((item) => String(item.database || '').trim() === String(remote.database || '').trim())) { + await storage.remove(remote.database); + } await this.loadRemotes(); await notify(`[${remote.url}] removed successfully!`); } @@ -340,6 +397,7 @@ class OptionsApp extends Component { let name = currentName; let database = currentDatabase; let datasrc = currentDatasource; + let logoDataUrl = remote.logoDataUrl || ''; if (customAlert) { const inputSuffix = `${Date.now()}-${Math.random().toString(36).slice(2)}`; @@ -371,6 +429,17 @@ class OptionsApp extends Component { +
+ Remote logo +
+
Company Logo (optional)
+ + +
PNG, JPEG, WebP or GIF; maximum 512 KB.
+
+
`; @@ -384,11 +453,19 @@ class OptionsApp extends Component { const nameEl = document.getElementById(nameId); const databaseEl = document.getElementById(databaseId); const datasourceEl = document.getElementById(datasourceId); + const logoEl = document.getElementById(`edit-remote-logo-${inputSuffix}`); + const removeLogoEl = document.getElementById(`edit-remote-remove-logo-${inputSuffix}`); host = normalizeHost(hostEl ? hostEl.value : currentHost); name = ((nameEl ? nameEl.value : currentName) || '').trim(); database = ((databaseEl ? databaseEl.value : currentDatabase) || '').trim(); datasrc = datasourceEl ? datasourceEl.value : currentDatasource; + if (removeLogoEl?.checked) { + logoDataUrl = ''; + } else if (logoEl?.files?.[0]) { + try { logoDataUrl = await this.readLogoFile(logoEl.files[0]); } + catch (err) { await notify(err.message || 'Could not use the selected company logo.'); return; } + } } else { return; } @@ -404,39 +481,41 @@ class OptionsApp extends Component { } const remotes = await readRemotes(); - const duplicate = remotes.find( - (item) => !(item.url === currentHost && item.database === currentDatabase) - && item.url === host - && item.database === database + const originalKey = remoteIdentity(remote); + const candidate = {url: host, database, datasrc}; + const duplicate = remotes.some( + (item) => remoteIdentity(item) !== originalKey && remoteIdentity(item) === remoteIdentity(candidate) ); if (duplicate) { - this.state.error = `${host} and ${database} already exist; duplicates are not allowed`; + await notify(this.duplicateRemoteMessage(candidate)); return; } - const index = remotes.findIndex( - (item) => item.url === currentHost && item.database === currentDatabase - ); + const index = remotes.findIndex((item) => remoteIdentity(item) === originalKey); if (index === -1) { - this.state.error = 'Remote not found'; + await notify('Remote not found. Refresh the list and try again.'); return; } + const oldHost = normalizeHost(remotes[index].url || ''); + const oldDb = String(remotes[index].database || '').trim(); + remotes[index] = { ...remotes[index], url: host, name, database, datasrc, + logoDataUrl, }; await writeRemotes(remotes); - if (host !== currentHost) { - await clearOdooSessionCookies(currentHost); + if (oldHost !== host && !remotes.some((item) => normalizeHost(item.url || '') === oldHost)) { + await clearOdooSessionCookies(oldHost); } - if (database !== currentDatabase) { - await storage.remove(currentDatabase); + if (oldDb !== database && !remotes.some((item) => String(item.database || '').trim() === oldDb)) { + await storage.remove(oldDb); } await this.loadRemotes(); @@ -557,9 +636,11 @@ class OptionsApp extends Component { } const remotes = await readRemotes(); - for (const remote of remotes) { - await clearOdooSessionCookies(remote.url); - await storage.remove(remote.database); + for (const host of [...new Set(remotes.map((remote) => normalizeHost(remote.url || '')).filter(Boolean))]) { + await clearOdooSessionCookies(host); + } + for (const database of [...new Set(remotes.map((remote) => String(remote.database || '').trim()).filter(Boolean))]) { + await storage.remove(database); } await writeRemotes([]); diff --git a/dist/firefox/js/components/popup-app.js b/dist/firefox/js/components/popup-app.js index 8534301..088a419 100644 --- a/dist/firefox/js/components/popup-app.js +++ b/dist/firefox/js/components/popup-app.js @@ -12,6 +12,7 @@ import { priorityStars, matchesIssue, extractMessageSummary, + remoteIdentity, notify, confirmDialog, promptDialog, @@ -212,14 +213,14 @@ function createPopupAppTemplate(app, bdom, helpers) { for (let i = 0; i < remoteCount; i++) { ctx.remote = remoteItems[i]; - const remoteKey = ctx.remote.database + ctx.remote.url; + const remoteKey = ctx.remoteKey(ctx.remote); if (seenRemoteKeys.has(String(remoteKey))) { throw new OwlError(`Got duplicate key in t-foreach: ${remoteKey}`); } seenRemoteKeys.add(String(remoteKey)); const optionValue = String(ctx.remote.__index); const optionSelected = selectedRemoteIndex === String(ctx.remote.__index); - const optionText = ctx.remote.name; + const optionText = ctx.remoteOptionLabel(ctx.remote); remoteChildren[i] = withKey(remoteOptionBlock([optionValue, optionSelected, optionText]), remoteKey); } @@ -476,6 +477,9 @@ class PopupApp extends Component { dataSource: DEFAULTS.dataSource, currentCompanyId: null, allowedCompanyIds: [], + availableCompanyIds: [], + currentUserId: null, + companyNames: {}, serverVersion: '', odooOWLVersion: owl.__info__.version, supportedFields: {}, @@ -528,6 +532,17 @@ class PopupApp extends Component { return this.state.remotes[idx] || null; } + + remoteKey(remote) { return remoteIdentity(remote); } + remoteOptionLabel(remote) { + const source = remote?.datasrc || DATA_SOURCE_ISSUE; + const sourceLabel = source === DATA_SOURCE_HELPDESK + ? 'Helpdesk Tickets' + : source === DATA_SOURCE_TASK ? 'Tasks' : 'Issues'; + return `${remote?.name || remote?.database || 'Remote'} — ${sourceLabel}`; + } + get currentRemoteLogoSrc() { return this.currentRemote?.logoDataUrl || '/img/logo.png'; } + /** * Formatted active timer duration. */ @@ -604,11 +619,34 @@ class PopupApp extends Component { const contextIds = Array.isArray(sessionInfo?.user_context?.allowed_company_ids) ? sessionInfo.user_context.allowed_company_ids.map(Number).filter(Boolean) : []; + const rawAllowed = sessionInfo?.user_companies?.allowed_companies; + let availableIds = []; + const companyNames = {}; + if (Array.isArray(rawAllowed)) { + for (const item of rawAllowed) { + const id = Number(typeof item === 'object' ? item?.id : item); + if (!id) continue; + availableIds.push(id); + if (typeof item === 'object' && item?.name) companyNames[id] = String(item.name); + } + } else if (rawAllowed && typeof rawAllowed === 'object') { + availableIds = Object.keys(rawAllowed).map(Number).filter(Boolean); + for (const [id, item] of Object.entries(rawAllowed)) { + if (item?.name) companyNames[Number(id)] = String(item.name); + } + } const current = Number( - sessionInfo?.user_companies?.current_company || contextIds[0] || 0 + sessionInfo?.user_companies?.current_company || contextIds[0] || availableIds[0] || 0 ) || null; + this.state.currentUserId = Number(sessionInfo?.uid || 0) || null; this.state.currentCompanyId = current; - this.state.allowedCompanyIds = contextIds.length ? contextIds : (current ? [current] : []); + this.state.availableCompanyIds = availableIds.length + ? [...new Set(availableIds)] + : [...new Set(contextIds.length ? contextIds : (current ? [current] : []))]; + this.state.allowedCompanyIds = contextIds.length + ? [...new Set(contextIds)] + : [...this.state.availableCompanyIds]; + this.state.companyNames = companyNames; } helpdeskTimesheetCompanyId(issue) { @@ -632,6 +670,130 @@ class PopupApp extends Component { return {allowed_company_ids: allowed}; } + + helpdeskCompanyLabel(companyId) { + if (!companyId) return 'the ticket/project company'; + return this.state.companyNames?.[companyId] || `company #${companyId}`; + } + + helpdeskTimesheetBlocked(issue) { + return this.isHelpdeskSource && issue?.__timesheetWritable === false; + } + + helpdeskTimesheetBlockMessage(issue) { + return issue?.__timesheetBlockMessage || 'Odoo is not currently able to create a timesheet for this ticket.'; + } + + async showHelpdeskTimesheetInfo(issue) { + await notify(this.helpdeskTimesheetBlockMessage(issue)); + } + + staticHelpdeskTimesheetReadiness(issue) { + if (!this.state.sourceCapabilities.canRecordTime) { + return {writable: false, message: helpdeskTimesheetUnavailableMessage()}; + } + if (!isHelpdeskTimesheetEnabled(issue, this.state.sourceCapabilities)) { + return { + writable: false, + message: 'Timesheets are disabled for this ticket team/project. Enable Helpdesk Timesheets in Odoo before starting the timer.', + }; + } + const projectId = this.relationId(issue?.project_id); + const analyticAccount = this.resolveAnalyticAccount(issue); + if (!projectId && !analyticAccount) { + return { + writable: false, + message: 'This Helpdesk ticket has no timesheet project or analytic account. Configure its Helpdesk team/project in Odoo before starting the timer.', + }; + } + const companyId = this.helpdeskTimesheetCompanyId(issue); + const accessible = this.state.availableCompanyIds || []; + if (companyId && accessible.length && !accessible.map(Number).includes(Number(companyId))) { + return { + writable: false, + message: `The ticket/project belongs to ${this.helpdeskCompanyLabel(companyId)}, which is not an allowed company for this Odoo user.`, + }; + } + return {writable: null, message: '', companyId}; + } + + async annotateHelpdeskTimesheetReadiness(records) { + if (!this.isHelpdeskSource) return records; + const annotated = records.map((issue) => { + const check = this.staticHelpdeskTimesheetReadiness(issue); + return { + ...issue, + __timesheetWritable: check.writable, + __timesheetBlockMessage: check.message || '', + __timesheetCompanyId: check.companyId || this.helpdeskTimesheetCompanyId(issue), + }; + }); + + const pending = annotated.filter((issue) => issue.__timesheetWritable === null); + if (!pending.length || !this.state.currentUserId) return annotated; + + const companyIds = [...new Set(pending.map((issue) => Number(issue.__timesheetCompanyId || 0)).filter(Boolean))]; + const domain = [ + ['user_id', '=', this.state.currentUserId], + ['active', '=', true], + ]; + if (companyIds.length) domain.push(['company_id', 'in', companyIds]); + else if (this.state.allowedCompanyIds?.length) domain.push(['company_id', 'in', this.state.allowedCompanyIds]); + + let employees; + try { + const contextIds = companyIds.length + ? [...new Set([...companyIds, ...(this.state.allowedCompanyIds || [])])] + : (this.state.allowedCompanyIds || []); + const result = await this.rpc.searchRead('hr.employee', domain, ['id', 'company_id'], { + limit: 100, + context: contextIds.length ? {allowed_company_ids: contextIds} : {}, + }); + employees = result.records || []; + } catch (err) { + // Some users cannot read hr.employee even though account.analytic.line + // create() can resolve the employee with sudo. In that case the check + // is inconclusive, so do not disable a timer that Odoo may accept. + console.warn('Helpdesk timesheet employee preflight could not be verified', err); + return annotated; + } + + const employeeCompanies = new Set( + employees.map((employee) => this.relationId(employee.company_id)).filter(Boolean) + ); + for (const issue of annotated) { + if (issue.__timesheetWritable !== null) continue; + const companyId = Number(issue.__timesheetCompanyId || 0) || null; + if (companyId) { + if (employeeCompanies.has(companyId)) { + issue.__timesheetWritable = true; + } else { + issue.__timesheetWritable = false; + issue.__timesheetBlockMessage = + `Odoo cannot create this timesheet because your user has no active employee in ${this.helpdeskCompanyLabel(companyId)}. ` + + 'Check the employee company and allowed companies, then reconnect.'; + } + } else if (!employees.length) { + issue.__timesheetWritable = false; + issue.__timesheetBlockMessage = + 'Odoo cannot create this timesheet because your user has no active employee in the selected companies. ' + + 'Check the employee company and allowed companies, then reconnect.'; + } else if (employees.length === 1) { + issue.__timesheetWritable = true; + } + } + return annotated; + } + + async refreshHelpdeskTimesheetReadiness(issue) { + if (!this.isHelpdeskSource) return issue; + const [checked] = await this.annotateHelpdeskTimesheetReadiness([{...issue}]); + if (!checked) return issue; + const index = this.state.issues.findIndex((item) => Number(item.id) === Number(issue.id)); + if (index >= 0) this.state.issues[index] = {...this.state.issues[index], ...checked}; + return checked; + } + async updateLimitPreference(value) { this.state.limitTo = value; await storage.set(STORAGE_KEYS.searchLimit, value); @@ -745,6 +907,9 @@ class PopupApp extends Component { this.state.issues = []; this.state.currentCompanyId = null; this.state.allowedCompanyIds = []; + this.state.availableCompanyIds = []; + this.state.currentUserId = null; + this.state.companyNames = {}; this.state.serverVersion = ''; this.state.odooOWLVersion = owl.__info__.version; this.state.supportedFields = {}; @@ -877,11 +1042,19 @@ class PopupApp extends Component { this.rpc.setHost(this.state.currentHost); - const remoteIndex = this.state.remotes.findIndex( - (remote) => - remote.url === this.state.currentHost && - remote.database === this.state.currentDatabase + const storedRemoteKey = remoteIdentity({ + url: this.state.currentHost, + database: this.state.currentDatabase, + datasrc: this.state.dataSource, + }); + let remoteIndex = this.state.remotes.findIndex( + (remote) => remoteIdentity(remote) === storedRemoteKey ); + if (remoteIndex < 0) { + remoteIndex = this.state.remotes.findIndex( + (remote) => remote.url === this.state.currentHost && remote.database === this.state.currentDatabase + ); + } if (remoteIndex >= 0) { this.state.selectedRemoteIndex = String(remoteIndex); @@ -1184,8 +1357,10 @@ class PopupApp extends Component { const [userResult, serverInfo] = await Promise.all([ userPromise, serverInfoPromise, - this.loadProjects().catch((err) => console.warn('loadProjects failed', err)), - this.loadIssues().catch((err) => console.warn('loadIssues failed', err)), + (async () => { + await this.loadProjects().catch((err) => console.warn('loadProjects failed', err)); + await this.loadIssues().catch((err) => console.warn('loadIssues failed', err)); + })(), ]); this.state.user = userResult.records?.[0] || { @@ -1436,7 +1611,7 @@ class PopupApp extends Component { } } - this.state.issues = records.map((issue) => ({ + let preparedRecords = records.map((issue) => ({ ...issue, message_summary: extractMessageSummary( issue.message_summary || issue.description || issue.display_name || issue.name || '' @@ -1444,6 +1619,10 @@ class PopupApp extends Component { priority_level: priorityStars(issue.priority), stage_sequence: stageSequenceById.get(issue.stage_id?.[0]) ?? 9999, })); + if (model === DATA_SOURCE_HELPDESK) { + preparedRecords = await this.annotateHelpdeskTimesheetReadiness(preparedRecords); + } + this.state.issues = preparedRecords; } catch (err) { if (model === DATA_SOURCE_HELPDESK) { this.state.issues = []; @@ -1476,13 +1655,12 @@ class PopupApp extends Component { await notify(`Stop the active ${this.itemLabelSingular} timer before starting another one.`); return; } - if (this.isHelpdeskSource && !this.state.sourceCapabilities.canRecordTime) { - await notify(helpdeskTimesheetUnavailableMessage()); - return; - } - if (this.isHelpdeskSource && !isHelpdeskTimesheetEnabled(issue, this.state.sourceCapabilities)) { - await notify('Timesheets are disabled for this ticket team/project. Enable Helpdesk Timesheets in Odoo before starting the timer.'); - return; + if (this.isHelpdeskSource) { + const checkedIssue = await this.refreshHelpdeskTimesheetReadiness(issue); + if (this.helpdeskTimesheetBlocked(checkedIssue)) { + await this.showHelpdeskTimesheetInfo(checkedIssue); + return; + } } const now = new Date().toISOString(); @@ -1843,6 +2021,9 @@ class PopupApp extends Component { this.state.projects = []; this.state.currentCompanyId = null; this.state.allowedCompanyIds = []; + this.state.availableCompanyIds = []; + this.state.currentUserId = null; + this.state.companyNames = {}; this.state.view = VIEW_LOGIN; this.state.useExistingSession = true; } diff --git a/dist/firefox/js/lib/common.js b/dist/firefox/js/lib/common.js index 2b70962..e13c320 100644 --- a/dist/firefox/js/lib/common.js +++ b/dist/firefox/js/lib/common.js @@ -136,6 +136,22 @@ export function normalizeHost(host) { return out.replace(/\/$/, ''); } +/** + * Return the functional identity of a saved remote. + * + * A single Odoo database can legitimately be configured more than once when + * each entry targets a different timer resource (for example Tasks and + * Helpdesk Tickets). Display name, logo and UI state are intentionally not + * part of this identity. + */ +export function remoteIdentity(remote) { + const host = normalizeHost(remote?.url || ''); + const database = String(remote?.database || '').trim(); + const dataSource = String(remote?.datasrc || 'project.issue').trim() || 'project.issue'; + return JSON.stringify([host, database, dataSource]); +} + + export function toCSV(rows) { if (!rows || !rows.length) return ''; const headers = Array.from(new Set(rows.flatMap((r) => Object.keys(r)))); diff --git a/dist/firefox/js/templates.js b/dist/firefox/js/templates.js index c0f2a20..de42e91 100644 --- a/dist/firefox/js/templates.js +++ b/dist/firefox/js/templates.js @@ -9,10 +9,10 @@ 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(`


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 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


Remote logo preview
PNG, JPEG, WebP or GIF; maximum 512 KB.
Controls
`); let block2 = createBlock(`
`); - let block4 = createBlock(`
List of Available Remotes
RemoteHostDatabaseSourceState
`); - let block6 = createBlock(``); + let block4 = createBlock(`
List of Available Remotes
LogoRemoteHostDatabaseSourceState
`); + let block6 = createBlock(`Remote logo`); return function template(ctx, node, key = "") { let b2, b4; @@ -45,11 +45,14 @@ export const templates = { let prop7 = new Boolean(ctx['state'].form.remote_datasrc==='helpdesk.ticket'); const v8 = ctx['state']; let hdlr10 = [()=>v8.form.remote_datasrc='helpdesk.ticket', ctx]; - let hdlr11 = [ctx['addRemote'], ctx]; - let hdlr12 = [ctx['loadRemotes'], ctx]; + let attr5 = ctx['formLogoSrc']; + let hdlr11 = [ctx['onRemoteLogoChange'], ctx]; + let hdlr12 = [ctx['clearRemoteLogo'], ctx]; + let hdlr13 = [ctx['addRemote'], ctx]; + let hdlr14 = [ctx['loadRemotes'], ctx]; const v9 = ctx['state']; - let hdlr13 = [()=>v9.showList=!v9.showList, ctx]; - let hdlr14 = [ctx['removeAllRemotes'], ctx]; + let hdlr15 = [()=>v9.showList=!v9.showList, ctx]; + let hdlr16 = [ctx['removeAllRemotes'], ctx]; if (ctx['state'].error) { const b3 = safeOutput(ctx['state'].error); b2 = block2([], [b3]); @@ -59,7 +62,8 @@ export const templates = { 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 key1 = ctx['remoteKey'](ctx['remote']); + let attr6 = ctx['remoteLogoSrc'](ctx['remote']); 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); @@ -67,17 +71,17 @@ export const templates = { const b11 = comp5({text: ctx['remote'].state||'Inactive',limit: 18}, key + `__5__${key1}`, node, this, null); const v10 = ctx['editRemote']; const v11 = ctx['remote']; - let hdlr15 = [()=>v10(v11), ctx]; + let hdlr17 = [()=>v10(v11), ctx]; const v12 = ctx['removeRemote']; const v13 = ctx['remote']; - let hdlr16 = [()=>v12(v13), ctx]; - c_block5[i1] = withKey(block6([hdlr15, hdlr16], [b7, b8, b9, b10, b11]), key1); + let hdlr18 = [()=>v12(v13), ctx]; + c_block5[i1] = withKey(block6([attr6, hdlr17, hdlr18], [b7, b8, b9, b10, b11]), key1); } ctx = ctx.__proto__; const b5 = list(c_block5); b4 = block4([], [b5]); } - return block1([attr1, hdlr1, attr2, hdlr2, attr3, attr4, hdlr3, prop1, hdlr4, prop2, hdlr5, prop3, hdlr6, prop4, hdlr7, prop5, hdlr8, prop6, hdlr9, prop7, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14], [b2, b4]); + return block1([attr1, hdlr1, attr2, hdlr2, attr3, attr4, hdlr3, prop1, hdlr4, prop2, hdlr5, prop3, hdlr6, prop4, hdlr7, prop5, hdlr8, prop6, hdlr9, prop7, hdlr10, attr5, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15, hdlr16], [b2, b4]); } }, @@ -96,7 +100,7 @@ export const templates = { const comp9 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); const comp10 = app.createComponent(`ReadMore`, true, false, false, ["text","limit"]); - let block1 = createBlock(`
Please wait — or grab a cup of coffee ☕
`); + let block1 = createBlock(`
Please wait — or grab a cup of coffee ☕
`); let block3 = createBlock(`

`); let block5 = createBlock(`
Hello 😉, you have not configured any remotes. Open Options below and add one.
`); let block6 = createBlock(`
`); @@ -124,33 +128,35 @@ export const templates = { let block44 = createBlock(`Hours Left`); let block45 = createBlock(``); let block48 = createBlock(``); - let block49 = createBlock(``); - let block50 = createBlock(``); - let block52 = createBlock(``); - let block53 = createBlock(``); - let block54 = createBlock(``); - let block56 = createBlock(``); - let block57 = createBlock(``); - let block58 = createBlock(``); - let block60 = createBlock(`
`); - let block62 = createBlock(``); - let block64 = createBlock(``); - let block66 = createBlock(``); - let block69 = createBlock(``); - let block71 = createBlock(``); - let block72 = createBlock(``); - let block73 = createBlock(``); - let block75 = createBlock(``); - let block78 = createBlock(``); + let block50 = createBlock(``); + let block51 = createBlock(``); + let block52 = createBlock(``); + let block54 = createBlock(``); + let block55 = createBlock(``); + let block56 = createBlock(``); + let block58 = createBlock(``); + let block59 = createBlock(``); + let block60 = createBlock(``); + let block62 = createBlock(`
`); + let block64 = createBlock(``); + let block66 = createBlock(``); + let block68 = createBlock(``); + let block71 = createBlock(``); + let block73 = createBlock(``); + let block74 = createBlock(``); + let block75 = createBlock(``); + let block77 = createBlock(``); let block80 = createBlock(``); - let block82 = createBlock(``); - let block84 = createBlock(` No matching items are currently available `); + let block82 = createBlock(``); + let block84 = createBlock(``); + let block86 = createBlock(` No matching items are currently available `); return function template(ctx, node, key = "") { - let b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b84, b85, b86, b87, b88, b89; + let b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b86, b87, b88, b89, b90, b91; let attr1 = ctx['state'].view==='loading'?'':'hide'; b2 = safeOutput(ctx['state'].busyMessage); let attr2 = ctx['state'].view==='login'?'login-view':'login-view hide'; + let attr3 = ctx['currentRemoteLogoSrc']; if (ctx['state'].bootError) { const b4 = safeOutput(ctx['state'].bootError); b3 = block3([], [b4]); @@ -172,16 +178,16 @@ export const templates = { b9 = block9([prop1, hdlr2]); } if (!ctx['state'].useExistingSession) { - let attr3 = ctx['state'].showPassword?'text':'password'; + let attr4 = 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]; - b10 = block10([attr3, prop2, hdlr3]); + b10 = block10([attr4, prop2, hdlr3]); } if (!ctx['state'].useExistingSession) { let hdlr4 = [ctx['togglePassword'], ctx]; - let attr4 = ctx['state'].showPassword?'fa fa-eye-slash':'fa fa-eye'; - b11 = block11([hdlr4, attr4]); + let attr5 = ctx['state'].showPassword?'fa fa-eye-slash':'fa fa-eye'; + b11 = block11([hdlr4, attr5]); } const v3 = ctx['state']; let hdlr5 = [(_ev)=>v3.selectedRemoteIndex=_ev.target.value, ctx]; @@ -189,11 +195,11 @@ export const templates = { const [k_block12, v_block12, l_block12, c_block12] = prepareList(ctx['state'].remotes);; for (let i1 = 0; i1 < l_block12; i1++) { ctx[`remote`] = k_block12[i1]; - const key1 = ctx['remote'].database+ctx['remote'].url; - let attr5 = ctx['remote'].__index; + const key1 = ctx['remoteKey'](ctx['remote']); + let attr6 = ctx['remote'].__index; let prop3 = new Boolean(ctx['state'].selectedRemoteIndex===ctx['remote'].__index); - const b14 = safeOutput(ctx['remote'].name); - c_block12[i1] = withKey(block13([attr5, prop3], [b14]), key1); + const b14 = safeOutput(ctx['remoteOptionLabel'](ctx['remote'])); + c_block12[i1] = withKey(block13([attr6, prop3], [b14]), key1); } ctx = ctx.__proto__; b12 = list(c_block12); @@ -209,7 +215,7 @@ export const templates = { } b6 = block6([hdlr1, hdlr5, prop4, hdlr6], [b7, b9, b10, b11, b12, b15, b16]); } - let attr6 = ctx['state'].view==='main'?'':'hide'; + let attr7 = ctx['state'].view==='main'?'':'hide'; if (ctx['state'].sourceError) { const b20 = safeOutput(ctx['state'].sourceError); b19 = block19([], [b20]); @@ -226,10 +232,10 @@ export const templates = { for (let i1 = 0; i1 < l_block22; i1++) { ctx[`stage`] = k_block22[i1]; const key1 = ctx['stage'].id; - let attr7 = ''+ctx['stage'].id; + let attr8 = ''+ctx['stage'].id; let prop7 = new Boolean(ctx['state'].helpdeskStageFilter===''+ctx['stage'].id); const b24 = safeOutput(ctx['stage'].name); - c_block22[i1] = withKey(block23([attr7, prop7], [b24]), key1); + c_block22[i1] = withKey(block23([attr8, prop7], [b24]), key1); } ctx = ctx.__proto__; const b22 = list(c_block22); @@ -295,112 +301,122 @@ export const templates = { for (let i1 = 0; i1 < l_block47; i1++) { ctx[`issue`] = k_block47[i1]; const key1 = ctx['issue'].id; - let b49, b50, b51, b68; - let attr8 = ctx['isActiveTimerItem'](ctx['issue'])?'active-row':''; + let b49, b52, b53, b70; + let attr9 = ctx['isActiveTimerItem'](ctx['issue'])?'active-row':''; if (!ctx['state'].activeTimerId) { - const v9 = ctx['startTimer']; - const v10 = ctx['issue']; - let hdlr18 = [()=>v9(v10), ctx]; - b49 = block49([hdlr18]); + let b50, b51; + if (ctx['isHelpdeskSource']&&ctx['helpdeskTimesheetBlocked'](ctx['issue'])) { + let attr10 = ctx['helpdeskTimesheetBlockMessage'](ctx['issue']); + const v9 = ctx['showHelpdeskTimesheetInfo']; + const v10 = ctx['issue']; + let hdlr18 = [()=>v9(v10), ctx]; + b50 = block50([attr10, hdlr18]); + } else { + const v11 = ctx['startTimer']; + const v12 = ctx['issue']; + let hdlr19 = [()=>v11(v12), ctx]; + b51 = block51([hdlr19]); + } + b49 = multi([b50, b51]); } if (ctx['isActiveTimerItem'](ctx['issue'])) { - const v11 = ctx['stopTimer']; - const v12 = ctx['issue']; - let hdlr19 = [()=>v11(v12), ctx]; - b50 = block50([hdlr19]); + const v13 = ctx['stopTimer']; + const v14 = ctx['issue']; + let hdlr20 = [()=>v13(v14), ctx]; + b52 = block52([hdlr20]); } if (ctx['isHelpdeskSource']) { - let b52, b54, b58, b62, b64, b66; + let b54, b56, b60, b64, b66, b68; if (ctx['showHelpdeskTherpLink']) { - let b53; + let b55; if (ctx['helpdeskTherpLinkHref'](ctx['issue'])) { - let attr9 = ctx['helpdeskTherpLinkHref'](ctx['issue']); - b53 = block53([attr9]); + let attr11 = ctx['helpdeskTherpLinkHref'](ctx['issue']); + b55 = block55([attr11]); } - b52 = block52([], [b53]); + b54 = block54([], [b55]); } - let b55, b57; + let b57, b59; if (ctx['issue'].priority_level.length) { ctx = Object.create(ctx); - const [k_block55, v_block55, l_block55, c_block55] = prepareList(ctx['issue'].priority_level);; - for (let i2 = 0; i2 < l_block55; i2++) { - ctx[`priority`] = k_block55[i2]; + const [k_block57, v_block57, l_block57, c_block57] = prepareList(ctx['issue'].priority_level);; + for (let i2 = 0; i2 < l_block57; i2++) { + ctx[`priority`] = k_block57[i2]; const key2 = ctx['priority']+'_'+ctx['issue'].id; - c_block55[i2] = withKey(block56(), key2); + c_block57[i2] = withKey(block58(), key2); } ctx = ctx.__proto__; - b55 = list(c_block55); + b57 = list(c_block57); } if (!ctx['issue'].priority_level.length) { - b57 = block57(); + b59 = block59(); } - b54 = block54([], [b55, b57]); - let b59, b60; - b59 = comp1({text: ctx['issueLabel'](ctx['issue']),limit: 55,href: ctx['issueHref'](ctx['issue'])}, key + `__1__${key1}`, node, this, null); + b56 = block56([], [b57, b59]); + let b61, b62; + b61 = comp1({text: ctx['issueLabel'](ctx['issue']),limit: 55,href: ctx['issueHref'](ctx['issue'])}, key + `__1__${key1}`, node, this, null); if (ctx['showHelpdeskDescription']&&ctx['helpdeskDescriptionText'](ctx['issue'])) { - const b61 = comp2({text: ctx['helpdeskDescriptionText'](ctx['issue']),limit: 70}, key + `__2__${key1}`, node, this, null); - b60 = block60([], [b61]); + const b63 = comp2({text: ctx['helpdeskDescriptionText'](ctx['issue']),limit: 70}, key + `__2__${key1}`, node, this, null); + b62 = block62([], [b63]); } - b58 = block58([], [b59, b60]); - const b63 = comp3({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__3__${key1}`, node, this, null); - b62 = block62([], [b63]); + b60 = block60([], [b61, b62]); + const b65 = comp3({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__3__${key1}`, node, this, null); + b64 = block64([], [b65]); if (ctx['showHelpdeskAssignee']) { - const b65 = comp4({text: ctx['relationLabel'](ctx['helpdeskAssigneeValue'](ctx['issue'])),limit: 22}, key + `__4__${key1}`, node, this, null); - b64 = block64([], [b65]); + const b67 = comp4({text: ctx['relationLabel'](ctx['helpdeskAssigneeValue'](ctx['issue'])),limit: 22}, key + `__4__${key1}`, node, this, null); + b66 = block66([], [b67]); } if (ctx['showHelpdeskHours']) { - const b67 = comp5({text: ctx['formatHours'](ctx['helpdeskHoursValue'](ctx['issue'])),limit: 10}, key + `__5__${key1}`, node, this, null); - b66 = block66([], [b67]); + const b69 = comp5({text: ctx['formatHours'](ctx['helpdeskHoursValue'](ctx['issue'])),limit: 10}, key + `__5__${key1}`, node, this, null); + b68 = block68([], [b69]); } - b51 = multi([b52, b54, b58, b62, b64, b66]); + b53 = multi([b54, b56, b60, b64, b66, b68]); } else { - let b69, b73, b75, b77, b82; - let b70, b72; + let b71, b75, b77, b79, b84; + let b72, b74; if (ctx['issue'].priority_level.length) { ctx = Object.create(ctx); - const [k_block70, v_block70, l_block70, c_block70] = prepareList(ctx['issue'].priority_level);; - for (let i2 = 0; i2 < l_block70; i2++) { - ctx[`priority`] = k_block70[i2]; + const [k_block72, v_block72, l_block72, c_block72] = prepareList(ctx['issue'].priority_level);; + for (let i2 = 0; i2 < l_block72; i2++) { + ctx[`priority`] = k_block72[i2]; const key2 = ctx['priority']+'_'+ctx['issue'].id; - c_block70[i2] = withKey(block71(), key2); + c_block72[i2] = withKey(block73(), key2); } ctx = ctx.__proto__; - b70 = list(c_block70); + b72 = list(c_block72); } if (!ctx['issue'].priority_level.length) { - b72 = block72(); + b74 = block74(); } - b69 = block69([], [b70, b72]); - const b74 = comp6({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__6__${key1}`, node, this, null); - b73 = block73([], [b74]); - const b76 = comp7({text: ctx['issueLabel'](ctx['issue']),limit: 70,href: ctx['issueHref'](ctx['issue'])}, key + `__7__${key1}`, node, this, null); + b71 = block71([], [b72, b74]); + const b76 = comp6({text: ctx['relationLabel'](ctx['issue'].stage_id),limit: 15}, key + `__6__${key1}`, node, this, null); b75 = block75([], [b76]); + const b78 = comp7({text: ctx['issueLabel'](ctx['issue']),limit: 70,href: ctx['issueHref'](ctx['issue'])}, key + `__7__${key1}`, node, this, null); + b77 = block77([], [b78]); if (ctx['state'].dataSource==='project.task') { - const b79 = comp8({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].effective_hours)),limit: 9}, key + `__8__${key1}`, node, this, null); - const b78 = block78([], [b79]); - const b81 = comp9({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].remaining_hours)),limit: 9}, key + `__9__${key1}`, node, this, null); + const b81 = comp8({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].effective_hours)),limit: 9}, key + `__8__${key1}`, node, this, null); const b80 = block80([], [b81]); - b77 = multi([b78, b80]); + const b83 = comp9({text: ctx['normalizeText'](ctx['formatHours'](ctx['issue'].remaining_hours)),limit: 9}, key + `__9__${key1}`, node, this, null); + const b82 = block82([], [b83]); + b79 = multi([b80, b82]); } - const b83 = comp10({text: ctx['relationLabel'](ctx['resourceRelationValue'](ctx['issue'])),limit: 22}, key + `__10__${key1}`, node, this, null); - b82 = block82([], [b83]); - b68 = multi([b69, b73, b75, b77, b82]); + const b85 = comp10({text: ctx['relationLabel'](ctx['resourceRelationValue'](ctx['issue'])),limit: 22}, key + `__10__${key1}`, node, this, null); + b84 = block84([], [b85]); + b70 = multi([b71, b75, b77, b79, b84]); } - c_block47[i1] = withKey(block48([attr8], [b49, b50, b51, b68]), key1); + c_block47[i1] = withKey(block48([attr9], [b49, b52, b53, b70]), key1); } ctx = ctx.__proto__; b47 = list(c_block47); } if (!ctx['filteredIssues'].length) { - let attr10 = ctx['tableColumnCount']; - b84 = block84([attr10]); + let attr12 = ctx['tableColumnCount']; + b86 = block86([attr12]); } - b85 = safeOutput(ctx['state'].serverVersion||'Unknown'); - b86 = safeOutput(ctx['state'].odooOWLVersion||'Unknown'); - b87 = safeOutput(ctx['state'].currentHost||'-'); - b88 = safeOutput(ctx['state'].currentDatabase||'-'); - b89 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); - return block1([attr1, attr2, attr6, prop5, hdlr7, prop8, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15], [b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b84, b85, b86, b87, b88, b89]); + b87 = safeOutput(ctx['state'].serverVersion||'Unknown'); + b88 = safeOutput(ctx['state'].odooOWLVersion||'Unknown'); + b89 = safeOutput(ctx['state'].currentHost||'-'); + b90 = safeOutput(ctx['state'].currentDatabase||'-'); + b91 = safeOutput(ctx['state'].user?ctx['state'].user.display_name:'-'); + return block1([attr1, attr2, attr3, attr7, prop5, hdlr7, prop8, hdlr9, hdlr10, hdlr11, hdlr12, hdlr13, hdlr14, hdlr15], [b2, b3, b5, b6, b19, b21, b25, b27, b36, b47, b86, b87, b88, b89, b90, b91]); } }, diff --git a/src/templates/options_app.xml b/src/templates/options_app.xml index d066a73..a41fef9 100644 --- a/src/templates/options_app.xml +++ b/src/templates/options_app.xml @@ -174,6 +174,23 @@
+
+ +
+ Remote logo preview +
+ +
PNG, JPEG, WebP or GIF; maximum 512 KB.
+ +
+
+
Controls
+ Logo Remote Host Database @@ -225,9 +243,13 @@ + + Remote logo + diff --git a/src/templates/popup_app.xml b/src/templates/popup_app.xml index 6e7b539..2104992 100644 --- a/src/templates/popup_app.xml +++ b/src/templates/popup_app.xml @@ -22,7 +22,7 @@