From 5f3c8c74a5c7af709fb18d36acd313f623733569 Mon Sep 17 00:00:00 2001 From: Barnaby Keene <1636971+Southclaws@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:33:15 +0100 Subject: [PATCH 1/2] add UI for tag listing and tag editing for nodes --- web/package.json | 6 +- web/panda.config.ts | 4 + web/src/app/(dashboard)/tags/[tag]/page.tsx | 21 ++ web/src/app/(dashboard)/tags/page.tsx | 12 + .../LibraryPageTagsList.tsx | 58 ++++ .../search/DatagraphSearchResults.tsx | 1 - web/src/components/tag/TagBadge.tsx | 92 ++++++ web/src/components/tag/TagBadgeList.tsx | 19 ++ web/src/components/ui/Breadcrumbs.tsx | 65 +++++ web/src/components/ui/combobox.tsx | 145 ++++++--- web/src/components/ui/combotags.tsx | 149 ++++++++++ web/src/components/ui/tags-input.tsx | 112 ++++--- web/src/lib/library/library.ts | 36 ++- web/src/recipes/combobox.ts | 136 +++++++++ web/src/recipes/tags-input.ts | 126 ++++++++ .../components/ComposeForm/useComposeForm.ts | 2 +- .../LibraryPageScreen/LibraryPageScreen.tsx | 3 + web/src/screens/tags/TagScreen.tsx | 53 ++++ web/src/screens/tags/TagsIndexScreen.tsx | 44 +++ web/styled-system/recipes/combobox.d.ts | 2 +- web/styled-system/recipes/combobox.mjs | 56 ++++ web/styled-system/recipes/tags-input.d.ts | 4 +- web/styled-system/recipes/tags-input.mjs | 41 +++ web/styled-system/tokens/tokens.d.ts | 2 +- web/yarn.lock | 275 +++++++++--------- 25 files changed, 1239 insertions(+), 225 deletions(-) create mode 100644 web/src/app/(dashboard)/tags/[tag]/page.tsx create mode 100644 web/src/app/(dashboard)/tags/page.tsx create mode 100644 web/src/components/library/LibraryPageTagsList/LibraryPageTagsList.tsx create mode 100644 web/src/components/tag/TagBadge.tsx create mode 100644 web/src/components/tag/TagBadgeList.tsx create mode 100644 web/src/components/ui/Breadcrumbs.tsx create mode 100644 web/src/components/ui/combotags.tsx create mode 100644 web/src/recipes/combobox.ts create mode 100644 web/src/recipes/tags-input.ts create mode 100644 web/src/screens/tags/TagScreen.tsx create mode 100644 web/src/screens/tags/TagsIndexScreen.tsx diff --git a/web/package.json b/web/package.json index b1e0a34d5..53cbea5f2 100644 --- a/web/package.json +++ b/web/package.json @@ -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", @@ -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", diff --git a/web/panda.config.ts b/web/panda.config.ts index 72abb7345..f3555471a 100644 --- a/web/panda.config.ts +++ b/web/panda.config.ts @@ -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"; @@ -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"; @@ -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, diff --git a/web/src/app/(dashboard)/tags/[tag]/page.tsx b/web/src/app/(dashboard)/tags/[tag]/page.tsx new file mode 100644 index 000000000..d50283708 --- /dev/null +++ b/web/src/app/(dashboard)/tags/[tag]/page.tsx @@ -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 ; + } catch (e) { + return ; + } +} diff --git a/web/src/app/(dashboard)/tags/page.tsx b/web/src/app/(dashboard)/tags/page.tsx new file mode 100644 index 000000000..77da2f885 --- /dev/null +++ b/web/src/app/(dashboard)/tags/page.tsx @@ -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 ; + } catch (e) { + return ; + } +} diff --git a/web/src/components/library/LibraryPageTagsList/LibraryPageTagsList.tsx b/web/src/components/library/LibraryPageTagsList/LibraryPageTagsList.tsx new file mode 100644 index 000000000..e3361e94e --- /dev/null +++ b/web/src/components/library/LibraryPageTagsList/LibraryPageTagsList.tsx @@ -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 { + 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 ( + <> + + + ); + } + + if (props.node.tags.length === 0) { + return null; + } + + return ; +} diff --git a/web/src/components/search/DatagraphSearchResults.tsx b/web/src/components/search/DatagraphSearchResults.tsx index 7ac5661bf..cc173b82d 100644 --- a/web/src/components/search/DatagraphSearchResults.tsx +++ b/web/src/components/search/DatagraphSearchResults.tsx @@ -42,7 +42,6 @@ export function DatagraphResultItem(props: DatagraphItem) { {props.description} - {props.kind} ); diff --git a/web/src/components/tag/TagBadge.tsx b/web/src/components/tag/TagBadge.tsx new file mode 100644 index 000000000..4fb2c40fd --- /dev/null +++ b/web/src/components/tag/TagBadge.tsx @@ -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 ( + + {showItemCount && ( + + {tag.item_count} + + )} + + + {tag.name} + + + ); +} + +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 }; +} diff --git a/web/src/components/tag/TagBadgeList.tsx b/web/src/components/tag/TagBadgeList.tsx new file mode 100644 index 000000000..d0282e224 --- /dev/null +++ b/web/src/components/tag/TagBadgeList.tsx @@ -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 ( + + {tags.map((r) => ( + + ))} + + ); +} diff --git a/web/src/components/ui/Breadcrumbs.tsx b/web/src/components/ui/Breadcrumbs.tsx new file mode 100644 index 000000000..66fafe30d --- /dev/null +++ b/web/src/components/ui/Breadcrumbs.tsx @@ -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 ( + + + {index.label} + + {crumbs.map((c) => { + return ( + + + + + + + + ); + })} + + ); +} + +function BreadcrumbButton({ crumb }: { crumb: Breadcrumb }) { + return ( + + + {crumb.label} + + + ); +} diff --git a/web/src/components/ui/combobox.tsx b/web/src/components/ui/combobox.tsx index 274142132..a369cdf6a 100644 --- a/web/src/components/ui/combobox.tsx +++ b/web/src/components/ui/combobox.tsx @@ -1,37 +1,108 @@ -'use client' - -import { Combobox } from '@ark-ui/react/combobox' -import type { ComponentProps } from 'react' -import { styled } from 'styled-system/jsx' -import { combobox } from 'styled-system/recipes' -import { createStyleContext } from '@/utils/create-style-context' - -const { withProvider, withContext } = createStyleContext(combobox) - -export const Root = withProvider(styled(Combobox.Root), 'root') -export const ClearTrigger = withContext(styled(Combobox.ClearTrigger), 'clearTrigger') -export const Content = withContext(styled(Combobox.Content), 'content') -export const Control = withContext(styled(Combobox.Control), 'control') -export const Input = withContext(styled(Combobox.Input), 'input') -export const Item = withContext(styled(Combobox.Item), 'item') -export const ItemGroup = withContext(styled(Combobox.ItemGroup), 'itemGroup') -export const ItemGroupLabel = withContext(styled(Combobox.ItemGroupLabel), 'itemGroupLabel') -export const ItemIndicator = withContext(styled(Combobox.ItemIndicator), 'itemIndicator') -export const ItemText = withContext(styled(Combobox.ItemText), 'itemText') -export const Label = withContext(styled(Combobox.Label), 'label') -export const Positioner = withContext(styled(Combobox.Positioner), 'positioner') -export const Trigger = withContext(styled(Combobox.Trigger), 'trigger') - -export interface RootProps extends ComponentProps {} -export interface ClearTriggerProps extends ComponentProps {} -export interface ContentProps extends ComponentProps {} -export interface ControlProps extends ComponentProps {} -export interface InputProps extends ComponentProps {} -export interface ItemProps extends ComponentProps {} -export interface ItemGroupProps extends ComponentProps {} -export interface ItemGroupLabelProps extends ComponentProps {} -export interface ItemIndicatorProps extends ComponentProps {} -export interface ItemTextProps extends ComponentProps {} -export interface LabelProps extends ComponentProps {} -export interface PositionerProps extends ComponentProps {} -export interface TriggerProps extends ComponentProps {} +"use client"; + +import type { Assign } from "@ark-ui/react"; +import { Combobox } from "@ark-ui/react/combobox"; + +import { type ComboboxVariantProps, combobox } from "@/styled-system/recipes"; +import type { ComponentProps, HTMLStyledProps } from "@/styled-system/types"; +import { createStyleContext } from "@/utils/create-style-context"; + +const { withProvider, withContext } = createStyleContext(combobox); + +export type RootProviderProps = ComponentProps; +export const RootProvider = withProvider< + HTMLDivElement, + Assign< + Assign< + HTMLStyledProps<"div">, + Combobox.RootProviderBaseProps + >, + ComboboxVariantProps + > +>(Combobox.RootProvider, "root"); + +export type RootProps = ComponentProps; +export const Root = withProvider< + HTMLDivElement, + Assign< + Assign< + HTMLStyledProps<"div">, + Combobox.RootBaseProps + >, + ComboboxVariantProps + > +>(Combobox.Root, "root"); + +export const ClearTrigger = withContext< + HTMLButtonElement, + Assign, Combobox.ClearTriggerBaseProps> +>(Combobox.ClearTrigger, "clearTrigger"); + +export const Content = withContext< + HTMLDivElement, + Assign, Combobox.ContentBaseProps> +>(Combobox.Content, "content"); + +export const Control = withContext< + HTMLDivElement, + Assign, Combobox.ControlBaseProps> +>(Combobox.Control, "control"); + +export const Input = withContext< + HTMLInputElement, + Assign, Combobox.InputBaseProps> +>(Combobox.Input, "input"); + +export const ItemGroupLabel = withContext< + HTMLDivElement, + Assign, Combobox.ItemGroupLabelBaseProps> +>(Combobox.ItemGroupLabel, "itemGroupLabel"); + +export const ItemGroup = withContext< + HTMLDivElement, + Assign, Combobox.ItemGroupBaseProps> +>(Combobox.ItemGroup, "itemGroup"); + +export const ItemIndicator = withContext< + HTMLDivElement, + Assign, Combobox.ItemIndicatorBaseProps> +>(Combobox.ItemIndicator, "itemIndicator"); + +export const Item = withContext< + HTMLDivElement, + Assign, Combobox.ItemBaseProps> +>(Combobox.Item, "item"); + +export const ItemText = withContext< + HTMLDivElement, + Assign, Combobox.ItemTextBaseProps> +>(Combobox.ItemText, "itemText"); + +export const Label = withContext< + HTMLLabelElement, + Assign, Combobox.LabelBaseProps> +>(Combobox.Label, "label"); + +export const List = withContext< + HTMLDivElement, + Assign, Combobox.ListBaseProps> +>(Combobox.List, "list"); + +export const Positioner = withContext< + HTMLDivElement, + Assign, Combobox.PositionerBaseProps> +>(Combobox.Positioner, "positioner"); + +export const Trigger = withContext< + HTMLButtonElement, + Assign, Combobox.TriggerBaseProps> +>(Combobox.Trigger, "trigger"); + +export { ComboboxContext as Context } from "@ark-ui/react/combobox"; + +export type { + ComboboxHighlightChangeDetails as HighlightChangeDetails, + ComboboxInputValueChangeDetails as InputValueChangeDetails, + ComboboxOpenChangeDetails as OpenChangeDetails, + ComboboxValueChangeDetails as ValueChangeDetails, +} from "@ark-ui/react/combobox"; diff --git a/web/src/components/ui/combotags.tsx b/web/src/components/ui/combotags.tsx new file mode 100644 index 000000000..618392011 --- /dev/null +++ b/web/src/components/ui/combotags.tsx @@ -0,0 +1,149 @@ +import { createListCollection, useTagsInput } from "@ark-ui/react"; +import { XIcon } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +import * as Combobox from "@/components/ui/combobox"; +import { IconButton } from "@/components/ui/icon-button"; +import * as TagsInput from "@/components/ui/tags-input"; + +export type Props = { + initialValue?: string[]; + onQuery: (query: string) => Promise; + onChange: (values: string[]) => Promise; +}; + +// Combotags provides a mix of a tags input and a combobox where the tags input +// field is used to filter the combobox results. The combobox results are then +// used to add new tags to the tags input. It also allows just hitting enter or +// comma to add a custom tag value to the tags list for creating new tags. +export function Combotags(props: Props) { + const [searchQuery, setSearchQuery] = useState(""); + const [searchResults, setSearchResults] = useState([]); + const [isComboboxOpen, setIsComboboxOpen] = useState(false); + + // NOTE: Because we're combining the combobox with a tags input, we need to + // use the context provider here for easier control of the tags input values. + const tagsInput = useTagsInput({ + defaultValue: props.initialValue, + inputValue: searchQuery, + addOnPaste: true, + onInputValueChange: handleInputValueChange, + onValueChange: handleValueChange, + onInteractOutside: handleInteractOutside, + }); + + // Used by the combobox event handler to update the values of the tags input. + const tagsInputRef = useRef(tagsInput); + useEffect(() => { + tagsInputRef.current = tagsInput; + }, [tagsInput]); + + // Used for positioning the combobox by computing the height of the input. + const inputControlRef = useRef(null); + + async function handleInputValueChange({ inputValue }) { + setSearchQuery(inputValue); + const result = await props.onQuery(inputValue); + + setSearchResults(result); + setIsComboboxOpen(() => true); + } + + async function handleValueChange({ value }) { + // Immediately update the local list, filtering out the newly added values. + setSearchResults(searchResults.filter((item) => !value.includes(item))); + + // NOTE: Not awaited to facilitate optimistic updates. + props.onChange(value); + } + + function handleInteractOutside() { + setIsComboboxOpen(() => false); + } + + const handleSelect = (value: string) => () => { + if (tagsInputRef.current.value.includes(value)) { + // Don't add duplicates. + return; + } + + // This is necessary because `addValue` is broken at the moment. + const newValue = [...tagsInputRef.current.value, value]; + tagsInputRef.current.setValue(newValue); + + setSearchQuery(""); + }; + + const collection = createListCollection({ items: searchResults }); + const rect = inputControlRef.current?.getBoundingClientRect(); + const offset = rect?.height ?? 0; + + return ( + <> + + + {(api) => ( + <> + + {api.value.map((value, index) => ( + + + {value} + + + + + + + + + + ))} + + + + )} + + + + + {searchResults.map((item) => ( + + + {item} + + + ))} + + + + + ); +} diff --git a/web/src/components/ui/tags-input.tsx b/web/src/components/ui/tags-input.tsx index cba47c32b..6a2096473 100644 --- a/web/src/components/ui/tags-input.tsx +++ b/web/src/components/ui/tags-input.tsx @@ -1,34 +1,78 @@ -'use client' - -import { TagsInput } from '@ark-ui/react/tags-input' -import type { ComponentProps } from 'react' -import { styled } from 'styled-system/jsx' -import { tagsInput } from 'styled-system/recipes' -import { createStyleContext } from '@/utils/create-style-context' - -const { withProvider, withContext } = createStyleContext(tagsInput) - -export const Root = withProvider(styled(TagsInput.Root), 'root') -export const ClearTrigger = withContext(styled(TagsInput.ClearTrigger), 'clearTrigger') -export const Control = withContext(styled(TagsInput.Control), 'control') -export const Input = withContext(styled(TagsInput.Input), 'input') -export const Item = withContext(styled(TagsInput.Item), 'item') -export const ItemDeleteTrigger = withContext( - styled(TagsInput.ItemDeleteTrigger), - 'itemDeleteTrigger', -) -export const ItemInput = withContext(styled(TagsInput.ItemInput), 'itemInput') -export const ItemPreview = withContext(styled(TagsInput.ItemPreview), 'itemPreview') -export const ItemText = withContext(styled(TagsInput.ItemText), 'itemText') -export const Label = withContext(styled(TagsInput.Label), 'label') - -export interface RootProps extends ComponentProps {} -export interface ClearTriggerProps extends ComponentProps {} -export interface ControlProps extends ComponentProps {} -export interface InputProps extends ComponentProps {} -export interface ItemProps extends ComponentProps {} -export interface ItemDeleteTriggerProps extends ComponentProps {} -export interface ItemInputProps extends ComponentProps {} -export interface ItemPreviewProps extends ComponentProps {} -export interface ItemTextProps extends ComponentProps {} -export interface LabelProps extends ComponentProps {} +"use client"; + +import type { Assign } from "@ark-ui/react"; +import { TagsInput } from "@ark-ui/react/tags-input"; + +import { type TagsInputVariantProps, tagsInput } from "@/styled-system/recipes"; +import type { ComponentProps, HTMLStyledProps } from "@/styled-system/types"; +import { createStyleContext } from "@/utils/create-style-context"; + +const { withProvider, withContext } = createStyleContext(tagsInput); + +export type RootProviderProps = ComponentProps; +export const RootProvider = withProvider< + HTMLDivElement, + Assign< + Assign, TagsInput.RootProviderBaseProps>, + TagsInputVariantProps + > +>(TagsInput.RootProvider, "root"); + +export type RootProps = ComponentProps; +export const Root = withProvider< + HTMLDivElement, + Assign< + Assign, TagsInput.RootBaseProps>, + TagsInputVariantProps + > +>(TagsInput.Root, "root"); + +export const ClearTrigger = withContext< + HTMLButtonElement, + Assign, TagsInput.ClearTriggerBaseProps> +>(TagsInput.ClearTrigger, "clearTrigger"); + +export const Control = withContext< + HTMLDivElement, + Assign, TagsInput.ControlBaseProps> +>(TagsInput.Control, "control"); + +export const Input = withContext< + HTMLInputElement, + Assign, TagsInput.InputBaseProps> +>(TagsInput.Input, "input"); + +export const ItemDeleteTrigger = withContext< + HTMLButtonElement, + Assign, TagsInput.ItemDeleteTriggerBaseProps> +>(TagsInput.ItemDeleteTrigger, "itemDeleteTrigger"); + +export const ItemInput = withContext< + HTMLInputElement, + Assign, TagsInput.ItemInputBaseProps> +>(TagsInput.ItemInput, "itemInput"); + +export const ItemPreview = withContext< + HTMLDivElement, + Assign, TagsInput.ItemPreviewBaseProps> +>(TagsInput.ItemPreview, "itemPreview"); + +export const Item = withContext< + HTMLDivElement, + Assign, TagsInput.ItemBaseProps> +>(TagsInput.Item, "item"); + +export const ItemText = withContext< + HTMLSpanElement, + Assign, TagsInput.ItemTextBaseProps> +>(TagsInput.ItemText, "itemText"); + +export const Label = withContext< + HTMLLabelElement, + Assign, TagsInput.LabelBaseProps> +>(TagsInput.Label, "label"); + +export { + TagsInputContext as Context, + TagsInputHiddenInput as HiddenInput, +} from "@ark-ui/react/tags-input"; diff --git a/web/src/lib/library/library.ts b/web/src/lib/library/library.ts index ec44d9594..cc4f3965e 100644 --- a/web/src/lib/library/library.ts +++ b/web/src/lib/library/library.ts @@ -25,6 +25,7 @@ import { NodeListParams, NodeMutableProps, NodeWithChildren, + TagReference, Visibility, } from "@/api/openapi-schema"; import { useSession } from "@/auth"; @@ -64,14 +65,15 @@ export type CoverImageArgs = isReplacement: true; }; -export function useLibraryMutation(params?: NodeListParams) { +// TODO: Remove slug params from API calls and use the node object instead. +export function useLibraryMutation(node?: Node) { const session = useSession(); const { mutate } = useSWRConfig(); const router = useRouter(); const libraryPath = useLibraryPath(); // for revalidating all node list queries (published and private) - const nodeListKey = getNodeListKey(params); + const nodeListKey = getNodeListKey(); const nodeListAllKeyFn = (key: Arguments) => { return ( Array.isArray(key) && @@ -90,6 +92,14 @@ export function useLibraryMutation(params?: NodeListParams) { return dequal(key, nodeListPrivateKey); }; + // For revalidating one specific node. + const nodeKey = node && getNodeGetKey(node.slug); + const nodeKeyFn = + node && + ((key: Arguments) => { + return Array.isArray(key) && key[0].startsWith(nodeKey); + }); + const createNode = async ({ initialName, parentSlug }: CreateNodeArgs) => { if (!session) return; @@ -115,6 +125,7 @@ export function useLibraryMutation(params?: NodeListParams) { meta: {}, children: [], assets: [], + tags: [], visibility: "draft", recomentations: [], }; @@ -148,10 +159,23 @@ export function useLibraryMutation(params?: NodeListParams) { const withNewCover = cover?.asset && mergePrimaryImageAsset(data, cover); + const withTags = { + tags: + newNode.tags?.map( + (t) => + ({ + name: t, + colour: "white", + item_count: 1, + }) satisfies TagReference, + ) ?? [], + }; + const updated = { ...data, ...nodeProps, ...withNewCover, + ...withTags, } satisfies NodeWithChildren; return updated; @@ -175,11 +199,6 @@ export function useLibraryMutation(params?: NodeListParams) { }; }; - const nodeKey = getNodeGetKey(slug); - const nodeKeyFn = (key: Arguments) => { - return Array.isArray(key) && key[0].startsWith(nodeKey); - }; - const slugChanged = newNode.slug !== undefined && newNode.slug !== slug; await mutate(nodeListAllKeyFn, listMutator, { revalidate: false }); @@ -346,6 +365,9 @@ export function useLibraryMutation(params?: NodeListParams) { const revalidate = async (data?: MutatorCallback) => { await mutate(nodeListAllKeyFn, data); + if (node) { + await mutate(nodeKeyFn); + } }; return { diff --git a/web/src/recipes/combobox.ts b/web/src/recipes/combobox.ts new file mode 100644 index 000000000..484cc414b --- /dev/null +++ b/web/src/recipes/combobox.ts @@ -0,0 +1,136 @@ +import { comboboxAnatomy } from "@ark-ui/anatomy"; +import { defineSlotRecipe } from "@pandacss/dev"; + +export const combobox = defineSlotRecipe({ + className: "combobox", + slots: comboboxAnatomy.keys(), + base: { + root: { + display: "flex", + flexDirection: "column", + gap: "1.5", + width: "full", + }, + control: { + position: "relative", + }, + label: { + color: "fg.default", + fontWeight: "medium", + }, + trigger: { + bottom: "0", + color: "fg.muted", + position: "absolute", + right: "0", + top: "0", + }, + content: { + background: "bg.default", + borderRadius: "l2", + boxShadow: "lg", + display: "flex", + flexDirection: "column", + zIndex: "dropdown", + _hidden: { + display: "none", + }, + _open: { + animation: "fadeIn 0.25s ease-out", + }, + _closed: { + animation: "fadeOut 0.2s ease-out", + }, + _focusVisible: { + outlineOffset: "2px", + outline: "2px solid", + outlineColor: "border.outline", + }, + }, + item: { + alignItems: "center", + borderRadius: "l1", + cursor: "pointer", + display: "flex", + justifyContent: "space-between", + transitionDuration: "fast", + transitionProperty: "background, color", + transitionTimingFunction: "default", + _hover: { + background: "bg.muted", + }, + _highlighted: { + background: "bg.muted", + }, + _disabled: { + color: "fg.disabled", + cursor: "not-allowed", + _hover: { + background: "transparent", + }, + }, + }, + itemGroupLabel: { + fontWeight: "semibold", + textStyle: "sm", + }, + itemIndicator: { + color: "colorPalette.default", + }, + }, + defaultVariants: { + size: "md", + }, + variants: { + size: { + sm: { + content: { p: "0.5", gap: "1" }, + item: { textStyle: "sm", px: "2", height: "9" }, + itemIndicator: { + "& :where(svg)": { + width: "4", + height: "4", + }, + }, + itemGroupLabel: { + px: "2", + py: "1.5", + }, + label: { textStyle: "sm" }, + trigger: { right: "2.5" }, + }, + md: { + content: { p: "1", gap: "1" }, + item: { textStyle: "md", px: "2", height: "10" }, + itemIndicator: { + "& :where(svg)": { + width: "4", + height: "4", + }, + }, + itemGroupLabel: { + px: "2", + py: "1.5", + }, + label: { textStyle: "sm" }, + trigger: { right: "3" }, + }, + lg: { + content: { p: "1.5", gap: "1" }, + item: { textStyle: "md", px: "2", height: "11" }, + itemIndicator: { + "& :where(svg)": { + width: "5", + height: "5", + }, + }, + itemGroupLabel: { + px: "2", + py: "1.5", + }, + label: { textStyle: "sm" }, + trigger: { right: "3.5" }, + }, + }, + }, +}); diff --git a/web/src/recipes/tags-input.ts b/web/src/recipes/tags-input.ts new file mode 100644 index 000000000..d41edf04b --- /dev/null +++ b/web/src/recipes/tags-input.ts @@ -0,0 +1,126 @@ +import { tagsInputAnatomy } from "@ark-ui/anatomy"; +import { defineSlotRecipe } from "@pandacss/dev"; + +export const tagsInput = defineSlotRecipe({ + className: "tagsInput", + slots: tagsInputAnatomy.keys(), + jsx: ["TagsInput", "Combotags"], + staticCss: [{ size: ["sm", "md"] }], + base: { + root: { + colorPalette: "accent", + display: "flex", + flexDirection: "column", + gap: "1.5", + width: "full", + minWidth: "0", + }, + control: { + alignItems: "center", + borderColor: "border.default", + borderRadius: "l2", + borderWidth: "1px", + display: "flex", + flexWrap: "wrap", + outline: 0, + minWidth: "0", + transitionDuration: "normal", + transitionProperty: "border-color, box-shadow", + transitionTimingFunction: "default", + _focusWithin: { + borderColor: "colorPalette.default", + boxShadow: "0 0 0 1px var(--colors-color-palette-default)", + }, + }, + input: { + background: "transparent", + color: "fg.default", + outline: "none", + _placeholder: { + opacity: "full", + color: "fg.subtle", + }, + }, + item: { + overflow: "hidden", + }, + itemPreview: { + alignItems: "center", + borderColor: "border.default", + borderRadius: "l1", + borderWidth: "1px", + color: "fg.default", + display: "inline-flex", + fontWeight: "medium", + minWidth: "0", + width: "full", + _highlighted: { + borderColor: "colorPalette.default", + boxShadow: "0 0 0 1px var(--colors-color-palette-default)", + }, + _hidden: { + display: "none", + }, + }, + itemText: { + textOverflow: "ellipsis", + textWrap: "nowrap", + overflow: "hidden", + }, + itemInput: { + background: "transparent", + color: "fg.default", + outline: "none", + }, + label: { + color: "fg.default", + fontWeight: "medium", + textStyle: "sm", + }, + }, + defaultVariants: { + size: "md", + }, + variants: { + size: { + sm: { + root: { + gap: "1.5", + }, + control: { + fontSize: "sm", + gap: "1.5", + minW: "10", + px: "2", + py: "2", + }, + itemPreview: { + gap: "1", + h: "8", + pe: "1", + ps: "2", + textStyle: "sm", + }, + }, + md: { + root: { + gap: "1.5", + }, + control: { + fontSize: "md", + gap: "1.5", + minW: "10", + px: "3", + py: "7px", // TODO line break + }, + itemPreview: { + gap: "1", + h: "6", + pe: "1", + ps: "2", + textStyle: "sm", + }, + }, + }, + }, +}); diff --git a/web/src/screens/compose/components/ComposeForm/useComposeForm.ts b/web/src/screens/compose/components/ComposeForm/useComposeForm.ts index 54f9bdbc6..9d3b580ff 100644 --- a/web/src/screens/compose/components/ComposeForm/useComposeForm.ts +++ b/web/src/screens/compose/components/ComposeForm/useComposeForm.ts @@ -31,7 +31,7 @@ export function useComposeForm({ initialDraft, editing }: Props) { ? { title: initialDraft.title, body: initialDraft.body, - tags: initialDraft.tags, + tags: initialDraft.tags.map((t) => t.name), url: initialDraft.link?.url, } : { diff --git a/web/src/screens/library/LibraryPageScreen/LibraryPageScreen.tsx b/web/src/screens/library/LibraryPageScreen/LibraryPageScreen.tsx index ac3953e9a..254d9768e 100644 --- a/web/src/screens/library/LibraryPageScreen/LibraryPageScreen.tsx +++ b/web/src/screens/library/LibraryPageScreen/LibraryPageScreen.tsx @@ -14,6 +14,7 @@ import { LibraryPageAssetList } from "@/components/library/LibraryPageAssetList/ import { LibraryPageCoverImageControl } from "@/components/library/LibraryPageCoverImageControl/LibraryPageCoverImageControl"; import { LibraryPageImportFromURL } from "@/components/library/LibraryPageImportFromURL/LibraryPageImportFromURL"; import { LibraryPageMenu } from "@/components/library/LibraryPageMenu/LibraryPageMenu"; +import { LibraryPageTagsList } from "@/components/library/LibraryPageTagsList/LibraryPageTagsList"; import { UnreadyBanner } from "@/components/site/Unready"; import { Heading } from "@/components/ui/heading"; import { LinkButton } from "@/components/ui/link-button"; @@ -185,6 +186,8 @@ export function LibraryPage(props: Props) { + + {editing ? ( diff --git a/web/src/screens/tags/TagScreen.tsx b/web/src/screens/tags/TagScreen.tsx new file mode 100644 index 000000000..2eb76a480 --- /dev/null +++ b/web/src/screens/tags/TagScreen.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useTagGet } from "@/api/openapi-client/tags"; +import { Tag, TagName } from "@/api/openapi-schema"; +import { DatagraphResultItem } from "@/components/search/DatagraphSearchResults"; +import { Unready } from "@/components/site/Unready"; +import { TagBadge } from "@/components/tag/TagBadge"; +import { Breadcrumbs } from "@/components/ui/Breadcrumbs"; +import { HStack, LStack } from "@/styled-system/jsx"; + +type Props = { + slug: TagName; + initialTag: Tag; +}; + +export function TagScreen(props: Props) { + const { data, error } = useTagGet(props.slug, { + swr: { fallbackData: props.initialTag }, + }); + if (!data) { + return ; + } + + const tag = data; + + return ( + + + + + +

Threads and library pages tagged with

+ +
+
+ + {tag.items.map((item) => ( + + ))} +
+ ); +} diff --git a/web/src/screens/tags/TagsIndexScreen.tsx b/web/src/screens/tags/TagsIndexScreen.tsx new file mode 100644 index 000000000..628870a3d --- /dev/null +++ b/web/src/screens/tags/TagsIndexScreen.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { useTagList } from "@/api/openapi-client/tags"; +import { TagListResult } from "@/api/openapi-schema"; +import { Unready } from "@/components/site/Unready"; +import { TagBadgeList } from "@/components/tag/TagBadgeList"; +import { Breadcrumbs } from "@/components/ui/Breadcrumbs"; +import { Heading } from "@/components/ui/heading"; +import { Text } from "@/components/ui/text"; +import { LStack } from "@/styled-system/jsx"; + +type Props = { + initialTagList: TagListResult; +}; + +export function TagsIndexScreen(props: Props) { + const { data, error } = useTagList( + {}, + { swr: { fallbackData: props.initialTagList } }, + ); + if (!data) { + return ; + } + + return ( + + + + + + Threads and library pages can be tagged with related topics. + + + + + + ); +} diff --git a/web/styled-system/recipes/combobox.d.ts b/web/styled-system/recipes/combobox.d.ts index 31786a078..df66b55f5 100644 --- a/web/styled-system/recipes/combobox.d.ts +++ b/web/styled-system/recipes/combobox.d.ts @@ -19,7 +19,7 @@ export type ComboboxVariantProps = { export interface ComboboxRecipe { __type: ComboboxVariantProps - (props?: ComboboxVariantProps): Pretty> + (props?: ComboboxVariantProps): Pretty> raw: (props?: ComboboxVariantProps) => ComboboxVariantProps variantMap: ComboboxVariantMap variantKeys: Array diff --git a/web/styled-system/recipes/combobox.mjs b/web/styled-system/recipes/combobox.mjs index 1db6cc10d..5f11a3eb3 100644 --- a/web/styled-system/recipes/combobox.mjs +++ b/web/styled-system/recipes/combobox.mjs @@ -115,6 +115,62 @@ const comboboxSlotNames = [ "positioner", "combobox__positioner" ], + [ + "trigger", + "combobox__trigger" + ], + [ + "root", + "combobox__root" + ], + [ + "clearTrigger", + "combobox__clearTrigger" + ], + [ + "content", + "combobox__content" + ], + [ + "control", + "combobox__control" + ], + [ + "input", + "combobox__input" + ], + [ + "item", + "combobox__item" + ], + [ + "itemGroup", + "combobox__itemGroup" + ], + [ + "itemGroupLabel", + "combobox__itemGroupLabel" + ], + [ + "itemIndicator", + "combobox__itemIndicator" + ], + [ + "itemText", + "combobox__itemText" + ], + [ + "label", + "combobox__label" + ], + [ + "list", + "combobox__list" + ], + [ + "positioner", + "combobox__positioner" + ], [ "trigger", "combobox__trigger" diff --git a/web/styled-system/recipes/tags-input.d.ts b/web/styled-system/recipes/tags-input.d.ts index 3ef522cec..3f0384c93 100644 --- a/web/styled-system/recipes/tags-input.d.ts +++ b/web/styled-system/recipes/tags-input.d.ts @@ -6,7 +6,7 @@ interface TagsInputVariant { /** * @default "md" */ -size: "md" +size: "sm" | "md" } type TagsInputVariantMap = { @@ -19,7 +19,7 @@ export type TagsInputVariantProps = { export interface TagsInputRecipe { __type: TagsInputVariantProps - (props?: TagsInputVariantProps): Pretty> + (props?: TagsInputVariantProps): Pretty> raw: (props?: TagsInputVariantProps) => TagsInputVariantProps variantMap: TagsInputVariantMap variantKeys: Array diff --git a/web/styled-system/recipes/tags-input.mjs b/web/styled-system/recipes/tags-input.mjs index b75478f6d..addb7eba2 100644 --- a/web/styled-system/recipes/tags-input.mjs +++ b/web/styled-system/recipes/tags-input.mjs @@ -83,6 +83,46 @@ const tagsInputSlotNames = [ "itemText", "tagsInput__itemText" ], + [ + "itemDeleteTrigger", + "tagsInput__itemDeleteTrigger" + ], + [ + "root", + "tagsInput__root" + ], + [ + "label", + "tagsInput__label" + ], + [ + "control", + "tagsInput__control" + ], + [ + "input", + "tagsInput__input" + ], + [ + "clearTrigger", + "tagsInput__clearTrigger" + ], + [ + "item", + "tagsInput__item" + ], + [ + "itemPreview", + "tagsInput__itemPreview" + ], + [ + "itemInput", + "tagsInput__itemInput" + ], + [ + "itemText", + "tagsInput__itemText" + ], [ "itemDeleteTrigger", "tagsInput__itemDeleteTrigger" @@ -106,6 +146,7 @@ export const tagsInput = /* @__PURE__ */ Object.assign(tagsInputFn, { variantKeys: tagsInputVariantKeys, variantMap: { "size": [ + "sm", "md" ] }, diff --git a/web/styled-system/tokens/tokens.d.ts b/web/styled-system/tokens/tokens.d.ts index a74282422..b24dd6398 100644 --- a/web/styled-system/tokens/tokens.d.ts +++ b/web/styled-system/tokens/tokens.d.ts @@ -63,4 +63,4 @@ export type Tokens = { shadows: ShadowToken } & { [token: string]: never } -export type TokenCategory = "aspectRatios" | "zIndex" | "opacity" | "colors" | "fonts" | "fontSizes" | "fontWeights" | "lineHeights" | "letterSpacings" | "sizes" | "shadows" | "spacing" | "radii" | "borders" | "borderWidths" | "durations" | "easings" | "animations" | "blurs" | "gradients" | "breakpoints" | "assets" \ No newline at end of file +export type TokenCategory = "aspectRatios" | "zIndex" | "opacity" | "colors" | "fonts" | "fontSizes" | "fontWeights" | "lineHeights" | "letterSpacings" | "sizes" | "cursor" | "shadows" | "spacing" | "radii" | "borders" | "borderWidths" | "durations" | "easings" | "animations" | "blurs" | "gradients" | "breakpoints" | "assets" \ No newline at end of file diff --git a/web/yarn.lock b/web/yarn.lock index 941037354..cf48111d9 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1503,34 +1503,34 @@ __metadata: languageName: node linkType: hard -"@pandacss/config@npm:0.46.1, @pandacss/config@npm:^0.46.1": - version: 0.46.1 - resolution: "@pandacss/config@npm:0.46.1" - dependencies: - "@pandacss/logger": "npm:0.46.1" - "@pandacss/preset-base": "npm:0.46.1" - "@pandacss/preset-panda": "npm:0.46.1" - "@pandacss/shared": "npm:0.46.1" - "@pandacss/types": "npm:0.46.1" +"@pandacss/config@npm:0.47.0, @pandacss/config@npm:^0.47.0": + version: 0.47.0 + resolution: "@pandacss/config@npm:0.47.0" + dependencies: + "@pandacss/logger": "npm:0.47.0" + "@pandacss/preset-base": "npm:0.47.0" + "@pandacss/preset-panda": "npm:0.47.0" + "@pandacss/shared": "npm:0.47.0" + "@pandacss/types": "npm:0.47.0" bundle-n-require: "npm:1.1.1" escalade: "npm:3.1.2" merge-anything: "npm:5.1.7" microdiff: "npm:1.3.2" typescript: "npm:5.3.3" - checksum: 10c0/4a92dc738e3f977a6309aa2eec599ed12e2caae664cf09922f387a439d20a33645a7478821e6f33da1cadd704da1bdd70a5fadb5c55f903236ff36ce140fa797 + checksum: 10c0/d0dd7d2429e54f8ac08bd95d143e0e13c1329af08d0efc70cf41daf4d11dfb23a664068534418d5b7d3fe61648526254ca046b9a21e4f07878fbcc2dc635eefe languageName: node linkType: hard -"@pandacss/core@npm:0.46.1, @pandacss/core@npm:^0.46.1": - version: 0.46.1 - resolution: "@pandacss/core@npm:0.46.1" +"@pandacss/core@npm:0.47.0, @pandacss/core@npm:^0.47.0": + version: 0.47.0 + resolution: "@pandacss/core@npm:0.47.0" dependencies: "@csstools/postcss-cascade-layers": "npm:4.0.6" - "@pandacss/is-valid-prop": "npm:^0.46.1" - "@pandacss/logger": "npm:0.46.1" - "@pandacss/shared": "npm:0.46.1" - "@pandacss/token-dictionary": "npm:0.46.1" - "@pandacss/types": "npm:0.46.1" + "@pandacss/is-valid-prop": "npm:^0.47.0" + "@pandacss/logger": "npm:0.47.0" + "@pandacss/shared": "npm:0.47.0" + "@pandacss/token-dictionary": "npm:0.47.0" + "@pandacss/types": "npm:0.47.0" browserslist: "npm:4.23.3" hookable: "npm:5.5.3" lightningcss: "npm:1.25.1" @@ -1539,97 +1539,97 @@ __metadata: postcss: "npm:8.4.47" postcss-discard-duplicates: "npm:7.0.1" postcss-discard-empty: "npm:7.0.0" - postcss-merge-rules: "npm:7.0.2" - postcss-minify-selectors: "npm:7.0.2" + postcss-merge-rules: "npm:7.0.4" + postcss-minify-selectors: "npm:7.0.4" postcss-nested: "npm:6.0.1" postcss-normalize-whitespace: "npm:7.0.0" - postcss-selector-parser: "npm:6.1.1" + postcss-selector-parser: "npm:6.1.2" ts-pattern: "npm:5.0.8" - checksum: 10c0/476f80ca59a2c7938c41f10e84b23b2e25328ae9d4071d289a9feeafdd5069625d03013a05122831c3c2c0ce0d974805fe1e1dd9dc0bee2c8ad87791ac07f2ce + checksum: 10c0/84d37d17e0ebac822977669f433a3628530f45a98b34216d0127cf7026b7382a5c563e813e267287a732b6c3653c0d4d370fd5f2c97051cf6185cabf16f0a62a languageName: node linkType: hard -"@pandacss/dev@npm:^0.46.1": - version: 0.46.1 - resolution: "@pandacss/dev@npm:0.46.1" +"@pandacss/dev@npm:^0.47.0": + version: 0.47.0 + resolution: "@pandacss/dev@npm:0.47.0" dependencies: "@clack/prompts": "npm:0.7.0" - "@pandacss/config": "npm:0.46.1" - "@pandacss/logger": "npm:0.46.1" - "@pandacss/node": "npm:0.46.1" - "@pandacss/postcss": "npm:0.46.1" - "@pandacss/preset-panda": "npm:0.46.1" - "@pandacss/shared": "npm:0.46.1" - "@pandacss/token-dictionary": "npm:0.46.1" - "@pandacss/types": "npm:0.46.1" + "@pandacss/config": "npm:0.47.0" + "@pandacss/logger": "npm:0.47.0" + "@pandacss/node": "npm:0.47.0" + "@pandacss/postcss": "npm:0.47.0" + "@pandacss/preset-panda": "npm:0.47.0" + "@pandacss/shared": "npm:0.47.0" + "@pandacss/token-dictionary": "npm:0.47.0" + "@pandacss/types": "npm:0.47.0" cac: "npm:6.7.14" bin: panda: bin.js pandacss: bin.js - checksum: 10c0/5a4516c094dd268f0113cf2f5292fcbd58712b2c2c81fa8c914fe9f271ceb0742879190bb94a51579f891ff6cd414783344106a3c16864aa33cc86cfb7cd7b60 + checksum: 10c0/e3c6394d8f04ddbaf8265167a0adb68784dd976fd69a6252a21b47d984318d3afcada9a95fcc11b3302b4a75676f47bef05ef27db9a3473d844fae55aae6fbb1 languageName: node linkType: hard -"@pandacss/extractor@npm:0.46.1": - version: 0.46.1 - resolution: "@pandacss/extractor@npm:0.46.1" +"@pandacss/extractor@npm:0.47.0": + version: 0.47.0 + resolution: "@pandacss/extractor@npm:0.47.0" dependencies: - "@pandacss/shared": "npm:0.46.1" + "@pandacss/shared": "npm:0.47.0" ts-evaluator: "npm:1.2.0" ts-morph: "npm:21.0.1" - checksum: 10c0/63946e2d9c441a04d80ff9c06e55b69fc21670b92f7ad5c9cbdea2432819da5df18b13721417cb8fdec3504987382fea87038758b38fb8170fb24bd93fa65acc + checksum: 10c0/f2ea1dfcf1a1a78bcbf5704ec77f79b66e4c2204581fcaa82163823414873c4a8ade661d60bd51b7638cd259e7310b6f2c06f1fd0163604f6a0b156305c67c8b languageName: node linkType: hard -"@pandacss/generator@npm:0.46.1": - version: 0.46.1 - resolution: "@pandacss/generator@npm:0.46.1" +"@pandacss/generator@npm:0.47.0": + version: 0.47.0 + resolution: "@pandacss/generator@npm:0.47.0" dependencies: - "@pandacss/core": "npm:0.46.1" - "@pandacss/is-valid-prop": "npm:^0.46.1" - "@pandacss/logger": "npm:0.46.1" - "@pandacss/shared": "npm:0.46.1" - "@pandacss/token-dictionary": "npm:0.46.1" - "@pandacss/types": "npm:0.46.1" + "@pandacss/core": "npm:0.47.0" + "@pandacss/is-valid-prop": "npm:^0.47.0" + "@pandacss/logger": "npm:0.47.0" + "@pandacss/shared": "npm:0.47.0" + "@pandacss/token-dictionary": "npm:0.47.0" + "@pandacss/types": "npm:0.47.0" javascript-stringify: "npm:2.1.0" outdent: "npm: ^0.8.0" pluralize: "npm:8.0.0" postcss: "npm:8.4.47" ts-pattern: "npm:5.0.8" - checksum: 10c0/a7d6e69ee9fffb1404136a8db10e7e83cc4d5a03addfd8ed8e9c18e211b8848b52b39b97351b21d086861e0c3db4bfa30a6d75ab2d947b29c594dd8ac9315fa7 + checksum: 10c0/b123e2a4cff72bfe2358aeb46daecc580f52eed7412c38c7c7ba3643362772bc335c3f3730122e05f317ad11fbb93ebb1b7322dcaad03806ceebe34d915d03fe languageName: node linkType: hard -"@pandacss/is-valid-prop@npm:^0.46.1": - version: 0.46.1 - resolution: "@pandacss/is-valid-prop@npm:0.46.1" - checksum: 10c0/b920ce153793925ed57b83dfe1c70f7d4d27bcc0ce47dbfceed7154d3a33ec6a39c5e51797e4b8cb97495b7d4a6b85e6dfe5441647be7a6e33d3f87368f6095d +"@pandacss/is-valid-prop@npm:^0.47.0": + version: 0.47.0 + resolution: "@pandacss/is-valid-prop@npm:0.47.0" + checksum: 10c0/8b6310588b6ab5b02bda80c687e603eba6d9ee0987b2c319b86ed172fb592fa0c5b952357237362d6be5857a3edbf6a02111aafe3a1b9adf4d7b979744f7ce7f languageName: node linkType: hard -"@pandacss/logger@npm:0.46.1, @pandacss/logger@npm:^0.46.1": - version: 0.46.1 - resolution: "@pandacss/logger@npm:0.46.1" +"@pandacss/logger@npm:0.47.0, @pandacss/logger@npm:^0.47.0": + version: 0.47.0 + resolution: "@pandacss/logger@npm:0.47.0" dependencies: - "@pandacss/types": "npm:0.46.1" + "@pandacss/types": "npm:0.47.0" kleur: "npm:4.1.5" - checksum: 10c0/93642251de28070dcbb2e4c99f635e312ad48f3bcc3f203c4b39680f5b0ded6966e8498b0722f5b187f505f12b36a80cd53116ce6f4aae6955842c1e053ee48c + checksum: 10c0/6c0f74c183ed56f4dfdd78337907271e27f0c25566d27c911047b6981847a6831cdedbf6185853cd3864cf4fc0022a9dd85032d47e45e619ea15fa4a00e434bc languageName: node linkType: hard -"@pandacss/node@npm:0.46.1": - version: 0.46.1 - resolution: "@pandacss/node@npm:0.46.1" +"@pandacss/node@npm:0.47.0": + version: 0.47.0 + resolution: "@pandacss/node@npm:0.47.0" dependencies: - "@pandacss/config": "npm:0.46.1" - "@pandacss/core": "npm:0.46.1" - "@pandacss/extractor": "npm:0.46.1" - "@pandacss/generator": "npm:0.46.1" - "@pandacss/logger": "npm:0.46.1" - "@pandacss/parser": "npm:0.46.1" - "@pandacss/shared": "npm:0.46.1" - "@pandacss/token-dictionary": "npm:0.46.1" - "@pandacss/types": "npm:0.46.1" + "@pandacss/config": "npm:0.47.0" + "@pandacss/core": "npm:0.47.0" + "@pandacss/extractor": "npm:0.47.0" + "@pandacss/generator": "npm:0.47.0" + "@pandacss/logger": "npm:0.47.0" + "@pandacss/parser": "npm:0.47.0" + "@pandacss/shared": "npm:0.47.0" + "@pandacss/token-dictionary": "npm:0.47.0" + "@pandacss/types": "npm:0.47.0" browserslist: "npm:4.23.3" chokidar: "npm:3.6.0" fast-glob: "npm:3.3.2" @@ -1650,79 +1650,79 @@ __metadata: ts-morph: "npm:21.0.1" ts-pattern: "npm:5.0.8" tsconfck: "npm:3.0.2" - checksum: 10c0/1c7b2fb28e9b121880b36e64e824615a16d253eaf9304ca79764dea87fd421bd680e8cace1cc1103e440c20e8cdbf3b5affecbb95a29bb192a3dd51f9d71785e + checksum: 10c0/f787a72040d5b3e214fd24f992b42cb20f78057fe7d8f90693c7d88d44e9e5010acec4b90f3262d62926e1a6a16ce90347c597f5e3b557f274a11b1f15300e0e languageName: node linkType: hard -"@pandacss/parser@npm:0.46.1": - version: 0.46.1 - resolution: "@pandacss/parser@npm:0.46.1" +"@pandacss/parser@npm:0.47.0": + version: 0.47.0 + resolution: "@pandacss/parser@npm:0.47.0" dependencies: - "@pandacss/config": "npm:^0.46.1" - "@pandacss/core": "npm:^0.46.1" - "@pandacss/extractor": "npm:0.46.1" - "@pandacss/logger": "npm:0.46.1" - "@pandacss/shared": "npm:0.46.1" - "@pandacss/types": "npm:0.46.1" + "@pandacss/config": "npm:^0.47.0" + "@pandacss/core": "npm:^0.47.0" + "@pandacss/extractor": "npm:0.47.0" + "@pandacss/logger": "npm:0.47.0" + "@pandacss/shared": "npm:0.47.0" + "@pandacss/types": "npm:0.47.0" "@vue/compiler-sfc": "npm:3.4.19" - magic-string: "npm:0.30.11" + magic-string: "npm:0.30.12" ts-morph: "npm:21.0.1" ts-pattern: "npm:5.0.8" - checksum: 10c0/bc109d10e956349295b02ad6002f430f42074594f3590833ecf72d450b86b28960a665864ee31345391e56a179c500f243379d31f7f819abbd7fc3ae3b6e5090 + checksum: 10c0/998119c383296bbd6c208e7c61cf2e206625a09467328deb4936bf6a8fc1f546e01f052eb193f443c914d4f965e33b06364ab112d2b562008ed008db3f79a754 languageName: node linkType: hard -"@pandacss/postcss@npm:0.46.1": - version: 0.46.1 - resolution: "@pandacss/postcss@npm:0.46.1" +"@pandacss/postcss@npm:0.47.0": + version: 0.47.0 + resolution: "@pandacss/postcss@npm:0.47.0" dependencies: - "@pandacss/node": "npm:0.46.1" + "@pandacss/node": "npm:0.47.0" postcss: "npm:8.4.47" - checksum: 10c0/6a7901cb92aac2486324878358df03b703b5732dbddc3ca41b275ac9edbc97fc2ab0ad5945f7d46e2c3db43803b7a3a4e8c674ba863c83d672e6f53b08ac54e2 + checksum: 10c0/85947aefd6c496102176ce9d8103dd31c3c8ad8b58a8827dbe97fe7280a90ac9b413d9672b770ab07dc20b392b8e0133e4e318521a82261b96f5f4a8e501b862 languageName: node linkType: hard -"@pandacss/preset-base@npm:0.46.1": - version: 0.46.1 - resolution: "@pandacss/preset-base@npm:0.46.1" +"@pandacss/preset-base@npm:0.47.0": + version: 0.47.0 + resolution: "@pandacss/preset-base@npm:0.47.0" dependencies: - "@pandacss/types": "npm:0.46.1" - checksum: 10c0/b882c649e4beab52208f56e9f3adbe7d6f377d6cc3c1617d1352ef3104cdcab9294751509749aa22c20109596c369e0cf67d261eb287ef749f97845934a9888a + "@pandacss/types": "npm:0.47.0" + checksum: 10c0/ed00d815954dbe94f22ab3624ef6872610c7856992cfb1bed322d6f74c3b22c13b7cdbeee661c3df9a0d7723b0122ac52a60cb583ca80dcc77e34493a51ba570 languageName: node linkType: hard -"@pandacss/preset-panda@npm:0.46.1": - version: 0.46.1 - resolution: "@pandacss/preset-panda@npm:0.46.1" +"@pandacss/preset-panda@npm:0.47.0": + version: 0.47.0 + resolution: "@pandacss/preset-panda@npm:0.47.0" dependencies: - "@pandacss/types": "npm:0.46.1" - checksum: 10c0/9bb1aa8bf79e59935dfa6a336ab17c0609feee9c09b8aceee851199c7ca7f03695d5be610c581fabffb82b9a81e4e51ef62c8f3125b328ea16950199c4d2777c + "@pandacss/types": "npm:0.47.0" + checksum: 10c0/d2b8cc19fc1e0506147f5083788e56c265d1d9cb034cdf9f0ee7f3cc74144c1867566adf4dcacd06256d4e258b3d7beefeccc71919212b5cb478b0e1d0a8131a languageName: node linkType: hard -"@pandacss/shared@npm:0.46.1": - version: 0.46.1 - resolution: "@pandacss/shared@npm:0.46.1" - checksum: 10c0/9780ecaf158a5f74a793df7b3261122e784e271cd0be37e205c652f0b6fdd8aaece3388ed574d8975ccb900fae70e8c1c836875d4ca08cb38d94539da17c2eb9 +"@pandacss/shared@npm:0.47.0": + version: 0.47.0 + resolution: "@pandacss/shared@npm:0.47.0" + checksum: 10c0/627bf52e256237205c0493fa3408efd61a43128011ef95905bdff3b47744f24c3af1bfd6ebc5820caf4d8ed2414aa507bc2f3f90ffe6deee39ea855a7d4bc3e9 languageName: node linkType: hard -"@pandacss/token-dictionary@npm:0.46.1": - version: 0.46.1 - resolution: "@pandacss/token-dictionary@npm:0.46.1" +"@pandacss/token-dictionary@npm:0.47.0": + version: 0.47.0 + resolution: "@pandacss/token-dictionary@npm:0.47.0" dependencies: - "@pandacss/logger": "npm:^0.46.1" - "@pandacss/shared": "npm:0.46.1" - "@pandacss/types": "npm:0.46.1" + "@pandacss/logger": "npm:^0.47.0" + "@pandacss/shared": "npm:0.47.0" + "@pandacss/types": "npm:0.47.0" ts-pattern: "npm:5.0.8" - checksum: 10c0/e7635d639e3105e0c90bf72ee21a1b2ebf3dae554f03f69d999785165477f2b4780a6064e5b3c37e979fe5382e1b84d6c9baa1dc59751d883cc571fb2de5c14a + checksum: 10c0/b172997d94e6e38708656bcd75d18b12cb4b5dcf0eb3d161113d07683d75ddc0b3325f009db7a636ccf906e3cabb4ba8cb42fa6b30b7a3eb343e64c51c5207b0 languageName: node linkType: hard -"@pandacss/types@npm:0.46.1, @pandacss/types@npm:^0.46.1": - version: 0.46.1 - resolution: "@pandacss/types@npm:0.46.1" - checksum: 10c0/950ee13051a7ee1619beed582917c44874ad5f6013deca92db3d9b6e6500cf01710558b97d77e9e1a5735b2199e2ee3643633dd3ef781437fd62f84efa876127 +"@pandacss/types@npm:0.47.0, @pandacss/types@npm:^0.47.0": + version: 0.47.0 + resolution: "@pandacss/types@npm:0.47.0" + checksum: 10c0/d438cff30379a5d0b88801ce4349ab495e6342b46e9a7217074814f4b899f2269c515d012a9441b567ac704bddf3935999b208f84d4d7a631826e35b1185e83f languageName: node linkType: hard @@ -5309,7 +5309,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.23.1, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0": +"browserslist@npm:^4.0.0, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0": version: 4.24.0 resolution: "browserslist@npm:4.24.0" dependencies: @@ -8134,7 +8134,16 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:0.30.11, magic-string@npm:^0.30.6": +"magic-string@npm:0.30.12": + version: 0.30.12 + resolution: "magic-string@npm:0.30.12" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + checksum: 10c0/469f457d18af37dfcca8617086ea8a65bcd8b60ba8a1182cb024ce43e470ace3c9d1cb6bee58d3b311768fb16bc27bd50bdeebcaa63dadd0fd46cac4d2e11d5f + languageName: node + linkType: hard + +"magic-string@npm:^0.30.6": version: 0.30.11 resolution: "magic-string@npm:0.30.11" dependencies: @@ -9322,29 +9331,29 @@ __metadata: languageName: node linkType: hard -"postcss-merge-rules@npm:7.0.2": - version: 7.0.2 - resolution: "postcss-merge-rules@npm:7.0.2" +"postcss-merge-rules@npm:7.0.4": + version: 7.0.4 + resolution: "postcss-merge-rules@npm:7.0.4" dependencies: - browserslist: "npm:^4.23.1" + browserslist: "npm:^4.23.3" caniuse-api: "npm:^3.0.0" cssnano-utils: "npm:^5.0.0" - postcss-selector-parser: "npm:^6.1.0" + postcss-selector-parser: "npm:^6.1.2" peerDependencies: postcss: ^8.4.31 - checksum: 10c0/fbad20382ca45f1b3b5ff704c075f899cc9ba8418ae6effbdeb9e7c1f9b5c24996d1941ad36cd0936d60cbf127a72f235b2cbb0c44d9239a8a61042406d95b4a + checksum: 10c0/fffdcef4ada68e92ab8e6dc34a3b9aa2b87188cd4d08f5ba0ff2aff7e3e3c7f086830748ff64db091b5ccb9ac59ac37cfaab1268ed3efb50ab9c4f3714eb5f6d languageName: node linkType: hard -"postcss-minify-selectors@npm:7.0.2": - version: 7.0.2 - resolution: "postcss-minify-selectors@npm:7.0.2" +"postcss-minify-selectors@npm:7.0.4": + version: 7.0.4 + resolution: "postcss-minify-selectors@npm:7.0.4" dependencies: cssesc: "npm:^3.0.0" - postcss-selector-parser: "npm:^6.1.0" + postcss-selector-parser: "npm:^6.1.2" peerDependencies: postcss: ^8.4.31 - checksum: 10c0/87e0c21a0135b6c61b58d62c4c1e0cbd3cfb516ff8105db714c6a33a5edc477846ae220399d368e4ef6518529c711aa2dee9ff49e9befd93e83d5c939f3084a1 + checksum: 10c0/212b8f3d62eb2a27ed57d4e76b75b0886806ddb9e2497c0bb79308fa75dabaaaa4ed2b97734896e87603272d05231fd74aee2c256a48d77aa468b5b64cc7866a languageName: node linkType: hard @@ -9370,17 +9379,7 @@ __metadata: languageName: node linkType: hard -"postcss-selector-parser@npm:6.1.1": - version: 6.1.1 - resolution: "postcss-selector-parser@npm:6.1.1" - dependencies: - cssesc: "npm:^3.0.0" - util-deprecate: "npm:^1.0.2" - checksum: 10c0/5608765e033fee35d448e1f607ffbaa750eb86901824a8bc4a911ea8bc137cb82f29239330787427c5d3695afd90d8721e190f211dbbf733e25033d8b3100763 - languageName: node - linkType: hard - -"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.1.0": +"postcss-selector-parser@npm:6.1.2, postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.13, postcss-selector-parser@npm:^6.1.2": version: 6.1.2 resolution: "postcss-selector-parser@npm:6.1.2" dependencies: @@ -11138,8 +11137,8 @@ __metadata: "@dnd-kit/sortable": "npm:^8.0.0" "@heroicons/react": "npm:^2.1.5" "@hookform/resolvers": "npm:^3.9.0" - "@pandacss/dev": "npm:^0.46.1" - "@pandacss/types": "npm:^0.46.1" + "@pandacss/dev": "npm:^0.47.0" + "@pandacss/types": "npm:^0.47.0" "@park-ui/panda-preset": "npm:^0.42.0" "@simplewebauthn/browser": "npm:^10.0.0" "@simplewebauthn/types": "npm:^10.0.0" From 144db682015356490396bb06f9ec63aaf881bc81 Mon Sep 17 00:00:00 2001 From: Barnaby Keene <1636971+Southclaws@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:34:35 +0100 Subject: [PATCH 2/2] fix query for tag counts --- app/resources/tag/tag_querier/querier.go | 28 ++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/app/resources/tag/tag_querier/querier.go b/app/resources/tag/tag_querier/querier.go index 556efbd5d..45d4d4fd7 100644 --- a/app/resources/tag/tag_querier/querier.go +++ b/app/resources/tag/tag_querier/querier.go @@ -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, @@ -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(