fix(wiki): gate write controls behind the manage:wiki capability

This commit is contained in:
matevip 2026-05-17 17:55:48 +08:00
parent c9f73bbbd5
commit 71aba2649e
7 changed files with 61 additions and 17 deletions

View File

@ -45,7 +45,7 @@
<!-- Error state with actions -->
<div v-if="status === 'failed'" class="stage-error">
<span class="error-badge"> {{ t('wiki.jobStage.failed') }} {{ errorCode }}</span>
<div class="error-actions">
<div v-if="canManageWiki" class="error-actions">
<button class="btn-mini" @click="$emit('reprocess')">{{ t('wiki.reprocess') }}</button>
<button class="btn-mini btn-mini-alt" @click="$emit('repair')">{{ t('wiki.page.repair') }}</button>
</div>
@ -56,8 +56,13 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
const { t } = useI18n()
const workspace = useWorkspaceStore()
// Reprocess / repair are write actions viewers see job status without them.
const canManageWiki = computed(() => workspace.can('manage:wiki'))
const props = defineProps<{
stage: string

View File

@ -24,7 +24,7 @@
<el-icon :size="12"><Link /></el-icon>
{{ t('wiki.page.enriched') }}
</span>
<button v-else class="btn-mini-enrich" @click="$emit('enrich')">
<button v-else-if="canManageWiki" class="btn-mini-enrich" @click="$emit('enrich')">
<el-icon :size="12"><Link /></el-icon>
{{ t('wiki.page.enrich') }}
</button>
@ -41,8 +41,13 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Link } from '@element-plus/icons-vue'
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
const { t } = useI18n()
const workspace = useWorkspaceStore()
// Enriching a page (adding cross-links) is a write action viewers only read.
const canManageWiki = computed(() => workspace.can('manage:wiki'))
const props = defineProps<{
page: {

View File

@ -1,7 +1,7 @@
<template>
<div class="raw-panel">
<!-- Upload + Add text row -->
<div class="upload-row">
<div v-if="canManageWiki" class="upload-row">
<div
class="upload-zone"
:class="{ 'is-dragging': isDragging, 'is-uploading': uploadingFiles.length > 0 }"
@ -45,7 +45,7 @@
</div>
<!-- Directory scan -->
<div class="dir-scan-row">
<div v-if="canManageWiki" class="dir-scan-row">
<div class="dir-input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
@ -163,7 +163,7 @@
{{ raw.errorMessage }}
</span>
</div>
<div class="raw-item-actions">
<div v-if="canManageWiki" class="raw-item-actions">
<button
v-if="raw.processingStatus === 'processing' && !cancellingIds.has(raw.id)"
class="btn-icon btn-icon-danger" :title="t('wiki.cancel')"
@ -290,12 +290,18 @@ import { mcToast } from '@/composables/useMcToast'
import { useFileDrop } from '@/composables/useFileDrop'
import { Download } from '@element-plus/icons-vue'
import { useWikiStore } from '@/stores/useWikiStore'
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
import { wikiApi } from '@/api/index'
import JobStageBar from './JobStageBar.vue'
import type { WikiProcessingJob } from '@/composables/useWikiJobPoller'
const { t } = useI18n()
const store = useWikiStore()
const workspace = useWorkspaceStore()
// Uploading, scanning and (re)processing raw material all require manage:wiki.
// Viewers can still see the material list but get no write controls.
const canManageWiki = computed(() => workspace.can('manage:wiki'))
const fileInput = ref<HTMLInputElement | null>(null)
// While raw materials are active, subscribe to the backend SSE progress stream.

View File

@ -9,6 +9,7 @@
@keydown.space.prevent="$emit('open', kb.id)"
>
<button
v-if="canManageWiki"
type="button"
class="kb-card-delete"
:title="t('wiki.library.deleteKB')"
@ -61,6 +62,7 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { WikiKB } from '@/stores/useWikiStore'
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
import { kbAccent, kbAccentFg, kbInitial, relativeTime } from '../utils/kbVisual'
const props = defineProps<{
@ -74,6 +76,10 @@ defineEmits<{
}>()
const { t, locale } = useI18n()
const workspace = useWorkspaceStore()
// Deleting a knowledge base requires manage:wiki; hide it from read-only viewers.
const canManageWiki = computed(() => workspace.can('manage:wiki'))
const failedJobCount = computed(() => props.failedJobCount ?? 0)
const initial = computed(() => kbInitial(props.kb))

View File

@ -6,7 +6,7 @@
<h1 class="mc-page-title">{{ t('nav.wiki') }}</h1>
<p class="mc-page-desc">{{ t('wiki.desc') }}</p>
</div>
<button class="btn-primary page-cta" @click="$emit('create')">
<button v-if="canManageWiki" class="btn-primary page-cta" @click="$emit('create')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
@ -48,7 +48,7 @@
</div>
<h3 class="library-empty-title">{{ t('wiki.library.empty') }}</h3>
<p class="library-empty-hint">{{ t('wiki.library.emptyHint') }}</p>
<button class="btn-primary" @click="$emit('create')">
<button v-if="canManageWiki" class="btn-primary" @click="$emit('create')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
{{ t('wiki.createKB') }}
</button>
@ -87,6 +87,7 @@
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import type { WikiKB } from '@/stores/useWikiStore'
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
import McPagination from '@/components/common/McPagination.vue'
import WikiKBCard from './WikiKBCard.vue'
@ -108,6 +109,10 @@ defineEmits<{
}>()
const { t } = useI18n()
const workspace = useWorkspaceStore()
// view:wiki users without manage:wiki may browse but not create knowledge bases.
const canManageWiki = computed(() => workspace.can('manage:wiki'))
const searchInput = ref('')
type SortMode = 'recent' | 'name' | 'pages'

View File

@ -13,8 +13,8 @@
<p class="summary-text">{{ store.currentPage.summary }}</p>
</div>
<!-- Actions bar -->
<div class="page-actions-bar">
<!-- Actions bar write actions, hidden for read-only viewers -->
<div v-if="canManageWiki" class="page-actions-bar">
<button class="btn-secondary btn-sm" @click="editing = !editing">
{{ editing ? t('common.cancel') : t('common.edit') }}
</button>
@ -99,6 +99,7 @@
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useWikiStore, isProtectedPage, type WikiPage } from '@/stores/useWikiStore'
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
import { wikiApi } from '@/api/index'
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
import { Link, SetUp } from '@element-plus/icons-vue'
@ -109,8 +110,13 @@ import ImageLightbox from './ImageLightbox.vue'
const { t } = useI18n()
const store = useWikiStore()
const workspace = useWorkspaceStore()
const { renderMarkdown } = useMarkdownRenderer()
// Editing, enriching, repairing and deleting pages all require manage:wiki.
// Read-only viewers get the rendered page without the action bar.
const canManageWiki = computed(() => workspace.can('manage:wiki'))
const editing = ref(false)
const editContent = ref('')
const backlinks = ref<WikiPage[]>([])

View File

@ -56,6 +56,7 @@
import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useWikiStore, type WikiKB } from '@/stores/useWikiStore'
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
import RawMaterialPanel from './RawMaterialPanel.vue'
import WikiPageViewer from './WikiPageViewer.vue'
import WikiConfig from './WikiConfig.vue'
@ -69,17 +70,27 @@ defineProps<{ kb: WikiKB }>()
const { t } = useI18n()
const store = useWikiStore()
const workspace = useWorkspaceStore()
// Read-only viewers (view:wiki without manage:wiki) only get the browsing tabs;
// the processing-config and transformations tabs are management surfaces.
const canManageWiki = computed(() => workspace.can('manage:wiki'))
const activeTab = ref('raw')
const tabs = computed(() => [
{ key: 'raw', label: t('wiki.rawMaterials') },
{ key: 'pages', label: t('wiki.pages') },
{ key: 'graph', label: t('wiki.graph.tab') },
{ key: 'config', label: t('wiki.config') },
{ key: 'transformations', label: t('wiki.transformations.tab') },
{ key: 'hotCache', label: t('wiki.hotCache.tab') },
])
const tabs = computed(() => {
const list = [
{ key: 'raw', label: t('wiki.rawMaterials') },
{ key: 'pages', label: t('wiki.pages') },
{ key: 'graph', label: t('wiki.graph.tab') },
]
if (canManageWiki.value) {
list.push({ key: 'config', label: t('wiki.config') })
list.push({ key: 'transformations', label: t('wiki.transformations.tab') })
}
list.push({ key: 'hotCache', label: t('wiki.hotCache.tab') })
return list
})
// When the user picks a different KB from the library, snap back to the
// raw-materials tab so they don't land on stale state from the previous KB.