Seth commited on
Commit ·
401b968
1
Parent(s): 6953c2b
update
Browse files
frontend/src/components/workspace/MainTableWorkspace.jsx
CHANGED
|
@@ -15,6 +15,8 @@ export default function MainTableWorkspace({
|
|
| 15 |
search = null,
|
| 16 |
right = null,
|
| 17 |
filters = null,
|
|
|
|
|
|
|
| 18 |
sectionIcon: SectionIcon,
|
| 19 |
sectionTitle,
|
| 20 |
sectionCount,
|
|
@@ -93,7 +95,7 @@ export default function MainTableWorkspace({
|
|
| 93 |
</div>
|
| 94 |
</div>
|
| 95 |
|
| 96 |
-
{filters && (
|
| 97 |
<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">{filters}</div>
|
| 98 |
)}
|
| 99 |
|
|
@@ -115,12 +117,23 @@ export default function MainTableWorkspace({
|
|
| 115 |
</span>
|
| 116 |
</button>
|
| 117 |
|
| 118 |
-
{sectionOpen &&
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
</div>
|
| 125 |
</div>
|
| 126 |
);
|
|
|
|
| 15 |
search = null,
|
| 16 |
right = null,
|
| 17 |
filters = null,
|
| 18 |
+
/** 'top' = full-width strip above the table card; 'left' = narrow rail beside the table inside the card */
|
| 19 |
+
filtersPlacement = 'top',
|
| 20 |
sectionIcon: SectionIcon,
|
| 21 |
sectionTitle,
|
| 22 |
sectionCount,
|
|
|
|
| 95 |
</div>
|
| 96 |
</div>
|
| 97 |
|
| 98 |
+
{filtersPlacement === 'top' && filters && (
|
| 99 |
<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">{filters}</div>
|
| 100 |
)}
|
| 101 |
|
|
|
|
| 117 |
</span>
|
| 118 |
</button>
|
| 119 |
|
| 120 |
+
{sectionOpen &&
|
| 121 |
+
(filtersPlacement === 'left' && filters ? (
|
| 122 |
+
<div className="flex flex-col gap-3 border-t border-slate-100 px-2 pb-3 pt-2 sm:px-4 md:flex-row md:items-start md:gap-3">
|
| 123 |
+
<aside className="w-full shrink-0 rounded-lg border border-slate-200 bg-slate-50/80 p-2.5 md:w-52 md:max-w-[13rem] overflow-y-auto max-h-[min(70vh,720px)]">
|
| 124 |
+
{filters}
|
| 125 |
+
</aside>
|
| 126 |
+
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
| 127 |
+
{tableToolbar}
|
| 128 |
+
{children}
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
) : (
|
| 132 |
+
<>
|
| 133 |
+
{tableToolbar}
|
| 134 |
+
{children}
|
| 135 |
+
</>
|
| 136 |
+
))}
|
| 137 |
</div>
|
| 138 |
</div>
|
| 139 |
);
|
frontend/src/pages/Contacts.jsx
CHANGED
|
@@ -48,7 +48,7 @@ export default function Contacts() {
|
|
| 48 |
const [seqLoading, setSeqLoading] = useState(false);
|
| 49 |
const [searchQuery, setSearchQuery] = useState('');
|
| 50 |
const [filterRows, setFilterRows] = useState(() => [makeFilterRow()]);
|
| 51 |
-
const [
|
| 52 |
const [sortBy, setSortBy] = useState('created_at');
|
| 53 |
const [sortDir, setSortDir] = useState('desc');
|
| 54 |
const [page, setPage] = useState(1);
|
|
@@ -221,7 +221,7 @@ export default function Contacts() {
|
|
| 221 |
};
|
| 222 |
|
| 223 |
const seedDemoContacts = async () => {
|
| 224 |
-
|
| 225 |
try {
|
| 226 |
const res = await fetch('/api/contacts/seed-demo', { method: 'POST' });
|
| 227 |
const data = await res.json().catch(() => ({}));
|
|
@@ -230,15 +230,11 @@ export default function Contacts() {
|
|
| 230 |
}
|
| 231 |
await fetchFields();
|
| 232 |
await fetchContacts();
|
| 233 |
-
const n = data.inserted ?? 0;
|
| 234 |
-
if (n > 0) {
|
| 235 |
-
alert(`Loaded ${n} demo contact${n === 1 ? '' : 's'} with varied Apollo-style fields.`);
|
| 236 |
-
}
|
| 237 |
} catch (e) {
|
| 238 |
console.error(e);
|
| 239 |
alert(e.message || 'Could not load demo data');
|
| 240 |
} finally {
|
| 241 |
-
|
| 242 |
}
|
| 243 |
};
|
| 244 |
|
|
@@ -572,30 +568,36 @@ export default function Contacts() {
|
|
| 572 |
};
|
| 573 |
|
| 574 |
const filtersBlock = (
|
| 575 |
-
<div className="space-y-
|
| 576 |
-
<div className="
|
| 577 |
-
<div className="flex items-
|
| 578 |
-
<SlidersHorizontal className="h-3.5 w-3.5" />
|
| 579 |
-
|
| 580 |
</div>
|
| 581 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 582 |
<Plus className="h-3.5 w-3.5" />
|
| 583 |
Add filter
|
| 584 |
</Button>
|
| 585 |
</div>
|
| 586 |
-
<div className="space-y-
|
| 587 |
{filterRows.map((row) => (
|
| 588 |
<div
|
| 589 |
key={row.id}
|
| 590 |
-
className="
|
| 591 |
>
|
| 592 |
-
<div
|
| 593 |
<label className="sr-only">Field</label>
|
| 594 |
<Select
|
| 595 |
value={row.field}
|
| 596 |
onValueChange={(v) => updateFilterRow(row.id, { field: v })}
|
| 597 |
>
|
| 598 |
-
<SelectTrigger className="border-slate-200 bg-white">
|
| 599 |
<SelectValue placeholder="Field" />
|
| 600 |
</SelectTrigger>
|
| 601 |
<SelectContent>
|
|
@@ -610,13 +612,13 @@ export default function Contacts() {
|
|
| 610 |
</div>
|
| 611 |
{row.field !== 'none' && (
|
| 612 |
<>
|
| 613 |
-
<div
|
| 614 |
<label className="sr-only">Operator</label>
|
| 615 |
<Select
|
| 616 |
value={row.op}
|
| 617 |
onValueChange={(v) => updateFilterRow(row.id, { op: v })}
|
| 618 |
>
|
| 619 |
-
<SelectTrigger className="border-slate-200 bg-white">
|
| 620 |
<SelectValue />
|
| 621 |
</SelectTrigger>
|
| 622 |
<SelectContent>
|
|
@@ -629,21 +631,19 @@ export default function Contacts() {
|
|
| 629 |
</Select>
|
| 630 |
</div>
|
| 631 |
{(row.op === 'contains' || row.op === 'equals') && (
|
| 632 |
-
<
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
/>
|
| 641 |
-
</div>
|
| 642 |
)}
|
| 643 |
{(row.op === 'from' || row.op === 'to' || row.op === 'between') && (
|
| 644 |
-
<div className="flex
|
| 645 |
<Input
|
| 646 |
-
className="border-slate-200 bg-white
|
| 647 |
placeholder="From"
|
| 648 |
value={row.fromVal}
|
| 649 |
onChange={(e) =>
|
|
@@ -651,7 +651,7 @@ export default function Contacts() {
|
|
| 651 |
}
|
| 652 |
/>
|
| 653 |
<Input
|
| 654 |
-
className="border-slate-200 bg-white
|
| 655 |
placeholder="To"
|
| 656 |
value={row.toVal}
|
| 657 |
onChange={(e) =>
|
|
@@ -660,16 +660,16 @@ export default function Contacts() {
|
|
| 660 |
/>
|
| 661 |
</div>
|
| 662 |
)}
|
| 663 |
-
<div className="flex
|
| 664 |
<Button
|
| 665 |
type="button"
|
| 666 |
variant="ghost"
|
| 667 |
size="icon"
|
| 668 |
-
className="text-slate-400 hover:text-red-600"
|
| 669 |
aria-label="Remove filter row"
|
| 670 |
onClick={() => removeFilterRow(row.id)}
|
| 671 |
>
|
| 672 |
-
<Trash2 className="h-
|
| 673 |
</Button>
|
| 674 |
</div>
|
| 675 |
</>
|
|
@@ -696,16 +696,16 @@ export default function Contacts() {
|
|
| 696 |
<div className="flex flex-wrap items-center gap-2 justify-end">
|
| 697 |
<Button
|
| 698 |
type="button"
|
| 699 |
-
variant="
|
| 700 |
size="sm"
|
| 701 |
onClick={seedDemoContacts}
|
| 702 |
-
disabled={
|
| 703 |
-
|
| 704 |
>
|
| 705 |
-
{
|
| 706 |
<Loader2 className="h-4 w-4 animate-spin shrink-0" aria-hidden />
|
| 707 |
) : null}
|
| 708 |
-
|
| 709 |
</Button>
|
| 710 |
<Button variant="outline" size="sm" onClick={() => fetchContacts()}>
|
| 711 |
Refresh
|
|
@@ -713,6 +713,7 @@ export default function Contacts() {
|
|
| 713 |
</div>
|
| 714 |
}
|
| 715 |
filters={filtersBlock}
|
|
|
|
| 716 |
sectionIcon={Users}
|
| 717 |
sectionTitle="All contacts"
|
| 718 |
sectionCount={total}
|
|
@@ -732,15 +733,15 @@ export default function Contacts() {
|
|
| 732 |
<div className="flex flex-wrap items-center justify-center gap-2">
|
| 733 |
<Button
|
| 734 |
type="button"
|
| 735 |
-
variant="
|
| 736 |
size="sm"
|
| 737 |
onClick={seedDemoContacts}
|
| 738 |
-
disabled={
|
| 739 |
>
|
| 740 |
-
{
|
| 741 |
<Loader2 className="h-4 w-4 animate-spin mr-2" aria-hidden />
|
| 742 |
) : null}
|
| 743 |
-
Load demo
|
| 744 |
</Button>
|
| 745 |
<Button variant="outline" size="sm" onClick={beginAddContact}>
|
| 746 |
New contact
|
|
|
|
| 48 |
const [seqLoading, setSeqLoading] = useState(false);
|
| 49 |
const [searchQuery, setSearchQuery] = useState('');
|
| 50 |
const [filterRows, setFilterRows] = useState(() => [makeFilterRow()]);
|
| 51 |
+
const [seedBusy, setSeedBusy] = useState(false);
|
| 52 |
const [sortBy, setSortBy] = useState('created_at');
|
| 53 |
const [sortDir, setSortDir] = useState('desc');
|
| 54 |
const [page, setPage] = useState(1);
|
|
|
|
| 221 |
};
|
| 222 |
|
| 223 |
const seedDemoContacts = async () => {
|
| 224 |
+
setSeedBusy(true);
|
| 225 |
try {
|
| 226 |
const res = await fetch('/api/contacts/seed-demo', { method: 'POST' });
|
| 227 |
const data = await res.json().catch(() => ({}));
|
|
|
|
| 230 |
}
|
| 231 |
await fetchFields();
|
| 232 |
await fetchContacts();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
} catch (e) {
|
| 234 |
console.error(e);
|
| 235 |
alert(e.message || 'Could not load demo data');
|
| 236 |
} finally {
|
| 237 |
+
setSeedBusy(false);
|
| 238 |
}
|
| 239 |
};
|
| 240 |
|
|
|
|
| 568 |
};
|
| 569 |
|
| 570 |
const filtersBlock = (
|
| 571 |
+
<div className="space-y-2 text-xs">
|
| 572 |
+
<div className="space-y-2">
|
| 573 |
+
<div className="flex items-start gap-1.5 text-slate-600 font-medium leading-snug">
|
| 574 |
+
<SlidersHorizontal className="h-3.5 w-3.5 mt-0.5 shrink-0" />
|
| 575 |
+
<span>Filters (AND)</span>
|
| 576 |
</div>
|
| 577 |
+
<Button
|
| 578 |
+
type="button"
|
| 579 |
+
variant="outline"
|
| 580 |
+
size="sm"
|
| 581 |
+
className="h-8 w-full gap-1 text-xs"
|
| 582 |
+
onClick={addFilterRow}
|
| 583 |
+
>
|
| 584 |
<Plus className="h-3.5 w-3.5" />
|
| 585 |
Add filter
|
| 586 |
</Button>
|
| 587 |
</div>
|
| 588 |
+
<div className="space-y-2">
|
| 589 |
{filterRows.map((row) => (
|
| 590 |
<div
|
| 591 |
key={row.id}
|
| 592 |
+
className="space-y-2 rounded-lg border border-slate-200 bg-white/90 p-2 shadow-sm"
|
| 593 |
>
|
| 594 |
+
<div>
|
| 595 |
<label className="sr-only">Field</label>
|
| 596 |
<Select
|
| 597 |
value={row.field}
|
| 598 |
onValueChange={(v) => updateFilterRow(row.id, { field: v })}
|
| 599 |
>
|
| 600 |
+
<SelectTrigger className="h-8 border-slate-200 bg-white text-xs">
|
| 601 |
<SelectValue placeholder="Field" />
|
| 602 |
</SelectTrigger>
|
| 603 |
<SelectContent>
|
|
|
|
| 612 |
</div>
|
| 613 |
{row.field !== 'none' && (
|
| 614 |
<>
|
| 615 |
+
<div>
|
| 616 |
<label className="sr-only">Operator</label>
|
| 617 |
<Select
|
| 618 |
value={row.op}
|
| 619 |
onValueChange={(v) => updateFilterRow(row.id, { op: v })}
|
| 620 |
>
|
| 621 |
+
<SelectTrigger className="h-8 border-slate-200 bg-white text-xs">
|
| 622 |
<SelectValue />
|
| 623 |
</SelectTrigger>
|
| 624 |
<SelectContent>
|
|
|
|
| 631 |
</Select>
|
| 632 |
</div>
|
| 633 |
{(row.op === 'contains' || row.op === 'equals') && (
|
| 634 |
+
<Input
|
| 635 |
+
className="h-8 border-slate-200 bg-white text-xs"
|
| 636 |
+
placeholder={`“${row.field}”`}
|
| 637 |
+
value={row.value}
|
| 638 |
+
onChange={(e) =>
|
| 639 |
+
updateFilterRow(row.id, { value: e.target.value })
|
| 640 |
+
}
|
| 641 |
+
/>
|
|
|
|
|
|
|
| 642 |
)}
|
| 643 |
{(row.op === 'from' || row.op === 'to' || row.op === 'between') && (
|
| 644 |
+
<div className="flex flex-col gap-2">
|
| 645 |
<Input
|
| 646 |
+
className="h-8 border-slate-200 bg-white text-xs"
|
| 647 |
placeholder="From"
|
| 648 |
value={row.fromVal}
|
| 649 |
onChange={(e) =>
|
|
|
|
| 651 |
}
|
| 652 |
/>
|
| 653 |
<Input
|
| 654 |
+
className="h-8 border-slate-200 bg-white text-xs"
|
| 655 |
placeholder="To"
|
| 656 |
value={row.toVal}
|
| 657 |
onChange={(e) =>
|
|
|
|
| 660 |
/>
|
| 661 |
</div>
|
| 662 |
)}
|
| 663 |
+
<div className="flex justify-end pt-0.5">
|
| 664 |
<Button
|
| 665 |
type="button"
|
| 666 |
variant="ghost"
|
| 667 |
size="icon"
|
| 668 |
+
className="h-8 w-8 text-slate-400 hover:text-red-600"
|
| 669 |
aria-label="Remove filter row"
|
| 670 |
onClick={() => removeFilterRow(row.id)}
|
| 671 |
>
|
| 672 |
+
<Trash2 className="h-3.5 w-3.5" />
|
| 673 |
</Button>
|
| 674 |
</div>
|
| 675 |
</>
|
|
|
|
| 696 |
<div className="flex flex-wrap items-center gap-2 justify-end">
|
| 697 |
<Button
|
| 698 |
type="button"
|
| 699 |
+
variant="secondary"
|
| 700 |
size="sm"
|
| 701 |
onClick={seedDemoContacts}
|
| 702 |
+
disabled={seedBusy}
|
| 703 |
+
title="Insert sample Apollo-style contacts for preview"
|
| 704 |
>
|
| 705 |
+
{seedBusy ? (
|
| 706 |
<Loader2 className="h-4 w-4 animate-spin shrink-0" aria-hidden />
|
| 707 |
) : null}
|
| 708 |
+
Demo data
|
| 709 |
</Button>
|
| 710 |
<Button variant="outline" size="sm" onClick={() => fetchContacts()}>
|
| 711 |
Refresh
|
|
|
|
| 713 |
</div>
|
| 714 |
}
|
| 715 |
filters={filtersBlock}
|
| 716 |
+
filtersPlacement="left"
|
| 717 |
sectionIcon={Users}
|
| 718 |
sectionTitle="All contacts"
|
| 719 |
sectionCount={total}
|
|
|
|
| 733 |
<div className="flex flex-wrap items-center justify-center gap-2">
|
| 734 |
<Button
|
| 735 |
type="button"
|
| 736 |
+
variant="secondary"
|
| 737 |
size="sm"
|
| 738 |
onClick={seedDemoContacts}
|
| 739 |
+
disabled={seedBusy}
|
| 740 |
>
|
| 741 |
+
{seedBusy ? (
|
| 742 |
<Loader2 className="h-4 w-4 animate-spin mr-2" aria-hidden />
|
| 743 |
) : null}
|
| 744 |
+
Load demo rows (preview UI)
|
| 745 |
</Button>
|
| 746 |
<Button variant="outline" size="sm" onClick={beginAddContact}>
|
| 747 |
New contact
|