Conversation
…440-Add-event-date-to-popup-in-map
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the ✨ Finishing touches🧪 Generate unit tests
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
enferno/static/js/common/config.js (1)
546-548: Expose event dates safely; consider normalizing nulls
- Add small normalization to avoid undefined leaks (use null coalescing).
- No blocking issues otherwise.
- x.location.from_date = x.from_date; - x.location.to_date = x.to_date; - x.location.estimated = Boolean(x.estimated); + x.location.from_date = x.from_date ?? null; + x.location.to_date = x.to_date ?? null; + x.location.estimated = Boolean(x.estimated);enferno/static/js/components/GlobalMap.js (1)
42-44: Avoid global mutation for snack functionAssigning window.showSnack creates a global footgun and cross-component coupling.
- mounted() { - window.showSnack = this.$root.showSnack; + mounted() { + // Prefer a scoped helper stored on this instance + this._showSnack = (msg) => this.$root.showSnack(msg);And update inline usage below to call a stable handler (see next comment for removing inline onclick altogether).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
enferno/admin/templates/admin/jsapi.jinja2(1 hunks)enferno/static/js/common/config.js(1 hunks)enferno/static/js/components/GlobalMap.js(2 hunks)enferno/static/js/mixins/global-mixin.js(2 hunks)
🔇 Additional comments (2)
enferno/static/js/mixins/global-mixin.js (1)
85-90: Hide midnight time: good; verify TZ semanticsLooks good. Midnight check happens after UTC/local/tz adjustments, which is correct. Verify this still yields dates (no time) for midnight timestamps across UTC vs local.
enferno/admin/templates/admin/jsapi.jinja2 (1)
29-30: Translations added for clipboard UXLGTM. Keys are consistent with naming and used in GlobalMap. Ensure these get pulled into all locales.
| generatePopupContent(loc) { | ||
| function renderIfExists(label, value) { | ||
| return value ? `${label ? `<b>${label}:</b>` : ''} ${value}` : '' | ||
| const t = this.translations | ||
|
|
||
| // Dates (use mdi-calendar-clock) | ||
| const dates = [] | ||
| if (loc.from_date) { | ||
| dates.push(`<span class="chip"> | ||
| ${this.$root.formatDate(loc.from_date)} | ||
| </span>`) | ||
| } | ||
| // Simple HTML structure for popup content. Adjust as needed. | ||
| return `<div class="popup-content"> | ||
| <h4>${renderIfExists('', loc.title)}</h4> | ||
| <div>${renderIfExists(this.translations.number_, loc.number)} ${renderIfExists(this.translations.parentId_, loc.parentId)}</div> | ||
| <div>${renderIfExists(this.translations.coordinates_, `${loc.lat?.toFixed(6)}, ${loc.lng?.toFixed(6)}`)}</div> | ||
| <div>${renderIfExists('', loc.full_string)}</div> | ||
| ${loc.main ? `<div>${this.translations.mainIncident_}</div>` : ''} | ||
| <div>${renderIfExists(this.translations.type_, loc.geotype?.title)}</div> | ||
| <div>${renderIfExists(this.translations.eventType_, loc.eventtype)}</div> | ||
| </div>`; | ||
| if (loc.to_date) { | ||
| dates.push(`<span><i class="mdi mdi-arrow-right mr-1"></i>${this.$root.formatDate(loc.to_date)}</span>`) | ||
| } | ||
|
|
||
| // Estimated → calendar-question icon w/ tooltip | ||
| const estimated = loc.estimated | ||
| ? `<i class="mdi mdi-calendar-question mr-1" title="${loc.comment || t.timingForThisEventIsEstimated_}"></i>` | ||
| : '<i class="mdi mdi-calendar-clock mr-1"></i>' | ||
|
|
||
| const dateRange = dates.length | ||
| ? `<div class="d-flex">${estimated} ${dates.join(' ')}</div>` | ||
| : '' | ||
|
|
||
| // ParentId check | ||
| const parentId = | ||
| loc?.parent?.id | ||
| ? `<div>#${loc.parent?.id}</div>` | ||
| : '' | ||
|
|
||
| // Copy button for coordinates (added next to location) | ||
| const copyCoordsBtn = | ||
| loc.lat && loc.lng | ||
| ? `<button class="ml-1" title="${t.copyCoordinates_}" onclick="navigator.clipboard.writeText('${loc.lat.toFixed(6)}, ${loc.lng.toFixed(6)}'); window.showSnack('${translations.copiedToClipboard_}')"> | ||
| <i class="mdi mdi-content-copy"></i> | ||
| </button>` | ||
| : '' | ||
|
|
||
| // Event type with icon (assuming eventtype.icon available) | ||
| const eventType = loc.eventtype | ||
| ? `<div class="d-flex align-center"> | ||
| <i class="mdi mdi-calendar-range mr-1"></i>${loc.eventtype || ''} | ||
| </div>` | ||
| : '' | ||
|
|
||
| // Event type with icon (assuming eventtype.icon available) | ||
| const locNumber = loc.number | ||
| ? `<div class="d-flex align-center"> | ||
| #${loc.number || ''} | ||
| </div>` | ||
| : '' | ||
|
|
||
| return `<div class="popup-content d-flex flex-column ga-1"> | ||
| <div class="d-flex ga-2">${locNumber} ${parentId} ${eventType}</div> | ||
| <h4>${loc.title || ''}</h4> | ||
| <div class="d-flex align-center"> | ||
| <i class="mdi mdi-map-marker-outline mr-1"></i> | ||
| ${loc.full_string || ''} | ||
| ${copyCoordsBtn} | ||
| </div> | ||
| ${loc.main ? `<div>${t.mainIncident_}</div>` : ''} | ||
| ${dateRange} | ||
| </div>` | ||
| }, |
There was a problem hiding this comment.
XSS risk: HTML string assembly with unescaped user content
loc.title, loc.full_string, loc.eventtype, loc.comment are interpolated directly into HTML/attributes. If any are user-controlled, this is an XSS vector.
Apply escaping and avoid inline event handlers:
generatePopupContent(loc) {
- const t = this.translations
+ const t = this.translations
+ const escapeHtml = (s) => s == null ? '' : String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
// Dates (use mdi-calendar-clock)
const dates = []
if (loc.from_date) {
- dates.push(`<span class="chip">
- ${this.$root.formatDate(loc.from_date)}
- </span>`)
+ dates.push(`<span class="chip">${escapeHtml(this.$root.formatDate(loc.from_date))}</span>`)
}
if (loc.to_date) {
- dates.push(`<span><i class="mdi mdi-arrow-right mr-1"></i>${this.$root.formatDate(loc.to_date)}</span>`)
+ dates.push(`<span><i class="mdi mdi-arrow-right mr-1"></i>${escapeHtml(this.$root.formatDate(loc.to_date))}</span>`)
}
// Estimated → calendar-question icon w/ tooltip
- const estimated = loc.estimated
- ? `<i class="mdi mdi-calendar-question mr-1" title="${loc.comment || t.timingForThisEventIsEstimated_}"></i>`
- : '<i class="mdi mdi-calendar-clock mr-1"></i>'
+ const estimated = loc.estimated
+ ? `<i class="mdi mdi-calendar-question mr-1" title="${escapeHtml(loc.comment || t.timingForThisEventIsEstimated_)}"></i>`
+ : '<i class="mdi mdi-calendar-clock mr-1"></i>'
const dateRange = dates.length
? `<div class="d-flex">${estimated} ${dates.join(' ')}</div>`
: ''
// ParentId check
- const parentId =
- loc?.parent?.id
- ? `<div>#${loc.parent?.id}</div>`
- : ''
+ const parentId = (loc.parentId ?? loc?.parent?.id) != null
+ ? `<div>#${escapeHtml(String(loc.parentId ?? loc.parent?.id))}</div>`
+ : ''
// Copy button for coordinates (added next to location)
- const copyCoordsBtn =
- loc.lat && loc.lng
- ? `<button class="ml-1" title="${t.copyCoordinates_}" onclick="navigator.clipboard.writeText('${loc.lat.toFixed(6)}, ${loc.lng.toFixed(6)}'); window.showSnack('${translations.copiedToClipboard_}')">
- <i class="mdi mdi-content-copy"></i>
- </button>`
- : ''
+ const copyCoordsBtn =
+ Number.isFinite(loc.lat) && Number.isFinite(loc.lng)
+ ? `<button type="button" class="ml-1" title="${escapeHtml(t.copyCoordinates_)}" data-copy="${loc.lat.toFixed(6)}, ${loc.lng.toFixed(6)}">
+ <i class="mdi mdi-content-copy" aria-hidden="true"></i>
+ <span class="sr-only">${escapeHtml(t.copyCoordinates_)}</span>
+ </button>`
+ : ''
// Event type with icon (assuming eventtype.icon available)
const eventType = loc.eventtype
? `<div class="d-flex align-center">
- <i class="mdi mdi-calendar-range mr-1"></i>${loc.eventtype || ''}
+ <i class="mdi mdi-calendar-range mr-1"></i>${escapeHtml(loc.eventtype || '')}
</div>`
: ''
// Event type with icon (assuming eventtype.icon available)
const locNumber = loc.number
? `<div class="d-flex align-center">
- #${loc.number || ''}
+ #${escapeHtml(String(loc.number || ''))}
</div>`
: ''
- return `<div class="popup-content d-flex flex-column ga-1">
+ const html = `<div class="popup-content d-flex flex-column ga-1">
<div class="d-flex ga-2">${locNumber} ${parentId} ${eventType}</div>
- <h4>${loc.title || ''}</h4>
+ <h4>${escapeHtml(loc.title || '')}</h4>
<div class="d-flex align-center">
<i class="mdi mdi-map-marker-outline mr-1"></i>
- ${loc.full_string || ''}
+ ${escapeHtml(loc.full_string || '')}
${copyCoordsBtn}
</div>
${loc.main ? `<div>${t.mainIncident_}</div>` : ''}
${dateRange}
- </div>`
+ </div>`;
+
+ // Turn string into DOM to attach a safe click handler
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = html;
+ const btn = wrapper.querySelector('button[data-copy]');
+ if (btn) {
+ btn.addEventListener('click', () => {
+ navigator.clipboard.writeText(btn.getAttribute('data-copy'))
+ .then(() => this.$root.showSnack(translations.copiedToClipboard_))
+ .catch(() => this.$root.showSnack(translations.oops_));
+ });
+ }
+ return wrapper;
},Notes:
- Returns a DOM node (Leaflet bindPopup accepts DOM elements), avoiding inline JS.
- Escapes all user-facing strings and attribute values.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In enferno/static/js/components/GlobalMap.js around lines 71 to 133, the popup
HTML is built by concatenating unescaped loc fields into strings and uses inline
onclick, causing an XSS risk; instead, build and return a DOM element (Leaflet
accepts nodes) and set all user-controlled values via textContent or
setAttribute with proper escaping, remove inline event handlers (attach event
listeners to buttons after creating the node), and avoid injecting raw
loc.comment/loc.title/loc.full_string/eventtype into HTML — use sanitized or
escaped values and .textContent, for coordinates put the raw values in a data-
attribute and attach a click handler that reads data- attributes and uses
navigator.clipboard, and ensure tooltip/title attributes are safely escaped or
created via element.title rather than string interpolation.
| const parentId = | ||
| loc?.parent?.id | ||
| ? `<div>#${loc.parent?.id}</div>` | ||
| : '' | ||
|
|
There was a problem hiding this comment.
Parent ID lookup uses wrong property
You assign parentId in the aggregators, not parent.id. Use loc.parentId (fallback to loc.parent?.id only if present).
See fix in the previous diff.
🤖 Prompt for AI Agents
In enferno/static/js/components/GlobalMap.js around lines 95 to 99, the code
builds parentId from loc.parent?.id but the correct source is loc.parentId (use
loc.parent?.id only as a fallback when parentId is missing); update the
assignment to prefer loc.parentId and fall back to loc.parent?.id, and ensure
this change is applied where aggregators compute parentId (not using parent.id)
so the displayed value uses the correct property.
| const copyCoordsBtn = | ||
| loc.lat && loc.lng | ||
| ? `<button class="ml-1" title="${t.copyCoordinates_}" onclick="navigator.clipboard.writeText('${loc.lat.toFixed(6)}, ${loc.lng.toFixed(6)}'); window.showSnack('${translations.copiedToClipboard_}')"> | ||
| <i class="mdi mdi-content-copy"></i> | ||
| </button>` | ||
| : '' | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Copy button skips valid 0 coordinates and swallows clipboard errors
- 0 is falsy; current check hides the button at lat/lng = 0.
- Unhandled clipboard rejections degrade UX on HTTP or denied permissions.
See robust check and promise handling in the previous diff (Number.isFinite + then/catch).
🤖 Prompt for AI Agents
In enferno/static/js/components/GlobalMap.js around lines 101 to 107, the
copy-coordinates button currently hides when lat or lng is 0 and does not handle
clipboard promise rejections; change the presence check to use
Number.isFinite(loc.lat) && Number.isFinite(loc.lng) so 0 is treated as valid,
and replace the inline onclick clipboard call with
navigator.clipboard.writeText(...).then(() =>
window.showSnack(translations.copiedToClipboard_)).catch(err => {
console.error(err); window.showSnack(translations.copyFailed_ || 'Copy failed');
}) to surface errors and avoid unhandled rejections (ensure you add or reuse a
translation key for copy failure if needed).
… each is coming from
…440-Add-event-date-to-popup-in-map
level09
left a comment
There was a problem hiding this comment.
All good , just small improvement can be added.
|
|
||
| const btn = wrapper.querySelector('button[data-copy]'); | ||
| if (btn) { | ||
| btn.addEventListener('click', () => { |
There was a problem hiding this comment.
Small improvement:
Clipboard copy can fail on non localhost, if user don't grant permissions or has some security settings / non https etc .. , right now this fails silently with this console error:
GlobalMap.js:117 Uncaught TypeError: Cannot read properties of undefined (reading 'writeText')
at HTMLButtonElement.<anonymous> (GlobalMap.js:117:31)
let's just handle that case
.catch(() => this.$root.showSnack('Failed to copy
coordinates'));
There was a problem hiding this comment.
Let's address in another PR, merging this one for now.
Jira Issue
Description
Tweaks to event popup ux in map.
Checklist
API Changes (if applicable)
Additional Notes
[Any other relevant information]
Summary by CodeRabbit