feat(settings): add Custom Fields tab with localStorage schema + task detail panel rendering

This commit is contained in:
2026-07-29 19:26:44 +00:00
parent b314bda934
commit c704916059
2 changed files with 341 additions and 0 deletions
@@ -90,6 +90,7 @@ export function TaskDetailPanel({
const [estimatedMinutes, setEstimatedMinutes] = useState('');
const [recurrenceType, setRecurrenceType] = useState('');
const [customRrule, setCustomRrule] = useState('');
const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({});
const [saving, setSaving] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
@@ -128,6 +129,9 @@ export function TaskDetailPanel({
setRecurrenceType('');
setCustomRrule('');
}
// Custom fields
setCustomFieldValues(data.customFields || {});
})
.catch((err) => {
console.error('Failed to load task:', err);
@@ -163,6 +167,12 @@ export function TaskDetailPanel({
body.recurrenceRule = recurrenceRule;
}
// Include custom fields if changed
const currentCustomFields = task.customFields || {};
if (JSON.stringify(customFieldValues) !== JSON.stringify(currentCustomFields)) {
body.customFields = customFieldValues;
}
const response = await fetch(`/api/domains/${domainId}/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
@@ -326,6 +336,83 @@ export function TaskDetailPanel({
)}
</div>
{/* Custom fields */}
{(() => {
const schemas = (() => {
try {
if (typeof window === 'undefined') return [];
const raw = localStorage.getItem('pe_custom_field_schemas');
return raw ? JSON.parse(raw) : [];
} catch { return []; }
})();
const taskSchemas = schemas.filter((s: any) => s.scope === 'task');
if (taskSchemas.length === 0) return null;
return (
<div className="space-y-3">
<Label>Custom fields</Label>
{taskSchemas.map((field: any) => (
<div key={field.id} className="space-y-1">
<Label className="text-xs text-muted-foreground">{field.name}</Label>
{field.type === 'checkbox' ? (
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={!!customFieldValues[field.name]}
onChange={(e) =>
setCustomFieldValues((prev) => ({
...prev,
[field.name]: e.target.checked,
}))
}
className="h-4 w-4 rounded border-gray-300"
/>
<span className="text-sm">{field.name}</span>
</div>
) : field.type === 'select' ? (
<Select
value={(customFieldValues[field.name] as string) || ''}
onValueChange={(v) =>
setCustomFieldValues((prev) => ({ ...prev, [field.name]: v }))
}
>
<SelectTrigger>
<SelectValue placeholder="Select..." />
</SelectTrigger>
<SelectContent>
{(field.options || []).map((opt: string) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
</SelectContent>
</Select>
) : field.type === 'number' ? (
<Input
type="number"
value={(customFieldValues[field.name] as string) || ''}
onChange={(e) =>
setCustomFieldValues((prev) => ({
...prev,
[field.name]: e.target.value ? Number(e.target.value) : '',
}))
}
placeholder={`Enter ${field.name}...`}
/>
) : (
<Input
value={(customFieldValues[field.name] as string) || ''}
onChange={(e) =>
setCustomFieldValues((prev) => ({ ...prev, [field.name]: e.target.value }))
}
placeholder={`Enter ${field.name}...`}
/>
)}
</div>
))}
</div>
);
})()}
{/* Tags */}
{task.tags && task.tags.length > 0 && (
<div className="space-y-2">