TagAccessDialog Component

The TagAccessDialog component provides a comprehensive interface for creating and editing tags with access control permissions.

Overview

This component offers a modal dialog for tag management, combining tag metadata (name, description) with sophisticated access control configuration. It supports both creating new tags and editing existing ones with full validation and state management.

Location

src/components/TagAccessDialog.tsx

Features

Tag Management

  • Create/Edit Mode: Single component handles both creation and editing
  • Name Validation: Required field with trimming and validation
  • Optional Description: Rich text description for tag purpose
  • Automatic ID Generation: Creates URL-friendly IDs from tag names

Access Control Integration

  • Full AccessSelector: Complete access permission configuration
  • Permission Types: Support for all access control types (private, public, domain, etc.)
  • User Domain Context: Organization-aware permission settings
  • Real-time Updates: Immediate feedback on permission changes

Dialog Management

  • Responsive Layout: Adapts to screen size with proper overflow handling
  • State Persistence: Maintains form state during editing
  • Form Reset: Cleans up state when dialog closes
  • Validation Feedback: Visual indicators for form validity

Props Interface

interface TagAccessDialogProps {
  isOpen: boolean;
  onOpenChange: (open: boolean) => void;
  tag?: Tag;                    // Existing tag for editing (optional)
  onSave: (tag: Tag) => void;   // Callback when tag is saved
  userDomain?: string;          // User's organization domain
  availableTags?: Tag[];        // Context for validation (optional)
}

Tag Data Structure

interface Tag {
  id: string;
  name: string;
  description?: string;
  accessControl: AccessControl;
  createdBy: string;
  createdAt: number;
  updatedAt: number;
}

Usage Examples

Creating New Tags

import { TagAccessDialog } from '@/components/TagAccessDialog';

function TagManager() {
  const [dialogOpen, setDialogOpen] = useState(false);
  
  const handleSaveTag = (tag) => {
    // Set createdBy field
    const completeTag = {
      ...tag,
      createdBy: currentUser.email
    };
    
    // Save to database
    await saveTag(completeTag);
    
    // Update local state
    setTags(prev => [...prev, completeTag]);
  };

  return (
    <>
      <button onClick={() => setDialogOpen(true)}>
        Create Tag
      </button>
      
      <TagAccessDialog
        isOpen={dialogOpen}
        onOpenChange={setDialogOpen}
        onSave={handleSaveTag}
        userDomain="example.com"
      />
    </>
  );
}

Editing Existing Tags

function TagEditButton({ tag }) {
  const [editOpen, setEditOpen] = useState(false);
  
  const handleUpdateTag = (updatedTag) => {
    // Update in database and local state
    updateTag(updatedTag);
  };

  return (
    <>
      <button onClick={() => setEditOpen(true)}>
        Edit Tag
      </button>
      
      <TagAccessDialog
        isOpen={editOpen}
        onOpenChange={setEditOpen}
        tag={tag}
        onSave={handleUpdateTag}
        userDomain={userDomain}
        availableTags={allTags}
      />
    </>
  );
}

State Management

Form State

const [name, setName] = useState(tag?.name || '');
const [description, setDescription] = useState(tag?.description || '');
const [accessControl, setAccessControl] = useState<AccessControl>(
  tag?.accessControl || { type: 'private' }
);

State Reset Effect

useEffect(() => {
  if (isOpen && tag) {
    // Editing existing tag
    setName(tag.name);
    setDescription(tag.description || '');
    setAccessControl(tag.accessControl);
  } else if (isOpen && !tag) {
    // Creating new tag
    setName('');
    setDescription('');
    setAccessControl({ type: 'private' });
  }
}, [isOpen, tag]);

Form Components

Tag Name Input

<div className="space-y-2">
  <Label htmlFor="tag-name">Tag Name</Label>
  <input
    id="tag-name"
    type="text"
    value={name}
    onChange={(e) => setName(e.target.value)}
    placeholder="e.g., Finance, HR, Engineering"
    className="w-full px-3 py-2 border rounded-md bg-background"
  />
</div>

Description Field

<div className="space-y-2">
  <Label htmlFor="tag-description">Description (optional)</Label>
  <textarea
    id="tag-description"
    value={description}
    onChange={(e) => setDescription(e.target.value)}
    placeholder="Describe what this tag is used for..."
    rows={3}
    className="w-full px-3 py-2 border rounded-md bg-background resize-none"
  />
</div>

Access Control Section

<div className="space-y-4">
  <Label>Access Permissions</Label>
  <p className="text-sm text-muted-foreground">
    Choose who can see and use resources with this tag
  </p>
  <AccessSelector
    value={accessControl.type}
    emails={accessControl.emails}
    domains={accessControl.domains}
    groups={accessControl.groups}
    userDomain={userDomain}
    onChange={setAccessControl}
  />
</div>

Save Functionality

Data Processing

const handleSave = () => {
  if (!name.trim()) return;

  const now = Date.now();
  const tagData: Tag = {
    id: tag?.id || name.toLowerCase().replace(/\s+/g, '-'),
    name: name.trim(),
    description: description.trim() || undefined,
    accessControl,
    createdBy: tag?.createdBy || '', // Should be set by parent
    createdAt: tag?.createdAt || now,
    updatedAt: now
  };

  onSave(tagData);
  onOpenChange(false);
};

Validation

const isValid = name.trim().length > 0;

Dialog Layout

Responsive Structure

<DialogContent className="w-[calc(100%-32px)] sm:max-w-lg md:max-w-xl max-h-[95vh] flex flex-col overflow-hidden">
  <DialogHeader className="px-1 shrink-0">
    {/* Title and description */}
  </DialogHeader>

  <div className="flex-1 overflow-y-auto overflow-x-hidden pr-3 py-2 -mr-3 min-h-0 space-y-6">
    {/* Form content */}
  </div>

  <DialogFooter className="mt-4 shrink-0 border-t pt-4">
    {/* Action buttons */}
  </DialogFooter>
</DialogContent>

Header Component

<DialogHeader>
  <DialogTitle className="flex items-center gap-2">
    <TagIcon className="h-5 w-5" />
    {tag ? 'Edit Tag' : 'Create Tag'}
  </DialogTitle>
  <DialogDescription>
    {tag 
      ? 'Update tag settings and access permissions'
      : 'Create a new tag to organize assistants and tools'
    }
  </DialogDescription>
</DialogHeader>
<DialogFooter className="flex justify-end gap-2 flex-wrap sm:flex-nowrap">
  <Button 
    variant="outline" 
    onClick={() => onOpenChange(false)}
    className="w-full sm:w-auto"
  >
    Cancel
  </Button>
  <Button 
    onClick={handleSave}
    disabled={!isValid}
    className="w-full sm:w-auto"
  >
    {tag ? 'Save Changes' : 'Create Tag'}
  </Button>
</DialogFooter>

ID Generation

Automatic ID Creation

// For new tags, generate ID from name
id: tag?.id || name.toLowerCase().replace(/\s+/g, '-')

Examples

  • “Engineering Team” → “engineering-team”
  • “Finance & Operations” → “finance-&-operations”
  • “Product Management” → “product-management”

Accessibility

Form Accessibility

  • Associated Labels: All inputs have proper label associations
  • Placeholder Text: Helpful hints for form completion
  • Keyboard Navigation: Tab order follows logical form flow
  • Focus Management: Dialog traps focus appropriately

ARIA Support

  • Dialog Role: Proper modal dialog semantics
  • Descriptive Text: Clear descriptions for complex sections
  • Button States: Disabled states clearly indicated

Responsive Design

Layout Adaptations

  • Mobile Width: Full width minus margins on mobile
  • Desktop Constraints: Maximum width restrictions for readability
  • Scrollable Content: Form content scrolls when needed
  • Flexible Footer: Buttons stack on mobile, inline on desktop

Overflow Handling

// Scrollable form area with proper overflow handling
className="flex-1 overflow-y-auto overflow-x-hidden pr-3 py-2 -mr-3 min-h-0"

Dependencies

  • @/components/ui/dialog - Modal dialog components
  • @/components/ui/button - Action buttons
  • @/components/ui/label - Form labels
  • @/components/ui/separator - Visual section separation
  • @/components/AccessSelector - Access control configuration
  • @/components/TagSelector - Tag selection (imported but not used in current implementation)
  • @/lib/firebase - AccessControl and Tag type definitions
  • lucide-react - Tag icon

Error Handling

Validation States

  • Empty Name: Save button disabled when name is empty
  • Trim Whitespace: Automatic whitespace removal
  • Optional Fields: Description can be empty

Form Reset

  • Dialog Close: Form state persists until explicitly reset
  • Successful Save: Dialog closes automatically after save
  • Cancel Action: Discards changes and closes dialog
  • AccessSelector - Provides access control configuration
  • TagSelector - Used for tag selection in other contexts
  • Tag management interfaces and administrative tools
  • Resource tagging systems

Best Practices

  1. Set Created By: Parent component should set the createdBy field
  2. Validate Permissions: Ensure user has permission to create/edit tags
  3. Handle Conflicts: Check for duplicate tag names when appropriate
  4. Provide Context: Include available tags for validation when possible
  5. Responsive Design: Test dialog on various screen sizes
  6. Accessibility: Support keyboard navigation and screen readers
  7. State Management: Clean up form state appropriately