Files
stack/apps/web/src/components/workspace/WorkspaceSettings.tsx
T
Jason WoltjeandClaude Opus 4.6 014264c592 fix(SEC-WEB-32+34): Add input maxLength limits + API request timeout
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]>
2026-02-06 18:11:00 -06:00

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>
);
}