Skip to content
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
27 changes: 25 additions & 2 deletions src/components/group/GroupFormComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
label="URL Slug"
hint="Lowercase letters, numbers, and hyphens only (3-100 characters)"
:rules="slugRules"
:error="!!slugError"
:error-message="slugError || undefined"
@update:model-value="slugError = null"
class="q-mb-sm"
/>
<p class="text-caption q-mt-xs text-grey-7">
Expand Down Expand Up @@ -213,6 +216,7 @@ const group = ref<GroupEntity>({
})

const loading = ref(false)
const slugError = ref<string | null>(null)

const onUpdateLocation = (address: { lat: number, lon: number, location: string }) => {
group.value.lat = address.lat
Expand Down Expand Up @@ -344,9 +348,28 @@ const onSubmit = async () => {
emit('created', res.data)
analyticsService.trackEvent('group_created', { group_id: res.data.id, name: res.data.name })
}
} catch (err) {
} catch (err: unknown) {
console.log(err)
error('Failed to create a group')
// Handle specific error types
const axiosError = err as { response?: { status?: number; data?: { message?: string } } }
if (axiosError.response?.status === 409) {
// Slug conflict - set field error and show notification
const errorMessage = axiosError.response.data?.message || 'This URL slug is already in use'
slugError.value = errorMessage
error(errorMessage + '. Please choose a different one.')
// Scroll to the slug field
await nextTick()
const slugField = document.querySelector('[data-cy="group-slug"]')
slugField?.scrollIntoView({ behavior: 'smooth', block: 'center' })
} else if (axiosError.response?.status === 422) {
// Validation error
error(axiosError.response.data?.message || 'Please check the form for validation errors.')
} else if (axiosError.response?.status === 403) {
// Forbidden - not authorized
error('You do not have permission to update this group.')
} else {
error(props.editGroupSlug ? 'Failed to update group' : 'Failed to create a group')
}
} finally {
Loading.hide()
}
Expand Down
20 changes: 4 additions & 16 deletions src/composables/useNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
import { useRouter, useRoute } from 'vue-router'
import { useRouter } from 'vue-router'
import { GroupEntity, EventEntity, UserEntity } from '../types'

export function useNavigation () {
const router = useRouter()
const route = useRoute()

/**
* Check if the current route is within the dashboard context
*/
const isInDashboardContext = (): boolean => {
return route.path.startsWith('/dashboard')
}

const navigateToGroup = (group: GroupEntity) => {
// When navigating from dashboard context, stay in dashboard and use replace
// to avoid back-button issues when slug changes
if (isInDashboardContext()) {
router.replace({ name: 'DashboardGroupPage', params: { slug: group.slug } })
} else {
router.push({ name: 'GroupPage', params: { slug: group.slug } })
}
// After creating/updating a group, always navigate to the public view page
// so the user can see the result of their changes
router.push({ name: 'GroupPage', params: { slug: group.slug } })
}

const navigateToEvent = (event: EventEntity) => {
Expand Down
79 changes: 22 additions & 57 deletions test/vitest/__tests__/composables/useNavigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { installRouter } from '../../install-router'
import { mount, flushPromises } from '@vue/test-utils'
import { defineComponent, h } from 'vue'
import { useNavigation } from '../../../../src/composables/useNavigation'
import { GroupEntity, GroupVisibility, GroupStatus, EventEntity, EventStatus } from '../../../../src/types'
import { GroupEntity, GroupVisibility, GroupStatus, EventEntity, EventStatus, UserEntity } from '../../../../src/types'

installQuasarPlugin()
installRouter({
Expand Down Expand Up @@ -69,7 +69,7 @@ describe('useNavigation', () => {
})
})

it('should navigate to DashboardGroupPage when on dashboard route', async () => {
it('should navigate to GroupPage (not DashboardGroupPage) when on dashboard route', async () => {
let navigation: ReturnType<typeof useNavigation>

const TestComponent = createTestComponent(() => {
Expand All @@ -82,24 +82,25 @@ describe('useNavigation', () => {

// Get the router mock from wrapper
const router = wrapper.router
const routerReplace = vi.spyOn(router, 'replace')
const routerPush = vi.spyOn(router, 'push')

// Simulate being on a dashboard route
await router.push({ path: '/dashboard/groups/original-group' })
await flushPromises()
routerPush.mockClear()

// Navigate to group with new slug
navigation!.navigateToGroup(mockGroup)
await flushPromises()

// Should use replace to navigate to DashboardGroupPage (to avoid back-button issues)
expect(routerReplace).toHaveBeenCalledWith({
name: 'DashboardGroupPage',
// Should navigate to public GroupPage so user can see their changes
expect(routerPush).toHaveBeenCalledWith({
name: 'GroupPage',
params: { slug: 'test-group' }
})
})

it('should use router.replace for dashboard navigation to avoid back-button issues', async () => {
it('should navigate to public group page after update (even from dashboard)', async () => {
let navigation: ReturnType<typeof useNavigation>

const TestComponent = createTestComponent(() => {
Expand All @@ -111,28 +112,27 @@ describe('useNavigation', () => {
await flushPromises()

const router = wrapper.router
const routerReplace = vi.spyOn(router, 'replace')
const routerPush = vi.spyOn(router, 'push')

// Simulate being on a dashboard group edit route
await router.push({ path: '/dashboard/groups/old-slug' })
await flushPromises()

// Clear call counts after setup
routerReplace.mockClear()
routerPush.mockClear()

// Navigate to updated group
// Navigate to updated group - should go to public view page
navigation!.navigateToGroup({ ...mockGroup, slug: 'new-slug' })
await flushPromises()

// Should use replace, not push
expect(routerReplace).toHaveBeenCalled()
// push should not be called for dashboard navigation
expect(routerPush).not.toHaveBeenCalled()
// Should navigate to public GroupPage so user can see their changes
expect(routerPush).toHaveBeenCalledWith({
name: 'GroupPage',
params: { slug: 'new-slug' }
})
})

it('should detect dashboard context from various dashboard paths', async () => {
it('should always navigate to public GroupPage regardless of current context', async () => {
let navigation: ReturnType<typeof useNavigation>

const TestComponent = createTestComponent(() => {
Expand All @@ -144,54 +144,19 @@ describe('useNavigation', () => {
await flushPromises()

const router = wrapper.router
const routerReplace = vi.spyOn(router, 'replace')
const routerPush = vi.spyOn(router, 'push')

// Test different dashboard paths
const dashboardPaths = [
// Test from various paths - all should go to public GroupPage
const testPaths = [
'/dashboard/groups/test-group',
'/dashboard/groups/create',
'/dashboard/events',
'/dashboard'
]

for (const path of dashboardPaths) {
await router.push({ path })
await flushPromises()
routerReplace.mockClear()

navigation!.navigateToGroup(mockGroup)
await flushPromises()

expect(routerReplace).toHaveBeenCalledWith({
name: 'DashboardGroupPage',
params: { slug: 'test-group' }
})
}
})

it('should navigate to public page from non-dashboard routes', async () => {
let navigation: ReturnType<typeof useNavigation>

const TestComponent = createTestComponent(() => {
navigation = useNavigation()
return {}
})

const wrapper = mount(TestComponent)
await flushPromises()

const router = wrapper.router
const routerPush = vi.spyOn(router, 'push')

// Test different non-dashboard paths
const publicPaths = [
'/dashboard',
'/groups/some-group',
'/events',
'/',
'/members/user'
'/'
]

for (const path of publicPaths) {
for (const path of testPaths) {
await router.push({ path })
await flushPromises()
routerPush.mockClear()
Expand Down Expand Up @@ -326,7 +291,7 @@ describe('useNavigation', () => {
const router = wrapper.router
const routerPush = vi.spyOn(router, 'push')

navigation!.navigateToMember({ slug: 'user-object-slug' } as { slug: string })
navigation!.navigateToMember({ slug: 'user-object-slug' } as unknown as UserEntity)
await flushPromises()

expect(routerPush).toHaveBeenCalledWith({
Expand Down