mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-08 16:48:48 -04:00
Tweak custom field display, tweak details UI (#1404)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface EditableFieldLabelProps {
|
||||
htmlFor: string;
|
||||
label: string;
|
||||
onLabelChange: (newLabel: string) => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Editable field label component with edit button.
|
||||
* Shows label text with a small edit icon. When clicked, shows an input field.
|
||||
*/
|
||||
const EditableFieldLabel: React.FC<EditableFieldLabelProps> = ({
|
||||
htmlFor,
|
||||
label,
|
||||
onLabelChange,
|
||||
onDelete
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(label);
|
||||
|
||||
const handleSave = () => {
|
||||
if (editValue.trim()) {
|
||||
onLabelChange(editValue.trim());
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditValue(label);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
className="flex-1 px-2 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 border border-primary-500 rounded focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:border-primary-400"
|
||||
placeholder="Field label"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
className="text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 text-xs px-2 py-1"
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 text-xs px-2 py-1"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<label htmlFor={htmlFor} className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-xs"
|
||||
title="Edit label"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
</button>
|
||||
{onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 text-xs"
|
||||
title="Delete field"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditableFieldLabel;
|
||||
@@ -5,6 +5,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import Modal from '@/entrypoints/popup/components/Dialogs/Modal';
|
||||
import EditableFieldLabel from '@/entrypoints/popup/components/Forms/EditableFieldLabel';
|
||||
import { FormInput } from '@/entrypoints/popup/components/Forms/FormInput';
|
||||
import PasswordField from '@/entrypoints/popup/components/Forms/PasswordField';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
@@ -28,6 +29,17 @@ type ItemFormData = {
|
||||
Fields: Record<string, string | string[]>; // FieldKey -> Value mapping
|
||||
};
|
||||
|
||||
/**
|
||||
* Temporary custom field definition (before persisting to database)
|
||||
*/
|
||||
type CustomFieldDefinition = {
|
||||
tempId: string; // Temporary ID until we create the FieldDefinition
|
||||
label: string;
|
||||
fieldType: FieldType;
|
||||
isHidden: boolean;
|
||||
displayOrder: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add or edit item page with dynamic field support.
|
||||
* Shows all applicable system fields for the item type, not just fields with values.
|
||||
@@ -44,11 +56,19 @@ const ItemAddEdit: React.FC = () => {
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [localLoading, setLocalLoading] = useState(true);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [showAddCustomFieldModal, setShowAddCustomFieldModal] = useState(false);
|
||||
const [item, setItem] = useState<Item | null>(null);
|
||||
|
||||
// Form state for dynamic fields
|
||||
const [fieldValues, setFieldValues] = useState<Record<string, string | string[]>>({});
|
||||
|
||||
// Custom field definitions (temporary until saved)
|
||||
const [customFields, setCustomFields] = useState<CustomFieldDefinition[]>([]);
|
||||
|
||||
// New custom field form state
|
||||
const [newCustomFieldLabel, setNewCustomFieldLabel] = useState('');
|
||||
const [newCustomFieldType, setNewCustomFieldType] = useState<FieldType>('Text');
|
||||
|
||||
/**
|
||||
* Get all applicable system fields for the current item type.
|
||||
* These are sorted by DefaultDisplayOrder.
|
||||
@@ -101,10 +121,25 @@ const ItemAddEdit: React.FC = () => {
|
||||
|
||||
// Initialize field values from existing fields
|
||||
const initialValues: Record<string, string | string[]> = {};
|
||||
const existingCustomFields: CustomFieldDefinition[] = [];
|
||||
|
||||
result.Fields.forEach(field => {
|
||||
initialValues[field.FieldKey] = field.Value;
|
||||
|
||||
// If field key starts with "custom_", it's a custom field
|
||||
if (field.FieldKey.startsWith('custom_')) {
|
||||
existingCustomFields.push({
|
||||
tempId: field.FieldKey,
|
||||
label: field.Label,
|
||||
fieldType: field.FieldType,
|
||||
isHidden: field.IsHidden,
|
||||
displayOrder: field.DisplayOrder
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setFieldValues(initialValues);
|
||||
setCustomFields(existingCustomFields);
|
||||
|
||||
setLocalLoading(false);
|
||||
setIsInitialLoading(false);
|
||||
@@ -139,6 +174,7 @@ const ItemAddEdit: React.FC = () => {
|
||||
// Build the fields array from fieldValues
|
||||
const fields: ItemField[] = [];
|
||||
|
||||
// Add system fields
|
||||
applicableSystemFields.forEach(systemField => {
|
||||
const value = fieldValues[systemField.FieldKey];
|
||||
|
||||
@@ -155,6 +191,23 @@ const ItemAddEdit: React.FC = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Add custom fields
|
||||
customFields.forEach(customField => {
|
||||
const value = fieldValues[customField.tempId];
|
||||
|
||||
// Only include fields with non-empty values
|
||||
if (value && (Array.isArray(value) ? value.length > 0 : value.trim() !== '')) {
|
||||
fields.push({
|
||||
FieldKey: customField.tempId,
|
||||
Label: customField.label,
|
||||
FieldType: customField.fieldType,
|
||||
Value: value,
|
||||
IsHidden: customField.isHidden,
|
||||
DisplayOrder: customField.displayOrder
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const updatedItem: Item = {
|
||||
...item,
|
||||
Fields: fields,
|
||||
@@ -216,6 +269,48 @@ const ItemAddEdit: React.FC = () => {
|
||||
}
|
||||
}, [isEditMode, id, navigate]);
|
||||
|
||||
/**
|
||||
* Add custom field handler.
|
||||
*/
|
||||
const handleAddCustomField = useCallback(() => {
|
||||
if (!newCustomFieldLabel.trim()) return;
|
||||
|
||||
const tempId = `custom_${crypto.randomUUID()}`;
|
||||
const newField: CustomFieldDefinition = {
|
||||
tempId,
|
||||
label: newCustomFieldLabel,
|
||||
fieldType: newCustomFieldType,
|
||||
isHidden: false,
|
||||
displayOrder: applicableSystemFields.length + customFields.length + 1
|
||||
};
|
||||
|
||||
setCustomFields(prev => [...prev, newField]);
|
||||
setNewCustomFieldLabel('');
|
||||
setNewCustomFieldType('Text');
|
||||
setShowAddCustomFieldModal(false);
|
||||
}, [newCustomFieldLabel, newCustomFieldType, applicableSystemFields.length, customFields.length]);
|
||||
|
||||
/**
|
||||
* Delete custom field handler.
|
||||
*/
|
||||
const handleDeleteCustomField = useCallback((tempId: string) => {
|
||||
setCustomFields(prev => prev.filter(f => f.tempId !== tempId));
|
||||
setFieldValues(prev => {
|
||||
const newValues = { ...prev };
|
||||
delete newValues[tempId];
|
||||
return newValues;
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Update custom field label handler.
|
||||
*/
|
||||
const handleUpdateCustomFieldLabel = useCallback((tempId: string, newLabel: string) => {
|
||||
setCustomFields(prev => prev.map(f =>
|
||||
f.tempId === tempId ? { ...f, label: newLabel } : f
|
||||
));
|
||||
}, []);
|
||||
|
||||
// Set header buttons
|
||||
useEffect(() => {
|
||||
const headerButtonsJSX = isEditMode ? (
|
||||
@@ -370,6 +465,44 @@ const ItemAddEdit: React.FC = () => {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Custom Fields Section */}
|
||||
{customFields.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
{t('credentials.customFields')}
|
||||
</h2>
|
||||
|
||||
{customFields.map(field => (
|
||||
<div key={field.tempId}>
|
||||
<EditableFieldLabel
|
||||
htmlFor={field.tempId}
|
||||
label={field.label}
|
||||
onLabelChange={(newLabel) => handleUpdateCustomFieldLabel(field.tempId, newLabel)}
|
||||
onDelete={() => handleDeleteCustomField(field.tempId)}
|
||||
/>
|
||||
|
||||
{/* Field input */}
|
||||
{renderFieldInput(
|
||||
field.tempId,
|
||||
'',
|
||||
field.fieldType,
|
||||
field.isHidden,
|
||||
false
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Custom Field Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddCustomFieldModal(true)}
|
||||
className="w-full px-4 py-3 border-2 border-dashed border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 rounded-md hover:border-primary-500 hover:text-primary-600 dark:hover:text-primary-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors"
|
||||
>
|
||||
+ Add Custom Field
|
||||
</button>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
@@ -390,6 +523,75 @@ const ItemAddEdit: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add Custom Field Dialog */}
|
||||
{showAddCustomFieldModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96 max-w-full mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Add Custom Field
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Field Label
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newCustomFieldLabel}
|
||||
onChange={(e) => setNewCustomFieldLabel(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Enter field name"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Field Type
|
||||
</label>
|
||||
<select
|
||||
value={newCustomFieldType}
|
||||
onChange={(e) => setNewCustomFieldType(e.target.value as FieldType)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="Text">Text</option>
|
||||
<option value="Password">Hidden (masked text)</option>
|
||||
<option value="Email">Email</option>
|
||||
<option value="URL">URL</option>
|
||||
<option value="Phone">Phone</option>
|
||||
<option value="Number">Number</option>
|
||||
<option value="Date">Date</option>
|
||||
<option value="TextArea">Text Area</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddCustomField}
|
||||
disabled={!newCustomFieldLabel.trim()}
|
||||
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowAddCustomFieldModal(false);
|
||||
setNewCustomFieldLabel('');
|
||||
setNewCustomFieldType('Text');
|
||||
}}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{isEditMode && (
|
||||
<Modal
|
||||
|
||||
@@ -17,6 +17,7 @@ import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import type { Item } from '@/utils/dist/shared/models/vault';
|
||||
import { groupFieldsByCategory } from '@/utils/dist/shared/models/vault';
|
||||
import SqliteClient from '@/utils/SqliteClient';
|
||||
|
||||
/**
|
||||
* Item details page with dynamic field rendering.
|
||||
@@ -102,27 +103,68 @@ const ItemDetails: React.FC = (): React.ReactElement => {
|
||||
return <div>{t('common.loading')}</div>;
|
||||
}
|
||||
|
||||
// Group fields by category for organized display
|
||||
const groupedFields = groupFieldsByCategory(item);
|
||||
// Extract URL fields for prominent display
|
||||
const urlFields = item.Fields.filter(field => field.FieldType === 'URL' && field.Value);
|
||||
|
||||
// Create a modified item without URL fields for grouping
|
||||
const itemWithoutUrls = {
|
||||
...item,
|
||||
Fields: item.Fields.filter(field => field.FieldType !== 'URL')
|
||||
};
|
||||
|
||||
// Group fields by category for organized display (excluding URLs)
|
||||
const groupedFields = groupFieldsByCategory(itemWithoutUrls);
|
||||
|
||||
console.log('Grouped fields:', groupedFields);
|
||||
console.log('Item:', item);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header with name and logo */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
{item.Logo && (
|
||||
<img
|
||||
src={`data:image/png;base64,${btoa(String.fromCharCode(...Array.from(item.Logo as Uint8Array)))}`}
|
||||
alt={item.Name || 'Item'}
|
||||
className="w-10 h-10 rounded"
|
||||
/>
|
||||
)}
|
||||
<h1 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{item.Name || 'Untitled Item'}
|
||||
</h1>
|
||||
{/* Header with name, logo, and URLs */}
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-start gap-3">
|
||||
<img
|
||||
src={SqliteClient.imgSrcFromBytes(item.Logo as Uint8Array | undefined)}
|
||||
alt={item.Name || 'Item'}
|
||||
className="w-12 h-12 rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{item.Name || 'Untitled Item'}
|
||||
</h1>
|
||||
{/* Display URLs prominently below title */}
|
||||
{urlFields.length > 0 && (
|
||||
<div className="mt-1 space-y-1">
|
||||
{urlFields.flatMap((urlField) => {
|
||||
// Handle both single values and arrays of URLs
|
||||
const urlValues = Array.isArray(urlField.Value) ? urlField.Value : [urlField.Value];
|
||||
|
||||
return urlValues.map((urlValue, idx) => {
|
||||
const isValidUrl = /^https?:\/\//i.test(urlValue);
|
||||
|
||||
return isValidUrl ? (
|
||||
<a
|
||||
key={`${urlField.FieldKey}-${idx}`}
|
||||
href={urlValue}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 break-all text-sm"
|
||||
>
|
||||
{urlValue}
|
||||
</a>
|
||||
) : (
|
||||
<span
|
||||
key={`${urlField.FieldKey}-${idx}`}
|
||||
className="block text-gray-500 dark:text-gray-300 break-all text-sm"
|
||||
>
|
||||
{urlValue}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -436,7 +436,7 @@ export class SqliteClient {
|
||||
// Custom field: has FieldDefinitionId, get metadata from FieldDefinitions
|
||||
return {
|
||||
ItemId: row.ItemId,
|
||||
FieldKey: `custom_${row.FieldDefinitionId}`, // Generate a key for custom fields
|
||||
FieldKey: row.FieldDefinitionId || '', // Use FieldDefinitionId as the key for custom fields
|
||||
Label: row.CustomLabel || '',
|
||||
FieldType: row.CustomFieldType || 'Text',
|
||||
IsHidden: row.CustomIsHidden || 0,
|
||||
@@ -581,7 +581,7 @@ export class SqliteClient {
|
||||
} else {
|
||||
// Custom field: has FieldDefinitionId, get metadata from FieldDefinitions
|
||||
return {
|
||||
FieldKey: `custom_${row.FieldDefinitionId}`, // Generate a key for custom fields
|
||||
FieldKey: row.FieldDefinitionId || '', // Use FieldDefinitionId as the key for custom fields
|
||||
Label: row.CustomLabel || '',
|
||||
FieldType: row.CustomFieldType || 'Text',
|
||||
IsHidden: row.CustomIsHidden || 0,
|
||||
@@ -1953,22 +1953,54 @@ export class SqliteClient {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isCustomField = field.FieldKey.startsWith('custom_');
|
||||
let fieldDefinitionId = null;
|
||||
|
||||
// For custom fields, create or get FieldDefinition
|
||||
if (isCustomField) {
|
||||
// Check if FieldDefinition already exists for this custom field
|
||||
const existingDefQuery = `
|
||||
SELECT Id FROM FieldDefinitions
|
||||
WHERE Id = ?`;
|
||||
|
||||
const existingDef = this.executeQuery<{ Id: string }>(existingDefQuery, [field.FieldKey]);
|
||||
|
||||
if (existingDef.length === 0) {
|
||||
// Create new FieldDefinition for custom field
|
||||
const fieldDefQuery = `
|
||||
INSERT INTO FieldDefinitions (Id, FieldType, Label, IsMultiValue, IsHidden, EnableHistory, Weight, ApplicableToTypes, CreatedAt, UpdatedAt, IsDeleted)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
this.executeUpdate(fieldDefQuery, [
|
||||
field.FieldKey, // Use the custom_ ID as the FieldDefinition ID
|
||||
field.FieldType,
|
||||
field.Label,
|
||||
0, // IsMultiValue
|
||||
field.IsHidden ? 1 : 0,
|
||||
0, // EnableHistory
|
||||
field.DisplayOrder ?? 0,
|
||||
item.ItemType, // ApplicableToTypes (single type for now)
|
||||
currentDateTime,
|
||||
currentDateTime,
|
||||
0
|
||||
]);
|
||||
}
|
||||
|
||||
fieldDefinitionId = field.FieldKey; // FieldDefinitionId = custom field ID
|
||||
}
|
||||
|
||||
const fieldValueId = crypto.randomUUID().toUpperCase();
|
||||
const fieldQuery = `
|
||||
INSERT INTO FieldValues (Id, ItemId, FieldDefinitionId, FieldKey, Value, Weight, CreatedAt, UpdatedAt, IsDeleted)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
/*
|
||||
* For system fields: FieldKey is set, FieldDefinitionId is NULL
|
||||
* For custom fields: FieldDefinitionId is set, FieldKey is NULL (future implementation)
|
||||
*/
|
||||
const valueString = Array.isArray(field.Value) ? JSON.stringify(field.Value) : field.Value;
|
||||
|
||||
this.executeUpdate(fieldQuery, [
|
||||
fieldValueId,
|
||||
itemId,
|
||||
null, // FieldDefinitionId (NULL for system fields)
|
||||
field.FieldKey, // FieldKey (set for system fields)
|
||||
fieldDefinitionId, // NULL for system fields, custom field ID for custom fields
|
||||
isCustomField ? null : field.FieldKey, // FieldKey set for system fields only
|
||||
valueString,
|
||||
field.DisplayOrder ?? 0,
|
||||
currentDateTime,
|
||||
@@ -2036,6 +2068,61 @@ export class SqliteClient {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isCustomField = field.FieldKey.startsWith('custom_');
|
||||
let fieldDefinitionId = null;
|
||||
|
||||
// For custom fields, create or update FieldDefinition
|
||||
if (isCustomField) {
|
||||
// Check if FieldDefinition already exists
|
||||
const existingDefQuery = `
|
||||
SELECT Id FROM FieldDefinitions
|
||||
WHERE Id = ? AND IsDeleted = 0`;
|
||||
|
||||
const existingDef = this.executeQuery<{ Id: string }>(existingDefQuery, [field.FieldKey]);
|
||||
|
||||
if (existingDef.length === 0) {
|
||||
// Create new FieldDefinition
|
||||
const fieldDefQuery = `
|
||||
INSERT INTO FieldDefinitions (Id, FieldType, Label, IsMultiValue, IsHidden, EnableHistory, Weight, ApplicableToTypes, CreatedAt, UpdatedAt, IsDeleted)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
this.executeUpdate(fieldDefQuery, [
|
||||
field.FieldKey,
|
||||
field.FieldType,
|
||||
field.Label,
|
||||
0, // IsMultiValue
|
||||
field.IsHidden ? 1 : 0,
|
||||
0, // EnableHistory
|
||||
field.DisplayOrder ?? 0,
|
||||
item.ItemType,
|
||||
currentDateTime,
|
||||
currentDateTime,
|
||||
0
|
||||
]);
|
||||
} else {
|
||||
// Update existing FieldDefinition (label might have changed)
|
||||
const updateDefQuery = `
|
||||
UPDATE FieldDefinitions
|
||||
SET Label = ?,
|
||||
FieldType = ?,
|
||||
IsHidden = ?,
|
||||
Weight = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
|
||||
this.executeUpdate(updateDefQuery, [
|
||||
field.Label,
|
||||
field.FieldType,
|
||||
field.IsHidden ? 1 : 0,
|
||||
field.DisplayOrder ?? 0,
|
||||
currentDateTime,
|
||||
field.FieldKey
|
||||
]);
|
||||
}
|
||||
|
||||
fieldDefinitionId = field.FieldKey;
|
||||
}
|
||||
|
||||
const fieldValueId = crypto.randomUUID().toUpperCase();
|
||||
const fieldQuery = `
|
||||
INSERT INTO FieldValues (Id, ItemId, FieldDefinitionId, FieldKey, Value, Weight, CreatedAt, UpdatedAt, IsDeleted)
|
||||
@@ -2046,8 +2133,8 @@ export class SqliteClient {
|
||||
this.executeUpdate(fieldQuery, [
|
||||
fieldValueId,
|
||||
item.Id,
|
||||
null, // FieldDefinitionId (NULL for system fields)
|
||||
field.FieldKey, // FieldKey (set for system fields)
|
||||
fieldDefinitionId, // NULL for system fields, custom field ID for custom fields
|
||||
isCustomField ? null : field.FieldKey, // FieldKey set for system fields only
|
||||
valueString,
|
||||
field.DisplayOrder ?? 0,
|
||||
currentDateTime,
|
||||
|
||||
Reference in New Issue
Block a user