mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
fix(wiki): gate write controls behind the manage:wiki capability
This commit is contained in:
parent
c9f73bbbd5
commit
71aba2649e
@ -45,7 +45,7 @@
|
|||||||
<!-- Error state with actions -->
|
<!-- Error state with actions -->
|
||||||
<div v-if="status === 'failed'" class="stage-error">
|
<div v-if="status === 'failed'" class="stage-error">
|
||||||
<span class="error-badge">❌ {{ t('wiki.jobStage.failed') }} — {{ errorCode }}</span>
|
<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" @click="$emit('reprocess')">{{ t('wiki.reprocess') }}</button>
|
||||||
<button class="btn-mini btn-mini-alt" @click="$emit('repair')">{{ t('wiki.page.repair') }}</button>
|
<button class="btn-mini btn-mini-alt" @click="$emit('repair')">{{ t('wiki.page.repair') }}</button>
|
||||||
</div>
|
</div>
|
||||||
@ -56,8 +56,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
|
|
||||||
const { t } = useI18n()
|
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<{
|
const props = defineProps<{
|
||||||
stage: string
|
stage: string
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
<el-icon :size="12"><Link /></el-icon>
|
<el-icon :size="12"><Link /></el-icon>
|
||||||
{{ t('wiki.page.enriched') }}
|
{{ t('wiki.page.enriched') }}
|
||||||
</span>
|
</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>
|
<el-icon :size="12"><Link /></el-icon>
|
||||||
{{ t('wiki.page.enrich') }}
|
{{ t('wiki.page.enrich') }}
|
||||||
</button>
|
</button>
|
||||||
@ -41,8 +41,13 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Link } from '@element-plus/icons-vue'
|
import { Link } from '@element-plus/icons-vue'
|
||||||
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
|
|
||||||
const { t } = useI18n()
|
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<{
|
const props = defineProps<{
|
||||||
page: {
|
page: {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="raw-panel">
|
<div class="raw-panel">
|
||||||
<!-- Upload + Add text row -->
|
<!-- Upload + Add text row -->
|
||||||
<div class="upload-row">
|
<div v-if="canManageWiki" class="upload-row">
|
||||||
<div
|
<div
|
||||||
class="upload-zone"
|
class="upload-zone"
|
||||||
:class="{ 'is-dragging': isDragging, 'is-uploading': uploadingFiles.length > 0 }"
|
:class="{ 'is-dragging': isDragging, 'is-uploading': uploadingFiles.length > 0 }"
|
||||||
@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Directory scan -->
|
<!-- Directory scan -->
|
||||||
<div class="dir-scan-row">
|
<div v-if="canManageWiki" class="dir-scan-row">
|
||||||
<div class="dir-input-wrap">
|
<div class="dir-input-wrap">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<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"/>
|
<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 }}
|
{{ raw.errorMessage }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="raw-item-actions">
|
<div v-if="canManageWiki" class="raw-item-actions">
|
||||||
<button
|
<button
|
||||||
v-if="raw.processingStatus === 'processing' && !cancellingIds.has(raw.id)"
|
v-if="raw.processingStatus === 'processing' && !cancellingIds.has(raw.id)"
|
||||||
class="btn-icon btn-icon-danger" :title="t('wiki.cancel')"
|
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 { useFileDrop } from '@/composables/useFileDrop'
|
||||||
import { Download } from '@element-plus/icons-vue'
|
import { Download } from '@element-plus/icons-vue'
|
||||||
import { useWikiStore } from '@/stores/useWikiStore'
|
import { useWikiStore } from '@/stores/useWikiStore'
|
||||||
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
import { wikiApi } from '@/api/index'
|
import { wikiApi } from '@/api/index'
|
||||||
import JobStageBar from './JobStageBar.vue'
|
import JobStageBar from './JobStageBar.vue'
|
||||||
import type { WikiProcessingJob } from '@/composables/useWikiJobPoller'
|
import type { WikiProcessingJob } from '@/composables/useWikiJobPoller'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useWikiStore()
|
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)
|
const fileInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
// While raw materials are active, subscribe to the backend SSE progress stream.
|
// While raw materials are active, subscribe to the backend SSE progress stream.
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
@keydown.space.prevent="$emit('open', kb.id)"
|
@keydown.space.prevent="$emit('open', kb.id)"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
v-if="canManageWiki"
|
||||||
type="button"
|
type="button"
|
||||||
class="kb-card-delete"
|
class="kb-card-delete"
|
||||||
:title="t('wiki.library.deleteKB')"
|
:title="t('wiki.library.deleteKB')"
|
||||||
@ -61,6 +62,7 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { WikiKB } from '@/stores/useWikiStore'
|
import type { WikiKB } from '@/stores/useWikiStore'
|
||||||
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
import { kbAccent, kbAccentFg, kbInitial, relativeTime } from '../utils/kbVisual'
|
import { kbAccent, kbAccentFg, kbInitial, relativeTime } from '../utils/kbVisual'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -74,6 +76,10 @@ defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
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 failedJobCount = computed(() => props.failedJobCount ?? 0)
|
||||||
const initial = computed(() => kbInitial(props.kb))
|
const initial = computed(() => kbInitial(props.kb))
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<h1 class="mc-page-title">{{ t('nav.wiki') }}</h1>
|
<h1 class="mc-page-title">{{ t('nav.wiki') }}</h1>
|
||||||
<p class="mc-page-desc">{{ t('wiki.desc') }}</p>
|
<p class="mc-page-desc">{{ t('wiki.desc') }}</p>
|
||||||
</div>
|
</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">
|
<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"/>
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="library-empty-title">{{ t('wiki.library.empty') }}</h3>
|
<h3 class="library-empty-title">{{ t('wiki.library.empty') }}</h3>
|
||||||
<p class="library-empty-hint">{{ t('wiki.library.emptyHint') }}</p>
|
<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>
|
<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') }}
|
{{ t('wiki.createKB') }}
|
||||||
</button>
|
</button>
|
||||||
@ -87,6 +87,7 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { WikiKB } from '@/stores/useWikiStore'
|
import type { WikiKB } from '@/stores/useWikiStore'
|
||||||
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
import McPagination from '@/components/common/McPagination.vue'
|
import McPagination from '@/components/common/McPagination.vue'
|
||||||
import WikiKBCard from './WikiKBCard.vue'
|
import WikiKBCard from './WikiKBCard.vue'
|
||||||
|
|
||||||
@ -108,6 +109,10 @@ defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
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('')
|
const searchInput = ref('')
|
||||||
type SortMode = 'recent' | 'name' | 'pages'
|
type SortMode = 'recent' | 'name' | 'pages'
|
||||||
|
|||||||
@ -13,8 +13,8 @@
|
|||||||
<p class="summary-text">{{ store.currentPage.summary }}</p>
|
<p class="summary-text">{{ store.currentPage.summary }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions bar -->
|
<!-- Actions bar — write actions, hidden for read-only viewers -->
|
||||||
<div class="page-actions-bar">
|
<div v-if="canManageWiki" class="page-actions-bar">
|
||||||
<button class="btn-secondary btn-sm" @click="editing = !editing">
|
<button class="btn-secondary btn-sm" @click="editing = !editing">
|
||||||
{{ editing ? t('common.cancel') : t('common.edit') }}
|
{{ editing ? t('common.cancel') : t('common.edit') }}
|
||||||
</button>
|
</button>
|
||||||
@ -99,6 +99,7 @@
|
|||||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useWikiStore, isProtectedPage, type WikiPage } from '@/stores/useWikiStore'
|
import { useWikiStore, isProtectedPage, type WikiPage } from '@/stores/useWikiStore'
|
||||||
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
import { wikiApi } from '@/api/index'
|
import { wikiApi } from '@/api/index'
|
||||||
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
|
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
|
||||||
import { Link, SetUp } from '@element-plus/icons-vue'
|
import { Link, SetUp } from '@element-plus/icons-vue'
|
||||||
@ -109,8 +110,13 @@ import ImageLightbox from './ImageLightbox.vue'
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useWikiStore()
|
const store = useWikiStore()
|
||||||
|
const workspace = useWorkspaceStore()
|
||||||
const { renderMarkdown } = useMarkdownRenderer()
|
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 editing = ref(false)
|
||||||
const editContent = ref('')
|
const editContent = ref('')
|
||||||
const backlinks = ref<WikiPage[]>([])
|
const backlinks = ref<WikiPage[]>([])
|
||||||
|
|||||||
@ -56,6 +56,7 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useWikiStore, type WikiKB } from '@/stores/useWikiStore'
|
import { useWikiStore, type WikiKB } from '@/stores/useWikiStore'
|
||||||
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
import RawMaterialPanel from './RawMaterialPanel.vue'
|
import RawMaterialPanel from './RawMaterialPanel.vue'
|
||||||
import WikiPageViewer from './WikiPageViewer.vue'
|
import WikiPageViewer from './WikiPageViewer.vue'
|
||||||
import WikiConfig from './WikiConfig.vue'
|
import WikiConfig from './WikiConfig.vue'
|
||||||
@ -69,17 +70,27 @@ defineProps<{ kb: WikiKB }>()
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useWikiStore()
|
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 activeTab = ref('raw')
|
||||||
|
|
||||||
const tabs = computed(() => [
|
const tabs = computed(() => {
|
||||||
{ key: 'raw', label: t('wiki.rawMaterials') },
|
const list = [
|
||||||
{ key: 'pages', label: t('wiki.pages') },
|
{ key: 'raw', label: t('wiki.rawMaterials') },
|
||||||
{ key: 'graph', label: t('wiki.graph.tab') },
|
{ key: 'pages', label: t('wiki.pages') },
|
||||||
{ key: 'config', label: t('wiki.config') },
|
{ key: 'graph', label: t('wiki.graph.tab') },
|
||||||
{ key: 'transformations', label: t('wiki.transformations.tab') },
|
]
|
||||||
{ key: 'hotCache', label: t('wiki.hotCache.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
|
// 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.
|
// raw-materials tab so they don't land on stale state from the previous KB.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user