Skip to content

Commit

Permalink
Custom updater modal (#1738)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
pierremtb authored Apr 17, 2024
1 parent 624b1fc commit b13c133
Show file tree
Hide file tree
Showing 13 changed files with 297 additions and 10 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
4 changes: 3 additions & 1 deletion src-tauri/capabilities/desktop.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
1 change: 0 additions & 1 deletion src-tauri/tauri.release.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"endpoints": [
"https://dl.zoo.dev/releases/modeling-app/last_update.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUzNzA4MjBEQjFBRTY4NzYKUldSMmFLNnhEWUp3NCtsT21Jd05wQktOaGVkOVp6MUFma0hNTDRDSnI2RkJJTEZOWG1ncFhqcU8K"
}
}
Expand Down
2 changes: 0 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -123,7 +122,6 @@ export function App() {
project={{ project, file }}
enableMenu={true}
/>
<ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} />
<Stream className="absolute inset-0 z-0" />
{/* <CamToggle /> */}
Expand Down
42 changes: 42 additions & 0 deletions src/components/UpdaterModal.test.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 84 additions & 0 deletions src/components/UpdaterModal.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 40 additions & 0 deletions src/components/UpdaterRestartModal.test.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 56 additions & 0 deletions src/components/UpdaterRestartModal.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
37 changes: 37 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -35,10 +44,38 @@ root.render(
},
}}
/>
<ModalContainer />
</HotkeysProvider>
)

// If you want to start measuring performance in your app, pass a function
// 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()
Loading

0 comments on commit b13c133

Please sign in to comment.