mirror of https://github.com/buster-so/buster.git
tag input is updated
This commit is contained in:
parent
11f2f3e550
commit
ce738df0c1
|
@ -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));
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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') {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in New Issue