Custom updater modal (#1738)
* WIP: Custom updater modal Fixes #1663 * First working example with data * Clean up, moved code to index.tsx * Clean up * Nicer dialog * Add relaunch dialog (macOS) * max-height in case of a long text * Clean up * Add component tests and fix name consistency * Update styling, re-add md parser * Clean up * Quick typo * Clean up * Rebase on tauri v2 * Clean up * Add updater permissions * Remove dialog from config * Fix restart after install
This commit is contained in:
@ -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",
|
||||
|
11
src-tauri/Cargo.lock
generated
11
src-tauri/Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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");
|
||||
|
@ -13,7 +13,6 @@
|
||||
"endpoints": [
|
||||
"https://dl.zoo.dev/releases/modeling-app/last_update.json"
|
||||
],
|
||||
"dialog": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
<ModalContainer />
|
||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||
<Stream className="absolute inset-0 z-0" />
|
||||
{/* <CamToggle /> */}
|
||||
|
42
src/components/UpdaterModal.test.tsx
Normal file
42
src/components/UpdaterModal.test.tsx
Normal file
@ -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(
|
||||
<UpdaterModal
|
||||
isOpen={true}
|
||||
onReject={() => {}}
|
||||
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 })
|
||||
})
|
||||
})
|
84
src/components/UpdaterModal.tsx
Normal file
84
src/components/UpdaterModal.tsx
Normal file
@ -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<ModalResolve, ModalReject> & {
|
||||
version: string
|
||||
date?: string
|
||||
body?: string
|
||||
}
|
||||
|
||||
export const createUpdaterModal = create<
|
||||
UpdaterModalProps,
|
||||
ModalResolve,
|
||||
ModalReject
|
||||
>
|
||||
|
||||
export const UpdaterModal = ({
|
||||
onResolve,
|
||||
version,
|
||||
date,
|
||||
body,
|
||||
}: UpdaterModalProps) => (
|
||||
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
|
||||
<div className="max-w-3xl min-w-[45rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
<div className="flex items-center">
|
||||
<h1 className="flex-grow text-3xl font-bold">New version available!</h1>
|
||||
<Logo className="h-9" />
|
||||
</div>
|
||||
<div className="my-4 flex items-baseline">
|
||||
<span
|
||||
className="px-3 py-1 text-xl rounded-full bg-energy-10 text-energy-80"
|
||||
data-testid="update-version"
|
||||
>
|
||||
v{version}
|
||||
</span>
|
||||
<span className="ml-4 text-sm text-gray-400">Published on {date}</span>
|
||||
</div>
|
||||
{/* TODO: fix list bullets */}
|
||||
{body && (
|
||||
<div
|
||||
className="my-4 max-h-60 overflow-y-auto"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Marked.parse(body, {
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
sanitize: true,
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => 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
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => onResolve({ wantUpdate: true })}
|
||||
icon={{ icon: 'arrowRight', bgClassName: 'dark:bg-chalkboard-80' }}
|
||||
className="dark:hover:bg-chalkboard-80/50"
|
||||
data-testid="update-button-update"
|
||||
>
|
||||
Update
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
40
src/components/UpdaterRestartModal.test.tsx
Normal file
40
src/components/UpdaterRestartModal.test.tsx
Normal file
@ -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(
|
||||
<UpdaterRestartModal
|
||||
isOpen={true}
|
||||
onReject={() => {}}
|
||||
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 })
|
||||
})
|
||||
})
|
56
src/components/UpdaterRestartModal.tsx
Normal file
56
src/components/UpdaterRestartModal.tsx
Normal file
@ -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<ModalResolve, ModalReject> & {
|
||||
version: string
|
||||
}
|
||||
|
||||
export const createUpdaterRestartModal = create<
|
||||
UpdaterRestartModalProps,
|
||||
ModalResolve,
|
||||
ModalReject
|
||||
>
|
||||
|
||||
export const UpdaterRestartModal = ({
|
||||
onResolve,
|
||||
version,
|
||||
}: UpdaterRestartModalProps) => (
|
||||
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
|
||||
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
<h1 className="text-3xl font-bold">Ready to restart?</h1>
|
||||
<p className="my-4" data-testid="update-restart-version">
|
||||
v{version} is now installed. Restart the app to use the new features.
|
||||
</p>
|
||||
<div className="flex justify-between">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => 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
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => 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
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
@ -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(
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ModalContainer />
|
||||
</HotkeysProvider>
|
||||
)
|
||||
|
||||
@ -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()
|
||||
|
14
yarn.lock
14
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"
|
||||
|
Reference in New Issue
Block a user