Skip to content

Commit

Permalink
Merge pull request #820 from NeurodataWithoutBorders/timezone
Browse files Browse the repository at this point in the history
Timezone Control in the GUIDE
  • Loading branch information
CodyCBakerPhD authored Jun 5, 2024
2 parents db12c65 + 8473378 commit 47467bb
Show file tree
Hide file tree
Showing 23 changed files with 454 additions and 186 deletions.
1 change: 1 addition & 0 deletions environments/environment-Linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ dependencies:
- pytest-cov == 4.1.0
- scikit-learn == 1.4.0
- tqdm_publisher >= 0.0.1
- tzlocal >= 5.2
1 change: 1 addition & 0 deletions environments/environment-MAC-apple-silicon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ dependencies:
- pytest-cov == 4.1.0
- scikit-learn == 1.4.0
- tqdm_publisher >= 0.0.1
- tzlocal >= 5.2
1 change: 1 addition & 0 deletions environments/environment-MAC-intel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ dependencies:
- pytest-cov == 4.1.0
- scikit-learn == 1.4.0
- tqdm_publisher >= 0.0.1
- tzlocal >= 5.2
1 change: 1 addition & 0 deletions environments/environment-Windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ dependencies:
- pytest-cov == 4.1.0
- scikit-learn == 1.4.0
- tqdm_publisher >= 0.0.1
- tzlocal >= 5.2
68 changes: 53 additions & 15 deletions src/electron/frontend/core/components/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,26 @@ export class Dashboard extends LitElement {

this.page.set(toPass, false);

// Constrain based on workflow configuration
const workflowConfig = page.workflow ?? (page.workflow = {});
const workflowValues = page.info.globalState?.project?.workflow ?? {};

// Define the value for each workflow value
Object.entries(workflowValues).forEach(([key, value]) => {
const config = workflowConfig[key] ?? (workflowConfig[key] = {});
config.value = value;
});

// Toggle elements based on workflow configuration
Object.entries(workflowConfig).forEach(([key, config]) => {
const { value, elements } = config;
if (elements) {
if (value) elements.forEach((el) => el.removeAttribute("hidden"));
else elements.forEach((el) => el.setAttribute("hidden", true));
}
});

// Ensure that all states are synced to the proper state for this page (e.g. conversions have been run)
this.page
.checkSyncState()
.then(async () => {
Expand All @@ -254,25 +274,34 @@ export class Dashboard extends LitElement {
? `<h4 style="margin-bottom: 0px;">${projectName}</h4><small>Conversion Pipeline</small>`
: projectName;

this.updateSections({ sidebar: false, main: true });

if (this.#transitionPromise.value) this.#transitionPromise.trigger(page); // This ensures calls to page.to() can be properly awaited until the next page is ready

const { skipped } = this.subSidebar.sections[info.section]?.pages?.[info.id] ?? {};

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

// Run skip functions
Object.entries(page.workflow).forEach(([key, state]) => {
if (typeof state.skip === "function") state.skip();
});

// Skip right over the page if configured as such
if (previous && previous.info.previous === this.page) await this.page.onTransition(-1);
else await this.page.onTransition(1);
const backwards = previous && previous.info.previous === this.page;

return (
Promise.all(
Object.entries(page.workflow).map(async ([_, state]) => {
if (typeof state.skip === "function" && !backwards) return await state.skip(); // Run skip functions
})
)

// Skip right over the page if configured as such
.then(async () => {
if (backwards) await this.main.onTransition(-1);
else await this.main.onTransition(1);
})
);
}

page.requestUpdate(); // Re-render the page on each load

// Update main to render page
this.updateSections({ sidebar: false, main: true });
})

.catch((e) => {
const previousId = previous?.info?.id ?? -1;
this.main.onTransition(previousId); // Revert back to previous page
Expand All @@ -283,6 +312,9 @@ export class Dashboard extends LitElement {
: `<h4 style="margin: 0">Fallback to previous page after error occurred</h4><small>${e}</small>`,
"error"
);
})
.finally(() => {
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
});
}

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

this.main.onTransition = async (transition) => {
const promise = (this.#transitionPromise.value = new Promise(
(resolve) => (this.#transitionPromise.trigger = resolve)
));
const promise =
this.#transitionPromise.value ??
(this.#transitionPromise.value = new Promise(
(resolve) =>
(this.#transitionPromise.trigger = (value) => {
delete this.#transitionPromise.value;
resolve(value);
})
));

if (typeof transition === "number") {
const info = this.page.info;
Expand Down
57 changes: 40 additions & 17 deletions src/electron/frontend/core/components/DateTimeSelector.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
import { LitElement, css } from "lit";
import { getTimezoneOffset, formatTimezoneOffset } from "../../../../schemas/timezone.schema";

const convertToDateTimeLocalString = (date) => {
// Function to format the GMT offset
export function extractISOString(date = new Date(), { offset = false, timezone = undefined } = {}) {
if (typeof date === "string") date = new Date();

// Extract the GMT offset
const offsetMs = getTimezoneOffset(date, timezone);
const gmtOffset = formatTimezoneOffset(offsetMs);

// Format the date back to the original format with GMT offset
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
return `${year}-${month}-${day}T${hours}:${minutes}`;
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are zero-indexed
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");

// Recreate the ISO string with the GMT offset
const formattedDate = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
return offset ? formattedDate + gmtOffset : formattedDate;
}

export const renderDateTime = (value) => {
if (typeof value === "string") return extractISOString(new Date(value));
else if (value instanceof Date) return extractISOString(value);
return value;
};

export const resolveDateTime = renderDateTime;

export class DateTimeSelector extends LitElement {
static get styles() {
return css`
Expand All @@ -20,31 +41,33 @@ export class DateTimeSelector extends LitElement {
}

get value() {
return this.input.value;
const date = new Date(this.input.value);
const resolved = resolveDateTime(date);

console.log(this.input.value, resolved);
// return this.input.value;
return resolved;
}

set value(newValue) {
if (newValue) this.input.value = newValue;
else {
const d = new Date();
d.setHours(0, 0, 0, 0);
this.input.value = convertToDateTimeLocalString(d);
}
const date = newValue ? new Date(newValue) : new Date();
if (!newValue) date.setHours(0, 0, 0, 0);
this.input.value = resolveDateTime(date);
}
get min() {
return this.input.min;
}

set min(newValue) {
this.input.min = newValue;
set min(value) {
this.input.min = value;
}

get max() {
return this.input.max;
}

set max(newValue) {
this.input.max = newValue;
set max(value) {
this.input.max = value;
}

constructor({ value, min, max } = {}) {
Expand Down
11 changes: 6 additions & 5 deletions src/electron/frontend/core/components/JSONSchemaForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { resolveProperties } from "./pages/guided-mode/data/utils";
import { JSONSchemaInput, getEditableItems } from "./JSONSchemaInput";
import { InspectorListItem } from "./preview/inspector/InspectorList";

import { Validator } from "jsonschema";
import { successHue, warningHue, errorHue } from "./globals";
import { Button } from "./Button";

const encode = (str) => {
try {
document.querySelector(`#${str}`);
Expand Down Expand Up @@ -65,10 +69,6 @@ const additionalPropPattern = "additional";

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

import { Validator } from "jsonschema";
import { successHue, warningHue, errorHue } from "./globals";
import { Button } from "./Button";

var validator = new Validator();

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

const value = parent[name];
let value = parent[name];

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

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

// Run validation functions
const jsonSchemaErrors = validateArgs.length === 2 ? this.validateSchema(...validateArgs, name) : [];

const valid = skipValidation ? true : await this.validateOnChange(name, parent, pathToValidate, value);

if (valid === null) return null; // Skip validation / data change if the value is null
Expand Down
16 changes: 3 additions & 13 deletions src/electron/frontend/core/components/JSONSchemaInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,10 @@ import tippy from "tippy.js";
import { merge } from "./pages/utils";
import { OptionalSection } from "./OptionalSection";
import { InspectorListItem } from "./preview/inspector/InspectorList";
import { renderDateTime, resolveDateTime } from "./DateTimeSelector";

const isDevelopment = !!import.meta.env;

const dateTimeRegex = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/;

function resolveDateTime(value) {
if (typeof value === "string") {
const match = value.match(dateTimeRegex);
if (match) return `${match[1]}-${match[2]}-${match[3]}T${match[4]}:${match[5]}`;
return value;
}

return value;
}

export function createTable(fullPath, { onUpdate, onThrow, overrides = {} }) {
const name = fullPath.slice(-1)[0];
const path = fullPath.slice(0, -1);
Expand Down Expand Up @@ -1226,7 +1215,7 @@ export class JSONSchemaInput extends LitElement {
? "datetime-local"
: schema.format ?? (schema.type === "string" ? "text" : schema.type);

const value = isDateTime ? resolveDateTime(this.value) : this.value;
const value = isDateTime ? renderDateTime(this.value) : this.value;

const { minimum, maximum, exclusiveMax, exclusiveMin } = schema;
const min = exclusiveMin ?? minimum;
Expand All @@ -1249,6 +1238,7 @@ export class JSONSchemaInput extends LitElement {
if (isInteger) value = newValue = parseInt(value);
else if (isNumber) value = newValue = parseFloat(value);
else if (isDateTime) value = newValue = resolveDateTime(value);
if (isNumber) {
if ("min" in schema && newValue < schema.min) newValue = schema.min;
Expand Down
18 changes: 0 additions & 18 deletions src/electron/frontend/core/components/Main.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,6 @@ export class Main extends LitElement {
page.onTransition = this.onTransition;
page.updatePages = this.updatePages;

// Constrain based on workflow configuration
const workflowConfig = page.workflow ?? (page.workflow = {});
const workflowValues = page.info.globalState?.project?.workflow ?? {};

Object.entries(workflowConfig).forEach(([key, state]) => {
workflowConfig[key].value = workflowValues[key];

const value = workflowValues[key];

if (state.elements) {
const elements = state.elements;
if (value) elements.forEach((el) => el.removeAttribute("hidden"));
else elements.forEach((el) => el.setAttribute("hidden", true));
}
});

page.requestUpdate(); // Ensure the page is re-rendered with new workflow configurations

if (this.content)
this.toRender = toRender.page ? toRender : { page }; // Ensure re-render in either case
else this.#queue.push(page);
Expand Down
1 change: 1 addition & 0 deletions src/electron/frontend/core/components/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class Search extends LitElement {
}

#close = () => {
console.log("CLOSING", this.getSelectedOption());
if (this.listMode === "input" && this.getAttribute("interacted") === "true") {
this.setAttribute("interacted", false);
this.#onSelect(this.getSelectedOption());
Expand Down
Loading

0 comments on commit 47467bb

Please sign in to comment.