Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Version 2.1

- `Data Export` Introduced option toggle for Date/Time display formats (ISO 8601, American, European, Asian) and optimized formatting logic (contribution by [Camille Guillory](https://github.com/CamilleGuillory))
- `Popup` Add filter icon and menu on User tab search input [discussion #1147](https://github.com/tprouvot/Salesforce-Inspector-reloaded/discussions/1147)
- `Event Monitor` Allow users to generate, publish and save Platform Events based on their definition

Expand Down
14 changes: 6 additions & 8 deletions addon/data-export.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* global React ReactDOM */
import {sfConn, apiVersion} from "./inspector.js";
import {getLinkTarget, nullToEmptyString, isOptionEnabled, PromptTemplate, Constants, UserInfoModel, createSpinForMethod, copyToClipboard, downloadCsvFile, StorageHistory} from "./utils.js";
import {getLinkTarget, nullToEmptyString, isOptionEnabled, PromptTemplate, Constants, UserInfoModel, createSpinForMethod, copyToClipboard, downloadCsvFile, StorageHistory, formatDateCell, getDateFormatOptions} from "./utils.js";
/* global initButton */
import {Enumerable, DescribeInfo, initScrollTable, s} from "./data-load.js";
import {PageHeader} from "./components/PageHeader.js";
Expand Down Expand Up @@ -1245,13 +1245,10 @@ function RecordTable(vm) {
}
}
function cellToString(cell) {
if (cell == null) {
return "";
} else if (typeof cell == "object" && cell.attributes && cell.attributes.type) {
return "[" + cell.attributes.type + "]";
} else {
return "" + cell;
}
if (cell == null) return "";
if (typeof cell == "object" && cell.attributes?.type) return "[" + cell.attributes.type + "]";
const formatted = formatDateCell(cell, rt.dateFormatOptions);
return formatted !== null ? formatted : "" + cell;
}

let isVisible = (row, filter) => {
Expand Down Expand Up @@ -1292,6 +1289,7 @@ function RecordTable(vm) {
isTooling: false,
totalSize: -1,
preventLineWrap: vm.prefPreventLineWrap,
dateFormatOptions: getDateFormatOptions(),
addToTable(expRecords) {
rt.records = rt.records.concat(expRecords);
if (rt.table.length == 0 && expRecords.length > 0) {
Expand Down
29 changes: 4 additions & 25 deletions addon/data-load.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {sfConn, apiVersion} from "./inspector.js";
import {isRecordId} from "./utils.js";
import {formatDateCell, getDateFormatOptions, isRecordId} from "./utils.js";

const greyOutSkippedColumns = localStorage.getItem("greyOutSkippedColumns") === "true" && !window.location.href.includes("data-export");
// Inspired by C# System.Linq.Enumerable
Expand Down Expand Up @@ -305,11 +305,7 @@ function renderCell(rt, cell, td) {
// test the text to identify if this is a path to an eventLogFile
return /^\/services\/data\/v[0-9]{2,3}.[0-9]{1}\/sobjects\/EventLogFile\/[a-z0-9]{5}0000[a-z0-9]{9}\/LogFile$/i.exec(text);
}
function isDateTimeFormat(text) {
// test the text to identify if this is in Salesforce's dateTime format
// YYYY-MM-DDTHH:mm:ss[.SSSSSS][+hhmm]
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?([+-]\d{4})$/.test(text);
}

if (typeof cell == "object" && cell != null && cell.attributes && cell.attributes.type) {
if (cell.attributes.type == "AggregateResult") {
td.textContent = cell.attributes.type;
Expand Down Expand Up @@ -353,26 +349,9 @@ function renderCell(rt, cell, td) {
);
} else if (cell == null) {
td.textContent = "";
} else if (localStorage.getItem("showLocalTime") == "true" && isDateTimeFormat(cell) && typeof cell == "string") {
let textDate = new Date(cell);

// Get the local timezone offset in minutes and convert to hours and minutes
let offsetMinutes = textDate.getTimezoneOffset();
let offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
let offsetMinutesRemainder = Math.abs(offsetMinutes) % 60;

// Adjust the date to the local time based on the offset
textDate.setMinutes(textDate.getMinutes() - offsetMinutes);

// Format the date in the required format (YYYY-MM-DDTHH:mm:ss.sss+hhmm)
let localTime = textDate.toISOString().replace("Z", "") // Remove 'Z' from ISO string (UTC)
+ (offsetMinutes > 0 ? "-" : "+") // Use the appropriate sign based on offset
+ String(offsetHours).padStart(2, "0") // Format hours with leading zero
+ String(offsetMinutesRemainder).padStart(2, "0"); // Format minutes with leading zero

td.textContent = localTime;
} else {
td.textContent = cell;
const formatted = formatDateCell(cell, rt.dateFormatOptions || getDateFormatOptions());
td.textContent = formatted !== null ? formatted : cell;
}
}

Expand Down
40 changes: 35 additions & 5 deletions addon/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,23 @@ class OptionsTabSelector extends React.Component {
content: [
{option: CSVSeparatorOption, props: {key: 1}},
{option: Option, props: {type: "toggle", title: "Display Query Execution Time", key: "displayQueryPerformance", default: true}},
{option: Option, props: {type: "toggle", title: "Show Local Time", key: "showLocalTime", default: false}},
{option: MultiCheckboxButtonGroup,
props: {
title: "Date/Time Display Format",
key: "dateTimeFormat",
unique: true,
requireSelection: true,
tooltip: "Choose how date and time values are displayed in the data export table",
checkboxes: [
{label: "Salesforce Default (ISO 8601)", name: "iso8601", checked: true, tooltip: "YYYY-MM-DDTHH:MM:SS.sss+0000"},
{label: "American", name: "us", tooltip: "MM/DD/YYYY HH:MM:SS AM/PM"},
{label: "European", name: "european", tooltip: "DD/MM/YYYY HH:MM:SS"},
{label: "Asian", name: "asian", tooltip: "YYYY/MM/DD HH:MM:SS"}
]
}
},
{option: Option, props: {type: "toggle", title: "Use Local Timezone", key: "showLocalTime", default: false, tooltip: "When enabled, converts date/time values to your local timezone instead of UTC"}},
{option: Option, props: {type: "toggle", title: "Display Timezone", key: "displayTimezone", default: false, tooltip: "When enabled, displays the timezone abbreviation (e.g., PST, UTC) after the time"}},
{option: Option, props: {type: "toggle", title: "Use SObject context on Data Export ", key: "useSObjectContextOnDataImpoltrink", default: true}},
{option: Option, props: {type: "toggle", title: "Enable List View Export", key: "enableListViewExport", default: false, tooltip: "If enabled, Data Export link will be automatically populated with current ListView"}},
{option: MultiCheckboxButtonGroup,
Expand Down Expand Up @@ -1236,7 +1252,9 @@ class MultiCheckboxButtonGroup extends React.Component {
this.title = props.title;
this.key = props.storageKey;
this.unique = props.unique || false;
this.requireSelection = props.requireSelection || false;
this.length = props.length || 6;
this.tooltip = props.tooltip;

// Load checkboxes from localStorage or default to props.checkboxes
const storedCheckboxes = localStorage.getItem(this.key) ? JSON.parse(localStorage.getItem(this.key)) : [];
Expand All @@ -1259,8 +1277,15 @@ class MultiCheckboxButtonGroup extends React.Component {

handleCheckboxChange = (event) => {
const {name, checked} = event.target;

// Prevent unchecking the last item if selection is required
if (this.requireSelection && !checked && !this.state.checkboxes.some(cb => cb.name !== name && cb.checked)) {
return;
}

const updatedCheckboxes = this.state.checkboxes.map((checkbox) => ({
...checkbox,
// Unique mode: uncheck others if selecting this one. Otherwise standard toggle.
checked: this.unique && checked ? checkbox.name === name : checkbox.name === name ? checked : checkbox.checked
}));

Expand All @@ -1271,16 +1296,21 @@ class MultiCheckboxButtonGroup extends React.Component {
render() {
return h("div", {className: "slds-grid slds-border_bottom slds-p-horizontal_small slds-p-vertical_xx-small"},
h("div", {className: "slds-col slds-size_3-of-12 text-align-middle"},
h("span", {}, this.title)
h("span", {}, this.title,
this.tooltip && h(Tooltip, {tooltip: this.tooltip, idKey: this.key})
)
),
h("div", {className: "slds-col slds-size_" + this.length + "-of-12 slds-form-element slds-grid slds-grid_align-start slds-grid_vertical-align-center slds-gutters_small slds-m-left_xxx-small"},
h("div", {className: "slds-form-element__control"},
h("div", {className: "slds-checkbox_button-group"},
this.state.checkboxes.map((checkbox, index) =>
h("span", {className: "slds-button slds-checkbox_button", key: this.key + index},
h("input", {type: "checkbox", id: `${this.key}-${checkbox.value}-${index}`, name: checkbox.name, checked: checkbox.checked, onChange: this.handleCheckboxChange, title: checkbox.title}),
h("label", {className: "slds-checkbox_button__label", htmlFor: `${this.key}-${checkbox.value}-${index}`},
h("span", {className: "slds-checkbox_faux"}, checkbox.label)
h("input", {type: "checkbox", id: `${this.key}-${checkbox.value}-${index}`, name: checkbox.name, checked: checkbox.checked, onChange: this.handleCheckboxChange, title: checkbox.tooltip ? undefined : checkbox.title}),
h("label", {className: "slds-checkbox_button__label", htmlFor: `${this.key}-${checkbox.value}-${index}`, title: checkbox.tooltip ? undefined : checkbox.title},
h("span", {className: "slds-checkbox_faux", style: {display: "inline-flex", alignItems: "center", whiteSpace: "nowrap"}},
checkbox.label,
checkbox.tooltip && h("span", {onClick: (e) => e.stopPropagation(), style: {marginLeft: "4px", display: "inline-flex"}}, h(Tooltip, {tooltip: checkbox.tooltip, idKey: `${this.key}-${checkbox.name}`}))
)
)
)
)
Expand Down
98 changes: 98 additions & 0 deletions addon/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1362,3 +1362,101 @@ export function formatDuration(minutes) {

return parts.length > 0 ? parts.join(" ") : "Less than a minute";
}
const RX_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
const RX_DATE = /^\d{4}-\d{2}-\d{2}$/;

export const isDateTimeFormat = s => typeof s === "string" && RX_DATETIME.test(s);
export const isDateFormat = s => typeof s === "string" && RX_DATE.test(s);

export function getDateFormatOptions() {
let format = localStorage.getItem("dateTimeFormat") || "iso8601";
if (format.startsWith("[")) {
try {
format = JSON.parse(format).find(o => o.checked)?.name || "iso8601";
} catch {
format = "iso8601";
}
}
return {
format,
showLocalTime: localStorage.getItem("showLocalTime") === "true",
displayTimezone: localStorage.getItem("displayTimezone") === "true"
};
}

export const formatDateCell = (cell, opts) => {
if (typeof cell !== "string") return null;
if (RX_DATE.test(cell)) return formatDateTime(cell, opts, true);
if (RX_DATETIME.test(cell)) return formatDateTime(cell, opts, false);
return null;
};

const pad = (n, w = 2) => String(n).padStart(w, "0");

const getOffset = d => {
const off = -d.getTimezoneOffset();
const abs = Math.abs(off);
return `${off >= 0 ? "+" : "-"}${pad((abs / 60) | 0)}${pad(abs % 60)}`;
};

const getTz = (d, utc, show) => {
if (!show) return "";
if (utc) return " UTC";
try {
return " " + (new Intl.DateTimeFormat("en-US", {timeZoneName: "short"}).formatToParts(d).find(p => p.type === "timeZoneName")?.value || `UTC${getOffset(d)}`);
} catch {
return ` UTC${getOffset(d)}`;
}
};

export function formatDateTime(str, {format = "iso8601", showLocalTime = false, displayTimezone = false} = {}, dateOnly = false) {
if (!str) return "";

// Date-only optimization
if (dateOnly) {
const [Y, M, D] = str.split("-");
if (!Y) return str;
switch (format) {
case "us": return `${M}/${D}/${Y}`;
case "european": return `${D}/${M}/${Y}`;
case "asian": return `${Y}/${M}/${D}`;
default: return str; // iso8601
}
}

const d = new Date(str);
if (isNaN(d.getTime())) return str;

const utc = !showLocalTime;
// fast path for ISO8601 UTC which is common
if (format === "iso8601" && utc) {
return displayTimezone ? `${str} UTC` : str;
}

const g = k => d[`get${utc ? "UTC" : ""}${k}`]();
const Y = g("FullYear");
const M = g("Month") + 1;
const D = g("Date");
const h = g("Hours");
const m = g("Minutes");
const s = g("Seconds");
const tz = getTz(d, utc, displayTimezone);

switch (format) {
case "iso8601": {
const ms = g("Milliseconds");
return `${Y}-${pad(M)}-${pad(D)}T${pad(h)}:${pad(m)}:${pad(s)}.${pad(ms, 3)}${utc ? "+0000" : getOffset(d)}${tz}`;
}
case "us": {
const hr = h % 12 || 12;
const ampm = h < 12 ? "AM" : "PM";
return `${pad(M)}/${pad(D)}/${Y} ${pad(hr)}:${pad(m)}:${pad(s)} ${ampm}${tz}`;
}
case "european":
return `${pad(D)}/${pad(M)}/${Y} ${pad(h)}:${pad(m)}:${pad(s)}${tz}`;
case "asian":
return `${Y}/${pad(M)}/${pad(D)} ${pad(h)}:${pad(m)}:${pad(s)}${tz}`;
default:
return str;
}
}
Loading