Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ import {
ToggleGroup,
useTable,
} from "@dub/ui";
import { cn, currencyFormatter, fetcher, formatDateTime } from "@dub/utils";
import {
cn,
currencyFormatter,
fetcher,
formatDateTime,
pluralize,
} from "@dub/utils";
import { CommissionType } from "@prisma/client";
import { useAction } from "next-safe-action/hooks";
import Link from "next/link";
Expand Down Expand Up @@ -536,14 +542,17 @@ function CreateCommissionSheetContent({
</h2>
</label>
<ToggleGroup
className="mt-2 flex w-full items-center gap-1 rounded-md border border-neutral-200 bg-neutral-50 p-1"
className="mt-2 flex w-full items-center gap-1 rounded-lg border border-neutral-200 bg-neutral-50 p-1"
optionClassName="h-8 flex items-center justify-center rounded-md flex-1 text-sm normal-case"
indicatorClassName="bg-white"
options={[
{ value: "new", label: "Create new events" },
{
value: "new",
label: "Create new event",
},
{
value: "existing",
label: "Use existing events",
label: `Use existing ${pluralize("event", filteredEvents.length)}`,
},
]}
selected={useExistingEvents ? "existing" : "new"}
Expand Down Expand Up @@ -647,18 +656,25 @@ function CreateCommissionSheetContent({

{hasCustomLeadEventName && (
<div className="p-px">
<label
htmlFor="leadEventName"
className="block text-sm font-medium text-neutral-900"
>
Lead event name
</label>
<input
id="leadEventName"
type="text"
className={cn(
"block w-full rounded-md border-neutral-300 px-3 py-2 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm",
"mt-2 block w-full rounded-md border-neutral-300 px-3 py-2 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm",
errors.leadEventName &&
"border-red-600 focus:border-red-500 focus:ring-red-600",
)}
{...register("leadEventName", {
setValueAs: (value) =>
value === "" ? null : value,
})}
placeholder="Enter lead event name"
placeholder="Sign up"
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function PartnerStats({
return (
<As
key={label}
href={href ?? ""}
href={href ?? "#"}
target="_blank"
className="flex flex-col bg-white p-3 transition-colors duration-150 hover:bg-neutral-50"
>
Expand Down
47 changes: 21 additions & 26 deletions apps/web/ui/customers/customer-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import useCustomers from "@/lib/swr/use-customers";
import { CUSTOMERS_MAX_PAGE_SIZE } from "@/lib/zod/schemas/customers";
import { Button, Combobox } from "@dub/ui";
import { Combobox } from "@dub/ui";
import { cn, OG_AVATAR_URL } from "@dub/utils";
import { useEffect, useMemo, useState } from "react";
import { useDebounce } from "use-debounce";
Expand Down Expand Up @@ -44,16 +44,18 @@ export function CustomerSelector({
});

const customerOptions = useMemo(() => {
return customers?.map((customer) => ({
value: customer.id,
label: customer.name || customer.email || customer.externalId,
icon: (
<img
src={customer.avatar || `${OG_AVATAR_URL}${customer.id}`}
className="size-4 rounded-full"
/>
),
}));
return (
customers?.map((customer) => ({
value: customer.id,
label: customer.name || customer.email || customer.externalId,
icon: (
<img
src={customer.avatar || `${OG_AVATAR_URL}${customer.id}`}
className="size-4 rounded-full"
/>
),
})) || []
);
}, [customers]);

const selectedOption = useMemo(() => {
Expand Down Expand Up @@ -89,8 +91,15 @@ export function CustomerSelector({
icon={selectedCustomersLoading ? null : selectedOption?.icon}
caret={true}
placeholder={selectedCustomersLoading ? "" : "Select customer"}
searchPlaceholder="Search customer..."
searchPlaceholder="Search or create customer..."
onSearchChange={setSearch}
createLabel={(search) =>
`Create ${search ? `"${search}"` : "new customer"}`
}
onCreate={async () => {
setShowAddCustomerModal(true);
return true;
}}
shouldFilter={!useAsync}
matchTriggerWidth
open={openPopover}
Expand All @@ -102,20 +111,6 @@ export function CustomerSelector({
"focus:ring-1 focus:ring-neutral-500 focus:border-neutral-500 transition-none",
),
}}
emptyState={
<div className="flex w-full flex-col items-center gap-2 py-4">
No customers found
<Button
onClick={() => {
setOpenPopover(false);
setShowAddCustomerModal(true);
}}
variant="primary"
className="h-7 w-fit px-2"
text="Create customer"
/>
</div>
}
>
{selectedCustomersLoading ? (
<div className="my-0.5 h-5 w-1/3 animate-pulse rounded bg-neutral-200" />
Expand Down
1 change: 1 addition & 0 deletions apps/web/ui/links/link-builder/tag-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export const TagSelect = memo(() => {
selectedTags.length === 0 && "text-neutral-400",
),
}}
createLabel={(search) => `Create ${search ? `"${search}"` : "new tag"}`}
onCreate={(search) => createTag(search)}
open={isOpen}
onOpenChange={setIsOpen}
Expand Down
46 changes: 41 additions & 5 deletions apps/web/ui/modals/add-customer-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import useWorkspace from "@/lib/swr/use-workspace";
import { CustomerProps } from "@/lib/types";
import { createCustomerBodySchema } from "@/lib/zod/schemas/customers";
import { Button, Modal, useMediaQuery } from "@dub/ui";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { mutate } from "swr";
Expand All @@ -12,6 +12,7 @@ interface AddCustomerModalProps {
showModal: boolean;
setShowModal: (showModal: boolean) => void;
onSuccess?: (customer: CustomerProps) => void;
initialName?: string;
}

type FormData = z.infer<typeof createCustomerBodySchema>;
Expand All @@ -20,6 +21,7 @@ const AddCustomerModal = ({
showModal,
setShowModal,
onSuccess,
initialName,
}: AddCustomerModalProps) => {
const { id: workspaceId } = useWorkspace();
const { isMobile } = useMediaQuery();
Expand All @@ -28,6 +30,7 @@ const AddCustomerModal = ({
register,
handleSubmit,
watch,
reset,
formState: { isSubmitting },
} = useForm<FormData>({
defaultValues: {
Expand All @@ -38,6 +41,24 @@ const AddCustomerModal = ({
},
});

useEffect(() => {
if (showModal && initialName) {
reset({
name: initialName,
email: null,
externalId: "",
stripeCustomerId: null,
});
} else if (showModal) {
reset({
name: null,
email: null,
externalId: "",
stripeCustomerId: null,
});
}
}, [showModal, initialName, reset]);

const onSubmit = async (data: FormData) => {
try {
const response = await fetch(
Expand Down Expand Up @@ -183,22 +204,37 @@ export function useAddCustomerModal({
onSuccess?: (customer: CustomerProps) => void;
} = {}) {
const [showAddCustomerModal, setShowAddCustomerModal] = useState(false);
const [initialName, setInitialName] = useState<string | undefined>();

const AddCustomerModalCallback = useCallback(() => {
return (
<AddCustomerModal
showModal={showAddCustomerModal}
setShowModal={setShowAddCustomerModal}
setShowModal={(show) => {
setShowAddCustomerModal(show);
if (!show) {
setInitialName(undefined);
}
}}
onSuccess={onSuccess}
initialName={initialName}
/>
);
}, [showAddCustomerModal, setShowAddCustomerModal]);
}, [showAddCustomerModal, initialName, onSuccess]);

const setShowAddCustomerModalWithName = useCallback(
(show: boolean, name?: string) => {
setShowAddCustomerModal(show);
setInitialName(name);
},
[],
);

return useMemo(
() => ({
setShowAddCustomerModal,
setShowAddCustomerModal: setShowAddCustomerModalWithName,
AddCustomerModal: AddCustomerModalCallback,
}),
[setShowAddCustomerModal, AddCustomerModalCallback],
[setShowAddCustomerModalWithName, AddCustomerModalCallback],
);
}
78 changes: 48 additions & 30 deletions packages/ui/src/combobox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cn } from "@dub/utils";
import { Command, CommandInput, CommandItem, useCommandState } from "cmdk";
import { Command, useCommandState } from "cmdk";
import { ChevronDown } from "lucide-react";
import {
forwardRef,
Expand Down Expand Up @@ -200,6 +200,35 @@ export function Combobox({

useEffect(() => onSearchChange?.(search), [search]);

const createOptionItem = (
<Command.Item
className={cn(
"flex cursor-pointer items-center gap-3 whitespace-nowrap rounded-md px-3 py-2 text-left text-sm text-neutral-700",
"data-[selected=true]:bg-neutral-100",
optionClassName,
)}
onSelect={async () => {
setIsCreating(true);
const success = await onCreate?.(search);
if (success) {
setSearch("");
setIsOpen(false);
}
setIsCreating(false);
}}
>
{isCreating ? (
<LoadingSpinner className="size-4 shrink-0" />
) : (
<CreateIcon className="size-4 shrink-0" />
)}
<div className="grow truncate">
{createLabel?.(search) ??
`Create ${search ? `"${search}"` : "new option..."}`}
</div>
</Command.Item>
);

return (
<Popover
openPopover={isOpen}
Expand All @@ -225,7 +254,7 @@ export function Combobox({
<Command loop shouldFilter={shouldFilter}>
{!hideSearch && (
<div className="flex items-center overflow-hidden rounded-t-lg border-b border-neutral-200">
<CommandInput
<Command.Input
placeholder={searchPlaceholder}
value={search}
onValueChange={setSearch}
Expand All @@ -252,7 +281,12 @@ export function Combobox({
)}
</div>
)}
<ScrollContainer className="max-h-[min(50vh,250px)]">
<ScrollContainer
className={cn(
"max-h-[min(50vh,250px)]",
onCreate && !multiple && "max-h-[calc(min(50vh,250px)-3.5rem)]",
)}
>
<Command.List
className={cn("flex w-full min-w-[100px] flex-col gap-1 p-1")}
>
Expand All @@ -271,33 +305,11 @@ export function Combobox({
className={optionClassName}
/>
))}
{search.length > 0 && onCreate && (
<CommandItem
className={cn(
"flex cursor-pointer items-center gap-3 whitespace-nowrap rounded-md px-3 py-2 text-left text-sm text-neutral-700",
"data-[selected=true]:bg-neutral-100",
optionClassName,
)}
onSelect={async () => {
setIsCreating(true);
const success = await onCreate?.(search);
if (success) {
setSearch("");
setIsOpen(false);
}
setIsCreating(false);
}}
>
{isCreating ? (
<LoadingSpinner className="size-4 shrink-0" />
) : (
<CreateIcon className="size-4 shrink-0" />
)}
<div className="grow truncate">
{createLabel?.(search) || `Create "${search}"`}
</div>
</CommandItem>
)}
{/* for multiple selection, the create option item is shown at the bottom of the list */}
{onCreate &&
multiple &&
search.length > 0 &&
createOptionItem}
Comment on lines +308 to +312
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

Inconsistent create option visibility between selection modes.

For multiple selection (line 312), the create option only appears when search.length > 0. For single selection (line 335), the create option is always visible regardless of search input.

Is this intentional behavior? If so, consider documenting why the two modes differ. If not, add the same search.length > 0 check for single selection.

Also applies to: 334-339


🏁 Script executed:

#!/bin/bash
# Check if there are other usages or tests that might clarify this behavior
rg -n "onCreate.*multiple" --type=tsx --type=ts -C 3

Length of output: 27


🏁 Script executed:

#!/bin/bash
# View the actual file to see both create option implementations
cat packages/ui/src/combobox/index.tsx | head -350 | tail -100

Length of output: 4328


🏁 Script executed:

#!/bin/bash
# Search for onCreate usage without incorrect file type
rg -n "onCreate" packages/ui/src/combobox/ -A 5 -B 5

Length of output: 4947


🏁 Script executed:

#!/bin/bash
# Search for tests related to combobox onCreate behavior
fd -e test -e spec | xargs grep -l "onCreate.*multiple\|multiple.*onCreate" 2>/dev/null | head -10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for any tests in the combobox directory
fd "test|spec" packages/ui/src/combobox/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# View the createLabel and createOptionItem definitions
cat packages/ui/src/combobox/index.tsx | grep -n "createLabel\|createOptionItem" | head -20

Length of output: 221


🏁 Script executed:

#!/bin/bash
# View the section where createLabel and createOptionItem are defined
cat packages/ui/src/combobox/index.tsx | sed -n '175,240p'

Length of output: 1719


🏁 Script executed:

#!/bin/bash
# Search for any comments or documentation about the search.length behavior
rg -n "search.length" packages/ui/src/combobox/

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Look for README or documentation files in the combobox or ui package
fd "README\|CHANGELOG\|\.md$" packages/ui/src/combobox/ packages/ui/ | head -10

Length of output: 0


Add consistent search.length check for single selection create option visibility.

The multiple selection mode only displays the create option when search.length > 0 (line 312), but single selection mode displays it unconditionally (line 335). Since createLabel already handles empty search gracefully with "Create new option...", the visibility logic should be consistent.

Either add search.length > 0 check for single selection (line 335) to match multiple mode, or remove it from multiple mode to always allow creation with empty search. Recommend the latter for better UX consistency.

πŸ€– Prompt for AI Agents
In packages/ui/src/combobox/index.tsx around lines 309–313 and 335, the
create-option visibility is inconsistent: multiple selection currently requires
search.length > 0 while single selection shows the create option
unconditionally. Remove the search.length > 0 condition from the
multiple-selection branch so both modes use the same visibility logic (i.e.,
show create option whenever onCreate exists, letting createLabel handle
empty-search text), and run tests/preview to confirm behavior.

{shouldFilter ? (
<Empty className="flex min-h-12 items-center justify-center text-sm text-neutral-500">
{emptyState ? emptyState : "No matches"}
Expand All @@ -318,6 +330,12 @@ export function Combobox({
)}
</Command.List>
</ScrollContainer>
{/* for single selection, the create option item is shown as a sticky item outside of the scroll container */}
{onCreate && !multiple && (
<div className="rounded-b-lg border-t border-neutral-200 bg-white p-1">
{createOptionItem}
</div>
)}
</Command>
</AnimatedSizeContainer>
}
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/scroll-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function ScrollContainer({
</div>
{/* Bottom scroll fade */}
<div
className="pointer-events-none absolute bottom-0 left-0 hidden h-16 w-full rounded-b-lg bg-gradient-to-t from-white sm:block"
className="pointer-events-none absolute bottom-0 left-0 hidden h-16 w-full rounded-b-lg bg-gradient-to-t from-white to-transparent sm:block z-10"
style={{ opacity: 1 - Math.pow(scrollProgress, 2) }}
/>
</div>
Expand Down