diff --git a/apps/mobile-app/app/(tabs)/items/add-edit.tsx b/apps/mobile-app/app/(tabs)/items/add-edit.tsx index 5a5141611..1e38d3671 100644 --- a/apps/mobile-app/app/(tabs)/items/add-edit.tsx +++ b/apps/mobile-app/app/(tabs)/items/add-edit.tsx @@ -21,13 +21,13 @@ import { extractServiceNameFromUrl } from '@/utils/UrlUtility'; import { useColors } from '@/hooks/useColorScheme'; import { useVaultMutate } from '@/hooks/useVaultMutate'; -import { FolderSelector } from '@/components/folders/FolderSelector'; import { AddFieldMenu, type OptionalSection } from '@/components/form/AddFieldMenu'; import { AdvancedPasswordField } from '@/components/form/AdvancedPasswordField'; import { EmailDomainField } from '@/components/form/EmailDomainField'; import { FormField, FormFieldRef } from '@/components/form/FormField'; import { FormSection } from '@/components/form/FormSection'; import { HiddenField } from '@/components/form/HiddenField'; +import { ItemNameField, ItemNameFieldRef } from '@/components/form/ItemNameField'; import { AttachmentUploader } from '@/components/items/details/AttachmentUploader'; import { TotpEditor } from '@/components/items/details/TotpEditor'; import { ItemTypeSelector } from '@/components/items/ItemTypeSelector'; @@ -76,7 +76,7 @@ export default function AddEditItemScreen(): React.ReactNode { const { t } = useTranslation(); const [isPasswordVisible, setIsPasswordVisible] = useState(false); - const itemNameRef = useRef(null); + const itemNameRef = useRef(null); const [isSyncing, setIsSyncing] = useState(false); const [isSaveDisabled, setIsSaveDisabled] = useState(false); const [attachments, setAttachments] = useState([]); @@ -1248,15 +1248,19 @@ export default function AddEditItemScreen(): React.ReactNode { {/* Item Name and Primary Fields Section */} - { setItem(prev => prev ? { ...prev, Name: value } : prev); setHasUnsavedChanges(true); }} - label={t('items.serviceName')} - required + folders={folders} + selectedFolderId={item.FolderId} + onFolderChange={(folderId) => { + setItem(prev => prev ? { ...prev, FolderId: folderId } : prev); + setHasUnsavedChanges(true); + }} /> {/* Primary fields (like URL) */} {primaryFields.map(field => ( @@ -1270,17 +1274,6 @@ export default function AddEditItemScreen(): React.ReactNode { )} ))} - {/* Folder selection */} - {folders.length > 0 && ( - { - setItem(prev => prev ? { ...prev, FolderId: folderId } : prev); - setHasUnsavedChanges(true); - }} - /> - )} {/* Passkey Section - only in edit mode for items with passkeys */} diff --git a/apps/mobile-app/components/form/ItemNameField.tsx b/apps/mobile-app/components/form/ItemNameField.tsx new file mode 100644 index 000000000..a204fa36f --- /dev/null +++ b/apps/mobile-app/components/form/ItemNameField.tsx @@ -0,0 +1,283 @@ +import MaterialIcons from '@expo/vector-icons/MaterialIcons'; +import React, { useState, useCallback, forwardRef, useImperativeHandle, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Modal, + StyleSheet, + Text, + TextInput, + View, + ScrollView, +} from 'react-native'; + +import { RobustPressable } from '@/components/ui/RobustPressable'; +import { useColors } from '@/hooks/useColorScheme'; + +type Folder = { + Id: string; + Name: string; +}; + +export interface ItemNameFieldRef { + focus: () => void; +} + +interface IItemNameFieldProps { + value: string; + onChangeText: (text: string) => void; + folders: Folder[]; + selectedFolderId: string | null | undefined; + onFolderChange: (folderId: string | null) => void; +} + +/** + * ItemNameField component + * + * An item name input field with an integrated folder selection button. + * The folder button appears inside the input when folders are available, + * matching the browser extension's design pattern. + */ +export const ItemNameField = forwardRef(({ + value, + onChangeText, + folders, + selectedFolderId, + onFolderChange, +}, ref) => { + const { t } = useTranslation(); + const colors = useColors(); + const [showModal, setShowModal] = useState(false); + const inputRef = useRef(null); + + useImperativeHandle(ref, () => ({ + /** + * Focus the input field + */ + focus: (): void => { + inputRef.current?.focus(); + } + })); + + const selectedFolder = folders.find(f => f.Id === selectedFolderId); + const hasFolders = folders.length > 0; + + /** + * Handle folder selection. + */ + const handleSelectFolder = useCallback((folderId: string | null): void => { + onFolderChange(folderId); + setShowModal(false); + }, [onFolderChange]); + + const styles = StyleSheet.create({ + backdrop: { + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + flex: 1, + justifyContent: 'center', + }, + closeButton: { + padding: 4, + position: 'absolute', + right: 16, + top: 16, + }, + container: { + backgroundColor: colors.background, + borderColor: colors.accentBorder, + borderRadius: 8, + borderWidth: 1, + flexDirection: 'row', + alignItems: 'center', + }, + folderButton: { + alignItems: 'center', + borderLeftColor: colors.accentBorder, + borderLeftWidth: 1, + flexDirection: 'row', + gap: 4, + paddingHorizontal: 10, + paddingVertical: 10, + }, + folderButtonText: { + color: colors.tint, + fontSize: 12, + fontWeight: '500', + maxWidth: 60, + }, + folderOption: { + alignItems: 'center', + borderRadius: 8, + flexDirection: 'row', + gap: 12, + paddingHorizontal: 12, + paddingVertical: 12, + }, + folderOptionActive: { + backgroundColor: colors.tint + '15', + }, + folderOptionText: { + color: colors.text, + flex: 1, + fontSize: 16, + }, + folderOptionTextActive: { + color: colors.tint, + fontWeight: '600', + }, + input: { + color: colors.text, + flex: 1, + fontSize: 16, + paddingHorizontal: 12, + paddingVertical: 12, + }, + label: { + color: colors.textMuted, + fontSize: 14, + fontWeight: '500', + marginBottom: 6, + }, + modalContainer: { + backgroundColor: colors.background, + borderRadius: 12, + marginHorizontal: 20, + maxHeight: '70%', + maxWidth: 400, + padding: 20, + width: '90%', + }, + optionsList: { + marginTop: 16, + }, + requiredAsterisk: { + color: colors.destructive, + }, + title: { + color: colors.text, + fontSize: 18, + fontWeight: '600', + marginBottom: 4, + }, + wrapper: { + marginBottom: 16, + }, + }); + + return ( + + + {t('items.serviceName')} * + + + + {hasFolders && ( + setShowModal(true)} + > + + {selectedFolderId && selectedFolder && ( + + {selectedFolder.Name} + + )} + + )} + + + setShowModal(false)} + > + + + {t('items.folders.selectFolder')} + + setShowModal(false)} + > + + + + + {/* No folder option */} + handleSelectFolder(null)} + > + + + {t('items.folders.noFolder')} + + {!selectedFolderId && ( + + )} + + + {/* Folder options */} + {folders.map(folder => ( + handleSelectFolder(folder.Id)} + > + + + {folder.Name} + + {selectedFolderId === folder.Id && ( + + )} + + ))} + + + + + + ); +}); + +ItemNameField.displayName = 'ItemNameField'; + +export default ItemNameField;