tag input is updated

This commit is contained in:
Nate Kelley 2025-03-20 11:56:40 -06:00
parent 11f2f3e550
commit ce738df0c1
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
9 changed files with 34 additions and 60 deletions

View File

@ -1,8 +1,10 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { AppModal } from '@/components/ui/modal'; import { AppModal } from '@/components/ui/modal';
import { TagInput } from '@/components/ui/inputs/InputTagInput'; import { InputTagInput } from '@/components/ui/inputs/InputTagInput';
import { useInviteUser } from '@/api/buster_rest/users'; import { useInviteUser } from '@/api/buster_rest/users';
import { validate } from 'email-validator';
import { useBusterNotifications } from '@/context/BusterNotifications';
export const InvitePeopleModal: React.FC<{ export const InvitePeopleModal: React.FC<{
open: boolean; open: boolean;
@ -10,16 +12,13 @@ export const InvitePeopleModal: React.FC<{
}> = React.memo(({ open, onClose }) => { }> = React.memo(({ open, onClose }) => {
const [emails, setEmails] = React.useState<string[]>([]); const [emails, setEmails] = React.useState<string[]>([]);
const { mutateAsync: inviteUsers, isPending: inviting } = useInviteUser(); const { mutateAsync: inviteUsers, isPending: inviting } = useInviteUser();
const { openErrorMessage } = useBusterNotifications();
const handleInvite = useMemoizedFn(async () => { const handleInvite = useMemoizedFn(async () => {
await inviteUsers({ emails }); await inviteUsers({ emails });
onClose(); onClose();
}); });
const onCloseEmail = useMemoizedFn((email: string) => {
setEmails(emails.filter((e) => e !== email));
});
const memoizedHeader = useMemo(() => { const memoizedHeader = useMemo(() => {
return { return {
title: 'Invite others to join your workspace', title: 'Invite others to join your workspace',
@ -41,10 +40,14 @@ export const InvitePeopleModal: React.FC<{
return ( return (
<AppModal open={open} onClose={onClose} header={memoizedHeader} footer={memoizedFooter}> <AppModal open={open} onClose={onClose} header={memoizedHeader} footer={memoizedFooter}>
<div className="flex flex-col"> <div className="flex flex-col">
<TagInput <InputTagInput
value={emails} tags={emails}
onTagAdd={(v) => { onTagAdd={(v) => {
setEmails([...emails, v]); if (validate(v)) {
setEmails([...emails, v]);
} else {
openErrorMessage(`Invalid email - ${v}`);
}
}} }}
onTagRemove={(index) => { onTagRemove={(index) => {
setEmails(emails.filter((_, i) => i !== index)); setEmails(emails.filter((_, i) => i !== index));

View File

@ -1,13 +1,13 @@
'use client'; 'use client';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { TagInput } from './InputTagInput'; import { InputTagInput } from './InputTagInput';
import { useState } from 'react'; import { useState } from 'react';
import React from 'react'; import React from 'react';
const meta: Meta<typeof TagInput> = { const meta: Meta<typeof InputTagInput> = {
title: 'UI/Inputs/InputTagInput', title: 'UI/Inputs/InputTagInput',
component: TagInput, component: InputTagInput,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
variant: { variant: {
@ -31,10 +31,10 @@ const meta: Meta<typeof TagInput> = {
}; };
export default meta; export default meta;
type Story = StoryObj<typeof TagInput>; type Story = StoryObj<typeof InputTagInput>;
// Interactive component for Storybook // Interactive component for Storybook
const InteractiveTagInput = (args: React.ComponentProps<typeof TagInput>) => { const InteractiveTagInput = (args: React.ComponentProps<typeof InputTagInput>) => {
const [tags, setTags] = useState<string[]>(args.tags || []); const [tags, setTags] = useState<string[]>(args.tags || []);
const handleTagAdd = (tag: string) => { const handleTagAdd = (tag: string) => {
@ -47,7 +47,9 @@ const InteractiveTagInput = (args: React.ComponentProps<typeof TagInput>) => {
setTags(newTags); setTags(newTags);
}; };
return <TagInput {...args} tags={tags} onTagAdd={handleTagAdd} onTagRemove={handleTagRemove} />; return (
<InputTagInput {...args} tags={tags} onTagAdd={handleTagAdd} onTagRemove={handleTagRemove} />
);
}; };
export const Default: Story = { export const Default: Story = {

View File

@ -7,18 +7,17 @@ import { useMemoizedFn } from '@/hooks';
import { inputVariants } from './Input'; import { inputVariants } from './Input';
import { InputTag } from './InputTag'; import { InputTag } from './InputTag';
export interface TagInputProps export interface TagInputProps extends VariantProps<typeof inputVariants> {
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>, tags: string[];
VariantProps<typeof inputVariants> {
tags?: string[];
onTagAdd?: (tag: string) => void; onTagAdd?: (tag: string) => void;
onTagRemove?: (index: number) => void; onTagRemove?: (index: number) => void;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
maxTags?: number; maxTags?: number;
className?: string;
} }
const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>( const InputTagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
( (
{ {
className, className,
@ -116,6 +115,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
))} ))}
<input <input
ref={ref} ref={ref}
{...props}
type="text" type="text"
value={inputValue} value={inputValue}
onChange={handleInputChange} onChange={handleInputChange}
@ -123,13 +123,12 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
className="placeholder:text-gray-light min-w-[120px] flex-1 bg-transparent outline-none disabled:cursor-not-allowed disabled:opacity-50" className="placeholder:text-gray-light min-w-[120px] flex-1 bg-transparent outline-none disabled:cursor-not-allowed disabled:opacity-50"
placeholder={tags.length === 0 ? placeholder : undefined} placeholder={tags.length === 0 ? placeholder : undefined}
disabled={isDisabledInput} disabled={isDisabledInput}
{...props}
/> />
</div> </div>
</div> </div>
); );
} }
); );
TagInput.displayName = 'TagInput'; InputTagInput.displayName = 'TagInput';
export { TagInput }; export { InputTagInput };

View File

@ -135,32 +135,6 @@ export const BusterInfiniteList: React.FC<BusterInfiniteListProps> = React.memo(
return () => scrollableParent.removeEventListener('scroll', handleScroll); return () => scrollableParent.removeEventListener('scroll', handleScroll);
}, [onScrollEnd, scrollEndThreshold]); }, [onScrollEnd, scrollEndThreshold]);
useWhyDidYouUpdate('BusterInfiniteList', {
columns,
rows,
selectedRowKeys,
onSelectChange,
emptyState,
contextMenu,
hideLastRowBorder,
showSelectAll,
onScrollEnd,
loadingNewContent,
rowClassName,
scrollEndThreshold,
useRowClickSelectChange,
showHeader,
itemData,
globalCheckStatus,
lastChildIndex,
showEmptyState,
containerRef,
scrollRef,
onGlobalSelectChange,
onSelectSectionChange,
onSelectChangePreflight
});
return ( return (
<div ref={containerRef} className="infinite-list-container relative"> <div ref={containerRef} className="infinite-list-container relative">
{showHeader && !showEmptyState && ( {showHeader && !showEmptyState && (

View File

@ -10,8 +10,6 @@ interface EmptyStateListProps {
export const EmptyStateList = React.memo( export const EmptyStateList = React.memo(
({ show = true, text, variant = 'default' }: EmptyStateListProps) => { ({ show = true, text, variant = 'default' }: EmptyStateListProps) => {
console.log('hit!', show, text, variant);
if (!show) return null; if (!show) return null;
if (variant === 'card') { if (variant === 'card') {

View File

@ -13,7 +13,7 @@ const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value; const SelectValue = SelectPrimitive.Value;
export const selectVariants = cva( export const selectVariants = cva(
'flex w-full gap-x-1.5 transition-colors transition-border duration-200 items-center justify-between rounded border px-3 py-1 text-sm focus:outline-none cursor-pointer disabled:cursor-not-allowed disabled:opacity-60 [&>span]:line-clamp-1', 'flex w-full gap-x-1.5 transition-colors transition-border text-foreground duration-200 items-center justify-between rounded border px-3 py-1 text-sm focus:outline-none cursor-pointer disabled:cursor-not-allowed disabled:opacity-60 [&>span]:line-clamp-1',
{ {
variants: { variants: {
variant: { variant: {

View File

@ -157,9 +157,9 @@ const PermissionGroupAssignedCell: React.FC<{
<div className="flex" onClick={(e) => e.stopPropagation()}> <div className="flex" onClick={(e) => e.stopPropagation()}>
<Select <Select
items={PERMISSION_USERS_OPTIONS} items={PERMISSION_USERS_OPTIONS}
value={assigned ? 'true' : 'false'} value={assigned ? 'included' : 'not_included'}
onChange={(value) => { onChange={(value) => {
onSelect({ id, assigned: value === 'true' }); onSelect({ id, assigned: value === 'included' });
}} }}
/> />
</div> </div>

View File

@ -58,10 +58,8 @@ const PermissionUsersAssignButton: React.FC<{
return { return {
selectable: true, selectable: true,
items: options.map((v) => ({ items: options.map((v) => ({
icon: v.icon, ...v,
label: v.label, onClick: () => onAssignClick(v.value === 'included')
value: v.value ? 'included' : 'not_included',
onClick: () => onAssignClick(v.value === 'true')
})) }))
}; };
}, [selectedRowKeys]); }, [selectedRowKeys]);

View File

@ -1,12 +1,12 @@
import { SelectItem } from '@/components/ui/select'; import { SelectItem } from '@/components/ui/select';
export const PERMISSION_USERS_OPTIONS: SelectItem<'true' | 'false'>[] = [ export const PERMISSION_USERS_OPTIONS: SelectItem<'included' | 'not_included'>[] = [
{ {
label: 'Assigned', label: 'Assigned',
value: 'true' value: 'included'
}, },
{ {
label: 'Not assigned', label: 'Not assigned',
value: 'false' value: 'not_included'
} }
]; ];