Skip to content

Commit

Permalink
Merge pull request #260 from Southclaws/tags-ui
Browse files Browse the repository at this point in the history
Tags UI
  • Loading branch information
Southclaws authored Oct 25, 2024
2 parents d1d5f30 + 144db68 commit 1702fea
Show file tree
Hide file tree
Showing 26 changed files with 1,256 additions and 236 deletions.
28 changes: 17 additions & 11 deletions app/resources/tag/tag_querier/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,6 @@ func New(db *ent.Client, raw *sqlx.DB) *Querier {
return &Querier{db, raw}
}

func (q *Querier) List(ctx context.Context) (tag_ref.Tags, error) {
r, err := q.db.Tag.Query().All(ctx)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

tags := dt.Map(r, tag_ref.Map(nil))

return tags, nil
}

const tagItemsCountManyQuery = `select
t.id tag_id, -- tag ID
count(tp.tag_id) + count(tn.tag_id) items -- number of items,
Expand All @@ -45,6 +34,23 @@ group by
t.id
`

func (q *Querier) List(ctx context.Context) (tag_ref.Tags, error) {
r, err := q.db.Tag.Query().All(ctx)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

var counts tag_ref.TagItemsResults
err = q.raw.SelectContext(ctx, &counts, tagItemsCountManyQuery)
if err != nil {
return nil, fault.Wrap(err, fctx.With(ctx))
}

tags := dt.Map(r, tag_ref.Map(counts))

return tags, nil
}

func (q *Querier) Search(ctx context.Context, query string) (tag_ref.Tags, error) {
r, err := q.db.Tag.Query().
Where(
Expand Down
6 changes: 3 additions & 3 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"prepare": "panda codegen",
"dev": "next dev --turbo",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
Expand Down Expand Up @@ -66,8 +66,8 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@pandacss/dev": "^0.46.1",
"@pandacss/types": "^0.46.1",
"@pandacss/dev": "^0.47.0",
"@pandacss/types": "^0.47.0",
"@park-ui/panda-preset": "^0.42.0",
"@simplewebauthn/types": "^10.0.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
Expand Down
4 changes: 4 additions & 0 deletions web/panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { admonition } from "@/recipes/admonition";
import { badge } from "@/recipes/badge";
import { button } from "@/recipes/button";
import { colorPicker } from "@/recipes/color-picker";
import { combobox } from "@/recipes/combobox";
import { fileUpload } from "@/recipes/file-upload";
import { headingInput } from "@/recipes/heading-input";
import { input } from "@/recipes/input";
Expand All @@ -19,6 +20,7 @@ import { popover } from "@/recipes/popover";
import { richCard } from "@/recipes/rich-card";
import { select } from "@/recipes/select";
import { table } from "@/recipes/table";
import { tagsInput } from "@/recipes/tags-input";
import { treeView } from "@/recipes/tree-view";
import { typographyHeading } from "@/recipes/typography-heading";

Expand Down Expand Up @@ -270,10 +272,12 @@ export default defineConfig({
slotRecipes: {
select: select,
colorPicker: colorPicker,
combobox: combobox,
menu: menu,
fileUpload: fileUpload,
popover: popover,
table: table,
tagsInput: tagsInput,
treeView: treeView,
},
semanticTokens,
Expand Down
21 changes: 21 additions & 0 deletions web/src/app/(dashboard)/tags/[tag]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { tagGet } from "@/api/openapi-server/tags";
import { UnreadyBanner } from "@/components/site/Unready";
import { TagScreen } from "@/screens/tags/TagScreen";

type Props = {
params: Promise<{
tag: string;
}>;
};

export default async function Page(props: Props) {
const params = await props.params;
try {
const { tag } = params;

const { data } = await tagGet(tag);
return <TagScreen initialTag={data} slug={tag} />;
} catch (e) {
return <UnreadyBanner error={e} />;
}
}
12 changes: 12 additions & 0 deletions web/src/app/(dashboard)/tags/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { tagList } from "@/api/openapi-server/tags";
import { UnreadyBanner } from "@/components/site/Unready";
import { TagsIndexScreen } from "@/screens/tags/TagsIndexScreen";

export default async function Page() {
try {
const { data } = await tagList();
return <TagsIndexScreen initialTagList={data} />;
} catch (e) {
return <UnreadyBanner error={e} />;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { handle } from "@/api/client";
import { tagList } from "@/api/openapi-client/tags";
import { Node, TagNameList } from "@/api/openapi-schema";
import { TagBadgeList } from "@/components/tag/TagBadgeList";
import { Combotags } from "@/components/ui/combotags";
import { useLibraryMutation } from "@/lib/library/library";

export type Props = {
editing: boolean;
node: Node;
};

export function LibraryPageTagsList(props: Props) {
const { updateNode, revalidate } = useLibraryMutation(props.node);

const currentTags = props.node.tags.map((t) => t.name);

async function handleChange(values: string[]) {
await handle(
async () => {
await updateNode(props.node.slug, { tags: values });
},
{
cleanup: async () => await revalidate(),
},
);
}

async function handleQuery(q: string): Promise<TagNameList> {
const tags =
(await handle(async () => {
const { tags } = await tagList({ q });
return tags.map((t) => t.name);
})) ?? [];

const filtered = tags.filter((t) => !currentTags.includes(t));

return filtered;
}

if (props.editing) {
return (
<>
<Combotags
initialValue={currentTags}
onQuery={handleQuery}
onChange={handleChange}
/>
</>
);
}

if (props.node.tags.length === 0) {
return null;
}

return <TagBadgeList tags={props.node.tags} />;
}
1 change: 0 additions & 1 deletion web/src/components/search/DatagraphSearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export function DatagraphResultItem(props: DatagraphItem) {
</Flex>

<styled.p lineClamp={3}>{props.description}</styled.p>
<styled.p lineClamp={3}>{props.kind}</styled.p>
</FeedItem>
</Box>
);
Expand Down
92 changes: 92 additions & 0 deletions web/src/components/tag/TagBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import chroma from "chroma-js";
import Link from "next/link";

import { TagReference } from "@/api/openapi-schema";
import { css, cx } from "@/styled-system/css";
import { styled } from "@/styled-system/jsx";
import { badge } from "@/styled-system/recipes";

import { BadgeProps } from "../ui/badge";

export type Props = BadgeProps & {
tag: TagReference;
showItemCount?: boolean;
};

// tags are always lowercase, which means most ascenders and descenders are
// slightly mis-aligned to the optical center of the badge.
const OPTICAL_ALIGNMENT_ADJUSTMENT = 1.5;

const badgeStyles = css({
bgColor: "colorPalette",
borderColor: "colorPalette.muted",
color: "colorPalette.text",
});

export function TagBadge({ tag, showItemCount, ...props }: Props) {
const cssVars = badgeColourCSS(tag.colour);

const styles = {
...cssVars,
"--optical-adjustment-top": `${-OPTICAL_ALIGNMENT_ADJUSTMENT}px`,
"--optical-adjustment-bot": `${OPTICAL_ALIGNMENT_ADJUSTMENT}px`,
"--optical-adjustment-count-right": "0.4rem",
};

const titleLabel = `${tag.item_count} items tagged with ${tag.name}`;

return (
<Link
className={cx(
badge({
size: "sm",
...props,
}),
badgeStyles,
)}
style={styles}
title={titleLabel}
href={`/tags/${tag.name}`}
>
{showItemCount && (
<styled.span
borderRightStyle="solid"
borderRightWidth="thin"
borderRightColor="colorPalette.muted"
paddingRight="var(--optical-adjustment-count-right)"
>
{tag.item_count}
</styled.span>
)}

<styled.span
mb="var(--optical-adjustment-bot)"
mt="var(--optical-adjustment-top)"
>
{tag.name}
</styled.span>
</Link>
);
}

function badgeColourCSS(c: string) {
const { bg, bo, fg } = badgeColours(c);

return {
"--colors-color-palette-text": fg,
"--colors-color-palette-muted": bo,
"--colors-color-palette": bg,
} as React.CSSProperties;
}

function badgeColours(c: string) {
const colour = chroma(c);

const hue = colour.lch()[2];

const bg = chroma(0.95, 0.1, hue, "oklch").css();
const bo = chroma(0.85, 0.2, hue, "oklch").css();
const fg = chroma(0.55, 0.2, hue, "oklch").css();

return { bg, bo, fg };
}
19 changes: 19 additions & 0 deletions web/src/components/tag/TagBadgeList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { TagReferenceList } from "@/api/openapi-schema";
import { HStack } from "@/styled-system/jsx";

import { TagBadge } from "./TagBadge";

export type Props = {
tags: TagReferenceList;
showItemCount?: boolean;
};

export function TagBadgeList({ tags, showItemCount }: Props) {
return (
<HStack flexWrap="wrap">
{tags.map((r) => (
<TagBadge key={r.name} tag={r} showItemCount={showItemCount} />
))}
</HStack>
);
}
65 changes: 65 additions & 0 deletions web/src/components/ui/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { Fragment } from "react";

import { LinkButton } from "@/components/ui/link-button";
import { Box, HStack, styled } from "@/styled-system/jsx";

type Props = {
index: Breadcrumb;
crumbs: Breadcrumb[];
};

type Breadcrumb = {
label: string;
href: string;
};

export function Breadcrumbs({ index, crumbs }: Props) {
return (
<HStack
w="full"
color="fg.subtle"
overflowX="scroll"
pt="scrollGutter"
mt="-scrollGutter"
>
<LinkButton
size="xs"
variant="subtle"
flexShrink="0"
minW="min"
href={index.href}
>
{index.label}
</LinkButton>
{crumbs.map((c) => {
return (
<Fragment key={c.href}>
<Box flexShrink="0">
<ChevronRightIcon width="1rem" />
</Box>

<BreadcrumbButton crumb={c} />
</Fragment>
);
})}
</HStack>
);
}

function BreadcrumbButton({ crumb }: { crumb: Breadcrumb }) {
return (
<LinkButton
size="xs"
variant="subtle"
flexShrink="0"
maxW="64"
overflow="hidden"
href={crumb.href}
>
<styled.span overflowX="hidden" textWrap="nowrap" textOverflow="ellipsis">
{crumb.label}
</styled.span>
</LinkButton>
);
}
Loading

0 comments on commit 1702fea

Please sign in to comment.