feat: calculs tableaux Structured Views + Link Preview + fixes
- Calculs en pied de tableau : Somme/Moyenne/Min/Max/Compte, cliquable pour changer - Link Preview : métadonnées persistées + texte indexable pour recherche/embeddings - Fix: bg-memento-paper → bg-card (dark mode) sur dialogs Structured Views - Fix: bouton Ajouter un champ → brand-accent au lieu de primary - Calendar view retiré du sélecteur (non pertinent)
This commit is contained in:
@@ -50,7 +50,7 @@ export function AddPropertyDialog({ open, onClose, onSubmit }: AddPropertyDialog
|
|||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal
|
aria-modal
|
||||||
className="w-full max-w-md rounded-2xl border border-border bg-memento-paper shadow-xl p-6 space-y-5"
|
className="w-full max-w-md rounded-2xl border border-border bg-card shadow-xl p-6 space-y-5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="font-memento-serif text-lg">{t('structuredViews.addPropertyTitle')}</h2>
|
<h2 className="font-memento-serif text-lg">{t('structuredViews.addPropertyTitle')}</h2>
|
||||||
@@ -117,7 +117,7 @@ export function AddPropertyDialog({ open, onClose, onSubmit }: AddPropertyDialog
|
|||||||
<Button type="button" variant="ghost" onClick={onClose}>
|
<Button type="button" variant="ghost" onClick={onClose}>
|
||||||
{t('general.cancel')}
|
{t('general.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={saving || !name.trim()}>
|
<Button type="submit" disabled={saving || !name.trim()} className="bg-brand-accent text-white hover:bg-brand-accent/90">
|
||||||
{t('structuredViews.addProperty')}
|
{t('structuredViews.addProperty')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { getNoteDisplayTitle } from '@/lib/note-preview'
|
|||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
|
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
|
||||||
import { enUS, fr, faIR } from 'date-fns/locale'
|
import { enUS, fr, faIR } from 'date-fns/locale'
|
||||||
import { ChevronDown, ChevronUp, Filter, Trash2, Sparkles, Brain, Loader2, ArrowUpRight, Link2 } from 'lucide-react'
|
import { ChevronDown, ChevronUp, Filter, Trash2, Sparkles, Brain, Loader2, ArrowUpRight, Link2, Sigma } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { openNotePeek } from '@/lib/note-peek-sync'
|
import { openNotePeek } from '@/lib/note-peek-sync'
|
||||||
|
|
||||||
@@ -88,6 +88,7 @@ export function NotesStructuredTable({
|
|||||||
const [filterValue, setFilterValue] = useState('')
|
const [filterValue, setFilterValue] = useState('')
|
||||||
const [propertyToDelete, setPropertyToDelete] = useState<{ id: string; name: string } | null>(null)
|
const [propertyToDelete, setPropertyToDelete] = useState<{ id: string; name: string } | null>(null)
|
||||||
const [deletingProperty, setDeletingProperty] = useState(false)
|
const [deletingProperty, setDeletingProperty] = useState(false)
|
||||||
|
const [calcTypes, setCalcTypes] = useState<Record<string, 'none' | 'sum' | 'avg' | 'min' | 'max' | 'count'>>({})
|
||||||
|
|
||||||
// Memory Echo states
|
// Memory Echo states
|
||||||
const { requestAiConsent } = useAiConsent()
|
const { requestAiConsent } = useAiConsent()
|
||||||
@@ -189,6 +190,53 @@ export function NotesStructuredTable({
|
|||||||
return sortNotesWithProperties(filtered, noteValues, sort, schema.properties)
|
return sortNotesWithProperties(filtered, noteValues, sort, schema.properties)
|
||||||
}, [notes, noteValues, filters, sort, schema.properties])
|
}, [notes, noteValues, filters, sort, schema.properties])
|
||||||
|
|
||||||
|
const numberProperties = schema.properties.filter(p => p.type === 'number')
|
||||||
|
|
||||||
|
const calculations = useMemo(() => {
|
||||||
|
const result: Record<string, { value: string | null; type: string }> = {}
|
||||||
|
for (const prop of schema.properties) {
|
||||||
|
const calcType = calcTypes[prop.id] || (prop.type === 'number' ? 'sum' : 'count')
|
||||||
|
if (calcType === 'none') { result[prop.id] = { value: null, type: 'none' }; continue }
|
||||||
|
|
||||||
|
const values = displayed
|
||||||
|
.map(n => noteValues[n.id]?.[prop.id])
|
||||||
|
.filter(v => v !== null && v !== undefined && v !== '')
|
||||||
|
|
||||||
|
if (prop.type === 'number') {
|
||||||
|
const nums = values.map(v => parseFloat(String(v))).filter(n => !isNaN(n))
|
||||||
|
if (nums.length === 0) { result[prop.id] = { value: null, type: calcType }; continue }
|
||||||
|
if (calcType === 'sum') result[prop.id] = { value: formatNum(nums.reduce((a, b) => a + b, 0)), type: 'sum' }
|
||||||
|
else if (calcType === 'avg') result[prop.id] = { value: formatNum(nums.reduce((a, b) => a + b, 0) / nums.length), type: 'avg' }
|
||||||
|
else if (calcType === 'min') result[prop.id] = { value: formatNum(Math.min(...nums)), type: 'min' }
|
||||||
|
else if (calcType === 'max') result[prop.id] = { value: formatNum(Math.max(...nums)), type: 'max' }
|
||||||
|
else if (calcType === 'count') result[prop.id] = { value: String(nums.length), type: 'count' }
|
||||||
|
} else {
|
||||||
|
result[prop.id] = { value: calcType === 'count' ? String(values.length) : null, type: calcType }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}, [displayed, noteValues, schema.properties, calcTypes])
|
||||||
|
|
||||||
|
function formatNum(n: number): string {
|
||||||
|
return Number.isInteger(n) ? String(n) : n.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function cycleCalcType(propId: string, propType: string) {
|
||||||
|
const current = calcTypes[propId] || (propType === 'number' ? 'sum' : 'count')
|
||||||
|
const numberCycle: Array<typeof current> = ['sum', 'avg', 'min', 'max', 'count', 'none']
|
||||||
|
const otherCycle: Array<typeof current> = ['count', 'none']
|
||||||
|
const cycle = propType === 'number' ? numberCycle : otherCycle
|
||||||
|
const nextIdx = (cycle.indexOf(current) + 1) % cycle.length
|
||||||
|
setCalcTypes(prev => ({ ...prev, [propId]: cycle[nextIdx] }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcLabel = (type: string): string => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
sum: 'Σ', avg: 'x̄', min: 'min', max: 'max', count: '#', none: ''
|
||||||
|
}
|
||||||
|
return labels[type] || ''
|
||||||
|
}
|
||||||
|
|
||||||
const toggleSort = (propertyId: ColumnSort['propertyId']) => {
|
const toggleSort = (propertyId: ColumnSort['propertyId']) => {
|
||||||
setSort((prev) =>
|
setSort((prev) =>
|
||||||
prev.propertyId === propertyId
|
prev.propertyId === propertyId
|
||||||
@@ -429,6 +477,38 @@ export function NotesStructuredTable({
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
{numberProperties.length > 0 && (
|
||||||
|
<tfoot>
|
||||||
|
<tr className="border-t-2 border-border/40 bg-muted/20">
|
||||||
|
<td className="px-4 py-2 text-[10px] uppercase tracking-widest font-bold text-muted-foreground">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Sigma size={12} />
|
||||||
|
{t('structuredViews.calculations')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{schema.properties.map((p) => {
|
||||||
|
const calc = calculations[p.id]
|
||||||
|
if (!calc || calc.value === null) {
|
||||||
|
return <td key={p.id} className="px-4 py-2 text-[11px] text-muted-foreground/40">—</td>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<td key={p.id} className="px-4 py-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => cycleCalcType(p.id, p.type)}
|
||||||
|
className="text-[11px] font-semibold tabular-nums hover:text-primary transition-colors"
|
||||||
|
title={t('structuredViews.calcClickToChange')}
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground mr-1">{calcLabel(calc.type)}</span>
|
||||||
|
{calc.value}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<td />
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
)}
|
||||||
</table>
|
</table>
|
||||||
{displayed.length === 0 && (
|
{displayed.length === 0 && (
|
||||||
<p className="text-center py-8 text-muted-foreground text-sm">{t('structuredViews.noMatchingNotes')}</p>
|
<p className="text-center py-8 text-muted-foreground text-sm">{t('structuredViews.noMatchingNotes')}</p>
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function StructuredViewsWizard({
|
|||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal
|
aria-modal
|
||||||
className="w-full max-w-lg rounded-2xl border border-border bg-memento-paper shadow-xl p-6 space-y-5"
|
className="w-full max-w-lg rounded-2xl border border-border bg-card shadow-xl p-6 space-y-5"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -2647,6 +2647,8 @@
|
|||||||
"deleteProperty": "Delete field",
|
"deleteProperty": "Delete field",
|
||||||
"deletePropertyTitle": "Delete this field?",
|
"deletePropertyTitle": "Delete this field?",
|
||||||
"deletePropertyConfirm": "The field \"{name}\" and all its values on notes in this notebook will be removed. This cannot be undone.",
|
"deletePropertyConfirm": "The field \"{name}\" and all its values on notes in this notebook will be removed. This cannot be undone.",
|
||||||
|
"calculations": "Calculations",
|
||||||
|
"calcClickToChange": "Click to change calculation type (Sum, Average, Min, Max, Count, None)",
|
||||||
"deletePropertySuccess": "Field deleted",
|
"deletePropertySuccess": "Field deleted",
|
||||||
"cellEmpty": "—",
|
"cellEmpty": "—",
|
||||||
"multiselectPick": "Choose…",
|
"multiselectPick": "Choose…",
|
||||||
|
|||||||
@@ -2651,6 +2651,8 @@
|
|||||||
"deleteProperty": "Supprimer le champ",
|
"deleteProperty": "Supprimer le champ",
|
||||||
"deletePropertyTitle": "Supprimer ce champ ?",
|
"deletePropertyTitle": "Supprimer ce champ ?",
|
||||||
"deletePropertyConfirm": "Le champ « {name} » et toutes ses valeurs sur les notes de ce carnet seront supprimés. Cette action est irréversible.",
|
"deletePropertyConfirm": "Le champ « {name} » et toutes ses valeurs sur les notes de ce carnet seront supprimés. Cette action est irréversible.",
|
||||||
|
"calculations": "Calculs",
|
||||||
|
"calcClickToChange": "Cliquer pour changer le type de calcul (Somme, Moyenne, Min, Max, Compte, Aucun)",
|
||||||
"deletePropertySuccess": "Champ supprimé",
|
"deletePropertySuccess": "Champ supprimé",
|
||||||
"cellEmpty": "—",
|
"cellEmpty": "—",
|
||||||
"multiselectPick": "Choisir…",
|
"multiselectPick": "Choisir…",
|
||||||
|
|||||||
Reference in New Issue
Block a user