diff --git a/config/tailwind/package.json b/config/tailwind/package.json new file mode 100644 index 0000000000..38f4500628 --- /dev/null +++ b/config/tailwind/package.json @@ -0,0 +1,26 @@ +{ + "name": "@pubpub/tailwind", + "type": "module", + "version": "0.0.0", + "private": true, + "license": "MIT", + "exports": { + "./style.css": "./style.css", + "./postcss-config": "./postcss.config.js" + }, + "scripts": {}, + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "tailwindcss-animate": "catalog:", + "@tailwindcss/typography": "catalog:", + "@tailwindcss/forms": "catalog:" + }, + "dependencies": { + "@tailwindcss/postcss": "catalog:", + "postcss": "catalog:", + "tailwindcss": "catalog:" + }, + "devDependencies": {} +} diff --git a/config/tailwind/postcss.config.js b/config/tailwind/postcss.config.js new file mode 100644 index 0000000000..b78d5b1610 --- /dev/null +++ b/config/tailwind/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +} diff --git a/config/tailwind/style.css b/config/tailwind/style.css new file mode 100644 index 0000000000..1cffcfa617 --- /dev/null +++ b/config/tailwind/style.css @@ -0,0 +1,192 @@ +/** biome-ignore-all lint/suspicious/noDuplicateCustomProperties: biome doesn't understand tailwind */ +@import "tailwindcss"; + +@plugin "tailwindcss-animate"; +@plugin "@tailwindcss/typography"; + +@custom-variant dark (&:where(.dark, .dark *)); + +@utility container { + margin-inline: auto; + padding-inline: 2rem; + @media (width >= --theme(--breakpoint-sm)) { + max-width: none; + } + @media (width >= 1400px) { + max-width: 1400px; + } +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + + @variant dark { + --hwhite: oklch(89.763% 0.00435 15.946); + + --background: oklch(17.304% 0.00002 271.152); + --foreground: var(--hwhite); + --card: oklch(0.205 0 0); + --card-foreground: var(--hwhite); + --popover: oklch(0.205 0 0); + --popover-foreground: var(--hwhite); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: var(--hwhite); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: var(--hwhite); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: var(--hwhite); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: var(--hwhite); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: var(--hwhite); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); + } +} + +*, +::after, +::before, +::backdrop, +::file-selector-button { + border-color: var(--border, currentColor); +} + +@theme inline { + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-input: var(--input); + --color-border: var(--border); + --color-ring: var(--ring); + + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-active: var(--sidebar-active); + + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + --animate-collapsible-down: collapsible-down 0.2s ease-out; + --animate-collapsible-up: collapsible-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } + @keyframes collapsible-down { + from { + height: 0; + } + to { + height: var(--radix-collapsible-content-height); + } + } + @keyframes collapsible-up { + from { + height: var(--radix-collapsible-content-height); + } + to { + height: 0; + } + } +} + +.editor .token { + font-size: 0.7rem; + color: #3b90f7; + font-family: monospace; + background-color: rgba(59, 144, 247, 0.2); + padding: 2px 1px; + box-shadow: 0px 0px 0px 1px rgba(59, 144, 247, 0.5); + border-radius: 3px; + margin: 0 1px; +} +.editor .jsonata-token { + font-size: 0.7rem; + color: #e7a316; + font-family: monospace; + background-color: rgba(231, 163, 22, 0.2); + padding: 2px 1px; + box-shadow: 0px 0px 0px 1px rgba(231, 163, 22, 0.5); + border-radius: 3px; + margin: 0 1px; +} + +.editor.markdown { + min-height: 200px; +} diff --git a/core/actions/_lib/ActionField.tsx b/core/actions/_lib/ActionField.tsx index a4ecd1136c..a30a8a7182 100644 --- a/core/actions/_lib/ActionField.tsx +++ b/core/actions/_lib/ActionField.tsx @@ -132,11 +132,10 @@ const JSONataToggleButton = memo( aria-label={`Toggle JSONata mode for ${fieldName}`} data-testid={`toggle-jsonata-${fieldName}`} className={cn( - "font-mono font-semibold text-gray-900 hover:bg-amber-50", + "font-mono font-semibold text-foreground hover:bg-amber-50", "transition-colors duration-200", - inputState.state === "jsonata" && - "border-orange-400 bg-orange-50 text-orange-900" + inputState.state === "jsonata" && "border-amber-400 bg-amber-600 text-amber-200" )} onClick={handleToggle} > @@ -241,7 +240,7 @@ const InnerActionField = memo(
{label} - + {props.description ?? fieldSchema.description}
diff --git a/core/actions/_lib/ActionFieldJsonataInput.tsx b/core/actions/_lib/ActionFieldJsonataInput.tsx index 2040ae615d..7ee8c60f7f 100644 --- a/core/actions/_lib/ActionFieldJsonataInput.tsx +++ b/core/actions/_lib/ActionFieldJsonataInput.tsx @@ -36,7 +36,7 @@ export function ActionFieldJsonataInput(props: { JSONata {/* TODO: write actual docs */} - + You can write {autoEvaluate ? : } @@ -351,7 +351,7 @@ export function ActionFieldJsonataTestPanel(props: {
{/* to make it easier for screen readers to understand the output */} {testResult.status === "pending" && ( -
+
Evaluating...
@@ -378,7 +378,7 @@ export function ActionFieldJsonataTestPanel(props: { htmlFor={props.configKey} aria-label="Success: JSONata test interpolated value" > -
+								
 									{JSON.stringify(testResult.interpolated, null, 2)}
 								
@@ -406,7 +406,7 @@ export function ActionFieldJsonataTestPanel(props: { htmlFor={props.configKey} aria-label="Error: JSONata test interpolated value" > -
+									
 										{JSON.stringify(testResult.interpolated, null, 2)}
 									
diff --git a/core/actions/_lib/ActionForm.tsx b/core/actions/_lib/ActionForm.tsx index 11bbe24279..5f03dffcf7 100644 --- a/core/actions/_lib/ActionForm.tsx +++ b/core/actions/_lib/ActionForm.tsx @@ -77,8 +77,7 @@ export function ActionForm(props: ActionFormProps) { return result.data } - toast({ - title: "Invalid initial values", + toast.error({ description: `Can't parse values ${JSON.stringify(props.values)}: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("\n")}. This is likely an issue on our end, please report this.`, variant: "destructive", }) diff --git a/core/actions/_lib/triggers/PubInStageForDurationConfigForm.tsx b/core/actions/_lib/triggers/PubInStageForDurationConfigForm.tsx index 1fd159f0c4..505a626a57 100644 --- a/core/actions/_lib/triggers/PubInStageForDurationConfigForm.tsx +++ b/core/actions/_lib/triggers/PubInStageForDurationConfigForm.tsx @@ -32,7 +32,6 @@ export const PubInStageForDurationConfigForm: AddionalConfigForm p.field.onChange( diff --git a/core/app/(user)/communities/AddCommunityForm.tsx b/core/app/(user)/communities/AddCommunityForm.tsx index 707811c3d5..c96e27e500 100644 --- a/core/app/(user)/communities/AddCommunityForm.tsx +++ b/core/app/(user)/communities/AddCommunityForm.tsx @@ -30,10 +30,7 @@ export const AddCommunityForm = (props: Props) => { const result = await runCreateCommunity({ ...data }) if (didSucceed(result)) { props.setOpen(false) - toast({ - title: "Success", - description: "Community created", - }) + toast.success("Community created") } } const form = useForm>({ diff --git a/core/app/(user)/communities/RemoveCommunityButton.tsx b/core/app/(user)/communities/RemoveCommunityButton.tsx index 06bbb1bd76..e062b41238 100644 --- a/core/app/(user)/communities/RemoveCommunityButton.tsx +++ b/core/app/(user)/communities/RemoveCommunityButton.tsx @@ -49,11 +49,7 @@ export const RemoveCommunityButton = ({ community }: { community: TableCommunity onClick={async () => { const response = await runRemoveCommunity({ community }) if (didSucceed(response)) { - toast({ - title: "Success", - description: "Community successfully removed", - variant: "default", - }) + toast.success("Community successfully removed") } }} > diff --git a/core/app/(user)/reset/page.tsx b/core/app/(user)/reset/page.tsx index 5023a0e24d..11274da15c 100644 --- a/core/app/(user)/reset/page.tsx +++ b/core/app/(user)/reset/page.tsx @@ -12,7 +12,7 @@ export default async function Page() { if (!user) { return ( -
+

Invalid

It looks like this link has expired. Please request a new one.

diff --git a/core/app/(user)/settings/ResetPasswordButton.tsx b/core/app/(user)/settings/ResetPasswordButton.tsx index 73227fb497..c9fadd67cf 100644 --- a/core/app/(user)/settings/ResetPasswordButton.tsx +++ b/core/app/(user)/settings/ResetPasswordButton.tsx @@ -19,18 +19,10 @@ export const ResetPasswordButton = ({ user }: { user: UserLoginData }) => { const result = await runResetPassword({ email: user.email }) if (result && "error" in result) { - toast({ - title: "Error", - description: result.error, - variant: "destructive", - }) + toast.error(result.error) } - toast({ - title: "Success", - description: "Password reset email sent! Please check your inbox.", - duration: 5000, - }) + toast.success("Password reset email sent! Please check your inbox.") } return ( diff --git a/core/app/(user)/settings/UserInfoForm.tsx b/core/app/(user)/settings/UserInfoForm.tsx index 81f6113078..622117bdea 100644 --- a/core/app/(user)/settings/UserInfoForm.tsx +++ b/core/app/(user)/settings/UserInfoForm.tsx @@ -47,10 +47,7 @@ export function UserInfoForm({ user }: { user: UserLoginData }) { const onSubmit = async (data: z.infer) => { const result = await runUpdateUserInfo({ data }) if (result && "success" in result) { - toast({ - title: "Success", - description: "User information updated", - }) + toast.success("User information updated") } } @@ -139,7 +136,7 @@ export function UserInfoForm({ user }: { user: UserLoginData }) { !form.formState.isValid || !form.formState.isDirty } - className="w-min flex-grow-0" + className="w-min grow-0" > Save {form.formState.isSubmitting && } diff --git a/core/app/(user)/settings/page.tsx b/core/app/(user)/settings/page.tsx index 9b81e9db9f..2cc871d17e 100644 --- a/core/app/(user)/settings/page.tsx +++ b/core/app/(user)/settings/page.tsx @@ -28,7 +28,7 @@ export default async function Page() { ) return ( -
+

Settings

@@ -65,7 +65,7 @@ export default async function Page() { {community.name[0]} -
{community.name}
+
{community.name}
) diff --git a/core/app/(user)/verify/page.tsx b/core/app/(user)/verify/page.tsx index 9afa3fbdaa..fb358effdf 100644 --- a/core/app/(user)/verify/page.tsx +++ b/core/app/(user)/verify/page.tsx @@ -43,7 +43,7 @@ export default async function Page({ searchParams }: { searchParams: Promise +

Verify your email

{description}

diff --git a/core/app/RootToaster.tsx b/core/app/RootToaster.tsx deleted file mode 100644 index 8fe4828ab9..0000000000 --- a/core/app/RootToaster.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client" - -import type { ToasterToast } from "ui/use-toast" - -import { useEffect } from "react" -import { CircleCheck } from "lucide-react" -import { parseAsBoolean, useQueryStates } from "nuqs" - -import { Toaster } from "ui/toaster" -import { toast } from "ui/use-toast" - -import { entries, fromEntries, keys } from "~/lib/mapping" - -const PERSISTED_TOAST = { - verified: { - title: "Verified", - description: ( - - Your email is now verified - - ), - variant: "success", - }, -} as const satisfies { [key: string]: Omit } - -const usePersistedToasts = () => { - const toastQueries = fromEntries( - keys(PERSISTED_TOAST).map((key) => [key, parseAsBoolean.withDefault(false)]) - ) - - const [params, setParams] = useQueryStates(toastQueries, { - history: "replace", - scroll: false, - }) - const activeToasts = entries(params) - .filter(([_param, active]) => active) - .map(([param]) => param) - - useEffect(() => { - for (const activeToastKey of activeToasts) { - const toastData = PERSISTED_TOAST[activeToastKey] - toast(toastData) - setParams({ - [activeToastKey]: null, - }) - } - }, [activeToasts]) -} - -export const RootToaster = () => { - usePersistedToasts() - - return -} diff --git a/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts b/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts index 20e3f8a12f..bb0555ff68 100644 --- a/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts +++ b/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts @@ -190,6 +190,7 @@ const handler = createNextHandler( } } + console.log(rest) const [pubs, pubCount] = await Promise.all([ getPubsWithRelatedValues( { diff --git a/core/app/c/(public)/[communitySlug]/public/forms/[formSlug]/fill/RequestLink.tsx b/core/app/c/(public)/[communitySlug]/public/forms/[formSlug]/fill/RequestLink.tsx index c21372b485..4419f3dfd9 100644 --- a/core/app/c/(public)/[communitySlug]/public/forms/[formSlug]/fill/RequestLink.tsx +++ b/core/app/c/(public)/[communitySlug]/public/forms/[formSlug]/fill/RequestLink.tsx @@ -31,16 +31,13 @@ export const RequestLink = ({ return } - toast({ - title: "Link sent", - description: "Successfully requested new link", - }) + toast.success("Link sent") }, [token, formSlug, pubId, communityId]) return ( diff --git a/core/app/c/[communitySlug]/CommunitySwitcher.tsx b/core/app/c/[communitySlug]/CommunitySwitcher.tsx index c51ebb7705..7cb3d58c14 100644 --- a/core/app/c/[communitySlug]/CommunitySwitcher.tsx +++ b/core/app/c/[communitySlug]/CommunitySwitcher.tsx @@ -21,8 +21,7 @@ type Props = { } const CommunitySwitcher: React.FC = async ({ community, availableCommunities }) => { - const avatarClasses = - "rounded-md w-9 h-9 mr-1 group-data-[collapsible=icon]:h-8 group-data-[collapsible=icon]:w-8 border" + const avatarClasses = "rounded-md size-6" const textClasses = "flex-auto text-base font-semibold w-44 text-left" const onlyOneCommunity = availableCommunities.length === 1 @@ -44,10 +43,14 @@ const CommunitySwitcher: React.FC = async ({ community, availableCommunit const button = ( - - + + {community.name[0]} {community.name} @@ -62,7 +65,7 @@ const CommunitySwitcher: React.FC = async ({ community, availableCommunit return ( {button} - + {availableCommunities .filter((option) => { return option?.slug !== community.slug @@ -72,17 +75,20 @@ const CommunitySwitcher: React.FC = async ({ community, availableCommunit -
- - - {option.name[0]} - - - {option.name} - -
+ + + {option.name[0]} + + + {option.name} +
) diff --git a/core/app/c/[communitySlug]/ContentLayout.tsx b/core/app/c/[communitySlug]/ContentLayout.tsx index 768546909a..fcdfceda60 100644 --- a/core/app/c/[communitySlug]/ContentLayout.tsx +++ b/core/app/c/[communitySlug]/ContentLayout.tsx @@ -14,10 +14,10 @@ const Heading = ({ right?: ReactNode }) => { return ( -
+
{COLLAPSIBLE_TYPE === "icon" ? null : } {left} -

+

{title}

{right} @@ -42,7 +42,11 @@ export const ContentLayout = ({
-
{children}
+
+ {children} +
) diff --git a/core/app/c/[communitySlug]/LoginSwitcher.tsx b/core/app/c/[communitySlug]/LoginSwitcher.tsx index 70044dabf3..362cb80fac 100644 --- a/core/app/c/[communitySlug]/LoginSwitcher.tsx +++ b/core/app/c/[communitySlug]/LoginSwitcher.tsx @@ -1,8 +1,5 @@ -import type { User } from "lucia" - import Link from "next/link" -import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar" import { Button } from "ui/button" import { ChevronsUpDown, UserRoundCog } from "ui/icon" import { Popover, PopoverContent, PopoverTrigger } from "ui/popover" @@ -11,20 +8,7 @@ import { SidebarMenuButton } from "ui/sidebar" import { getLoginData } from "~/lib/authentication/loginData" import LogoutButton from "../../components/LogoutButton" - -const AvatarThing = ({ user }: { user: User }) => ( -
- - - {(user.firstName || user.email)[0].toUpperCase()} - - -
-

{user.firstName}

-

{user.email}

-
-
-) +import { UserDisplay } from "./UserDisplay" export default async function LoginSwitcher() { const { user } = await getLoginData() @@ -41,7 +25,7 @@ export default async function LoginSwitcher() { data-testid="user-menu-button" aria-haspopup="true" > - +
- +
diff --git a/core/app/c/[communitySlug]/NavLink.tsx b/core/app/c/[communitySlug]/NavLink.tsx index 6ab6832eba..cc6c551e89 100644 --- a/core/app/c/[communitySlug]/NavLink.tsx +++ b/core/app/c/[communitySlug]/NavLink.tsx @@ -40,7 +40,10 @@ export default function NavLink({ const isActive = regex.test(pathname) const content = ( - + {icon ? icon : null} {text} diff --git a/core/app/c/[communitySlug]/SideNav.tsx b/core/app/c/[communitySlug]/SideNav.tsx index 7861608d5b..2afe6be25c 100644 --- a/core/app/c/[communitySlug]/SideNav.tsx +++ b/core/app/c/[communitySlug]/SideNav.tsx @@ -31,9 +31,10 @@ import { SidebarMenuSkeleton, SidebarMenuSubItem, SidebarRail, - SidebarSeparator, } from "ui/sidebar" +import { SidebarSearchDialogTrigger } from "~/app/components/search/SearchDialogTrigger" +import { SidebarDarkmodeToggle } from "~/app/components/theme/DarkmodeToggle" import { getLoginData } from "~/lib/authentication/loginData" import { userCan, userCanViewStagePage } from "~/lib/authorization/capabilities" import CommunitySwitcher from "./CommunitySwitcher" @@ -322,7 +323,7 @@ const LinkGroup = async ({ return ( - + {group.name} @@ -345,8 +346,8 @@ const SideNav: React.FC = async ({ community, availableCommunities }) => } return ( - - + + = async ({ community, availableCommunities }) => /> - + {/* */}
-
- - - -
+ + +
- + + + + + + + + + + + diff --git a/core/app/c/[communitySlug]/UserDisplay.tsx b/core/app/c/[communitySlug]/UserDisplay.tsx new file mode 100644 index 0000000000..1fd0f4c3d1 --- /dev/null +++ b/core/app/c/[communitySlug]/UserDisplay.tsx @@ -0,0 +1,20 @@ +import type { User } from "contracts" + +import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar" + +export const UserDisplay = ({ user }: { user: User }) => ( +
+ + + + {user.firstName[0]} + {user.lastName?.[0] ?? ""} + + + +
+

{user.firstName}

+

{user.email}

+
+
+) diff --git a/core/app/c/[communitySlug]/activity/actions/page.tsx b/core/app/c/[communitySlug]/activity/actions/page.tsx index 77eb9271fc..ff5a96b652 100644 --- a/core/app/c/[communitySlug]/activity/actions/page.tsx +++ b/core/app/c/[communitySlug]/activity/actions/page.tsx @@ -54,8 +54,8 @@ export default async function Page(props: { - Action - Logs + {" "} + Action Logs } > diff --git a/core/app/c/[communitySlug]/fields/FieldForm.tsx b/core/app/c/[communitySlug]/fields/FieldForm.tsx index e01a80d14a..c5c73442f8 100644 --- a/core/app/c/[communitySlug]/fields/FieldForm.tsx +++ b/core/app/c/[communitySlug]/fields/FieldForm.tsx @@ -213,7 +213,7 @@ const IsRelationCheckbox = ({ form, isDisabled }: { form: FormType; isDisabled: field.onChange(change) } }} - className="rounded" + className="rounded-sm" data-testid="isRelation-checkbox" /> @@ -258,7 +258,7 @@ export const FieldForm = ({ isRelation: values.isRelation, }) if (didSucceed(result)) { - toast({ title: `Created field ${values.name}` }) + toast.success(`Created field ${values.name}`) onSubmitSuccess() } }, @@ -268,7 +268,7 @@ export const FieldForm = ({ const handleUpdate = useCallback(async (values: FormValues) => { const result = await updateFieldName(values.id, values.name) if (didSucceed(result)) { - toast({ title: `Updated field ${values.name}` }) + toast.success(`Updated field ${values.name}`) onSubmitSuccess() } }, []) diff --git a/core/app/c/[communitySlug]/fields/FieldsTable.tsx b/core/app/c/[communitySlug]/fields/FieldsTable.tsx index 1cf5b7d09e..a408dd04ed 100644 --- a/core/app/c/[communitySlug]/fields/FieldsTable.tsx +++ b/core/app/c/[communitySlug]/fields/FieldsTable.tsx @@ -65,6 +65,10 @@ export const FieldsTable = ({ fields }: { fields: PubField[] }) => { data={data} onRowClick={handleRowClick} defaultSort={[{ id: "updated", desc: true }]} + pagination={{ + pageIndex: 0, + pageSize: 50, + }} /> { open={isOpen} trigger={ - diff --git a/core/app/c/[communitySlug]/fields/getFieldTableColumns.tsx b/core/app/c/[communitySlug]/fields/getFieldTableColumns.tsx index d32a9bcd6e..411ef51032 100644 --- a/core/app/c/[communitySlug]/fields/getFieldTableColumns.tsx +++ b/core/app/c/[communitySlug]/fields/getFieldTableColumns.tsx @@ -7,7 +7,8 @@ import { SCHEMA_TYPES_WITH_ICONS } from "schemas" import { Checkbox } from "ui/checkbox" import { DataTableColumnHeader } from "ui/data-table" import { DropdownMenuItem } from "ui/dropdown-menu" -import { Archive, CurlyBraces, History } from "ui/icon" +import { Archive, History } from "ui/icon" +import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip" import { toast } from "ui/use-toast" import { MenuItemButton, TableActionMenu } from "~/app/components/TableActionMenu" @@ -29,7 +30,7 @@ const ArchiveMenuItem = ({ field }: { field: TableData }) => { const handleArchive = useCallback(async () => { const result = await archiveField(field.id) if (didSucceed(result)) { - toast({ title: `Archived ${field.name}` }) + toast.success(`Archived ${field.name}`) } }, [field.id]) return ( @@ -66,41 +67,43 @@ export const getFieldTableColumns = () => enableSorting: false, enableHiding: false, }, + { + header: () => , + accessorKey: "schemaName", + id: "schemaNameIcon", + cell: ({ cell }) => { + const schemaName = cell.getValue() + const Icon = schemaName ? SCHEMA_TYPES_WITH_ICONS[schemaName].icon : null + if (!Icon) return null + return ( + + + + {schemaName} + + {schemaName} + + ) + }, + }, { header: ({ column }) => , accessorKey: "name", cell: ({ row }) => { - const { schemaName, name } = row.original - const Icon = schemaName ? SCHEMA_TYPES_WITH_ICONS[schemaName].icon : null return (
- {Icon ? ( - - - - ) : null} {row.original.name}
) }, }, - { - header: ({ column }) => ( - } - /> - ), - accessorKey: "schemaName", - }, { id: "slug", header: ({ column }) => , accessorKey: "slug", cell: ({ row }) => (
- + {row.original.slug}
diff --git a/core/app/c/[communitySlug]/fields/page.tsx b/core/app/c/[communitySlug]/fields/page.tsx index 04b0bb0e68..3b886c8839 100644 --- a/core/app/c/[communitySlug]/fields/page.tsx +++ b/core/app/c/[communitySlug]/fields/page.tsx @@ -74,7 +74,11 @@ export default async function Page(props: Props) { - {" "} + {" "} Fields } diff --git a/core/app/c/[communitySlug]/forms/NewFormButton.tsx b/core/app/c/[communitySlug]/forms/NewFormButton.tsx index 3f0176249d..b29c39b16c 100644 --- a/core/app/c/[communitySlug]/forms/NewFormButton.tsx +++ b/core/app/c/[communitySlug]/forms/NewFormButton.tsx @@ -57,19 +57,12 @@ export const NewFormButton = ({ pubTypes }: Props) => { const onSubmit = async ({ pubTypeName, name, slug }: z.infer) => { const pubTypeId = pubTypes.find((type) => type.name === pubTypeName)?.id if (!pubTypeId) { - toast({ - title: "Error", - description: `Unable to find pub type ${pubTypeName}`, - variant: "destructive", - }) + toast.error(`Unable to find pub type ${pubTypeName}`) return } const result = await runCreateForm(pubTypeId, name, slug, community.id) if (didSucceed(result)) { - toast({ - title: "Success", - description: "Form created", - }) + toast.success("Form created") form.reset() setIsOpen(false) } @@ -81,12 +74,12 @@ export const NewFormButton = ({ pubTypes }: Props) => { - + Create New Form
diff --git a/core/app/c/[communitySlug]/forms/[formSlug]/edit/EditFormTitleButton.tsx b/core/app/c/[communitySlug]/forms/[formSlug]/edit/EditFormTitleButton.tsx index bd59937400..71dafbf081 100644 --- a/core/app/c/[communitySlug]/forms/[formSlug]/edit/EditFormTitleButton.tsx +++ b/core/app/c/[communitySlug]/forms/[formSlug]/edit/EditFormTitleButton.tsx @@ -10,7 +10,6 @@ import { z } from "zod" import { Button } from "ui/button" import { Dialog, DialogContent, DialogFooter, DialogTitle, DialogTrigger } from "ui/dialog" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "ui/form" -import { CircleCheck } from "ui/icon" import { Input } from "ui/input" import { toast } from "ui/use-toast" @@ -35,14 +34,7 @@ const EditFormTitleButton = ({ formId, name }: { formId: FormsId; name: string } const onSubmit = async (data: z.infer) => { const result = await runUpdateFormTitle({ formId, name: data.name }) if (didSucceed(result)) { - toast({ - className: "rounded border-emerald-100 bg-emerald-50", - action: ( -
- Name Successfully Updated -
- ), - }) + toast.success("Name Successfully Updated") setIsOpen(false) } } diff --git a/core/app/c/[communitySlug]/forms/[formSlug]/edit/page.tsx b/core/app/c/[communitySlug]/forms/[formSlug]/edit/page.tsx index 0552449369..4a7961be68 100644 --- a/core/app/c/[communitySlug]/forms/[formSlug]/edit/page.tsx +++ b/core/app/c/[communitySlug]/forms/[formSlug]/edit/page.tsx @@ -67,31 +67,34 @@ export default async function Page(props: { return ( -
- {" "} - {form.name} - -
- {form.isDefault && ( -
- Default editor for this type - - - - - - This form is used as the default internal editor for all Pubs of - this type. - - + <> + {" "} +
+
+

{form.name}

+
- )} -
+ + {form.isDefault && ( +
+ Default editor for this type + + + + + + This form is used as the default internal editor for all + Pubs of this type. + + +
+ )} +
+ } right={
diff --git a/core/app/c/[communitySlug]/forms/page.tsx b/core/app/c/[communitySlug]/forms/page.tsx index f19a61ad15..8f856e2598 100644 --- a/core/app/c/[communitySlug]/forms/page.tsx +++ b/core/app/c/[communitySlug]/forms/page.tsx @@ -87,7 +87,11 @@ export default async function Page(props: { - {" "} + {" "} Forms } diff --git a/core/app/c/[communitySlug]/layout.tsx b/core/app/c/[communitySlug]/layout.tsx index ec71cadd55..b4918af556 100644 --- a/core/app/c/[communitySlug]/layout.tsx +++ b/core/app/c/[communitySlug]/layout.tsx @@ -9,11 +9,11 @@ import { cn } from "utils" import { LAST_VISITED_COOKIE } from "~/app/components/LastVisitedCommunity/constants" import SetLastVisited from "~/app/components/LastVisitedCommunity/SetLastVisited" import { CommunityProvider } from "~/app/components/providers/CommunityProvider" -import { SearchDialog } from "~/app/components/SearchDialog" +import { SearchDialogProvider } from "~/app/components/search/SearchDialogProvider" import { getPageLoginData } from "~/lib/authentication/loginData" import { getCommunityRole } from "~/lib/authentication/roles" import { findCommunityBySlug } from "~/lib/server/community" -import SideNav, { COLLAPSIBLE_TYPE } from "./SideNav" +import SideNav from "./SideNav" type Props = { children: React.ReactNode; params: Promise<{ communitySlug: string }> } @@ -64,22 +64,34 @@ export default async function MainLayout(props: Props) { return ( - {params.communitySlug !== lastVisited?.value && ( - - )} -
- - -
{children}
- - -
-
+ + {params.communitySlug !== lastVisited?.value && ( + + )} +
+ + +
+ {children} +
+ +
+
+
) } diff --git a/core/app/c/[communitySlug]/loading.tsx b/core/app/c/[communitySlug]/loading.tsx new file mode 100644 index 0000000000..d599ae7086 --- /dev/null +++ b/core/app/c/[communitySlug]/loading.tsx @@ -0,0 +1,22 @@ +import { Skeleton } from "ui/skeleton" +import { Spinner } from "ui/spinner" + +import { SkeletonButton } from "~/app/components/skeletons/SkeletonButton" +import { ContentLayout } from "./ContentLayout" + +export default function Loading() { + return ( + + + + + } + right={} + className="grid place-items-center overflow-hidden" + > + + + ) +} diff --git a/core/app/c/[communitySlug]/loading/page.tsx b/core/app/c/[communitySlug]/loading/page.tsx new file mode 100644 index 0000000000..4184ff10bd --- /dev/null +++ b/core/app/c/[communitySlug]/loading/page.tsx @@ -0,0 +1,22 @@ +import { Skeleton } from "ui/skeleton" +import { Spinner } from "ui/spinner" + +import { SkeletonButton } from "~/app/components/skeletons/SkeletonButton" +import { ContentLayout } from "../ContentLayout" + +export default function Loading() { + return ( + + + + + } + right={} + className="grid place-items-center overflow-hidden" + > + + + ) +} diff --git a/core/app/c/[communitySlug]/members/RemoveMemberButton.tsx b/core/app/c/[communitySlug]/members/RemoveMemberButton.tsx index f0713da251..03f9b87a2c 100644 --- a/core/app/c/[communitySlug]/members/RemoveMemberButton.tsx +++ b/core/app/c/[communitySlug]/members/RemoveMemberButton.tsx @@ -52,11 +52,7 @@ export const RemoveMemberButton = ({ member }: { member: TableMember }) => { onClick={async () => { const response = await runRemoveMember({ member }) if (didSucceed(response)) { - toast({ - title: "Success", - description: "Member successfully removed", - variant: "default", - }) + toast.success("Member successfully removed") } }} > diff --git a/core/app/c/[communitySlug]/members/getMemberTableColumns.tsx b/core/app/c/[communitySlug]/members/getMemberTableColumns.tsx index adf76e7860..c2389fae2a 100644 --- a/core/app/c/[communitySlug]/members/getMemberTableColumns.tsx +++ b/core/app/c/[communitySlug]/members/getMemberTableColumns.tsx @@ -112,7 +112,7 @@ export const getMemberTableColumns = (props: TableColumnsProps) => ? "default" : role === "editor" ? "secondary" - : "outline" + : "outline-solid" } > {role} diff --git a/core/app/c/[communitySlug]/members/page.tsx b/core/app/c/[communitySlug]/members/page.tsx index 9b493b784f..221bb5c54c 100644 --- a/core/app/c/[communitySlug]/members/page.tsx +++ b/core/app/c/[communitySlug]/members/page.tsx @@ -132,7 +132,8 @@ export default async function Page(props: { - Members + {" "} + Members } right={ diff --git a/core/app/c/[communitySlug]/pubs/PubHeader.tsx b/core/app/c/[communitySlug]/pubs/PubHeader.tsx deleted file mode 100644 index 3ca63553c9..0000000000 --- a/core/app/c/[communitySlug]/pubs/PubHeader.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { CommunitiesId } from "db/public" - -import { Suspense } from "react" -import Link from "next/link" - -import { Button } from "ui/button" - -import { CreatePubButton } from "~/app/components/pubs/CreatePubButton" -import { SkeletonButton } from "~/app/components/skeletons/SkeletonButton" - -type Props = { - communityId: CommunitiesId -} - -const PubHeader: React.FC = ({ communityId }) => { - return ( -
-

Pubs

-
- }> - - - -
-
- ) -} -export default PubHeader diff --git a/core/app/c/[communitySlug]/pubs/PubList.tsx b/core/app/c/[communitySlug]/pubs/PubList.tsx index 540f4be5b7..dc39065327 100644 --- a/core/app/c/[communitySlug]/pubs/PubList.tsx +++ b/core/app/c/[communitySlug]/pubs/PubList.tsx @@ -90,7 +90,7 @@ const PaginatedPubListInner = async ( (props.searchParams.pubTypes?.length ?? 0) > 0 || (props.searchParams.stages?.length ?? 0) > 0 return ( -
+
{pubs.length === 0 && ( diff --git a/core/app/c/[communitySlug]/pubs/PubSearchFooter.tsx b/core/app/c/[communitySlug]/pubs/PubSearchFooter.tsx index b1bf087bb8..769d9a5f52 100644 --- a/core/app/c/[communitySlug]/pubs/PubSearchFooter.tsx +++ b/core/app/c/[communitySlug]/pubs/PubSearchFooter.tsx @@ -43,7 +43,7 @@ export const PubSearchFooter = ( return (
@@ -151,7 +151,7 @@ export const PubSearchResultsPerPageInput = () => { })) }} > - + diff --git a/core/app/c/[communitySlug]/pubs/PubSearchInput.tsx b/core/app/c/[communitySlug]/pubs/PubSearchInput.tsx index 0935b273d4..1395e7c0ba 100644 --- a/core/app/c/[communitySlug]/pubs/PubSearchInput.tsx +++ b/core/app/c/[communitySlug]/pubs/PubSearchInput.tsx @@ -84,10 +84,10 @@ export const PubSearch = (props: PubSearchProps) => { return (
-
-
+
+
{ setQuery(e.target.value) }} placeholder="Search updates as you type..." - className={cn( - "bg-white pl-8 tracking-wide shadow-none", - inputValues && "pr-8" - )} + className={cn("pl-8 tracking-wide shadow-none", inputValues && "pr-8")} /> - + {inputValues?.query && ( +
+ {values.map((value) => + value.id ? ( + + ) : ( + // Blank space if there is no value +
+ ) + )} +
+ + ) +} + +const VVV = ({ + value, + setIsEditing, + pubId, + formSlug, + isEditing, +}: { + value: FullProcessedPubWithForm["values"][number] & { + formElementId: FormElementsId + id: PubValuesId + } + setIsEditing: (isEditing: boolean) => void + pubId: PubsId + formSlug: string + isEditing: boolean +}) => { + const [val, setValue] = useState(value.value) + + return !isEditing ? ( + + ) : ( + + ) +} + +const MiniForm = ({ + value, + setIsEditing, + pubId, + formSlug, + setValue, +}: { + value: FullProcessedPubWithForm["values"][number] & { + formElementId: FormElementsId + id: PubValuesId + } + setIsEditing: (isEditing: boolean) => void + pubId: PubsId + formSlug: string + setValue: (value: unknown) => void +}) => { + const miniForm = useForm({ + defaultValues: { + [value.fieldSlug]: value.value, + }, + resolver: typeboxResolver( + Type.Object({ + [value.fieldSlug]: getJsonSchemaByCoreSchemaType(value.schemaName), + }) + ), + }) + + const runUpdatePub = useServerAction(updatePub) + + const onSubmit = async (values: Record) => { + console.log(values) + setIsEditing(false) + const oldValue = value.value + setValue(values[value.fieldSlug] as unknown) + const result = await runUpdatePub({ + pubId, + continueOnValidationError: false, + deleted: [], + pubValues: { + [value.fieldSlug]: values[value.fieldSlug] as JsonValue, + }, + formSlug, + }) + if (didSucceed(result)) { + return + } + + setIsEditing(true) + setValue(oldValue) + } + + return ( + + + + + + + ) +} diff --git a/core/app/c/[communitySlug]/pubs/[pubId]/components/PubValue.tsx b/core/app/c/[communitySlug]/pubs/[pubId]/components/PubValue.tsx new file mode 100644 index 0000000000..f09d46c4dd --- /dev/null +++ b/core/app/c/[communitySlug]/pubs/[pubId]/components/PubValue.tsx @@ -0,0 +1,106 @@ +import type { User } from "contracts" +import type { FileUploadFile } from "~/lib/fields/fileUpload" +import type { FullProcessedPubWithForm } from "~/lib/server" + +import { CoreSchemaType } from "db/public" +import { Badge } from "ui/badge" +import { Checkbox } from "ui/checkbox" +import { ColorCircle, ColorValue } from "ui/color" +import { ShowMore } from "ui/show-more" + +import { FileUploadPreview } from "~/app/components/forms/FileUpload" +import { UserDisplay } from "../../../UserDisplay" +import { DateTimeDisplay } from "./DateTimeDisplay" + +export const PubValue = ({ value }: { value: FullProcessedPubWithForm["values"][number] }) => { + if (value.value === null) { + return "-" + } + + switch (value.schemaName) { + case CoreSchemaType.String: + return ( +

+ {value.value as string} +

+ ) + case CoreSchemaType.RichText: + return ( + +
+ + ) + + case CoreSchemaType.URL: + return ( + + {value.value as string} + + ) + case CoreSchemaType.Email: + return ( + + {value.value as string} + + ) + case CoreSchemaType.DateTime: + return ( + + ) + case CoreSchemaType.Color: + return ( +
+ Pick a color + + +
+ ) + case CoreSchemaType.FileUpload: + return + case CoreSchemaType.Boolean: + return + case CoreSchemaType.MemberId: + return + case CoreSchemaType.Vector3: + case CoreSchemaType.NumericArray: + return ( +
+ [{value.value.join(", ")}] +
+ ) + case CoreSchemaType.Number: + return {value.value as number} + case CoreSchemaType.StringArray: + return (value.value as string[]).map((str) => {str}) + case CoreSchemaType.Null: + return null + default: { + const _never: never = value.schemaName + return ( +
+					{JSON.stringify(value.value, null, 2)}
+				
+ ) + } + } + + // return
+ + //
{value.fieldName}
+ //
{value.value}
+ //
+} diff --git a/core/app/c/[communitySlug]/pubs/[pubId]/components/PubValues.tsx b/core/app/c/[communitySlug]/pubs/[pubId]/components/PubValues.tsx index 731e781e95..1121b86f2b 100644 --- a/core/app/c/[communitySlug]/pubs/[pubId]/components/PubValues.tsx +++ b/core/app/c/[communitySlug]/pubs/[pubId]/components/PubValues.tsx @@ -1,22 +1,9 @@ -"use client" +import type { ProcessedPubWithForm } from "contracts" -import type { JsonValue, ProcessedPubWithForm } from "contracts" -import type { ReactNode } from "react" -import type { InputTypeForCoreSchemaType } from "schemas" +import { type CommunityMembershipsId, CoreSchemaType } from "db/public" -import { useState } from "react" -import Link from "next/link" -import partition from "lodash.partition" - -import { CoreSchemaType } from "db/public" -import { Button } from "ui/button" -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "ui/collapsible" -import { ColorCircle, ColorLabel, ColorValue } from "ui/color" -import { ChevronDown, ChevronRight } from "ui/icon" -import { ShowMore } from "ui/show-more" - -import { FileUploadPreview } from "~/app/components/forms/FileUpload" -import { getPubTitle, valuesWithoutTitle } from "~/lib/pubs" +import { getMember } from "~/lib/server/user" +import { FieldBlock } from "./FieldBlock" type FullProcessedPubWithForm = ProcessedPubWithForm<{ withRelatedPubs: true @@ -25,80 +12,10 @@ type FullProcessedPubWithForm = ProcessedPubWithForm<{ withMembers: true }> -/** - * Get the label a form/pub value combo might have. In preference order: - * 1. "label" on a FormElement - * 2. "config.label" on a FormElement - * 3. the name of the PubField - **/ -const getLabel = (value: FullProcessedPubWithForm["values"][number]) => { - // Default to the field name - const defaultLabel = value.fieldName - let configLabel: string | null = null - let formElementLabel: string | null = null - if ("formElementId" in value) { - const config = value.formElementConfig - if (config) { - configLabel = "label" in config ? (config.label ?? null) : null - } - formElementLabel = value.formElementLabel - } - return formElementLabel || configLabel || defaultLabel -} - -const PubValueHeading = ({ - depth, - children, - ...props -}: React.HTMLAttributes & { depth: number }) => { - // For "Other Fields" section header which might be one lower than any pub depth - if (depth < 1) { - return

{children}

- } - // Pub depth starts at 1 - switch (depth - 1) { - case 0: - return

{children}

- case 1: - return

{children}

- case 2: - return

{children}

- default: - return
{children}
- } -} - -const FieldBlock = ({ - name, - values, - depth, -}: { - name: string - values: FullProcessedPubWithForm["values"] - depth: number -}) => { - return ( -
- - {name} - -
- {values.map((value) => - value.id ? ( - - ) : ( - // Blank space if there is no value -
- ) - )} -
-
- ) -} - -export const PubValues = ({ +export const PubValues = async ({ pub, isNested, + formSlug, }: { pub: FullProcessedPubWithForm /** @@ -108,150 +25,37 @@ export const PubValues = ({ * forms joined currently **/ isNested?: boolean -}): ReactNode => { - const { values, depth } = pub - if (!values.length) { - return null - } - - const filteredValues = valuesWithoutTitle(pub) - - // Group values by field so we only render one heading for relationship values that have multiple entries - const groupedValues: Record< - string, - { label: string; isInForm: boolean; values: FullProcessedPubWithForm["values"] } - > = {} - filteredValues.forEach((value) => { - if (groupedValues[value.fieldSlug]) { - groupedValues[value.fieldSlug].values.push(value) - } else { - const label = getLabel(value) - const isInForm = "formElementId" in value - groupedValues[value.fieldSlug] = { label, values: [value], isInForm } - } - }) - - const [valuesInForm, valuesNotInForm] = partition( - Object.values(groupedValues), - (values) => values.isInForm - ) - return ( -
- {valuesInForm.map(({ label, values }) => { - return - })} - {valuesNotInForm.length ? ( -
- {valuesInForm.length ?
: null} - {!isNested ? ( - - Other Fields - - ) : null} - {valuesNotInForm.map(({ label, values }) => ( - - ))} -
- ) : null} -
+ formSlug: string +}) => { + const valuesWithMembers = await Promise.all( + pub.values.map(async (val) => { + if (val.schemaName !== CoreSchemaType.MemberId) { + return val + } + + const member = await getMember(val.value as CommunityMembershipsId).executeTakeFirst() + + return { + ...val, + value: member, + } + }) ) -} - -const PubValue = ({ value }: { value: FullProcessedPubWithForm["values"][number] }) => { - const [isOpen, setIsOpen] = useState(false) - if (value.relatedPub) { - const { relatedPub, ...justValue } = value - const justValueElement = justValue.value ? ( - {}: - ) : null - if (relatedPub.isCycle) { - return ( - <> - {justValueElement} - {getPubTitle(value.relatedPub)} - Current pub - - ) - } - const renderRelatedValues = - value.relatedPub.depth < 3 && valuesWithoutTitle(relatedPub).length > 0 - return ( - -
- {justValueElement} - - {getPubTitle(value.relatedPub)} - - {renderRelatedValues && ( - - - - )} -
- - {renderRelatedValues && ( -
- -
- )} -
-
- ) - } - - if (value.schemaName === CoreSchemaType.FileUpload) { - return ( - } - /> - ) - } - - if (value.schemaName === CoreSchemaType.DateTime) { - const date = new Date(value.value as string) - if (date.toString() !== "Invalid Date") { - return date.toISOString().split("T")[0] - } - } - if (value.schemaName === CoreSchemaType.RichText) { - return ( - -
+ ) } diff --git a/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx b/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx index b7931a9c4e..1275f2e099 100644 --- a/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx +++ b/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx @@ -5,18 +5,23 @@ import Link from "next/link" import { notFound } from "next/navigation" import { BookOpen, Eye, Pencil } from "lucide-react" -import { Capabilities, type CommunitiesId, MembershipType, type PubsId } from "db/public" +import { + AutomationEvent, + Capabilities, + type CommunitiesId, + MembershipType, + type PubsId, +} from "db/public" import { Button } from "ui/button" import { PubFieldProvider } from "ui/pubFields" import { StagesProvider, stagesDAO } from "ui/stages" import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip" import { tryCatch } from "utils/try-catch" -import { PubsRunAutomationsDropDownMenu } from "~/app/components/ActionUI/PubsRunAutomationDropDownMenu" import { FormSwitcher } from "~/app/components/FormSwitcher/FormSwitcher" -import { AddMemberDialog } from "~/app/components/Memberships/AddMemberDialog" -import { MembersList } from "~/app/components/Memberships/MembersList" -import { CreatePubButton } from "~/app/components/pubs/CreatePubButton" +import { PubFormProvider } from "~/app/components/providers/PubFormProvider" +import { PubTypeLabel } from "~/app/components/pubs/PubCard/PubTypeLabel" +import { StageMoveButton } from "~/app/components/pubs/PubCard/StageMoveButton" import { RemovePubButton } from "~/app/components/pubs/RemovePubButton" import { getPageLoginData } from "~/lib/authentication/loginData" import { @@ -36,14 +41,7 @@ import { getPubFields } from "~/lib/server/pubFields" import { getStages } from "~/lib/server/stages" import { ContentLayout } from "../../ContentLayout" import Move from "../../stages/components/Move" -import { - addPubMember, - addUserWithPubMembership, - removePubMember, - setPubMemberRole, -} from "./actions" import { PubValues } from "./components/PubValues" -import { RelatedPubsTableWrapper } from "./components/RelatedPubsTableWrapper" const getPubsWithRelatedValuesCached = cache(async (pubId: PubsId, communityId: CommunitiesId) => { const [error, pub] = await tryCatch( @@ -118,7 +116,7 @@ export default async function Page(props: { communityId: community.id, userId: user.id, }, - { withAutomations: "full" } + { withAutomations: AutomationEvent.manual } ).execute() // We don't pass the userId here because we want to include related pubs regardless of authorization @@ -137,12 +135,12 @@ export default async function Page(props: { availableViewForms, availableUpdateForms, canArchive, - canRunActions, - canAddMember, - canRemoveMember, - canCreateRelatedPub, - canRunActionsAllPubs, - canOverrideAutomationConditions, + _canRunActions, + _canAddMember, + _canRemoveMember, + _canCreateRelatedPub, + _canRunActionsAllPubs, + _canOverrideAutomationConditions, communityStages, withExtraPubValues, form, @@ -208,8 +206,8 @@ export default async function Page(props: { return null } - const pubTypeHasRelatedPubs = pub.pubType.fields.some((field) => field.isRelation) - const pubHasRelatedPubs = pub.values.some((value) => !!value.relatedPub) + const _pubTypeHasRelatedPubs = pub.pubType.fields.some((field) => field.isRelation) + const _pubHasRelatedPubs = pub.values.some((value) => !!value.relatedPub) const { stage } = pub const pubByForm = getPubByForm({ pub, form, withExtraPubValues }) @@ -225,8 +223,8 @@ export default async function Page(props: { - -
+ +
{getPubTitle(pub)} @@ -239,12 +237,30 @@ export default async function Page(props: { {getPubTitle(pub)} -
- {pub.pubType.name}• +
+ + {pub.stage && ( + + } + /> + )} @@ -281,104 +297,20 @@ export default async function Page(props: { > -
-
+ +
- -
-
- {pub.stage ? ( -
-
Current Stage
-
- -
-
- ) : null} -
-
Actions
- {pub.stage?.automations && - pub.stage?.automations.length > 0 && - stage && - canRunActions ? ( -
- -
- ) : ( -
- Configure actions to run for this Pub in the stage - management settings -
- )} -
- -
-
- Members - {canAddMember && ( - member.id - )} - isSuperAdmin={user.isSuperAdmin} - membershipType={MembershipType.pub} - availableForms={availableViewForms} - /> - )} -
- -
+
- {(pubTypeHasRelatedPubs || pubHasRelatedPubs) && ( -
-

Related Pubs

- {canCreateRelatedPub && ( - - )} - -
- )} -
+ diff --git a/core/app/c/[communitySlug]/pubs/page.tsx b/core/app/c/[communitySlug]/pubs/page.tsx index ad44b07318..f0b578cd4d 100644 --- a/core/app/c/[communitySlug]/pubs/page.tsx +++ b/core/app/c/[communitySlug]/pubs/page.tsx @@ -1,15 +1,13 @@ import type { CommunitiesId } from "db/public" import type { Metadata } from "next" -import { Suspense } from "react" import Link from "next/link" import { BookOpen } from "lucide-react" import { Capabilities, MembershipType } from "db/public" import { Button } from "ui/button" -import { CreatePubButton } from "~/app/components/pubs/CreatePubButton" -import { SkeletonButton } from "~/app/components/skeletons/SkeletonButton" +import { MainCreatePubButton } from "~/app/components/pubs/CreatePubButton" import { getPageLoginData } from "~/lib/authentication/loginData" import { userCan, userCanCreateAnyPub } from "~/lib/authorization/capabilities" import { findCommunityBySlug } from "~/lib/server/community" @@ -52,7 +50,8 @@ export default async function Page(props: Props) { - Pubs + {" "} + Pubs } right={ @@ -63,12 +62,7 @@ export default async function Page(props: Props) { )} {canCreateAnyPub && ( - }> - - + )}
} diff --git a/core/app/c/[communitySlug]/settings/actions/[action]/ActionConfigDefaultForm.tsx b/core/app/c/[communitySlug]/settings/actions/[action]/ActionConfigDefaultForm.tsx index 9cd77a05f4..e6ba090a72 100644 --- a/core/app/c/[communitySlug]/settings/actions/[action]/ActionConfigDefaultForm.tsx +++ b/core/app/c/[communitySlug]/settings/actions/[action]/ActionConfigDefaultForm.tsx @@ -29,10 +29,7 @@ export const ActionConfigDefaultForm = (props: Props) => { async (values: z.infer) => { const result = await updateActionConfigDefault(props.action, values) if (didSucceed(result)) { - toast({ - title: "Success", - description: "Action config defaults updated", - }) + toast.success("Action config defaults updated") } }, [props.action] diff --git a/core/app/c/[communitySlug]/settings/actions/[action]/page.tsx b/core/app/c/[communitySlug]/settings/actions/[action]/page.tsx index 5d16670a90..58d982bd8a 100644 --- a/core/app/c/[communitySlug]/settings/actions/[action]/page.tsx +++ b/core/app/c/[communitySlug]/settings/actions/[action]/page.tsx @@ -63,12 +63,12 @@ export default async function Page(props: Props) { - + {actionTitle} Action Defaults } > -
+

Set default configuration values for the {actionTitle} action.
These diff --git a/core/app/c/[communitySlug]/settings/actions/page.tsx b/core/app/c/[communitySlug]/settings/actions/page.tsx index c961372400..c1dca97a6a 100644 --- a/core/app/c/[communitySlug]/settings/actions/page.tsx +++ b/core/app/c/[communitySlug]/settings/actions/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation" import { Activity } from "lucide-react" import { Capabilities, MembershipType } from "db/public" +import { Item, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from "ui/item" import { cn } from "utils" import { actions } from "~/actions/api" @@ -43,44 +44,49 @@ export default async function Page(_props: Props) { - + Action Settings } > -

+

Set default configuration values for your actions.
These defaults will be applied to new instances of actions in your community.

-
+ {Object.values(actions).map((action, idx) => ( - -
-
+ -

{action.name}

-
-

- {action.description} -

-
- + + + +

{action.name}

+
+ {action.description} +
+ + ))} -
+
) diff --git a/core/app/c/[communitySlug]/settings/legacy-migration/MigrationForm.tsx b/core/app/c/[communitySlug]/settings/legacy-migration/MigrationForm.tsx index 5df07c7de5..58ae87da60 100644 --- a/core/app/c/[communitySlug]/settings/legacy-migration/MigrationForm.tsx +++ b/core/app/c/[communitySlug]/settings/legacy-migration/MigrationForm.tsx @@ -57,10 +57,7 @@ export function MigrationForm() { const result = await runImportFromLegacy(data) if (didSucceed(result)) { - toast({ - title: "Import successful!", - description: "The import has been completed successfully.", - }) + toast.success("Import successful!") } } @@ -153,10 +150,7 @@ export function UndoMigrationForm({ }) if (didSucceed(result)) { - toast({ - title: "Migration undone", - description: "The migration has been undone", - }) + toast.success("Migration undone") setUndo(false) } @@ -350,7 +344,7 @@ export function UndoMigrationForm({ Undo Migration - + Undo Migration Select what data you want to be deleted diff --git a/core/app/c/[communitySlug]/settings/page.tsx b/core/app/c/[communitySlug]/settings/page.tsx index d6669f2320..c62aaa9578 100644 --- a/core/app/c/[communitySlug]/settings/page.tsx +++ b/core/app/c/[communitySlug]/settings/page.tsx @@ -37,7 +37,7 @@ export default async function Page(props: { params: Promise<{ communitySlug: str return (

Community Settings

-
+
  • diff --git a/core/app/c/[communitySlug]/settings/tokens/CreateTokenButton.tsx b/core/app/c/[communitySlug]/settings/tokens/CreateTokenButton.tsx index c431adcc6a..f1f2ede380 100644 --- a/core/app/c/[communitySlug]/settings/tokens/CreateTokenButton.tsx +++ b/core/app/c/[communitySlug]/settings/tokens/CreateTokenButton.tsx @@ -52,7 +52,7 @@ export const CreateTokenButton = ({ )} - + {!success ? ( <> diff --git a/core/app/c/[communitySlug]/settings/tokens/page.tsx b/core/app/c/[communitySlug]/settings/tokens/page.tsx index 46f81be514..9bb014a6ac 100644 --- a/core/app/c/[communitySlug]/settings/tokens/page.tsx +++ b/core/app/c/[communitySlug]/settings/tokens/page.tsx @@ -77,7 +77,8 @@ export default async function Page(_props: { params: { communitySlug: string } } - API Tokens + API + Tokens } right={} diff --git a/core/app/c/[communitySlug]/stages/[stageId]/page.tsx b/core/app/c/[communitySlug]/stages/[stageId]/page.tsx index dd10f1916e..ad8dc760f8 100644 --- a/core/app/c/[communitySlug]/stages/[stageId]/page.tsx +++ b/core/app/c/[communitySlug]/stages/[stageId]/page.tsx @@ -109,7 +109,7 @@ export default async function Page(props: { {stage.name} @@ -134,7 +134,7 @@ export default async function Page(props: {
} > -
+
} > diff --git a/core/app/c/[communitySlug]/stages/components/Move.tsx b/core/app/c/[communitySlug]/stages/components/Move.tsx index e462b6099c..92fab8ca27 100644 --- a/core/app/c/[communitySlug]/stages/components/Move.tsx +++ b/core/app/c/[communitySlug]/stages/components/Move.tsx @@ -9,6 +9,7 @@ import { Capabilities, MembershipType } from "db/public" import { getLoginData } from "~/lib/authentication/loginData" import { userCan } from "~/lib/authorization/capabilities" +import { getStage } from "~/lib/db/queries" import { makeStagesById } from "~/lib/stages" import { BasicMoveButton } from "./BasicMoveButton" import { MoveInteractive } from "./MoveInteractive" @@ -30,8 +31,8 @@ type Props = { } & XOR< { communityStages: CommunityStage[] }, { - moveFrom: CommunityStage["moveConstraintSources"] - moveTo: CommunityStage["moveConstraints"] + moveFrom: CommunityStage["moveConstraintSources"] | null + moveTo: CommunityStage["moveConstraints"] | null } > @@ -69,15 +70,21 @@ const getStageDisplayName = (props: Props) => { } async function MoveButton({ hideIfNowhereToMove = true, ...props }: Props) { - const { sources, destinations } = makeSourcesAndDestinations(props) const stageName = getStageDisplayName(props) + const loginData = await getLoginData() - if (destinations.length === 0 && sources.length === 0 && hideIfNowhereToMove) { + if (!loginData.user) { return } - const loginData = await getLoginData() - if (!loginData.user) { + const { moveConstraints: moveFrom, moveConstraintSources: moveTo } = + props.moveFrom && props.moveTo + ? { moveConstraints: props.moveTo, moveConstraintSources: props.moveFrom } + : ((await getStage(props.stageId, loginData.user.id).executeTakeFirst()) ?? {}) + + const { sources, destinations } = makeSourcesAndDestinations({ ...props, moveFrom, moveTo }) + + if (destinations?.length === 0 && sources?.length === 0 && hideIfNowhereToMove) { return } diff --git a/core/app/c/[communitySlug]/stages/components/MoveInteractive.tsx b/core/app/c/[communitySlug]/stages/components/MoveInteractive.tsx index 18d713f150..a248d5a843 100644 --- a/core/app/c/[communitySlug]/stages/components/MoveInteractive.tsx +++ b/core/app/c/[communitySlug]/stages/components/MoveInteractive.tsx @@ -9,7 +9,7 @@ import Link from "next/link" import { Button } from "ui/button" import { ArrowLeft, ArrowRight, FlagTriangleRightIcon } from "ui/icon" import { Popover, PopoverContent, PopoverTrigger } from "ui/popover" -import { useToast } from "ui/use-toast" +import { toast } from "ui/use-toast" import { move } from "~/app/c/[communitySlug]/stages/components/lib/actions" import { useCommunity } from "~/app/components/providers/CommunityProvider" @@ -42,7 +42,6 @@ export function MoveInteractive({ hideIfNowhereToMove, }: Props) { const [popoverIsOpen, setPopoverIsOpen] = useState(false) - const { toast } = useToast() const community = useCommunity() const [isMoving, startTransition] = useTransition() @@ -56,10 +55,7 @@ export function MoveInteractive({ return } - toast({ - title: "Success", - description: "Pub was successfully moved", - variant: "default", + toast("Pub was successfully moved", { action: ( - +
} > -
+
{ - + Create Type {isOpen && ( diff --git a/core/app/c/[communitySlug]/types/NewTypeForm.tsx b/core/app/c/[communitySlug]/types/NewTypeForm.tsx index 4cf11354b8..798d5ebf33 100644 --- a/core/app/c/[communitySlug]/types/NewTypeForm.tsx +++ b/core/app/c/[communitySlug]/types/NewTypeForm.tsx @@ -102,7 +102,7 @@ export const NewTypeForm = ({ fields: [], }) if (result && didSucceed(result)) { - toast({ title: `Type ${values.name} updated` }) + toast.success(`Type ${values.name} updated`) onSubmitSuccess(props.pubTypeId) return } @@ -118,7 +118,7 @@ export const NewTypeForm = ({ values.fields ) if (result && didSucceed(result)) { - toast({ title: `Type ${values.name} created` }) + toast.success(`Type ${values.name} created`) onSubmitSuccess(result.data.id) return } diff --git a/core/app/c/[communitySlug]/types/UpdatePubTypeDialog.tsx b/core/app/c/[communitySlug]/types/UpdatePubTypeDialog.tsx index 53b823a976..cc9b3a618f 100644 --- a/core/app/c/[communitySlug]/types/UpdatePubTypeDialog.tsx +++ b/core/app/c/[communitySlug]/types/UpdatePubTypeDialog.tsx @@ -31,7 +31,7 @@ export const UpdatePubTypeButton = ({ {children} - + Edit Type {isOpen && ( diff --git a/core/app/c/[communitySlug]/types/[pubTypeId]/edit/FieldBlock.tsx b/core/app/c/[communitySlug]/types/[pubTypeId]/edit/FieldBlock.tsx index 055cae4333..1a2d428698 100644 --- a/core/app/c/[communitySlug]/types/[pubTypeId]/edit/FieldBlock.tsx +++ b/core/app/c/[communitySlug]/types/[pubTypeId]/edit/FieldBlock.tsx @@ -48,14 +48,14 @@ export const FieldBlock = ({ const restoreRemoveButton = field.deleted ? ( <> -
Deleted on save
+
Deleted on save
- ID + ID
@@ -501,7 +494,7 @@ export const SelectField = ({ panelState }: { panelState: PanelState }) => { type="button" variant="outline" key={field.id} - className="flex h-[68px] flex-1 flex-shrink-0 justify-start gap-4 bg-white p-4" + className="flex h-[68px] flex-1 shrink-0 justify-start gap-4 bg-card p-4" onClick={() => { addElement({ id: field.id, @@ -529,7 +522,7 @@ export const SelectField = ({ panelState }: { panelState: PanelState }) => { ) }) return ( -
+
- Types + {" "} + Types } right={} diff --git a/core/app/components/ActionUI/AutomationRunForm.tsx b/core/app/components/ActionUI/AutomationRunForm.tsx index a6e8d61eb7..c4f3d5825a 100644 --- a/core/app/components/ActionUI/AutomationRunForm.tsx +++ b/core/app/components/ActionUI/AutomationRunForm.tsx @@ -48,16 +48,16 @@ export const AutomationRunForm = (props: Props) => { }) if (didSucceed(result)) { - toast({ - title: - "title" in result && typeof result.title === "string" - ? result.title - : `Successfully ran ${props.automation.name || action.name}`, - variant: "default", - description: ( -
{result.report}
- ), - }) + toast.success( + "title" in result && typeof result.title === "string" + ? result.title + : `Successfully ran ${props.automation.name || action.name}`, + { + description: ( +
{result.report}
+ ), + } + ) return } if ("issues" in result && result.issues) { @@ -110,7 +110,7 @@ export const AutomationRunForm = (props: Props) => { {props.automation.name} @@ -121,7 +121,7 @@ export const AutomationRunForm = (props: Props) => { {props.automation.name} diff --git a/core/app/components/CreateEditDialog.tsx b/core/app/components/CreateEditDialog.tsx index 3a81dd13f7..30dd2d8607 100644 --- a/core/app/components/CreateEditDialog.tsx +++ b/core/app/components/CreateEditDialog.tsx @@ -52,7 +52,7 @@ export const CreateEditDialog = ({ {trigger} - + {title} diff --git a/core/app/components/DataTable/DataTable.tsx b/core/app/components/DataTable/DataTable.tsx index bbc0444aeb..f4243d61ee 100644 --- a/core/app/components/DataTable/DataTable.tsx +++ b/core/app/components/DataTable/DataTable.tsx @@ -47,7 +47,7 @@ export interface DataTableProps { defaultSort?: SortingState } -const STRIPED_ROW_STYLING = "hover:bg-gray-100 data-[state=selected]:bg-sky-50" +const STRIPED_ROW_STYLING = "hover:bg-muted/50 data-[state=selected]:bg-muted" export function DataTable({ columns, @@ -157,7 +157,7 @@ export function DataTable({ className={cn([ isNotDefaultSize ? "overflow-clip" - : "max-w-[12rem] overflow-auto", + : "max-w-48 overflow-auto", ])} style={ isNotDefaultSize @@ -202,7 +202,7 @@ export function DataTable({ // data-testid={getRowId?.(row.original)} className={cn({ "cursor-pointer": Boolean(onRowClick), - "bg-gray-100/50": striped && idx % 2, + "bg-muted/50": striped && idx % 2, [STRIPED_ROW_STYLING]: striped, })} > @@ -215,7 +215,7 @@ export function DataTable({ className={cn([ isNotDefaultSize ? "overflow-clip" - : "max-w-[12rem] overflow-auto", + : "max-w-48 overflow-auto", ])} style={ isNotDefaultSize diff --git a/core/app/components/EllipsisMenu.tsx b/core/app/components/EllipsisMenu.tsx index 2e2e77e22c..971f43fa57 100644 --- a/core/app/components/EllipsisMenu.tsx +++ b/core/app/components/EllipsisMenu.tsx @@ -81,7 +81,7 @@ export const EllipsisMenu = ({ size={triggerSize} className={cn( "h-8 w-8 p-0", - "hover:bg-gray-100 focus:bg-gray-100", + "hover:bg-muted focus-visible:bg-muted", "transition-colors duration-150", triggerClassName )} diff --git a/core/app/components/FormBuilder/ElementPanel/ComponentConfig/ColorPicker.tsx b/core/app/components/FormBuilder/ElementPanel/ComponentConfig/ColorPicker.tsx index 07c66cb627..3d9e900d4d 100644 --- a/core/app/components/FormBuilder/ElementPanel/ComponentConfig/ColorPicker.tsx +++ b/core/app/components/FormBuilder/ElementPanel/ComponentConfig/ColorPicker.tsx @@ -42,14 +42,14 @@ export const FormBuilderColorPickerPopover = ({ } return ( -
+
- + {isEditingLabel ? ( {label || color} - {color} + {color}
)} diff --git a/core/app/components/FormBuilder/ElementPanel/ComponentConfig/RelationBlock.tsx b/core/app/components/FormBuilder/ElementPanel/ComponentConfig/RelationBlock.tsx index 9d59170c0c..e69437d378 100644 --- a/core/app/components/FormBuilder/ElementPanel/ComponentConfig/RelationBlock.tsx +++ b/core/app/components/FormBuilder/ElementPanel/ComponentConfig/RelationBlock.tsx @@ -58,7 +58,7 @@ export default ({ form }: ComponentConfigFormProps value: pubType.id, label: pubType.name, node: ( - + {pubType.name} ), @@ -66,7 +66,7 @@ export default ({ form }: ComponentConfigFormProps placeholder="Select a Pub Type" onValueChange={(value) => field.onChange(value)} animation={0} - badgeClassName="bg-blue-200 text-blue-400 rounded-sm font-mono font-normal border border-blue-400 whitespace-nowrap" + badgeClassName="bg-blue-200 text-blue-400 rounded-xs font-mono font-normal border border-blue-400 whitespace-nowrap" defaultValue={field.value ?? []} data-testid="related-pub-type-selector" /> diff --git a/core/app/components/FormBuilder/ElementPanel/InputComponentConfigurationForm.tsx b/core/app/components/FormBuilder/ElementPanel/InputComponentConfigurationForm.tsx index fa3fbae192..7508fd2cd5 100644 --- a/core/app/components/FormBuilder/ElementPanel/InputComponentConfigurationForm.tsx +++ b/core/app/components/FormBuilder/ElementPanel/InputComponentConfigurationForm.tsx @@ -100,7 +100,7 @@ const componentInfo: Record = { [InputComponent.checkboxGroup]: { name: "Checkbox Group", demoComponent: () => ( -
+
Label
@@ -120,7 +120,7 @@ const componentInfo: Record = { [InputComponent.radioGroup]: { name: "Radio Group", demoComponent: () => ( - +
Label
@@ -140,7 +140,7 @@ const componentInfo: Record = { [InputComponent.selectDropdown]: { name: "Select Dropdown", demoComponent: () => ( -
+
Label
- - - - - + + + {Object.values(FormAccessType).map((t) => { const { Icon, description, name } = iconsAndCopy[t] @@ -47,11 +47,11 @@ export const SelectAccess = () => { value={t.toString()} data-testid={`select-form-access-${t}`} > -
+
{name}
-
+
{description}
diff --git a/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx b/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx index 12f4936a79..ebda49d8d7 100644 --- a/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx +++ b/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx @@ -44,7 +44,7 @@ export const SelectElement = ({ panelState }: { panelState: PanelState }) => { type="button" variant="outline" key={field.id} - className="flex h-[68px] flex-1 flex-shrink-0 justify-start gap-4 bg-white p-4" + className="flex h-[68px] flex-1 shrink-0 justify-start gap-4 bg-card p-4" onClick={() => { addElement({ fieldId: field.id, @@ -80,7 +80,7 @@ export const SelectElement = ({ panelState }: { panelState: PanelState }) => { ) }) return ( - + Field @@ -89,10 +89,7 @@ export const SelectElement = ({ panelState }: { panelState: PanelState }) => { Structure - + { Cancel - +
{Object.values(StructuralFormElement).map((elementType) => { const { Icon, enabled, name } = structuralElements[elementType] diff --git a/core/app/components/FormBuilder/ElementPanel/StructuralElementConfigurationForm.tsx b/core/app/components/FormBuilder/ElementPanel/StructuralElementConfigurationForm.tsx index 350fd0225d..22c693585f 100644 --- a/core/app/components/FormBuilder/ElementPanel/StructuralElementConfigurationForm.tsx +++ b/core/app/components/FormBuilder/ElementPanel/StructuralElementConfigurationForm.tsx @@ -48,13 +48,13 @@ export const StructuralElementConfigurationForm = ({ index, structuralElement }: return (
{ e.stopPropagation() //prevent submission from propagating to parent form form.handleSubmit(onSubmit)(e) }} > -
+
{(schema.keyof().options as string[]).map((name) => ( { Add New
- Slug + Slug
diff --git a/core/app/components/FormBuilder/FormBuilder.tsx b/core/app/components/FormBuilder/FormBuilder.tsx index 9e38cd9fd5..37feada607 100644 --- a/core/app/components/FormBuilder/FormBuilder.tsx +++ b/core/app/components/FormBuilder/FormBuilder.tsx @@ -21,7 +21,6 @@ import { formElementsInitializerSchema } from "db/public" import { logger } from "logger" import { Form, FormControl, FormField, FormItem } from "ui/form" import { useUnsavedChangesWarning } from "ui/hooks" -import { CircleCheck } from "ui/icon" import { Tabs, TabsContent, TabsList, TabsTrigger } from "ui/tabs" import { TokenProvider } from "ui/tokens" import { toast } from "ui/use-toast" @@ -259,14 +258,7 @@ export function FormBuilder({ pubForm, id, stages }: Props) { const onSubmit = async (_formData: FormBuilderSchema) => { const result = await runSaveForm(payload) if (didSucceed(result)) { - toast({ - className: "rounded border-emerald-100 bg-emerald-50", - action: ( -
- Form Successfully Saved -
- ), - }) + toast.success("Form saved") } } const addElement = useCallback( diff --git a/core/app/components/FormBuilder/FormElement.tsx b/core/app/components/FormBuilder/FormElement.tsx index 466f3c5ff4..bc5e73142b 100644 --- a/core/app/components/FormBuilder/FormElement.tsx +++ b/core/app/components/FormBuilder/FormElement.tsx @@ -47,12 +47,12 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme const restoreRemoveButton = element.deleted ? ( <> -
Deleted on save
+
Deleted on save
- + {props.title} {isOpen && }>{props.children}} diff --git a/core/app/components/ResultsPerPageInput.tsx b/core/app/components/ResultsPerPageInput.tsx index 01fdcca298..5234ab5cbd 100644 --- a/core/app/components/ResultsPerPageInput.tsx +++ b/core/app/components/ResultsPerPageInput.tsx @@ -23,7 +23,7 @@ export const ResultsPerPageInput = () => { setPaging({ perPage: parseInt(value, 10) }, { shallow: false }) }} > - + diff --git a/core/app/components/SearchDialog.tsx b/core/app/components/SearchDialog.tsx deleted file mode 100644 index cbf161b8cb..0000000000 --- a/core/app/components/SearchDialog.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client" - -import type { fullTextSearch } from "~/lib/server/pub" - -import { useState } from "react" -import { useRouter } from "next/navigation" -import { useQuery } from "@tanstack/react-query" -import { useDebounce } from "use-debounce" - -import { - Command, - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "ui/command" -import { useKeyboardShortcut } from "ui/hooks" -import { AlertCircle, Loader2 } from "ui/icon" - -import { client } from "~/lib/api" -import { getPubTitle } from "~/lib/pubs" -import { useCommunity } from "./providers/CommunityProvider" - -interface SearchDialogProps { - defaultOpen?: boolean - onOpenChange?: (open: boolean) => void - onPubSelect?: (pub: any) => void -} - -export function SearchDialog({ defaultOpen, onOpenChange, onPubSelect }: SearchDialogProps) { - const [open, setOpen] = useState(defaultOpen) - const [query, setQuery] = useState("") - const [debouncedQuery] = useDebounce(query, 300) - const community = useCommunity() - - const { - data: results = { status: 400, body: [] }, - error, - isError, - isFetching, - } = useQuery({ - queryKey: ["pubs", "search", community.id, debouncedQuery], - queryFn: () => - client.pubs.search.query({ - query: { query: debouncedQuery }, - params: { communitySlug: community.slug }, - }), - enabled: debouncedQuery.length > 0, - placeholderData: (prev, prevQuery) => { - if (prevQuery?.state.error || prevQuery?.state.data?.status !== 200) { - return prevQuery?.state.data - } - return prev - }, - }) - - useKeyboardShortcut("Mod+k", () => { - onOpenChange?.(true) - setOpen(true) - }) - - const router = useRouter() - - return ( - - - - - {results.status === 200 && results.body.length === 0 && ( - - {isFetching ? ( - - ) : isError ? ( - - ) : ( - "No results found" - )} - - )} - - {results.status === 200 && - results.body.map( - (pub: Awaited>[number]) => ( - { - router.push(`/c/${community.slug}/pubs/${pub.id}`) - setOpen(false) - }} - className="flex flex-col items-start gap-1 py-3" - > -
- {pub.pubType.name} - {pub.stage && ( - <> - • - {pub.stage.name} - - )} -
-
- - {/* Matching values that aren't titles */} - {pub.matchingValues - .filter((match) => !match.isTitle) - .map((match, idx) => ( -
- - {match.name}: - - -
- ))} - - ) - )} - - - - - ) -} diff --git a/core/app/components/SearchDialogTrigger.tsx b/core/app/components/SearchDialogTrigger.tsx new file mode 100644 index 0000000000..507180d7dc --- /dev/null +++ b/core/app/components/SearchDialogTrigger.tsx @@ -0,0 +1,31 @@ +"use client" + +import type { ComponentProps } from "react" + +import { Button, type ButtonProps } from "ui/button" +import { Search } from "ui/icon" +import { SidebarMenuButton } from "ui/sidebar" + +import { useSearchDialog } from "./search/SearchDialog" + +export function SearchDialogTrigger(props: ButtonProps) { + const { open } = useSearchDialog() + + return ( + + ) +} + +export function SidebarSearchDialogTrigger(props: ComponentProps) { + const { open } = useSearchDialog() + + return ( + + + Search pubs + + ) +} diff --git a/core/app/components/SidePanel.tsx b/core/app/components/SidePanel.tsx index b72fc55a00..49c32aebbb 100644 --- a/core/app/components/SidePanel.tsx +++ b/core/app/components/SidePanel.tsx @@ -31,7 +31,7 @@ export const PanelHeader = ({ return ( <>
-
{title}
+
{title}
{showCancel && ( diff --git a/core/app/components/forms/elements/DateElement.tsx b/core/app/components/forms/elements/DateElement.tsx index 6fd8c4a3c9..bcdeaad4e6 100644 --- a/core/app/components/forms/elements/DateElement.tsx +++ b/core/app/components/forms/elements/DateElement.tsx @@ -33,7 +33,7 @@ export const DateElement = ({ slug, label, config }: ElementProps ( - + {label} () const formElementToggle = useFormElementToggleContext() @@ -73,11 +76,7 @@ export const FileUploadElement = ({ } field.onChange(field.value.filter((f) => f.fileName !== file.fileName)) - toast({ - title: "Successfully removed file", - variant: "success", - description: res?.report, - }) + toast.success("Removed file") }, [runDelete, slug, pubId, form.slug, mode] ) @@ -99,6 +98,7 @@ export const FileUploadElement = ({ { diff --git a/core/app/components/forms/elements/RelatedPubsElement.tsx b/core/app/components/forms/elements/RelatedPubsElement.tsx index 2f2d2570ce..b35fa520e9 100644 --- a/core/app/components/forms/elements/RelatedPubsElement.tsx +++ b/core/app/components/forms/elements/RelatedPubsElement.tsx @@ -79,7 +79,7 @@ const RelatedPubBlock = ({
{/* Max width to keep long 'value's truncated. 90% to leave room for the trash button */}
diff --git a/core/app/components/pubs/CreatePubButton.tsx b/core/app/components/pubs/CreatePubButton.tsx index 3199e48b50..4c623687e2 100644 --- a/core/app/components/pubs/CreatePubButton.tsx +++ b/core/app/components/pubs/CreatePubButton.tsx @@ -2,6 +2,8 @@ import type { CommunitiesId, PubsId, PubTypesId, StagesId } from "db/public" import type { ButtonProps } from "ui/button" import type { PubTypeWithForm } from "~/lib/authorization/capabilities" +import { Suspense } from "react" + import { Plus } from "ui/icon" import { getLoginData } from "~/lib/authentication/loginData" @@ -11,6 +13,7 @@ import { findCommunityBySlug } from "~/lib/server/community" import { getPubFields } from "~/lib/server/pubFields" import { ContextEditorContextProvider } from "../ContextEditor/ContextEditorContext" import { PathAwareDialog } from "../PathAwareDialog" +import { SkeletonButton } from "../skeletons/SkeletonButton" import { InitialCreatePubForm } from "./InitialCreatePubForm" type RelatedPubData = { @@ -136,3 +139,11 @@ export const CreatePubButton = async (props: Props) => { ) } + +export const MainCreatePubButton = (props: Props) => { + return ( + }> + + + ) +} diff --git a/core/app/components/pubs/FormPubSearchSelect.tsx b/core/app/components/pubs/FormPubSearchSelect.tsx index bcf330ac53..a683bcf113 100644 --- a/core/app/components/pubs/FormPubSearchSelect.tsx +++ b/core/app/components/pubs/FormPubSearchSelect.tsx @@ -137,7 +137,7 @@ export const FormPubSearchSelect = ({
@@ -167,7 +167,7 @@ export const FormPubSearchSelect = ({ )} {showEmpty && ( -
+
{emptyMessage}
)} diff --git a/core/app/components/pubs/PubCard/PubCard.tsx b/core/app/components/pubs/PubCard/PubCard.tsx index ba224acdb0..ab0740d47b 100644 --- a/core/app/components/pubs/PubCard/PubCard.tsx +++ b/core/app/components/pubs/PubCard/PubCard.tsx @@ -9,12 +9,12 @@ import Link from "next/link" import { Capabilities, MembershipType } from "db/public" import { Button } from "ui/button" import { Card, CardContent, CardDescription, CardFooter, CardTitle } from "ui/card" -import { Calendar, History, Pencil, Trash2 } from "ui/icon" +import { Pencil, Trash2 } from "ui/icon" import { cn } from "utils" +import { DateTimeDisplay } from "~/app/c/[communitySlug]/pubs/[pubId]/components/DateTimeDisplay" import Move from "~/app/c/[communitySlug]/stages/components/Move" import { userCan, userCanEditPub } from "~/lib/authorization/capabilities" -import { formatDateAsMonthDayYear, formatDateAsPossiblyDistance } from "~/lib/dates" import { getPubTitle } from "~/lib/pubs" import { PubSelector } from "../../../c/[communitySlug]/pubs/PubSelector" import { PubsRunAutomationsDropDownMenu } from "../../ActionUI/PubsRunAutomationDropDownMenu" @@ -85,9 +85,9 @@ export const PubCard = async ({ const hasActions = pub.stage && manualAutomations && manualAutomations.length !== 0 return ( a:focus]:border-black has-[h3>a:focus]:ring-2 has-[h3>a:focus]:ring-gray-200" )} @@ -142,7 +142,7 @@ export const PubCard = async ({ {showMatchingValues && (
@@ -160,18 +160,12 @@ export const PubCard = async ({ ))}
)} - -
- - {formatDateAsMonthDayYear(new Date(pub.createdAt))} -
-
- - {formatDateAsPossiblyDistance(new Date(pub.updatedAt))} -
+ + + -
+
{/* We use grid and order-[x] to place items according to the design, but PubsRunActionDropDownMenu needs to be first so it can have `peer`. The other buttons check if the `peer` is open, and if it is, it does not lose opacity. diff --git a/core/app/components/pubs/PubCard/PubCardClient.tsx b/core/app/components/pubs/PubCard/PubCardClient.tsx index 6a70a5ef63..ba9fb0da03 100644 --- a/core/app/components/pubs/PubCard/PubCardClient.tsx +++ b/core/app/components/pubs/PubCard/PubCardClient.tsx @@ -4,14 +4,15 @@ import type { ProcessedPub } from "contracts" import { useCallback } from "react" -import { Badge } from "ui/badge" import { Card, CardFooter, CardTitle } from "ui/card" import { Checkbox } from "ui/checkbox" import { Calendar, History } from "ui/icon" import { cn } from "utils" +import { BasicMoveButton } from "~/app/c/[communitySlug]/stages/components/BasicMoveButton" import { formatDateAsMonthDayYear, formatDateAsPossiblyDistance } from "~/lib/dates" import { getPubTitle } from "~/lib/pubs" +import { PubTypeLabel } from "./PubTypeLabel" export type PubCardClientProps = { pub: ProcessedPub<{ withPubType: true; withStage?: boolean }> @@ -45,7 +46,7 @@ export const PubCardClient = ({ return ( -
+
- - {pub.pubType.name} - - {pub.stage && ( - - {pub.stage.name} - - )} + + {pub.stage && }
- -

-
-

+ +

{showMatchingValues && (
@@ -107,7 +100,7 @@ export const PubCardClient = ({
{showCheckbox && ( -
+
{pubType.name} @@ -54,7 +54,7 @@ export const FilterablePubTypeLabel = ({ @@ -78,7 +80,7 @@ export const RelationsDropDown = ({ pubId, numRelations }: Props) => { {relatedPub.pubType.name} diff --git a/core/app/components/pubs/RemovePubFormClient.tsx b/core/app/components/pubs/RemovePubFormClient.tsx index 37c4972d54..b44c7b6484 100644 --- a/core/app/components/pubs/RemovePubFormClient.tsx +++ b/core/app/components/pubs/RemovePubFormClient.tsx @@ -51,10 +51,7 @@ export const PubRemoveForm = ({ pubId, redirectTo }: PubRemoveProps) => { }) if (result && "success" in result) { - toast({ - title: "Success", - description: result.report, - }) + toast.success(result.report) closeForm() } } diff --git a/core/app/components/search/SearchDialog.tsx b/core/app/components/search/SearchDialog.tsx new file mode 100644 index 0000000000..78bfda1baf --- /dev/null +++ b/core/app/components/search/SearchDialog.tsx @@ -0,0 +1,124 @@ +"use client" + +import type { ProcessedPub } from "contracts" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { useDebounce } from "use-debounce" + +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "ui/command" +import { useKeyboardShortcut } from "ui/hooks" +import { AlertCircle, Loader2 } from "ui/icon" + +import { client } from "~/lib/api" +import { useCommunity } from "../providers/CommunityProvider" +import { PubCardClient } from "../pubs/PubCard/PubCardClient" +import { useSearchDialog } from "./SearchDialogContext" + +export function SearchDialog() { + const { isOpen, close, open } = useSearchDialog() + const [query, setQuery] = useState("") + const [debouncedQuery] = useDebounce(query, 300) + const community = useCommunity() + + const { + data: results = { status: 400, body: [] }, + error, + isError, + isFetching, + isFetched, + } = client.pubs.getMany.useQuery({ + queryKey: ["pubs", "search", community.id, debouncedQuery], + queryData: { + query: { + search: debouncedQuery, + depth: 1, + withPubType: true, + withSearchValues: true, + withValues: false, + withStage: true, + limit: 10, + offset: 0, + orderBy: "updatedAt", + orderDirection: "desc", + }, + params: { communitySlug: community.slug }, + }, + enabled: debouncedQuery.length > 0, + placeholderData: (prev, prevQuery) => { + if (prevQuery?.state.error || prevQuery?.state.data?.status !== 200) { + return prevQuery?.state.data + } + return prev + }, + }) + + useKeyboardShortcut("Mod+k", open) + + const router = useRouter() + + const handleOpenChange = (newOpen: boolean) => { + if (newOpen) { + return + } + close() + } + + return ( + + + + {results.status === 200 && results.body.length === 0 && ( + + {isFetching ? ( + + ) : isError ? ( + + ) : ( + "No results found" + )} + + )} + + {results.status === 200 && + ( + results.body as ProcessedPub<{ + withValues: false + withPubType: true + withStage: true + }>[] + ).map((pub) => ( + { + router.push(`/c/${community.slug}/pubs/${pub.id}`) + close() + }} + className="flex flex-col items-start gap-1 py-3" + asChild + > + + + ))} + + + + ) +} diff --git a/core/app/components/search/SearchDialogContext.tsx b/core/app/components/search/SearchDialogContext.tsx new file mode 100644 index 0000000000..9a8435bcca --- /dev/null +++ b/core/app/components/search/SearchDialogContext.tsx @@ -0,0 +1,21 @@ +"use client" + +import { createContext, useContext } from "react" + +export interface SearchDialogContextValue { + open: () => void + close: () => void + isOpen: boolean +} + +export const SearchDialogContext = createContext(null) + +export function useSearchDialog() { + const context = useContext(SearchDialogContext) + + if (!context) { + throw new Error("useSearchDialog must be used within SearchDialogProvider") + } + + return context +} diff --git a/core/app/components/search/SearchDialogProvider.tsx b/core/app/components/search/SearchDialogProvider.tsx new file mode 100644 index 0000000000..a4ab6541e5 --- /dev/null +++ b/core/app/components/search/SearchDialogProvider.tsx @@ -0,0 +1,27 @@ +"use client" + +import { type ReactNode, useState } from "react" + +import { SearchDialog } from "./SearchDialog" +import { SearchDialogContext, type SearchDialogContextValue } from "./SearchDialogContext" + +interface SearchDialogProviderProps { + children: ReactNode +} + +export function SearchDialogProvider({ children }: SearchDialogProviderProps) { + const [isOpen, setIsOpen] = useState(false) + + const value: SearchDialogContextValue = { + open: () => setIsOpen(true), + close: () => setIsOpen(false), + isOpen, + } + + return ( + + {children} + + + ) +} diff --git a/core/app/components/search/SearchDialogTrigger.tsx b/core/app/components/search/SearchDialogTrigger.tsx new file mode 100644 index 0000000000..3ad4aecc7e --- /dev/null +++ b/core/app/components/search/SearchDialogTrigger.tsx @@ -0,0 +1,31 @@ +"use client" + +import type { ComponentProps } from "react" + +import { Button, type ButtonProps } from "ui/button" +import { Search } from "ui/icon" +import { SidebarMenuButton } from "ui/sidebar" + +import { useSearchDialog } from "./SearchDialogContext" + +export function SearchDialogTrigger(props: ButtonProps) { + const { open } = useSearchDialog() + + return ( + + ) +} + +export function SidebarSearchDialogTrigger(props: ComponentProps) { + const { open } = useSearchDialog() + + return ( + + + Search pubs + + ) +} diff --git a/core/app/components/theme/DarkmodeToggle.tsx b/core/app/components/theme/DarkmodeToggle.tsx new file mode 100644 index 0000000000..283ae6559a --- /dev/null +++ b/core/app/components/theme/DarkmodeToggle.tsx @@ -0,0 +1,63 @@ +"use client" + +import { Computer, Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" + +import { Button } from "ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "ui/dropdown-menu" +import { SidebarMenuButton } from "ui/sidebar" + +export function DarkmodeToggle({ children }: { children?: React.ReactNode }) { + const { setTheme } = useTheme() + + return ( + + + {children ?? ( + + )} + + + setTheme("light")}>Light + setTheme("dark")}>Dark + setTheme("system")}>System + + + ) +} + +export function SidebarDarkmodeToggle() { + const { theme } = useTheme() + + return ( + + + {theme === "dark" ? ( + <> + + Dark + + ) : theme === "light" ? ( + <> + + Light + + ) : ( + <> + + System + + )} + + + ) +} diff --git a/core/app/components/theme/ThemeProvider.tsx b/core/app/components/theme/ThemeProvider.tsx new file mode 100644 index 0000000000..0bdfc04886 --- /dev/null +++ b/core/app/components/theme/ThemeProvider.tsx @@ -0,0 +1,10 @@ +"use client" + +import { ThemeProvider as NextThemesProvider } from "next-themes" + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children} +} diff --git a/core/app/globals.css b/core/app/globals.css index 8822420762..3b42f32a49 100644 --- a/core/app/globals.css +++ b/core/app/globals.css @@ -1,6 +1,11 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; + +@import "@pubpub/tailwind/style.css"; + +@source "../../packages/ui/src/**/*.{ts,tsx}"; /* Not clear why we need this here if we also import ui/styles which includes it */ + +/* @custom-variant dark (&:is(.dark *)); */ +@custom-variant dark (&:where(.dark, .dark *)); diff --git a/core/app/layout.tsx b/core/app/layout.tsx index e997eae50d..b7a8848cee 100644 --- a/core/app/layout.tsx +++ b/core/app/layout.tsx @@ -1,18 +1,18 @@ import { NuqsAdapter } from "nuqs/adapters/next/app" -import "ui/styles.css" +import "./globals.css" -import { Suspense } from "react" import Script from "next/script" import { KeyboardShortcutProvider } from "ui/hooks" +import { Toaster } from "ui/toaster" import { TooltipProvider } from "ui/tooltip" import { getLoginData } from "~/lib/authentication/loginData" import { env } from "~/lib/env/env" import { ReactQueryProvider } from "./components/providers/QueryProvider" import { UserProvider } from "./components/providers/UserProvider" -import { RootToaster } from "./RootToaster" +import { ThemeProvider } from "./components/theme/ThemeProvider" export const metadata = { title: "PubPub Platform", @@ -22,7 +22,7 @@ export const metadata = { export default async function RootLayout({ children }: { children: React.ReactNode }) { const loginData = await getLoginData() return ( - + {env.NODE_ENV === "development" && (