add tags
This commit is contained in:
@@ -3,22 +3,22 @@ package config
|
|||||||
import "os"
|
import "os"
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port string
|
Port string
|
||||||
DatabaseURL string
|
DatabaseURL string
|
||||||
OCREndpoint string
|
OCREndpoint string
|
||||||
UploadDir string
|
UploadDir string
|
||||||
HMACSecret string
|
HMACSecret string
|
||||||
ServerHost string
|
ServerHost string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() *Config {
|
func Load() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Port: envOr("PORT", "8080"),
|
Port: envOr("PORT", "8080"),
|
||||||
DatabaseURL: envOr("DATABASE_URL", "postgres://localhost:5432/vaultdrop?sslmode=disable"),
|
DatabaseURL: envOr("DATABASE_URL", "postgres://localhost:5432/vaultdrop?sslmode=disable"),
|
||||||
OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"),
|
OCREndpoint: envOr("OCR_ENDPOINT", "http://localhost:9090"),
|
||||||
UploadDir: envOr("UPLOAD_DIR", "./uploads"),
|
UploadDir: envOr("UPLOAD_DIR", "./uploads"),
|
||||||
HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"),
|
HMACSecret: envOr("HMAC_SECRET", "thisismyrandomstring"),
|
||||||
ServerHost: envOr("SERVER_HOST", "http://192.168.1.17:8080"),
|
ServerHost: envOr("SERVER_HOST", "http://192.168.1.17:8080"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,14 +140,20 @@ func (h *FileHandler) AddTags(c *gin.Context) {
|
|||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
|
|
||||||
var body struct {
|
var body struct {
|
||||||
Tags []string `json:"tags" binding:"required"`
|
Tags []string `json:"tags" binding:"required"`
|
||||||
|
TagType string `json:"tag_type"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain a 'tags' array")
|
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain a 'tags' array")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.files.AddTags(id, body.Tags); err != nil {
|
tagType := body.TagType
|
||||||
|
if tagType == "" {
|
||||||
|
tagType = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.files.AddTags(id, body.Tags, tagType); err != nil {
|
||||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to add tags")
|
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to add tags")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,14 +122,14 @@ func (s *FileService) UpdateOCRText(id, text string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileService) AddTags(fileID string, tagNames []string) error {
|
func (s *FileService) AddTags(fileID string, tagNames []string, tagType string) error {
|
||||||
for _, name := range tagNames {
|
for _, name := range tagNames {
|
||||||
tag, err := s.queries.GetTagByName(context.Background(), name)
|
tag, err := s.queries.GetTagByName(context.Background(), name)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
tag, err = s.queries.CreateTag(context.Background(), db.CreateTagParams{
|
tag, err = s.queries.CreateTag(context.Background(), db.CreateTagParams{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
TagName: name,
|
TagName: name,
|
||||||
TagType: "none",
|
TagType: tagType,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create tag %q: %w", name, err)
|
return fmt.Errorf("create tag %q: %w", name, err)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ expo-env.d.ts
|
|||||||
*.p12
|
*.p12
|
||||||
*.key
|
*.key
|
||||||
*.mobileprovision
|
*.mobileprovision
|
||||||
|
.env
|
||||||
|
|
||||||
# Metro
|
# Metro
|
||||||
.metro-health-check*
|
.metro-health-check*
|
||||||
|
|||||||
+135
-2
@@ -1,10 +1,10 @@
|
|||||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||||
import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, KeyboardAvoidingView, Platform, Keyboard, Alert } from 'react-native';
|
import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, KeyboardAvoidingView, Platform, Keyboard, Alert, Modal, TextInput } from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { useFiles, useFileImage, useDeleteFile } from '../hooks/useFiles';
|
import { useFiles, useFileImage, useDeleteFile, useAddTags } from '../hooks/useFiles';
|
||||||
import { FileItem } from '../types';
|
import { FileItem } from '../types';
|
||||||
import { SearchBar, SearchFilters } from '../components/SearchBar';
|
import { SearchBar, SearchFilters } from '../components/SearchBar';
|
||||||
import { FileThumbnail } from '../components/FileThumbnail';
|
import { FileThumbnail } from '../components/FileThumbnail';
|
||||||
@@ -135,6 +135,10 @@ export function HomeScreen() {
|
|||||||
const [keyboardOpen, setKeyboardOpen] = useState(false);
|
const [keyboardOpen, setKeyboardOpen] = useState(false);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [groupByTags, setGroupByTags] = useState(false);
|
const [groupByTags, setGroupByTags] = useState(false);
|
||||||
|
const [tagModalVisible, setTagModalVisible] = useState(false);
|
||||||
|
const [tagModalMode, setTagModalMode] = useState<'tag' | 'folder'>('tag');
|
||||||
|
const [tagInput, setTagInput] = useState('');
|
||||||
|
const addTags = useAddTags();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const show = Keyboard.addListener('keyboardDidShow', () => setKeyboardOpen(true));
|
const show = Keyboard.addListener('keyboardDidShow', () => setKeyboardOpen(true));
|
||||||
@@ -151,6 +155,8 @@ export function HomeScreen() {
|
|||||||
[files, searchQuery, filters]
|
[files, searchQuery, filters]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(filteredFiles.map(f => f.tags))
|
||||||
|
|
||||||
const groups = groupByTags ? groupByTag(filteredFiles) : groupByDate(filteredFiles);
|
const groups = groupByTags ? groupByTag(filteredFiles) : groupByDate(filteredFiles);
|
||||||
const groupKeys = Object.keys(groups);
|
const groupKeys = Object.keys(groups);
|
||||||
|
|
||||||
@@ -204,6 +210,24 @@ export function HomeScreen() {
|
|||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
}, [selectedIds, navigation]);
|
}, [selectedIds, navigation]);
|
||||||
|
|
||||||
|
const openTagModal = useCallback((mode: 'tag' | 'folder') => {
|
||||||
|
setTagModalMode(mode);
|
||||||
|
setTagInput('');
|
||||||
|
setTagModalVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddTag = useCallback(async () => {
|
||||||
|
const name = tagInput.trim();
|
||||||
|
if (!name) return;
|
||||||
|
const ids = Array.from(selectedIds);
|
||||||
|
const tagType = tagModalMode === 'folder' ? 'folder' : 'none';
|
||||||
|
for (const id of ids) {
|
||||||
|
await addTags.mutateAsync({ fileId: id, tags: [name], tagType });
|
||||||
|
}
|
||||||
|
setTagModalVisible(false);
|
||||||
|
setSelectedIds(new Set());
|
||||||
|
}, [tagInput, selectedIds, tagModalMode, addTags]);
|
||||||
|
|
||||||
const handleItemPress = useCallback((file: FileItem) => {
|
const handleItemPress = useCallback((file: FileItem) => {
|
||||||
if (selectionMode) {
|
if (selectionMode) {
|
||||||
toggleSelection(file.id);
|
toggleSelection(file.id);
|
||||||
@@ -323,6 +347,20 @@ export function HomeScreen() {
|
|||||||
<MaterialIcons name="edit" size={20} color="#1976D2" />
|
<MaterialIcons name="edit" size={20} color="#1976D2" />
|
||||||
<Text style={[styles.selectionActionText, { color: '#1976D2' }]}>Éditer</Text>
|
<Text style={[styles.selectionActionText, { color: '#1976D2' }]}>Éditer</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.selectionActionBtn, styles.tagActionBtn]}
|
||||||
|
onPress={() => openTagModal('tag')}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="label" size={20} color="#7B1FA2" />
|
||||||
|
<Text style={[styles.selectionActionText, { color: '#7B1FA2' }]}>Tags</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.selectionActionBtn, styles.folderActionBtn]}
|
||||||
|
onPress={() => openTagModal('folder')}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="create-new-folder" size={20} color="#F57C00" />
|
||||||
|
<Text style={[styles.selectionActionText, { color: '#F57C00' }]}>Folder</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
@@ -349,6 +387,38 @@ export function HomeScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Modal visible={tagModalVisible} transparent animationType="fade" onRequestClose={() => setTagModalVisible(false)}>
|
||||||
|
<TouchableOpacity style={styles.modalOverlay} activeOpacity={1} onPress={() => setTagModalVisible(false)}>
|
||||||
|
<TouchableOpacity activeOpacity={1} style={styles.modalContent} onPress={() => {}}>
|
||||||
|
<Text style={styles.modalTitle}>
|
||||||
|
{tagModalMode === 'folder' ? 'Créer un dossier' : 'Ajouter un tag'}
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.modalInput}
|
||||||
|
placeholder={tagModalMode === 'folder' ? 'Nom du dossier...' : 'Nom du tag...'}
|
||||||
|
placeholderTextColor="#999"
|
||||||
|
value={tagInput}
|
||||||
|
onChangeText={setTagInput}
|
||||||
|
autoFocus
|
||||||
|
returnKeyType="done"
|
||||||
|
onSubmitEditing={handleAddTag}
|
||||||
|
/>
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<TouchableOpacity style={styles.modalCancelBtn} onPress={() => setTagModalVisible(false)}>
|
||||||
|
<Text style={styles.modalCancelText}>Annuler</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.modalConfirmBtn, !tagInput.trim() && styles.modalConfirmDisabled]}
|
||||||
|
onPress={handleAddTag}
|
||||||
|
disabled={!tagInput.trim()}
|
||||||
|
>
|
||||||
|
<Text style={styles.modalConfirmText}>Ajouter</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -509,4 +579,67 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
tagActionBtn: {
|
||||||
|
borderColor: '#7B1FA2',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
folderActionBtn: {
|
||||||
|
borderColor: '#F57C00',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 20,
|
||||||
|
width: '80%',
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#333',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
modalInput: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#ddd',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 10,
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#333',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
modalActions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
modalCancelBtn: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
modalCancelText: {
|
||||||
|
fontSize: 15,
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
modalConfirmBtn: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
backgroundColor: '#1976D2',
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
modalConfirmDisabled: {
|
||||||
|
backgroundColor: '#ccc',
|
||||||
|
},
|
||||||
|
modalConfirmText: {
|
||||||
|
fontSize: 15,
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ export function useAddTags() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ fileId, tags }: { fileId: string; tags: string[] }) =>
|
mutationFn: ({ fileId, tags, tagType }: { fileId: string; tags: string[]; tagType?: string }) =>
|
||||||
apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags }),
|
apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags, tag_type: tagType }),
|
||||||
onSuccess: (_, { fileId }) => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['files', fileId] });
|
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user