diff --git a/package.json b/package.json index e4e56b2c..4826968f 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,6 @@ "assert": "^1.4.1", "axios": "^0.21.1", "axios-hooks": "^2.2.0", - "butter-toast": "^3.3.5", "camelcase": "^5.0.0", "change-case": "^4.1.1", "commonmark": "^0.28.1", @@ -233,6 +232,7 @@ "react-table": "^7.7.0", "react-table-6": "^6.11.0", "react-tabs": "^3.0.0", + "react-toastify": "9.1.3", "react-tooltip-lite": "^1.10.0", "remark-gfm": "^1.0.0", "sanitize-filename": "^1.6.1", diff --git a/src/components/Notify.tsx b/src/components/Notify.tsx index f17e0c48..f1947827 100644 --- a/src/components/Notify.tsx +++ b/src/components/Notify.tsx @@ -4,72 +4,60 @@ import { css } from "@emotion/react"; import { showInExplorer } from "../other/crossPlatformUtilities"; //import { store } from "react-notifications-component"; import * as React from "react"; -import ButterToast, { Cinnamon, POS_BOTTOM, POS_RIGHT } from "butter-toast"; import userSettings from "../other/UserSettings"; import { sentryException } from "../other/errorHandling"; import { t } from "@lingui/macro"; import * as remote from "@electron/remote"; const electron = require("electron"); - +import { Theme, ToastOptions, toast } from "react-toastify"; +export const kred = "#E53935"; const activeToasts: string[] = []; -const autoCloseTicks = 60 * 1000; -export function NotifyError(message: string, details?: string) { - const key = message + details; - // don't show a message again if it's still showing - if (activeToasts.indexOf(key) < 0) { - activeToasts.push(key); - // the delay helps with messages that we wouldn't see on startup becuase the rect window isn't ready for it - window.setTimeout( - () => - ButterToast.raise({ - content: ( - // expand to fit the insides - -
{message}
-
- {details} -
- - } - scheme={Cinnamon.Crunch.SCHEME_RED} - /> - ), - onclick: () => { - try { - const index = activeToasts.indexOf(message); - if (index > -1) activeToasts.splice(index, 1); - } catch (err) { - //swallow - } - }, - timeout: autoCloseTicks - }), - 0 - ); - // we don't have a callback from butter-toast, but we know when we told it to hide. +const errorToastProps: ToastOptions = { + type: "error", + theme: "colored" as Theme, + autoClose: 10000 +}; - window.setTimeout(() => { - try { - const index = activeToasts.indexOf(key); - if (index > -1) activeToasts.splice(index, 1); - } catch (err) { - console.error(err); - } - }, autoCloseTicks); - } +function notify( + message: string, + details?: string | React.ReactNode, + options?: ToastOptions +) { + // the delay helps with messages that we wouldn't see on startup becuase the react window isn't ready for it + window.setTimeout(() => { + const key = message + details; + // don't show a message again if it's still showing + if (activeToasts.indexOf(key) < 0) { + activeToasts.push(key); + toast( + +
{message}
+
+ {details} +
+
, + { + ...options, + onClose: () => { + activeToasts.splice(activeToasts.indexOf(key), 1); + } + } + ); + } + }, 0); +} +export function NotifyError(message: string, details?: string) { + notify(message, details, errorToastProps); } export function getCannotRenameFileMsg() { return t`lameta was not able to rename that file.`; @@ -147,37 +135,27 @@ export function NotifyFileAccessProblem(message: string, err: any) { https://github.com/onset/lameta/issues.`} ); - // the delay helps with messages that we wouldn't see on startup becuase the rect window isn't ready for it - window.setTimeout( - () => - ButterToast.raise({ - content: ( - -

{message}

- {/* Enhance: can we get this html into the localization system somehow? For now I think the benefit of the structure outweigh the English-only nature. */} - {tips} -
- {err &&
{err.toString()}
} -
- - } - scheme={Cinnamon.Crunch.SCHEME_RED} - /> - ), - timeout: 60 * 1000 - }), - 0 - ); + notify(message, err.toString(), errorToastProps); + // toast.error( + //
+ //

{message}

+ + // {/* Enhance: can we get this html into the localization system somehow? For now I think the benefit of the structure outweigh the English-only nature. */} + // {tips} + //
+ // {err &&
{err.toString()}
} + //
+ //
, + // { + // ...errorToastProps + // } + // ); } export function NotifyException( @@ -189,85 +167,34 @@ export function NotifyException( NotifyError(message ? message : errWas, details ?? "" + errWas); sentryException(err); } -export function NotifyNoBigDeal(message: string, onClick?: () => void) { - // the delay helps with messages that we wouldn't see on startup becuase the rect window isn't ready for it - window.setTimeout( - () => - ButterToast.raise({ - onClick, - content: ( - - ) - }), - 0 - ); - // remove any existing messages of this type - window.setTimeout(() => { - ButterToast.dismissAll((toast) => { - return toast.scheme === Cinnamon.Crunch.SCHEME_GREY; - }); - }, 0); +export function NotifyNoBigDeal(message: string) { + notify(message, "", { + type: "warning", + autoClose: 2000, + hideProgressBar: true, + closeButton: false + }); } -export function NotifyWarning(message: string, onClick?: () => void) { - // the delay helps with messages that we wouldn't see on startup becuase the rect window isn't ready for it - window.setTimeout( - () => - ButterToast.raise({ - onClick, - content: ( - - ) - }), - 0 - ); +export function NotifyErrorWithClick(message: string, onClick: () => void) { + notify(message, "", { ...errorToastProps, onClick: onClick }); +} +export function NotifyWarning(message: string) { + notify(message, "", { type: "warning" }); } export function NotifySuccess(message: string, onClick?: () => void) { - // the delay helps with messages that we wouldn't see on startup becuase the rect window isn't ready for it - window.setTimeout( - () => - ButterToast.raise({ - content: ( - - ) - }), - 0 - ); + notify(message, "", { + type: "success", + onClick: onClick, + autoClose: 2000, + hideProgressBar: true + }); } export function NotifyUpdateAvailable( open: () => void //download: DownloadFunction ) { - // the delay helps with messages that we wouldn't see on startup becuase the rect window isn't ready for it - window.setTimeout(() => { - ButterToast.raise({ - content: ( - - {/* I would like to dismiss the toast when you click, but the current ButterToast docs don't seem to match what we - actually get if we provide an OnClick to raise. At this time, it does not give a dismiss function as claimed. */} - {t`View Release Notes`} - - } - /> - ), - timeout: 10 * 1000 - }); - }, 0); + notify(t`Update available`, {t`View Release Notes`}); } export function NotifyMultipleProjectFiles( @@ -276,29 +203,24 @@ export function NotifyMultipleProjectFiles( name: string, folder: string ) { - // the delay helps with messages that we wouldn't see on startup becuase the rect window isn't ready for it - window.setTimeout( - () => - NotifyWarning( - t`There is a problem with the files in the folder for ${displayName}. Click for more information.`, - () => { - electron.ipcRenderer - .invoke("showMessageBox", { - buttons: [t`Cancel`, t`Show me the folder with the problem`], - title: t`Something is wrong here...`, - message: t`There are more than one files of type "${projectType}" in this folder, and there can only be one.`, - detail: t`lameta will now open this folder on your hard disk and then exit. You should open these ${projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named ${name}.` - }) - .then((response) => { - if (response > 0) { - showInExplorer(folder); - if (!userSettings.DeveloperMode) { - window.setTimeout(() => remote.app.quit(), 1000); - } - } - }); - } - ), - 0 + NotifyErrorWithClick( + t`There is a problem with the files in the folder for ${displayName}. Click for more information.`, + () => { + electron.ipcRenderer + .invoke("showMessageBox", { + buttons: [t`Cancel`, t`Show me the folder with the problem`], + title: t`Something is wrong here...`, + message: t`There are more than one files of type "${projectType}" in this folder, and there can only be one.`, + detail: t`lameta will now open this folder on your hard disk and then exit. You should open these ${projectType} files in a text editor and decide which one you want, and delete the others. The one you choose should be named ${name}.` + }) + .then((response) => { + if (response > 0) { + showInExplorer(folder); + if (!userSettings.DeveloperMode) { + window.setTimeout(() => remote.app.quit(), 1000); + } + } + }); + } ); } diff --git a/src/components/Workspace.tsx b/src/components/Workspace.tsx index cf551514..f71f8406 100644 --- a/src/components/Workspace.tsx +++ b/src/components/Workspace.tsx @@ -194,9 +194,9 @@ class Home extends React.Component { click: () => { if (this.props.project) { if ( - (this.props.project.getFolderArrayFromType( - folderType - ) as any).countOfMarkedFolders() === 0 + ( + this.props.project.getFolderArrayFromType(folderType) as any + ).countOfMarkedFolders() === 0 ) { ShowMessageDialog({ title: ``, diff --git a/src/components/people/person/PersonForm.tsx b/src/components/people/person/PersonForm.tsx index 3ea2f803..02f53227 100644 --- a/src/components/people/person/PersonForm.tsx +++ b/src/components/people/person/PersonForm.tsx @@ -34,9 +34,14 @@ class PersonForm extends React.Component { validate={(value: string) => this.props.validateFullName(value)} field={this.props.fields.getTextField("name")} onBlur={() => { - this.props.person.nameMightHaveChanged(); - // ID is s function of the name and the code - this.props.person.IdMightHaveChanged(); + if (this.props.person.getNeedRenameOfFolder()) { + // todo: show a dialog that says we're working + setTimeout(() => { + this.props.person.nameMightHaveChanged(); + // ID is s function of the name and the code + this.props.person.IdMightHaveChanged(); + }, 100); + } }} className="full-name left-side" /> diff --git a/src/containers/App.tsx b/src/containers/App.tsx index 48cf82e9..d30ea268 100644 --- a/src/containers/App.tsx +++ b/src/containers/App.tsx @@ -7,7 +7,6 @@ import { RenameFileDialog } from "../components/RenameFileDialog/RenameFileDialo import { I18nProvider } from "@lingui/react"; import { i18n } from "../other/localization"; import RegistrationDialog from "../components/registration/RegistrationDialog"; -import ButterToast from "butter-toast"; import userSettingsSingleton from "../other/UserSettings"; import { observer } from "mobx-react"; import { ReleasesDialog } from "../components/ReleasesDialog"; @@ -42,7 +41,6 @@ export const App: React.FunctionComponent = observer(() => { - diff --git a/src/containers/HomePage.tsx b/src/containers/HomePage.tsx index 80ee3bdf..54b4138b 100644 --- a/src/containers/HomePage.tsx +++ b/src/containers/HomePage.tsx @@ -37,7 +37,8 @@ import { PatientFS } from "../other/patientFile"; import { SpreadsheetImportDialog } from "../components/import/SpreadsheetImportDialog"; import { locateDependencyForFilesystemCall } from "../other/locateDependency"; import { copyDirSync } from "../other/crossPlatformUtilities"; - +import { Slide, ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; const isDev = require("electron-is-dev"); // Added this as part of a workaround in typing when upgrading to mobx6. @@ -329,6 +330,13 @@ class HomePage extends React.Component { + ); } diff --git a/src/model/Folder/Folder.ts b/src/model/Folder/Folder.ts index 3f49fb4d..c3be2311 100644 --- a/src/model/Folder/Folder.ts +++ b/src/model/Folder/Folder.ts @@ -512,12 +512,19 @@ export abstract class Folder { return "UNUSED-IN-THIS-CLASS"; } + public getNeedRenameOfFolder(): boolean { + const newFileName = sanitizeForArchive( + this.textValueThatControlsFolderName(), + userSettingsSingleton.IMDIMode + ); + + // Note, this code hasn't been tested with Linux, which has a case-sensitive file system. + // Windows is always case-insensitive, and macos usually (but not always!) is. This method + // so far gets by with being case sensitive. + return newFileName.length > 0 && newFileName !== this.safeFileNameBase; + } + public nameMightHaveChanged(): boolean { - // Enhance: If something goes wrong here, we're going to have things out of sync. Is there some - // way to do this atomically (that is, as a transaction), or at least do the most dangerous - // part first (i.e. the file renaming)? - // Then if that failed, we would need to rename the files that had already been changed, and then - // change the id/name field back to what it was previously. const newFileName = sanitizeForArchive( this.textValueThatControlsFolderName(), userSettingsSingleton.IMDIMode diff --git a/src/other/patientFile.ts b/src/other/patientFile.ts index 7f8f94ee..fa26f3fa 100644 --- a/src/other/patientFile.ts +++ b/src/other/patientFile.ts @@ -100,21 +100,21 @@ export class PatientFS { } private static patientFileOperationSync(operation: () => any): any { // note, graceful-fs is already pausing up to 60 seconds on each attempt. - const kattempts = 5; + const kretryAttempts = 15; // I wish i could visibly show something if we're going to wait... let attempt = 1; - for (; attempt <= kattempts; attempt++) { + for (; attempt <= kretryAttempts; attempt++) { try { - const result = operation(); + const result = operation(); // this can throw, causing us to loop if (attempt > 1) { // there is no way to asynchronously show any UI, but after a long wait in which we finally got through, it might help to tell people what caused the a delay. NotifyNoBigDeal( - `There was a delay in accessing a file... perhaps another program, file sync service, or antivirus is interfering.` + `There was a delay in accessing a file... perhaps a file-sync service, or antivirus is interfering.` ); } return result; } catch (err) { if (err.code === "EBUSY" || err.code === "EPERM") { - if (attempt === kattempts) { + if (attempt === kretryAttempts) { throw err; // give up } console.log("patientReadFileSync: Sleeping..."); @@ -127,7 +127,9 @@ export class PatientFS { } private static sleepForShortWhile() { - console.error("patientFile:sleepForShortWhile"); + console.error( + "patientFile:sleepForShortWhile because file wasn't available..." + ); //"sleep" would probably work on mac/linux. But the equivalent "timeout" on windows fails when there is no keyboad input. // So we're doing a ping. Note that a ping of "-n 1" is 0ms on windows, oddly, while "-n 2" takes about a second child_process.spawnSync("ping", ["-n 2 127.0.0.1"], { diff --git a/yarn.lock b/yarn.lock index b827efcf..a742dc6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5944,13 +5944,6 @@ builtin-status-codes@^3.0.0: resolved "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== -butter-toast@^3.3.5: - version "3.3.5" - resolved "https://registry.npmjs.org/butter-toast/-/butter-toast-3.3.5.tgz#3a40883c7b2162eda10ff05cae01d2537f27182c" - integrity sha512-6TZkT5lGcX4jvucK26X156wnw79Lo137yelHPvHUnpVgz7ZiyJGYNCXFY/twE0dLDQpVDlnUWOBhNKLt5gcl2w== - dependencies: - "@babel/runtime" "^7.3.1" - bytes@3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -6549,7 +6542,7 @@ clone@^1.0.2: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -clsx@^1.0.4, clsx@^1.1.0: +clsx@^1.0.4, clsx@^1.1.0, clsx@^1.1.1: version "1.2.1" resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== @@ -14627,6 +14620,13 @@ react-themeable@^1.1.0: dependencies: object-assign "^3.0.0" +react-toastify@9.1.3: + version "9.1.3" + resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz#1e798d260d606f50e0fab5ee31daaae1d628c5ff" + integrity sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg== + dependencies: + clsx "^1.1.1" + react-tooltip-lite@^1.10.0: version "1.12.0" resolved "https://registry.npmjs.org/react-tooltip-lite/-/react-tooltip-lite-1.12.0.tgz#f6cd1323cdd9f5f80dd0e71a30ef59f401dee9ba"