feat: calculs tableaux Structured Views + Link Preview + fixes
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m31s
CI / Deploy production (on server) (push) Has been skipped

- 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:
Antigravity
2026-06-14 17:56:54 +00:00
parent ba3ab3422a
commit 83110200d5
5 changed files with 88 additions and 4 deletions

View File

@@ -50,7 +50,7 @@ export function AddPropertyDialog({ open, onClose, onSubmit }: AddPropertyDialog
<div
role="dialog"
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">
<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}>
{t('general.cancel')}
</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')}
</Button>
</div>

View File

@@ -28,7 +28,7 @@ import { getNoteDisplayTitle } from '@/lib/note-preview'
import { useLanguage } from '@/lib/i18n'
import { formatAbsoluteDateLocalized } from '@/lib/utils/format-localized-date'
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 { openNotePeek } from '@/lib/note-peek-sync'
@@ -88,6 +88,7 @@ export function NotesStructuredTable({
const [filterValue, setFilterValue] = useState('')
const [propertyToDelete, setPropertyToDelete] = useState<{ id: string; name: string } | null>(null)
const [deletingProperty, setDeletingProperty] = useState(false)
const [calcTypes, setCalcTypes] = useState<Record<string, 'none' | 'sum' | 'avg' | 'min' | 'max' | 'count'>>({})
// Memory Echo states
const { requestAiConsent } = useAiConsent()
@@ -189,6 +190,53 @@ export function NotesStructuredTable({
return sortNotesWithProperties(filtered, noteValues, 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']) => {
setSort((prev) =>
prev.propertyId === propertyId
@@ -429,6 +477,38 @@ export function NotesStructuredTable({
)
})}
</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>
{displayed.length === 0 && (
<p className="text-center py-8 text-muted-foreground text-sm">{t('structuredViews.noMatchingNotes')}</p>

View File

@@ -140,7 +140,7 @@ export function StructuredViewsWizard({
<div
role="dialog"
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>

View File

@@ -2647,6 +2647,8 @@
"deleteProperty": "Delete 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.",
"calculations": "Calculations",
"calcClickToChange": "Click to change calculation type (Sum, Average, Min, Max, Count, None)",
"deletePropertySuccess": "Field deleted",
"cellEmpty": "—",
"multiselectPick": "Choose…",

View File

@@ -2651,6 +2651,8 @@
"deleteProperty": "Supprimer le 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.",
"calculations": "Calculs",
"calcClickToChange": "Cliquer pour changer le type de calcul (Somme, Moyenne, Min, Max, Compte, Aucun)",
"deletePropertySuccess": "Champ supprimé",
"cellEmpty": "—",
"multiselectPick": "Choisir…",