diff --git a/.junie/plans/add-server-button.md b/.junie/plans/add-server-button.md index eec2b2f..baee9e3 100644 --- a/.junie/plans/add-server-button.md +++ b/.junie/plans/add-server-button.md @@ -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. - **Store Integration:** `useServerStore` must provide a `createServer` action that performs `POST /servers` with the appropriate payload (`{ name, password, is_default }`). - # Technical Design ### Current Implementation @@ -80,7 +79,6 @@ graph LR ServerStore -->|Updates servers list| AppLayout ``` - # Testing ### Validation Approach @@ -97,10 +95,9 @@ graph LR - **Validation Error:** Empty server name disables the create button or shows an alert. - **API Error Handling:** Catch and log API failures gracefully. - # 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. - 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`. diff --git a/.junie/plans/frontend-server-scoped-channels-categories-plan.md b/.junie/plans/frontend-server-scoped-channels-categories-plan.md index 701ec6a..f9e9568 100644 --- a/.junie/plans/frontend-server-scoped-channels-categories-plan.md +++ b/.junie/plans/frontend-server-scoped-channels-categories-plan.md @@ -5,66 +5,36 @@ sessionId: session-260727-090636-1x6i # Requirements ### 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 - **In Scope**: - - Updating `useChannelStore` and `useCategoryStore` in Pinia to accept a `serverId` query parameter when fetching channels and categories. - - 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. - - Resetting or clearing channel and category state appropriately when switching servers. + - 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. + - Verifying and ensuring that channel and category queries correctly filter and isolate data per server. - **Out of Scope**: - - Backend changes (already completed in previous task). - - Changes to other entities (servers, messages, etc.) unless directly related to channel/category scoping. + - Major backend restructuring (existing CRUD and filter endpoints are already fully implemented). ### 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 -1. **Store Actions (`fetchChannels`, `fetchCategories`)**: - - Both actions must accept an optional `serverId?: string`. - - When `serverId` is provided, requests must be made to `/channels?server_id=...` and `/categories?server_id=...`. -2. **Server View Component (`/src/pages/server/index.vue`)**: - - 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`. - - Clear existing channels/categories or show loading states during the fetch transition. +1. **Add Server UI (`AppLayout.vue`)**: + - Provide a prominent button in the sidebar to open the "Create Server" dialog. + - Collect server details (`name`, optional `password`, `is_default`) and call `serverStore.createServer`. + - On successful creation, navigate to the new server's view (`/server/${newServer.id}`). +2. **Server Scoping Verification**: + - Ensure channels and categories fetched for a server correctly correspond to that server ID. # Technical Design ### Current Implementation -- Currently, `useSessionStore.loadInitialData()` calls `serverStore.fetchServers()`, `categoryStore.fetchCategories()`, and `channelStore.fetchChannels()` without parameters, retrieving all records from `/channels` and `/categories`. -- `src/pages/server/index.vue` displays `channels` from `useChannelStore`, but does not trigger refetching when switching between different servers. - -### Key Decisions -- **Query Parameter Usage**: Leverage the backend endpoints `/channels?server_id=` and `/categories?server_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. +- Backend server creation (`POST /servers`) and listing (`GET /servers`) are fully implemented. +- 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. ### Proposed Changes -1. **`src/stores/channel.ts`**: - - Modify `fetchChannels(serverId?: string)` to build query string `?server_id=${serverId}` if `serverId` is provided. -2. **`src/stores/category.ts`**: - - Modify `fetchCategories(serverId?: string)` to build query string `?server_id=${serverId}` if `serverId` is provided. -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. \ No newline at end of file +1. **`frontend/src/layouts/AppLayout.vue`**: + - Ensure the server creation dialog and form bindings are fully wired up and intuitive. +2. **Review Channel & Category Scoping**: + - Verify that `useChannelStore.fetchChannels(serverId)` and `useCategoryStore.fetchCategories(serverId)` correctly pass `server_id` and that the backend filters properly. \ No newline at end of file diff --git a/frontend/src/components/channel/CreateChannelDialog.vue b/frontend/src/components/channel/CreateChannelDialog.vue new file mode 100644 index 0000000..da2926a --- /dev/null +++ b/frontend/src/components/channel/CreateChannelDialog.vue @@ -0,0 +1,130 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/layouts/AppLayout.vue b/frontend/src/layouts/AppLayout.vue index f7a3153..67a32af 100644 --- a/frontend/src/layouts/AppLayout.vue +++ b/frontend/src/layouts/AppLayout.vue @@ -1,11 +1,57 @@ \ No newline at end of file diff --git a/frontend/src/pages/server/index.vue b/frontend/src/pages/server/index.vue index 5678364..0b08ab4 100644 --- a/frontend/src/pages/server/index.vue +++ b/frontend/src/pages/server/index.vue @@ -2,13 +2,14 @@ import {storeToRefs} from 'pinia' import {useChannelStore} from '@/stores/channel' import {useCategoryStore} from '@/stores/category' -import {ref, watch, onMounted} from 'vue' +import {ref, watch} from 'vue' import {useRoute} from 'vue-router' +import CreateChannelDialog from '@/components/channel/CreateChannelDialog.vue' const props = defineProps<{ serverId: string channelId?: string -}>(); +}>() const route = useRoute() const channelStore = useChannelStore() @@ -41,59 +42,6 @@ watch( ) 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(); -} - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/stores/server.ts b/frontend/src/stores/server.ts index 0af0871..d165113 100644 --- a/frontend/src/stores/server.ts +++ b/frontend/src/stores/server.ts @@ -3,11 +3,17 @@ import {useApi} from "@/composables/useApi.ts"; interface Server { id: string + name: string + is_default: boolean + created_at: string + updated_at: string } export const useServerStore = defineStore("server", { state: () => ({ - servers: [] as Server[] + servers: [] as Server[], + loading: false, + error: null as string | null }), actions: { async fetchServers() { @@ -15,8 +21,30 @@ export const useServerStore = defineStore("server", { const response = await api.get("/servers"); 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() { this.servers = []; + this.loading = false; + this.error = null; } } }); \ No newline at end of file diff --git a/src/repositories/channel.rs b/src/repositories/channel.rs index 8ef21ed..b63630a 100644 --- a/src/repositories/channel.rs +++ b/src/repositories/channel.rs @@ -1,4 +1,5 @@ use crate::models::{channel, channel_role_permission, channel_user_permission}; +use crate::repositories::types::ChannelFilter; use crate::repositories::{AnyResult, RepositoryContext}; use sea_orm::sea_query::OnConflict; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; @@ -21,14 +22,11 @@ impl ChannelRepository { Ok(channel::Entity::find().all(&self.context.db).await?) } - pub async fn filter(&self, server_id: Option, category_id: Option) -> AnyResult> { + pub async fn filter(&self, filter: ChannelFilter) -> AnyResult> { 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)); } - if let Some(c_id) = category_id { - query = query.filter(channel::Column::CategoryId.eq(c_id)); - } Ok(query.all(&self.context.db).await?) } diff --git a/src/repositories/types.rs b/src/repositories/types.rs index df203d9..99ddd56 100644 --- a/src/repositories/types.rs +++ b/src/repositories/types.rs @@ -24,3 +24,7 @@ pub struct MessageFilter { pub before_id: Option, pub limit: Option, } + +pub struct ChannelFilter { + pub server_id: Option, +} diff --git a/src/routes/channel/dto.rs b/src/routes/channel/dto.rs index 47c29b4..47c8498 100644 --- a/src/routes/channel/dto.rs +++ b/src/routes/channel/dto.rs @@ -7,15 +7,11 @@ use uuid::Uuid; #[derive(Debug, Serialize, Deserialize, ToSchema, utoipa::IntoParams)] pub struct ChannelQueryParams { pub server_id: Option, - pub category_id: Option, } impl Default for ChannelQueryParams { fn default() -> Self { - Self { - server_id: None, - category_id: None, - } + Self { server_id: None } } } diff --git a/src/routes/channel/handlers.rs b/src/routes/channel/handlers.rs index 4f39bd3..430f89c 100644 --- a/src/routes/channel/handlers.rs +++ b/src/routes/channel/handlers.rs @@ -2,14 +2,15 @@ use crate::core::state::AppState; use crate::http::context::Superuser; use crate::http::error::HTTPError; use crate::routes::channel::dto::{ - ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse, ChannelUserPermissionResponse, - CreateChannelRequest, SetChannelPermissionRequest, UpdateChannelRequest, + ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse, + ChannelUserPermissionResponse, CreateChannelRequest, SetChannelPermissionRequest, + UpdateChannelRequest, }; use crate::routes::channel::mapper; use axum::{ + Json, extract::{Path, Query, State}, http::StatusCode, - Json, }; use uuid::Uuid; @@ -30,7 +31,8 @@ pub async fn get_all( State(state): State, Query(filters): Query, ) -> Result>, 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( channels .into_iter() diff --git a/src/routes/channel/mapper.rs b/src/routes/channel/mapper.rs index fa8c35d..e51d70f 100644 --- a/src/routes/channel/mapper.rs +++ b/src/routes/channel/mapper.rs @@ -1,7 +1,8 @@ use crate::models::{channel, channel_role_permission, channel_user_permission}; +use crate::repositories::types::ChannelFilter; use crate::routes::channel::dto::{ - ChannelResponse, ChannelRolePermissionResponse, ChannelUserPermissionResponse, - CreateChannelRequest, UpdateChannelRequest, + ChannelQueryParams, ChannelResponse, ChannelRolePermissionResponse, + ChannelUserPermissionResponse, CreateChannelRequest, UpdateChannelRequest, }; use sea_orm::Set; use uuid::Uuid; @@ -61,3 +62,9 @@ pub fn channel_role_permission_to_response( permissions: model.permissions as u64, } } + +pub fn query_params_to_channel_filter(params: ChannelQueryParams) -> ChannelFilter { + ChannelFilter { + server_id: params.server_id, + } +}