SEC-WEB-32: Added maxLength to form inputs (names: 100, descriptions: 500, emails: 254) in WorkspaceSettings, TeamSettings, InviteMember components. SEC-WEB-34: Added AbortController timeout (30s default, configurable) to apiRequest and apiPostFormData in API client. Co-Authored-By: Claude Opus 4.6 <[email protected]>
179 lines
6.0 KiB
TypeScript
179 lines
6.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import type { Workspace } from "@mosaic/shared";
|
|
import { WorkspaceMemberRole } from "@mosaic/shared";
|
|
|
|
interface WorkspaceSettingsProps {
|
|
workspace: Workspace;
|
|
userRole: WorkspaceMemberRole;
|
|
onUpdate: (name: string) => Promise<void>;
|
|
onDelete: () => Promise<void>;
|
|
}
|
|
|
|
export function WorkspaceSettings({
|
|
workspace,
|
|
userRole,
|
|
onUpdate,
|
|
onDelete,
|
|
}: WorkspaceSettingsProps): React.JSX.Element {
|
|
const [name, setName] = useState(workspace.name);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
|
|
const canEdit = userRole === WorkspaceMemberRole.OWNER || userRole === WorkspaceMemberRole.ADMIN;
|
|
const canDelete = userRole === WorkspaceMemberRole.OWNER;
|
|
|
|
const handleSave = async (): Promise<void> => {
|
|
if (name.trim() === "" || name === workspace.name) {
|
|
setIsEditing(false);
|
|
setName(workspace.name);
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
try {
|
|
await onUpdate(name);
|
|
setIsEditing(false);
|
|
} catch (error) {
|
|
console.error("Failed to update workspace:", error);
|
|
alert("Failed to update workspace");
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (): Promise<void> => {
|
|
setIsDeleting(true);
|
|
try {
|
|
await onDelete();
|
|
} catch (error) {
|
|
console.error("Failed to delete workspace:", error);
|
|
alert("Failed to delete workspace");
|
|
setIsDeleting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-6">Workspace Settings</h2>
|
|
|
|
<div className="space-y-6">
|
|
{/* Workspace Name */}
|
|
<div>
|
|
<label htmlFor="workspace-name" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Workspace Name
|
|
</label>
|
|
{isEditing ? (
|
|
<div className="flex gap-2">
|
|
<input
|
|
id="workspace-name"
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => {
|
|
setName(e.target.value);
|
|
}}
|
|
maxLength={100}
|
|
disabled={isSaving}
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100"
|
|
/>
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
{isSaving ? "Saving..." : "Save"}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setIsEditing(false);
|
|
setName(workspace.name);
|
|
}}
|
|
disabled={isSaving}
|
|
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 disabled:opacity-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-gray-900">{workspace.name}</p>
|
|
{canEdit && (
|
|
<button
|
|
onClick={() => {
|
|
setIsEditing(true);
|
|
}}
|
|
className="px-3 py-1 text-sm text-blue-600 hover:text-blue-700"
|
|
>
|
|
Edit
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Workspace ID */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Workspace ID</label>
|
|
<code className="block px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm text-gray-600">
|
|
{workspace.id}
|
|
</code>
|
|
</div>
|
|
|
|
{/* Created Date */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Created</label>
|
|
<p className="text-gray-600">{new Date(workspace.createdAt).toLocaleString()}</p>
|
|
</div>
|
|
|
|
{/* Delete Workspace */}
|
|
{canDelete && (
|
|
<div className="pt-6 border-t border-gray-200">
|
|
<h3 className="text-sm font-medium text-red-700 mb-2">Danger Zone</h3>
|
|
<p className="text-sm text-gray-600 mb-4">
|
|
Deleting this workspace will permanently remove all associated data, including tasks,
|
|
events, and projects. This action cannot be undone.
|
|
</p>
|
|
{showDeleteConfirm ? (
|
|
<div className="space-y-3">
|
|
<p className="text-sm font-medium text-gray-900">
|
|
Are you sure you want to delete this workspace?
|
|
</p>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={handleDelete}
|
|
disabled={isDeleting}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50"
|
|
>
|
|
{isDeleting ? "Deleting..." : "Yes, Delete Workspace"}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setShowDeleteConfirm(false);
|
|
}}
|
|
disabled={isDeleting}
|
|
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 disabled:opacity-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={() => {
|
|
setShowDeleteConfirm(true);
|
|
}}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
|
>
|
|
Delete Workspace
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|