init
This commit is contained in:
@@ -30,7 +30,6 @@ The goal of this task is to add an "Add Server" button and creation functionalit
|
|||||||
- Cancel and Create buttons with loading state handling.
|
- Cancel and Create buttons with loading state handling.
|
||||||
- **Store Integration:** `useServerStore` must provide a `createServer` action that performs `POST /servers` with the appropriate payload (`{ name, password, is_default }`).
|
- **Store Integration:** `useServerStore` must provide a `createServer` action that performs `POST /servers` with the appropriate payload (`{ name, password, is_default }`).
|
||||||
|
|
||||||
|
|
||||||
# Technical Design
|
# Technical Design
|
||||||
|
|
||||||
### Current Implementation
|
### Current Implementation
|
||||||
@@ -80,7 +79,6 @@ graph LR
|
|||||||
ServerStore -->|Updates servers list| AppLayout
|
ServerStore -->|Updates servers list| AppLayout
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
|
|
||||||
### Validation Approach
|
### Validation Approach
|
||||||
@@ -97,10 +95,9 @@ graph LR
|
|||||||
- **Validation Error:** Empty server name disables the create button or shows an alert.
|
- **Validation Error:** Empty server name disables the create button or shows an alert.
|
||||||
- **API Error Handling:** Catch and log API failures gracefully.
|
- **API Error Handling:** Catch and log API failures gracefully.
|
||||||
|
|
||||||
|
|
||||||
# Delivery Steps
|
# Delivery Steps
|
||||||
|
|
||||||
### Step 1: Extend server Pinia store with createServer action
|
### * Step 1: Extend server Pinia store with createServer action
|
||||||
Extend the server Pinia store with createServer action.
|
Extend the server Pinia store with createServer action.
|
||||||
- Update `frontend/src/stores/server.ts` to include `createServer(payload: { name: string; password?: string | null; is_default?: boolean })`.
|
- Update `frontend/src/stores/server.ts` to include `createServer(payload: { name: string; password?: string | null; is_default?: boolean })`.
|
||||||
- Handle error states, API POST requests to `/servers`, and state updates to push the newly created server to `this.servers`.
|
- Handle error states, API POST requests to `/servers`, and state updates to push the newly created server to `this.servers`.
|
||||||
|
|||||||
@@ -5,66 +5,36 @@ sessionId: session-260727-090636-1x6i
|
|||||||
# Requirements
|
# Requirements
|
||||||
|
|
||||||
### Overview & Goals
|
### Overview & Goals
|
||||||
The goal of this task is to adapt the frontend data-loading policy for channels and categories. Previously, all channels and categories were fetched globally once upon initial load. Now, whenever the user swaps servers (or enters a server view), channels and categories should be reloaded specifically for that active server using the backend query parameters (`server_id`) implemented in the previous backend task.
|
The goal of this task is to add an "Add Server" button and creation modal on the frontend (leveraging existing backend server creation endpoints), and investigate/address any server scoping issues with channels and categories when multiple servers exist.
|
||||||
|
|
||||||
### Scope
|
### Scope
|
||||||
- **In Scope**:
|
- **In Scope**:
|
||||||
- Updating `useChannelStore` and `useCategoryStore` in Pinia to accept a `serverId` query parameter when fetching channels and categories.
|
- Adding an "Add Server" button in the frontend layout/navigation (e.g. in `AppLayout.vue` or sidebar) with a creation dialog supporting server name, password, and default flags.
|
||||||
- Updating `src/pages/server/index.vue` (and/or router navigation / lifecycle hooks) to trigger fetching of channels and categories filtered by the current `serverId` whenever the active server changes.
|
- Verifying and ensuring that channel and category queries correctly filter and isolate data per server.
|
||||||
- Resetting or clearing channel and category state appropriately when switching servers.
|
|
||||||
- **Out of Scope**:
|
- **Out of Scope**:
|
||||||
- Backend changes (already completed in previous task).
|
- Major backend restructuring (existing CRUD and filter endpoints are already fully implemented).
|
||||||
- Changes to other entities (servers, messages, etc.) unless directly related to channel/category scoping.
|
|
||||||
|
|
||||||
### User Stories
|
### User Stories
|
||||||
- **As a user**, when I switch from one server to another, I want the channels and categories list to be reloaded automatically for the newly selected server so that I only see relevant content.
|
- **As a user**, I want an "Add Server" button in the UI so that I can easily create a new server.
|
||||||
|
- **As a user**, when I switch between servers, I want to ensure that channels and categories are strictly scoped to the active server.
|
||||||
|
|
||||||
### Functional Requirements
|
### Functional Requirements
|
||||||
1. **Store Actions (`fetchChannels`, `fetchCategories`)**:
|
1. **Add Server UI (`AppLayout.vue`)**:
|
||||||
- Both actions must accept an optional `serverId?: string`.
|
- Provide a prominent button in the sidebar to open the "Create Server" dialog.
|
||||||
- When `serverId` is provided, requests must be made to `/channels?server_id=...` and `/categories?server_id=...`.
|
- Collect server details (`name`, optional `password`, `is_default`) and call `serverStore.createServer`.
|
||||||
2. **Server View Component (`/src/pages/server/index.vue`)**:
|
- On successful creation, navigate to the new server's view (`/server/${newServer.id}`).
|
||||||
- On component mount or when `serverId` route parameter changes (e.g. via `watch(() => route.params.serverId, ...)`), trigger fetching of channels and categories for the new `serverId`.
|
2. **Server Scoping Verification**:
|
||||||
- Clear existing channels/categories or show loading states during the fetch transition.
|
- Ensure channels and categories fetched for a server correctly correspond to that server ID.
|
||||||
|
|
||||||
# Technical Design
|
# Technical Design
|
||||||
|
|
||||||
### Current Implementation
|
### Current Implementation
|
||||||
- Currently, `useSessionStore.loadInitialData()` calls `serverStore.fetchServers()`, `categoryStore.fetchCategories()`, and `channelStore.fetchChannels()` without parameters, retrieving all records from `/channels` and `/categories`.
|
- Backend server creation (`POST /servers`) and listing (`GET /servers`) are fully implemented.
|
||||||
- `src/pages/server/index.vue` displays `channels` from `useChannelStore`, but does not trigger refetching when switching between different servers.
|
- Frontend `useServerStore` has `createServer` and `fetchServers`.
|
||||||
|
- `AppLayout.vue` already includes a dialog structure for creating servers triggered by an `mdi-plus` icon in the navigation drawer.
|
||||||
### Key Decisions
|
|
||||||
- **Query Parameter Usage**: Leverage the backend endpoints `/channels?server_id=<id>` and `/categories?server_id=<id>` that were created previously.
|
|
||||||
- **Store & Lifecycle Integration**: Use Vue router route watchers in `src/pages/server/index.vue` (or a dedicated composable/watcher) to detect `serverId` changes and invoke store fetch actions.
|
|
||||||
|
|
||||||
### Proposed Changes
|
### Proposed Changes
|
||||||
1. **`src/stores/channel.ts`**:
|
1. **`frontend/src/layouts/AppLayout.vue`**:
|
||||||
- Modify `fetchChannels(serverId?: string)` to build query string `?server_id=${serverId}` if `serverId` is provided.
|
- Ensure the server creation dialog and form bindings are fully wired up and intuitive.
|
||||||
2. **`src/stores/category.ts`**:
|
2. **Review Channel & Category Scoping**:
|
||||||
- Modify `fetchCategories(serverId?: string)` to build query string `?server_id=${serverId}` if `serverId` is provided.
|
- Verify that `useChannelStore.fetchChannels(serverId)` and `useCategoryStore.fetchCategories(serverId)` correctly pass `server_id` and that the backend filters properly.
|
||||||
3. **`src/pages/server/index.vue`**:
|
|
||||||
- Add a watcher on `route.params.serverId` (or `serverId` prop).
|
|
||||||
- On mount and when `serverId` changes, call `channelStore.fetchChannels(serverId)` and `categoryStore.fetchCategories(serverId)`.
|
|
||||||
|
|
||||||
### File Structure
|
|
||||||
- **Modified Files**:
|
|
||||||
- `frontend/src/stores/channel.ts`
|
|
||||||
- `frontend/src/stores/category.ts`
|
|
||||||
- `frontend/src/pages/server/index.vue`
|
|
||||||
|
|
||||||
### Risks & Mitigations
|
|
||||||
- **Race conditions during rapid server switching**: Mitigated by handling loading states or canceling/ignoring stale promises if necessary, or simply allowing the latest fetch response to overwrite the store state.
|
|
||||||
|
|
||||||
# Delivery Steps
|
|
||||||
|
|
||||||
### ✓ Step 1: Update channel and category stores to support server-scoped fetching
|
|
||||||
Update Pinia stores for channels and categories to support filtered fetching by server_id.
|
|
||||||
- Update `useChannelStore.fetchChannels(serverId?: string)` to accept an optional `serverId` and pass it as a query parameter (`/channels?server_id=...`).
|
|
||||||
- Update `useCategoryStore.fetchCategories(serverId?: string)` to accept an optional `serverId` and pass it as a query parameter (`/categories?server_id=...`).
|
|
||||||
- Clear channels and categories when no server is selected or when swapping servers.
|
|
||||||
|
|
||||||
### ✓ Step 2: Integrate server-scoped data loading into server view and router navigation
|
|
||||||
Integrate server-scoped loading into the server view lifecycle and route changes.
|
|
||||||
- Update `/src/pages/server/index.vue` to watch `serverId` (or trigger on route parameter change / component mount) and fetch channels and categories specifically for the active `serverId`.
|
|
||||||
- Clear channels and categories when switching between servers or leaving the server view.
|
|
||||||
- Ensure proper loading states and error handling during server switching.
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref, watch} from 'vue'
|
||||||
|
import {useChannelStore} from '@/stores/channel'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
serverId: string
|
||||||
|
categoryId?: string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'created', channel: any): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const channelStore = useChannelStore()
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
name: '',
|
||||||
|
channel_type: 'text',
|
||||||
|
position: 0
|
||||||
|
})
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
const channelTypeOptions = [
|
||||||
|
{title: 'Text', value: 'text'},
|
||||||
|
{title: 'Voice', value: 'voice'},
|
||||||
|
{title: 'DM', value: 'dm'}
|
||||||
|
]
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
name: '',
|
||||||
|
channel_type: 'text',
|
||||||
|
position: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formData.value.name.trim()) {
|
||||||
|
alert('Channel name is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting.value = true
|
||||||
|
try {
|
||||||
|
const newChannel = await channelStore.createChannel({
|
||||||
|
name: formData.value.name,
|
||||||
|
channel_type: formData.value.channel_type,
|
||||||
|
server_id: props.serverId,
|
||||||
|
category_id: props.categoryId ?? null,
|
||||||
|
position: formData.value.position
|
||||||
|
})
|
||||||
|
emit('created', newChannel)
|
||||||
|
handleClose()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create channel:', error)
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog :model-value="modelValue" width="400" @update:model-value="handleClose">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Create Channel</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="mt-4 space-y-4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.name"
|
||||||
|
density="compact"
|
||||||
|
label="Channel Name"
|
||||||
|
outlined
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="formData.channel_type"
|
||||||
|
:items="channelTypeOptions"
|
||||||
|
density="compact"
|
||||||
|
label="Channel Type"
|
||||||
|
outlined
|
||||||
|
></v-select>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
variant="text"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
:disabled="!formData.name.trim()"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.space-y-4 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,57 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {storeToRefs} from 'pinia'
|
import {storeToRefs} from 'pinia'
|
||||||
import {useServerStore} from '@/stores/server'
|
import {useServerStore} from '@/stores/server'
|
||||||
|
import {ref} from 'vue'
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
|
||||||
const serverStore = useServerStore()
|
const serverStore = useServerStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const {servers} = storeToRefs(serverStore)
|
const {servers} = storeToRefs(serverStore)
|
||||||
console.log(servers.value)
|
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const formData = ref({
|
||||||
|
name: '',
|
||||||
|
password: '',
|
||||||
|
is_default: false
|
||||||
|
})
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
name: '',
|
||||||
|
password: '',
|
||||||
|
is_default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formData.value.name.trim()) {
|
||||||
|
alert('Server name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
const newServer = await serverStore.createServer({
|
||||||
|
name: formData.value.name,
|
||||||
|
password: formData.value.password || null,
|
||||||
|
is_default: formData.value.is_default
|
||||||
|
});
|
||||||
|
showDialog.value = false;
|
||||||
|
resetForm();
|
||||||
|
router.push(`/server/${newServer.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create server:', error);
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
showDialog.value = false;
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -49,12 +95,70 @@ console.log(servers.value)
|
|||||||
></v-avatar>
|
></v-avatar>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
class="d-block mx-auto mb-9"
|
||||||
|
color="grey-lighten-2"
|
||||||
|
icon="mdi-plus"
|
||||||
|
size="28"
|
||||||
|
variant="flat"
|
||||||
|
@click="showDialog = true"
|
||||||
|
></v-btn>
|
||||||
|
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
<router-view/>
|
<router-view/>
|
||||||
|
|
||||||
|
<v-dialog v-model="showDialog" width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Create Server</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="mt-4 space-y-4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.name"
|
||||||
|
density="compact"
|
||||||
|
label="Server Name"
|
||||||
|
outlined
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.password"
|
||||||
|
density="compact"
|
||||||
|
label="Password (optional)"
|
||||||
|
outlined
|
||||||
|
type="password"
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
></v-text-field>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
variant="text"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
:disabled="!formData.name.trim()"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.space-y-4 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -2,13 +2,14 @@
|
|||||||
import {storeToRefs} from 'pinia'
|
import {storeToRefs} from 'pinia'
|
||||||
import {useChannelStore} from '@/stores/channel'
|
import {useChannelStore} from '@/stores/channel'
|
||||||
import {useCategoryStore} from '@/stores/category'
|
import {useCategoryStore} from '@/stores/category'
|
||||||
import {ref, watch, onMounted} from 'vue'
|
import {ref, watch} from 'vue'
|
||||||
import {useRoute} from 'vue-router'
|
import {useRoute} from 'vue-router'
|
||||||
|
import CreateChannelDialog from '@/components/channel/CreateChannelDialog.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
serverId: string
|
serverId: string
|
||||||
channelId?: string
|
channelId?: string
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const channelStore = useChannelStore()
|
const channelStore = useChannelStore()
|
||||||
@@ -41,59 +42,6 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const formData = ref({
|
|
||||||
name: '',
|
|
||||||
channel_type: 'text',
|
|
||||||
server_id: null,
|
|
||||||
category_id: null,
|
|
||||||
position: 0
|
|
||||||
})
|
|
||||||
const isSubmitting = ref(false)
|
|
||||||
|
|
||||||
const channelTypeOptions = [
|
|
||||||
{title: 'Text', value: 'text'},
|
|
||||||
{title: 'Voice', value: 'voice'},
|
|
||||||
{title: 'DM', value: 'dm'}
|
|
||||||
]
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
formData.value = {
|
|
||||||
name: '',
|
|
||||||
channel_type: 'text',
|
|
||||||
server_id: null,
|
|
||||||
category_id: null,
|
|
||||||
position: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!formData.value.name.trim()) {
|
|
||||||
alert('Channel name is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSubmitting.value = true;
|
|
||||||
try {
|
|
||||||
await channelStore.createChannel({
|
|
||||||
name: formData.value.name,
|
|
||||||
channel_type: formData.value.channel_type,
|
|
||||||
server_id: formData.value.server_id,
|
|
||||||
category_id: formData.value.category_id,
|
|
||||||
position: formData.value.position
|
|
||||||
});
|
|
||||||
showDialog.value = false;
|
|
||||||
resetForm();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to create channel:', error);
|
|
||||||
} finally {
|
|
||||||
isSubmitting.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
showDialog.value = false;
|
|
||||||
resetForm();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -125,59 +73,12 @@ const handleCancel = () => {
|
|||||||
</v-list>
|
</v-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<v-dialog v-model="showDialog" width="400">
|
<CreateChannelDialog
|
||||||
<v-card>
|
v-model="showDialog"
|
||||||
<v-card-title>Create Channel</v-card-title>
|
:server-id="serverId"
|
||||||
<v-card-text>
|
/>
|
||||||
<div class="mt-4 space-y-4">
|
|
||||||
<v-text-field
|
|
||||||
v-model="formData.name"
|
|
||||||
density="compact"
|
|
||||||
label="Channel Name"
|
|
||||||
outlined
|
|
||||||
@keyup.enter="handleSubmit"
|
|
||||||
></v-text-field>
|
|
||||||
|
|
||||||
<v-select
|
|
||||||
v-model="formData.channel_type"
|
|
||||||
:items="channelTypeOptions"
|
|
||||||
density="compact"
|
|
||||||
label="Channel Type"
|
|
||||||
outlined
|
|
||||||
></v-select>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
variant="text"
|
|
||||||
@click="handleCancel"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
:disabled="!formData.name.trim()"
|
|
||||||
:loading="isSubmitting"
|
|
||||||
color="primary"
|
|
||||||
variant="tonal"
|
|
||||||
@click="handleSubmit"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
|
|
||||||
<v-main>
|
<v-main>
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</v-main>
|
</v-main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.space-y-4 {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -3,11 +3,17 @@ import {useApi} from "@/composables/useApi.ts";
|
|||||||
|
|
||||||
interface Server {
|
interface Server {
|
||||||
id: string
|
id: string
|
||||||
|
name: string
|
||||||
|
is_default: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useServerStore = defineStore("server", {
|
export const useServerStore = defineStore("server", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
servers: [] as Server[]
|
servers: [] as Server[],
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async fetchServers() {
|
async fetchServers() {
|
||||||
@@ -15,8 +21,30 @@ export const useServerStore = defineStore("server", {
|
|||||||
const response = await api.get("/servers");
|
const response = await api.get("/servers");
|
||||||
this.servers = await response.json();
|
this.servers = await response.json();
|
||||||
},
|
},
|
||||||
|
async createServer(payload: { name: string; password?: string | null; is_default?: boolean }) {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = null;
|
||||||
|
try {
|
||||||
|
const api = useApi();
|
||||||
|
const response = await api.post("/servers", payload);
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.message || 'Failed to create server');
|
||||||
|
}
|
||||||
|
const newServer = await response.json();
|
||||||
|
this.servers.push(newServer);
|
||||||
|
return newServer;
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.servers = [];
|
this.servers = [];
|
||||||
|
this.loading = false;
|
||||||
|
this.error = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::models::{channel, channel_role_permission, channel_user_permission};
|
use crate::models::{channel, channel_role_permission, channel_user_permission};
|
||||||
|
use crate::repositories::types::ChannelFilter;
|
||||||
use crate::repositories::{AnyResult, RepositoryContext};
|
use crate::repositories::{AnyResult, RepositoryContext};
|
||||||
use sea_orm::sea_query::OnConflict;
|
use sea_orm::sea_query::OnConflict;
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
@@ -21,14 +22,11 @@ impl ChannelRepository {
|
|||||||
Ok(channel::Entity::find().all(&self.context.db).await?)
|
Ok(channel::Entity::find().all(&self.context.db).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn filter(&self, server_id: Option<Uuid>, category_id: Option<Uuid>) -> AnyResult<Vec<channel::Model>> {
|
pub async fn filter(&self, filter: ChannelFilter) -> AnyResult<Vec<channel::Model>> {
|
||||||
let mut query = channel::Entity::find();
|
let mut query = channel::Entity::find();
|
||||||
if let Some(s_id) = server_id {
|
if let Some(s_id) = filter.server_id {
|
||||||
query = query.filter(channel::Column::ServerId.eq(s_id));
|
query = query.filter(channel::Column::ServerId.eq(s_id));
|
||||||
}
|
}
|
||||||
if let Some(c_id) = category_id {
|
|
||||||
query = query.filter(channel::Column::CategoryId.eq(c_id));
|
|
||||||
}
|
|
||||||
Ok(query.all(&self.context.db).await?)
|
Ok(query.all(&self.context.db).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ pub struct MessageFilter {
|
|||||||
pub before_id: Option<uuid::Uuid>,
|
pub before_id: Option<uuid::Uuid>,
|
||||||
pub limit: Option<u64>,
|
pub limit: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ChannelFilter {
|
||||||
|
pub server_id: Option<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,15 +7,11 @@ use uuid::Uuid;
|
|||||||
#[derive(Debug, Serialize, Deserialize, ToSchema, utoipa::IntoParams)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema, utoipa::IntoParams)]
|
||||||
pub struct ChannelQueryParams {
|
pub struct ChannelQueryParams {
|
||||||
pub server_id: Option<Uuid>,
|
pub server_id: Option<Uuid>,
|
||||||
pub category_id: Option<Uuid>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ChannelQueryParams {
|
impl Default for ChannelQueryParams {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self { server_id: None }
|
||||||
server_id: None,
|
|
||||||
category_id: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ use crate::core::state::AppState;
|
|||||||
use crate::http::context::Superuser;
|
use crate::http::context::Superuser;
|
||||||
use crate::http::error::HTTPError;
|
use crate::http::error::HTTPError;
|
||||||
use crate::routes::channel::dto::{
|
use crate::routes::channel::dto::{
|
||||||
ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse, ChannelUserPermissionResponse,
|
ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse,
|
||||||
CreateChannelRequest, SetChannelPermissionRequest, UpdateChannelRequest,
|
ChannelUserPermissionResponse, CreateChannelRequest, SetChannelPermissionRequest,
|
||||||
|
UpdateChannelRequest,
|
||||||
};
|
};
|
||||||
use crate::routes::channel::mapper;
|
use crate::routes::channel::mapper;
|
||||||
use axum::{
|
use axum::{
|
||||||
|
Json,
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
Json,
|
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@ pub async fn get_all(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(filters): Query<ChannelQueryParams>,
|
Query(filters): Query<ChannelQueryParams>,
|
||||||
) -> Result<Json<Vec<ChannelResponse>>, HTTPError> {
|
) -> Result<Json<Vec<ChannelResponse>>, HTTPError> {
|
||||||
let channels = state.repositories.channel.filter(filters.server_id, filters.category_id).await?;
|
let params = mapper::query_params_to_channel_filter(filters);
|
||||||
|
let channels = state.repositories.channel.filter(params).await?;
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
channels
|
channels
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::models::{channel, channel_role_permission, channel_user_permission};
|
use crate::models::{channel, channel_role_permission, channel_user_permission};
|
||||||
|
use crate::repositories::types::ChannelFilter;
|
||||||
use crate::routes::channel::dto::{
|
use crate::routes::channel::dto::{
|
||||||
ChannelResponse, ChannelRolePermissionResponse, ChannelUserPermissionResponse,
|
ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse,
|
||||||
CreateChannelRequest, UpdateChannelRequest,
|
ChannelUserPermissionResponse, CreateChannelRequest, UpdateChannelRequest,
|
||||||
};
|
};
|
||||||
use sea_orm::Set;
|
use sea_orm::Set;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -61,3 +62,9 @@ pub fn channel_role_permission_to_response(
|
|||||||
permissions: model.permissions as u64,
|
permissions: model.permissions as u64,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn query_params_to_channel_filter(params: ChannelQueryParams) -> ChannelFilter {
|
||||||
|
ChannelFilter {
|
||||||
|
server_id: params.server_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user