mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-08 16:48:48 -04:00
Update folder selector in AddEdit (#1404)
This commit is contained in:
@@ -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<FormFieldRef>(null);
|
||||
const itemNameRef = useRef<ItemNameFieldRef>(null);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
@@ -1248,15 +1248,19 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
|
||||
{/* Item Name and Primary Fields Section */}
|
||||
<FormSection title={t('items.service')}>
|
||||
<FormField
|
||||
<ItemNameField
|
||||
ref={itemNameRef}
|
||||
value={item.Name ?? ''}
|
||||
onChangeText={(value) => {
|
||||
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 {
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
{/* Folder selection */}
|
||||
{folders.length > 0 && (
|
||||
<FolderSelector
|
||||
folders={folders}
|
||||
selectedFolderId={item.FolderId}
|
||||
onFolderChange={(folderId) => {
|
||||
setItem(prev => prev ? { ...prev, FolderId: folderId } : prev);
|
||||
setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</FormSection>
|
||||
|
||||
{/* Passkey Section - only in edit mode for items with passkeys */}
|
||||
|
||||
283
apps/mobile-app/components/form/ItemNameField.tsx
Normal file
283
apps/mobile-app/components/form/ItemNameField.tsx
Normal file
@@ -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<ItemNameFieldRef, IItemNameFieldProps>(({
|
||||
value,
|
||||
onChangeText,
|
||||
folders,
|
||||
selectedFolderId,
|
||||
onFolderChange,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const colors = useColors();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const inputRef = useRef<TextInput>(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 (
|
||||
<View style={styles.wrapper}>
|
||||
<Text style={styles.label}>
|
||||
{t('items.serviceName')} <Text style={styles.requiredAsterisk}>*</Text>
|
||||
</Text>
|
||||
<View style={styles.container}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
style={styles.input}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={t('items.serviceName')}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
{hasFolders && (
|
||||
<RobustPressable
|
||||
style={styles.folderButton}
|
||||
onPress={() => setShowModal(true)}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="folder"
|
||||
size={18}
|
||||
color={selectedFolderId ? colors.tint : colors.textMuted}
|
||||
/>
|
||||
{selectedFolderId && selectedFolder && (
|
||||
<Text style={styles.folderButtonText} numberOfLines={1}>
|
||||
{selectedFolder.Name}
|
||||
</Text>
|
||||
)}
|
||||
</RobustPressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Modal
|
||||
visible={showModal}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowModal(false)}
|
||||
>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={styles.modalContainer}>
|
||||
<Text style={styles.title}>{t('items.folders.selectFolder')}</Text>
|
||||
|
||||
<RobustPressable
|
||||
style={styles.closeButton}
|
||||
onPress={() => setShowModal(false)}
|
||||
>
|
||||
<MaterialIcons name="close" size={24} color={colors.textMuted} />
|
||||
</RobustPressable>
|
||||
|
||||
<ScrollView style={styles.optionsList}>
|
||||
{/* No folder option */}
|
||||
<RobustPressable
|
||||
style={[
|
||||
styles.folderOption,
|
||||
!selectedFolderId && styles.folderOptionActive,
|
||||
]}
|
||||
onPress={() => handleSelectFolder(null)}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="folder-open"
|
||||
size={22}
|
||||
color={!selectedFolderId ? colors.tint : colors.textMuted}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.folderOptionText,
|
||||
!selectedFolderId && styles.folderOptionTextActive,
|
||||
]}
|
||||
>
|
||||
{t('items.folders.noFolder')}
|
||||
</Text>
|
||||
{!selectedFolderId && (
|
||||
<MaterialIcons name="check" size={20} color={colors.tint} />
|
||||
)}
|
||||
</RobustPressable>
|
||||
|
||||
{/* Folder options */}
|
||||
{folders.map(folder => (
|
||||
<RobustPressable
|
||||
key={folder.Id}
|
||||
style={[
|
||||
styles.folderOption,
|
||||
selectedFolderId === folder.Id && styles.folderOptionActive,
|
||||
]}
|
||||
onPress={() => handleSelectFolder(folder.Id)}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="folder"
|
||||
size={22}
|
||||
color={selectedFolderId === folder.Id ? colors.tint : colors.textMuted}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.folderOptionText,
|
||||
selectedFolderId === folder.Id && styles.folderOptionTextActive,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{folder.Name}
|
||||
</Text>
|
||||
{selectedFolderId === folder.Id && (
|
||||
<MaterialIcons name="check" size={20} color={colors.tint} />
|
||||
)}
|
||||
</RobustPressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
ItemNameField.displayName = 'ItemNameField';
|
||||
|
||||
export default ItemNameField;
|
||||
Reference in New Issue
Block a user