refactor(workspaces): normalize workspace bootstrap and edit flows
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { authFetch } from "./client";
|
import { authFetch } from "./client";
|
||||||
|
|
||||||
export interface Workspace {
|
export interface Workspace {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
owner?: string;
|
owner?: string;
|
||||||
@@ -16,9 +16,9 @@ export interface PaginatedResponse<T> {
|
|||||||
results: T[];
|
results: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceMembership {
|
export interface WorkspaceMembership {
|
||||||
id: string;
|
id: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -29,14 +29,27 @@ export interface WorkspaceMembership {
|
|||||||
role: 'owner' | 'admin' | 'member' | 'guest';
|
role: 'owner' | 'admin' | 'member' | 'guest';
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
joined_at?: string;
|
joined_at?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const fetchWorkspaces = async (params?: Record<string, string>): Promise<PaginatedResponse<Workspace>> => {
|
type QueryValue = string | number | boolean | undefined | null;
|
||||||
const query = params ? new URLSearchParams(params).toString() : '';
|
|
||||||
const url = `/api/workspaces/${query ? `?${query}` : ''}`;
|
const toQueryString = (params?: Record<string, QueryValue>) => {
|
||||||
const response = await authFetch(url);
|
if (!params) return "";
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
query.set(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return query.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchWorkspaces = async (params?: Record<string, QueryValue>): Promise<PaginatedResponse<Workspace>> => {
|
||||||
|
const query = toQueryString(params);
|
||||||
|
const url = `/api/workspaces/${query ? `?${query}` : ''}`;
|
||||||
|
const response = await authFetch(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to fetch workspaces");
|
throw new Error("Failed to fetch workspaces");
|
||||||
@@ -98,9 +111,9 @@ export const deleteWorkspace = async (id: string): Promise<void> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchWorkspaceMemberships = async (params?: Record<string, string>): Promise<PaginatedResponse<WorkspaceMembership>> => {
|
export const fetchWorkspaceMemberships = async (params?: Record<string, QueryValue>): Promise<PaginatedResponse<WorkspaceMembership>> => {
|
||||||
const queryParams = new URLSearchParams((params || {}));
|
const queryParams = toQueryString(params);
|
||||||
const response = await authFetch(`/api/workspace-memberships/?${queryParams.toString()}`);
|
const response = await authFetch(`/api/workspace-memberships/?${queryParams.toString()}`);
|
||||||
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch workspace memberships");
|
if (!response.ok) throw new Error("Failed to fetch workspace memberships");
|
||||||
|
|
||||||
|
|||||||
@@ -40,22 +40,22 @@ export const WorkspaceSelector: React.FC = () => {
|
|||||||
refreshWorkspacesList();
|
refreshWorkspacesList();
|
||||||
}) as EventListener;
|
}) as EventListener;
|
||||||
|
|
||||||
const handleWorkspaceCreated = ((e: CustomEvent) => {
|
const handleWorkspaceCreated = ((e: CustomEvent) => {
|
||||||
if (e.detail) {
|
if (e.detail?.id) {
|
||||||
setActiveWorkspace(e.detail);
|
setActiveWorkspace(e.detail);
|
||||||
}
|
}
|
||||||
refreshWorkspacesList();
|
refreshWorkspacesList();
|
||||||
}) as EventListener;
|
}) as EventListener;
|
||||||
|
|
||||||
const handleWorkspaceEdited = ((e: CustomEvent) => {
|
const handleWorkspaceEdited = ((e: CustomEvent) => {
|
||||||
// آپدیت نام کارتابل در نوبار در صورتی که کارتابل فعال ویرایش شده باشد
|
// آپدیت نام کارتابل در نوبار در صورتی که کارتابل فعال ویرایش شده باشد
|
||||||
if (activeWorkspace?.id === e.detail?.id) {
|
if (activeWorkspace?.id === e.detail?.id) {
|
||||||
setActiveWorkspace({
|
setActiveWorkspace({
|
||||||
...activeWorkspace,
|
...activeWorkspace,
|
||||||
name: e.detail.name,
|
name: e.detail.name,
|
||||||
description: e.detail.description
|
description: e.detail.description
|
||||||
});
|
} as Workspace);
|
||||||
}
|
}
|
||||||
refreshWorkspacesList();
|
refreshWorkspacesList();
|
||||||
}) as EventListener;
|
}) as EventListener;
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +1,47 @@
|
|||||||
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
|
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
|
||||||
import { getUserProfile } from '../api/users';
|
import { getUserProfile } from '../api/users';
|
||||||
import { fetchWorkspaces } from '../api/workspaces';
|
|
||||||
|
interface User {
|
||||||
interface User {
|
id: string;
|
||||||
id: string;
|
mobile: string;
|
||||||
phone_number: string;
|
first_name: string;
|
||||||
first_name: string;
|
last_name: string;
|
||||||
last_name: string;
|
email?: string;
|
||||||
}
|
profile_picture?: string | null;
|
||||||
|
}
|
||||||
interface Workspace {
|
|
||||||
id: string;
|
interface AppContextType {
|
||||||
name: string;
|
user: User | null;
|
||||||
}
|
refreshUser: () => Promise<void>;
|
||||||
|
}
|
||||||
interface AppContextType {
|
|
||||||
user: User | null;
|
const AppContext = createContext<AppContextType | null>(null);
|
||||||
workspaces: Workspace[];
|
|
||||||
activeWorkspace: Workspace | null;
|
export const AppProvider = ({ children }: { children: ReactNode }) => {
|
||||||
setActiveWorkspace: (ws: Workspace) => void;
|
const [user, setUser] = useState<User | null>(null);
|
||||||
fetchInitialData: () => Promise<void>;
|
|
||||||
}
|
const refreshUser = async () => {
|
||||||
|
try {
|
||||||
const AppContext = createContext<AppContextType | null>(null);
|
const userData = await getUserProfile();
|
||||||
|
setUser(userData);
|
||||||
export const AppProvider = ({ children }: { children: ReactNode }) => {
|
} catch (error) {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
console.error("Failed to fetch user context data:", error);
|
||||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
setUser(null);
|
||||||
const [activeWorkspace, setActiveWorkspace] = useState<Workspace | null>(null);
|
}
|
||||||
|
};
|
||||||
const fetchInitialData = async () => {
|
|
||||||
try {
|
useEffect(() => {
|
||||||
const [userData, wsData] = await Promise.all([
|
if (localStorage.getItem('accessToken')) {
|
||||||
getUserProfile(),
|
void refreshUser();
|
||||||
fetchWorkspaces() // fetchWorkspaces({ limit: 50 })
|
}
|
||||||
]);
|
}, []);
|
||||||
|
|
||||||
setUser(userData);
|
return (
|
||||||
|
<AppContext.Provider value={{ user, refreshUser }}>
|
||||||
const workspacesList = Array.isArray(wsData.data) ? wsData.data : (wsData?.data?.results || []);
|
{children}
|
||||||
|
</AppContext.Provider>
|
||||||
setWorkspaces(workspacesList);
|
);
|
||||||
|
};
|
||||||
const savedWsId = localStorage.getItem('active_workspace');
|
|
||||||
const targetWs = workspacesList.find((w: Workspace) => w.id === savedWsId) || workspacesList[0];
|
|
||||||
|
|
||||||
if (targetWs) {
|
|
||||||
setActiveWorkspace(targetWs);
|
|
||||||
localStorage.setItem('active_workspace', targetWs.id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch initial context data:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (localStorage.getItem('accessToken')) {
|
|
||||||
fetchInitialData();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppContext.Provider value={{ user, workspaces, activeWorkspace, setActiveWorkspace, fetchInitialData }}>
|
|
||||||
{children}
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAppContext = () => {
|
export const useAppContext = () => {
|
||||||
const context = useContext(AppContext);
|
const context = useContext(AppContext);
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { createContext, useContext, useState, useEffect, ReactNode } from "react"
|
import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
|
||||||
import { fetchWorkspaces, createWorkspace, type Workspace } from "../api/workspaces"
|
import { fetchWorkspaces, createWorkspace, type Workspace } from "../api/workspaces"
|
||||||
import { useTranslation } from "../hooks/useTranslation"
|
import { useTranslation } from "../hooks/useTranslation"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { Button } from "../components/ui/button"
|
import { Button } from "../components/ui/button"
|
||||||
import { Input } from "../components/ui/input"
|
import { Input } from "../components/ui/input"
|
||||||
|
|
||||||
interface WorkspaceContextType {
|
interface WorkspaceContextType {
|
||||||
workspaces: Workspace[]
|
workspaces: Workspace[]
|
||||||
activeWorkspace: Workspace | null
|
activeWorkspace: Workspace | null
|
||||||
setActiveWorkspace: (workspace: Workspace) => void
|
setActiveWorkspace: (workspace: Workspace | null) => void
|
||||||
addWorkspace: (name: string) => Promise<void>
|
addWorkspace: (name: string) => Promise<void>
|
||||||
isLoading: boolean
|
refreshWorkspaces: () => Promise<void>
|
||||||
}
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const WorkspaceContext = createContext<WorkspaceContextType | undefined>(undefined)
|
const WorkspaceContext = createContext<WorkspaceContextType | undefined>(undefined)
|
||||||
|
|
||||||
@@ -29,45 +30,58 @@ export const WorkspaceProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const [newWorkspaceName, setNewWorkspaceName] = useState("")
|
const [newWorkspaceName, setNewWorkspaceName] = useState("")
|
||||||
const [isCreatingFirst, setIsCreatingFirst] = useState(false)
|
const [isCreatingFirst, setIsCreatingFirst] = useState(false)
|
||||||
|
|
||||||
const isAuthenticated = !!localStorage.getItem("accessToken")
|
const isAuthenticated = !!localStorage.getItem("accessToken")
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshWorkspaces = async () => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadWorkspaces = async () => {
|
try {
|
||||||
try {
|
setIsLoading(true)
|
||||||
const response = await fetchWorkspaces()
|
const response = await fetchWorkspaces()
|
||||||
|
|
||||||
const data = Array.isArray(response) ? response : (response?.results || [])
|
const data = Array.isArray(response) ? response : (response?.results || [])
|
||||||
setWorkspaces(data)
|
setWorkspaces(data)
|
||||||
|
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
const storedId = localStorage.getItem("activeWorkspaceId")
|
const storedId = localStorage.getItem("activeWorkspaceId")
|
||||||
const stored = data.find((w: Workspace) => w.id === storedId)
|
const stored = data.find((w: Workspace) => w.id === storedId)
|
||||||
if (stored) {
|
if (stored) {
|
||||||
setActiveWorkspaceState(stored)
|
setActiveWorkspaceState(stored)
|
||||||
} else {
|
} else {
|
||||||
setActiveWorkspaceState(data[0])
|
setActiveWorkspaceState(data[0])
|
||||||
localStorage.setItem("activeWorkspaceId", data[0].id)
|
localStorage.setItem("activeWorkspaceId", data[0].id)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
} catch (error) {
|
setActiveWorkspaceState(null)
|
||||||
console.error(error)
|
localStorage.removeItem("activeWorkspaceId")
|
||||||
} finally {
|
}
|
||||||
setIsLoading(false)
|
} catch (error) {
|
||||||
}
|
console.error(error)
|
||||||
}
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
loadWorkspaces()
|
}
|
||||||
}, [isAuthenticated])
|
}
|
||||||
|
|
||||||
const setActiveWorkspace = (workspace: Workspace) => {
|
useEffect(() => {
|
||||||
setActiveWorkspaceState(workspace)
|
if (!isAuthenticated) {
|
||||||
localStorage.setItem("activeWorkspaceId", workspace.id)
|
setIsLoading(false)
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshWorkspaces()
|
||||||
|
}, [isAuthenticated])
|
||||||
|
|
||||||
|
const setActiveWorkspace = (workspace: Workspace | null) => {
|
||||||
|
setActiveWorkspaceState(workspace)
|
||||||
|
if (workspace) {
|
||||||
|
localStorage.setItem("activeWorkspaceId", workspace.id)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem("activeWorkspaceId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const addWorkspace = async (name: string) => {
|
const addWorkspace = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -75,10 +89,10 @@ export const WorkspaceProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
const newWs = await createWorkspace({ name, description: "" })
|
const newWs = await createWorkspace({ name, description: "" })
|
||||||
setWorkspaces((prev) => [...prev, newWs])
|
setWorkspaces((prev) => [...prev, newWs])
|
||||||
setActiveWorkspace(newWs)
|
setActiveWorkspace(newWs)
|
||||||
toast.success(t.workspace?.createSuccess || "Workspace created!")
|
toast.success(t.workspace?.successCreate || t.workspace?.toast?.successCreate || "Workspace created!")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t.workspace?.createError || "Failed to create workspace")
|
toast.error(t.workspace?.toast?.errorCreate || "Failed to create workspace")
|
||||||
throw error
|
throw error
|
||||||
} finally {
|
} finally {
|
||||||
setIsCreatingFirst(false)
|
setIsCreatingFirst(false)
|
||||||
setNewWorkspaceName("")
|
setNewWorkspaceName("")
|
||||||
@@ -123,10 +137,10 @@ export const WorkspaceProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceContext.Provider
|
<WorkspaceContext.Provider
|
||||||
value={{ workspaces, activeWorkspace, setActiveWorkspace, addWorkspace, isLoading }}
|
value={{ workspaces, activeWorkspace, setActiveWorkspace, addWorkspace, refreshWorkspaces, isLoading }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</WorkspaceContext.Provider>
|
</WorkspaceContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function WorkspaceCreate() {
|
|||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [memberIdToDelete, setMemberIdToDelete] = useState<string | null>(null);
|
const [memberIdToDelete, setMemberIdToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
const searchTimeoutRef = useRef<NodeJS.Timeout>();
|
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const hasUnsavedChanges = name.trim() !== '' || description.trim() !== '' || members.length > 0;
|
const hasUnsavedChanges = name.trim() !== '' || description.trim() !== '' || members.length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef, Fragment, useMemo } from 'react';
|
import React, { useState, useEffect, useRef, Fragment, useMemo, useCallback } from 'react';
|
||||||
import { useBlocker, useNavigate, useParams } from 'react-router-dom';
|
import { useBlocker, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useTranslation } from '../hooks/useTranslation';
|
import { useTranslation } from '../hooks/useTranslation';
|
||||||
import { AlertCircle, UserPlus, Trash2, Shield } from 'lucide-react';
|
import { AlertCircle, UserPlus, Trash2, Shield } from 'lucide-react';
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import { searchUserByExactMobile, type SearchedUser } from '../api/users';
|
import { searchUserByExactMobile, type SearchedUser } from '../api/users';
|
||||||
import { useAppContext } from '../context/AppContext';
|
import { useAppContext } from '../context/AppContext';
|
||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
import { InfiniteScroll } from '../components/infiniteScroll';
|
import { InfiniteScroll } from '../components/InfiniteScroll';
|
||||||
import { Select } from '../components/ui/Select';
|
import { Select } from '../components/ui/Select';
|
||||||
import { Input } from '../components/ui/input';
|
import { Input } from '../components/ui/input';
|
||||||
import { TextAreaInput } from '../components/ui/TextAreaInput';
|
import { TextAreaInput } from '../components/ui/TextAreaInput';
|
||||||
@@ -67,12 +67,13 @@ export default function EditWorkspace() {
|
|||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [memberIdToDelete, setMemberIdToDelete] = useState<string | null>(null);
|
const [memberIdToDelete, setMemberIdToDelete] = useState<string | null>(null);
|
||||||
|
|
||||||
const searchTimeoutRef = useRef<NodeJS.Timeout>();
|
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const [initialData, setInitialData] = useState({
|
const [initialData, setInitialData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasUnsavedChanges = useMemo(() => {
|
const hasUnsavedChanges = useMemo(() => {
|
||||||
if (isLoading) return false;
|
if (isLoading) return false;
|
||||||
|
|
||||||
@@ -100,13 +101,6 @@ export default function EditWorkspace() {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
useBlocker(({ currentLocation, nextLocation }) => {
|
|
||||||
if (hasUnsavedChanges && !isSaving && currentLocation.pathname !== nextLocation.pathname) {
|
|
||||||
return !window.confirm(t.confirmLeave || "You have unsaved changes. Are you sure you want to leave?");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) loadData();
|
if (id) loadData();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
@@ -125,7 +119,9 @@ export default function EditWorkspace() {
|
|||||||
|
|
||||||
setMembers(results);
|
setMembers(results);
|
||||||
setOffset(LIMIT);
|
setOffset(LIMIT);
|
||||||
setHasMore(!!membersData.next);
|
|
||||||
|
// Robust hasMore check: use `.next` if available, otherwise check if array filled the limit
|
||||||
|
setHasMore(membersData.next ? true : results.length >= LIMIT);
|
||||||
|
|
||||||
setInitialData({
|
setInitialData({
|
||||||
name: workspaceData.name,
|
name: workspaceData.name,
|
||||||
@@ -139,22 +135,35 @@ export default function EditWorkspace() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMoreMembers = async () => {
|
const loadMoreMembers = useCallback(async () => {
|
||||||
if (isLoadingMembers || !hasMore || !id) return;
|
if (isLoadingMembers || !hasMore || !id) return;
|
||||||
try {
|
try {
|
||||||
setIsLoadingMembers(true);
|
setIsLoadingMembers(true);
|
||||||
const membersData = await fetchWorkspaceMemberships({ workspace: id, limit: `${LIMIT}`, offset: `${offset}` });
|
|
||||||
const results = membersData.results || [];
|
|
||||||
|
|
||||||
setMembers((prev) => [...prev, ...results]);
|
// Send as pure numbers, axios handles them cleanly
|
||||||
setOffset((prev) => prev + LIMIT);
|
const membersData = await fetchWorkspaceMemberships({
|
||||||
setHasMore(!!membersData.next);
|
workspace: id,
|
||||||
|
limit: LIMIT,
|
||||||
|
offset: offset
|
||||||
|
});
|
||||||
|
const results = membersData.results || (Array.isArray(membersData) ? membersData : []);
|
||||||
|
|
||||||
|
setMembers((prev) => {
|
||||||
|
// Safe deduplication to avoid React key warnings
|
||||||
|
const existingIds = new Set(prev.map(m => m.id));
|
||||||
|
const newItems = results.filter((item: any) => !existingIds.has(item.id));
|
||||||
|
return [...prev, ...newItems];
|
||||||
|
});
|
||||||
|
|
||||||
|
setOffset(offset + LIMIT);
|
||||||
|
setHasMore(membersData.next ? true : results.length >= LIMIT);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load more members", error);
|
console.error("Failed to load more members", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingMembers(false);
|
setIsLoadingMembers(false);
|
||||||
}
|
}
|
||||||
};
|
}, [id, isLoadingMembers, hasMore, offset]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
|
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
|
||||||
@@ -191,14 +200,11 @@ export default function EditWorkspace() {
|
|||||||
if (!name.trim() || !id) return;
|
if (!name.trim() || !id) return;
|
||||||
try {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
||||||
await updateWorkspace(id, { name, description });
|
await updateWorkspace(id, { name, description });
|
||||||
|
|
||||||
toast.success(t.workspace?.toast?.successUpdate || "Workspace updated successfully.");
|
toast.success(t.workspace?.toast?.successUpdate || "Workspace updated successfully.");
|
||||||
window.dispatchEvent(new CustomEvent('workspace_edited', {
|
window.dispatchEvent(new CustomEvent('workspace_edited', {
|
||||||
detail: { id, name, description }
|
detail: { id, name, description }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
navigate('/workspaces');
|
navigate('/workspaces');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t.workspace?.toast?.errorUpdate || "Failed to update workspace.");
|
toast.error(t.workspace?.toast?.errorUpdate || "Failed to update workspace.");
|
||||||
@@ -210,11 +216,11 @@ export default function EditWorkspace() {
|
|||||||
const handleAddMember = async () => {
|
const handleAddMember = async () => {
|
||||||
if (!searchResult || !id) return;
|
if (!searchResult || !id) return;
|
||||||
try {
|
try {
|
||||||
const newMembership = await addWorkspaceMembership({
|
const newMembership = await addWorkspaceMembership({
|
||||||
workspace: id,
|
workspace: id,
|
||||||
user: searchResult.id,
|
user: String(searchResult.id),
|
||||||
role: newMemberRole
|
role: newMemberRole
|
||||||
});
|
});
|
||||||
setMembers([newMembership, ...members]);
|
setMembers([newMembership, ...members]);
|
||||||
toast.success(t.workspace?.toast?.successAdd || "Member added successfully.");
|
toast.success(t.workspace?.toast?.successAdd || "Member added successfully.");
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
@@ -264,8 +270,6 @@ export default function EditWorkspace() {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-4 sm:gap-6 flex-1 min-h-0">
|
<div className="flex flex-col lg:flex-row gap-4 sm:gap-6 flex-1 min-h-0">
|
||||||
|
|
||||||
{/* --- ستون سمت چپ: فرم ویرایش --- */}
|
|
||||||
<div className="w-full lg:w-1/3 lg:max-w-md flex flex-col shrink-0 overflow-y-auto bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
|
<div className="w-full lg:w-1/3 lg:max-w-md flex flex-col shrink-0 overflow-y-auto bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col h-full p-6">
|
<form onSubmit={handleSubmit} className="flex flex-col h-full p-6">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@@ -292,27 +296,17 @@ export default function EditWorkspace() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto pt-6 flex justify-end gap-3 border-t border-slate-100 dark:border-slate-800 shrink-0">
|
<div className="mt-auto pt-6 flex justify-end gap-3 border-t border-slate-100 dark:border-slate-800 shrink-0">
|
||||||
<Button
|
<Button type="button" variant="ghost" onClick={() => navigate('/workspaces')}>
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => navigate('/workspaces')}
|
|
||||||
>
|
|
||||||
{t.actions?.cancel || "Cancel"}
|
{t.actions?.cancel || "Cancel"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="submit" disabled={isSaving || !name.trim()}>
|
||||||
type="submit"
|
|
||||||
disabled={isSaving || !name.trim()}
|
|
||||||
>
|
|
||||||
{isSaving ? (t.workspace?.loading || "Saving...") : (t.workspace?.save || "Save")}
|
{isSaving ? (t.workspace?.loading || "Saving...") : (t.workspace?.save || "Save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* --- ستون سمت راست: لیست اعضا --- */}
|
|
||||||
<div className="w-full lg:w-2/3 flex-1 flex flex-col min-h-100 lg:min-h-0 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden">
|
<div className="w-full lg:w-2/3 flex-1 flex flex-col min-h-100 lg:min-h-0 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden">
|
||||||
|
|
||||||
{/* بخش جستجو و هدر (ثابت در بالا) */}
|
|
||||||
<div className="p-6 shrink-0 border-b border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900 z-10">
|
<div className="p-6 shrink-0 border-b border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900 z-10">
|
||||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
|
<h2 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">
|
||||||
{ t.workspace?.members || "Members" }
|
{ t.workspace?.members || "Members" }
|
||||||
@@ -391,7 +385,6 @@ export default function EditWorkspace() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* لیست اعضا (با قابلیت اسکرول مجزا) */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-6 space-y-3 bg-slate-50/30 dark:bg-slate-900/30">
|
<div className="flex-1 overflow-y-auto p-6 space-y-3 bg-slate-50/30 dark:bg-slate-900/30">
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
onLoadMore={loadMoreMembers}
|
onLoadMore={loadMoreMembers}
|
||||||
@@ -438,8 +431,10 @@ export default function EditWorkspace() {
|
|||||||
) : (
|
) : (
|
||||||
<span className="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 rounded-md capitalize flex items-center gap-1">
|
<span className="text-xs px-2 py-1 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 rounded-md capitalize flex items-center gap-1">
|
||||||
{m.role === 'owner' && <Shield className="w-3 h-3" />}
|
{m.role === 'owner' && <Shield className="w-3 h-3" />}
|
||||||
{m.role ? t.workspace?.roles?.[m.role] || m.role : "-"}
|
{m.role && m.role in t.workspace.roles
|
||||||
</span>
|
? t.workspace.roles[m.role as keyof typeof t.workspace.roles]
|
||||||
|
: m.role || "-"}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canManageMembers && !isThisMemberTheFirstOwner && (isFirstOwner || m.role !== 'owner') && (
|
{canManageMembers && !isThisMemberTheFirstOwner && (isFirstOwner || m.role !== 'owner') && (
|
||||||
@@ -504,16 +499,10 @@ export default function EditWorkspace() {
|
|||||||
{t.workspace?.confirmDeleteMessage || "Are you sure you want to remove this member from the workspace?"}
|
{t.workspace?.confirmDeleteMessage || "Are you sure you want to remove this member from the workspace?"}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-6 flex justify-end gap-3">
|
<div className="mt-6 flex justify-end gap-3">
|
||||||
<Button
|
<Button variant="secondary" onClick={() => setIsDeleteDialogOpen(false)}>
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setIsDeleteDialogOpen(false)}
|
|
||||||
>
|
|
||||||
{t.actions?.cancel || "Cancel"}
|
{t.actions?.cancel || "Cancel"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="destructive" onClick={handleDeleteMember}>
|
||||||
variant="destructive"
|
|
||||||
onClick={handleDeleteMember}
|
|
||||||
>
|
|
||||||
{t.actions?.delete || "Delete"}
|
{t.actions?.delete || "Delete"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user