feat(managers): table / cards / rows layouts for the channel manager (E4 S3)
A generic ViewSwitcher (reused from the feed) drives a per-account layout shared by the Subscriptions + Discovery tabs. `table` keeps the DataTable (numbered pager); `cards` and `rows` render a virtualized grid over the same client-side list via the new ChannelLayoutGrid, which composes cells straight from each table's Column<T> render closures (cardPrimary/hideInCard/cardLabel), so both tabs — with their different columns — get the views for free. Status chips surface in the fixed band when the table's own controls row is gone.
This commit is contained in:
@@ -9,9 +9,12 @@ import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
History,
|
||||
LayoutGrid,
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Rows3,
|
||||
Table,
|
||||
UserMinus,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
@@ -25,6 +28,9 @@ import { subsColumn } from "./channelColumns";
|
||||
import { notify } from "../lib/notifications";
|
||||
import Tooltip from "./Tooltip";
|
||||
import DataTable, { type Column } from "./DataTable";
|
||||
import ChannelLayoutGrid from "./ChannelLayoutGrid";
|
||||
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
|
||||
import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
import ChannelLink from "./ChannelLink";
|
||||
import AboutCell from "./AboutCell";
|
||||
@@ -48,6 +54,12 @@ const STATUS_FILTERS: { id: ChannelStatusFilter; labelKey: string }[] = [
|
||||
{ id: "hidden", labelKey: "channels.filters.hidden" },
|
||||
];
|
||||
|
||||
const LAYOUT_ICON: Record<ChannelLayout, typeof Table> = {
|
||||
table: Table,
|
||||
cards: LayoutGrid,
|
||||
rows: Rows3,
|
||||
};
|
||||
|
||||
export default function Channels() {
|
||||
const me = useMe();
|
||||
const canWrite = me.can_write;
|
||||
@@ -61,11 +73,13 @@ export default function Channels() {
|
||||
search,
|
||||
statusFilter,
|
||||
view,
|
||||
layout,
|
||||
filtersResetToken,
|
||||
focusName: focusChannelName,
|
||||
focusToken: focusChannelToken,
|
||||
} = useChannels();
|
||||
const { setSearch, setStatusFilter, setView, focusChannel: onFocusChannel } = useChannelsActions();
|
||||
const { setSearch, setStatusFilter, setView, setLayout, focusChannel: onFocusChannel } =
|
||||
useChannelsActions();
|
||||
const { openChannel: onViewChannel, setPage } = useNavigationActions();
|
||||
const feedFilters = useFeedFilters();
|
||||
const { setFilters: setFeedFilters } = useFeedFiltersActions();
|
||||
@@ -463,6 +477,24 @@ export default function Channels() {
|
||||
</div>
|
||||
);
|
||||
|
||||
// Table / cards / rows density — shared by the subscribed + discovery tables (blocked is a chip
|
||||
// list, no layout to switch). Rides the same generic ViewSwitcher the feed uses.
|
||||
const layoutOptions: ViewOption<ChannelLayout>[] = CHANNEL_LAYOUTS.map((id) => ({
|
||||
id,
|
||||
label: t("channels.layout." + id),
|
||||
icon: LAYOUT_ICON[id],
|
||||
}));
|
||||
const layoutSwitcher = (
|
||||
<ViewSwitcher
|
||||
value={layout}
|
||||
options={layoutOptions}
|
||||
onChange={setLayout}
|
||||
label={t("channels.layoutLabel")}
|
||||
labelClassName="hidden lg:inline"
|
||||
/>
|
||||
);
|
||||
const hasLayout = view === "subscribed" || view === "discovery";
|
||||
|
||||
const tabs = (
|
||||
<div className="px-4 pt-4 max-w-[96rem] mx-auto flex items-center justify-between gap-4 flex-wrap">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
@@ -480,7 +512,10 @@ export default function Channels() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{view === "subscribed" && statsBlock}
|
||||
<div className="flex items-center gap-3">
|
||||
{view === "subscribed" && statsBlock}
|
||||
{hasLayout && layoutSwitcher}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -541,6 +576,11 @@ export default function Channels() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* In table mode the status chips ride in DataTable's own in-band controls row (beside the
|
||||
pager). The card/row layouts have no such row, so surface the same chips here. */}
|
||||
{view === "subscribed" && layout !== "table" && (
|
||||
<div className="px-4 pb-2 max-w-[96rem] mx-auto">{statusChips}</div>
|
||||
)}
|
||||
</PageToolbar>
|
||||
{tagManagerOpen && (
|
||||
<TagManager onClose={() => setTagManagerOpen(false)} onFocusChannel={onFocusChannel} />
|
||||
@@ -594,10 +634,11 @@ export default function Channels() {
|
||||
visual gap). */
|
||||
<div className="px-4 pb-4 max-w-[96rem] mx-auto">
|
||||
{/* Channel table — only the rows scroll; the header row is sticky, and the controls row
|
||||
(status chips + pager) rides in the fixed band right above it (controlsInBand). */}
|
||||
(status chips + pager) rides in the fixed band right above it (controlsInBand). The
|
||||
card/row layouts drop the table + pager for a virtualized grid over the same rows. */}
|
||||
{channelsQuery.isLoading ? (
|
||||
<div className="text-muted py-8">{t("channels.loading")}</div>
|
||||
) : (
|
||||
) : layout === "table" ? (
|
||||
<DataTable
|
||||
rows={visibleChannels}
|
||||
columns={columns}
|
||||
@@ -610,6 +651,16 @@ export default function Channels() {
|
||||
rowClassName={(c) => (c.hidden ? "opacity-60" : "")}
|
||||
emptyText={t("channels.empty")}
|
||||
/>
|
||||
) : (
|
||||
<ChannelLayoutGrid
|
||||
rows={visibleChannels}
|
||||
columns={columns}
|
||||
rowKey={(c) => c.id}
|
||||
layout={layout}
|
||||
actionsKey="actions"
|
||||
rowClassName={(c) => (c.hidden ? "opacity-60" : "")}
|
||||
emptyText={t("channels.empty")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user