diff --git a/app/components/WorkspacePanel.tsx b/app/components/WorkspacePanel.tsx index 49102da..fc2f176 100644 --- a/app/components/WorkspacePanel.tsx +++ b/app/components/WorkspacePanel.tsx @@ -4,7 +4,7 @@ import { Workspace, Repository } from '../types/workspace'; import { PlusIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; export const WorkspacePanel: React.FC = () => { - const { +const { workspaces, currentWorkspace, setCurrentWorkspace, @@ -14,8 +14,19 @@ export const WorkspacePanel: React.FC = () => { addRepository, removeRepository, updateRepositorySettings, + createGroup, + updateGroup, + deleteGroup, + getGroup, } = useWorkspaces(); + const [showNewGroupForm, setShowNewGroupForm] = useState(false); + const [newGroupName, setNewGroupName] = useState(""); + const [selectedGroup, setSelectedGroup] = useState<string | null>(null); + const [isRenamingGroup, setIsRenamingGroup] = useState(false); + const [renamingGroupId, setRenamingGroupId] = useState<string | null>(null); + const [editedGroupName, setEditedGroupName] = useState(""); + const [showNewWorkspace, setShowNewWorkspace] = React.useState(false); const [newWorkspaceName, setNewWorkspaceName] = React.useState(''); const [newWorkspaceVisibility, setNewWorkspaceVisibility] = React.useState<'private' | 'public' | 'team'>('private'); @@ -75,7 +86,55 @@ export const WorkspacePanel: React.FC = () => { } }; - const currentWorkspaceData = workspaces.find(w => w.id === currentWorkspace); +const currentWorkspaceData = workspaces.find((w) =>w.id === currentWorkspace); + + const handleCreateGroup = () =>{ + if (newGroupName && currentWorkspace) { + createGroup(currentWorkspace, newGroupName); + setNewGroupName(""); + setShowNewGroupForm(false); + } + }; + + const handleRenameGroup = (group: Group) =>{ + setRenamingGroupId(group.id); + setEditedGroupName(group.name); + setIsRenamingGroup(true); + }; + + const handleSaveGroupName = (groupId: string) =>{ + if (currentWorkspace && editedGroupName) { + updateGroup(currentWorkspace, groupId, { name: editedGroupName }); + } + setIsRenamingGroup(false); + setRenamingGroupId(null); + setEditedGroupName(""); + }; + + const handleDeleteGroup = (groupId: string) =>{ + if ( + currentWorkspace && + confirm("Are you sure you want to delete this group?") + ) { + deleteGroup(currentWorkspace, groupId); + setSelectedGroup(null); + } + }; + + const handleSelectGroup = (groupId: string) =>{ + setSelectedGroup(groupId); + }; + + const filteredRepositories = React.useMemo(() =>{ + if (!currentWorkspaceData) return []; + if (!selectedGroup) return currentWorkspaceData.repositories; + + const group = getGroup(currentWorkspaceData.id, selectedGroup); + if (!group) return []; + + return currentWorkspaceData.repositories.filter((repo) =>group.repositoryIds.includes(repo.id) + ); + }, [currentWorkspaceData, selectedGroup]); return (
@@ -90,81 +149,195 @@ export const WorkspacePanel: React.FC = () => {
-
- {workspaces.map(workspace => ( -
setCurrentWorkspace(workspace.id)} - > -
-
-
-

{workspace.name}

- - {workspace.settings?.visibility || 'private'} - -
- {workspace.description && ( -

{workspace.description}

- )} - {workspace.settings?.collaborators?.length > 0 && ( -
- {workspace.settings.collaborators.length} collaborator(s) -
- )} -
-
- - -
-
-
- {workspace.repositories.length} repositories - -
-
- ))} -
+ > + <div className="flex justify-between items-start mb-2"> + <div> + <div className="flex items-center gap-2"> + <h3 className="font-medium">{workspace.name}</h3> + <span + className={`text-xs px-2 py-0.5 rounded-full ${ + workspace.settings?.visibility === "public" + ? "bg-green-600/20 text-green-300" + : workspace.settings?.visibility === "team" + ? "bg-blue-600/20 text-blue-300" + : "bg-gray-600/20 text-gray-300" + }`} + > + {workspace.settings?.visibility || "private"} + </span> + </div> + {workspace.description && ( + <p className="text-sm text-gray-400"> + {workspace.description} + </p> + )} + {workspace.settings?.collaborators?.length > 0 && ( + <div className="text-xs text-gray-400 mt-1"> + {workspace.settings.collaborators.length} collaborator(s) + </div> + )} + </div> + <div className="flex items-center gap-2"> + <button + onClick={(e) => { + e.stopPropagation(); + updateWorkspace(workspace.id, { + name: + prompt("New workspace name:", workspace.name) || + workspace.name, + }); + }} + className="p-1 rounded hover:bg-[#444444] transition-colors" + > + <PencilIcon className="h-4 w-4" /> + </button> + <button + onClick={(e) => { + e.stopPropagation(); + if ( + confirm("Are you sure you want to delete this workspace?") + ) { + deleteWorkspace(workspace.id); + } + }} + className="p-1 rounded hover:bg-[#444444] transition-colors" + > + <TrashIcon className="h-4 w-4" /> + </button> + </div> + </div> + <div className="flex items-center gap-2 text-sm text-gray-400"> + <span>{workspace.repositories.length} repositories</span> + <button + onClick={(e) => { + e.stopPropagation(); + setCurrentWorkspace(workspace.id); + setShowCollaborators(true); + }} + className="px-2 py-1 rounded bg-[#444444] hover:bg-[#555555] transition-colors" + > + Manage Collaborators + </button> + </div> + </div> + {workspace.id === currentWorkspace && ( + <div className="mt-4"> + <div className="flex justify-between items-center mb-2"> + <h4 className="text-lg font-medium">Groups</h4> + <button + onClick={() => setShowNewGroupForm(true)} + className="px-3 py-1.5 rounded text-sm bg-indigo-600 hover:bg-indigo-700 transition-colors flex items-center gap-2" + > + <PlusIcon className="h-4 w-4" /> + New Group + </button> + </div> + <div className="space-y-2"> + {workspace.groups.map((group) => ( + <div + key={group.id} + className={`p-2 rounded-lg ${ + selectedGroup === group.id + ? "bg-indigo-600/20 border border-indigo-500/30" + : "bg-[#444444] hover:bg-[#4A4A4A]" + } transition-colors cursor-pointer`} + onClick={() => handleSelectGroup(group.id)} + > + <div className="flex justify-between items-center"> + <span className="font-medium"> + {isRenamingGroup && renamingGroupId === group.id ? ( + <input + type="text" + value={editedGroupName} + onChange={(e) => + setEditedGroupName(e.target.value) + } + onBlur={() => handleSaveGroupName(group.id)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSaveGroupName(group.id); + } + }} + className="px-2 py-1 rounded bg-[#333333] text-white focus:outline-none focus:ring-2 focus:ring-indigo-500" + autoFocus + /> + ) : ( + group.name + )} + </span> + <div className="flex items-center gap-2"> + <button + onClick={(e) => { + e.stopPropagation(); + handleRenameGroup(group); + }} + className="p-1 rounded hover:bg-[#555555] transition-colors" + > + <PencilIcon className="h-4 w-4" /> + </button> + <button + onClick={(e) => { + e.stopPropagation(); + handleDeleteGroup(group.id); + }} + className="p-1 rounded hover:bg-[#555555] transition-colors" + > + <TrashIcon className="h-4 w-4" /> + </button> + </div> + </div> + </div> + ))} + </div> + </div> + )} + {/* New Group Form */} + {showNewGroupForm && ( + <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> + <div className="bg-[#212121] p-6 rounded-lg w-full max-w-md mx-4"> + <h2 className="text-xl font-semibold mb-4"> + Create New Group + </h2> + <input + type="text" + value={newGroupName} + onChange={(e) => setNewGroupName(e.target.value)} + placeholder="Group name" + className="w-full p-2 rounded bg-[#333333] text-white mb-4" + /> + <div className="flex justify-end gap-4"> + <button + onClick={() => setShowNewGroupForm(false)} + className="px-4 py-2 rounded bg-[#444444] hover:bg-[#555555] transition-colors" + > + Cancel + </button> + <button + onClick={handleCreateGroup} + className="px-4 py-2 rounded bg-indigo-600 hover:bg-indigo-700 transition-colors" + > + Create + </button> + </div> + </div> + </div> + )} + </div> + ))} + </div> {currentWorkspaceData && (
diff --git a/app/hooks/useWorkspaces.ts b/app/hooks/useWorkspaces.ts index 90d455a..14f5d77 100644 --- a/app/hooks/useWorkspaces.ts +++ b/app/hooks/useWorkspaces.ts @@ -4,39 +4,149 @@ import { Workspace, Repository, WorkspaceSettings } from '../types/workspace'; const STORAGE_KEY = 'workspaces'; export const useWorkspaces = () => { - const [workspaces, setWorkspaces] = useState([]); +const [workspaces, setWorkspaces] = useState<Workspace[]>([]); + const [groups, setGroups] = useState<Group[]>([]); const [currentWorkspace, setCurrentWorkspace] = useState(null); - useEffect(() => { +useEffect(() =>{ // Load workspaces from localStorage on mount const storedWorkspaces = localStorage.getItem(STORAGE_KEY); if (storedWorkspaces) { const parsed = JSON.parse(storedWorkspaces); setWorkspaces(parsed); - if (parsed.length > 0) { - setCurrentWorkspace(parsed[0].id); + if (parsed.length >0) { + setCurrentWorkspace(parsed.find((w: Workspace) =>w.isDefault)?.id || parsed[0].id); + } else { + // Create a default workspace if none exists + const defaultWorkspace: Workspace = { + id: crypto.randomUUID(), + name: 'Default Workspace', + description: 'Your default workspace', + repositories: [], + settings: { + visibility: 'private', + collaborators: [], + lastAccessed: new Date(), + }, + createdAt: new Date(), + updatedAt: new Date(), + isDefault: true, + }; + setWorkspaces([defaultWorkspace]); + setCurrentWorkspace(defaultWorkspace.id); } + } else { + // Create a default workspace if none exists + const defaultWorkspace: Workspace = { + id: crypto.randomUUID(), + name: 'Default Workspace', + description: 'Your default workspace', + repositories: [], + settings: { + visibility: 'private', + collaborators: [], + lastAccessed: new Date(), + }, + createdAt: new Date(), + updatedAt: new Date(), + isDefault: true, + }; + setWorkspaces([defaultWorkspace]); + setCurrentWorkspace(defaultWorkspace.id); } }, []); +const getWorkspace = (workspaceId: string): Workspace | undefined =>{ + return workspaces.find((w) =>w.id === workspaceId); + }; + + const getGroup = (workspaceId: string, groupId: string): Group | undefined =>{ + const workspace = getWorkspace(workspaceId); + if (!workspace) return undefined; + return workspace.groups.find((g) =>g.id === groupId); + }; + + const createGroup = (workspaceId: string, groupName: string): Group =>{ + const newGroup: Group = { + id: crypto.randomUUID(), + name: groupName, + repositoryIds: [], + }; + + setWorkspaces( + workspaces.map((workspace) =>workspace.id === workspaceId + ? { ...workspace, groups: [...workspace.groups, newGroup] } + : workspace + ) + ); + + return newGroup; + }; + + const updateGroup = ( + workspaceId: string, + groupId: string, + updates: Partial<Group> + ) =>{ + setWorkspaces( + workspaces.map((workspace) =>workspace.id === workspaceId + ? { + ...workspace, + groups: workspace.groups.map((group) =>group.id === groupId ? { ...group, ...updates } : group + ), + } + : workspace + ) + ); + }; + + const deleteGroup = (workspaceId: string, groupId: string) =>{ + setWorkspaces( + workspaces.map((workspace) =>workspace.id === workspaceId + ? { + ...workspace, + groups: workspace.groups.filter((group) =>group.id !== groupId), + } + : workspace + ) + ); + }; + useEffect(() => { // Save workspaces to localStorage when they change localStorage.setItem(STORAGE_KEY, JSON.stringify(workspaces)); }, [workspaces]); - const createWorkspace = (workspace: Omit) => { +const createWorkspace = ( + workspace: Omit<Workspace, "id" | "createdAt" | "updatedAt" | "groups"> + ) =>{ const newWorkspace: Workspace = { ...workspace, id: crypto.randomUUID(), createdAt: new Date(), updatedAt: new Date(), + groups: [], settings: { ...workspace.settings, lastAccessed: new Date(), - visibility: workspace.settings?.visibility || 'private' + visibility: workspace.settings?.visibility || "private", }, }; + + const defaultGroup: Group = { + id: crypto.randomUUID(), + name: "Default Group", + repositoryIds: [], + }; + + newWorkspace.groups.push(defaultGroup); + + if (workspaces.length === 0) { + newWorkspace.isDefault = true; + } + setWorkspaces([...workspaces, newWorkspace]); + setCurrentWorkspace(newWorkspace.id); return newWorkspace; }; @@ -48,37 +158,74 @@ export const useWorkspaces = () => { )); }; - const deleteWorkspace = (id: string) => { - setWorkspaces(workspaces.filter(workspace => workspace.id !== id)); +const deleteWorkspace = (id: string) =>{ + const updatedWorkspaces = workspaces.filter(workspace =>workspace.id !== id); + setWorkspaces(updatedWorkspaces); if (currentWorkspace === id) { - setCurrentWorkspace(workspaces[0]?.id ?? null); + setCurrentWorkspace( + updatedWorkspaces.find(w =>w.isDefault)?.id || + updatedWorkspaces[0]?.id || + null + ); } }; - const addRepository = (workspaceId: string, repository: Omit) => { +const addRepository = ( + workspaceId: string, + repository: Omit<Repository, "id" | "addedAt">, + groupId?: string + ) =>{ + const workspace = getWorkspace(workspaceId); + if (!workspace) { + throw new Error(`Workspace not found: ${workspaceId}`); + } + const newRepository: Repository = { ...repository, id: crypto.randomUUID(), addedAt: new Date(), + groupId: groupId || workspace.groups[0]?.id, // Add to the specified group or the first group by default }; updateWorkspace(workspaceId, { - repositories: [ - ...(workspaces.find(w => w.id === workspaceId)?.repositories || []), - newRepository - ] + repositories: [...workspace.repositories, newRepository], }); + // Add the repository ID to the group's repositoryIds array + if (groupId) { + updateGroup(workspaceId, groupId, { + repositoryIds: [ + ...(workspace.groups.find((g) =>g.id === groupId)?.repositoryIds || + []), + newRepository.id, + ], + }); + } + return newRepository; }; - const removeRepository = (workspaceId: string, repositoryId: string) => { - const workspace = workspaces.find(w => w.id === workspaceId); - if (workspace) { - updateWorkspace(workspaceId, { - repositories: workspace.repositories.filter(r => r.id !== repositoryId) - }); +const removeRepository = (workspaceId: string, repositoryId: string) =>{ + const workspace = getWorkspace(workspaceId); + if (!workspace) return; + + const repository = workspace.repositories.find((r) =>r.id === repositoryId); + if (!repository) return; + + // Remove the repository from its group + if (repository.groupId) { + const group = getGroup(workspaceId, repository.groupId); + if (group) { + updateGroup(workspaceId, repository.groupId, { + repositoryIds: group.repositoryIds.filter((id) =>id !== repositoryId), + }); + } } + + // Remove the repository from the workspace + updateWorkspace(workspaceId, { + repositories: workspace.repositories.filter((r) =>r.id !== repositoryId), + }); }; const updateRepositorySettings = ( @@ -105,7 +252,7 @@ export const useWorkspaces = () => { } }; - return { +return { workspaces, currentWorkspace, setCurrentWorkspace, @@ -116,5 +263,9 @@ export const useWorkspaces = () => { removeRepository, updateRepositorySettings, updateWorkspaceSettings, + createGroup, + updateGroup, + deleteGroup, + getGroup, }; }; diff --git a/app/types/workspace.ts b/app/types/workspace.ts index 44efb4f..28ea182 100644 --- a/app/types/workspace.ts +++ b/app/types/workspace.ts @@ -3,9 +3,17 @@ export interface Workspace { name: string; description?: string; repositories: Repository[]; + groups: Group[]; settings: WorkspaceSettings; createdAt: Date; updatedAt: Date; + isDefault?: boolean; +} + +export interface Group { + id: string; + name: string; + repositoryIds: string[]; // Array to store IDs of repositories in the group } export interface Repository { @@ -16,6 +24,7 @@ export interface Repository { defaultBranch: string; settings?: RepositorySettings; addedAt: Date; + groupId?: string; // Optional group ID for the repository } export interface WorkspaceSettings {