mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-08-30 17:09:13 +08:00
Make enabled model plaza discoverable from /home
The model plaza route already supports public access, but both built-in /home headers omit its entry. Add the link to compact and default headers while keeping the existing feature and authentication settings authoritative, then cover the visibility matrix with focused component tests. Constraint: Keep the change frontend-only and preserve router-owned access control Rejected: Add the link to AppHeader only | /home renders its own headers and never mounts AppHeader Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep the model plaza entry gated by the existing opt-in flag and require-auth setting Tested: HomeView focused Vitest, full frontend Vitest (223 files / 1554 tests), ESLint, vue-tsc, production build Not-tested: Manual browser click-through against a running backend Related: #5524
This commit is contained in:
@@ -40,6 +40,15 @@
|
||||
>
|
||||
<Icon name="book" size="md" />
|
||||
</a>
|
||||
<router-link
|
||||
v-if="showModelPlazaEntry"
|
||||
to="/model-plaza"
|
||||
class="flex h-10 shrink-0 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-dark-400 dark:hover:bg-dark-800 dark:hover:text-white"
|
||||
:title="t('nav.modelPlaza')"
|
||||
>
|
||||
<Icon name="grid" size="md" />
|
||||
<span class="hidden sm:inline">{{ t('nav.modelPlaza') }}</span>
|
||||
</router-link>
|
||||
<button
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 dark:text-dark-400 dark:hover:bg-dark-800"
|
||||
:title="isDark ? t('home.switchToLight') : t('home.switchToDark')"
|
||||
@@ -132,6 +141,17 @@
|
||||
<Icon name="book" size="md" />
|
||||
</a>
|
||||
|
||||
<!-- Model Plaza Link -->
|
||||
<router-link
|
||||
v-if="showModelPlazaEntry"
|
||||
to="/model-plaza"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg p-2 text-sm text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-dark-400 dark:hover:bg-dark-800 dark:hover:text-white"
|
||||
:title="t('nav.modelPlaza')"
|
||||
>
|
||||
<Icon name="grid" size="md" />
|
||||
<span class="hidden sm:inline">{{ t('nav.modelPlaza') }}</span>
|
||||
</router-link>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<button
|
||||
@click="toggleTheme"
|
||||
@@ -480,6 +500,7 @@ import { useAuthStore, useAppStore } from '@/stores'
|
||||
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { sanitizeUrl } from '@/utils/url'
|
||||
import { FeatureFlags, isFeatureFlagEnabled } from '@/utils/featureFlags'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -494,6 +515,7 @@ const docUrl = computed(() => sanitizeUrl(appStore.cachedPublicSettings?.doc_url
|
||||
const homeContent = computed(() => appStore.cachedPublicSettings?.home_content || '')
|
||||
const hasHomeContent = computed(() => homeContent.value.trim().length > 0)
|
||||
const compactHomeEnabled = computed(() => appStore.cachedPublicSettings?.compact_home_enabled === true)
|
||||
const modelPlazaEnabled = computed(() => isFeatureFlagEnabled(FeatureFlags.modelPlaza))
|
||||
|
||||
// Check if homeContent is a URL (for iframe display)
|
||||
const isHomeContentUrl = computed(() => {
|
||||
@@ -509,6 +531,12 @@ const githubUrl = 'https://github.com/Wei-Shaw/sub2api'
|
||||
|
||||
// Auth state
|
||||
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
||||
const modelPlazaRequiresAuth = computed(
|
||||
() => appStore.cachedPublicSettings?.model_plaza_require_auth === true,
|
||||
)
|
||||
const showModelPlazaEntry = computed(
|
||||
() => modelPlazaEnabled.value && (isAuthenticated.value || !modelPlazaRequiresAuth.value),
|
||||
)
|
||||
const isAdmin = computed(() => authStore.isAdmin)
|
||||
const dashboardPath = computed(() => isAdmin.value ? '/admin/dashboard' : '/dashboard')
|
||||
const userInitial = computed(() => {
|
||||
|
||||
@@ -25,6 +25,10 @@ vi.mock('@/stores', () => ({
|
||||
useAuthStore: () => authStore,
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/app', () => ({
|
||||
useAppStore: () => appStore,
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('vue-i18n')>()
|
||||
return {
|
||||
@@ -55,6 +59,13 @@ function compactDestination(wrapper: ReturnType<typeof mountHome>) {
|
||||
return wrapper.get('[data-testid="compact-home"]').findComponent(RouterLinkStub).props('to')
|
||||
}
|
||||
|
||||
function modelPlazaDestination(wrapper: ReturnType<typeof mountHome>) {
|
||||
return wrapper
|
||||
.findAllComponents(RouterLinkStub)
|
||||
.find((link) => link.props('to') === '/model-plaza')
|
||||
?.props('to')
|
||||
}
|
||||
|
||||
describe('HomeView compact mode', () => {
|
||||
beforeEach(() => {
|
||||
authStore.isAuthenticated = false
|
||||
@@ -119,4 +130,55 @@ describe('HomeView compact mode', () => {
|
||||
expect(authStore.checkAuth).toHaveBeenCalledOnce()
|
||||
expect(appStore.fetchPublicSettings).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows the model plaza link to anonymous visitors when public access is enabled', () => {
|
||||
const wrapper = mountHome({
|
||||
compact_home_enabled: true,
|
||||
model_plaza_enabled: true,
|
||||
model_plaza_require_auth: false,
|
||||
})
|
||||
|
||||
expect(modelPlazaDestination(wrapper)).toBe('/model-plaza')
|
||||
})
|
||||
|
||||
it('hides the model plaza link from anonymous visitors when sign-in is required', () => {
|
||||
const wrapper = mountHome({
|
||||
compact_home_enabled: true,
|
||||
model_plaza_enabled: true,
|
||||
model_plaza_require_auth: true,
|
||||
})
|
||||
|
||||
expect(modelPlazaDestination(wrapper)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('shows the model plaza link to authenticated visitors when sign-in is required', () => {
|
||||
authStore.isAuthenticated = true
|
||||
|
||||
const wrapper = mountHome({
|
||||
compact_home_enabled: true,
|
||||
model_plaza_enabled: true,
|
||||
model_plaza_require_auth: true,
|
||||
})
|
||||
|
||||
expect(modelPlazaDestination(wrapper)).toBe('/model-plaza')
|
||||
})
|
||||
|
||||
it('shows the model plaza link in the default home header', () => {
|
||||
const wrapper = mountHome({
|
||||
model_plaza_enabled: true,
|
||||
model_plaza_require_auth: false,
|
||||
})
|
||||
|
||||
expect(modelPlazaDestination(wrapper)).toBe('/model-plaza')
|
||||
})
|
||||
|
||||
it('hides the model plaza link when the feature is disabled', () => {
|
||||
const wrapper = mountHome({
|
||||
compact_home_enabled: true,
|
||||
model_plaza_enabled: false,
|
||||
model_plaza_require_auth: false,
|
||||
})
|
||||
|
||||
expect(modelPlazaDestination(wrapper)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user