Persist custom fields in edit mode even if they have no values (#1473)
This commit is contained in:
@@ -618,23 +618,20 @@ const ItemAddEdit: React.FC = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Add custom fields
|
||||
// Add custom fields - always persist even if empty (only deleted when explicitly removed)
|
||||
customFields.forEach(customField => {
|
||||
const value = fieldValues[customField.tempId];
|
||||
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,
|
||||
IsCustomField: true,
|
||||
EnableHistory: false // Custom fields don't have history enabled by default
|
||||
});
|
||||
}
|
||||
fields.push({
|
||||
FieldKey: customField.tempId,
|
||||
Label: customField.label,
|
||||
FieldType: customField.fieldType,
|
||||
Value: value,
|
||||
IsHidden: customField.isHidden,
|
||||
DisplayOrder: customField.displayOrder,
|
||||
IsCustomField: true,
|
||||
EnableHistory: false // Custom fields don't have history enabled by default
|
||||
});
|
||||
});
|
||||
|
||||
let updatedItem: Item = {
|
||||
@@ -1303,29 +1300,6 @@ const ItemAddEdit: React.FC = () => {
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Custom Fields Section */}
|
||||
{customFields.length > 0 && (
|
||||
<FormSection title={t('common.customFields')}>
|
||||
{customFields.map(field => (
|
||||
<div key={field.tempId}>
|
||||
<EditableFieldLabel
|
||||
htmlFor={field.tempId}
|
||||
label={field.label}
|
||||
onLabelChange={(newLabel) => handleUpdateCustomFieldLabel(field.tempId, newLabel)}
|
||||
onDelete={() => handleDeleteCustomField(field.tempId)}
|
||||
/>
|
||||
{renderFieldInput(
|
||||
field.tempId,
|
||||
'',
|
||||
field.fieldType,
|
||||
field.isHidden,
|
||||
false
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
{/* Notes Section */}
|
||||
{notesField && visibleFieldKeys.has('notes.content') && (
|
||||
<FormSection
|
||||
@@ -1356,6 +1330,29 @@ const ItemAddEdit: React.FC = () => {
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
{/* Custom Fields Section */}
|
||||
{customFields.length > 0 && (
|
||||
<FormSection title={t('common.customFields')}>
|
||||
{customFields.map(field => (
|
||||
<div key={field.tempId}>
|
||||
<EditableFieldLabel
|
||||
htmlFor={field.tempId}
|
||||
label={field.label}
|
||||
onLabelChange={(newLabel) => handleUpdateCustomFieldLabel(field.tempId, newLabel)}
|
||||
onDelete={() => handleDeleteCustomField(field.tempId)}
|
||||
/>
|
||||
{renderFieldInput(
|
||||
field.tempId,
|
||||
'',
|
||||
field.fieldType,
|
||||
field.isHidden,
|
||||
false
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
{/* 2FA TOTP Section - only for types with login fields */}
|
||||
{show2FA && hasLoginFields && (
|
||||
<TotpEditor
|
||||
|
||||
@@ -227,18 +227,7 @@ const ItemDetails: React.FC = (): React.ReactElement => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupedFields[FieldCategories.Custom] && groupedFields[FieldCategories.Custom].length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{t('common.customFields')}
|
||||
</h2>
|
||||
{groupedFields[FieldCategories.Custom].map((field) => (
|
||||
<FieldBlock key={field.FieldKey} field={field} itemId={item.Id} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notes - shown at bottom for non-Note types (metadata) */}
|
||||
{/* Notes - shown before custom fields for non-Note types */}
|
||||
{item.ItemType !== ItemTypes.Note && groupedFields[FieldCategories.Notes] && groupedFields[FieldCategories.Notes].length > 0 && (
|
||||
groupedFields[FieldCategories.Notes].map((field) => (
|
||||
<div key={field.FieldKey} className="space-y-2">
|
||||
@@ -249,6 +238,18 @@ const ItemDetails: React.FC = (): React.ReactElement => {
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{/* Custom Fields */}
|
||||
{groupedFields[FieldCategories.Custom] && groupedFields[FieldCategories.Custom].length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{t('common.customFields')}
|
||||
</h2>
|
||||
{groupedFields[FieldCategories.Custom].map((field) => (
|
||||
<FieldBlock key={field.FieldKey} field={field} itemId={item.Id} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"settings": "Settings",
|
||||
"recentEmails": "Recent emails",
|
||||
"credentials": "Credentials",
|
||||
"customFields": "Custom",
|
||||
"customFields": "Custom Fields",
|
||||
"twoFactorAuthentication": "Two-factor authentication",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
|
||||
@@ -436,8 +436,9 @@ export class ItemRepository extends BaseRepository {
|
||||
currentDateTime: string
|
||||
): void {
|
||||
for (const field of fields) {
|
||||
// Skip empty fields
|
||||
if (!field.Value || (typeof field.Value === 'string' && field.Value.trim() === '')) {
|
||||
// Skip empty system fields, but always persist custom fields (even if empty)
|
||||
const isEmpty = !field.Value || (typeof field.Value === 'string' && field.Value.trim() === '');
|
||||
if (isEmpty && !field.IsCustomField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -452,7 +453,12 @@ export class ItemRepository extends BaseRepository {
|
||||
const values = Array.isArray(field.Value) ? field.Value : [field.Value];
|
||||
const filteredValues = values.filter(v => v && v.trim() !== '');
|
||||
|
||||
for (const value of filteredValues) {
|
||||
// For custom fields with no values, insert with empty string to preserve the field
|
||||
const valuesToInsert = field.IsCustomField && filteredValues.length === 0
|
||||
? ['']
|
||||
: filteredValues;
|
||||
|
||||
for (const value of valuesToInsert) {
|
||||
this.client.executeUpdate(FieldValueQueries.INSERT, [
|
||||
this.generateId(),
|
||||
itemId,
|
||||
@@ -546,7 +552,9 @@ export class ItemRepository extends BaseRepository {
|
||||
// Update existing or insert new FieldValues
|
||||
if (item.Fields && item.Fields.length > 0) {
|
||||
for (const field of item.Fields) {
|
||||
if (!field.Value || (typeof field.Value === 'string' && field.Value.trim() === '')) {
|
||||
// Skip empty system fields, but always persist custom fields (even if empty)
|
||||
const isEmpty = !field.Value || (typeof field.Value === 'string' && field.Value.trim() === '');
|
||||
if (isEmpty && !field.IsCustomField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -559,11 +567,14 @@ export class ItemRepository extends BaseRepository {
|
||||
const values = Array.isArray(field.Value) ? field.Value : [field.Value];
|
||||
const effectiveKey = field.FieldKey;
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const value = values[i];
|
||||
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||
continue;
|
||||
}
|
||||
// For custom fields with no values, use empty string to preserve the field
|
||||
const filteredValues = values.filter(v => v && (typeof v !== 'string' || v.trim() !== ''));
|
||||
const valuesToProcess = field.IsCustomField && filteredValues.length === 0
|
||||
? ['']
|
||||
: filteredValues;
|
||||
|
||||
for (let i = 0; i < valuesToProcess.length; i++) {
|
||||
const value = valuesToProcess[i];
|
||||
|
||||
const lookupKey = `${effectiveKey}:${i}`;
|
||||
const existing = existingByKey.get(lookupKey);
|
||||
|
||||
@@ -189,8 +189,8 @@ export default function ItemDetailsScreen() : React.ReactNode {
|
||||
<LoginFields item={item} />
|
||||
<CardDetails item={item} />
|
||||
<AliasDetails item={item} />
|
||||
<CustomFieldsSection item={item} />
|
||||
<NotesSection item={item} />
|
||||
<CustomFieldsSection item={item} />
|
||||
<AttachmentSection item={item} />
|
||||
</ThemedScrollView>
|
||||
</ThemedContainer>
|
||||
|
||||
@@ -760,22 +760,20 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
}
|
||||
});
|
||||
|
||||
// Add custom fields
|
||||
// Add custom fields - always persist even if empty (only deleted when explicitly removed)
|
||||
customFields.forEach(customField => {
|
||||
const value = fieldValues[customField.tempId];
|
||||
const value = fieldValues[customField.tempId] || '';
|
||||
|
||||
if (value && (Array.isArray(value) ? value.length > 0 : value.toString().trim() !== '')) {
|
||||
fields.push({
|
||||
FieldKey: customField.tempId,
|
||||
Label: customField.label,
|
||||
FieldType: customField.fieldType,
|
||||
Value: value,
|
||||
IsHidden: customField.isHidden,
|
||||
DisplayOrder: customField.displayOrder,
|
||||
IsCustomField: true,
|
||||
EnableHistory: false
|
||||
});
|
||||
}
|
||||
fields.push({
|
||||
FieldKey: customField.tempId,
|
||||
Label: customField.label,
|
||||
FieldType: customField.fieldType,
|
||||
Value: value,
|
||||
IsHidden: customField.isHidden,
|
||||
DisplayOrder: customField.displayOrder,
|
||||
IsCustomField: true,
|
||||
EnableHistory: false
|
||||
});
|
||||
});
|
||||
|
||||
// Normalize birthdate if present
|
||||
@@ -1533,28 +1531,6 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Custom Fields Section */}
|
||||
{customFields.length > 0 && (
|
||||
<FormSection title={t('itemTypes.customFields')}>
|
||||
{customFields.map(field => (
|
||||
<View key={field.tempId}>
|
||||
<EditableFieldLabel
|
||||
label={field.label}
|
||||
onLabelChange={(newLabel) => handleUpdateCustomFieldLabel(field.tempId, newLabel)}
|
||||
onDelete={() => handleDeleteCustomField(field.tempId)}
|
||||
/>
|
||||
{renderFieldInput(
|
||||
field.tempId,
|
||||
'', // Label is shown by EditableFieldLabel
|
||||
field.fieldType,
|
||||
field.isHidden,
|
||||
false
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
{/* Notes Section */}
|
||||
{notesField && visibleFieldKeys.has('notes.content') && (
|
||||
<FormSection
|
||||
@@ -1580,6 +1556,28 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
{/* Custom Fields Section */}
|
||||
{customFields.length > 0 && (
|
||||
<FormSection title={t('itemTypes.customFields')}>
|
||||
{customFields.map(field => (
|
||||
<View key={field.tempId}>
|
||||
<EditableFieldLabel
|
||||
label={field.label}
|
||||
onLabelChange={(newLabel) => handleUpdateCustomFieldLabel(field.tempId, newLabel)}
|
||||
onDelete={() => handleDeleteCustomField(field.tempId)}
|
||||
/>
|
||||
{renderFieldInput(
|
||||
field.tempId,
|
||||
'', // Label is shown by EditableFieldLabel
|
||||
field.fieldType,
|
||||
field.isHidden,
|
||||
false
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
{/* 2FA TOTP Section - only for types with login fields */}
|
||||
{show2FA && hasLoginFields && (
|
||||
<FormSection
|
||||
|
||||
@@ -436,11 +436,16 @@ export class ItemRepository extends BaseRepository {
|
||||
const values = Array.isArray(field.Value) ? field.Value : [field.Value];
|
||||
const filteredValues = values.filter(v => v !== undefined && v !== null && v !== '');
|
||||
|
||||
// Skip empty fields
|
||||
if (filteredValues.length === 0) {
|
||||
// Skip empty system fields, but always persist custom fields (even if empty)
|
||||
if (filteredValues.length === 0 && !field.IsCustomField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For custom fields with no values, use empty string to preserve the field
|
||||
const valuesToInsert = field.IsCustomField && filteredValues.length === 0
|
||||
? ['']
|
||||
: filteredValues;
|
||||
|
||||
let fieldDefinitionId: string | null = null;
|
||||
|
||||
// For custom fields, create or get FieldDefinition first
|
||||
@@ -448,8 +453,8 @@ export class ItemRepository extends BaseRepository {
|
||||
fieldDefinitionId = await this.ensureFieldDefinition(field, itemType, now);
|
||||
}
|
||||
|
||||
for (let j = 0; j < filteredValues.length; j++) {
|
||||
const value = filteredValues[j];
|
||||
for (let j = 0; j < valuesToInsert.length; j++) {
|
||||
const value = valuesToInsert[j];
|
||||
|
||||
await this.client.executeUpdate(FieldValueQueries.INSERT, [
|
||||
this.generateId(),
|
||||
@@ -597,12 +602,17 @@ export class ItemRepository extends BaseRepository {
|
||||
const values = Array.isArray(field.Value) ? field.Value : [field.Value];
|
||||
const existingForKey = existingByKey.get(field.FieldKey) || [];
|
||||
|
||||
// Skip empty fields
|
||||
// Skip empty system fields, but always persist custom fields (even if empty)
|
||||
const filteredValues = values.filter(v => v !== undefined && v !== null && v !== '');
|
||||
if (filteredValues.length === 0) {
|
||||
if (filteredValues.length === 0 && !field.IsCustomField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For custom fields with no values, use empty string to preserve the field
|
||||
const valuesToProcess = field.IsCustomField && filteredValues.length === 0
|
||||
? ['']
|
||||
: filteredValues;
|
||||
|
||||
let fieldDefinitionId: string | null = null;
|
||||
|
||||
// For custom fields, ensure FieldDefinition exists and is up-to-date
|
||||
@@ -610,8 +620,8 @@ export class ItemRepository extends BaseRepository {
|
||||
fieldDefinitionId = await this.ensureOrUpdateFieldDefinition(field, itemType, now);
|
||||
}
|
||||
|
||||
for (let j = 0; j < filteredValues.length; j++) {
|
||||
const value = filteredValues[j];
|
||||
for (let j = 0; j < valuesToProcess.length; j++) {
|
||||
const value = valuesToProcess[j];
|
||||
|
||||
const existingEntry = existingForKey[j];
|
||||
|
||||
|
||||
@@ -175,7 +175,9 @@ public sealed class ItemEdit
|
||||
var hasValue = !string.IsNullOrEmpty(field.Value) ||
|
||||
(field.IsMultiValue && field.Values.Any(v => !string.IsNullOrEmpty(v)));
|
||||
|
||||
if (!hasValue)
|
||||
// For system fields, skip if no value
|
||||
// For custom fields, always persist (even if empty) - they're only deleted when explicitly removed
|
||||
if (!hasValue && !field.IsCustomField)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user