diff --git a/README.md b/README.md index e12237a..63f7e24 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The following steps dive into all the features SmartBlocks has to offer in incre ## Demo -[![](docs/media/vargas-smartblocks-demo-thumbnail.gif)](https://www.loom.com/share/954d916643754027a3889fd5bf7f24dd) +[![](https://raw.githubusercontent.com/RoamJs/smartblocks/main/docs/media/vargas-smartblocks-demo-thumbnail.gif)](https://www.loom.com/share/954d916643754027a3889fd5bf7f24dd) <!-- <div style="position: relative; padding-bottom: 66.66666666666666%; height: 0;"><iframe src="https://www.loom.com/embed/954d916643754027a3889fd5bf7f24dd" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe></div> --> diff --git a/package-lock.json b/package-lock.json index 5a0544c..df08bb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "smartblocks", - "version": "1.6.11", + "version": "1.6.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "smartblocks", - "version": "1.6.11", + "version": "1.6.12", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e058baa..d3de161 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smartblocks", - "version": "1.6.11", + "version": "1.6.12", "description": "Create custom and programmable templates from within Roam!", "main": "./build/main.js", "scripts": { diff --git a/src/components/DailyConfigComponent.tsx b/src/components/DailyConfigComponent.tsx index 377cba2..7ce88ac 100644 --- a/src/components/DailyConfigComponent.tsx +++ b/src/components/DailyConfigComponent.tsx @@ -1,6 +1,15 @@ -import { Checkbox, InputGroup, Label, Switch } from "@blueprintjs/core"; -import { TimePicker } from "@blueprintjs/datetime"; import React, { useMemo, useState, useEffect } from "react"; +import { + Checkbox, + FormGroup, + InputGroup, + Label, + Switch, + Tab, + Tabs, + Text, +} from "@blueprintjs/core"; +import { TimePicker } from "@blueprintjs/datetime"; import getDailyConfig from "../utils/getDailyConfig"; import saveDailyConfig from "../utils/saveDailyConfig"; import scheduleNextDailyRun from "../utils/scheduleNextDailyRun"; @@ -9,16 +18,26 @@ import localStorageSet from "roamjs-components/util/localStorageSet"; import nanoid from "nanoid"; const DailyConfig = () => { - const config = useMemo(getDailyConfig, []); - const [disabled, setDisabled] = useState(!config.enabled); - const [onlyOnThisDevice, setOnlyOnThisDevice] = useState( - !!config.device && config.device === localStorageGet("device") - ); - const [workflowName, setWorkflowName] = useState(config["workflow name"]); - const defaultTime = useMemo(() => { + const initialConfig = useMemo(getDailyConfig, []); + const [localConfig, setLocalConfig] = useState(initialConfig); + const setConfig = async (newConfig: { [key: string]: any }) => { + setLocalConfig((prev) => ({ ...prev, ...newConfig })); + await saveDailyConfig(newConfig); + }; + const { + enabled, + device, + time: timeToRun, + "workflow name": workflowName, + "last-run": lastRun, + "next-run": configNextRun, + } = localConfig; + + const currentDevice = useMemo(() => localStorageGet("device"), []); + const timePicker = useMemo(() => { const date = new Date(); - if (config && config["time"]) { - const [h, m] = config["time"].split(":").map(Number); + if (localConfig && timeToRun) { + const [h, m] = timeToRun.split(":").map(Number); date.setHours(h); date.setMinutes(m); } else { @@ -26,23 +45,21 @@ const DailyConfig = () => { date.setMinutes(0); } return date; - }, [config]); - const lastRun = config?.["last-run"]; + }, [localConfig]); const [now, setNow] = useState(new Date()); const nextRun = useMemo(() => { - if (!config.enabled) return 0; - return config["next-run"] - now.valueOf(); - }, [now, config]); + if (!localConfig.enabled) return 0; + return configNextRun - now.valueOf(); + }, [now, localConfig]); // migrate old config useEffect(() => { - if (config["workflow name"] && !config["enabled"]) { - saveDailyConfig({ - enabled: true, - }); - setDisabled(false); + if (workflowName && !enabled) { + setConfig({ enabled: true }); } - }, [config]); + }, [initialConfig]); + + // Set up an interval to periodically update the current time useEffect(() => { const int = window.setInterval(() => { setNow(new Date()); @@ -51,91 +68,158 @@ const DailyConfig = () => { window.clearInterval(int); }; }, [setNow]); + return ( - <div - className="flex items-start gap-2 flex-col" - style={{ - width: "100%", - minWidth: 256, - }} - > - <Switch - defaultChecked={config.enabled} - onChange={(e) => { - const enabled = (e.target as HTMLInputElement).checked; - if (enabled) { - saveDailyConfig({ - "workflow name": workflowName || "Daily", - }); - scheduleNextDailyRun({ tomorrow: true }); - } else { - window.clearTimeout(getDailyConfig()["next-run-timeout"]); - saveDailyConfig({ "workflow name": "" }); - setWorkflowName(""); - } - setDisabled(!enabled); - }} - label={disabled ? "Disabled" : "Enabled"} - /> - <Label> - Workflow Name - <InputGroup - value={workflowName} - onChange={(e) => { - saveDailyConfig({ - "workflow name": e.target.value, - }); - setWorkflowName(e.target.value); + <div> + <Tabs className="roamjs-daily-config-tabs" defaultSelectedTabId="dct"> + {enabled && ( + <Tab + className="text-white" + id="dct" + title="Daily Config" + panel={ + <div className="flex items-start gap-2 flex-col"> + <FormGroup + label="Workflow Name" + labelFor="roamjs-workflow-name" + className="my-4" + > + <InputGroup + id="roamjs-workflow-name" + value={workflowName} + onChange={(e) => { + setConfig({ "workflow name": e.target.value }); + }} + style={{ minWidth: "initial" }} + placeholder={"Enter workflow name"} + /> + </FormGroup> + + <FormGroup + label="Time To Run" + labelFor="roamjs-time-picker" + className="mb-4 flex" + style={{ alignItems: "center" }} + inline={true} + > + <div className="flex items-center"> + <TimePicker + value={timePicker} + onChange={async (e) => { + const newTime = `${e.getHours()}:${e.getMinutes()}`; + await setConfig({ time: newTime }); + await scheduleNextDailyRun({ tomorrow: true }); + setLocalConfig(getDailyConfig()); + }} + showArrowButtons + className={"user-select-none flex-1 text-center"} + /> + </div> + </FormGroup> + + <FormGroup + label="Only Run On This Device" + labelFor="roam-js-only-on-this-device" + inline={true} + > + <Checkbox + id="roam-js-only-on-this-device" + defaultChecked={!!device && device === currentDevice} + onChange={(e) => { + const enabled = (e.target as HTMLInputElement).checked; + if (enabled) { + const deviceId = currentDevice || nanoid(); + setConfig({ device: deviceId }); + localStorageSet("device", deviceId); + } else { + setConfig({ device: "" }); + } + }} + disabled={!enabled} + /> + </FormGroup> + </div> + } + disabled={!enabled} + /> + )} + {enabled && ( + <Tab + className="text-white" + id="rdt" + title="Run Details" + panel={ + <div className="flex items-start gap-2 flex-col"> + <Label> + Last Run + <Text style={{ color: "#8A9BA8" }}> + {lastRun && lastRun !== "01-01-1970" + ? `Last ran on page ${lastRun}` + : "Last run data not available"} + </Text> + </Label> + <Label> + Next Run + <Text style={{ color: "#8A9BA8" }}> + {!!nextRun + ? `${Math.floor( + nextRun / (60 * 60 * 1000) + )} hours, ${Math.floor( + (nextRun % (60 * 60 * 1000)) / (60 * 1000) + )} minutes, ${Math.floor( + (nextRun % (60 * 1000)) / 1000 + )} seconds` + : "Next run data not available"} + </Text> + </Label> + <Label> + Current Date and Time + <Text style={{ color: "#8A9BA8" }}> + {new Date().toLocaleString()} + </Text> + </Label> + <Label> + Next Run Scheduled At + <Text style={{ color: "#8A9BA8", maxWidth: "200px" }}> + {new Date(configNextRun).toLocaleString()} + </Text> + </Label> + <Label> + Set "Time To Run" + <Text style={{ color: "#8A9BA8", maxWidth: "200px" }}> + {timeToRun + .split(":") + .map((val, i) => (i === 0 ? `${val} HR` : `${val} M`)) + .join(" ")} + </Text> + </Label> + </div> + } + /> + )} + <Tabs.Expander /> + <Switch + large={true} + className="w-full text-right" + checked={enabled} + onChange={async (e) => { + const enabled = (e.target as HTMLInputElement).checked; + if (enabled) { + await setConfig({ + enabled: true, + "workflow name": workflowName || "Daily", + }); + scheduleNextDailyRun({ tomorrow: true }); + } else { + window.clearTimeout(getDailyConfig()["next-run-timeout"]); + setConfig({ + "workflow name": "", + enabled: false, + }); + } }} - disabled={disabled} - placeholder={"Daily"} - className={"w-full"} - /> - </Label> - <Label> - Time To Run - <TimePicker - defaultValue={defaultTime} - onChange={(e) => - saveDailyConfig({ time: `${e.getHours()}:${e.getMinutes()}` }) - } - showArrowButtons - disabled={disabled} - className={"w-full user-select-none"} /> - </Label> - <span> - {lastRun && - lastRun !== "01-01-1970" && - `Last ran daily workflow on page ${lastRun}.`} - </span> - <span> - {!!nextRun && - !disabled && - `Next run is in ${Math.floor( - nextRun / (60 * 60 * 1000) - )} hours, ${Math.floor( - (nextRun % (60 * 60 * 1000)) / (60 * 1000) - )} minutes, ${Math.floor((nextRun % (60 * 1000)) / 1000)} seconds.`} - </span> - <Checkbox - defaultChecked={onlyOnThisDevice} - onChange={(e) => { - const enabled = (e.target as HTMLInputElement).checked; - if (enabled) { - const deviceId = localStorageGet("device") || nanoid(); - saveDailyConfig({ - device: deviceId, - }); - localStorageSet("device", deviceId); - } else { - saveDailyConfig({ device: "" }); - } - setOnlyOnThisDevice(enabled); - }} - label={"Only run on this device"} - disabled={disabled} - /> + </Tabs> </div> ); }; diff --git a/src/index.ts b/src/index.ts index 74c89f6..57385ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -104,6 +104,13 @@ export default runExtension(async ({ extensionAPI }) => { .roamjs-smartblock-menu { width: 300px; +} +.rm-settings-tabs .roamjs-daily-config-tabs .bp3-tab-list { + padding: 2px; + background: none; +} +.rm-settings-tabs .roamjs-daily-config-tabs .bp3-timepicker.bp3-disabled .bp3-timepicker-input{ + color: #4b5563; }`); const toggleCommandPalette = (flag: boolean) => { diff --git a/src/utils/getDailyConfig.ts b/src/utils/getDailyConfig.ts index a673346..4e546e0 100644 --- a/src/utils/getDailyConfig.ts +++ b/src/utils/getDailyConfig.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import getExtensionApi from "roamjs-components/util/extensionApiContext"; +import localStorageGet from "roamjs-components/util/localStorageGet"; const zDailyConfig = z .object({ @@ -9,7 +10,7 @@ const zDailyConfig = z "next-run": z.number().optional().default(0), "next-run-timeout": z.number().optional().default(0), enabled: z.boolean().optional().default(false), - device: z.string().optional().default(""), + device: z.string().optional().default(localStorageGet("device")), }) .or( z.null().transform(() => ({ @@ -19,7 +20,7 @@ const zDailyConfig = z "next-run": 0, "next-run-timeout": 0, enabled: false, - device: "", + device: localStorageGet("device"), })) ) .optional() diff --git a/src/utils/saveDailyConfig.ts b/src/utils/saveDailyConfig.ts index fccce74..efba568 100644 --- a/src/utils/saveDailyConfig.ts +++ b/src/utils/saveDailyConfig.ts @@ -1,9 +1,9 @@ import getExtensionApi from "roamjs-components/util/extensionApiContext"; import getDailyConfig, { DailyConfig } from "./getDailyConfig"; -const saveDailyConfig = (config: Partial<DailyConfig>) => { +const saveDailyConfig = async (config: Partial<DailyConfig>) => { const currentConfig = getDailyConfig(); - getExtensionApi().settings.set("daily", { + return getExtensionApi().settings.set("daily", { ...currentConfig, ...config, }); diff --git a/src/utils/scheduleNextDailyRun.ts b/src/utils/scheduleNextDailyRun.ts index 3bae701..0ed1c1e 100644 --- a/src/utils/scheduleNextDailyRun.ts +++ b/src/utils/scheduleNextDailyRun.ts @@ -25,7 +25,7 @@ const getTriggerTime = (day: Date) => { export const runDaily = async () => { const dailyConfig = getDailyConfig(); const dailyWorkflowName = dailyConfig["workflow name"]; - if (!dailyWorkflowName) return; + if (!dailyWorkflowName) return; // useEffect in DailyConfigComponent requires this instead of if (enabled) if (dailyConfig.device && dailyConfig.device !== localStorageGet("device")) return; @@ -57,7 +57,6 @@ export const runDaily = async () => { const todayUid = window.roamAlphaAPI.util.dateToPageUid(today); const latestDate = parseRoamDateUid(lastRun); - let saveLastRun = false; if (isBefore(startOfDay(latestDate), startOfDay(today))) { const availableWorkflows = getCleanCustomWorkflows(); const srcUid = availableWorkflows.find( @@ -80,7 +79,6 @@ export const runDaily = async () => { target: { uid: todayUid, isParent: true }, fromDaily: true, }); - saveLastRun = true; } else { renderToast({ id: "smartblocks-error", @@ -98,19 +96,19 @@ export const runDaily = async () => { scheduleNextDailyRun({ saveLastRun: true, tomorrow: true }); }; -const scheduleNextDailyRun = ( +const scheduleNextDailyRun = async ( args: { tomorrow?: boolean; saveLastRun?: boolean } = {} ) => { const today = new Date(); const triggerDay = args.tomorrow ? addDays(today, 1) : today; const triggerDate = addSeconds(getTriggerTime(triggerDay), 1); const ms = differenceInMilliseconds(triggerDate, today); - const timeout = window.setTimeout(() => { + const timeoutId = window.setTimeout(() => { runDaily(); }, ms); - saveDailyConfig({ + await saveDailyConfig({ "next-run": triggerDate.valueOf(), - "next-run-timeout": timeout, + "next-run-timeout": timeoutId, ...(args.saveLastRun ? { "last-run": window.roamAlphaAPI.util.dateToPageUid(today) } : {}),