Project Editor

Form component for creating and editing projects with dual-API pattern

Live Example

Create New Project

Fill in the form to create a new project. The editor uses the dual-API pattern (Tastypie for write, GET API for refresh).

Edit Existing Project

Select a project from the autocomplete dropdown to edit. The editor will load the current data and allow you to update it.

Select a project to edit:

Editor in Drawer

Open the editor in a side drawer for a better user experience. This pattern is useful when you want to keep the context visible.

Project Editor

A form component for creating new projects or editing existing ones. Implements the dual-API pattern (Tastypie for writes, GET API for refresh).

Usage

Create New Project

import { ProjectEditor } from '@/components/sgerp/editors/project-editor';
import { useState } from 'react';

function MyComponent() {
  const [showEditor, setShowEditor] = useState(false);

  return (
    <>
      <button onClick={() => setShowEditor(true)}>
        Create Project
      </button>
      
      {showEditor && (
        <ProjectEditor
          onSuccess={(project) => {
            console.log('Created:', project);
            setShowEditor(false);
          }}
          onCancel={() => setShowEditor(false)}
        />
      )}
    </>
  );
}

Edit Existing Project (by ID)

import { ProjectEditor } from '@/components/sgerp/editors/project-editor';

function MyComponent() {
  const projectId = 123; // The project you want to edit

  return (
    <ProjectEditor
      projectId={projectId}
      onSuccess={(project) => {
        console.log('Updated:', project);
      }}
      onCancel={() => {
        // Handle cancel
      }}
    />
  );
}

Edit with Initial Data (No Fetch)

When you already have the project data (e.g., from a table), pass it directly to avoid unnecessary fetching:

import { ProjectEditor } from '@/components/sgerp/editors/project-editor';
import type { Project } from 'sgerp-frontend-lib/lib/sgerp/types/sharing/project';

function MyComponent({ project }: { project: Project }) {
  return (
    <ProjectEditor
      projectId={project.id}
      initialData={project}
      onSuccess={(updatedProject) => {
        console.log('Updated:', updatedProject);
      }}
      onCancel={() => {
        // Handle cancel
      }}
    />
  );
}

Features

  • Create Mode: When projectId is not provided, creates a new project
  • Edit Mode: When projectId is provided, loads and updates existing project
  • Dual-API Pattern: Writes to Tastypie API, refreshes from GET API for consistency
  • Form Validation: Required field validation (name is required)
  • Error Handling: Displays error messages from API
  • Loading States: Shows loading indicator while fetching/saving
  • Collection Integration: Automatically updates the project collection on success

Props

PropTypeRequiredDescription
projectIdnumberNoID of project to edit (omit for create mode)
initialDataProjectNoPre-loaded project data to avoid fetching. When provided, editor uses this data immediately without API call
onSuccess(project: Project) => voidNoCallback when save succeeds with fresh project data
onCancel() => voidNoCallback when user cancels editing

Form Fields

Required Fields

  • Name: Project name (string, required)

Optional Fields

  • Timezone: Timezone for date/time calculations (dropdown, defaults to UTC)
  • External ID: External identifier for imports (string)
  • Latitude: Default map location latitude (number)
  • Longitude: Default map location longitude (number)

Dual-API Implementation

The editor follows the dual-API pattern documented in CLAUDE.md:

  1. Write Operation: POST/PATCH to /api/v2/project/ (Tastypie API)
  2. Refresh Operation: GET from /api/v2/microservices/get?model=project&id={id} (GET API)
  3. Collection Update: Updates api.collections.project with fresh data

This ensures all data in collections maintains consistent format from the GET API.

Example: With Autocomplete Selection

Use SGERPAutocomplete to select a project to edit:

import { ProjectEditor } from '@/components/sgerp/editors/project-editor';
import { SGERPAutocomplete } from '@/components/sgerp/sgerp-autocomplete';
import { useState } from 'react';

function MyComponent() {
  const [editingProject, setEditingProject] = useState<number | null>(null);

  return (
    <div>
      {editingProject === null ? (
        <div className="space-y-4">
          <p>Select a project to edit:</p>
          <SGERPAutocomplete
            collectionName="project"
            onSelectionChange={(id) => id && setEditingProject(id)}
            renderLabel={(project) => project.name}
            placeholder="Search projects..."
          />
        </div>
      ) : (
        <ProjectEditor
          projectId={editingProject}
          onSuccess={(project) => {
            console.log('Updated:', project);
            setEditingProject(null);
          }}
          onCancel={() => setEditingProject(null)}
        />
      )}
    </div>
  );
}

Example: In a Drawer

A drawer pattern provides a better user experience by keeping context visible:

import { ProjectEditor } from '@/components/sgerp/editors/project-editor';
import { useState } from 'react';

function MyComponent() {
  const [drawerOpen, setDrawerOpen] = useState(false);

  return (
    <>
      <button onClick={() => setDrawerOpen(true)}>
        Create Project
      </button>
      
      {/* Drawer */}
      {drawerOpen && (
        <div className="fixed inset-0 z-50 overflow-hidden">
          {/* Backdrop */}
          <div 
            className="absolute inset-0 bg-black/50"
            onClick={() => setDrawerOpen(false)}
          />
          
          {/* Drawer */}
          <div className="absolute right-0 top-0 h-full w-full max-w-md bg-background shadow-xl">
            <div className="flex h-full flex-col">
              <div className="border-b px-6 py-4">
                <h2 className="text-lg font-semibold">Create Project</h2>
              </div>
              <div className="flex-1 overflow-y-auto px-6 py-4">
                <ProjectEditor
                  onSuccess={(project) => {
                    console.log('Created:', project);
                    setDrawerOpen(false);
                  }}
                  onCancel={() => setDrawerOpen(false)}
                />
              </div>
            </div>
          </div>
        </div>
      )}
    </>
  );
}

Example: In a Dialog

import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ProjectEditor } from '@/components/sgerp/editors/project-editor';
import { useState } from 'react';

function MyComponent() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>
        Create Project
      </button>
      
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Create New Project</DialogTitle>
          </DialogHeader>
          <ProjectEditor
            onSuccess={(project) => {
              console.log('Created:', project);
              setOpen(false);
            }}
            onCancel={() => setOpen(false)}
          />
        </DialogContent>
      </Dialog>
    </>
  );
}

Error Handling

The editor handles various error scenarios:

  • Network Errors: Displays error message from API response
  • Validation Errors: Browser validation for required fields
  • Loading Errors: Shows error if project fails to load in edit mode
<ProjectEditor
  projectId={123}
  onSuccess={(project) => {
    // Success toast
    toast.success(`Updated: ${project.name}`);
  }}
  onCancel={() => {
    // User cancelled
  }}
/>

Styling

The editor uses Tailwind CSS classes and inherits theme colors. All form elements are styled consistently with the design system.