Skip to content

Commit 47467bb

Browse files
Merge pull request #820 from NeurodataWithoutBorders/timezone
Timezone Control in the GUIDE
2 parents db12c65 + 8473378 commit 47467bb

23 files changed

+454
-186
lines changed

environments/environment-Linux.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ dependencies:
2121
- pytest-cov == 4.1.0
2222
- scikit-learn == 1.4.0
2323
- tqdm_publisher >= 0.0.1
24+
- tzlocal >= 5.2

environments/environment-MAC-apple-silicon.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ dependencies:
2727
- pytest-cov == 4.1.0
2828
- scikit-learn == 1.4.0
2929
- tqdm_publisher >= 0.0.1
30+
- tzlocal >= 5.2

environments/environment-MAC-intel.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ dependencies:
2424
- pytest-cov == 4.1.0
2525
- scikit-learn == 1.4.0
2626
- tqdm_publisher >= 0.0.1
27+
- tzlocal >= 5.2

environments/environment-Windows.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ dependencies:
2424
- pytest-cov == 4.1.0
2525
- scikit-learn == 1.4.0
2626
- tqdm_publisher >= 0.0.1
27+
- tzlocal >= 5.2

src/electron/frontend/core/components/Dashboard.js

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,26 @@ export class Dashboard extends LitElement {
245245

246246
this.page.set(toPass, false);
247247

248+
// Constrain based on workflow configuration
249+
const workflowConfig = page.workflow ?? (page.workflow = {});
250+
const workflowValues = page.info.globalState?.project?.workflow ?? {};
251+
252+
// Define the value for each workflow value
253+
Object.entries(workflowValues).forEach(([key, value]) => {
254+
const config = workflowConfig[key] ?? (workflowConfig[key] = {});
255+
config.value = value;
256+
});
257+
258+
// Toggle elements based on workflow configuration
259+
Object.entries(workflowConfig).forEach(([key, config]) => {
260+
const { value, elements } = config;
261+
if (elements) {
262+
if (value) elements.forEach((el) => el.removeAttribute("hidden"));
263+
else elements.forEach((el) => el.setAttribute("hidden", true));
264+
}
265+
});
266+
267+
// Ensure that all states are synced to the proper state for this page (e.g. conversions have been run)
248268
this.page
249269
.checkSyncState()
250270
.then(async () => {
@@ -254,25 +274,34 @@ export class Dashboard extends LitElement {
254274
? `<h4 style="margin-bottom: 0px;">${projectName}</h4><small>Conversion Pipeline</small>`
255275
: projectName;
256276

257-
this.updateSections({ sidebar: false, main: true });
258-
259-
if (this.#transitionPromise.value) this.#transitionPromise.trigger(page); // This ensures calls to page.to() can be properly awaited until the next page is ready
260-
261277
const { skipped } = this.subSidebar.sections[info.section]?.pages?.[info.id] ?? {};
262278

263279
if (skipped) {
264280
if (isStorybook) return; // Do not skip on storybook
265281

266-
// Run skip functions
267-
Object.entries(page.workflow).forEach(([key, state]) => {
268-
if (typeof state.skip === "function") state.skip();
269-
});
270-
271-
// Skip right over the page if configured as such
272-
if (previous && previous.info.previous === this.page) await this.page.onTransition(-1);
273-
else await this.page.onTransition(1);
282+
const backwards = previous && previous.info.previous === this.page;
283+
284+
return (
285+
Promise.all(
286+
Object.entries(page.workflow).map(async ([_, state]) => {
287+
if (typeof state.skip === "function" && !backwards) return await state.skip(); // Run skip functions
288+
})
289+
)
290+
291+
// Skip right over the page if configured as such
292+
.then(async () => {
293+
if (backwards) await this.main.onTransition(-1);
294+
else await this.main.onTransition(1);
295+
})
296+
);
274297
}
298+
299+
page.requestUpdate(); // Re-render the page on each load
300+
301+
// Update main to render page
302+
this.updateSections({ sidebar: false, main: true });
275303
})
304+
276305
.catch((e) => {
277306
const previousId = previous?.info?.id ?? -1;
278307
this.main.onTransition(previousId); // Revert back to previous page
@@ -283,6 +312,9 @@ export class Dashboard extends LitElement {
283312
: `<h4 style="margin: 0">Fallback to previous page after error occurred</h4><small>${e}</small>`,
284313
"error"
285314
);
315+
})
316+
.finally(() => {
317+
if (this.#transitionPromise.value) this.#transitionPromise.trigger(this.main.page); // This ensures calls to page.to() can be properly awaited until the next page is ready
286318
});
287319
}
288320

@@ -342,9 +374,15 @@ export class Dashboard extends LitElement {
342374
if (!active) active = this.activePage; // default to active page
343375

344376
this.main.onTransition = async (transition) => {
345-
const promise = (this.#transitionPromise.value = new Promise(
346-
(resolve) => (this.#transitionPromise.trigger = resolve)
347-
));
377+
const promise =
378+
this.#transitionPromise.value ??
379+
(this.#transitionPromise.value = new Promise(
380+
(resolve) =>
381+
(this.#transitionPromise.trigger = (value) => {
382+
delete this.#transitionPromise.value;
383+
resolve(value);
384+
})
385+
));
348386

349387
if (typeof transition === "number") {
350388
const info = this.page.info;

src/electron/frontend/core/components/DateTimeSelector.js

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
11
import { LitElement, css } from "lit";
2+
import { getTimezoneOffset, formatTimezoneOffset } from "../../../../schemas/timezone.schema";
23

3-
const convertToDateTimeLocalString = (date) => {
4+
// Function to format the GMT offset
5+
export function extractISOString(date = new Date(), { offset = false, timezone = undefined } = {}) {
6+
if (typeof date === "string") date = new Date();
7+
8+
// Extract the GMT offset
9+
const offsetMs = getTimezoneOffset(date, timezone);
10+
const gmtOffset = formatTimezoneOffset(offsetMs);
11+
12+
// Format the date back to the original format with GMT offset
413
const year = date.getFullYear();
5-
const month = (date.getMonth() + 1).toString().padStart(2, "0");
6-
const day = date.getDate().toString().padStart(2, "0");
7-
const hours = date.getHours().toString().padStart(2, "0");
8-
const minutes = date.getMinutes().toString().padStart(2, "0");
9-
return `${year}-${month}-${day}T${hours}:${minutes}`;
14+
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-indexed
15+
const day = String(date.getDate()).padStart(2, "0");
16+
const hours = String(date.getHours()).padStart(2, "0");
17+
const minutes = String(date.getMinutes()).padStart(2, "0");
18+
const seconds = String(date.getSeconds()).padStart(2, "0");
19+
20+
// Recreate the ISO string with the GMT offset
21+
const formattedDate = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
22+
return offset ? formattedDate + gmtOffset : formattedDate;
23+
}
24+
25+
export const renderDateTime = (value) => {
26+
if (typeof value === "string") return extractISOString(new Date(value));
27+
else if (value instanceof Date) return extractISOString(value);
28+
return value;
1029
};
1130

31+
export const resolveDateTime = renderDateTime;
32+
1233
export class DateTimeSelector extends LitElement {
1334
static get styles() {
1435
return css`
@@ -20,31 +41,33 @@ export class DateTimeSelector extends LitElement {
2041
}
2142

2243
get value() {
23-
return this.input.value;
44+
const date = new Date(this.input.value);
45+
const resolved = resolveDateTime(date);
46+
47+
console.log(this.input.value, resolved);
48+
// return this.input.value;
49+
return resolved;
2450
}
2551

2652
set value(newValue) {
27-
if (newValue) this.input.value = newValue;
28-
else {
29-
const d = new Date();
30-
d.setHours(0, 0, 0, 0);
31-
this.input.value = convertToDateTimeLocalString(d);
32-
}
53+
const date = newValue ? new Date(newValue) : new Date();
54+
if (!newValue) date.setHours(0, 0, 0, 0);
55+
this.input.value = resolveDateTime(date);
3356
}
3457
get min() {
3558
return this.input.min;
3659
}
3760

38-
set min(newValue) {
39-
this.input.min = newValue;
61+
set min(value) {
62+
this.input.min = value;
4063
}
4164

4265
get max() {
4366
return this.input.max;
4467
}
4568

46-
set max(newValue) {
47-
this.input.max = newValue;
69+
set max(value) {
70+
this.input.max = value;
4871
}
4972

5073
constructor({ value, min, max } = {}) {

src/electron/frontend/core/components/JSONSchemaForm.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { resolveProperties } from "./pages/guided-mode/data/utils";
1212
import { JSONSchemaInput, getEditableItems } from "./JSONSchemaInput";
1313
import { InspectorListItem } from "./preview/inspector/InspectorList";
1414

15+
import { Validator } from "jsonschema";
16+
import { successHue, warningHue, errorHue } from "./globals";
17+
import { Button } from "./Button";
18+
1519
const encode = (str) => {
1620
try {
1721
document.querySelector(`#${str}`);
@@ -65,10 +69,6 @@ const additionalPropPattern = "additional";
6569

6670
const templateNaNMessage = `<br/><small>Type <b>NaN</b> to represent an unknown value.</small>`;
6771

68-
import { Validator } from "jsonschema";
69-
import { successHue, warningHue, errorHue } from "./globals";
70-
import { Button } from "./Button";
71-
7272
var validator = new Validator();
7373

7474
const isObject = (item) => {
@@ -902,14 +902,15 @@ export class JSONSchemaForm extends LitElement {
902902
if (!parent) parent = this.#get(path, this.resolved);
903903
if (!schema) schema = this.getSchema(localPath);
904904

905-
const value = parent[name];
905+
let value = parent[name];
906906

907907
const skipValidation = this.validateEmptyValues === null && value === undefined;
908908

909909
const validateArgs = input.pattern || skipValidation ? [] : [value, schema];
910910

911911
// Run validation functions
912912
const jsonSchemaErrors = validateArgs.length === 2 ? this.validateSchema(...validateArgs, name) : [];
913+
913914
const valid = skipValidation ? true : await this.validateOnChange(name, parent, pathToValidate, value);
914915

915916
if (valid === null) return null; // Skip validation / data change if the value is null

src/electron/frontend/core/components/JSONSchemaInput.js

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,10 @@ import tippy from "tippy.js";
1616
import { merge } from "./pages/utils";
1717
import { OptionalSection } from "./OptionalSection";
1818
import { InspectorListItem } from "./preview/inspector/InspectorList";
19+
import { renderDateTime, resolveDateTime } from "./DateTimeSelector";
1920

2021
const isDevelopment = !!import.meta.env;
2122

22-
const dateTimeRegex = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/;
23-
24-
function resolveDateTime(value) {
25-
if (typeof value === "string") {
26-
const match = value.match(dateTimeRegex);
27-
if (match) return `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}`;
28-
return value;
29-
}
30-
31-
return value;
32-
}
33-
3423
export function createTable(fullPath, { onUpdate, onThrow, overrides = {} }) {
3524
const name = fullPath.slice(-1)[0];
3625
const path = fullPath.slice(0, -1);
@@ -1226,7 +1215,7 @@ export class JSONSchemaInput extends LitElement {
12261215
? "datetime-local"
12271216
: schema.format ?? (schema.type === "string" ? "text" : schema.type);
12281217

1229-
const value = isDateTime ? resolveDateTime(this.value) : this.value;
1218+
const value = isDateTime ? renderDateTime(this.value) : this.value;
12301219

12311220
const { minimum, maximum, exclusiveMax, exclusiveMin } = schema;
12321221
const min = exclusiveMin ?? minimum;
@@ -1249,6 +1238,7 @@ export class JSONSchemaInput extends LitElement {
12491238
12501239
if (isInteger) value = newValue = parseInt(value);
12511240
else if (isNumber) value = newValue = parseFloat(value);
1241+
else if (isDateTime) value = newValue = resolveDateTime(value);
12521242
12531243
if (isNumber) {
12541244
if ("min" in schema && newValue < schema.min) newValue = schema.min;

src/electron/frontend/core/components/Main.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,24 +73,6 @@ export class Main extends LitElement {
7373
page.onTransition = this.onTransition;
7474
page.updatePages = this.updatePages;
7575

76-
// Constrain based on workflow configuration
77-
const workflowConfig = page.workflow ?? (page.workflow = {});
78-
const workflowValues = page.info.globalState?.project?.workflow ?? {};
79-
80-
Object.entries(workflowConfig).forEach(([key, state]) => {
81-
workflowConfig[key].value = workflowValues[key];
82-
83-
const value = workflowValues[key];
84-
85-
if (state.elements) {
86-
const elements = state.elements;
87-
if (value) elements.forEach((el) => el.removeAttribute("hidden"));
88-
else elements.forEach((el) => el.setAttribute("hidden", true));
89-
}
90-
});
91-
92-
page.requestUpdate(); // Ensure the page is re-rendered with new workflow configurations
93-
9476
if (this.content)
9577
this.toRender = toRender.page ? toRender : { page }; // Ensure re-render in either case
9678
else this.#queue.push(page);

src/electron/frontend/core/components/Search.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class Search extends LitElement {
3333
}
3434

3535
#close = () => {
36+
console.log("CLOSING", this.getSelectedOption());
3637
if (this.listMode === "input" && this.getAttribute("interacted") === "true") {
3738
this.setAttribute("interacted", false);
3839
this.#onSelect(this.getSelectedOption());

0 commit comments

Comments
 (0)