mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-18 01:28:58 +00:00
refactor: add PageWrapper component
This commit is contained in:
@@ -55,17 +55,17 @@
|
||||
</NcAppNavigationItem>
|
||||
</template>
|
||||
</NcAppNavigationItem>
|
||||
</NcAppNavigationItem>
|
||||
|
||||
<!-- Preferences menu item -->
|
||||
<NcAppNavigationItem
|
||||
:name="strings.navPreferences"
|
||||
:to="{ path: '/preferences' }"
|
||||
:active="isPreferencesActive"
|
||||
>
|
||||
<template #icon>
|
||||
<AccountCogIcon :size="20" />
|
||||
</template>
|
||||
<!-- Preferences menu item -->
|
||||
<NcAppNavigationItem
|
||||
:name="strings.navPreferences"
|
||||
:to="{ path: '/preferences' }"
|
||||
:active="isPreferencesActive"
|
||||
>
|
||||
<template #icon>
|
||||
<AccountCogIcon :size="20" />
|
||||
</template>
|
||||
</NcAppNavigationItem>
|
||||
</NcAppNavigationItem>
|
||||
|
||||
<!-- Admin menu item - only visible to admins -->
|
||||
|
||||
35
src/components/PageWrapper.vue
Normal file
35
src/components/PageWrapper.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="page-wrapper" :class="{ 'full-width': fullWidth }">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PageWrapper',
|
||||
props: {
|
||||
/**
|
||||
* Whether to use full width or fixed width (900px max with auto margins)
|
||||
*/
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-wrapper {
|
||||
padding: 16px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
|
||||
&.full-width {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,431 +0,0 @@
|
||||
<template>
|
||||
<div class="user-inner">
|
||||
<!-- Toolbar -->
|
||||
<AppToolbar>
|
||||
<template #left>
|
||||
<div style="max-width: 320px">
|
||||
<NcTextField
|
||||
v-model="search"
|
||||
:label="strings.searchLabel"
|
||||
:placeholder="strings.searchPlaceholder"
|
||||
trailing-button-icon="close"
|
||||
:show-trailing-button="search !== ''"
|
||||
@trailing-button-click="clearSearch"
|
||||
/>
|
||||
</div>
|
||||
<NcButton @click="refresh" :disabled="loading">{{ strings.refresh }}</NcButton>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<NcButton type="secondary" @click="toggleForm">
|
||||
{{ formOpen ? strings.hideForm : strings.showForm }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</AppToolbar>
|
||||
|
||||
<!-- Quick info / doc -->
|
||||
<NcNoteCard class="mt-12" type="info">
|
||||
<p v-html="strings.quickHelp"></p>
|
||||
</NcNoteCard>
|
||||
|
||||
<!-- Add item form -->
|
||||
<section v-if="formOpen" class="card mt-16">
|
||||
<h3 class="card-title">{{ strings.formHeader }}</h3>
|
||||
<div class="row gap-16 align-start">
|
||||
<div style="max-width: 260px">
|
||||
<NcTextField
|
||||
v-model="name"
|
||||
:label="strings.nameInputLabel"
|
||||
:placeholder="strings.nameInputPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 220px">
|
||||
<NcSelect
|
||||
v-model="themeLabel"
|
||||
:options="themeOptionsLabels"
|
||||
:input-label="strings.themeLabel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row gap-8 align-center">
|
||||
<NcButton @click="addFromForm" :disabled="name.trim() === '' || loading">
|
||||
{{ strings.add }}
|
||||
</NcButton>
|
||||
<NcButton type="tertiary" @click="clearForm" :disabled="loading">
|
||||
{{ strings.clear }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-12">
|
||||
{{ strings.livePreview }} <b>{{ previewGreeting }}</b>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="filteredHellos.length === 0"
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="seedOne">{{ strings.addExample }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- List -->
|
||||
<section v-else class="mt-16">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%">{{ strings.colMessage }}</th>
|
||||
<th style="width: 30%">{{ strings.colAt }}</th>
|
||||
<th style="width: 20%">{{ strings.colActions }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(hello, idx) in filteredHellos" :key="hello.id">
|
||||
<td class="ellipsis">
|
||||
<span class="mono">{{ hello.message }}</span>
|
||||
</td>
|
||||
<td class="nowrap">
|
||||
<NcDateTime v-if="hello.at" :timestamp="new Date(hello.at).valueOf()" />
|
||||
<span v-else class="muted">{{ strings.never }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="row gap-8">
|
||||
<NcButton type="tertiary" @click="duplicate(idx)">{{ strings.duplicate }}</NcButton>
|
||||
<NcButton type="error" @click="remove(idx)">{{ strings.remove }}</NcButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Footer actions -->
|
||||
<div class="row gap-12 mt-12">
|
||||
<NcButton type="secondary" @click="refresh" :disabled="loading">{{
|
||||
strings.refresh
|
||||
}}</NcButton>
|
||||
<NcButton type="secondary" @click="clearAll" :disabled="loading || hellos.length === 0">
|
||||
{{ strings.clearAll }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Inner view rendered inside AppUserWrapper via <router-view>.
|
||||
* Uses the Hello controller (GET/POST /hello).
|
||||
*/
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import NcSelect from '@nextcloud/vue/components/NcSelect'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
|
||||
import AppToolbar from '@/components/AppToolbar.vue'
|
||||
|
||||
import { ocs } from '@/axios'
|
||||
import { t, n } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'AppUserHome',
|
||||
components: {
|
||||
NcButton,
|
||||
NcNoteCard,
|
||||
NcTextField,
|
||||
NcSelect,
|
||||
NcEmptyContent,
|
||||
NcLoadingIcon,
|
||||
NcDateTime,
|
||||
AppToolbar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
formOpen: true,
|
||||
|
||||
// Toolbar
|
||||
search: '',
|
||||
|
||||
// Form data
|
||||
name: '',
|
||||
themeLabel: null,
|
||||
themeOptions: [
|
||||
{ label: t('forum', 'Light'), value: 'light' },
|
||||
{ label: t('forum', 'Dark'), value: 'dark' },
|
||||
{
|
||||
label: n('forum', 'System (1 option)', 'System (%n options)', 2),
|
||||
value: 'system',
|
||||
},
|
||||
],
|
||||
|
||||
// List of "hellos"
|
||||
hellos: [],
|
||||
|
||||
strings: {
|
||||
// Toolbar
|
||||
searchLabel: t('forum', 'Search'),
|
||||
searchPlaceholder: t('forum', 'Filter messages…'),
|
||||
refresh: t('forum', 'Refresh'),
|
||||
showForm: t('forum', 'Show form'),
|
||||
hideForm: t('forum', 'Hide form'),
|
||||
|
||||
// Info
|
||||
quickHelp: t(
|
||||
'forum',
|
||||
'Use the form to post a hello. The list shows recent hellos fetched from the server. All user-visible text is centralized in {cStart}strings{cEnd}.',
|
||||
{ cStart: '<code>', cEnd: '</code>' },
|
||||
undefined,
|
||||
{ escape: false },
|
||||
),
|
||||
|
||||
// Form
|
||||
formHeader: t('forum', 'Say hello'),
|
||||
nameInputLabel: t('forum', 'Name'),
|
||||
nameInputPlaceholder: t('forum', 'e.g. Ada'),
|
||||
themeLabel: t('forum', 'Theme'),
|
||||
add: t('forum', 'Add'),
|
||||
clear: t('forum', 'Clear'),
|
||||
livePreview: t('forum', 'Preview:'),
|
||||
|
||||
// List
|
||||
loading: t('forum', 'Loading…'),
|
||||
emptyTitle: t('forum', 'No hellos yet'),
|
||||
emptyDesc: t('forum', 'Try adding one using the form above.'),
|
||||
addExample: t('forum', 'Add example'),
|
||||
colMessage: t('forum', 'Message'),
|
||||
colAt: t('forum', 'Time'),
|
||||
colActions: t('forum', 'Actions'),
|
||||
duplicate: t('forum', 'Duplicate'),
|
||||
remove: t('forum', 'Remove'),
|
||||
clearAll: t('forum', 'Clear all'),
|
||||
never: t('forum', 'Never'),
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.refresh()
|
||||
},
|
||||
computed: {
|
||||
themeOptionsLabels() {
|
||||
return this.themeOptions.map((x) => x.label)
|
||||
},
|
||||
activeTheme() {
|
||||
return this.themeOptions.find((x) => x.label === this.themeLabel) ?? this.themeOptions[0]
|
||||
},
|
||||
previewGreeting() {
|
||||
const n = this.name.trim()
|
||||
return n ? `Hello, ${n}!` : 'Hello!'
|
||||
},
|
||||
filteredHellos() {
|
||||
const q = this.search.trim().toLowerCase()
|
||||
if (!q) return this.hellos
|
||||
return this.hellos.filter((h) => h.message.toLowerCase().includes(q))
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleForm() {
|
||||
this.formOpen = !this.formOpen
|
||||
},
|
||||
clearForm() {
|
||||
this.name = ''
|
||||
this.themeLabel = null
|
||||
},
|
||||
clearSearch() {
|
||||
this.search = ''
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
this.loading = true
|
||||
// GET /hello -> { ocs: { data: { message, at } } }
|
||||
const resp = await ocs.get('/hello')
|
||||
const data = resp.data
|
||||
if (data?.message) {
|
||||
this.hellos.unshift({
|
||||
id: genId(),
|
||||
message: data.message,
|
||||
at: data.at ?? null,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
async addFromForm() {
|
||||
const name = this.name.trim()
|
||||
if (!name) return
|
||||
try {
|
||||
this.loading = true
|
||||
const payload = {
|
||||
name,
|
||||
theme: this.activeTheme.value,
|
||||
items: [],
|
||||
counter: 0,
|
||||
}
|
||||
// POST /hello -> { ocs: { data: { message, at } } }
|
||||
const resp = await ocs.post('/hello', { data: payload })
|
||||
const data = resp.data
|
||||
const message = data?.message ?? `Hello, ${name}!`
|
||||
const at = data?.at ?? new Date().toISOString()
|
||||
this.hellos.unshift({ id: genId(), message, at })
|
||||
this.clearForm()
|
||||
this.formOpen = false
|
||||
} catch (e) {
|
||||
console.error('Failed to add hello', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
duplicate(index) {
|
||||
const src = this.hellos[index]
|
||||
if (!src) return
|
||||
this.hellos.splice(index + 1, 0, { ...src, id: genId() })
|
||||
},
|
||||
|
||||
remove(index) {
|
||||
this.hellos.splice(index, 1)
|
||||
},
|
||||
|
||||
clearAll() {
|
||||
this.hellos = []
|
||||
},
|
||||
|
||||
seedOne() {
|
||||
this.hellos.push({
|
||||
id: genId(),
|
||||
message: '👋 Hello example',
|
||||
at: new Date().toISOString(),
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function genId() {
|
||||
return Math.random().toString(36).slice(2, 10)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-inner {
|
||||
.muted {
|
||||
color: var(--color-text-maxcontrast);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.ml-8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
|
||||
&.align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.gap-8 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&.gap-12 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&.gap-16 {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: var(--color-main-background);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
thead tr,
|
||||
tr:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody tr {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +1,66 @@
|
||||
<template>
|
||||
<div class="categories-view">
|
||||
<header class="page-header">
|
||||
<h2>{{ forumTitle }}</h2>
|
||||
<p class="muted">{{ forumSubtitle }}</p>
|
||||
</header>
|
||||
<PageWrapper :full-width="true">
|
||||
<div class="categories-view">
|
||||
<header class="page-header">
|
||||
<h2>{{ forumTitle }}</h2>
|
||||
<p class="muted">{{ forumSubtitle }}</p>
|
||||
</header>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<AppToolbar>
|
||||
<template #left>
|
||||
<h2 class="view-title">{{ strings.title }}</h2>
|
||||
</template>
|
||||
<!-- Toolbar -->
|
||||
<AppToolbar>
|
||||
<template #left>
|
||||
<h2 class="view-title">{{ strings.title }}</h2>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<NcButton
|
||||
@click="refresh"
|
||||
:disabled="loading"
|
||||
:aria-label="strings.refresh"
|
||||
:title="strings.refresh"
|
||||
>
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</template>
|
||||
</AppToolbar>
|
||||
<template #right>
|
||||
<NcButton
|
||||
@click="refresh"
|
||||
:disabled="loading"
|
||||
:aria-label="strings.refresh"
|
||||
:title="strings.refresh"
|
||||
>
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</template>
|
||||
</AppToolbar>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="categoryHeaders.length === 0"
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
/>
|
||||
|
||||
<!-- Categories list -->
|
||||
<section v-else class="mt-16">
|
||||
<div v-for="header in categoryHeaders" :key="header.id" class="header-section">
|
||||
<h3 class="header-title">{{ header.name }}</h3>
|
||||
|
||||
<!-- Categories grid -->
|
||||
<div v-if="header.categories && header.categories.length > 0" class="categories-grid">
|
||||
<CategoryCard
|
||||
v-for="category in header.categories"
|
||||
:key="category.id"
|
||||
:category="category"
|
||||
@click="navigateToCategory(category)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty state for header with no categories -->
|
||||
<p v-else class="no-categories muted">{{ strings.noCategories }}</p>
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="categoryHeaders.length === 0"
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
/>
|
||||
|
||||
<!-- Categories list -->
|
||||
<section v-else class="mt-16">
|
||||
<div v-for="header in categoryHeaders" :key="header.id" class="header-section">
|
||||
<h3 class="header-title">{{ header.name }}</h3>
|
||||
|
||||
<!-- Categories grid -->
|
||||
<div v-if="header.categories && header.categories.length > 0" class="categories-grid">
|
||||
<CategoryCard
|
||||
v-for="category in header.categories"
|
||||
:key="category.id"
|
||||
:category="category"
|
||||
@click="navigateToCategory(category)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty state for header with no categories -->
|
||||
<p v-else class="no-categories muted">{{ strings.noCategories }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -67,6 +69,7 @@ import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import AppToolbar from '@/components/AppToolbar.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import CategoryCard from '@/components/CategoryCard.vue'
|
||||
import RefreshIcon from '@icons/Refresh.vue'
|
||||
import { useCategories } from '@/composables/useCategories'
|
||||
@@ -81,6 +84,7 @@ export default defineComponent({
|
||||
NcEmptyContent,
|
||||
NcLoadingIcon,
|
||||
AppToolbar,
|
||||
PageWrapper,
|
||||
CategoryCard,
|
||||
RefreshIcon,
|
||||
},
|
||||
|
||||
@@ -1,100 +1,102 @@
|
||||
<template>
|
||||
<div class="category-view">
|
||||
<!-- Toolbar -->
|
||||
<AppToolbar>
|
||||
<template #left>
|
||||
<NcButton @click="goBack">
|
||||
<template #icon>
|
||||
<ArrowLeftIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.back }}
|
||||
</NcButton>
|
||||
</template>
|
||||
<PageWrapper :full-width="true">
|
||||
<div class="category-view">
|
||||
<!-- Toolbar -->
|
||||
<AppToolbar>
|
||||
<template #left>
|
||||
<NcButton @click="goBack">
|
||||
<template #icon>
|
||||
<ArrowLeftIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.back }}
|
||||
</NcButton>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<NcButton
|
||||
@click="refresh"
|
||||
:disabled="loading"
|
||||
:aria-label="strings.refresh"
|
||||
:title="strings.refresh"
|
||||
>
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton @click="createThread" :disabled="loading" variant="primary">
|
||||
<template #icon>
|
||||
<MessagePlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.newThread }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</AppToolbar>
|
||||
<template #right>
|
||||
<NcButton
|
||||
@click="refresh"
|
||||
:disabled="loading"
|
||||
:aria-label="strings.refresh"
|
||||
:title="strings.refresh"
|
||||
>
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton @click="createThread" :disabled="loading" variant="primary">
|
||||
<template #icon>
|
||||
<MessagePlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.newThread }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</AppToolbar>
|
||||
|
||||
<!-- Category Header -->
|
||||
<div v-if="category && !loading" class="category-header mt-16">
|
||||
<h2 class="category-name">{{ category.name }}</h2>
|
||||
<p v-if="category.description" class="category-description">{{ category.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.retry }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Empty state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="threads.length === 0"
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="createThread" variant="primary">
|
||||
<template #icon>
|
||||
<MessagePlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.newThread }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Threads list -->
|
||||
<section v-else class="mt-16">
|
||||
<div class="threads-list">
|
||||
<ThreadCard
|
||||
v-for="thread in sortedThreads"
|
||||
:key="thread.id"
|
||||
:thread="thread"
|
||||
:is-unread="isThreadUnread(thread)"
|
||||
@click="navigateToThread(thread)"
|
||||
/>
|
||||
<!-- Category Header -->
|
||||
<div v-if="category && !loading" class="category-header mt-16">
|
||||
<h2 class="category-name">{{ category.name }}</h2>
|
||||
<p v-if="category.description" class="category-description">{{ category.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Pagination info -->
|
||||
<div v-if="threads.length >= limit" class="pagination-info mt-16">
|
||||
<p class="muted">{{ strings.showingThreads(threads.length) }}</p>
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.retry }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Empty state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="threads.length === 0"
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="createThread" variant="primary">
|
||||
<template #icon>
|
||||
<MessagePlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.newThread }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Threads list -->
|
||||
<section v-else class="mt-16">
|
||||
<div class="threads-list">
|
||||
<ThreadCard
|
||||
v-for="thread in sortedThreads"
|
||||
:key="thread.id"
|
||||
:thread="thread"
|
||||
:is-unread="isThreadUnread(thread)"
|
||||
@click="navigateToThread(thread)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Pagination info -->
|
||||
<div v-if="threads.length >= limit" class="pagination-info mt-16">
|
||||
<p class="muted">{{ strings.showingThreads(threads.length) }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -103,6 +105,7 @@ import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import AppToolbar from '@/components/AppToolbar.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import ThreadCard from '@/components/ThreadCard.vue'
|
||||
import ArrowLeftIcon from '@icons/ArrowLeft.vue'
|
||||
import RefreshIcon from '@icons/Refresh.vue'
|
||||
@@ -118,6 +121,7 @@ export default defineComponent({
|
||||
NcEmptyContent,
|
||||
NcLoadingIcon,
|
||||
AppToolbar,
|
||||
PageWrapper,
|
||||
ThreadCard,
|
||||
ArrowLeftIcon,
|
||||
RefreshIcon,
|
||||
|
||||
@@ -11,41 +11,43 @@
|
||||
</template>
|
||||
</AppToolbar>
|
||||
|
||||
<div class="create-thread-view">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header mt-16">
|
||||
<h2 class="page-title">{{ strings.title }}</h2>
|
||||
<p v-if="category" class="page-subtitle">{{ strings.subtitle(category.name) }}</p>
|
||||
</div>
|
||||
<PageWrapper>
|
||||
<div class="create-thread-view">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header mt-16">
|
||||
<h2 class="page-title">{{ strings.title }}</h2>
|
||||
<p v-if="category" class="page-subtitle">{{ strings.subtitle(category.name) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading && !category">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading && !category">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="goBack">
|
||||
<template #icon>
|
||||
<ArrowLeftIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.back }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="goBack">
|
||||
<template #icon>
|
||||
<ArrowLeftIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.back }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Create Thread Form -->
|
||||
<div v-else class="mt-16">
|
||||
<ThreadCreateForm ref="createForm" @submit="handleCreateThread" @cancel="goBack" />
|
||||
<!-- Create Thread Form -->
|
||||
<div v-else class="mt-16">
|
||||
<ThreadCreateForm ref="createForm" @submit="handleCreateThread" @cancel="goBack" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -54,6 +56,7 @@ import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import AppToolbar from '@/components/AppToolbar.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import ThreadCreateForm from '@/components/ThreadCreateForm.vue'
|
||||
import ArrowLeftIcon from '@icons/ArrowLeft.vue'
|
||||
import type { Category, Thread } from '@/types'
|
||||
@@ -68,6 +71,7 @@ export default defineComponent({
|
||||
NcEmptyContent,
|
||||
NcLoadingIcon,
|
||||
AppToolbar,
|
||||
PageWrapper,
|
||||
ThreadCreateForm,
|
||||
ArrowLeftIcon,
|
||||
},
|
||||
@@ -172,9 +176,6 @@ export default defineComponent({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.create-thread-view {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
|
||||
.muted {
|
||||
color: var(--color-text-maxcontrast);
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -25,129 +25,131 @@
|
||||
</template>
|
||||
</AppToolbar>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.retry }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Profile content -->
|
||||
<div v-else class="profile-content mt-16">
|
||||
<!-- User Header -->
|
||||
<div class="user-header">
|
||||
<div class="user-avatar">
|
||||
<NcAvatar :user="userId" :size="80" :show-user-status="false" />
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<h2 class="user-name">{{ displayName }}</h2>
|
||||
<div class="user-meta">
|
||||
<span v-if="userStats && userStats.createdAt" class="meta-item">
|
||||
<span class="meta-label">{{ strings.firstPost }}</span>
|
||||
<NcDateTime :timestamp="userStats.createdAt * 1000" />
|
||||
</span>
|
||||
<span v-if="userStats && userStats.createdAt" class="meta-divider">·</span>
|
||||
<span class="meta-item">
|
||||
<span class="meta-label">{{ strings.threads }}</span>
|
||||
<span class="meta-value">{{ userStats?.threadCount || 0 }}</span>
|
||||
</span>
|
||||
<span class="meta-divider">·</span>
|
||||
<span class="meta-item">
|
||||
<span class="meta-label">{{ strings.posts }}</span>
|
||||
<span class="meta-value">{{ userStats?.postCount || 0 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<PageWrapper>
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="profile-tabs mt-24">
|
||||
<div class="tabs-header">
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'threads' }"
|
||||
@click="activeTab = 'threads'"
|
||||
>
|
||||
{{ strings.threads }} ({{ threads.length }})
|
||||
</button>
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'posts' }"
|
||||
@click="activeTab = 'posts'"
|
||||
>
|
||||
{{ strings.replies }} ({{ posts.length }})
|
||||
</button>
|
||||
</div>
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.retry }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<div class="tabs-content mt-16">
|
||||
<!-- Threads Tab -->
|
||||
<div v-if="activeTab === 'threads'" class="tab-pane">
|
||||
<div v-if="loadingThreads" class="center">
|
||||
<NcLoadingIcon :size="24" />
|
||||
</div>
|
||||
<NcEmptyContent
|
||||
v-else-if="threads.length === 0"
|
||||
:title="strings.noThreads"
|
||||
:description="strings.noThreadsDesc"
|
||||
/>
|
||||
<div v-else class="threads-list">
|
||||
<ThreadCard
|
||||
v-for="thread in threads"
|
||||
:key="thread.id"
|
||||
:thread="thread"
|
||||
@click="navigateToThread(thread)"
|
||||
/>
|
||||
<!-- Profile content -->
|
||||
<div v-else class="profile-content mt-16">
|
||||
<!-- User Header -->
|
||||
<div class="user-header">
|
||||
<div class="user-avatar">
|
||||
<NcAvatar :user="userId" :size="80" :show-user-status="false" />
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<h2 class="user-name">{{ displayName }}</h2>
|
||||
<div class="user-meta">
|
||||
<span v-if="userStats && userStats.createdAt" class="meta-item">
|
||||
<span class="meta-label">{{ strings.firstPost }}</span>
|
||||
<NcDateTime :timestamp="userStats.createdAt * 1000" />
|
||||
</span>
|
||||
<span v-if="userStats && userStats.createdAt" class="meta-divider">·</span>
|
||||
<span class="meta-item">
|
||||
<span class="meta-label">{{ strings.threads }}</span>
|
||||
<span class="meta-value">{{ userStats?.threadCount || 0 }}</span>
|
||||
</span>
|
||||
<span class="meta-divider">·</span>
|
||||
<span class="meta-item">
|
||||
<span class="meta-label">{{ strings.posts }}</span>
|
||||
<span class="meta-value">{{ userStats?.postCount || 0 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts Tab -->
|
||||
<div v-if="activeTab === 'posts'" class="tab-pane">
|
||||
<div v-if="loadingPosts" class="center">
|
||||
<NcLoadingIcon :size="24" />
|
||||
<!-- Tabs -->
|
||||
<div class="profile-tabs mt-24">
|
||||
<div class="tabs-header">
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'threads' }"
|
||||
@click="activeTab = 'threads'"
|
||||
>
|
||||
{{ strings.threads }} ({{ threads.length }})
|
||||
</button>
|
||||
<button
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'posts' }"
|
||||
@click="activeTab = 'posts'"
|
||||
>
|
||||
{{ strings.replies }} ({{ posts.length }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs-content mt-16">
|
||||
<!-- Threads Tab -->
|
||||
<div v-if="activeTab === 'threads'" class="tab-pane">
|
||||
<div v-if="loadingThreads" class="center">
|
||||
<NcLoadingIcon :size="24" />
|
||||
</div>
|
||||
<NcEmptyContent
|
||||
v-else-if="threads.length === 0"
|
||||
:title="strings.noThreads"
|
||||
:description="strings.noThreadsDesc"
|
||||
/>
|
||||
<div v-else class="threads-list">
|
||||
<ThreadCard
|
||||
v-for="thread in threads"
|
||||
:key="thread.id"
|
||||
:thread="thread"
|
||||
@click="navigateToThread(thread)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NcEmptyContent
|
||||
v-else-if="posts.length === 0"
|
||||
:title="strings.noPosts"
|
||||
:description="strings.noPostsDesc"
|
||||
/>
|
||||
<div v-else class="posts-list">
|
||||
<div
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
class="post-item"
|
||||
@click="navigateToPost(post)"
|
||||
>
|
||||
<div class="post-meta">
|
||||
<span class="post-thread" v-if="post.threadTitle">
|
||||
{{ strings.inThread }} <strong>{{ post.threadTitle }}</strong>
|
||||
</span>
|
||||
<span class="post-date">
|
||||
<NcDateTime v-if="post.createdAt" :timestamp="post.createdAt * 1000" />
|
||||
</span>
|
||||
|
||||
<!-- Posts Tab -->
|
||||
<div v-if="activeTab === 'posts'" class="tab-pane">
|
||||
<div v-if="loadingPosts" class="center">
|
||||
<NcLoadingIcon :size="24" />
|
||||
</div>
|
||||
<NcEmptyContent
|
||||
v-else-if="posts.length === 0"
|
||||
:title="strings.noPosts"
|
||||
:description="strings.noPostsDesc"
|
||||
/>
|
||||
<div v-else class="posts-list">
|
||||
<div
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
class="post-item"
|
||||
@click="navigateToPost(post)"
|
||||
>
|
||||
<div class="post-meta">
|
||||
<span class="post-thread" v-if="post.threadTitle">
|
||||
{{ strings.inThread }} <strong>{{ post.threadTitle }}</strong>
|
||||
</span>
|
||||
<span class="post-date">
|
||||
<NcDateTime v-if="post.createdAt" :timestamp="post.createdAt * 1000" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="post-content" v-html="post.content"></div>
|
||||
</div>
|
||||
<div class="post-content" v-html="post.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -159,6 +161,7 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
|
||||
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
|
||||
import AppToolbar from '@/components/AppToolbar.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import ThreadCard from '@/components/ThreadCard.vue'
|
||||
import ArrowLeftIcon from '@icons/ArrowLeft.vue'
|
||||
import RefreshIcon from '@icons/Refresh.vue'
|
||||
@@ -177,6 +180,7 @@ export default defineComponent({
|
||||
NcAvatar,
|
||||
NcDateTime,
|
||||
AppToolbar,
|
||||
PageWrapper,
|
||||
ThreadCard,
|
||||
ArrowLeftIcon,
|
||||
RefreshIcon,
|
||||
@@ -358,158 +362,154 @@ export default defineComponent({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profile-view {
|
||||
padding: 16px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.ml-8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mt-24 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
.user-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
background: var(--color-main-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--color-text-maxcontrast);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.meta-divider {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
.profile-tabs {
|
||||
.tabs-header {
|
||||
.center {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
.ml-8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mt-24 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--color-text-maxcontrast);
|
||||
transition: all 0.2s;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-light);
|
||||
background: var(--color-background-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-text-light);
|
||||
border-bottom-color: var(--color-text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
min-height: 200px;
|
||||
.user-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
background: var(--color-main-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.threads-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.posts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
padding: 16px;
|
||||
background: var(--color-main-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-hover);
|
||||
border-color: var(--color-primary-element);
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
.user-name {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.post-thread {
|
||||
strong {
|
||||
.user-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--color-text-maxcontrast);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.post-content {
|
||||
color: var(--color-text-light);
|
||||
line-height: 1.6;
|
||||
.meta-divider {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
// Truncate long content
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
.profile-tabs {
|
||||
.tabs-header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-maxcontrast);
|
||||
transition: all 0.2s;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-light);
|
||||
background: var(--color-background-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-text-light);
|
||||
border-bottom-color: var(--color-text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.threads-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.posts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
padding: 16px;
|
||||
background: var(--color-main-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-hover);
|
||||
border-color: var(--color-primary-element);
|
||||
}
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
.post-thread {
|
||||
strong {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
}
|
||||
|
||||
.post-content {
|
||||
color: var(--color-text-light);
|
||||
line-height: 1.6;
|
||||
|
||||
// Truncate long content
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,132 +1,134 @@
|
||||
<template>
|
||||
<div class="search-view">
|
||||
<!-- Search Header -->
|
||||
<div class="search-header">
|
||||
<h2 class="search-title">{{ strings.searchTitle }}</h2>
|
||||
<PageWrapper>
|
||||
<div class="search-view">
|
||||
<!-- Search Header -->
|
||||
<div class="search-header">
|
||||
<h2 class="search-title">{{ strings.searchTitle }}</h2>
|
||||
|
||||
<!-- Search Input -->
|
||||
<div class="search-input-wrapper">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="strings.searchPlaceholder"
|
||||
class="search-input"
|
||||
@keydown.enter="performSearch"
|
||||
/>
|
||||
<NcButton variant="primary" @click="performSearch" :disabled="!canSearch || loading">
|
||||
<template #icon>
|
||||
<MagnifyIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.search }}
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<!-- Search Options -->
|
||||
<div class="search-options">
|
||||
<NcCheckboxRadioSwitch v-model="searchThreads" @update:checked="onOptionsChange">
|
||||
{{ strings.searchThreads }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcCheckboxRadioSwitch v-model="searchPosts" @update:checked="onOptionsChange">
|
||||
{{ strings.searchPosts }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcButton variant="tertiary" @click="showSyntaxHelp = !showSyntaxHelp">
|
||||
<template #icon>
|
||||
<HelpCircleIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.syntaxHelp }}
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<!-- Syntax Help -->
|
||||
<div v-if="showSyntaxHelp" class="syntax-help">
|
||||
<h3>{{ strings.searchSyntax }}</h3>
|
||||
<ul>
|
||||
<li><code>"exact phrase"</code> - {{ strings.helpExactPhrase }}</li>
|
||||
<li><code>term1 AND term2</code> - {{ strings.helpAnd }}</li>
|
||||
<li><code>term1 OR term2</code> - {{ strings.helpOr }}</li>
|
||||
<li><code>(term1 OR term2) AND term3</code> - {{ strings.helpGrouping }}</li>
|
||||
<li><code>-excluded</code> - {{ strings.helpExclude }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.searching }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="performSearch">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Empty State (no query) -->
|
||||
<NcEmptyContent
|
||||
v-else-if="!hasSearched"
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #icon>
|
||||
<MagnifyIcon :size="64" />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- No Results -->
|
||||
<NcEmptyContent
|
||||
v-else-if="hasSearched && threadResults.length === 0 && postResults.length === 0"
|
||||
:title="strings.noResultsTitle"
|
||||
:description="strings.noResultsDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #icon>
|
||||
<MagnifyIcon :size="64" />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Results -->
|
||||
<div v-else class="search-results mt-16">
|
||||
<!-- Thread Results Section -->
|
||||
<section v-if="searchThreads && threadResults.length > 0" class="results-section">
|
||||
<h3 class="results-header">
|
||||
{{ strings.threadResults(threadCount) }}
|
||||
</h3>
|
||||
<div class="results-list">
|
||||
<SearchThreadResult
|
||||
v-for="thread in threadResults"
|
||||
:key="thread.id"
|
||||
:thread="thread"
|
||||
:query="currentQuery"
|
||||
@click="navigateToThread(thread)"
|
||||
<!-- Search Input -->
|
||||
<div class="search-input-wrapper">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="strings.searchPlaceholder"
|
||||
class="search-input"
|
||||
@keydown.enter="performSearch"
|
||||
/>
|
||||
<NcButton variant="primary" @click="performSearch" :disabled="!canSearch || loading">
|
||||
<template #icon>
|
||||
<MagnifyIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.search }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Post Results Section -->
|
||||
<section v-if="searchPosts && postResults.length > 0" class="results-section mt-16">
|
||||
<h3 class="results-header">
|
||||
{{ strings.postResults(postCount) }}
|
||||
</h3>
|
||||
<div class="results-list">
|
||||
<SearchPostResult
|
||||
v-for="post in postResults"
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
:query="currentQuery"
|
||||
/>
|
||||
<!-- Search Options -->
|
||||
<div class="search-options">
|
||||
<NcCheckboxRadioSwitch v-model="searchThreads" @update:checked="onOptionsChange">
|
||||
{{ strings.searchThreads }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcCheckboxRadioSwitch v-model="searchPosts" @update:checked="onOptionsChange">
|
||||
{{ strings.searchPosts }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcButton variant="tertiary" @click="showSyntaxHelp = !showSyntaxHelp">
|
||||
<template #icon>
|
||||
<HelpCircleIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.syntaxHelp }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Syntax Help -->
|
||||
<div v-if="showSyntaxHelp" class="syntax-help">
|
||||
<h3>{{ strings.searchSyntax }}</h3>
|
||||
<ul>
|
||||
<li><code>"exact phrase"</code> - {{ strings.helpExactPhrase }}</li>
|
||||
<li><code>term1 AND term2</code> - {{ strings.helpAnd }}</li>
|
||||
<li><code>term1 OR term2</code> - {{ strings.helpOr }}</li>
|
||||
<li><code>(term1 OR term2) AND term3</code> - {{ strings.helpGrouping }}</li>
|
||||
<li><code>-excluded</code> - {{ strings.helpExclude }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.searching }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="performSearch">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Empty State (no query) -->
|
||||
<NcEmptyContent
|
||||
v-else-if="!hasSearched"
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #icon>
|
||||
<MagnifyIcon :size="64" />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- No Results -->
|
||||
<NcEmptyContent
|
||||
v-else-if="hasSearched && threadResults.length === 0 && postResults.length === 0"
|
||||
:title="strings.noResultsTitle"
|
||||
:description="strings.noResultsDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #icon>
|
||||
<MagnifyIcon :size="64" />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Results -->
|
||||
<div v-else class="search-results mt-16">
|
||||
<!-- Thread Results Section -->
|
||||
<section v-if="searchThreads && threadResults.length > 0" class="results-section">
|
||||
<h3 class="results-header">
|
||||
{{ strings.threadResults(threadCount) }}
|
||||
</h3>
|
||||
<div class="results-list">
|
||||
<SearchThreadResult
|
||||
v-for="thread in threadResults"
|
||||
:key="thread.id"
|
||||
:thread="thread"
|
||||
:query="currentQuery"
|
||||
@click="navigateToThread(thread)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Post Results Section -->
|
||||
<section v-if="searchPosts && postResults.length > 0" class="results-section mt-16">
|
||||
<h3 class="results-header">
|
||||
{{ strings.postResults(postCount) }}
|
||||
</h3>
|
||||
<div class="results-list">
|
||||
<SearchPostResult
|
||||
v-for="post in postResults"
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
:query="currentQuery"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -135,6 +137,7 @@ import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import MagnifyIcon from '@icons/Magnify.vue'
|
||||
import HelpCircleIcon from '@icons/HelpCircle.vue'
|
||||
import SearchThreadResult from '@/components/SearchThreadResult.vue'
|
||||
@@ -151,6 +154,7 @@ export default defineComponent({
|
||||
NcEmptyContent,
|
||||
NcLoadingIcon,
|
||||
NcCheckboxRadioSwitch,
|
||||
PageWrapper,
|
||||
MagnifyIcon,
|
||||
HelpCircleIcon,
|
||||
SearchThreadResult,
|
||||
@@ -276,10 +280,6 @@ export default defineComponent({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-view {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
|
||||
.search-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -1,195 +1,197 @@
|
||||
<template>
|
||||
<div class="thread-view">
|
||||
<!-- Toolbar -->
|
||||
<AppToolbar>
|
||||
<template #left>
|
||||
<NcButton @click="goBack">
|
||||
<template #icon>
|
||||
<ArrowLeftIcon :size="20" />
|
||||
</template>
|
||||
{{ thread?.categoryName ? strings.backToCategory(thread.categoryName) : strings.back }}
|
||||
</NcButton>
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<!-- Subscription toggle switch -->
|
||||
<NcCheckboxRadioSwitch
|
||||
v-if="!loading && thread"
|
||||
v-model="thread.isSubscribed"
|
||||
@update:model-value="handleToggleSubscription"
|
||||
type="switch"
|
||||
>
|
||||
<span class="icon-label">
|
||||
<BellIcon :size="20" />
|
||||
{{ thread.isSubscribed ? strings.subscribed : strings.subscribe }}
|
||||
</span>
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcButton
|
||||
@click="refresh"
|
||||
:disabled="loading"
|
||||
:aria-label="strings.refresh"
|
||||
:title="strings.refresh"
|
||||
>
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
|
||||
<!-- Moderation buttons (only visible to moderators) -->
|
||||
<template v-if="canModerate && !loading">
|
||||
<NcButton
|
||||
@click="handleToggleLock"
|
||||
:aria-label="thread?.isLocked ? strings.unlockThread : strings.lockThread"
|
||||
:title="thread?.isLocked ? strings.unlockThread : strings.lockThread"
|
||||
>
|
||||
<PageWrapper :full-width="true">
|
||||
<div class="thread-view">
|
||||
<!-- Toolbar -->
|
||||
<AppToolbar>
|
||||
<template #left>
|
||||
<NcButton @click="goBack">
|
||||
<template #icon>
|
||||
<LockOpenIcon v-if="thread?.isLocked" :size="20" />
|
||||
<LockIcon v-else :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
|
||||
<NcButton
|
||||
@click="handleTogglePin"
|
||||
:aria-label="thread?.isPinned ? strings.unpinThread : strings.pinThread"
|
||||
:title="thread?.isPinned ? strings.unpinThread : strings.pinThread"
|
||||
>
|
||||
<template #icon>
|
||||
<PinOffIcon v-if="thread?.isPinned" :size="20" />
|
||||
<PinIcon v-else :size="20" />
|
||||
<ArrowLeftIcon :size="20" />
|
||||
</template>
|
||||
{{ thread?.categoryName ? strings.backToCategory(thread.categoryName) : strings.back }}
|
||||
</NcButton>
|
||||
</template>
|
||||
|
||||
<NcButton
|
||||
@click="replyToThread"
|
||||
:disabled="loading || (thread?.isLocked && !canModerate)"
|
||||
variant="primary"
|
||||
>
|
||||
<template #icon>
|
||||
<ReplyIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.reply }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</AppToolbar>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.retry }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Thread Header -->
|
||||
<div v-else-if="thread" class="thread-header mt-16">
|
||||
<div class="thread-title-section">
|
||||
<h2 class="thread-title">
|
||||
<span v-if="thread.isPinned" class="badge badge-pinned" :title="strings.pinned">
|
||||
<PinIcon :size="20" />
|
||||
</span>
|
||||
<span v-if="thread.isLocked" class="badge badge-locked" :title="strings.locked">
|
||||
<LockIcon :size="20" />
|
||||
</span>
|
||||
{{ thread.title }}
|
||||
</h2>
|
||||
<div class="thread-meta">
|
||||
<span class="meta-item">
|
||||
<span class="meta-label">{{ strings.by }}</span>
|
||||
<span class="meta-value" :class="{ 'deleted-user': thread.authorIsDeleted }">
|
||||
{{ thread.authorDisplayName || thread.authorId }}
|
||||
<template #right>
|
||||
<!-- Subscription toggle switch -->
|
||||
<NcCheckboxRadioSwitch
|
||||
v-if="!loading && thread"
|
||||
v-model="thread.isSubscribed"
|
||||
@update:model-value="handleToggleSubscription"
|
||||
type="switch"
|
||||
>
|
||||
<span class="icon-label">
|
||||
<BellIcon :size="20" />
|
||||
{{ thread.isSubscribed ? strings.subscribed : strings.subscribe }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="meta-divider">·</span>
|
||||
<span class="meta-item">
|
||||
<NcDateTime v-if="thread.createdAt" :timestamp="thread.createdAt * 1000" />
|
||||
</span>
|
||||
<span class="meta-divider">·</span>
|
||||
<span class="meta-item">
|
||||
<span class="stat-icon">
|
||||
<EyeIcon :size="16" />
|
||||
</span>
|
||||
<span class="stat-label">{{ strings.views(thread.viewCount) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<!-- Posts list -->
|
||||
<section v-if="!loading && !error && posts.length > 0" class="mt-16">
|
||||
<div class="posts-list">
|
||||
<PostCard
|
||||
v-for="(post, index) in posts"
|
||||
:key="post.id"
|
||||
:ref="(el) => setPostCardRef(el, post.id)"
|
||||
:post="post"
|
||||
:is-first-post="index === 0"
|
||||
:is-unread="isPostUnread(post)"
|
||||
@reply="handleReply"
|
||||
@update="handleUpdate"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
<NcButton
|
||||
@click="refresh"
|
||||
:disabled="loading"
|
||||
:aria-label="strings.refresh"
|
||||
:title="strings.refresh"
|
||||
>
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
|
||||
<!-- Pagination info -->
|
||||
<div v-if="posts.length >= limit" class="pagination-info mt-16">
|
||||
<p class="muted">{{ strings.showingPosts(posts.length) }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Moderation buttons (only visible to moderators) -->
|
||||
<template v-if="canModerate && !loading">
|
||||
<NcButton
|
||||
@click="handleToggleLock"
|
||||
:aria-label="thread?.isLocked ? strings.unlockThread : strings.lockThread"
|
||||
:title="thread?.isLocked ? strings.unlockThread : strings.lockThread"
|
||||
>
|
||||
<template #icon>
|
||||
<LockOpenIcon v-if="thread?.isLocked" :size="20" />
|
||||
<LockIcon v-else :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
|
||||
<!-- Empty posts state (thread exists but no posts) -->
|
||||
<NcEmptyContent
|
||||
v-else-if="!loading && !error && thread && posts.length === 0"
|
||||
:title="strings.emptyPostsTitle"
|
||||
:description="strings.emptyPostsDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="replyToThread" variant="primary">
|
||||
<template #icon>
|
||||
<ReplyIcon :size="20" />
|
||||
<NcButton
|
||||
@click="handleTogglePin"
|
||||
:aria-label="thread?.isPinned ? strings.unpinThread : strings.pinThread"
|
||||
:title="thread?.isPinned ? strings.unpinThread : strings.pinThread"
|
||||
>
|
||||
<template #icon>
|
||||
<PinOffIcon v-if="thread?.isPinned" :size="20" />
|
||||
<PinIcon v-else :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</template>
|
||||
{{ strings.reply }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Locked message (only shown to non-moderators) -->
|
||||
<div
|
||||
v-if="!loading && !error && thread && thread.isLocked && !canModerate"
|
||||
class="locked-message mt-16"
|
||||
>
|
||||
<NcEmptyContent :title="strings.locked" :description="strings.lockedMessage">
|
||||
<template #icon>
|
||||
<LockIcon :size="64" />
|
||||
<NcButton
|
||||
@click="replyToThread"
|
||||
:disabled="loading || (thread?.isLocked && !canModerate)"
|
||||
variant="primary"
|
||||
>
|
||||
<template #icon>
|
||||
<ReplyIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.reply }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</AppToolbar>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div class="center mt-16" v-if="loading">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">
|
||||
<template #icon>
|
||||
<RefreshIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.retry }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
</div>
|
||||
|
||||
<!-- Reply form (moderators can reply even when locked) -->
|
||||
<PostReplyForm
|
||||
v-if="!loading && !error && thread && (!thread.isLocked || canModerate)"
|
||||
ref="replyForm"
|
||||
@submit="handleSubmitReply"
|
||||
@cancel="handleCancelReply"
|
||||
/>
|
||||
</div>
|
||||
<!-- Thread Header -->
|
||||
<div v-else-if="thread" class="thread-header mt-16">
|
||||
<div class="thread-title-section">
|
||||
<h2 class="thread-title">
|
||||
<span v-if="thread.isPinned" class="badge badge-pinned" :title="strings.pinned">
|
||||
<PinIcon :size="20" />
|
||||
</span>
|
||||
<span v-if="thread.isLocked" class="badge badge-locked" :title="strings.locked">
|
||||
<LockIcon :size="20" />
|
||||
</span>
|
||||
{{ thread.title }}
|
||||
</h2>
|
||||
<div class="thread-meta">
|
||||
<span class="meta-item">
|
||||
<span class="meta-label">{{ strings.by }}</span>
|
||||
<span class="meta-value" :class="{ 'deleted-user': thread.authorIsDeleted }">
|
||||
{{ thread.authorDisplayName || thread.authorId }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="meta-divider">·</span>
|
||||
<span class="meta-item">
|
||||
<NcDateTime v-if="thread.createdAt" :timestamp="thread.createdAt * 1000" />
|
||||
</span>
|
||||
<span class="meta-divider">·</span>
|
||||
<span class="meta-item">
|
||||
<span class="stat-icon">
|
||||
<EyeIcon :size="16" />
|
||||
</span>
|
||||
<span class="stat-label">{{ strings.views(thread.viewCount) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts list -->
|
||||
<section v-if="!loading && !error && posts.length > 0" class="mt-16">
|
||||
<div class="posts-list">
|
||||
<PostCard
|
||||
v-for="(post, index) in posts"
|
||||
:key="post.id"
|
||||
:ref="(el) => setPostCardRef(el, post.id)"
|
||||
:post="post"
|
||||
:is-first-post="index === 0"
|
||||
:is-unread="isPostUnread(post)"
|
||||
@reply="handleReply"
|
||||
@update="handleUpdate"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Pagination info -->
|
||||
<div v-if="posts.length >= limit" class="pagination-info mt-16">
|
||||
<p class="muted">{{ strings.showingPosts(posts.length) }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Empty posts state (thread exists but no posts) -->
|
||||
<NcEmptyContent
|
||||
v-else-if="!loading && !error && thread && posts.length === 0"
|
||||
:title="strings.emptyPostsTitle"
|
||||
:description="strings.emptyPostsDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="replyToThread" variant="primary">
|
||||
<template #icon>
|
||||
<ReplyIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.reply }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Locked message (only shown to non-moderators) -->
|
||||
<div
|
||||
v-if="!loading && !error && thread && thread.isLocked && !canModerate"
|
||||
class="locked-message mt-16"
|
||||
>
|
||||
<NcEmptyContent :title="strings.locked" :description="strings.lockedMessage">
|
||||
<template #icon>
|
||||
<LockIcon :size="64" />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
</div>
|
||||
|
||||
<!-- Reply form (moderators can reply even when locked) -->
|
||||
<PostReplyForm
|
||||
v-if="!loading && !error && thread && (!thread.isLocked || canModerate)"
|
||||
ref="replyForm"
|
||||
@submit="handleSubmitReply"
|
||||
@cancel="handleCancelReply"
|
||||
/>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -200,6 +202,7 @@ import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
|
||||
import AppToolbar from '@/components/AppToolbar.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import PostCard from '@/components/PostCard.vue'
|
||||
import PostReplyForm from '@/components/PostReplyForm.vue'
|
||||
import PinIcon from '@icons/Pin.vue'
|
||||
@@ -227,6 +230,7 @@ export default defineComponent({
|
||||
NcLoadingIcon,
|
||||
NcDateTime,
|
||||
AppToolbar,
|
||||
PageWrapper,
|
||||
PostCard,
|
||||
PostReplyForm,
|
||||
PinIcon,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
</AppToolbar>
|
||||
|
||||
<div class="preferences-content">
|
||||
<PageWrapper>
|
||||
<div class="page-header">
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<NcButton type="primary" :disabled="saving || !hasChanges" @click="savePreferences">
|
||||
<NcButton variant="primary" :disabled="saving || !hasChanges" @click="savePreferences">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="saving" :size="20" />
|
||||
<CheckIcon v-else :size="20" />
|
||||
@@ -71,7 +71,7 @@
|
||||
<span>{{ strings.saveSuccess }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -82,6 +82,7 @@ import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import AppToolbar from '@/components/AppToolbar.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import ArrowLeftIcon from '@icons/ArrowLeft.vue'
|
||||
import CheckIcon from '@icons/Check.vue'
|
||||
import { ocs } from '@/axios'
|
||||
@@ -99,6 +100,7 @@ export default defineComponent({
|
||||
NcLoadingIcon,
|
||||
NcCheckboxRadioSwitch,
|
||||
AppToolbar,
|
||||
PageWrapper,
|
||||
ArrowLeftIcon,
|
||||
CheckIcon,
|
||||
},
|
||||
@@ -199,25 +201,6 @@ export default defineComponent({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-preferences-view {
|
||||
.preferences-content {
|
||||
padding: 16px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--color-text-maxcontrast);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--color-text-maxcontrast);
|
||||
opacity: 0.7;
|
||||
@@ -235,10 +218,18 @@ export default defineComponent({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.preferences-form {
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 32px;
|
||||
padding: 24px;
|
||||
@@ -273,9 +264,6 @@ export default defineComponent({
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.success-message {
|
||||
|
||||
@@ -1,255 +1,257 @@
|
||||
<template>
|
||||
<div class="admin-bbcode-list">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<NcButton @click="showHelp = true">
|
||||
<template #icon>
|
||||
<HelpCircleIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.help }}
|
||||
</NcButton>
|
||||
<NcButton variant="primary" @click="createBBCode">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.createBBCode }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BBCode Help Dialog -->
|
||||
<BBCodeHelpDialog v-model:open="showHelp" :show-custom="false" />
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- BBCode list -->
|
||||
<div v-else class="bbcode-list">
|
||||
<!-- Enabled BBCodes Section -->
|
||||
<section class="bbcodes-section">
|
||||
<div class="section-header">
|
||||
<h3>{{ strings.enabledTitle }}</h3>
|
||||
<p class="muted">{{ strings.enabledSubtitle }}</p>
|
||||
<PageWrapper>
|
||||
<div class="admin-bbcode-list">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<NcButton @click="showHelp = true">
|
||||
<template #icon>
|
||||
<HelpCircleIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.help }}
|
||||
</NcButton>
|
||||
<NcButton variant="primary" @click="createBBCode">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.createBBCode }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="enabledBBCodes.length > 0" class="bbcodes-table">
|
||||
<div v-for="bbcode in enabledBBCodes" :key="`bbcode-${bbcode.id}`" class="bbcode-row">
|
||||
<div class="bbcode-info">
|
||||
<div class="bbcode-header">
|
||||
<div class="bbcode-tag">[{{ bbcode.tag }}]</div>
|
||||
<div v-if="bbcode.parseInner" class="badge badge-info">
|
||||
{{ strings.parseInner }}
|
||||
<!-- BBCode Help Dialog -->
|
||||
<BBCodeHelpDialog v-model:open="showHelp" :show-custom="false" />
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- BBCode list -->
|
||||
<div v-else class="bbcode-list">
|
||||
<!-- Enabled BBCodes Section -->
|
||||
<section class="bbcodes-section">
|
||||
<div class="section-header">
|
||||
<h3>{{ strings.enabledTitle }}</h3>
|
||||
<p class="muted">{{ strings.enabledSubtitle }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="enabledBBCodes.length > 0" class="bbcodes-table">
|
||||
<div v-for="bbcode in enabledBBCodes" :key="`bbcode-${bbcode.id}`" class="bbcode-row">
|
||||
<div class="bbcode-info">
|
||||
<div class="bbcode-header">
|
||||
<div class="bbcode-tag">[{{ bbcode.tag }}]</div>
|
||||
<div v-if="bbcode.parseInner" class="badge badge-info">
|
||||
{{ strings.parseInner }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bbcode.description" class="bbcode-desc muted">
|
||||
{{ bbcode.description }}
|
||||
</div>
|
||||
<div class="bbcode-replacement">
|
||||
<span class="label muted">{{ strings.replacement }}:</span>
|
||||
<code>{{ bbcode.replacement }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bbcode.description" class="bbcode-desc muted">
|
||||
{{ bbcode.description }}
|
||||
<div class="bbcode-actions">
|
||||
<NcButton @click="editBBCode(bbcode)">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcButton>
|
||||
<NcButton @click="toggleEnabled(bbcode)">
|
||||
<template #icon>
|
||||
<EyeOffIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.disable }}
|
||||
</NcButton>
|
||||
<NcButton variant="error" @click="confirmDelete(bbcode)">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.delete }}
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="bbcode-replacement">
|
||||
<span class="label muted">{{ strings.replacement }}:</span>
|
||||
<code>{{ bbcode.replacement }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bbcode-actions">
|
||||
<NcButton @click="editBBCode(bbcode)">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcButton>
|
||||
<NcButton @click="toggleEnabled(bbcode)">
|
||||
<template #icon>
|
||||
<EyeOffIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.disable }}
|
||||
</NcButton>
|
||||
<NcButton variant="error" @click="confirmDelete(bbcode)">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.delete }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-bbcodes muted">
|
||||
{{ strings.noEnabledBBCodes }}
|
||||
</div>
|
||||
</section>
|
||||
<div v-else class="no-bbcodes muted">
|
||||
{{ strings.noEnabledBBCodes }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Disabled BBCodes Section -->
|
||||
<section v-if="disabledBBCodes.length > 0" class="bbcodes-section">
|
||||
<div class="section-header">
|
||||
<h3>{{ strings.disabledTitle }}</h3>
|
||||
<p class="muted">{{ strings.disabledSubtitle }}</p>
|
||||
<!-- Disabled BBCodes Section -->
|
||||
<section v-if="disabledBBCodes.length > 0" class="bbcodes-section">
|
||||
<div class="section-header">
|
||||
<h3>{{ strings.disabledTitle }}</h3>
|
||||
<p class="muted">{{ strings.disabledSubtitle }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bbcodes-table">
|
||||
<div
|
||||
v-for="bbcode in disabledBBCodes"
|
||||
:key="`bbcode-${bbcode.id}`"
|
||||
class="bbcode-row disabled"
|
||||
>
|
||||
<div class="bbcode-info">
|
||||
<div class="bbcode-header">
|
||||
<div class="bbcode-tag">[{{ bbcode.tag }}]</div>
|
||||
<div v-if="bbcode.parseInner" class="badge badge-info">
|
||||
{{ strings.parseInner }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bbcode.description" class="bbcode-desc muted">
|
||||
{{ bbcode.description }}
|
||||
</div>
|
||||
<div class="bbcode-replacement">
|
||||
<span class="label muted">{{ strings.replacement }}:</span>
|
||||
<code>{{ bbcode.replacement }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bbcode-actions">
|
||||
<NcButton @click="editBBCode(bbcode)">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcButton>
|
||||
<NcButton variant="primary" @click="toggleEnabled(bbcode)">
|
||||
<template #icon>
|
||||
<EyeIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.enable }}
|
||||
</NcButton>
|
||||
<NcButton variant="error" @click="confirmDelete(bbcode)">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.delete }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<NcDialog
|
||||
v-if="deleteDialog.show"
|
||||
:name="strings.deleteDialogTitle"
|
||||
@close="deleteDialog.show = false"
|
||||
>
|
||||
<div class="delete-dialog-content">
|
||||
<p>{{ strings.deleteConfirmMessage(deleteDialog.bbcode?.tag || '') }}</p>
|
||||
<p class="muted">{{ strings.deleteWarning }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bbcodes-table">
|
||||
<div
|
||||
v-for="bbcode in disabledBBCodes"
|
||||
:key="`bbcode-${bbcode.id}`"
|
||||
class="bbcode-row disabled"
|
||||
<template #actions>
|
||||
<NcButton @click="deleteDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton variant="error" @click="executeDelete">
|
||||
{{ strings.deleteBBCode }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
|
||||
<!-- BBCode Edit/Create Dialog -->
|
||||
<NcDialog
|
||||
v-if="editDialog.show"
|
||||
:name="editDialog.isEditing ? strings.editBBCodeTitle : strings.createBBCodeTitle"
|
||||
@close="editDialog.show = false"
|
||||
>
|
||||
<div class="bbcode-dialog-content">
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="editDialog.tag"
|
||||
:label="strings.tag"
|
||||
:placeholder="strings.tagPlaceholder"
|
||||
:required="true"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.tagHelp }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="editDialog.replacement"
|
||||
:label="strings.replacementLabel"
|
||||
:placeholder="strings.replacementPlaceholder"
|
||||
:rows="3"
|
||||
:required="true"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.replacementHelp }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="editDialog.example"
|
||||
:label="strings.exampleLabel"
|
||||
:placeholder="strings.examplePlaceholder"
|
||||
:rows="2"
|
||||
:required="true"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.exampleHelp }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="editDialog.description"
|
||||
:label="strings.description"
|
||||
:placeholder="strings.descriptionPlaceholder"
|
||||
:rows="2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcCheckboxRadioSwitch v-model="editDialog.enabled" type="switch">
|
||||
{{ strings.enabledLabel }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcCheckboxRadioSwitch v-model="editDialog.parseInner" type="switch">
|
||||
{{ strings.parseInnerLabel }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="help-text muted">{{ strings.parseInnerHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NcButton @click="editDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
variant="primary"
|
||||
:disabled="
|
||||
!editDialog.tag.trim() || !editDialog.replacement.trim() || !editDialog.example.trim()
|
||||
"
|
||||
@click="saveBBCode"
|
||||
>
|
||||
<div class="bbcode-info">
|
||||
<div class="bbcode-header">
|
||||
<div class="bbcode-tag">[{{ bbcode.tag }}]</div>
|
||||
<div v-if="bbcode.parseInner" class="badge badge-info">
|
||||
{{ strings.parseInner }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bbcode.description" class="bbcode-desc muted">
|
||||
{{ bbcode.description }}
|
||||
</div>
|
||||
<div class="bbcode-replacement">
|
||||
<span class="label muted">{{ strings.replacement }}:</span>
|
||||
<code>{{ bbcode.replacement }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bbcode-actions">
|
||||
<NcButton @click="editBBCode(bbcode)">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcButton>
|
||||
<NcButton variant="primary" @click="toggleEnabled(bbcode)">
|
||||
<template #icon>
|
||||
<EyeIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.enable }}
|
||||
</NcButton>
|
||||
<NcButton variant="error" @click="confirmDelete(bbcode)">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.delete }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<template v-if="editDialog.submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ editDialog.isEditing ? strings.update : strings.create }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<NcDialog
|
||||
v-if="deleteDialog.show"
|
||||
:name="strings.deleteDialogTitle"
|
||||
@close="deleteDialog.show = false"
|
||||
>
|
||||
<div class="delete-dialog-content">
|
||||
<p>{{ strings.deleteConfirmMessage(deleteDialog.bbcode?.tag || '') }}</p>
|
||||
<p class="muted">{{ strings.deleteWarning }}</p>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NcButton @click="deleteDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton variant="error" @click="executeDelete">
|
||||
{{ strings.deleteBBCode }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
|
||||
<!-- BBCode Edit/Create Dialog -->
|
||||
<NcDialog
|
||||
v-if="editDialog.show"
|
||||
:name="editDialog.isEditing ? strings.editBBCodeTitle : strings.createBBCodeTitle"
|
||||
@close="editDialog.show = false"
|
||||
>
|
||||
<div class="bbcode-dialog-content">
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="editDialog.tag"
|
||||
:label="strings.tag"
|
||||
:placeholder="strings.tagPlaceholder"
|
||||
:required="true"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.tagHelp }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="editDialog.replacement"
|
||||
:label="strings.replacementLabel"
|
||||
:placeholder="strings.replacementPlaceholder"
|
||||
:rows="3"
|
||||
:required="true"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.replacementHelp }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="editDialog.example"
|
||||
:label="strings.exampleLabel"
|
||||
:placeholder="strings.examplePlaceholder"
|
||||
:rows="2"
|
||||
:required="true"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.exampleHelp }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="editDialog.description"
|
||||
:label="strings.description"
|
||||
:placeholder="strings.descriptionPlaceholder"
|
||||
:rows="2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcCheckboxRadioSwitch v-model="editDialog.enabled" type="switch">
|
||||
{{ strings.enabledLabel }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcCheckboxRadioSwitch v-model="editDialog.parseInner" type="switch">
|
||||
{{ strings.parseInnerLabel }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="help-text muted">{{ strings.parseInnerHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NcButton @click="editDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
variant="primary"
|
||||
:disabled="
|
||||
!editDialog.tag.trim() || !editDialog.replacement.trim() || !editDialog.example.trim()
|
||||
"
|
||||
@click="saveBBCode"
|
||||
>
|
||||
<template v-if="editDialog.submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ editDialog.isEditing ? strings.update : strings.create }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -268,6 +270,7 @@ import EyeIcon from '@icons/Eye.vue'
|
||||
import EyeOffIcon from '@icons/EyeOff.vue'
|
||||
import HelpCircleIcon from '@icons/HelpCircle.vue'
|
||||
import BBCodeHelpDialog from '@/components/BBCodeHelpDialog.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import { ocs } from '@/axios'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
@@ -292,6 +295,7 @@ export default defineComponent({
|
||||
NcLoadingIcon,
|
||||
NcTextField,
|
||||
NcTextArea,
|
||||
PageWrapper,
|
||||
PlusIcon,
|
||||
PencilIcon,
|
||||
DeleteIcon,
|
||||
@@ -496,8 +500,6 @@ export default defineComponent({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-bbcode-list {
|
||||
max-width: 1200px;
|
||||
|
||||
.muted {
|
||||
color: var(--color-text-maxcontrast);
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -1,192 +1,195 @@
|
||||
<template>
|
||||
<div class="admin-category-edit">
|
||||
<div class="page-header">
|
||||
<div class="header-actions">
|
||||
<NcButton @click="goBack">
|
||||
<template #icon>
|
||||
<ArrowLeftIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.back }}
|
||||
</NcButton>
|
||||
<PageWrapper>
|
||||
<div class="admin-category-edit">
|
||||
<div class="page-header">
|
||||
<div class="header-actions">
|
||||
<NcButton @click="goBack">
|
||||
<template #icon>
|
||||
<ArrowLeftIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.back }}
|
||||
</NcButton>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ isEditing ? strings.editCategory : strings.createCategory }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ isEditing ? strings.editCategory : strings.createCategory }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<!-- Form -->
|
||||
<div v-else class="category-form">
|
||||
<section class="form-section">
|
||||
<h3>{{ strings.basicInfo }}</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>{{ strings.categoryHeader }} *</label>
|
||||
<div class="header-select-row">
|
||||
<NcSelect
|
||||
v-model="selectedHeader"
|
||||
:options="headerOptions"
|
||||
:placeholder="strings.selectHeader"
|
||||
label="label"
|
||||
track-by="id"
|
||||
class="header-select"
|
||||
/>
|
||||
<NcButton @click="createNewHeader">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.newHeader }}
|
||||
</NcButton>
|
||||
<NcButton v-if="selectedHeader" @click="editHeader">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.editHeader }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div v-else class="category-form">
|
||||
<section class="form-section">
|
||||
<h3>{{ strings.basicInfo }}</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>{{ strings.categoryHeader }} *</label>
|
||||
<div class="header-select-row">
|
||||
<NcSelect
|
||||
v-model="selectedHeader"
|
||||
:options="headerOptions"
|
||||
:placeholder="strings.selectHeader"
|
||||
label="label"
|
||||
track-by="id"
|
||||
class="header-select"
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="formData.name"
|
||||
:label="strings.name"
|
||||
:placeholder="strings.namePlaceholder"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="formData.slug"
|
||||
:label="strings.slug"
|
||||
:placeholder="strings.slugPlaceholder"
|
||||
:required="true"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.slugHelp }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="formData.description"
|
||||
:label="strings.description"
|
||||
:placeholder="strings.descriptionPlaceholder"
|
||||
:rows="3"
|
||||
/>
|
||||
<NcButton @click="createNewHeader">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.newHeader }}
|
||||
</NcButton>
|
||||
<NcButton v-if="selectedHeader" @click="editHeader">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.editHeader }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="formData.name"
|
||||
:label="strings.name"
|
||||
:placeholder="strings.namePlaceholder"
|
||||
:required="true"
|
||||
/>
|
||||
<!-- Permissions Section -->
|
||||
<section class="form-section">
|
||||
<h3>{{ strings.permissions }}</h3>
|
||||
<p class="muted">{{ strings.permissionsDescription }}</p>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>{{ strings.viewRoles }}</label>
|
||||
<NcSelect
|
||||
v-model="selectedViewRoles"
|
||||
:options="roleOptions"
|
||||
:placeholder="strings.selectRoles"
|
||||
label="label"
|
||||
track-by="id"
|
||||
:multiple="true"
|
||||
:taggable="false"
|
||||
:close-on-select="false"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.viewRolesHelp }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{ strings.moderateRoles }}</label>
|
||||
<NcSelect
|
||||
v-model="selectedModerateRoles"
|
||||
:options="roleOptions"
|
||||
:placeholder="strings.selectRoles"
|
||||
label="label"
|
||||
track-by="id"
|
||||
:multiple="true"
|
||||
:taggable="false"
|
||||
:close-on-select="false"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.moderateRolesHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<NcButton @click="goBack">{{ strings.cancel }}</NcButton>
|
||||
<NcButton variant="primary" :disabled="!canSubmit || submitting" @click="submitForm">
|
||||
<template v-if="submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ isEditing ? strings.update : strings.create }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header Edit/Create Dialog -->
|
||||
<NcDialog
|
||||
v-if="headerDialog.show"
|
||||
:name="headerDialog.isEditing ? strings.editHeaderTitle : strings.createHeaderTitle"
|
||||
@close="headerDialog.show = false"
|
||||
>
|
||||
<div class="header-dialog-content">
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="formData.slug"
|
||||
:label="strings.slug"
|
||||
:placeholder="strings.slugPlaceholder"
|
||||
v-model="headerDialog.name"
|
||||
:label="strings.headerName"
|
||||
:placeholder="strings.headerNamePlaceholder"
|
||||
:required="true"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.slugHelp }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="formData.description"
|
||||
:label="strings.description"
|
||||
:placeholder="strings.descriptionPlaceholder"
|
||||
:rows="3"
|
||||
v-model="headerDialog.description"
|
||||
:label="strings.headerDescription"
|
||||
:placeholder="strings.headerDescriptionPlaceholder"
|
||||
:rows="2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Permissions Section -->
|
||||
<section class="form-section">
|
||||
<h3>{{ strings.permissions }}</h3>
|
||||
<p class="muted">{{ strings.permissionsDescription }}</p>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>{{ strings.viewRoles }}</label>
|
||||
<NcSelect
|
||||
v-model="selectedViewRoles"
|
||||
:options="roleOptions"
|
||||
:placeholder="strings.selectRoles"
|
||||
label="label"
|
||||
track-by="id"
|
||||
:multiple="true"
|
||||
:taggable="false"
|
||||
:close-on-select="false"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.viewRolesHelp }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{ strings.moderateRoles }}</label>
|
||||
<NcSelect
|
||||
v-model="selectedModerateRoles"
|
||||
:options="roleOptions"
|
||||
:placeholder="strings.selectRoles"
|
||||
label="label"
|
||||
track-by="id"
|
||||
:multiple="true"
|
||||
:taggable="false"
|
||||
:close-on-select="false"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.moderateRolesHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<NcButton @click="goBack">{{ strings.cancel }}</NcButton>
|
||||
<NcButton variant="primary" :disabled="!canSubmit || submitting" @click="submitForm">
|
||||
<template v-if="submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ isEditing ? strings.update : strings.create }}
|
||||
</NcButton>
|
||||
</div>
|
||||
<template #actions>
|
||||
<NcButton @click="headerDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton variant="primary" :disabled="!headerDialog.name.trim()" @click="saveHeader">
|
||||
<template v-if="headerDialog.submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ headerDialog.isEditing ? strings.update : strings.create }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
</div>
|
||||
|
||||
<!-- Header Edit/Create Dialog -->
|
||||
<NcDialog
|
||||
v-if="headerDialog.show"
|
||||
:name="headerDialog.isEditing ? strings.editHeaderTitle : strings.createHeaderTitle"
|
||||
@close="headerDialog.show = false"
|
||||
>
|
||||
<div class="header-dialog-content">
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="headerDialog.name"
|
||||
:label="strings.headerName"
|
||||
:placeholder="strings.headerNamePlaceholder"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="headerDialog.description"
|
||||
:label="strings.headerDescription"
|
||||
:placeholder="strings.headerDescriptionPlaceholder"
|
||||
:rows="2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NcButton @click="headerDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton variant="primary" :disabled="!headerDialog.name.trim()" @click="saveHeader">
|
||||
<template v-if="headerDialog.submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ headerDialog.isEditing ? strings.update : strings.create }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
@@ -211,6 +214,7 @@ export default defineComponent({
|
||||
NcSelect,
|
||||
NcTextField,
|
||||
NcTextArea,
|
||||
PageWrapper,
|
||||
ArrowLeftIcon,
|
||||
PlusIcon,
|
||||
PencilIcon,
|
||||
@@ -559,8 +563,6 @@ export default defineComponent({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-category-edit {
|
||||
max-width: 800px;
|
||||
|
||||
.muted {
|
||||
color: var(--color-text-maxcontrast);
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -1,112 +1,56 @@
|
||||
<template>
|
||||
<div class="admin-category-list">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
<PageWrapper>
|
||||
<div class="admin-category-list">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<NcButton @click="createHeader">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.createHeader }}
|
||||
</NcButton>
|
||||
<NcButton variant="primary" @click="createCategory">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.createCategory }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<NcButton @click="createHeader">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.createHeader }}
|
||||
</NcButton>
|
||||
<NcButton variant="primary" @click="createCategory">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.createCategory }}
|
||||
</NcButton>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Category list -->
|
||||
<div v-else class="category-list">
|
||||
<!-- Categories by Header -->
|
||||
<section class="categories-section">
|
||||
<template v-for="(header, headerIndex) in categoryHeaders" :key="header.id">
|
||||
<div class="header-row">
|
||||
<div class="header-sort-buttons">
|
||||
<NcButton
|
||||
v-if="headerIndex > 0"
|
||||
variant="tertiary"
|
||||
@click="moveHeaderUp(headerIndex)"
|
||||
:aria-label="strings.moveUp"
|
||||
:title="strings.moveUp"
|
||||
>
|
||||
<template #icon>
|
||||
<ChevronUpIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton
|
||||
v-if="headerIndex < categoryHeaders.length - 1"
|
||||
variant="tertiary"
|
||||
@click="moveHeaderDown(headerIndex)"
|
||||
:aria-label="strings.moveDown"
|
||||
:title="strings.moveDown"
|
||||
>
|
||||
<template #icon>
|
||||
<ChevronDownIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="header-info">
|
||||
<h3>{{ header.name }}</h3>
|
||||
<span v-if="header.description" class="muted">{{ header.description }}</span>
|
||||
<span class="muted category-count"
|
||||
>{{ header.categories?.length || 0 }} {{ strings.categoriesCount }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<NcButton @click="editHeaderById(header.id)">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
variant="error"
|
||||
:disabled="categoryHeaders.length <= 1"
|
||||
@click="confirmDeleteHeader(header)"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.delete }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="header.categories && header.categories.length > 0" class="categories-table">
|
||||
<div
|
||||
v-for="(category, index) in header.categories"
|
||||
:key="category.id"
|
||||
class="category-row"
|
||||
>
|
||||
<div class="category-sort-buttons">
|
||||
<!-- Category list -->
|
||||
<div v-else class="category-list">
|
||||
<!-- Categories by Header -->
|
||||
<section class="categories-section">
|
||||
<template v-for="(header, headerIndex) in categoryHeaders" :key="header.id">
|
||||
<div class="header-row">
|
||||
<div class="header-sort-buttons">
|
||||
<NcButton
|
||||
v-if="index > 0"
|
||||
v-if="headerIndex > 0"
|
||||
variant="tertiary"
|
||||
@click="moveCategoryUp(header.id, index)"
|
||||
@click="moveHeaderUp(headerIndex)"
|
||||
:aria-label="strings.moveUp"
|
||||
:title="strings.moveUp"
|
||||
>
|
||||
@@ -115,9 +59,9 @@
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton
|
||||
v-if="index < header.categories.length - 1"
|
||||
v-if="headerIndex < categoryHeaders.length - 1"
|
||||
variant="tertiary"
|
||||
@click="moveCategoryDown(header.id, index)"
|
||||
@click="moveHeaderDown(headerIndex)"
|
||||
:aria-label="strings.moveDown"
|
||||
:title="strings.moveDown"
|
||||
>
|
||||
@@ -126,27 +70,25 @@
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="category-info">
|
||||
<div class="category-name">{{ category.name }}</div>
|
||||
<div v-if="category.description" class="category-desc muted">
|
||||
{{ category.description }}
|
||||
</div>
|
||||
<div class="category-meta muted">
|
||||
<span>Slug: {{ category.slug }}</span>
|
||||
<span>•</span>
|
||||
<span>Threads: {{ category.threadCount || 0 }}</span>
|
||||
<span>•</span>
|
||||
<span>Posts: {{ category.postCount || 0 }}</span>
|
||||
</div>
|
||||
<div class="header-info">
|
||||
<h3>{{ header.name }}</h3>
|
||||
<span v-if="header.description" class="muted">{{ header.description }}</span>
|
||||
<span class="muted category-count"
|
||||
>{{ header.categories?.length || 0 }} {{ strings.categoriesCount }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="category-actions">
|
||||
<NcButton @click="editCategory(category.id)">
|
||||
<div class="header-actions">
|
||||
<NcButton @click="editHeaderById(header.id)">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcButton>
|
||||
<NcButton variant="error" @click="confirmDelete(category)">
|
||||
<NcButton
|
||||
variant="error"
|
||||
:disabled="categoryHeaders.length <= 1"
|
||||
@click="confirmDeleteHeader(header)"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
@@ -154,201 +96,262 @@
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-categories muted">
|
||||
{{ strings.noCategories }}
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<NcDialog
|
||||
v-if="deleteDialog.show"
|
||||
:name="strings.deleteDialogTitle"
|
||||
@close="deleteDialog.show = false"
|
||||
>
|
||||
<div class="delete-dialog-content">
|
||||
<p>{{ strings.deleteConfirmMessage(deleteDialog.category?.name || '') }}</p>
|
||||
|
||||
<div v-if="deleteDialog.threadCount > 0" class="thread-warning">
|
||||
<InformationIcon :size="20" />
|
||||
<span>{{ strings.threadWarning(deleteDialog.threadCount) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="deleteDialog.threadCount > 0" class="migration-options">
|
||||
<h4>{{ strings.whatToDoWithThreads }}</h4>
|
||||
|
||||
<div class="radio-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="deleteDialog.action"
|
||||
value="migrate"
|
||||
type="radio"
|
||||
name="delete-action"
|
||||
>
|
||||
{{ strings.migrateThreads }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<div v-if="deleteDialog.action === 'migrate'" class="category-select">
|
||||
<label>{{ strings.selectTargetCategory }}</label>
|
||||
<NcSelect
|
||||
v-model="selectedTargetCategory"
|
||||
:options="targetCategoryOptions"
|
||||
:placeholder="strings.selectCategory"
|
||||
label="label"
|
||||
track-by="id"
|
||||
/>
|
||||
<div v-if="header.categories && header.categories.length > 0" class="categories-table">
|
||||
<div
|
||||
v-for="(category, index) in header.categories"
|
||||
:key="category.id"
|
||||
class="category-row"
|
||||
>
|
||||
<div class="category-sort-buttons">
|
||||
<NcButton
|
||||
v-if="index > 0"
|
||||
variant="tertiary"
|
||||
@click="moveCategoryUp(header.id, index)"
|
||||
:aria-label="strings.moveUp"
|
||||
:title="strings.moveUp"
|
||||
>
|
||||
<template #icon>
|
||||
<ChevronUpIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton
|
||||
v-if="index < header.categories.length - 1"
|
||||
variant="tertiary"
|
||||
@click="moveCategoryDown(header.id, index)"
|
||||
:aria-label="strings.moveDown"
|
||||
:title="strings.moveDown"
|
||||
>
|
||||
<template #icon>
|
||||
<ChevronDownIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="category-info">
|
||||
<div class="category-name">{{ category.name }}</div>
|
||||
<div v-if="category.description" class="category-desc muted">
|
||||
{{ category.description }}
|
||||
</div>
|
||||
<div class="category-meta muted">
|
||||
<span>Slug: {{ category.slug }}</span>
|
||||
<span>•</span>
|
||||
<span>Threads: {{ category.threadCount || 0 }}</span>
|
||||
<span>•</span>
|
||||
<span>Posts: {{ category.postCount || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="category-actions">
|
||||
<NcButton @click="editCategory(category.id)">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcButton>
|
||||
<NcButton variant="error" @click="confirmDelete(category)">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.delete }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-categories muted">
|
||||
{{ strings.noCategories }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="radio-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="deleteDialog.action"
|
||||
value="delete"
|
||||
type="radio"
|
||||
name="delete-action"
|
||||
>
|
||||
{{ strings.softDeleteThreads }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="help-text muted">{{ strings.softDeleteHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NcButton @click="deleteDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
variant="error"
|
||||
:disabled="deleteDialog.action === 'migrate' && !selectedTargetCategory"
|
||||
@click="executeDelete"
|
||||
>
|
||||
{{ strings.deleteCategory }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
|
||||
<!-- Header Edit/Create Dialog -->
|
||||
<NcDialog
|
||||
v-if="headerDialog.show"
|
||||
:name="headerDialog.isEditing ? strings.editHeaderTitle : strings.createHeaderTitle"
|
||||
@close="headerDialog.show = false"
|
||||
>
|
||||
<div class="header-dialog-content">
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="headerDialog.name"
|
||||
:label="strings.headerName"
|
||||
:placeholder="strings.headerNamePlaceholder"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="headerDialog.description"
|
||||
:label="strings.headerDescription"
|
||||
:placeholder="strings.headerDescriptionPlaceholder"
|
||||
:rows="2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model.number="headerDialog.sortOrder"
|
||||
:label="strings.headerSortOrder"
|
||||
:placeholder="strings.sortOrderPlaceholder"
|
||||
type="number"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.sortOrderHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NcButton @click="headerDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton variant="primary" :disabled="!headerDialog.name.trim()" @click="saveHeader">
|
||||
<template v-if="headerDialog.submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ headerDialog.isEditing ? strings.update : strings.create }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
|
||||
<!-- Header Delete Confirmation Dialog -->
|
||||
<NcDialog
|
||||
v-if="deleteHeaderDialog.show"
|
||||
:name="strings.deleteHeaderTitle"
|
||||
@close="deleteHeaderDialog.show = false"
|
||||
>
|
||||
<div class="delete-dialog-content">
|
||||
<p>{{ strings.deleteHeaderMessage(deleteHeaderDialog.header?.name || '') }}</p>
|
||||
|
||||
<div v-if="deleteHeaderDialog.categoryCount > 0" class="thread-warning">
|
||||
<InformationIcon :size="20" />
|
||||
<span>{{ strings.headerCategoryWarning(deleteHeaderDialog.categoryCount) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="deleteHeaderDialog.categoryCount > 0" class="migration-options">
|
||||
<h4>{{ strings.whatToDoWithCategories }}</h4>
|
||||
|
||||
<div class="radio-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="deleteHeaderDialog.action"
|
||||
value="migrate"
|
||||
type="radio"
|
||||
name="delete-header-action"
|
||||
>
|
||||
{{ strings.migrateCategories }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<div v-if="deleteHeaderDialog.action === 'migrate'" class="category-select">
|
||||
<label>{{ strings.selectTargetHeader }}</label>
|
||||
<NcSelect
|
||||
v-model="selectedTargetHeader"
|
||||
:options="targetHeaderOptions"
|
||||
:placeholder="strings.selectHeader"
|
||||
label="label"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="radio-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="deleteHeaderDialog.action"
|
||||
value="delete"
|
||||
type="radio"
|
||||
name="delete-header-action"
|
||||
>
|
||||
{{ strings.deleteCategories }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="help-text muted">{{ strings.deleteCategoriesHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NcButton @click="deleteHeaderDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
variant="error"
|
||||
:disabled="deleteHeaderDialog.action === 'migrate' && !selectedTargetHeader"
|
||||
@click="executeDeleteHeader"
|
||||
>
|
||||
{{ strings.deleteHeader }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
</div>
|
||||
<!-- Delete confirmation dialog -->
|
||||
<NcDialog
|
||||
v-if="deleteDialog.show"
|
||||
:name="strings.deleteDialogTitle"
|
||||
@close="deleteDialog.show = false"
|
||||
>
|
||||
<div class="delete-dialog-content">
|
||||
<p>{{ strings.deleteConfirmMessage(deleteDialog.category?.name || '') }}</p>
|
||||
|
||||
<div v-if="deleteDialog.threadCount > 0" class="thread-warning">
|
||||
<InformationIcon :size="20" />
|
||||
<span>{{ strings.threadWarning(deleteDialog.threadCount) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="deleteDialog.threadCount > 0" class="migration-options">
|
||||
<h4>{{ strings.whatToDoWithThreads }}</h4>
|
||||
|
||||
<div class="radio-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="deleteDialog.action"
|
||||
value="migrate"
|
||||
type="radio"
|
||||
name="delete-action"
|
||||
>
|
||||
{{ strings.migrateThreads }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<div v-if="deleteDialog.action === 'migrate'" class="category-select">
|
||||
<label>{{ strings.selectTargetCategory }}</label>
|
||||
<NcSelect
|
||||
v-model="selectedTargetCategory"
|
||||
:options="targetCategoryOptions"
|
||||
:placeholder="strings.selectCategory"
|
||||
label="label"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="radio-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="deleteDialog.action"
|
||||
value="delete"
|
||||
type="radio"
|
||||
name="delete-action"
|
||||
>
|
||||
{{ strings.softDeleteThreads }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="help-text muted">{{ strings.softDeleteHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NcButton @click="deleteDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
variant="error"
|
||||
:disabled="deleteDialog.action === 'migrate' && !selectedTargetCategory"
|
||||
@click="executeDelete"
|
||||
>
|
||||
{{ strings.deleteCategory }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
|
||||
<!-- Header Edit/Create Dialog -->
|
||||
<NcDialog
|
||||
v-if="headerDialog.show"
|
||||
:name="headerDialog.isEditing ? strings.editHeaderTitle : strings.createHeaderTitle"
|
||||
@close="headerDialog.show = false"
|
||||
>
|
||||
<div class="header-dialog-content">
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="headerDialog.name"
|
||||
:label="strings.headerName"
|
||||
:placeholder="strings.headerNamePlaceholder"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="headerDialog.description"
|
||||
:label="strings.headerDescription"
|
||||
:placeholder="strings.headerDescriptionPlaceholder"
|
||||
:rows="2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model.number="headerDialog.sortOrder"
|
||||
:label="strings.headerSortOrder"
|
||||
:placeholder="strings.sortOrderPlaceholder"
|
||||
type="number"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.sortOrderHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NcButton @click="headerDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton variant="primary" :disabled="!headerDialog.name.trim()" @click="saveHeader">
|
||||
<template v-if="headerDialog.submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ headerDialog.isEditing ? strings.update : strings.create }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
|
||||
<!-- Header Delete Confirmation Dialog -->
|
||||
<NcDialog
|
||||
v-if="deleteHeaderDialog.show"
|
||||
:name="strings.deleteHeaderTitle"
|
||||
@close="deleteHeaderDialog.show = false"
|
||||
>
|
||||
<div class="delete-dialog-content">
|
||||
<p>{{ strings.deleteHeaderMessage(deleteHeaderDialog.header?.name || '') }}</p>
|
||||
|
||||
<div v-if="deleteHeaderDialog.categoryCount > 0" class="thread-warning">
|
||||
<InformationIcon :size="20" />
|
||||
<span>{{ strings.headerCategoryWarning(deleteHeaderDialog.categoryCount) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="deleteHeaderDialog.categoryCount > 0" class="migration-options">
|
||||
<h4>{{ strings.whatToDoWithCategories }}</h4>
|
||||
|
||||
<div class="radio-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="deleteHeaderDialog.action"
|
||||
value="migrate"
|
||||
type="radio"
|
||||
name="delete-header-action"
|
||||
>
|
||||
{{ strings.migrateCategories }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<div v-if="deleteHeaderDialog.action === 'migrate'" class="category-select">
|
||||
<label>{{ strings.selectTargetHeader }}</label>
|
||||
<NcSelect
|
||||
v-model="selectedTargetHeader"
|
||||
:options="targetHeaderOptions"
|
||||
:placeholder="strings.selectHeader"
|
||||
label="label"
|
||||
track-by="id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="radio-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="deleteHeaderDialog.action"
|
||||
value="delete"
|
||||
type="radio"
|
||||
name="delete-header-action"
|
||||
>
|
||||
{{ strings.deleteCategories }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="help-text muted">{{ strings.deleteCategoriesHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<NcButton @click="deleteHeaderDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
variant="error"
|
||||
:disabled="deleteHeaderDialog.action === 'migrate' && !selectedTargetHeader"
|
||||
@click="executeDeleteHeader"
|
||||
>
|
||||
{{ strings.deleteHeader }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
||||
@@ -378,6 +381,7 @@ export default defineComponent({
|
||||
NcSelect,
|
||||
NcTextField,
|
||||
NcTextArea,
|
||||
PageWrapper,
|
||||
PlusIcon,
|
||||
PencilIcon,
|
||||
DeleteIcon,
|
||||
@@ -753,8 +757,6 @@ export default defineComponent({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-category-list {
|
||||
max-width: 1200px;
|
||||
|
||||
.muted {
|
||||
color: var(--color-text-maxcontrast);
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -1,139 +1,141 @@
|
||||
<template>
|
||||
<div class="admin-dashboard">
|
||||
<div class="page-header">
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
<PageWrapper>
|
||||
<div class="admin-dashboard">
|
||||
<div class="page-header">
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Dashboard content -->
|
||||
<div v-else-if="stats" class="dashboard-content">
|
||||
<!-- Totals section -->
|
||||
<section class="stats-section">
|
||||
<h3>{{ strings.totals }}</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<AccountMultipleIcon :size="32" />
|
||||
<!-- Dashboard content -->
|
||||
<div v-else-if="stats" class="dashboard-content">
|
||||
<!-- Totals section -->
|
||||
<section class="stats-section">
|
||||
<h3>{{ strings.totals }}</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<AccountMultipleIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.totals.users }}</div>
|
||||
<div class="stat-label">{{ strings.totalUsers }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.totals.users }}</div>
|
||||
<div class="stat-label">{{ strings.totalUsers }}</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<ForumIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.totals.threads }}</div>
|
||||
<div class="stat-label">{{ strings.totalThreads }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<MessageTextIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.totals.posts }}</div>
|
||||
<div class="stat-label">{{ strings.totalPosts }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<FolderIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.totals.categories }}</div>
|
||||
<div class="stat-label">{{ strings.totalCategories }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<ForumIcon :size="32" />
|
||||
<!-- Recent activity section -->
|
||||
<section class="stats-section mt-24">
|
||||
<h3>{{ strings.recentActivity }}</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<AccountPlusIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.recent.users }}</div>
|
||||
<div class="stat-label">{{ strings.newUsers }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.totals.threads }}</div>
|
||||
<div class="stat-label">{{ strings.totalThreads }}</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<ForumIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.recent.threads }}</div>
|
||||
<div class="stat-label">{{ strings.newThreads }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<MessageTextIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.recent.posts }}</div>
|
||||
<div class="stat-label">{{ strings.newPosts }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<MessageTextIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.totals.posts }}</div>
|
||||
<div class="stat-label">{{ strings.totalPosts }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<FolderIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.totals.categories }}</div>
|
||||
<div class="stat-label">{{ strings.totalCategories }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Recent activity section -->
|
||||
<section class="stats-section mt-24">
|
||||
<h3>{{ strings.recentActivity }}</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<AccountPlusIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.recent.users }}</div>
|
||||
<div class="stat-label">{{ strings.newUsers }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<ForumIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.recent.threads }}</div>
|
||||
<div class="stat-label">{{ strings.newThreads }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<MessageTextIcon :size="32" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stats.recent.posts }}</div>
|
||||
<div class="stat-label">{{ strings.newPosts }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Top contributors section -->
|
||||
<section class="stats-section mt-24">
|
||||
<h3>{{ strings.topContributors }}</h3>
|
||||
<div v-if="stats.topContributors.length > 0" class="contributors-list">
|
||||
<div
|
||||
v-for="(contributor, index) in stats.topContributors"
|
||||
:key="contributor.userId"
|
||||
class="contributor-item"
|
||||
>
|
||||
<div class="contributor-rank">{{ index + 1 }}</div>
|
||||
<UserInfo
|
||||
:user-id="contributor.userId"
|
||||
:display-name="contributor.userId"
|
||||
:avatar-size="40"
|
||||
<!-- Top contributors section -->
|
||||
<section class="stats-section mt-24">
|
||||
<h3>{{ strings.topContributors }}</h3>
|
||||
<div v-if="stats.topContributors.length > 0" class="contributors-list">
|
||||
<div
|
||||
v-for="(contributor, index) in stats.topContributors"
|
||||
:key="contributor.userId"
|
||||
class="contributor-item"
|
||||
>
|
||||
<template #meta>
|
||||
<div class="contributor-stats muted">
|
||||
{{ strings.postsCount(contributor.postCount) }}
|
||||
</div>
|
||||
</template>
|
||||
</UserInfo>
|
||||
<div class="contributor-rank">{{ index + 1 }}</div>
|
||||
<UserInfo
|
||||
:user-id="contributor.userId"
|
||||
:display-name="contributor.userId"
|
||||
:avatar-size="40"
|
||||
>
|
||||
<template #meta>
|
||||
<div class="contributor-stats muted">
|
||||
{{ strings.postsCount(contributor.postCount) }}
|
||||
</div>
|
||||
</template>
|
||||
</UserInfo>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="muted">{{ strings.noContributors }}</div>
|
||||
</section>
|
||||
<div v-else class="muted">{{ strings.noContributors }}</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -142,6 +144,7 @@ import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import UserInfo from '@/components/UserInfo.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import AccountMultipleIcon from '@icons/AccountMultiple.vue'
|
||||
import AccountPlusIcon from '@icons/AccountPlus.vue'
|
||||
import ForumIcon from '@icons/Forum.vue'
|
||||
@@ -175,6 +178,7 @@ export default defineComponent({
|
||||
NcEmptyContent,
|
||||
NcLoadingIcon,
|
||||
UserInfo,
|
||||
PageWrapper,
|
||||
AccountMultipleIcon,
|
||||
AccountPlusIcon,
|
||||
ForumIcon,
|
||||
|
||||
@@ -1,78 +1,80 @@
|
||||
<template>
|
||||
<div class="admin-general-settings">
|
||||
<div class="page-header">
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
<PageWrapper>
|
||||
<div class="page-header">
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="loadSettings">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="loadSettings">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Settings form -->
|
||||
<div v-else class="settings-form">
|
||||
<div class="form-section">
|
||||
<h3>{{ strings.appearanceTitle }}</h3>
|
||||
<p class="muted">{{ strings.appearanceDesc }}</p>
|
||||
<!-- Settings form -->
|
||||
<div v-else class="settings-form">
|
||||
<div class="form-section">
|
||||
<h3>{{ strings.appearanceTitle }}</h3>
|
||||
<p class="muted">{{ strings.appearanceDesc }}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="forum-title">{{ strings.forumTitle }}</label>
|
||||
<NcTextField
|
||||
id="forum-title"
|
||||
v-model.trim="formData.title"
|
||||
:placeholder="strings.forumTitlePlaceholder"
|
||||
:maxlength="100"
|
||||
/>
|
||||
<p class="hint">{{ strings.forumTitleHint }}</p>
|
||||
<div class="form-group">
|
||||
<label for="forum-title">{{ strings.forumTitle }}</label>
|
||||
<NcTextField
|
||||
id="forum-title"
|
||||
v-model.trim="formData.title"
|
||||
:placeholder="strings.forumTitlePlaceholder"
|
||||
:maxlength="100"
|
||||
/>
|
||||
<p class="hint">{{ strings.forumTitleHint }}</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="forum-subtitle">{{ strings.forumSubtitle }}</label>
|
||||
<NcTextArea
|
||||
id="forum-subtitle"
|
||||
v-model.trim="formData.subtitle"
|
||||
:placeholder="strings.forumSubtitlePlaceholder"
|
||||
:rows="3"
|
||||
:maxlength="500"
|
||||
/>
|
||||
<p class="hint">{{ strings.forumSubtitleHint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="forum-subtitle">{{ strings.forumSubtitle }}</label>
|
||||
<NcTextArea
|
||||
id="forum-subtitle"
|
||||
v-model.trim="formData.subtitle"
|
||||
:placeholder="strings.forumSubtitlePlaceholder"
|
||||
:rows="3"
|
||||
:maxlength="500"
|
||||
/>
|
||||
<p class="hint">{{ strings.forumSubtitleHint }}</p>
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<NcButton :disabled="saving || !hasChanges" @click="saveSettings">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="saving" :size="20" />
|
||||
<CheckIcon v-else :size="20" />
|
||||
</template>
|
||||
{{ strings.save }}
|
||||
</NcButton>
|
||||
<NcButton :disabled="saving || !hasChanges" @click="resetForm">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<!-- Success message -->
|
||||
<div v-if="saveSuccess" class="success-message">
|
||||
<CheckIcon :size="20" />
|
||||
<span>{{ strings.saveSuccess }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<NcButton :disabled="saving || !hasChanges" @click="saveSettings">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="saving" :size="20" />
|
||||
<CheckIcon v-else :size="20" />
|
||||
</template>
|
||||
{{ strings.save }}
|
||||
</NcButton>
|
||||
<NcButton :disabled="saving || !hasChanges" @click="resetForm">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<!-- Success message -->
|
||||
<div v-if="saveSuccess" class="success-message">
|
||||
<CheckIcon :size="20" />
|
||||
<span>{{ strings.saveSuccess }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -83,6 +85,7 @@ import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import NcTextArea from '@nextcloud/vue/components/NcTextArea'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import CheckIcon from '@icons/Check.vue'
|
||||
import { ocs } from '@/axios'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
@@ -100,6 +103,7 @@ export default defineComponent({
|
||||
NcLoadingIcon,
|
||||
NcTextField,
|
||||
NcTextArea,
|
||||
PageWrapper,
|
||||
CheckIcon,
|
||||
},
|
||||
data() {
|
||||
@@ -225,7 +229,6 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
max-width: 800px;
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 32px;
|
||||
|
||||
@@ -1,163 +1,165 @@
|
||||
<template>
|
||||
<div class="admin-role-edit">
|
||||
<div class="page-header">
|
||||
<div class="header-actions">
|
||||
<NcButton @click="goBack">
|
||||
<template #icon>
|
||||
<ArrowLeftIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.back }}
|
||||
</NcButton>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ isEditing ? strings.editRole : strings.createRole }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Form -->
|
||||
<div v-else class="role-form">
|
||||
<!-- Basic Info Section -->
|
||||
<section class="form-section">
|
||||
<h3>{{ strings.basicInfo }}</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="formData.name"
|
||||
:label="strings.name"
|
||||
:placeholder="strings.namePlaceholder"
|
||||
:disabled="isSystemRole"
|
||||
:required="true"
|
||||
/>
|
||||
<p v-if="isSystemRole" class="help-text muted">
|
||||
{{ strings.systemRoleNameWarning }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="formData.description"
|
||||
:label="strings.description"
|
||||
:placeholder="strings.descriptionPlaceholder"
|
||||
:rows="3"
|
||||
/>
|
||||
</div>
|
||||
<PageWrapper>
|
||||
<div class="admin-role-edit">
|
||||
<div class="page-header">
|
||||
<div class="header-actions">
|
||||
<NcButton @click="goBack">
|
||||
<template #icon>
|
||||
<ArrowLeftIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.back }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Role Permissions Section -->
|
||||
<section class="form-section">
|
||||
<h3>{{ strings.rolePermissions }}</h3>
|
||||
<p class="muted">{{ strings.rolePermissionsDesc }}</p>
|
||||
|
||||
<div class="permissions-checkboxes">
|
||||
<div class="checkbox-group">
|
||||
<NcCheckboxRadioSwitch v-model="formData.canAccessAdminTools">
|
||||
<strong>{{ strings.canAccessAdminTools }}</strong>
|
||||
<span class="checkbox-desc muted">{{ strings.canAccessAdminToolsDesc }}</span>
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<NcCheckboxRadioSwitch v-model="formData.canEditRoles">
|
||||
<strong>{{ strings.canEditRoles }}</strong>
|
||||
<span class="checkbox-desc muted">{{ strings.canEditRolesDesc }}</span>
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<NcCheckboxRadioSwitch v-model="formData.canEditCategories">
|
||||
<strong>{{ strings.canEditCategories }}</strong>
|
||||
<span class="checkbox-desc muted">{{ strings.canEditCategoriesDesc }}</span>
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ isEditing ? strings.editRole : strings.createRole }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Category Permissions Section -->
|
||||
<section class="form-section">
|
||||
<h3>{{ strings.categoryPermissions }}</h3>
|
||||
<p v-if="isAdmin" class="info-message">
|
||||
<InformationIcon :size="20" />
|
||||
{{ strings.adminFullAccess }}
|
||||
</p>
|
||||
<p v-else class="muted">{{ strings.categoryPermissionsDesc }}</p>
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="categoryHeaders.length > 0" class="permissions-table">
|
||||
<div class="table-header">
|
||||
<div class="col-category">{{ strings.category }}</div>
|
||||
<div class="col-permission">{{ strings.canView }}</div>
|
||||
<div class="col-permission">{{ strings.canModerate }}</div>
|
||||
</div>
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<template v-for="header in categoryHeaders" :key="`header-${header.id}`">
|
||||
<!-- Header row -->
|
||||
<div class="table-header-row">
|
||||
<div class="header-name">{{ header.name }}</div>
|
||||
<!-- Form -->
|
||||
<div v-else class="role-form">
|
||||
<!-- Basic Info Section -->
|
||||
<section class="form-section">
|
||||
<h3>{{ strings.basicInfo }}</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model="formData.name"
|
||||
:label="strings.name"
|
||||
:placeholder="strings.namePlaceholder"
|
||||
:disabled="isSystemRole"
|
||||
:required="true"
|
||||
/>
|
||||
<p v-if="isSystemRole" class="help-text muted">
|
||||
{{ strings.systemRoleNameWarning }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Category rows under this header -->
|
||||
<div v-for="category in header.categories" :key="category.id" class="table-row">
|
||||
<div class="col-category">
|
||||
<span class="category-name">{{ category.name }}</span>
|
||||
<span v-if="category.description" class="category-desc muted">
|
||||
{{ category.description }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-permission">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="ensurePermission(category.id).canView"
|
||||
:disabled="isAdmin"
|
||||
>
|
||||
{{ strings.allow }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
|
||||
<div class="col-permission">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="ensurePermission(category.id).canModerate"
|
||||
:disabled="isAdmin"
|
||||
>
|
||||
{{ strings.allow }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<NcTextArea
|
||||
v-model="formData.description"
|
||||
:label="strings.description"
|
||||
:placeholder="strings.descriptionPlaceholder"
|
||||
:rows="3"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="muted">{{ strings.noCategories }}</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<NcButton @click="goBack">{{ strings.cancel }}</NcButton>
|
||||
<NcButton variant="primary" :disabled="!canSubmit || submitting" @click="submitForm">
|
||||
<template v-if="submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ isEditing ? strings.update : strings.create }}
|
||||
</NcButton>
|
||||
<!-- Role Permissions Section -->
|
||||
<section class="form-section">
|
||||
<h3>{{ strings.rolePermissions }}</h3>
|
||||
<p class="muted">{{ strings.rolePermissionsDesc }}</p>
|
||||
|
||||
<div class="permissions-checkboxes">
|
||||
<div class="checkbox-group">
|
||||
<NcCheckboxRadioSwitch v-model="formData.canAccessAdminTools">
|
||||
<strong>{{ strings.canAccessAdminTools }}</strong>
|
||||
<span class="checkbox-desc muted">{{ strings.canAccessAdminToolsDesc }}</span>
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<NcCheckboxRadioSwitch v-model="formData.canEditRoles">
|
||||
<strong>{{ strings.canEditRoles }}</strong>
|
||||
<span class="checkbox-desc muted">{{ strings.canEditRolesDesc }}</span>
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<NcCheckboxRadioSwitch v-model="formData.canEditCategories">
|
||||
<strong>{{ strings.canEditCategories }}</strong>
|
||||
<span class="checkbox-desc muted">{{ strings.canEditCategoriesDesc }}</span>
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Category Permissions Section -->
|
||||
<section class="form-section">
|
||||
<h3>{{ strings.categoryPermissions }}</h3>
|
||||
<p v-if="isAdmin" class="info-message">
|
||||
<InformationIcon :size="20" />
|
||||
{{ strings.adminFullAccess }}
|
||||
</p>
|
||||
<p v-else class="muted">{{ strings.categoryPermissionsDesc }}</p>
|
||||
|
||||
<div v-if="categoryHeaders.length > 0" class="permissions-table">
|
||||
<div class="table-header">
|
||||
<div class="col-category">{{ strings.category }}</div>
|
||||
<div class="col-permission">{{ strings.canView }}</div>
|
||||
<div class="col-permission">{{ strings.canModerate }}</div>
|
||||
</div>
|
||||
|
||||
<template v-for="header in categoryHeaders" :key="`header-${header.id}`">
|
||||
<!-- Header row -->
|
||||
<div class="table-header-row">
|
||||
<div class="header-name">{{ header.name }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Category rows under this header -->
|
||||
<div v-for="category in header.categories" :key="category.id" class="table-row">
|
||||
<div class="col-category">
|
||||
<span class="category-name">{{ category.name }}</span>
|
||||
<span v-if="category.description" class="category-desc muted">
|
||||
{{ category.description }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-permission">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="ensurePermission(category.id).canView"
|
||||
:disabled="isAdmin"
|
||||
>
|
||||
{{ strings.allow }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
|
||||
<div class="col-permission">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="ensurePermission(category.id).canModerate"
|
||||
:disabled="isAdmin"
|
||||
>
|
||||
{{ strings.allow }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="muted">{{ strings.noCategories }}</div>
|
||||
</section>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="form-actions">
|
||||
<NcButton @click="goBack">{{ strings.cancel }}</NcButton>
|
||||
<NcButton variant="primary" :disabled="!canSubmit || submitting" @click="submitForm">
|
||||
<template v-if="submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
{{ isEditing ? strings.update : strings.create }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -170,6 +172,7 @@ import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import NcTextArea from '@nextcloud/vue/components/NcTextArea'
|
||||
import ArrowLeftIcon from '@icons/ArrowLeft.vue'
|
||||
import InformationIcon from '@icons/Information.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import { ocs } from '@/axios'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import type { Role, CategoryHeader } from '@/types'
|
||||
@@ -190,6 +193,7 @@ export default defineComponent({
|
||||
NcTextArea,
|
||||
ArrowLeftIcon,
|
||||
InformationIcon,
|
||||
PageWrapper,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -429,8 +433,6 @@ export default defineComponent({
|
||||
|
||||
<style scoped lang="scss">
|
||||
.admin-role-edit {
|
||||
max-width: 1200px;
|
||||
|
||||
.muted {
|
||||
color: var(--color-text-maxcontrast);
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -1,104 +1,106 @@
|
||||
<template>
|
||||
<div class="admin-role-list">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div>
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
<PageWrapper>
|
||||
<div class="admin-role-list">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div>
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
<NcButton @click="createRole" variant="primary">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.createRole }}
|
||||
</NcButton>
|
||||
</div>
|
||||
<NcButton @click="createRole" variant="primary">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.createRole }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- Role list -->
|
||||
<div v-else-if="roles.length > 0" class="roles-content">
|
||||
<div class="roles-table">
|
||||
<div class="table-header">
|
||||
<div class="col-id">{{ strings.id }}</div>
|
||||
<div class="col-name">{{ strings.name }}</div>
|
||||
<div class="col-description">{{ strings.description }}</div>
|
||||
<div class="col-created">{{ strings.created }}</div>
|
||||
<div class="col-actions">{{ strings.actions }}</div>
|
||||
</div>
|
||||
|
||||
<div v-for="role in roles" :key="role.id" class="table-row">
|
||||
<div class="col-id">
|
||||
<span class="role-id">{{ role.id }}</span>
|
||||
<!-- Role list -->
|
||||
<div v-else-if="roles.length > 0" class="roles-content">
|
||||
<div class="roles-table">
|
||||
<div class="table-header">
|
||||
<div class="col-id">{{ strings.id }}</div>
|
||||
<div class="col-name">{{ strings.name }}</div>
|
||||
<div class="col-description">{{ strings.description }}</div>
|
||||
<div class="col-created">{{ strings.created }}</div>
|
||||
<div class="col-actions">{{ strings.actions }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-name">
|
||||
<span class="role-name" :class="getRoleClass(role.id)">{{ role.name }}</span>
|
||||
</div>
|
||||
<div v-for="role in roles" :key="role.id" class="table-row">
|
||||
<div class="col-id">
|
||||
<span class="role-id">{{ role.id }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-description">
|
||||
<span v-if="role.description" class="role-description">{{ role.description }}</span>
|
||||
<span v-else class="muted">{{ strings.noDescription }}</span>
|
||||
</div>
|
||||
<div class="col-name">
|
||||
<span class="role-name" :class="getRoleClass(role.id)">{{ role.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-created">
|
||||
<NcDateTime :timestamp="role.createdAt * 1000" />
|
||||
</div>
|
||||
<div class="col-description">
|
||||
<span v-if="role.description" class="role-description">{{ role.description }}</span>
|
||||
<span v-else class="muted">{{ strings.noDescription }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-actions">
|
||||
<NcActions>
|
||||
<NcActionButton @click="editRole(role.id)">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :disabled="isSystemRole(role.id)" @click="confirmDelete(role)">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.delete }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
<div class="col-created">
|
||||
<NcDateTime :timestamp="role.createdAt * 1000" />
|
||||
</div>
|
||||
|
||||
<div class="col-actions">
|
||||
<NcActions>
|
||||
<NcActionButton @click="editRole(role.id)">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcActionButton>
|
||||
<NcActionButton :disabled="isSystemRole(role.id)" @click="confirmDelete(role)">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.delete }}
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<NcEmptyContent
|
||||
v-else
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="createRole">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.createRole }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
</div>
|
||||
<!-- Empty state -->
|
||||
<NcEmptyContent
|
||||
v-else
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="createRole">
|
||||
<template #icon>
|
||||
<PlusIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.createRole }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -112,6 +114,7 @@ import NcActionButton from '@nextcloud/vue/components/NcActionButton'
|
||||
import PlusIcon from '@icons/Plus.vue'
|
||||
import PencilIcon from '@icons/Pencil.vue'
|
||||
import DeleteIcon from '@icons/Delete.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import { ocs } from '@/axios'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import type { Role } from '@/types'
|
||||
@@ -128,6 +131,7 @@ export default defineComponent({
|
||||
PlusIcon,
|
||||
PencilIcon,
|
||||
DeleteIcon,
|
||||
PageWrapper,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,145 +1,151 @@
|
||||
<template>
|
||||
<div class="admin-user-list">
|
||||
<div class="page-header">
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
<PageWrapper :full-width="true">
|
||||
<div class="admin-user-list">
|
||||
<div class="page-header">
|
||||
<h2>{{ strings.title }}</h2>
|
||||
<p class="muted">{{ strings.subtitle }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="center mt-16">
|
||||
<NcLoadingIcon :size="32" />
|
||||
<span class="muted ml-8">{{ strings.loading }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
<!-- Error state -->
|
||||
<NcEmptyContent
|
||||
v-else-if="error"
|
||||
:title="strings.errorTitle"
|
||||
:description="error"
|
||||
class="mt-16"
|
||||
>
|
||||
<template #action>
|
||||
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<!-- User list -->
|
||||
<div v-else-if="users.length > 0" class="users-content">
|
||||
<div class="users-table">
|
||||
<div class="table-header">
|
||||
<div class="col-user">{{ strings.user }}</div>
|
||||
<div class="col-posts">{{ strings.posts }}</div>
|
||||
<div class="col-roles">{{ strings.roles }}</div>
|
||||
<div class="col-joined">{{ strings.joined }}</div>
|
||||
<div class="col-status">{{ strings.status }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="user in users"
|
||||
:key="user.userId"
|
||||
class="table-row"
|
||||
:class="{ 'is-deleted': user.isDeleted }"
|
||||
>
|
||||
<div class="col-user">
|
||||
<UserInfo :user-id="user.userId" :display-name="user.displayName" :avatar-size="40">
|
||||
<template #meta>
|
||||
<div class="user-id muted">@{{ user.userId }}</div>
|
||||
</template>
|
||||
</UserInfo>
|
||||
<!-- User list -->
|
||||
<div v-else-if="users.length > 0" class="users-content">
|
||||
<div class="users-table">
|
||||
<div class="table-header">
|
||||
<div class="col-user">{{ strings.user }}</div>
|
||||
<div class="col-posts">{{ strings.posts }}</div>
|
||||
<div class="col-roles">{{ strings.roles }}</div>
|
||||
<div class="col-joined">{{ strings.joined }}</div>
|
||||
<div class="col-status">{{ strings.status }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-posts">
|
||||
<div class="post-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ user.threadCount }}</span>
|
||||
<span class="stat-label muted">threads</span>
|
||||
</div>
|
||||
<div class="stat-divider">/</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ user.postCount }}</span>
|
||||
<span class="stat-label muted">posts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-roles">
|
||||
<div v-if="editingUserId === user.userId" class="roles-editor">
|
||||
<NcSelect
|
||||
v-model="editingRoles"
|
||||
:options="roleOptions"
|
||||
:placeholder="strings.selectRoles"
|
||||
:multiple="true"
|
||||
label="name"
|
||||
track-by="id"
|
||||
input-label="name"
|
||||
class="roles-select"
|
||||
/>
|
||||
<div class="edit-actions">
|
||||
<NcButton @click="cancelEdit" :aria-label="strings.cancel" :title="strings.cancel">
|
||||
<template #icon>
|
||||
<CloseIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton
|
||||
variant="primary"
|
||||
@click="saveRoles(user.userId)"
|
||||
:aria-label="strings.save"
|
||||
:title="strings.save"
|
||||
>
|
||||
<template #icon>
|
||||
<CheckIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="roles-display">
|
||||
<div class="roles-list">
|
||||
<span
|
||||
v-for="roleId in user.roles"
|
||||
:key="roleId"
|
||||
class="role-badge"
|
||||
:class="getRoleBadgeClass(roleId)"
|
||||
>
|
||||
{{ getRoleName(roleId) }}
|
||||
</span>
|
||||
<span v-if="user.roles.length === 0" class="muted">{{ strings.noRoles }}</span>
|
||||
</div>
|
||||
<NcButton
|
||||
@click="startEdit(user.userId, user.roles)"
|
||||
:aria-label="strings.edit"
|
||||
:title="strings.edit"
|
||||
>
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
<div
|
||||
v-for="user in users"
|
||||
:key="user.userId"
|
||||
class="table-row"
|
||||
:class="{ 'is-deleted': user.isDeleted }"
|
||||
>
|
||||
<div class="col-user">
|
||||
<UserInfo :user-id="user.userId" :display-name="user.displayName" :avatar-size="40">
|
||||
<template #meta>
|
||||
<div class="user-id muted">@{{ user.userId }}</div>
|
||||
</template>
|
||||
</NcButton>
|
||||
</UserInfo>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-joined">
|
||||
<NcDateTime :timestamp="user.createdAt * 1000" />
|
||||
</div>
|
||||
<div class="col-posts">
|
||||
<div class="post-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ user.threadCount }}</span>
|
||||
<span class="stat-label muted">threads</span>
|
||||
</div>
|
||||
<div class="stat-divider">/</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ user.postCount }}</span>
|
||||
<span class="stat-label muted">posts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-status">
|
||||
<span v-if="user.isDeleted" class="status-badge status-deleted">
|
||||
{{ strings.deleted }}
|
||||
</span>
|
||||
<span v-else class="status-badge status-active">
|
||||
{{ strings.active }}
|
||||
</span>
|
||||
<div class="col-roles">
|
||||
<div v-if="editingUserId === user.userId" class="roles-editor">
|
||||
<NcSelect
|
||||
v-model="editingRoles"
|
||||
:options="roleOptions"
|
||||
:placeholder="strings.selectRoles"
|
||||
:multiple="true"
|
||||
label="name"
|
||||
track-by="id"
|
||||
input-label="name"
|
||||
class="roles-select"
|
||||
/>
|
||||
<div class="edit-actions">
|
||||
<NcButton
|
||||
@click="cancelEdit"
|
||||
:aria-label="strings.cancel"
|
||||
:title="strings.cancel"
|
||||
>
|
||||
<template #icon>
|
||||
<CloseIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton
|
||||
variant="primary"
|
||||
@click="saveRoles(user.userId)"
|
||||
:aria-label="strings.save"
|
||||
:title="strings.save"
|
||||
>
|
||||
<template #icon>
|
||||
<CheckIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="roles-display">
|
||||
<div class="roles-list">
|
||||
<span
|
||||
v-for="roleId in user.roles"
|
||||
:key="roleId"
|
||||
class="role-badge"
|
||||
:class="getRoleBadgeClass(roleId)"
|
||||
>
|
||||
{{ getRoleName(roleId) }}
|
||||
</span>
|
||||
<span v-if="user.roles.length === 0" class="muted">{{ strings.noRoles }}</span>
|
||||
</div>
|
||||
<NcButton
|
||||
@click="startEdit(user.userId, user.roles)"
|
||||
:aria-label="strings.edit"
|
||||
:title="strings.edit"
|
||||
>
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-joined">
|
||||
<NcDateTime :timestamp="user.createdAt * 1000" />
|
||||
</div>
|
||||
|
||||
<div class="col-status">
|
||||
<span v-if="user.isDeleted" class="status-badge status-deleted">
|
||||
{{ strings.deleted }}
|
||||
</span>
|
||||
<span v-else class="status-badge status-active">
|
||||
{{ strings.active }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<NcEmptyContent
|
||||
v-else
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
/>
|
||||
</div>
|
||||
<!-- Empty state -->
|
||||
<NcEmptyContent
|
||||
v-else
|
||||
:title="strings.emptyTitle"
|
||||
:description="strings.emptyDesc"
|
||||
class="mt-16"
|
||||
/>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -153,6 +159,7 @@ import UserInfo from '@/components/UserInfo.vue'
|
||||
import PencilIcon from '@icons/Pencil.vue'
|
||||
import CheckIcon from '@icons/Check.vue'
|
||||
import CloseIcon from '@icons/Close.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import { ocs } from '@/axios'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import type { Role } from '@/types'
|
||||
@@ -186,6 +193,7 @@ export default defineComponent({
|
||||
PencilIcon,
|
||||
CheckIcon,
|
||||
CloseIcon,
|
||||
PageWrapper,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user