chore: fix type issues

This commit is contained in:
2026-03-10 01:42:42 +02:00
parent 8178b2f860
commit 016a764bbf
8 changed files with 338 additions and 59 deletions

View File

@@ -1,2 +1,3 @@
import AdminTable from './AdminTable.vue'
export default AdminTable
export type { TableColumn } from './AdminTable.vue'

View File

@@ -411,7 +411,7 @@ export default defineComponent({
// Navigate to first category if available
if (header.categories && header.categories.length > 0) {
const firstCategory = header.categories[0]
const firstCategory = header.categories[0]!
this.$router.push({ path: `/c/${firstCategory.slug}` })
}
},

View File

@@ -14,6 +14,18 @@ vi.mock('@nextcloud/vue/components/NcCheckboxRadioSwitch', () => ({
},
}))
type VM = InstanceType<typeof CategoryPermissionsTable> & {
toggleHeaderView: (id: number) => void
toggleHeaderPost: (id: number) => void
toggleHeaderReply: (id: number) => void
toggleHeaderModerate: (id: number) => void
getHeaderViewState: (id: number) => { checked: boolean; indeterminate: boolean }
getHeaderPostState: (id: number) => { checked: boolean; indeterminate: boolean }
getHeaderReplyState: (id: number) => { checked: boolean; indeterminate: boolean }
getHeaderModerateState: (id: number) => { checked: boolean; indeterminate: boolean }
ensurePermission: (id: number) => CategoryPermission
}
function createHeaders(): CategoryHeader[] {
return [
{
@@ -113,8 +125,8 @@ describe('CategoryPermissionsTable', () => {
})
const headerNames = wrapper.findAll('.header-name')
expect(headerNames).toHaveLength(2)
expect(headerNames[0].text()).toBe('General')
expect(headerNames[1].text()).toBe('Support')
expect(headerNames[0]!.text()).toBe('General')
expect(headerNames[1]!.text()).toBe('Support')
})
it('should render category names', () => {
@@ -126,9 +138,9 @@ describe('CategoryPermissionsTable', () => {
})
const categoryNames = wrapper.findAll('.category-name')
expect(categoryNames).toHaveLength(3)
expect(categoryNames[0].text()).toBe('Announcements')
expect(categoryNames[1].text()).toBe('Off-topic')
expect(categoryNames[2].text()).toBe('Bug reports')
expect(categoryNames[0]!.text()).toBe('Announcements')
expect(categoryNames[1]!.text()).toBe('Off-topic')
expect(categoryNames[2]!.text()).toBe('Bug reports')
})
it('should render category descriptions when present', () => {
@@ -140,7 +152,7 @@ describe('CategoryPermissionsTable', () => {
})
const descriptions = wrapper.findAll('.category-desc')
expect(descriptions).toHaveLength(1)
expect(descriptions[0].text()).toBe('Important announcements')
expect(descriptions[0]!.text()).toBe('Important announcements')
})
it('should render table column headers', () => {
@@ -157,6 +169,35 @@ describe('CategoryPermissionsTable', () => {
expect(header.text()).toContain('Can reply')
expect(header.text()).toContain('Can moderate')
})
it('should render the info box with permission descriptions', () => {
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions: createPermissions(),
},
})
const noteCard = wrapper.find('.nc-note-card')
expect(noteCard.exists()).toBe(true)
expect(noteCard.attributes('data-type')).toBe('info')
const items = noteCard.findAll('li')
expect(items).toHaveLength(4)
expect(items[0]!.html()).toContain('View:')
expect(items[1]!.html()).toContain('Post:')
expect(items[2]!.html()).toContain('Reply:')
expect(items[3]!.html()).toContain('Moderate:')
})
it('should render the info box even when no categories exist', () => {
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: [],
permissions: {},
},
})
const noteCard = wrapper.find('.nc-note-card')
expect(noteCard.exists()).toBe(true)
})
})
describe('checkbox states', () => {
@@ -243,12 +284,12 @@ describe('CategoryPermissionsTable', () => {
// Category 11 (Off-topic) currently has canView=false
// Find the category rows, second row's first checkbox (view)
const rows = wrapper.findAll('.table-row')
const offTopicRow = rows[1] // Off-topic is second category row
const viewCheckbox = offTopicRow.findAll('.nc-checkbox')[0]
const offTopicRow = rows[1]! // Off-topic is second category row
const viewCheckbox = offTopicRow.findAll('.nc-checkbox')[0]!
await viewCheckbox.trigger('click')
expect(permissions[11].canView).toBe(true)
expect(permissions[11]!.canView).toBe(true)
expect(wrapper.emitted('update:permissions')).toBeTruthy()
})
@@ -263,14 +304,72 @@ describe('CategoryPermissionsTable', () => {
// Category 10 (Announcements) currently has canModerate=false
const rows = wrapper.findAll('.table-row')
const announcementsRow = rows[0]
const moderateCheckbox = announcementsRow.findAll('.nc-checkbox')[3]
const announcementsRow = rows[0]!
const moderateCheckbox = announcementsRow.findAll('.nc-checkbox')[3]!
await moderateCheckbox.trigger('click')
expect(permissions[10].canModerate).toBe(true)
expect(permissions[10]!.canModerate).toBe(true)
expect(wrapper.emitted('update:permissions')).toBeTruthy()
})
it('should update canPost when a category post checkbox is toggled', async () => {
const permissions = createPermissions()
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions,
},
})
// Category 10 (Announcements) currently has canPost=false
const rows = wrapper.findAll('.table-row')
const announcementsRow = rows[0]!
const postCheckbox = announcementsRow.findAll('.nc-checkbox')[1]!
await postCheckbox.trigger('click')
expect(permissions[10]!.canPost).toBe(true)
expect(wrapper.emitted('update:permissions')).toBeTruthy()
})
it('should update canReply when a category reply checkbox is toggled', async () => {
const permissions = createPermissions()
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions,
},
})
// Category 10 (Announcements) currently has canReply=false
const rows = wrapper.findAll('.table-row')
const announcementsRow = rows[0]!
const replyCheckbox = announcementsRow.findAll('.nc-checkbox')[2]!
await replyCheckbox.trigger('click')
expect(permissions[10]!.canReply).toBe(true)
expect(wrapper.emitted('update:permissions')).toBeTruthy()
})
it('should not emit when clicking a disabled checkbox', async () => {
const permissions = createPermissions()
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions,
disableView: true,
},
})
const rows = wrapper.findAll('.table-row')
const viewCheckbox = rows[0]!.findAll('.nc-checkbox')[0]!
await viewCheckbox.trigger('click')
expect(wrapper.emitted('update:permissions')).toBeFalsy()
})
})
describe('header toggle behavior', () => {
@@ -287,17 +386,14 @@ describe('CategoryPermissionsTable', () => {
},
})
type VM = InstanceType<typeof CategoryPermissionsTable> & {
toggleHeaderView: (id: number) => void
}
const vm = wrapper.vm as unknown as VM
vm.toggleHeaderView(1)
// Both categories under "General" should now have canView=true
expect(permissions[10].canView).toBe(true)
expect(permissions[11].canView).toBe(true)
expect(permissions[10]!.canView).toBe(true)
expect(permissions[11]!.canView).toBe(true)
// "Support" category should be unchanged
expect(permissions[20].canView).toBe(false)
expect(permissions[20]!.canView).toBe(false)
})
it('should uncheck all categories in header when header view is toggled off', () => {
@@ -313,17 +409,56 @@ describe('CategoryPermissionsTable', () => {
},
})
type VM = InstanceType<typeof CategoryPermissionsTable> & {
toggleHeaderView: (id: number) => void
}
const vm = wrapper.vm as unknown as VM
vm.toggleHeaderView(1)
// Both categories under "General" should now have canView=false
expect(permissions[10].canView).toBe(false)
expect(permissions[11].canView).toBe(false)
expect(permissions[10]!.canView).toBe(false)
expect(permissions[11]!.canView).toBe(false)
// "Support" category should be unchanged
expect(permissions[20].canView).toBe(true)
expect(permissions[20]!.canView).toBe(true)
})
it('should check all categories in header when header post is toggled on', () => {
const permissions: Record<number, CategoryPermission> = {
10: { canView: false, canPost: false, canReply: false, canModerate: false },
11: { canView: false, canPost: false, canReply: false, canModerate: false },
20: { canView: false, canPost: false, canReply: false, canModerate: false },
}
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions,
},
})
const vm = wrapper.vm as unknown as VM
vm.toggleHeaderPost(1)
expect(permissions[10]!.canPost).toBe(true)
expect(permissions[11]!.canPost).toBe(true)
expect(permissions[20]!.canPost).toBe(false)
})
it('should check all categories in header when header reply is toggled on', () => {
const permissions: Record<number, CategoryPermission> = {
10: { canView: false, canPost: false, canReply: false, canModerate: false },
11: { canView: false, canPost: false, canReply: false, canModerate: false },
20: { canView: false, canPost: false, canReply: false, canModerate: false },
}
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions,
},
})
const vm = wrapper.vm as unknown as VM
vm.toggleHeaderReply(1)
expect(permissions[10]!.canReply).toBe(true)
expect(permissions[11]!.canReply).toBe(true)
expect(permissions[20]!.canReply).toBe(false)
})
it('should check all categories in header when header moderate is toggled on', () => {
@@ -339,15 +474,56 @@ describe('CategoryPermissionsTable', () => {
},
})
type VM = InstanceType<typeof CategoryPermissionsTable> & {
toggleHeaderModerate: (id: number) => void
}
const vm = wrapper.vm as unknown as VM
vm.toggleHeaderModerate(1)
expect(permissions[10].canModerate).toBe(true)
expect(permissions[11].canModerate).toBe(true)
expect(permissions[20].canModerate).toBe(false)
expect(permissions[10]!.canModerate).toBe(true)
expect(permissions[11]!.canModerate).toBe(true)
expect(permissions[20]!.canModerate).toBe(false)
})
it('should emit update:permissions when header is toggled', () => {
const permissions: Record<number, CategoryPermission> = {
10: { canView: false, canPost: false, canReply: false, canModerate: false },
11: { canView: false, canPost: false, canReply: false, canModerate: false },
20: { canView: false, canPost: false, canReply: false, canModerate: false },
}
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions,
},
})
const vm = wrapper.vm as unknown as VM
vm.toggleHeaderPost(1)
expect(wrapper.emitted('update:permissions')).toBeTruthy()
})
it('should not modify permissions when header has no categories', () => {
const headers: CategoryHeader[] = [
{
id: 1,
name: 'Empty',
description: null,
sortOrder: 0,
createdAt: 0,
categories: [],
},
]
const permissions: Record<number, CategoryPermission> = {}
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: headers,
permissions,
},
})
const vm = wrapper.vm as unknown as VM
vm.toggleHeaderView(1)
expect(Object.keys(permissions)).toHaveLength(0)
})
})
@@ -365,10 +541,6 @@ describe('CategoryPermissionsTable', () => {
},
})
type VM = InstanceType<typeof CategoryPermissionsTable> & {
getHeaderViewState: (id: number) => { checked: boolean; indeterminate: boolean }
getHeaderModerateState: (id: number) => { checked: boolean; indeterminate: boolean }
}
const vm = wrapper.vm as unknown as VM
// General header: 1/2 view checked → indeterminate
@@ -395,10 +567,6 @@ describe('CategoryPermissionsTable', () => {
},
})
type VM = InstanceType<typeof CategoryPermissionsTable> & {
getHeaderViewState: (id: number) => { checked: boolean; indeterminate: boolean }
getHeaderModerateState: (id: number) => { checked: boolean; indeterminate: boolean }
}
const vm = wrapper.vm as unknown as VM
// General header: 2/2 view checked → checked
@@ -425,9 +593,6 @@ describe('CategoryPermissionsTable', () => {
},
})
type VM = InstanceType<typeof CategoryPermissionsTable> & {
getHeaderViewState: (id: number) => { checked: boolean; indeterminate: boolean }
}
const vm = wrapper.vm as unknown as VM
// General header: 0/2 view checked → unchecked
@@ -435,6 +600,73 @@ describe('CategoryPermissionsTable', () => {
expect(generalView.checked).toBe(false)
expect(generalView.indeterminate).toBe(false)
})
it('should show indeterminate for post when some categories have canPost', () => {
const permissions: Record<number, CategoryPermission> = {
10: { canView: false, canPost: true, canReply: false, canModerate: false },
11: { canView: false, canPost: false, canReply: false, canModerate: false },
20: { canView: false, canPost: false, canReply: false, canModerate: false },
}
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions,
},
})
const vm = wrapper.vm as unknown as VM
const state = vm.getHeaderPostState(1)
expect(state.checked).toBe(false)
expect(state.indeterminate).toBe(true)
})
it('should show indeterminate for reply when some categories have canReply', () => {
const permissions: Record<number, CategoryPermission> = {
10: { canView: false, canPost: false, canReply: true, canModerate: false },
11: { canView: false, canPost: false, canReply: false, canModerate: false },
20: { canView: false, canPost: false, canReply: false, canModerate: false },
}
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions,
},
})
const vm = wrapper.vm as unknown as VM
const state = vm.getHeaderReplyState(1)
expect(state.checked).toBe(false)
expect(state.indeterminate).toBe(true)
})
it('should return unchecked for a non-existent header ID', () => {
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions: createPermissions(),
},
})
const vm = wrapper.vm as unknown as VM
const state = vm.getHeaderViewState(999)
expect(state.checked).toBe(false)
expect(state.indeterminate).toBe(false)
})
it('should handle missing permission entries gracefully', () => {
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions: {},
},
})
const vm = wrapper.vm as unknown as VM
// No permissions set, so header should be unchecked
const state = vm.getHeaderViewState(1)
expect(state.checked).toBe(false)
expect(state.indeterminate).toBe(false)
})
})
describe('ensurePermission', () => {
@@ -447,9 +679,6 @@ describe('CategoryPermissionsTable', () => {
},
})
type VM = InstanceType<typeof CategoryPermissionsTable> & {
ensurePermission: (id: number) => CategoryPermission
}
const vm = wrapper.vm as unknown as VM
const result = vm.ensurePermission(999)
@@ -472,13 +701,58 @@ describe('CategoryPermissionsTable', () => {
},
})
type VM = InstanceType<typeof CategoryPermissionsTable> & {
ensurePermission: (id: number) => CategoryPermission
}
const vm = wrapper.vm as unknown as VM
const result = vm.ensurePermission(10)
expect(result).toEqual({ canView: true, canPost: true, canReply: true, canModerate: true })
})
it('should persist created permission entry in the permissions object', () => {
const permissions: Record<number, CategoryPermission> = {}
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions,
},
})
const vm = wrapper.vm as unknown as VM
vm.ensurePermission(999)
expect(permissions[999]).toEqual({
canView: false,
canPost: false,
canReply: false,
canModerate: false,
})
})
})
describe('disabled states for post and reply', () => {
it('should disable post checkboxes when disablePost is true', () => {
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions: createPermissions(),
disablePost: true,
},
})
const disabledLabels = wrapper.findAll('.nc-checkbox.disabled')
// 2 header post + 3 category post = 5
expect(disabledLabels.length).toBe(5)
})
it('should disable reply checkboxes when disableReply is true', () => {
const wrapper = mount(CategoryPermissionsTable, {
props: {
categoryHeaders: createHeaders(),
permissions: createPermissions(),
disableReply: true,
},
})
const disabledLabels = wrapper.findAll('.nc-checkbox.disabled')
// 2 header reply + 3 category reply = 5
expect(disabledLabels.length).toBe(5)
})
})
})

View File

@@ -1,2 +1,3 @@
import ThreadCreateForm from './ThreadCreateForm.vue'
export default ThreadCreateForm
export type { DraftStatus } from './ThreadCreateForm.vue'

View File

@@ -55,6 +55,7 @@ export interface Thread {
categorySlug?: string | null
categoryName?: string | null
isSubscribed?: boolean
isBookmarked?: boolean
}
export interface Post {

View File

@@ -150,7 +150,7 @@
<NcButton
v-if="!isEditingTitle && canEditTitle"
@click="handleStartEditTitle"
type="tertiary"
variant="tertiary"
:aria-label="strings.editTitle"
:title="strings.editTitle"
class="edit-title-button"
@@ -163,7 +163,7 @@
v-if="isEditingTitle"
@click="handleSaveTitle"
:disabled="isSavingTitle || !editedTitle.trim()"
type="primary"
variant="primary"
:aria-label="strings.saveTitle"
:title="strings.saveTitle"
class="save-title-button"
@@ -198,7 +198,7 @@
<!-- First post (always shown) -->
<section v-if="!loading && !error && firstPost" class="mt-16 first-post-section">
<PostCard
:ref="(el) => setPostCardRef(el, firstPost.id)"
:ref="(el) => setPostCardRef(el, firstPost!.id)"
:post="firstPost"
:is-first-post="true"
:is-unread="isPostUnread(firstPost)"

View File

@@ -365,7 +365,7 @@ export default defineComponent({
replacementPlaceholder: t(
'forum',
'e.g., {strongStart}{content}{strongEnd}',
{ strongStart: '<strong>', strongEnd: '</strong>' },
{ strongStart: '<strong>', content: '{content}', strongEnd: '</strong>' },
{ escape: false },
),
replacementHelp: t(

View File

@@ -45,11 +45,11 @@
<strong>{{ strings.guestAccessDisabledTitle }}</strong>
</p>
<p>{{ strings.guestAccessDisabledMessage }}</p>
<template #action>
<NcButton @click="goToForumSettings" type="primary">
<div class="mt-16">
<NcButton @click="goToForumSettings" variant="primary">
{{ strings.goToForumSettings }}
</NcButton>
</template>
</div>
</NcNoteCard>
<!-- Basic Info Section -->
@@ -495,8 +495,9 @@ export default defineComponent({
this.categoryHeaders.forEach((header) => {
if (header.categories) {
header.categories.forEach((category) => {
if (this.permissions[category.id]) {
this.permissions[category.id].canModerate = false
const perm = this.permissions[category.id]
if (perm) {
perm.canModerate = false
}
})
}
@@ -508,8 +509,9 @@ export default defineComponent({
this.categoryHeaders.forEach((header) => {
if (header.categories) {
header.categories.forEach((category) => {
if (this.permissions[category.id]) {
this.permissions[category.id].canModerate = false
const perm = this.permissions[category.id]
if (perm) {
perm.canModerate = false
}
})
}