diff --git a/package.json b/package.json index 417a5f4a43..4856458c0f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "@tauri-apps/plugin-fs": "^2.0.0-beta.2", "@tauri-apps/plugin-http": "^2.0.0-beta.2", "@tauri-apps/plugin-os": "^2.0.0-beta.2", + "@tauri-apps/plugin-process": "^2.0.0-beta.2", "@tauri-apps/plugin-shell": "^2.0.0-beta.2", + "@tauri-apps/plugin-updater": "^2.0.0-beta.2", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^15.0.2", "@testing-library/user-event": "^14.5.2", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4883d36930..18b53a724d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -88,6 +88,7 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-http", "tauri-plugin-os", + "tauri-plugin-process", "tauri-plugin-shell", "tauri-plugin-updater", "tokio", @@ -4670,6 +4671,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "tauri-plugin-process" +version = "2.0.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11215c3615299090e97f37341ae4b01f518bc1d43e9c4391144c0e5e3b7d4f01" +dependencies = [ + "tauri", + "tauri-plugin", +] + [[package]] name = "tauri-plugin-shell" version = "2.0.0-beta.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fcc24b1369..089d01ac72 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,12 +21,13 @@ oauth2 = "4.4.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] } -tauri-plugin-dialog = { version = "2.0.0-beta.0" } -tauri-plugin-fs = { version = "2.0.0-beta.0" } -tauri-plugin-http = { version = "2.0.0-beta.0" } -tauri-plugin-os = { version = "2.0.0-beta.0" } -tauri-plugin-shell = { version = "2.0.0-beta.0" } -tauri-plugin-updater = { version = "2.0.0-beta.0" } +tauri-plugin-dialog = { version = "2.0.0-beta.2" } +tauri-plugin-fs = { version = "2.0.0-beta.2" } +tauri-plugin-http = { version = "2.0.0-beta.2" } +tauri-plugin-os = { version = "2.0.0-beta.2" } +tauri-plugin-process = { version = "2.0.0-beta.2" } +tauri-plugin-shell = { version = "2.0.0-beta.2" } +tauri-plugin-updater = { version = "2.0.0-beta.2" } tokio = { version = "1.37.0", features = ["time"] } toml = "0.8.2" diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json index 6ab70fac03..bb7b04b030 100644 --- a/src-tauri/capabilities/desktop.json +++ b/src-tauri/capabilities/desktop.json @@ -77,7 +77,9 @@ "os:allow-arch", "os:allow-exe-extension", "os:allow-locale", - "os:allow-hostname" + "os:allow-hostname", + "process:allow-restart", + "updater:default" ], "platforms": [ "linux", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b2d4e58120..0c0c274743 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -244,6 +244,7 @@ fn main() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_shell::init()) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.release.conf.json b/src-tauri/tauri.release.conf.json index 6049b166c2..b77568ed2b 100644 --- a/src-tauri/tauri.release.conf.json +++ b/src-tauri/tauri.release.conf.json @@ -13,7 +13,6 @@ "endpoints": [ "https://dl.zoo.dev/releases/modeling-app/last_update.json" ], - "dialog": true, "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K" } } diff --git a/src/App.tsx b/src/App.tsx index b6aeed9e1a..5fe644e2ec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,6 @@ import { uuidv4 } from 'lib/utils' import { useStore } from './useStore' import { useHotKeyListener } from './hooks/useHotKeyListener' import { Stream } from './components/Stream' -import ModalContainer from 'react-modal-promise' import { EngineCommand } from './lang/std/engineConnection' import { throttle } from './lib/utils' import { AppHeader } from './components/AppHeader' @@ -123,7 +122,6 @@ export function App() { project={{ project, file }} enableMenu={true} /> - {/* */} diff --git a/src/components/UpdaterModal.test.tsx b/src/components/UpdaterModal.test.tsx new file mode 100644 index 0000000000..fc9f156e6f --- /dev/null +++ b/src/components/UpdaterModal.test.tsx @@ -0,0 +1,42 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { vi } from 'vitest' +import { UpdaterModal } from './UpdaterModal' + +describe('UpdaterModal tests', () => { + test('Renders the modal', () => { + const callback = vi.fn() + const data = { + version: '1.2.3', + date: '2021-22-23T21:22:23Z', + body: 'This is the body.', + } + + render( + {}} + onResolve={callback} + instanceId="" + open={false} + close={(res) => {}} + version={data.version} + date={data.date} + body={data.body} + /> + ) + + expect(screen.getByTestId('update-version')).toHaveTextContent(data.version) + + const updateButton = screen.getByTestId('update-button-update') + expect(updateButton).toBeEnabled() + fireEvent.click(updateButton) + expect(callback.mock.calls).toHaveLength(1) + expect(callback.mock.lastCall[0]).toEqual({ wantUpdate: true }) + + const cancelButton = screen.getByTestId('update-button-cancel') + expect(cancelButton).toBeEnabled() + fireEvent.click(cancelButton) + expect(callback.mock.calls).toHaveLength(2) + expect(callback.mock.lastCall[0]).toEqual({ wantUpdate: false }) + }) +}) diff --git a/src/components/UpdaterModal.tsx b/src/components/UpdaterModal.tsx new file mode 100644 index 0000000000..6da0a4dcdb --- /dev/null +++ b/src/components/UpdaterModal.tsx @@ -0,0 +1,84 @@ +import { create, InstanceProps } from 'react-modal-promise' +import { ActionButton } from './ActionButton' +import { Logo } from './Logo' +import { Marked } from '@ts-stack/markdown' + +type ModalResolve = { + wantUpdate: boolean +} + +type ModalReject = boolean + +type UpdaterModalProps = InstanceProps & { + version: string + date?: string + body?: string +} + +export const createUpdaterModal = create< + UpdaterModalProps, + ModalResolve, + ModalReject +> + +export const UpdaterModal = ({ + onResolve, + version, + date, + body, +}: UpdaterModalProps) => ( +
+
+
+

New version available!

+ +
+
+ + v{version} + + Published on {date} +
+ {/* TODO: fix list bullets */} + {body && ( +
+ )} +
+ onResolve({ wantUpdate: false })} + icon={{ + icon: 'close', + bgClassName: 'bg-destroy-80', + iconClassName: 'text-destroy-20 group-hover:text-destroy-10', + }} + className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50" + data-testid="update-button-cancel" + > + Not now + + onResolve({ wantUpdate: true })} + icon={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }} + className="dark:hover:bg-chalkboard-80/50" + data-testid="update-button-update" + > + Update + +
+
+
+) diff --git a/src/components/UpdaterRestartModal.test.tsx b/src/components/UpdaterRestartModal.test.tsx new file mode 100644 index 0000000000..c177bbab84 --- /dev/null +++ b/src/components/UpdaterRestartModal.test.tsx @@ -0,0 +1,40 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { vi } from 'vitest' +import { UpdaterRestartModal } from './UpdaterRestartModal' + +describe('UpdaterRestartModal tests', () => { + test('Renders the modal', () => { + const callback = vi.fn() + const data = { + version: '1.2.3', + } + + render( + {}} + onResolve={callback} + instanceId="" + open={false} + close={(res) => {}} + version={data.version} + /> + ) + + expect(screen.getByTestId('update-restart-version')).toHaveTextContent( + data.version + ) + + const updateButton = screen.getByTestId('update-restrart-button-update') + expect(updateButton).toBeEnabled() + fireEvent.click(updateButton) + expect(callback.mock.calls).toHaveLength(1) + expect(callback.mock.lastCall[0]).toEqual({ wantRestart: true }) + + const cancelButton = screen.getByTestId('update-restrart-button-cancel') + expect(cancelButton).toBeEnabled() + fireEvent.click(cancelButton) + expect(callback.mock.calls).toHaveLength(2) + expect(callback.mock.lastCall[0]).toEqual({ wantRestart: false }) + }) +}) diff --git a/src/components/UpdaterRestartModal.tsx b/src/components/UpdaterRestartModal.tsx new file mode 100644 index 0000000000..d6cad915ce --- /dev/null +++ b/src/components/UpdaterRestartModal.tsx @@ -0,0 +1,56 @@ +import { create, InstanceProps } from 'react-modal-promise' +import { ActionButton } from './ActionButton' + +type ModalResolve = { + wantRestart: boolean +} + +type ModalReject = boolean + +type UpdaterRestartModalProps = InstanceProps & { + version: string +} + +export const createUpdaterRestartModal = create< + UpdaterRestartModalProps, + ModalResolve, + ModalReject +> + +export const UpdaterRestartModal = ({ + onResolve, + version, +}: UpdaterRestartModalProps) => ( +
+
+

Ready to restart?

+

+ v{version} is now installed. Restart the app to use the new features. +

+
+ onResolve({ wantRestart: false })} + icon={{ + icon: 'close', + bgClassName: 'bg-destroy-80', + iconClassName: 'text-destroy-20 group-hover:text-destroy-10', + }} + className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50" + data-testid="update-restrart-button-cancel" + > + Not now + + onResolve({ wantRestart: true })} + icon={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }} + className="dark:hover:bg-chalkboard-80/50" + data-testid="update-restrart-button-update" + > + Restart + +
+
+
+) diff --git a/src/index.tsx b/src/index.tsx index 6f5f322f35..7177882b81 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,15 @@ import reportWebVitals from './reportWebVitals' import { Toaster } from 'react-hot-toast' import { Router } from './Router' import { HotkeysProvider } from 'react-hotkeys-hook' +import ModalContainer from 'react-modal-promise' +import { UpdaterModal, createUpdaterModal } from 'components/UpdaterModal' +import { isTauri } from 'lib/isTauri' +import { relaunch } from '@tauri-apps/plugin-process' +import { check } from '@tauri-apps/plugin-updater' +import { + UpdaterRestartModal, + createUpdaterRestartModal, +} from 'components/UpdaterRestartModal' // uncomment for xstate inspector // import { DEV } from 'env' @@ -35,6 +44,7 @@ root.render( }, }} /> + ) @@ -42,3 +52,30 @@ root.render( // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals() + +const runTauriUpdater = async () => { + try { + const update = await check() + if (update && update.available) { + const { date, version, body } = update + const modal = createUpdaterModal(UpdaterModal) + const { wantUpdate } = await modal({ date, version, body }) + if (wantUpdate) { + await update.downloadAndInstall() + // On macOS and Linux, the restart needs to be manually triggered + const isNotWindows = navigator.userAgent.indexOf('Win') === -1 + if (isNotWindows) { + const relaunchModal = createUpdaterRestartModal(UpdaterRestartModal) + const { wantRestart } = await relaunchModal({ version }) + if (wantRestart) { + await relaunch() + } + } + } + } + } catch (error) { + console.error(error) + } +} + +isTauri() && runTauriUpdater() diff --git a/yarn.lock b/yarn.lock index a518b00388..3be050c676 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2210,6 +2210,13 @@ dependencies: "@tauri-apps/api" "2.0.0-beta.4" +"@tauri-apps/plugin-process@^2.0.0-beta.2": + version "2.0.0-beta.2" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-process/-/plugin-process-2.0.0-beta.2.tgz#682ef62c4db154a2d6da0bc352133b13796e7d16" + integrity sha512-CLF3Figv68fk+mqdV1q8bufFlcQS3SSTiNX8Lc7FbSD211XOWShgiGm4D6QMUkFBxgXzZICWh/mrYnWdv3aYQA== + dependencies: + "@tauri-apps/api" "2.0.0-beta.4" + "@tauri-apps/plugin-shell@^2.0.0-beta.2": version "2.0.0-beta.2" resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-beta.2.tgz#1eff697246140f17478527b0d947d76d3403a226" @@ -2217,6 +2224,13 @@ dependencies: "@tauri-apps/api" "2.0.0-beta.4" +"@tauri-apps/plugin-updater@^2.0.0-beta.2": + version "2.0.0-beta.2" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.0.0-beta.2.tgz#60002e54ad647a56db5e1b0b54e792f399d425a4" + integrity sha512-T8EkAXawbyV/6/Lcf1VVIWhtGuals63zKn+udYNqlC8CRM5iYQ+8bM8Nmy2E+pIzkkx93d1t6/8geFitLZPmKw== + dependencies: + "@tauri-apps/api" "2.0.0-beta.4" + "@testing-library/dom@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.0.0.tgz#ae1ab88aad35a728a38264041163174cafd7e8dd"