mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-18 01:28:58 +00:00
feat: improve category/header sort order
This commit is contained in:
@@ -192,4 +192,32 @@ class CatHeaderController extends OCSController {
|
||||
return new DataResponse(['error' => 'Failed to delete category header'], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder category headers
|
||||
*
|
||||
* @param list<array{id: int, sortOrder: int}> $headers Array of headers with new sort orders
|
||||
* @return DataResponse<Http::STATUS_OK, array{success: bool}, array{}>
|
||||
*
|
||||
* 200: Headers reordered successfully
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
#[RequirePermission('canEditCategories')]
|
||||
#[ApiRoute(verb: 'POST', url: '/api/headers/reorder')]
|
||||
public function reorder(array $headers): DataResponse {
|
||||
try {
|
||||
foreach ($headers as $headerData) {
|
||||
$header = $this->catHeaderMapper->find($headerData['id']);
|
||||
$header->setSortOrder($headerData['sortOrder']);
|
||||
$this->catHeaderMapper->update($header);
|
||||
}
|
||||
|
||||
return new DataResponse(['success' => true]);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new DataResponse(['error' => 'Header not found'], Http::STATUS_NOT_FOUND);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Error reordering headers: ' . $e->getMessage());
|
||||
return new DataResponse(['error' => 'Failed to reorder headers'], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,4 +353,32 @@ class CategoryController extends OCSController {
|
||||
return new DataResponse(['error' => 'Failed to update permissions'], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder categories
|
||||
*
|
||||
* @param list<array{id: int, sortOrder: int}> $categories Array of categories with new sort orders
|
||||
* @return DataResponse<Http::STATUS_OK, array{success: bool}, array{}>
|
||||
*
|
||||
* 200: Categories reordered successfully
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
#[RequirePermission('canEditCategories')]
|
||||
#[ApiRoute(verb: 'POST', url: '/api/categories/reorder')]
|
||||
public function reorder(array $categories): DataResponse {
|
||||
try {
|
||||
foreach ($categories as $categoryData) {
|
||||
$category = $this->categoryMapper->find($categoryData['id']);
|
||||
$category->setSortOrder($categoryData['sortOrder']);
|
||||
$this->categoryMapper->update($category);
|
||||
}
|
||||
|
||||
return new DataResponse(['success' => true]);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new DataResponse(['error' => 'Category not found'], Http::STATUS_NOT_FOUND);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Error reordering categories: ' . $e->getMessage());
|
||||
return new DataResponse(['error' => 'Failed to reorder categories'], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
266
openapi.json
266
openapi.json
@@ -1915,6 +1915,139 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/apps/forum/api/headers/reorder": {
|
||||
"post": {
|
||||
"operationId": "cat_header-reorder",
|
||||
"summary": "Reorder category headers",
|
||||
"tags": [
|
||||
"cat_header"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"headers"
|
||||
],
|
||||
"properties": {
|
||||
"headers": {
|
||||
"type": "array",
|
||||
"description": "Array of headers with new sort orders",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"sortOrder"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"sortOrder": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Headers reordered successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"success"
|
||||
],
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Current user is not logged in",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/apps/forum/api/categories": {
|
||||
"get": {
|
||||
"operationId": "category-index",
|
||||
@@ -3064,6 +3197,139 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/apps/forum/api/categories/reorder": {
|
||||
"post": {
|
||||
"operationId": "category-reorder",
|
||||
"summary": "Reorder categories",
|
||||
"tags": [
|
||||
"category"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"categories"
|
||||
],
|
||||
"properties": {
|
||||
"categories": {
|
||||
"type": "array",
|
||||
"description": "Array of categories with new sort orders",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"sortOrder"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"sortOrder": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Categories reordered successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"success"
|
||||
],
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Current user is not logged in",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/apps/forum/api/users": {
|
||||
"get": {
|
||||
"operationId": "forum_user-index",
|
||||
|
||||
@@ -86,16 +86,6 @@
|
||||
:rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<NcTextField
|
||||
v-model.number="formData.sortOrder"
|
||||
:label="strings.sortOrder"
|
||||
:placeholder="strings.sortOrderPlaceholder"
|
||||
type="number"
|
||||
/>
|
||||
<p class="help-text muted">{{ strings.sortOrderHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -173,16 +163,6 @@
|
||||
: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>
|
||||
@@ -249,7 +229,6 @@ export default defineComponent({
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
sortOrder: 0,
|
||||
},
|
||||
headerDialog: {
|
||||
show: false,
|
||||
@@ -258,7 +237,6 @@ export default defineComponent({
|
||||
id: null as number | null,
|
||||
name: '',
|
||||
description: '',
|
||||
sortOrder: 0,
|
||||
},
|
||||
|
||||
strings: {
|
||||
@@ -376,7 +354,6 @@ export default defineComponent({
|
||||
this.formData.name = category.name
|
||||
this.formData.slug = category.slug
|
||||
this.formData.description = category.description || ''
|
||||
this.formData.sortOrder = category.sortOrder || 0
|
||||
|
||||
// Set selectedHeader based on headerId
|
||||
const header = this.headers.find((h) => h.id === category.headerId)
|
||||
@@ -439,7 +416,6 @@ export default defineComponent({
|
||||
name: this.formData.name.trim(),
|
||||
slug: this.formData.slug.trim(),
|
||||
description: this.formData.description.trim() || null,
|
||||
sortOrder: this.formData.sortOrder,
|
||||
}
|
||||
|
||||
let categoryId: number
|
||||
@@ -498,7 +474,6 @@ export default defineComponent({
|
||||
this.headerDialog.id = null
|
||||
this.headerDialog.name = ''
|
||||
this.headerDialog.description = ''
|
||||
this.headerDialog.sortOrder = 0
|
||||
},
|
||||
|
||||
editHeader(): void {
|
||||
@@ -512,7 +487,6 @@ export default defineComponent({
|
||||
this.headerDialog.id = header.id
|
||||
this.headerDialog.name = header.name
|
||||
this.headerDialog.description = header.description || ''
|
||||
this.headerDialog.sortOrder = header.sortOrder || 0
|
||||
},
|
||||
|
||||
async saveHeader(): Promise<void> {
|
||||
@@ -524,7 +498,6 @@ export default defineComponent({
|
||||
const headerData = {
|
||||
name: this.headerDialog.name.trim(),
|
||||
description: this.headerDialog.description.trim() || null,
|
||||
sortOrder: this.headerDialog.sortOrder,
|
||||
}
|
||||
|
||||
let headerId: number
|
||||
@@ -541,7 +514,6 @@ export default defineComponent({
|
||||
...this.headers[index],
|
||||
name: headerData.name,
|
||||
description: headerData.description,
|
||||
sortOrder: headerData.sortOrder,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -44,13 +44,24 @@
|
||||
</div>
|
||||
|
||||
<div v-if="categoryHeaders.length > 0" class="headers-table">
|
||||
<div v-for="header in categoryHeaders" :key="`header-manage-${header.id}`" class="header-manage-row">
|
||||
<div v-for="(header, index) in categoryHeaders" :key="`header-manage-${header.id}`" class="header-manage-row">
|
||||
<div class="header-sort-buttons">
|
||||
<NcButton v-if="index > 0" type="tertiary" @click="moveHeaderUp(index)" :aria-label="strings.moveUp" :title="strings.moveUp">
|
||||
<template #icon>
|
||||
<ChevronUpIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton v-if="index < categoryHeaders.length - 1" type="tertiary" @click="moveHeaderDown(index)"
|
||||
:aria-label="strings.moveDown" :title="strings.moveDown">
|
||||
<template #icon>
|
||||
<ChevronDownIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="header-info">
|
||||
<div class="header-name">{{ header.name }}</div>
|
||||
<div v-if="header.description" class="header-desc muted">{{ header.description }}</div>
|
||||
<div class="header-meta muted">
|
||||
<span>Sort: {{ header.sortOrder || 0 }}</span>
|
||||
<span>•</span>
|
||||
<span>{{ (header.categories?.length || 0) }} {{ strings.categoriesCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,11 +72,7 @@
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
type="error"
|
||||
:disabled="categoryHeaders.length <= 1"
|
||||
@click="confirmDeleteHeader(header)"
|
||||
>
|
||||
<NcButton type="error" :disabled="categoryHeaders.length <= 1" @click="confirmDeleteHeader(header)">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
@@ -91,48 +98,58 @@
|
||||
<span v-if="header.description" class="muted">{{ header.description }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="header.categories && header.categories.length > 0" class="categories-table">
|
||||
<div v-for="category in header.categories" :key="category.id" class="category-row">
|
||||
<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 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" type="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" type="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 type="error" @click="confirmDelete(category)">
|
||||
<template #icon>
|
||||
<DeleteIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.delete }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="category-actions">
|
||||
<NcButton @click="editCategory(category.id)">
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
{{ strings.edit }}
|
||||
</NcButton>
|
||||
<NcButton type="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>
|
||||
</template>
|
||||
<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"
|
||||
>
|
||||
<NcDialog v-if="deleteDialog.show" :name="strings.deleteDialogTitle" @close="deleteDialog.show = false">
|
||||
<div class="delete-dialog-content">
|
||||
<p>{{ strings.deleteConfirmMessage(deleteDialog.category?.name || '') }}</p>
|
||||
|
||||
@@ -145,34 +162,19 @@
|
||||
<h4>{{ strings.whatToDoWithThreads }}</h4>
|
||||
|
||||
<div class="radio-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="deleteDialog.action"
|
||||
value="migrate"
|
||||
type="radio"
|
||||
name="delete-action"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
<NcCheckboxRadioSwitch v-model="deleteDialog.action" value="delete" type="radio" name="delete-action">
|
||||
{{ strings.softDeleteThreads }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="help-text muted">{{ strings.softDeleteHelp }}</p>
|
||||
@@ -184,48 +186,31 @@
|
||||
<NcButton @click="deleteDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
type="error"
|
||||
:disabled="deleteDialog.action === 'migrate' && !selectedTargetCategory"
|
||||
@click="executeDelete"
|
||||
>
|
||||
<NcButton type="error" :disabled="deleteDialog.action === 'migrate' && !selectedTargetCategory"
|
||||
@click="executeDelete">
|
||||
{{ strings.deleteCategory }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
|
||||
<!-- Header Edit/Create Dialog -->
|
||||
<NcDialog
|
||||
v-if="headerDialog.show"
|
||||
<NcDialog v-if="headerDialog.show"
|
||||
:name="headerDialog.isEditing ? strings.editHeaderTitle : strings.createHeaderTitle"
|
||||
@close="headerDialog.show = false"
|
||||
>
|
||||
@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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<NcTextField v-model.number="headerDialog.sortOrder" :label="strings.headerSortOrder"
|
||||
:placeholder="strings.sortOrderPlaceholder" type="number" />
|
||||
<p class="help-text muted">{{ strings.sortOrderHelp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,11 +219,7 @@
|
||||
<NcButton @click="headerDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
type="primary"
|
||||
:disabled="!headerDialog.name.trim()"
|
||||
@click="saveHeader"
|
||||
>
|
||||
<NcButton type="primary" :disabled="!headerDialog.name.trim()" @click="saveHeader">
|
||||
<template v-if="headerDialog.submitting" #icon>
|
||||
<NcLoadingIcon :size="20" />
|
||||
</template>
|
||||
@@ -248,11 +229,7 @@
|
||||
</NcDialog>
|
||||
|
||||
<!-- Header Delete Confirmation Dialog -->
|
||||
<NcDialog
|
||||
v-if="deleteHeaderDialog.show"
|
||||
:name="strings.deleteHeaderTitle"
|
||||
@close="deleteHeaderDialog.show = false"
|
||||
>
|
||||
<NcDialog v-if="deleteHeaderDialog.show" :name="strings.deleteHeaderTitle" @close="deleteHeaderDialog.show = false">
|
||||
<div class="delete-dialog-content">
|
||||
<p>{{ strings.deleteHeaderMessage(deleteHeaderDialog.header?.name || '') }}</p>
|
||||
|
||||
@@ -265,34 +242,21 @@
|
||||
<h4>{{ strings.whatToDoWithCategories }}</h4>
|
||||
|
||||
<div class="radio-group">
|
||||
<NcCheckboxRadioSwitch
|
||||
v-model="deleteHeaderDialog.action"
|
||||
value="migrate"
|
||||
type="radio"
|
||||
name="delete-header-action"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
<NcCheckboxRadioSwitch v-model="deleteHeaderDialog.action" value="delete" type="radio"
|
||||
name="delete-header-action">
|
||||
{{ strings.deleteCategories }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<p class="help-text muted">{{ strings.deleteCategoriesHelp }}</p>
|
||||
@@ -304,11 +268,8 @@
|
||||
<NcButton @click="deleteHeaderDialog.show = false">
|
||||
{{ strings.cancel }}
|
||||
</NcButton>
|
||||
<NcButton
|
||||
type="error"
|
||||
:disabled="deleteHeaderDialog.action === 'migrate' && !selectedTargetHeader"
|
||||
@click="executeDeleteHeader"
|
||||
>
|
||||
<NcButton type="error" :disabled="deleteHeaderDialog.action === 'migrate' && !selectedTargetHeader"
|
||||
@click="executeDeleteHeader">
|
||||
{{ strings.deleteHeader }}
|
||||
</NcButton>
|
||||
</template>
|
||||
@@ -325,6 +286,8 @@ import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import NcSelect from '@nextcloud/vue/components/NcSelect'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import ChevronUpIcon from '@icons/ChevronUp.vue'
|
||||
import ChevronDownIcon from '@icons/ChevronDown.vue'
|
||||
import NcTextArea from '@nextcloud/vue/components/NcTextArea'
|
||||
import PlusIcon from '@icons/Plus.vue'
|
||||
import PencilIcon from '@icons/Pencil.vue'
|
||||
@@ -349,6 +312,8 @@ export default defineComponent({
|
||||
PencilIcon,
|
||||
DeleteIcon,
|
||||
InformationIcon,
|
||||
ChevronUpIcon,
|
||||
ChevronDownIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -430,6 +395,8 @@ export default defineComponent({
|
||||
deleteCategoriesHelp: t('forum', 'All categories and their threads will be permanently deleted'),
|
||||
selectTargetHeader: t('forum', 'Select target header'),
|
||||
selectHeader: t('forum', '-- Select a header --'),
|
||||
moveUp: t('forum', 'Move up'),
|
||||
moveDown: t('forum', 'Move down'),
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -613,6 +580,85 @@ export default defineComponent({
|
||||
// TODO: Show error notification
|
||||
}
|
||||
},
|
||||
|
||||
async moveHeaderUp(index: number): Promise<void> {
|
||||
if (index <= 0) return
|
||||
|
||||
// Update sort orders on backend
|
||||
await this.updateHeaderSortOrders(index, -1)
|
||||
},
|
||||
|
||||
async moveHeaderDown(index: number): Promise<void> {
|
||||
if (index >= this.categoryHeaders.length - 1) return
|
||||
|
||||
// Update sort orders on backend
|
||||
await this.updateHeaderSortOrders(index, 1)
|
||||
},
|
||||
|
||||
async updateHeaderSortOrders(index: number, amount: number): Promise<void> {
|
||||
// Swap positions locally
|
||||
const temp = this.categoryHeaders[index]
|
||||
this.categoryHeaders[index] = this.categoryHeaders[index + amount]
|
||||
this.categoryHeaders[index + amount] = temp
|
||||
|
||||
try {
|
||||
// Build array of header IDs in their current order
|
||||
const sortOrders = this.categoryHeaders.map((header, idx) => ({
|
||||
id: header.id,
|
||||
sortOrder: idx,
|
||||
}))
|
||||
|
||||
await ocs.post('/headers/reorder', { headers: sortOrders })
|
||||
} catch (e) {
|
||||
console.error('Failed to update header sort orders', e)
|
||||
// Revert the swap on error
|
||||
const revertTemp = this.categoryHeaders[index + amount]
|
||||
this.categoryHeaders[index + amount] = this.categoryHeaders[index]
|
||||
this.categoryHeaders[index] = revertTemp
|
||||
}
|
||||
},
|
||||
|
||||
async moveCategoryUp(headerId: number, index: number): Promise<void> {
|
||||
const header = this.categoryHeaders.find((h) => h.id === headerId)
|
||||
if (!header || !header.categories || index <= 0) return
|
||||
|
||||
// Update sort orders on backend
|
||||
await this.updateCategorySortOrders(headerId, index, -1)
|
||||
},
|
||||
|
||||
async moveCategoryDown(headerId: number, index: number): Promise<void> {
|
||||
const header = this.categoryHeaders.find((h) => h.id === headerId)
|
||||
if (!header || !header.categories || index >= header.categories.length - 1) return
|
||||
|
||||
// Update sort orders on backend
|
||||
await this.updateCategorySortOrders(headerId, index, 1)
|
||||
},
|
||||
|
||||
async updateCategorySortOrders(headerId: number, index: number, amount: number): Promise<void> {
|
||||
const header = this.categoryHeaders.find((h) => h.id === headerId)
|
||||
if (!header || !header.categories) return
|
||||
|
||||
// Swap positions locally
|
||||
const temp = header.categories[index]
|
||||
header.categories[index] = header.categories[index + amount]
|
||||
header.categories[index + amount] = temp
|
||||
|
||||
try {
|
||||
// Build array of category IDs in their current order
|
||||
const sortOrders = header.categories.map((category, idx) => ({
|
||||
id: category.id,
|
||||
sortOrder: idx,
|
||||
}))
|
||||
|
||||
await ocs.post('/categories/reorder', { categories: sortOrders })
|
||||
} catch (e) {
|
||||
console.error('Failed to update category sort orders', e)
|
||||
// Revert the swap on error
|
||||
const revertTemp = header.categories[index + amount]
|
||||
header.categories[index + amount] = header.categories[index]
|
||||
header.categories[index] = revertTemp
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user