feat(web): add workspace management UI (M2 #12)
- Create workspace listing page at /settings/workspaces - List all user workspaces with role badges - Create new workspace functionality - Display member count per workspace - Create workspace detail page at /settings/workspaces/[id] - Workspace settings (name, ID, created date) - Member management with role editing - Invite member functionality - Delete workspace (owner only) - Add workspace components: - WorkspaceCard: Display workspace info with role badge - WorkspaceSettings: Edit workspace settings and delete - MemberList: Display and manage workspace members - InviteMember: Send invitations with role selection - Add WorkspaceMemberWithUser type to shared package - Follow existing app patterns for styling and structure - Use mock data (ready for API integration)
This commit is contained in:
143
apps/web/src/components/knowledge/EntryMetadata.tsx
Normal file
143
apps/web/src/components/knowledge/EntryMetadata.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import type { KnowledgeTag } from "@mosaic/shared";
|
||||
import { EntryStatus, Visibility } from "@mosaic/shared";
|
||||
|
||||
interface EntryMetadataProps {
|
||||
title: string;
|
||||
status: EntryStatus;
|
||||
visibility: Visibility;
|
||||
selectedTags: string[];
|
||||
availableTags: KnowledgeTag[];
|
||||
onTitleChange: (title: string) => void;
|
||||
onStatusChange: (status: EntryStatus) => void;
|
||||
onVisibilityChange: (visibility: Visibility) => void;
|
||||
onTagsChange: (tags: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* EntryMetadata - Title, tags, status, and visibility controls
|
||||
*/
|
||||
export function EntryMetadata({
|
||||
title,
|
||||
status,
|
||||
visibility,
|
||||
selectedTags,
|
||||
availableTags,
|
||||
onTitleChange,
|
||||
onStatusChange,
|
||||
onVisibilityChange,
|
||||
onTagsChange,
|
||||
}: EntryMetadataProps): JSX.Element {
|
||||
const handleTagToggle = (tagId: string): void => {
|
||||
if (selectedTags.includes(tagId)) {
|
||||
onTagsChange(selectedTags.filter((id) => id !== tagId));
|
||||
} else {
|
||||
onTagsChange([...selectedTags, tagId]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="entry-metadata space-y-4">
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="entry-title"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Title
|
||||
</label>
|
||||
<input
|
||||
id="entry-title"
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => onTitleChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Entry title..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status and Visibility Row */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Status */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="entry-status"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
id="entry-status"
|
||||
value={status}
|
||||
onChange={(e) => onStatusChange(e.target.value as EntryStatus)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value={EntryStatus.DRAFT}>Draft</option>
|
||||
<option value={EntryStatus.PUBLISHED}>Published</option>
|
||||
<option value={EntryStatus.ARCHIVED}>Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Visibility */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="entry-visibility"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Visibility
|
||||
</label>
|
||||
<select
|
||||
id="entry-visibility"
|
||||
value={visibility}
|
||||
onChange={(e) => onVisibilityChange(e.target.value as Visibility)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value={Visibility.PRIVATE}>Private</option>
|
||||
<option value={Visibility.WORKSPACE}>Workspace</option>
|
||||
<option value={Visibility.PUBLIC}>Public</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Tags
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableTags.length > 0 ? (
|
||||
availableTags.map((tag) => {
|
||||
const isSelected = selectedTags.includes(tag.id);
|
||||
return (
|
||||
<button
|
||||
key={tag.id}
|
||||
type="button"
|
||||
onClick={() => handleTagToggle(tag.id)}
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
|
||||
isSelected
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
}`}
|
||||
style={
|
||||
isSelected && tag.color
|
||||
? { backgroundColor: tag.color }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{tag.name}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No tags available. Create tags first.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user