Skip to content

Commit 85d8edf

Browse files
committed
feat: [OCISDEV-710] add theme mode
Added a new property to the theme called `mode`. This property specifies whether the theme is suitable for regular mode or vault mode. Valid values are `regular` and `vault`.
1 parent b7a23a9 commit 85d8edf

9 files changed

Lines changed: 133 additions & 11 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Enhancement: Add theme mode
2+
3+
We've added a new property to the theme called `mode`. This property specifies whether the theme is suitable for regular mode or vault mode.
4+
Valid values are `regular` and `vault`.
5+
6+
https://github.com/owncloud/web/pull/13631

packages/web-pkg/src/composables/piniaStores/theme.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useLocalStorage, usePreferredDark } from '@vueuse/core'
55
import { z } from 'zod'
66
import { applyCustomProp } from '@ownclouders/design-system/helpers'
77
import { ShareRole } from '@ownclouders/web-client'
8+
import { useVault } from '../vault'
89

910
const AppBanner = z.object({
1011
title: z.string().optional(),
@@ -76,6 +77,11 @@ const WebTheme = z.object({
7677
common: CommonSection.optional(),
7778
designTokens: DesignTokens.optional(),
7879
isDark: z.boolean(),
80+
/**
81+
* Specifies whether the theme is suitable for regular mode or vault mode.
82+
* If not specified, the theme is suitable for regular mode.
83+
*/
84+
mode: z.optional(z.enum(['regular', 'vault']).default('regular')),
7985
name: z.string(),
8086
loginPage: LoginPage.optional(),
8187
logo: Logo.optional(),
@@ -103,13 +109,24 @@ export const useThemeStore = defineStore('theme', () => {
103109
const currentLocalStorageThemeName = useLocalStorage(themeStorageKey, null)
104110

105111
const isDark = usePreferredDark()
112+
const { isInVault } = useVault()
106113

107114
const currentTheme = ref<WebThemeType | undefined>()
108115

109-
const availableThemes = ref<WebThemeType[]>([])
116+
const themes = ref<WebThemeType[]>([])
117+
118+
const availableThemes = computed(() => {
119+
return unref(themes).filter((theme) => {
120+
if (unref(isInVault)) {
121+
return theme.mode === 'vault'
122+
}
123+
124+
return theme.mode === 'regular' || theme.mode === undefined
125+
})
126+
})
110127

111128
const initializeThemes = (themeConfig: WebThemeConfigType) => {
112-
availableThemes.value = themeConfig.themes.map((theme) =>
129+
themes.value = themeConfig.themes.map((theme) =>
113130
merge<WebThemeType>(themeConfig.defaults, theme)
114131
)
115132
setThemeFromStorageOrSystem()
@@ -168,6 +185,7 @@ export const useThemeStore = defineStore('theme', () => {
168185
return {
169186
availableThemes,
170187
currentTheme,
188+
themes,
171189
initializeThemes,
172190
setAndApplyTheme,
173191
setAutoSystemTheme,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* This composable is used for various vault-related functionality.
3+
*/
4+
interface VaultComposable {
5+
/**
6+
* Checks whether the user is currently in the vault.
7+
* This is not reactive value because a full page reload is required to switch between regular and vault mode.
8+
*/
9+
isInVault: boolean
10+
}
11+
12+
export function useVault(): VaultComposable {
13+
const isInVault = (() => {
14+
const { pathname, hash } = window.location
15+
16+
if (pathname.startsWith('/vault')) {
17+
return true
18+
}
19+
20+
if (hash) {
21+
const vaultHashPattern = /^#\/vault(?:\/|$)/
22+
return vaultHashPattern.test(hash)
23+
}
24+
25+
return false
26+
})()
27+
28+
return {
29+
isInVault
30+
}
31+
}

packages/web-pkg/tests/unit/composables/piniaStores/theme.spec.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import { useThemeStore, WebThemeConfigType } from '../../../../src/composables/p
33
import { mockDeep } from 'vitest-mock-extended'
44
import { createPinia, setActivePinia } from 'pinia'
55
import { ref, computed } from 'vue'
6+
import { useVault } from '../../../../src/composables/vault'
67

78
vi.mock('@vueuse/core', () => {
89
return { useLocalStorage: vi.fn(() => ref('')), usePreferredDark: vi.fn(() => ref(false)) }
910
})
1011

12+
vi.mock('../../../../src/composables/vault', () => {
13+
return { useVault: vi.fn(() => ({ isInVault: false })) }
14+
})
15+
1116
describe('useThemeStore', () => {
1217
beforeEach(() => {
1318
setActivePinia(createPinia())
@@ -69,5 +74,62 @@ describe('useThemeStore', () => {
6974
expect(store.currentTheme.name).toEqual('light')
7075
})
7176
})
77+
78+
describe('availableThemes', () => {
79+
it('returns regular themes if not in vault', () => {
80+
vi.mocked(useVault).mockReturnValue({ isInVault: false })
81+
82+
const themeConfig = mockDeep<WebThemeConfigType>()
83+
themeConfig.themes = [
84+
{ name: 'light', designTokens: {}, isDark: false, mode: 'regular' },
85+
{ name: 'dark', designTokens: {}, isDark: true, mode: 'regular' },
86+
{ name: 'light', designTokens: {}, isDark: false, mode: 'vault' },
87+
{ name: 'dark', designTokens: {}, isDark: true, mode: 'vault' }
88+
]
89+
90+
const store = useThemeStore()
91+
store.initializeThemes(themeConfig)
92+
93+
for (const theme of store.availableThemes) {
94+
expect(theme.mode).toBe('regular')
95+
}
96+
})
97+
it('returns vault themes if in vault', () => {
98+
vi.mocked(useVault).mockReturnValue({ isInVault: true })
99+
100+
const themeConfig = mockDeep<WebThemeConfigType>()
101+
themeConfig.themes = [
102+
{ name: 'light', designTokens: {}, isDark: false, mode: 'regular' },
103+
{ name: 'dark', designTokens: {}, isDark: true, mode: 'regular' },
104+
{ name: 'light', designTokens: {}, isDark: false, mode: 'vault' },
105+
{ name: 'dark', designTokens: {}, isDark: true, mode: 'vault' }
106+
]
107+
108+
const store = useThemeStore()
109+
store.initializeThemes(themeConfig)
110+
111+
for (const theme of store.availableThemes) {
112+
expect(theme.mode).toBe('vault')
113+
}
114+
})
115+
it('treats themes without mode as regular themes', () => {
116+
vi.mocked(useVault).mockReturnValue({ isInVault: false })
117+
118+
const themeConfig = mockDeep<WebThemeConfigType>()
119+
themeConfig.themes = [
120+
{ name: 'light', designTokens: {}, isDark: false, mode: 'regular' },
121+
{ name: 'dark', designTokens: {}, isDark: true },
122+
{ name: 'light', designTokens: {}, isDark: false, mode: 'vault' },
123+
{ name: 'dark', designTokens: {}, isDark: true, mode: 'vault' }
124+
]
125+
126+
const store = useThemeStore()
127+
store.initializeThemes(themeConfig)
128+
expect(store.availableThemes.length).toBe(2)
129+
for (const theme of store.availableThemes) {
130+
expect(theme.mode).not.toBe('vault')
131+
}
132+
})
133+
})
72134
})
73135
})

packages/web-runtime/src/components/Account/ThemeSwitcher.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ export default defineComponent({
4747
return unref(currentTheme)
4848
})
4949
50-
const translatedThemeNames = unref(availableThemes).map((theme) => {
51-
return { ...theme, name: $gettext(theme.name) }
52-
})
50+
const translatedThemeNames = computed(() =>
51+
unref(availableThemes).map((theme) => {
52+
return { ...theme, name: $gettext(theme.name) }
53+
})
54+
)
5355
5456
const availableThemesAndAuto = computed(() => [
5557
unref(autoTheme),

packages/web-runtime/src/components/Topbar/UserMenu.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ export default defineComponent({
184184
185185
const accountPageRoute = computed(() => ({
186186
name: 'account',
187-
query: routeToContextQuery(unref(route))
187+
query: routeToContextQuery(unref(route)),
188+
params: {
189+
scope: unref(route).params.scope
190+
}
188191
}))
189192
190193
const loginLink = computed(() => {

packages/web-runtime/src/router/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const routes = [
8484
meta: { title: $gettext('Access denied'), authContext: 'anonymous' }
8585
},
8686
{
87-
path: '/account',
87+
path: '/:scope(vault)?/account',
8888
name: 'account',
8989
component: Account,
9090
meta: { title: $gettext('Account'), authContext: 'hybrid' }

packages/web-runtime/tests/unit/components/Account/ThemeSwitcher.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('ThemeSwitcher component', () => {
3434
})
3535

3636
function getWrapper({ hasOnlyOneTheme = false } = {}) {
37-
const availableThemes = hasOnlyOneTheme
37+
const themes = hasOnlyOneTheme
3838
? [defaultTheme.clients.web.themes[0]]
3939
: defaultTheme.clients.web.themes
4040

@@ -46,7 +46,7 @@ function getWrapper({ hasOnlyOneTheme = false } = {}) {
4646
piniaOptions: {
4747
stubActions: false,
4848
themeState: {
49-
availableThemes,
49+
themes,
5050
currentTheme: mock<WebThemeType>({
5151
...defaultOwnCloudTheme.defaults,
5252
...defaultOwnCloudTheme.themes[0]

packages/web-test-helpers/src/mocks/pinia.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export type PiniaMockOptions = {
3737
userContextReady?: boolean
3838
publicLinkContextReady?: boolean
3939
}
40-
themeState?: { availableThemes?: WebThemeType[]; currentTheme?: WebThemeType }
40+
themeState?: { themes?: WebThemeType[]; currentTheme?: WebThemeType }
4141
clipboardState?: { action?: ClipboardActions; resources?: Resource[] }
4242
configState?: {
4343
server?: string
@@ -135,7 +135,7 @@ export function createMockStore({
135135
...defaultOwnCloudTheme.defaults,
136136
...defaultOwnCloudTheme.themes[0]
137137
},
138-
availableThemes: defaultOwnCloudTheme.themes,
138+
themes: defaultOwnCloudTheme.themes,
139139
...themeState
140140
},
141141
resources: { resources: [], ...resourcesStore },

0 commit comments

Comments
 (0)