Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI for SPX version selector #1090

Merged
merged 2 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions spx-gui/src/components/navbar/NavbarProfile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
{{ $t({ en: 'Projects', zh: '项目列表' }) }}
</UIMenuItem>
</UIMenuGroup>
<UIMenuGroup>
<UIMenuItem v-if="spxVersion === 'v2'" @click="handleUseSpxV1">
{{ $t({ en: 'Use default SPX', zh: '使用默认 SPX' }) }}
</UIMenuItem>
<UIMenuItem v-if="spxVersion === 'v1'" @click="handleUseSpxV2">
{{ $t({ en: 'Use new SPX (in beta)', zh: '启用新 SPX(测试中)' }) }}
</UIMenuItem>
</UIMenuGroup>
<UIMenuGroup>
<UIMenuItem @click="handleSignOut">{{ $t({ en: 'Sign out', zh: '登出' }) }}</UIMenuItem>
</UIMenuGroup>
Expand All @@ -32,12 +40,15 @@
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useNetwork } from '@/utils/network'
import { useSpxVersion } from '@/utils/utils'
import { useI18n } from '@/utils/i18n'
import { useMessageHandle } from '@/utils/exception'
import { getUserPageRoute } from '@/router'
import { useUserStore } from '@/stores/user'
import { UIButton, UIDropdown, UIMenu, UIMenuGroup, UIMenuItem } from '@/components/ui'
import { computed } from 'vue'
import { UIButton, UIDropdown, UIMenu, UIMenuGroup, UIMenuItem, useConfirmDialog } from '@/components/ui'

const userStore = useUserStore()
const { isOnline } = useNetwork()
Expand All @@ -53,6 +64,44 @@ function handleProjects() {
router.push(getUserPageRoute(userInfo.value!.name, 'projects'))
}

const spxVersion = useSpxVersion()

const handleUseSpxV1 = useMessageHandle(
async () => {
spxVersion.value = 'v1'
},
undefined,
{
en: 'Back to the default version of SPX',
zh: '已切换回默认版本 SPX'
}
).fn

const i18n = useI18n()
const withConfirm = useConfirmDialog()

const handleUseSpxV2 = useMessageHandle(
async () => {
await withConfirm({
type: 'info',
title: i18n.t({
en: 'Use new version of SPX',
zh: '启用新版本 SPX'
}),
content: i18n.t({
en: 'The new version of SPX is still in beta. You can switch back to the default version anytime if you encounter issues.',
zh: '新版本 SPX 还在测试中,如果遇到问题可以随时退回到默认版本。'
})
})
spxVersion.value = 'v2'
},
undefined,
{
en: 'Now using the new version of SPX',
zh: '已启用新版本 SPX'
}
).fn

function handleSignOut() {
userStore.signOut()
router.go(0) // Reload the page to trigger navigation guards.
Expand Down
6 changes: 2 additions & 4 deletions spx-gui/src/components/project/runner/ProjectRunner.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { getSpxVersion, useLocalStorage } from '@/utils/utils'
import { useSpxVersion } from '@/utils/utils'
import type { Project } from '@/models/project'
import ProjectRunnerV1 from './v1/ProjectRunnerV1.vue'
import ProjectRunnerV2 from './v2/ProjectRunnerV2.vue'
Expand All @@ -17,9 +17,7 @@ function handleConsole(type: 'log' | 'warn', args: unknown[]) {
emit('console', type, args)
}

const version = useLocalStorage('spx-gui-runner', 'v1')
const specifiedVersion = getSpxVersion()
if (specifiedVersion != null) version.value = specifiedVersion
const version = useSpxVersion()

defineExpose({
run() {
Expand Down
51 changes: 49 additions & 2 deletions spx-gui/src/utils/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest'
import { isImage, isSound, nomalizeDegree, memoizeAsync } from './utils'
import { nextTick, watch } from 'vue'
import { describe, it, expect, vitest } from 'vitest'
import { isImage, isSound, nomalizeDegree, memoizeAsync, useLocalStorage } from './utils'
import { sleep } from './test'

describe('isImage', () => {
Expand Down Expand Up @@ -139,3 +140,49 @@ describe('memoizeAsync', () => {
expect(count).toBe(3)
})
})

describe('useLocalStorage', () => {
it('should work well', () => {
const key = 'test-key'
const stored = useLocalStorage(key, 'default-value')
expect(stored.value).toBe('default-value')
stored.value = 'new-value'
expect(stored.value).toBe('new-value')
expect(localStorage.getItem(key)).toBe('"new-value"')
stored.value = 'default-value'
expect(stored.value).toBe('default-value')
expect(localStorage.getItem(key)).toBeNull()
})

it('should sync within the same document', async () => {
const key = 'test-key'
const stored1 = useLocalStorage(key, 0)
const stored2 = useLocalStorage(key, 0)
expect(stored1.value).toBe(0)
expect(stored2.value).toBe(0)

stored1.value++
expect(stored1.value).toBe(1)
expect(stored2.value).toBe(1)
stored2.value++
expect(stored1.value).toBe(2)
expect(stored2.value).toBe(2)

const onStored1Change = vitest.fn()
const onStored2Change = vitest.fn()
watch(stored1, (v) => onStored1Change(v))
watch(stored2, (v) => onStored2Change(v))
stored1.value = 3
await nextTick()
expect(onStored1Change).toHaveBeenCalledTimes(1)
expect(onStored1Change).toHaveBeenCalledWith(3)
expect(onStored2Change).toHaveBeenCalledTimes(1)
expect(onStored2Change).toHaveBeenCalledWith(3)
stored2.value = 4
await nextTick()
expect(onStored1Change).toHaveBeenCalledTimes(2)
expect(onStored1Change).toHaveBeenCalledWith(4)
expect(onStored2Change).toHaveBeenCalledTimes(2)
expect(onStored2Change).toHaveBeenCalledWith(4)
})
})
56 changes: 35 additions & 21 deletions spx-gui/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { memoize } from 'lodash'
import dayjs from 'dayjs'
import { ref, shallowReactive, shallowRef, watch, watchEffect, type WatchSource } from 'vue'
import { ref, shallowReactive, shallowRef, watch, watchEffect, type ShallowRef, type WatchSource, computed } from 'vue'
import { useI18n, type LocaleMessage } from './i18n'

export const isImage = (url: string): boolean => {
Expand All @@ -25,14 +25,9 @@ export function isAddPublicLibraryEnabled() {
return /\blibrary\b/.test(window.location.search)
}

/**
* Get user specified spx version.
* Currently spx v2 is not production ready, so we provide a way to specify spx version by adding `?spx=(v2|v1)` in URL query.
* This is an informal & temporary behavior.
*/
export function getSpxVersion() {
const matched = /\bspx=(\w+)\b/.exec(window.location.search)
return matched?.[1]
/** Manage spx version. */
export function useSpxVersion(): ShallowRef<'v1' | 'v2'> {
return useLocalStorage<'v1' | 'v2'>('spx-gui-runner', 'v1')
}

export function useAsyncComputed<T>(getter: () => Promise<T>) {
Expand Down Expand Up @@ -70,20 +65,39 @@ export function computedShallowReactive<T extends object>(getter: () => T) {
return r
}

const lsSyncer = shallowReactive(new Map<string, number>())

function watchLSChange(key: string) {
lsSyncer.get(key)
}

function fireLSChange(key: string) {
const val = lsSyncer.get(key) ?? 0
lsSyncer.set(key, val + 1)
}

/**
* Manipulate data stored in localStorage.
* Changes will be synchronized within the same document.
*/
export function useLocalStorage<T>(key: string, initialValue: T) {
const ref = shallowRef<T>(initialValue)
const storedValue = localStorage.getItem(key)
if (storedValue != null) {
ref.value = JSON.parse(storedValue)
}
watch(ref, (newValue) => {
if (newValue === initialValue) {
// Remove the key if the value is the initial value.
// NOTE: this may be unexpected for some special use cases
localStorage.removeItem(key)
return
const ref = computed<T>({
get() {
watchLSChange(key)
const storedValue = localStorage.getItem(key)
if (storedValue == null) return initialValue
return JSON.parse(storedValue)
},
set(newValue) {
if (newValue === initialValue) {
// Remove the key if the value is the initial value.
// NOTE: this may be unexpected for some special use cases
localStorage.removeItem(key)
} else {
localStorage.setItem(key, JSON.stringify(newValue))
}
fireLSChange(key)
}
localStorage.setItem(key, JSON.stringify(newValue))
})
return ref
}
Expand Down
Loading