mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-02 10:22:45 -05:00
Simplify starting item types and item type selector (#1404)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
@@ -17,11 +17,11 @@ import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import { IdentityHelperUtils, CreateIdentityGenerator, convertAgeRangeToBirthdateOptions } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Item, ItemField, ItemType, FieldType } from '@/utils/dist/shared/models/vault';
|
||||
import { getSystemFieldsForItemType, isFieldShownByDefault, getOptionalFieldsForItemType } from '@/utils/dist/shared/models/vault';
|
||||
import { getSystemFieldsForItemType, isFieldShownByDefault } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
|
||||
// Valid item types from the shared model
|
||||
const VALID_ITEM_TYPES: ItemType[] = ['Login', 'CreditCard', 'Identity', 'Note'];
|
||||
const VALID_ITEM_TYPES: ItemType[] = ['Login', 'Alias', 'CreditCard', 'Note'];
|
||||
|
||||
// Default item type for new items
|
||||
const DEFAULT_ITEM_TYPE: ItemType = 'Login';
|
||||
@@ -48,6 +48,15 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'Alias',
|
||||
titleKey: 'itemTypes.alias.title',
|
||||
iconSvg: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'CreditCard',
|
||||
titleKey: 'itemTypes.creditCard.title',
|
||||
@@ -57,15 +66,6 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'Identity',
|
||||
titleKey: 'itemTypes.identity.title',
|
||||
iconSvg: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'Note',
|
||||
titleKey: 'itemTypes.note.title',
|
||||
@@ -140,6 +140,26 @@ const ItemAddEdit: React.FC = () => {
|
||||
// Add menu dropdown state (unified + button)
|
||||
const [showAddMenu, setShowAddMenu] = useState(false);
|
||||
|
||||
// Track if alias was already auto-generated (to avoid re-generating on re-renders)
|
||||
const aliasGeneratedRef = useRef(false);
|
||||
|
||||
// Ref for the item name input field (for auto-focus)
|
||||
const nameInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Track last generated values to avoid overwriting manual user entries on regenerate
|
||||
const [lastGeneratedValues, setLastGeneratedValues] = useState<{
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
email: string | null;
|
||||
}>({
|
||||
username: null,
|
||||
password: null,
|
||||
email: null
|
||||
});
|
||||
|
||||
// Track password field visibility (for showing generated passwords)
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
/**
|
||||
* Get all applicable system fields for the current item type.
|
||||
* These are sorted by DefaultDisplayOrder.
|
||||
@@ -152,21 +172,10 @@ const ItemAddEdit: React.FC = () => {
|
||||
}, [item]);
|
||||
|
||||
/**
|
||||
* Get optional fields that are not shown by default for the current item type.
|
||||
* These can be added via the "+" menu.
|
||||
*/
|
||||
const optionalFields = useMemo(() => {
|
||||
if (!item) {
|
||||
return [];
|
||||
}
|
||||
return getOptionalFieldsForItemType(item.ItemType);
|
||||
}, [item]);
|
||||
|
||||
/**
|
||||
* The notes field (login.notes) - handled separately for collapsible UI.
|
||||
* The notes field (metadata.notes) - handled separately for collapsible UI.
|
||||
*/
|
||||
const notesField = useMemo(() => {
|
||||
return applicableSystemFields.find(field => field.FieldKey === 'login.notes');
|
||||
return applicableSystemFields.find(field => field.FieldKey === 'metadata.notes');
|
||||
}, [applicableSystemFields]);
|
||||
|
||||
/**
|
||||
@@ -183,9 +192,16 @@ const ItemAddEdit: React.FC = () => {
|
||||
return isFieldShownByDefault(systemField, item.ItemType);
|
||||
}, [item, applicableSystemFields]);
|
||||
|
||||
/**
|
||||
* Primary fields (like URL) that should be shown in the name block.
|
||||
*/
|
||||
const primaryFields = useMemo(() => {
|
||||
return applicableSystemFields.filter(field => field.Category === 'Primary');
|
||||
}, [applicableSystemFields]);
|
||||
|
||||
/**
|
||||
* Group system fields by category for organized rendering.
|
||||
* Excludes metadata fields (notes) which are handled separately.
|
||||
* Excludes metadata fields (notes) and header fields which are handled separately.
|
||||
*/
|
||||
const groupedSystemFields = useMemo(() => {
|
||||
const groups: Record<string, typeof applicableSystemFields> = {};
|
||||
@@ -195,6 +211,10 @@ const ItemAddEdit: React.FC = () => {
|
||||
if (field.Category === 'Metadata') {
|
||||
return;
|
||||
}
|
||||
// Skip primary fields - rendered in name block
|
||||
if (field.Category === 'Primary') {
|
||||
return;
|
||||
}
|
||||
|
||||
const category = field.Category || 'Other';
|
||||
if (!groups[category]) {
|
||||
@@ -231,11 +251,16 @@ const ItemAddEdit: React.FC = () => {
|
||||
|
||||
// Check if notes should be shown by default for this type
|
||||
const typeFields = getSystemFieldsForItemType(effectiveType);
|
||||
const notesFieldDef = typeFields.find(f => f.FieldKey === 'login.notes');
|
||||
const notesFieldDef = typeFields.find(f => f.FieldKey === 'metadata.notes');
|
||||
if (notesFieldDef && isFieldShownByDefault(notesFieldDef, effectiveType)) {
|
||||
setShowNotes(true);
|
||||
}
|
||||
|
||||
// For Alias type, show alias fields by default
|
||||
if (effectiveType === 'Alias') {
|
||||
setShowAliasFields(true);
|
||||
}
|
||||
|
||||
// Load folders
|
||||
if (dbContext?.sqliteClient) {
|
||||
const allFolders = dbContext.sqliteClient.getAllFolders();
|
||||
@@ -291,6 +316,26 @@ const ItemAddEdit: React.FC = () => {
|
||||
}
|
||||
}, [dbContext?.sqliteClient, id, isEditMode, itemTypeParam, itemNameParam, navigate, setIsInitialLoading]);
|
||||
|
||||
/**
|
||||
* Auto-generate alias when Alias type is selected in create mode.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isEditMode && item?.ItemType === 'Alias' && !localLoading && dbContext?.sqliteClient && !aliasGeneratedRef.current) {
|
||||
aliasGeneratedRef.current = true;
|
||||
void handleGenerateAlias();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEditMode, item?.ItemType, localLoading, dbContext?.sqliteClient]);
|
||||
|
||||
/**
|
||||
* Auto-focus the name input field when in add mode.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isEditMode && !localLoading && nameInputRef.current) {
|
||||
nameInputRef.current.focus();
|
||||
}
|
||||
}, [isEditMode, localLoading]);
|
||||
|
||||
/**
|
||||
* Handle field value change.
|
||||
*/
|
||||
@@ -451,11 +496,19 @@ const ItemAddEdit: React.FC = () => {
|
||||
// Clear field values when changing type (except name)
|
||||
setFieldValues({});
|
||||
setCustomFields([]);
|
||||
setShowAliasFields(false);
|
||||
|
||||
// For Alias type, show alias fields by default; otherwise hide
|
||||
if (newType === 'Alias') {
|
||||
setShowAliasFields(true);
|
||||
// Reset the ref so alias will be auto-generated
|
||||
aliasGeneratedRef.current = false;
|
||||
} else {
|
||||
setShowAliasFields(false);
|
||||
}
|
||||
|
||||
// Check if notes should be shown by default for the new type
|
||||
const newTypeFields = getSystemFieldsForItemType(newType);
|
||||
const newNotesField = newTypeFields.find(f => f.FieldKey === 'login.notes');
|
||||
const newNotesField = newTypeFields.find(f => f.FieldKey === 'metadata.notes');
|
||||
const notesShownByDefault = newNotesField ? isFieldShownByDefault(newNotesField, newType) : false;
|
||||
setShowNotes(notesShownByDefault);
|
||||
|
||||
@@ -488,6 +541,7 @@ const ItemAddEdit: React.FC = () => {
|
||||
/**
|
||||
* Generate random alias and populate alias fields.
|
||||
* This shows the alias fields and fills them with random values.
|
||||
* Only overwrites username/password/email if they're empty or match the last generated value.
|
||||
*/
|
||||
const handleGenerateAlias = useCallback(async () => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
@@ -512,25 +566,55 @@ const ItemAddEdit: React.FC = () => {
|
||||
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
|
||||
|
||||
// Set field values for alias fields
|
||||
setFieldValues(prev => ({
|
||||
...prev,
|
||||
'alias.email': email,
|
||||
'alias.first_name': identity.firstName,
|
||||
'alias.last_name': identity.lastName,
|
||||
'alias.nickname': identity.nickName,
|
||||
'alias.gender': identity.gender,
|
||||
'alias.birthdate': IdentityHelperUtils.normalizeBirthDateForDisplay(identity.birthDate.toISOString()),
|
||||
// Also set username and password if they're empty
|
||||
'login.username': prev['login.username'] || identity.nickName,
|
||||
'login.password': prev['login.password'] || password
|
||||
}));
|
||||
setFieldValues(prev => {
|
||||
const currentUsername = (prev['login.username'] as string) || '';
|
||||
const currentPassword = (prev['login.password'] as string) || '';
|
||||
const currentEmail = (prev['alias.email'] as string) || '';
|
||||
|
||||
const newValues: Record<string, string | string[]> = {
|
||||
...prev,
|
||||
// Always update alias identity fields
|
||||
'alias.first_name': identity.firstName,
|
||||
'alias.last_name': identity.lastName,
|
||||
'alias.nickname': identity.nickName,
|
||||
'alias.gender': identity.gender,
|
||||
'alias.birthdate': IdentityHelperUtils.normalizeBirthDateForDisplay(identity.birthDate.toISOString())
|
||||
};
|
||||
|
||||
// Only overwrite email if it's empty or matches the last generated value
|
||||
if (!currentEmail || currentEmail === lastGeneratedValues.email) {
|
||||
newValues['alias.email'] = email;
|
||||
}
|
||||
|
||||
// Only overwrite username if it's empty or matches the last generated value
|
||||
if (!currentUsername || currentUsername === lastGeneratedValues.username) {
|
||||
newValues['login.username'] = identity.nickName;
|
||||
}
|
||||
|
||||
// Only overwrite password if it's empty or matches the last generated value
|
||||
if (!currentPassword || currentPassword === lastGeneratedValues.password) {
|
||||
newValues['login.password'] = password;
|
||||
}
|
||||
|
||||
return newValues;
|
||||
});
|
||||
|
||||
// Update tracking with new generated values
|
||||
setLastGeneratedValues({
|
||||
username: identity.nickName,
|
||||
password: password,
|
||||
email: email
|
||||
});
|
||||
|
||||
// Show the generated password (it's random so no need to hide)
|
||||
setShowPassword(true);
|
||||
|
||||
// Show alias fields section
|
||||
setShowAliasFields(true);
|
||||
} catch (error) {
|
||||
console.error('Error generating random alias:', error);
|
||||
}
|
||||
}, [dbContext.sqliteClient, initializeGenerators]);
|
||||
}, [dbContext.sqliteClient, initializeGenerators, lastGeneratedValues]);
|
||||
|
||||
/**
|
||||
* Clear all alias field values but keep them visible.
|
||||
@@ -561,7 +645,7 @@ const ItemAddEdit: React.FC = () => {
|
||||
const handleRemoveNotesSection = useCallback(() => {
|
||||
setFieldValues(prev => ({
|
||||
...prev,
|
||||
'login.notes': ''
|
||||
'metadata.notes': ''
|
||||
}));
|
||||
setShowNotes(false);
|
||||
}, []);
|
||||
@@ -603,7 +687,7 @@ const ItemAddEdit: React.FC = () => {
|
||||
|
||||
// Notes option - show when notes field exists, is optional (not shown by default), not yet shown, and has no value
|
||||
const notesIsOptional = notesField && !shouldShowFieldByDefault(notesField);
|
||||
if (notesField && notesIsOptional && !showNotes && !fieldValues['login.notes'] && !isEditMode) {
|
||||
if (notesField && notesIsOptional && !showNotes && !fieldValues['metadata.notes'] && !isEditMode) {
|
||||
options.push({
|
||||
key: 'notes',
|
||||
label: t('credentials.notes'),
|
||||
@@ -631,19 +715,6 @@ const ItemAddEdit: React.FC = () => {
|
||||
return options;
|
||||
}, [showNotes, notesField, fieldValues, isEditMode, t, handleAddNotesFromMenu, handleAddCustomFieldFromMenu, shouldShowFieldByDefault]);
|
||||
|
||||
/**
|
||||
* Whether to show the dedicated "Add alias" button.
|
||||
* Show when alias fields exist as optional (not shown by default) and not yet added.
|
||||
*/
|
||||
const showAddAliasButton = useMemo(() => {
|
||||
if (!item || isEditMode) {
|
||||
return false;
|
||||
}
|
||||
// Check if there are any alias fields that are optional (not shown by default)
|
||||
const hasOptionalAliasFields = optionalFields.some(f => f.Category === 'Alias');
|
||||
return hasOptionalAliasFields && !showAliasFields;
|
||||
}, [item, optionalFields, showAliasFields, isEditMode]);
|
||||
|
||||
// Set header buttons
|
||||
useEffect(() => {
|
||||
const headerButtonsJSX = (
|
||||
@@ -726,6 +797,8 @@ const ItemAddEdit: React.FC = () => {
|
||||
label={label}
|
||||
value={stringValue}
|
||||
onChange={(val) => handleFieldChange(fieldKey, val)}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -774,39 +847,66 @@ const ItemAddEdit: React.FC = () => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
}, [fieldValues, handleFieldChange]);
|
||||
}, [fieldValues, handleFieldChange, showPassword]);
|
||||
|
||||
/**
|
||||
* Handle form submission via Enter key.
|
||||
*/
|
||||
const handleFormSubmit = useCallback((e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
void handleSave();
|
||||
}, [handleSave]);
|
||||
|
||||
if (localLoading || !item) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<form onSubmit={handleFormSubmit} className="space-y-4">
|
||||
{/* Item Type Selector (create mode only) */}
|
||||
{!isEditMode && (
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTypeDropdown(!showTypeDropdown)}
|
||||
className="w-full px-4 py-2 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg text-left flex items-center justify-between hover:bg-primary-100 dark:hover:bg-primary-900/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary-600 dark:text-primary-400">
|
||||
{selectedTypeOption?.iconSvg}
|
||||
</span>
|
||||
<span className="text-primary-700 dark:text-primary-300 font-medium text-sm">
|
||||
{t('itemTypes.creating')} {selectedTypeOption ? t(selectedTypeOption.titleKey) : ''}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 text-primary-500 transition-transform ${showTypeDropdown ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
<div className="w-full px-4 py-2 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTypeDropdown(!showTypeDropdown)}
|
||||
className="flex-1 flex items-center justify-between hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary-600 dark:text-primary-400">
|
||||
{selectedTypeOption?.iconSvg}
|
||||
</span>
|
||||
<span className="text-primary-700 dark:text-primary-300 font-medium text-sm">
|
||||
{t('itemTypes.creating')} {selectedTypeOption ? t(selectedTypeOption.titleKey) : ''}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 text-primary-500 transition-transform ${showTypeDropdown ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Regenerate alias button - icon only for flexibility */}
|
||||
{item?.ItemType === 'Alias' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void handleGenerateAlias();
|
||||
}}
|
||||
className="flex-shrink-0 p-1.5 text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 hover:bg-primary-100 dark:hover:bg-primary-900/40 rounded transition-colors"
|
||||
title={t('itemTypes.regenerateAlias')}
|
||||
>
|
||||
<svg className='w-4 h-4' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M23 4v6h-6"/>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Type Dropdown Menu */}
|
||||
{showTypeDropdown && (
|
||||
@@ -846,44 +946,59 @@ const ItemAddEdit: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Item Name - Central prominent field */}
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<label htmlFor="itemName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('credentials.itemName')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="itemName"
|
||||
type="text"
|
||||
value={item.Name || ''}
|
||||
onChange={(e) => setItem({ ...item, Name: e.target.value })}
|
||||
placeholder={t('credentials.itemName')}
|
||||
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 ${folders.length > 0 ? 'pr-28' : ''}`}
|
||||
required
|
||||
/>
|
||||
{/* Folder Button inside input */}
|
||||
{folders.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFolderModal(true)}
|
||||
className={`absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1 px-2 py-1 rounded transition-colors text-xs ${
|
||||
item.FolderId
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 hover:bg-primary-200 dark:hover:bg-primary-900/50'
|
||||
: 'text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={item.FolderId ? folders.find(f => f.Id === item.FolderId)?.Name || t('items.folder') : t('items.noFolder')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
{item.FolderId && (
|
||||
<span className="max-w-16 truncate">
|
||||
{folders.find(f => f.Id === item.FolderId)?.Name}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{/* Item Name and Header fields block */}
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 space-y-4">
|
||||
<div>
|
||||
<label htmlFor="itemName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('credentials.itemName')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
ref={nameInputRef}
|
||||
id="itemName"
|
||||
type="text"
|
||||
value={item.Name || ''}
|
||||
onChange={(e) => setItem({ ...item, Name: e.target.value })}
|
||||
placeholder={t('credentials.itemName')}
|
||||
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 ${folders.length > 0 ? 'pr-28' : ''}`}
|
||||
required
|
||||
/>
|
||||
{/* Folder Button inside input */}
|
||||
{folders.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFolderModal(true)}
|
||||
className={`absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1 px-2 py-1 rounded transition-colors text-xs ${
|
||||
item.FolderId
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 hover:bg-primary-200 dark:hover:bg-primary-900/50'
|
||||
: 'text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={item.FolderId ? folders.find(f => f.Id === item.FolderId)?.Name || t('items.folder') : t('items.noFolder')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
{item.FolderId && (
|
||||
<span className="max-w-16 truncate">
|
||||
{folders.find(f => f.Id === item.FolderId)?.Name}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Primary fields (like URL) shown below name */}
|
||||
{primaryFields.map(field => (
|
||||
<div key={field.FieldKey}>
|
||||
{renderFieldInput(
|
||||
field.FieldKey,
|
||||
field.Label,
|
||||
field.FieldType,
|
||||
field.IsHidden,
|
||||
field.IsMultiValue
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Render fields grouped by category */}
|
||||
@@ -986,13 +1101,13 @@ const ItemAddEdit: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notes Section - Hidden by default in create mode, with remove button */}
|
||||
{notesField && (showNotes || isEditMode || fieldValues['login.notes']) && (
|
||||
{/* Notes Section - Hidden by default in create mode, with remove button if optional */}
|
||||
{notesField && (showNotes || isEditMode || fieldValues['metadata.notes']) && (
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center justify-between">
|
||||
<span>{t('credentials.notes')}</span>
|
||||
{/* Remove button for notes in create mode */}
|
||||
{!isEditMode && (
|
||||
{/* Remove button for notes in create mode - only if notes is optional (not shown by default) */}
|
||||
{!isEditMode && !shouldShowFieldByDefault(notesField) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveNotesSection}
|
||||
@@ -1018,25 +1133,6 @@ const ItemAddEdit: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dedicated "Add Alias" button - highlighted as core feature */}
|
||||
{showAddAliasButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateAlias}
|
||||
className="w-full px-4 py-3 bg-primary-50 dark:bg-primary-900/20 border-2 border-primary-300 dark:border-primary-700 text-primary-700 dark:text-primary-300 rounded-lg hover:bg-primary-100 dark:hover:bg-primary-900/30 hover:border-primary-400 dark:hover:border-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors flex items-center justify-center gap-2 font-medium"
|
||||
>
|
||||
<svg className='w-5 h-5' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="8" cy="8" r="1"/>
|
||||
<circle cx="16" cy="8" r="1"/>
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="8" cy="16" r="1"/>
|
||||
<circle cx="16" cy="16" r="1"/>
|
||||
</svg>
|
||||
{t('itemTypes.addAlias')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Generic + button with dropdown menu for Notes and Custom Fields */}
|
||||
<div className="relative">
|
||||
<button
|
||||
@@ -1253,7 +1349,7 @@ const ItemAddEdit: React.FC = () => {
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">{syncStatus}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -29,6 +29,15 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'Alias',
|
||||
titleKey: 'itemTypes.alias.title',
|
||||
iconSvg: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'CreditCard',
|
||||
titleKey: 'itemTypes.creditCard.title',
|
||||
@@ -38,15 +47,6 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'Identity',
|
||||
titleKey: 'itemTypes.identity.title',
|
||||
iconSvg: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
{
|
||||
type: 'Note',
|
||||
titleKey: 'itemTypes.note.title',
|
||||
|
||||
@@ -295,6 +295,7 @@
|
||||
"typeLabel": "Item Type",
|
||||
"creating": "Creating",
|
||||
"generateAlias": "Generate random alias",
|
||||
"regenerateAlias": "Regenerate alias",
|
||||
"addAlias": "Add alias",
|
||||
"addNotes": "Add notes",
|
||||
"addCustomField": "Add custom field",
|
||||
@@ -302,14 +303,14 @@
|
||||
"title": "Login",
|
||||
"description": "Username, password, and website credentials"
|
||||
},
|
||||
"alias": {
|
||||
"title": "Alias",
|
||||
"description": "Login with auto-generated email alias and identity"
|
||||
},
|
||||
"creditCard": {
|
||||
"title": "Credit Card",
|
||||
"description": "Credit card information and payment details"
|
||||
},
|
||||
"identity": {
|
||||
"title": "Identity",
|
||||
"description": "Personal information and contact details"
|
||||
},
|
||||
"note": {
|
||||
"title": "Secure Note",
|
||||
"description": "Encrypted notes and private information"
|
||||
|
||||
@@ -323,8 +323,12 @@ type ItemTag = {
|
||||
|
||||
/**
|
||||
* Item types supported by the vault
|
||||
* - Login: Username/password credentials with optional notes
|
||||
* - Alias: Login with pre-filled alias identity fields (email, name, etc.)
|
||||
* - CreditCard: Payment card information
|
||||
* - Note: Secure notes
|
||||
*/
|
||||
type ItemType = 'Login' | 'CreditCard' | 'Identity' | 'Note';
|
||||
type ItemType = 'Login' | 'Alias' | 'CreditCard' | 'Note';
|
||||
/**
|
||||
* Item type representing vault entries in the new field-based data model.
|
||||
* Replaces the old Credential type.
|
||||
@@ -428,8 +432,8 @@ type SystemFieldDefinition = {
|
||||
ApplicableToTypes: Partial<Record<ItemType, ItemTypeFieldConfig>>;
|
||||
/** Whether to track field value history */
|
||||
EnableHistory: boolean;
|
||||
/** Category for grouping in UI */
|
||||
Category: 'Login' | 'Alias' | 'Card' | 'Identity' | 'API' | 'Metadata';
|
||||
/** Category for grouping in UI. 'Primary' fields are shown in the name block. */
|
||||
Category: 'Primary' | 'Login' | 'Alias' | 'Card' | 'Metadata';
|
||||
/** Default display order within category (lower = first) */
|
||||
DefaultDisplayOrder: number;
|
||||
};
|
||||
@@ -437,6 +441,11 @@ type SystemFieldDefinition = {
|
||||
* Registry of all system-defined fields.
|
||||
* These fields are immutable and their metadata is defined in code.
|
||||
* DO NOT modify these definitions without careful consideration of backwards compatibility.
|
||||
*
|
||||
* Item Types:
|
||||
* - Login: Username/password credentials (alias fields optional)
|
||||
* - Alias: Login with pre-filled alias identity fields shown by default
|
||||
* - CreditCard: Payment card information
|
||||
*/
|
||||
declare const SystemFieldRegistry: Record<string, SystemFieldDefinition>;
|
||||
/**
|
||||
|
||||
@@ -225,7 +225,7 @@ function itemToCredential(item) {
|
||||
|
||||
// src/vault/SystemFieldRegistry.ts
|
||||
var SystemFieldRegistry = {
|
||||
// Login Fields
|
||||
/* =================== LOGIN FIELDS =================== */
|
||||
"login.username": {
|
||||
FieldKey: "login.username",
|
||||
Label: "Username",
|
||||
@@ -233,7 +233,8 @@ var SystemFieldRegistry = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: true }
|
||||
Login: { ShowByDefault: true },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: true,
|
||||
Category: "Login",
|
||||
@@ -246,7 +247,8 @@ var SystemFieldRegistry = {
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: true }
|
||||
Login: { ShowByDefault: true },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: true,
|
||||
Category: "Login",
|
||||
@@ -259,30 +261,14 @@ var SystemFieldRegistry = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: true,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: true }
|
||||
Login: { ShowByDefault: true },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Login",
|
||||
DefaultDisplayOrder: 30
|
||||
Category: "Primary",
|
||||
DefaultDisplayOrder: 5
|
||||
},
|
||||
// Metadata Fields
|
||||
"login.notes": {
|
||||
FieldKey: "login.notes",
|
||||
Label: "Notes",
|
||||
FieldType: "TextArea",
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
CreditCard: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: false },
|
||||
Note: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Metadata",
|
||||
DefaultDisplayOrder: 100
|
||||
},
|
||||
// Alias Fields
|
||||
/* =================== ALIAS FIELDS =================== */
|
||||
"alias.email": {
|
||||
FieldKey: "alias.email",
|
||||
Label: "Email",
|
||||
@@ -290,7 +276,8 @@ var SystemFieldRegistry = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false }
|
||||
Login: { ShowByDefault: false },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: true,
|
||||
Category: "Alias",
|
||||
@@ -304,7 +291,7 @@ var SystemFieldRegistry = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Alias",
|
||||
@@ -318,7 +305,7 @@ var SystemFieldRegistry = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Alias",
|
||||
@@ -331,7 +318,8 @@ var SystemFieldRegistry = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false }
|
||||
Login: { ShowByDefault: false },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Alias",
|
||||
@@ -345,7 +333,7 @@ var SystemFieldRegistry = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Alias",
|
||||
@@ -359,19 +347,108 @@ var SystemFieldRegistry = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Alias",
|
||||
DefaultDisplayOrder: 60
|
||||
},
|
||||
/* =================== CREDIT CARD FIELDS =================== */
|
||||
"card.cardholder_name": {
|
||||
FieldKey: "card.cardholder_name",
|
||||
Label: "Cardholder Name",
|
||||
FieldType: "Text",
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 10
|
||||
},
|
||||
"card.number": {
|
||||
FieldKey: "card.number",
|
||||
Label: "Card Number",
|
||||
FieldType: "Hidden",
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 20
|
||||
},
|
||||
"card.expiry_month": {
|
||||
FieldKey: "card.expiry_month",
|
||||
Label: "Expiry Month",
|
||||
FieldType: "Text",
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 30
|
||||
},
|
||||
"card.expiry_year": {
|
||||
FieldKey: "card.expiry_year",
|
||||
Label: "Expiry Year",
|
||||
FieldType: "Text",
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 40
|
||||
},
|
||||
"card.cvv": {
|
||||
FieldKey: "card.cvv",
|
||||
Label: "CVV",
|
||||
FieldType: "Hidden",
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 50
|
||||
},
|
||||
"card.pin": {
|
||||
FieldKey: "card.pin",
|
||||
Label: "PIN",
|
||||
FieldType: "Hidden",
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: false }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 60
|
||||
},
|
||||
/* =================== METADATA FIELDS =================== */
|
||||
"metadata.notes": {
|
||||
FieldKey: "metadata.notes",
|
||||
Label: "Notes",
|
||||
FieldType: "TextArea",
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Alias: { ShowByDefault: false },
|
||||
CreditCard: { ShowByDefault: false },
|
||||
Note: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Metadata",
|
||||
DefaultDisplayOrder: 100
|
||||
}
|
||||
/*
|
||||
* Note: Card, Identity, and API fields can be added here when those item types are implemented
|
||||
* Example:
|
||||
* 'card.number': { ... },
|
||||
* 'card.cardholder_name': { ... },
|
||||
* 'identity.phone_number': { ... },
|
||||
*/
|
||||
};
|
||||
function getSystemField(fieldKey) {
|
||||
return SystemFieldRegistry[fieldKey];
|
||||
|
||||
@@ -323,8 +323,12 @@ type ItemTag = {
|
||||
|
||||
/**
|
||||
* Item types supported by the vault
|
||||
* - Login: Username/password credentials with optional notes
|
||||
* - Alias: Login with pre-filled alias identity fields (email, name, etc.)
|
||||
* - CreditCard: Payment card information
|
||||
* - Note: Secure notes
|
||||
*/
|
||||
type ItemType = 'Login' | 'CreditCard' | 'Identity' | 'Note';
|
||||
type ItemType = 'Login' | 'Alias' | 'CreditCard' | 'Note';
|
||||
/**
|
||||
* Item type representing vault entries in the new field-based data model.
|
||||
* Replaces the old Credential type.
|
||||
@@ -428,8 +432,8 @@ type SystemFieldDefinition = {
|
||||
ApplicableToTypes: Partial<Record<ItemType, ItemTypeFieldConfig>>;
|
||||
/** Whether to track field value history */
|
||||
EnableHistory: boolean;
|
||||
/** Category for grouping in UI */
|
||||
Category: 'Login' | 'Alias' | 'Card' | 'Identity' | 'API' | 'Metadata';
|
||||
/** Category for grouping in UI. 'Primary' fields are shown in the name block. */
|
||||
Category: 'Primary' | 'Login' | 'Alias' | 'Card' | 'Metadata';
|
||||
/** Default display order within category (lower = first) */
|
||||
DefaultDisplayOrder: number;
|
||||
};
|
||||
@@ -437,6 +441,11 @@ type SystemFieldDefinition = {
|
||||
* Registry of all system-defined fields.
|
||||
* These fields are immutable and their metadata is defined in code.
|
||||
* DO NOT modify these definitions without careful consideration of backwards compatibility.
|
||||
*
|
||||
* Item Types:
|
||||
* - Login: Username/password credentials (alias fields optional)
|
||||
* - Alias: Login with pre-filled alias identity fields shown by default
|
||||
* - CreditCard: Payment card information
|
||||
*/
|
||||
declare const SystemFieldRegistry: Record<string, SystemFieldDefinition>;
|
||||
/**
|
||||
|
||||
@@ -225,7 +225,7 @@ function itemToCredential(item) {
|
||||
|
||||
// src/vault/SystemFieldRegistry.ts
|
||||
var SystemFieldRegistry = {
|
||||
// Login Fields
|
||||
/* =================== LOGIN FIELDS =================== */
|
||||
"login.username": {
|
||||
FieldKey: "login.username",
|
||||
Label: "Username",
|
||||
@@ -233,7 +233,8 @@ var SystemFieldRegistry = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: true }
|
||||
Login: { ShowByDefault: true },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: true,
|
||||
Category: "Login",
|
||||
@@ -246,7 +247,8 @@ var SystemFieldRegistry = {
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: true }
|
||||
Login: { ShowByDefault: true },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: true,
|
||||
Category: "Login",
|
||||
@@ -259,30 +261,14 @@ var SystemFieldRegistry = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: true,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: true }
|
||||
Login: { ShowByDefault: true },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Login",
|
||||
DefaultDisplayOrder: 30
|
||||
Category: "Primary",
|
||||
DefaultDisplayOrder: 5
|
||||
},
|
||||
// Metadata Fields
|
||||
"login.notes": {
|
||||
FieldKey: "login.notes",
|
||||
Label: "Notes",
|
||||
FieldType: "TextArea",
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
CreditCard: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: false },
|
||||
Note: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Metadata",
|
||||
DefaultDisplayOrder: 100
|
||||
},
|
||||
// Alias Fields
|
||||
/* =================== ALIAS FIELDS =================== */
|
||||
"alias.email": {
|
||||
FieldKey: "alias.email",
|
||||
Label: "Email",
|
||||
@@ -290,7 +276,8 @@ var SystemFieldRegistry = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false }
|
||||
Login: { ShowByDefault: false },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: true,
|
||||
Category: "Alias",
|
||||
@@ -304,7 +291,7 @@ var SystemFieldRegistry = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Alias",
|
||||
@@ -318,7 +305,7 @@ var SystemFieldRegistry = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Alias",
|
||||
@@ -331,7 +318,8 @@ var SystemFieldRegistry = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false }
|
||||
Login: { ShowByDefault: false },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Alias",
|
||||
@@ -345,7 +333,7 @@ var SystemFieldRegistry = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Alias",
|
||||
@@ -359,19 +347,108 @@ var SystemFieldRegistry = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Alias",
|
||||
DefaultDisplayOrder: 60
|
||||
},
|
||||
/* =================== CREDIT CARD FIELDS =================== */
|
||||
"card.cardholder_name": {
|
||||
FieldKey: "card.cardholder_name",
|
||||
Label: "Cardholder Name",
|
||||
FieldType: "Text",
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 10
|
||||
},
|
||||
"card.number": {
|
||||
FieldKey: "card.number",
|
||||
Label: "Card Number",
|
||||
FieldType: "Hidden",
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 20
|
||||
},
|
||||
"card.expiry_month": {
|
||||
FieldKey: "card.expiry_month",
|
||||
Label: "Expiry Month",
|
||||
FieldType: "Text",
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 30
|
||||
},
|
||||
"card.expiry_year": {
|
||||
FieldKey: "card.expiry_year",
|
||||
Label: "Expiry Year",
|
||||
FieldType: "Text",
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 40
|
||||
},
|
||||
"card.cvv": {
|
||||
FieldKey: "card.cvv",
|
||||
Label: "CVV",
|
||||
FieldType: "Hidden",
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 50
|
||||
},
|
||||
"card.pin": {
|
||||
FieldKey: "card.pin",
|
||||
Label: "PIN",
|
||||
FieldType: "Hidden",
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: false }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Card",
|
||||
DefaultDisplayOrder: 60
|
||||
},
|
||||
/* =================== METADATA FIELDS =================== */
|
||||
"metadata.notes": {
|
||||
FieldKey: "metadata.notes",
|
||||
Label: "Notes",
|
||||
FieldType: "TextArea",
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Alias: { ShowByDefault: false },
|
||||
CreditCard: { ShowByDefault: false },
|
||||
Note: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: "Metadata",
|
||||
DefaultDisplayOrder: 100
|
||||
}
|
||||
/*
|
||||
* Note: Card, Identity, and API fields can be added here when those item types are implemented
|
||||
* Example:
|
||||
* 'card.number': { ... },
|
||||
* 'card.cardholder_name': { ... },
|
||||
* 'identity.phone_number': { ... },
|
||||
*/
|
||||
};
|
||||
function getSystemField(fieldKey) {
|
||||
return SystemFieldRegistry[fieldKey];
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
/**
|
||||
* Item types supported by the vault
|
||||
* - Login: Username/password credentials with optional notes
|
||||
* - Alias: Login with pre-filled alias identity fields (email, name, etc.)
|
||||
* - CreditCard: Payment card information
|
||||
* - Note: Secure notes
|
||||
*/
|
||||
export type ItemType =
|
||||
| 'Login'
|
||||
| 'Alias'
|
||||
| 'CreditCard'
|
||||
| 'Identity'
|
||||
| 'Note';
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,8 +32,8 @@ export type SystemFieldDefinition = {
|
||||
ApplicableToTypes: Partial<Record<ItemType, ItemTypeFieldConfig>>;
|
||||
/** Whether to track field value history */
|
||||
EnableHistory: boolean;
|
||||
/** Category for grouping in UI */
|
||||
Category: 'Login' | 'Alias' | 'Card' | 'Identity' | 'API' | 'Metadata';
|
||||
/** Category for grouping in UI. 'Primary' fields are shown in the name block. */
|
||||
Category: 'Primary' | 'Login' | 'Alias' | 'Card' | 'Metadata';
|
||||
/** Default display order within category (lower = first) */
|
||||
DefaultDisplayOrder: number;
|
||||
};
|
||||
@@ -42,9 +42,14 @@ export type SystemFieldDefinition = {
|
||||
* Registry of all system-defined fields.
|
||||
* These fields are immutable and their metadata is defined in code.
|
||||
* DO NOT modify these definitions without careful consideration of backwards compatibility.
|
||||
*
|
||||
* Item Types:
|
||||
* - Login: Username/password credentials (alias fields optional)
|
||||
* - Alias: Login with pre-filled alias identity fields shown by default
|
||||
* - CreditCard: Payment card information
|
||||
*/
|
||||
export const SystemFieldRegistry: Record<string, SystemFieldDefinition> = {
|
||||
// Login Fields
|
||||
/* =================== LOGIN FIELDS =================== */
|
||||
'login.username': {
|
||||
FieldKey: 'login.username',
|
||||
Label: 'Username',
|
||||
@@ -52,7 +57,8 @@ export const SystemFieldRegistry: Record<string, SystemFieldDefinition> = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: true }
|
||||
Login: { ShowByDefault: true },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: true,
|
||||
Category: 'Login',
|
||||
@@ -65,7 +71,8 @@ export const SystemFieldRegistry: Record<string, SystemFieldDefinition> = {
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: true }
|
||||
Login: { ShowByDefault: true },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: true,
|
||||
Category: 'Login',
|
||||
@@ -78,32 +85,15 @@ export const SystemFieldRegistry: Record<string, SystemFieldDefinition> = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: true,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: true }
|
||||
Login: { ShowByDefault: true },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Login',
|
||||
DefaultDisplayOrder: 30
|
||||
Category: 'Primary',
|
||||
DefaultDisplayOrder: 5
|
||||
},
|
||||
|
||||
// Metadata Fields
|
||||
'login.notes': {
|
||||
FieldKey: 'login.notes',
|
||||
Label: 'Notes',
|
||||
FieldType: 'TextArea',
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
CreditCard: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: false },
|
||||
Note: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Metadata',
|
||||
DefaultDisplayOrder: 100
|
||||
},
|
||||
|
||||
// Alias Fields
|
||||
/* =================== ALIAS FIELDS =================== */
|
||||
'alias.email': {
|
||||
FieldKey: 'alias.email',
|
||||
Label: 'Email',
|
||||
@@ -111,7 +101,8 @@ export const SystemFieldRegistry: Record<string, SystemFieldDefinition> = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false }
|
||||
Login: { ShowByDefault: false },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: true,
|
||||
Category: 'Alias',
|
||||
@@ -125,7 +116,7 @@ export const SystemFieldRegistry: Record<string, SystemFieldDefinition> = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Alias',
|
||||
@@ -139,7 +130,7 @@ export const SystemFieldRegistry: Record<string, SystemFieldDefinition> = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Alias',
|
||||
@@ -152,7 +143,8 @@ export const SystemFieldRegistry: Record<string, SystemFieldDefinition> = {
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false }
|
||||
Login: { ShowByDefault: false },
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Alias',
|
||||
@@ -166,7 +158,7 @@ export const SystemFieldRegistry: Record<string, SystemFieldDefinition> = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Alias',
|
||||
@@ -180,20 +172,110 @@ export const SystemFieldRegistry: Record<string, SystemFieldDefinition> = {
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Identity: { ShowByDefault: true }
|
||||
Alias: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Alias',
|
||||
DefaultDisplayOrder: 60
|
||||
},
|
||||
|
||||
/*
|
||||
* Note: Card, Identity, and API fields can be added here when those item types are implemented
|
||||
* Example:
|
||||
* 'card.number': { ... },
|
||||
* 'card.cardholder_name': { ... },
|
||||
* 'identity.phone_number': { ... },
|
||||
*/
|
||||
/* =================== CREDIT CARD FIELDS =================== */
|
||||
'card.cardholder_name': {
|
||||
FieldKey: 'card.cardholder_name',
|
||||
Label: 'Cardholder Name',
|
||||
FieldType: 'Text',
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Card',
|
||||
DefaultDisplayOrder: 10
|
||||
},
|
||||
'card.number': {
|
||||
FieldKey: 'card.number',
|
||||
Label: 'Card Number',
|
||||
FieldType: 'Hidden',
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Card',
|
||||
DefaultDisplayOrder: 20
|
||||
},
|
||||
'card.expiry_month': {
|
||||
FieldKey: 'card.expiry_month',
|
||||
Label: 'Expiry Month',
|
||||
FieldType: 'Text',
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Card',
|
||||
DefaultDisplayOrder: 30
|
||||
},
|
||||
'card.expiry_year': {
|
||||
FieldKey: 'card.expiry_year',
|
||||
Label: 'Expiry Year',
|
||||
FieldType: 'Text',
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Card',
|
||||
DefaultDisplayOrder: 40
|
||||
},
|
||||
'card.cvv': {
|
||||
FieldKey: 'card.cvv',
|
||||
Label: 'CVV',
|
||||
FieldType: 'Hidden',
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Card',
|
||||
DefaultDisplayOrder: 50
|
||||
},
|
||||
'card.pin': {
|
||||
FieldKey: 'card.pin',
|
||||
Label: 'PIN',
|
||||
FieldType: 'Hidden',
|
||||
IsHidden: true,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
CreditCard: { ShowByDefault: false }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Card',
|
||||
DefaultDisplayOrder: 60
|
||||
},
|
||||
|
||||
/* =================== METADATA FIELDS =================== */
|
||||
'metadata.notes': {
|
||||
FieldKey: 'metadata.notes',
|
||||
Label: 'Notes',
|
||||
FieldType: 'TextArea',
|
||||
IsHidden: false,
|
||||
IsMultiValue: false,
|
||||
ApplicableToTypes: {
|
||||
Login: { ShowByDefault: false },
|
||||
Alias: { ShowByDefault: false },
|
||||
CreditCard: { ShowByDefault: false },
|
||||
Note: { ShowByDefault: true }
|
||||
},
|
||||
EnableHistory: false,
|
||||
Category: 'Metadata',
|
||||
DefaultDisplayOrder: 100
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user