feat: UI refactor on dropdown for better customization (#216)
* wip: combobox refactor * refactor: new combobox styling * refactor: rename new combobox * wip: refactor combobox factory * refactor: new combobox single component * wip: refactor form combobox * refactor: github branches typing * refactor: ens field form combobox * fix: readd removed branch icon * refactor: replace nfa picker combobox * refactor: remove old combobox * refactor: rename combobox factory * fix: remove leftover combobox code * refactor: query filter by keys * fix: create ap form title * fix: max combobox options height * feat: add chevron on combobox field * refactor: optmize elements generation structure * chore: add todo comments for things thats going to change * fix: code comment addressed on pr
This commit is contained in:
parent
fdedc18b02
commit
9934ad1cfd
|
|
@ -10,23 +10,20 @@ export abstract class AvatarStyles {
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
width: '$5',
|
|
||||||
height: '$5',
|
|
||||||
borderRadius: '100%',
|
borderRadius: '100%',
|
||||||
backgroundColor: '$slate2',
|
backgroundColor: '$slate2',
|
||||||
mr: '$2',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
static readonly Image = styled(Avatar.Image, {
|
static readonly Image = styled(Avatar.Image, {
|
||||||
width: '100%',
|
width: '1em',
|
||||||
height: '100%',
|
height: '1em',
|
||||||
objectFit: 'cover',
|
objectFit: 'cover',
|
||||||
borderRadius: 'inherit',
|
borderRadius: 'inherit',
|
||||||
});
|
});
|
||||||
|
|
||||||
static readonly Fallback = styled(Avatar.Fallback, {
|
static readonly Fallback = styled(Avatar.Fallback, {
|
||||||
width: '100%',
|
width: '1em',
|
||||||
height: '100%',
|
height: '1em',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
@ -37,36 +34,44 @@ export abstract class AvatarStyles {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AvatarProps = React.ComponentProps<typeof AvatarStyles.Root> & {
|
export namespace AvatarStyles {
|
||||||
/**
|
export type RootProps = React.ComponentPropsWithRef<
|
||||||
* Fallback node.
|
typeof AvatarStyles.Root
|
||||||
* In case of string, transformed to upper case and sliced to second letter.
|
> & {
|
||||||
*/
|
/**
|
||||||
fallback?: React.ReactNode;
|
* Fallback node.
|
||||||
/**
|
* In case of string, transformed to upper case and sliced to second letter.
|
||||||
* Source of the image.
|
*/
|
||||||
* If not provided, fallback will be used.
|
fallback?: React.ReactNode;
|
||||||
*/
|
/**
|
||||||
src?: AvatarImageProps['src'];
|
* Source of the image.
|
||||||
/**
|
* If not provided, fallback will be used.
|
||||||
* Alt text of the image.
|
*/
|
||||||
*/
|
src?: ImageProps['src'];
|
||||||
alt?: AvatarImageProps['alt'];
|
/**
|
||||||
|
* Alt text of the image.
|
||||||
|
*/
|
||||||
|
alt?: ImageProps['alt'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props of the image tag.
|
* Props of the image tag.
|
||||||
* @see {@link AvatarImageProps}
|
* @see {@link AvatarImageProps}
|
||||||
* @default {}
|
* @default {}
|
||||||
*/
|
*/
|
||||||
imageProps?: AvatarImageProps;
|
imageProps?: ImageProps;
|
||||||
/**
|
/**
|
||||||
* Props of the fallback tag.
|
* Props of the fallback tag.
|
||||||
* @see {@link AvatarFallbackProps}
|
* @see {@link AvatarFallbackProps}
|
||||||
* @default {}
|
* @default {}
|
||||||
*/
|
*/
|
||||||
fallbackProps?: AvatarFallbackProps;
|
fallbackProps?: FallbackProps;
|
||||||
};
|
};
|
||||||
export type AvatarImageProps = React.ComponentProps<typeof AvatarStyles.Image>;
|
|
||||||
export type AvatarFallbackProps = React.ComponentProps<
|
export type ImageProps = React.ComponentPropsWithRef<
|
||||||
typeof AvatarStyles.Fallback
|
typeof AvatarStyles.Image
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type FallbackProps = React.ComponentPropsWithRef<
|
||||||
|
typeof AvatarStyles.Fallback
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { forwardRef } from 'react';
|
import { forwardStyledRef } from '@/theme';
|
||||||
|
|
||||||
import { AvatarProps, AvatarStyles } from './avatar.styles';
|
import { AvatarStyles as AS } from './avatar.styles';
|
||||||
|
|
||||||
export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
|
export const Avatar = forwardStyledRef<HTMLDivElement, AS.RootProps>(
|
||||||
({ imageProps = {}, src, alt, css, ...rootProps }, ref) => {
|
({ imageProps = {}, src, alt, ...rootProps }, ref) => {
|
||||||
return (
|
return (
|
||||||
<AvatarStyles.Root {...rootProps} ref={ref} css={css}>
|
<AS.Root {...rootProps} ref={ref}>
|
||||||
<AvatarStyles.Image src={src} alt={alt} {...imageProps} />
|
<AS.Image src={src} alt={alt} {...imageProps} />
|
||||||
</AvatarStyles.Root>
|
</AS.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { Combobox as HeadlessCombobox } from '@headlessui/react';
|
||||||
|
|
||||||
|
import { styled } from '@/theme';
|
||||||
|
|
||||||
|
import { Icon } from '../icon';
|
||||||
|
import { IconStyles } from '../icon/icon.styles';
|
||||||
|
import { InputStyled } from '../input';
|
||||||
|
|
||||||
|
export const ComboboxStyles = {
|
||||||
|
Wrapper: styled('div', {
|
||||||
|
position: 'relative',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
unattached: {
|
||||||
|
true: {
|
||||||
|
position: 'static',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
Option: styled(HeadlessCombobox.Option, {
|
||||||
|
width: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '$3',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '$2 $3',
|
||||||
|
borderRadius: '$lg',
|
||||||
|
color: '$slate11',
|
||||||
|
transition: '$all-200',
|
||||||
|
|
||||||
|
'&[data-headlessui-state*="selected"]': {
|
||||||
|
backgroundColor: '$slate3',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&[data-headlessui-state*="active"]': {
|
||||||
|
backgroundColor: '$slate2',
|
||||||
|
color: '$slate12',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
Options: styled(HeadlessCombobox.Options, {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'absolute',
|
||||||
|
border: '1px solid $slate6',
|
||||||
|
backgroundColor: '$black',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 'calc(100% + $3)',
|
||||||
|
padding: '$3',
|
||||||
|
gap: '$2',
|
||||||
|
borderRadius: '$lg',
|
||||||
|
zIndex: 10,
|
||||||
|
maxHeight: '30vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
}),
|
||||||
|
|
||||||
|
Field: styled(HeadlessCombobox.Button, InputStyled, {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
gap: '$3',
|
||||||
|
|
||||||
|
'&:focus-within': {
|
||||||
|
outline: 'none',
|
||||||
|
borderColor: '$blue9',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
Input: styled(HeadlessCombobox.Input, {
|
||||||
|
width: '100%',
|
||||||
|
color: '$slate11',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
outline: 'none',
|
||||||
|
}),
|
||||||
|
|
||||||
|
RightPositionedIcon: styled(Icon, {
|
||||||
|
position: 'absolute',
|
||||||
|
right: '$3',
|
||||||
|
}),
|
||||||
|
|
||||||
|
Message: styled('span', {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '$2',
|
||||||
|
color: '$slate8',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
}),
|
||||||
|
|
||||||
|
InnerSearchContainer: styled('div', {
|
||||||
|
position: 'sticky',
|
||||||
|
top: '-$3',
|
||||||
|
padding: '$3 $2 $3 $2',
|
||||||
|
margin: '-$3 0 0 0',
|
||||||
|
zIndex: 10,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '$3',
|
||||||
|
borderBottom: '1px solid $slate6',
|
||||||
|
backgroundColor: '$black',
|
||||||
|
|
||||||
|
[`${IconStyles.Container}`]: {
|
||||||
|
fontSize: '1.5em',
|
||||||
|
color: '$slate8',
|
||||||
|
},
|
||||||
|
|
||||||
|
'input::placeholder': {
|
||||||
|
color: '$slate8',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
@ -1,279 +1,219 @@
|
||||||
import {
|
import {
|
||||||
Combobox as ComboboxLib,
|
Combobox as HeadlessCombobox,
|
||||||
ComboboxInputProps as ComboboxLibInputProps,
|
ComboboxInputProps,
|
||||||
Transition,
|
|
||||||
} from '@headlessui/react';
|
} from '@headlessui/react';
|
||||||
import React, { forwardRef, Fragment, useEffect, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Icon, IconName } from '@/components/core/icon';
|
import { Spinner } from '@/components/spinner';
|
||||||
import { Flex } from '@/components/layout';
|
import { createContext } from '@/utils';
|
||||||
import { useDebounce } from '@/hooks/use-debounce';
|
|
||||||
|
|
||||||
import { Separator } from '../separator.styles';
|
import { Icon } from '../icon';
|
||||||
import { cleanString } from './combobox.utils';
|
import { ComboboxStyles as CS } from './combobox.styles';
|
||||||
|
|
||||||
type ComboboxInputProps = ComboboxLibInputProps<'input', ComboboxItem>;
|
const EmptyMessage = <CS.Message>No items found</CS.Message>;
|
||||||
|
const LoadingMessage = (
|
||||||
|
<CS.Message>
|
||||||
|
<Spinner /> Searching...
|
||||||
|
</CS.Message>
|
||||||
|
);
|
||||||
|
|
||||||
const ComboboxInput: React.FC<ComboboxInputProps> = ({
|
const [Provider, useContext] = createContext<Combobox.Context<unknown>>({
|
||||||
|
name: 'ComboboxContext',
|
||||||
|
hookName: 'useComboboxContext',
|
||||||
|
providerName: 'ComboboxProvider',
|
||||||
|
});
|
||||||
|
|
||||||
|
const Input = <T,>(props: Combobox.InputProps<T>): JSX.Element => {
|
||||||
|
const {
|
||||||
|
query: [, setQuery],
|
||||||
|
} = useContext() as Combobox.Context<T>;
|
||||||
|
|
||||||
|
const onChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setQuery(event.target.value);
|
||||||
|
if (props.onChange) props.onChange(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <CS.Input {...props} onChange={onChange} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Options = <T,>({
|
||||||
|
disableSearch,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: ComboboxInputProps) => (
|
}: Combobox.OptionsProps<T>): JSX.Element => {
|
||||||
<div className="relative w-full">
|
const {
|
||||||
<Icon
|
query: [query],
|
||||||
name="search"
|
loading,
|
||||||
size="sm"
|
selected: [selected],
|
||||||
css={{
|
items,
|
||||||
position: 'absolute',
|
queryFilter,
|
||||||
left: '$3',
|
} = useContext() as Combobox.Context<T>;
|
||||||
top: '$3',
|
|
||||||
fontSize: '$xl',
|
|
||||||
color: '$slate8',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ComboboxLib.Input
|
|
||||||
placeholder="Search"
|
|
||||||
className={`w-full h-9 py-3 px-10 text-sm bg-transparent leading-5 text-slate11 outline-none `}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
type ComboboxOptionProps = {
|
const [
|
||||||
option: ComboboxItem;
|
optionRenderer,
|
||||||
};
|
EmptyRender = EmptyMessage,
|
||||||
|
LoadingRender = LoadingMessage,
|
||||||
const ComboboxOption: React.FC<ComboboxOptionProps> = ({
|
] = useMemo(
|
||||||
option,
|
() => (Array.isArray(children) ? children : [children]),
|
||||||
}: ComboboxOptionProps) => (
|
[children]
|
||||||
<ComboboxLib.Option
|
|
||||||
value={option}
|
|
||||||
className={({ active }) =>
|
|
||||||
`relative cursor-default select-none py-2 px-3.5 text-slate11 rounded-xl mb-2 text-sm ${
|
|
||||||
active ? 'bg-slate5 text-slate12' : 'bg-transparent'
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<Flex css={{ justifyContent: 'space-between' }}>
|
|
||||||
<Flex css={{ flexDirection: 'row', maxWidth: '95%' }}>
|
|
||||||
{option.icon}
|
|
||||||
<span
|
|
||||||
className={`${active ? 'text-slate12' : 'text-slate11'} ${
|
|
||||||
option.icon ? 'max-w-70' : 'max-w-full'
|
|
||||||
} whitespace-nowrap text-ellipsis overflow-hidden`}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</span>
|
|
||||||
</Flex>
|
|
||||||
{selected && <Icon name="check" color="white" />}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</ComboboxLib.Option>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const NoResults: React.FC = ({ css }: { css?: string }) => (
|
|
||||||
<div
|
|
||||||
className={`relative cursor-default select-none pt-2 px-3.5 pb-4 text-slate11 ${css}`}
|
|
||||||
>
|
|
||||||
Nothing found.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ComboboxItem = {
|
|
||||||
/**
|
|
||||||
* The key of the item.
|
|
||||||
*/
|
|
||||||
value: string;
|
|
||||||
/**
|
|
||||||
* The label to display of the item.
|
|
||||||
*/
|
|
||||||
label: string;
|
|
||||||
/**
|
|
||||||
* Optional icon to display on the left of the item.
|
|
||||||
*/
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ComboboxProps = {
|
|
||||||
/**
|
|
||||||
* List of items to be displayed in the combobox.
|
|
||||||
*/
|
|
||||||
items: ComboboxItem[];
|
|
||||||
/**
|
|
||||||
* The selected value of the combobox.
|
|
||||||
*/
|
|
||||||
selectedValue: ComboboxItem | undefined;
|
|
||||||
/**
|
|
||||||
* If true, the combobox will add the input if it doesn't exist in the list of items.
|
|
||||||
*/
|
|
||||||
withAutocomplete?: boolean;
|
|
||||||
/**
|
|
||||||
* Name of the left icon to display in the input. Defualt is "search".
|
|
||||||
*/
|
|
||||||
leftIcon?: IconName;
|
|
||||||
/**
|
|
||||||
* Callback when the selected value changes.
|
|
||||||
*/
|
|
||||||
onChange: (option: ComboboxItem) => void;
|
|
||||||
/**
|
|
||||||
* Function to handle the input blur
|
|
||||||
*/
|
|
||||||
onBlur?: () => void;
|
|
||||||
/**
|
|
||||||
* Value to indicate it's invalid
|
|
||||||
*/
|
|
||||||
error?: boolean;
|
|
||||||
css?: string; //tailwind css
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Combobox: React.FC<ComboboxProps> = ({
|
|
||||||
items,
|
|
||||||
selectedValue = { value: '', label: '' },
|
|
||||||
withAutocomplete = false,
|
|
||||||
leftIcon = 'search',
|
|
||||||
onChange,
|
|
||||||
onBlur,
|
|
||||||
error = false,
|
|
||||||
css,
|
|
||||||
}) => {
|
|
||||||
const [filteredItems, setFilteredItems] = useState<ComboboxItem[]>([]);
|
|
||||||
const [autocompleteItems, setAutocompleteItems] = useState<ComboboxItem[]>(
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const filteredItems = useMemo(
|
||||||
// If the selected value doesn't exist in the list of items, we add it
|
() => items.filter((item) => queryFilter(query, item)),
|
||||||
if (
|
[items, query, queryFilter]
|
||||||
items.filter((item) => item === selectedValue).length === 0 &&
|
);
|
||||||
selectedValue.value !== undefined &&
|
|
||||||
autocompleteItems.length === 0 &&
|
|
||||||
withAutocomplete
|
|
||||||
) {
|
|
||||||
setAutocompleteItems([selectedValue]);
|
|
||||||
}
|
|
||||||
}, [autocompleteItems.length, items, selectedValue, withAutocomplete]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFilteredItems(items);
|
|
||||||
}, [items]);
|
|
||||||
|
|
||||||
const handleSearch = useDebounce((searchValue: string) => {
|
|
||||||
if (searchValue === '') {
|
|
||||||
setFilteredItems(items);
|
|
||||||
|
|
||||||
if (withAutocomplete) {
|
|
||||||
setAutocompleteItems([]);
|
|
||||||
handleComboboxChange({} as ComboboxItem);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const filteredValues = items.filter((item) =>
|
|
||||||
cleanString(item.label).startsWith(cleanString(searchValue))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (withAutocomplete && filteredValues.length === 0) {
|
|
||||||
// If the search value doesn't exist in the list of items, we add it
|
|
||||||
setAutocompleteItems([{ value: searchValue, label: searchValue }]);
|
|
||||||
}
|
|
||||||
setFilteredItems(filteredValues);
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
const handleInputChange = (
|
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
|
||||||
): void => {
|
|
||||||
event.stopPropagation();
|
|
||||||
handleSearch(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleComboboxChange = (optionSelected: ComboboxItem): void => {
|
|
||||||
onChange(optionSelected);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLeaveTransition = (): void => {
|
|
||||||
setFilteredItems(items);
|
|
||||||
if (selectedValue.value === undefined && withAutocomplete) {
|
|
||||||
setAutocompleteItems([]);
|
|
||||||
handleComboboxChange({} as ComboboxItem);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComboboxLib
|
<CS.Options {...props}>
|
||||||
value={selectedValue}
|
{!disableSearch && (
|
||||||
by="value"
|
<CS.InnerSearchContainer>
|
||||||
onChange={handleComboboxChange}
|
<Icon name="search" />
|
||||||
>
|
<Input placeholder="Search..." />
|
||||||
{({ open }) => (
|
</CS.InnerSearchContainer>
|
||||||
<div className={`relative w-full ${css ? css : ''}`}>
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Icon
|
|
||||||
name={leftIcon}
|
|
||||||
size="sm"
|
|
||||||
css={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: '$3',
|
|
||||||
top: '$3',
|
|
||||||
fontSize: '$xl',
|
|
||||||
color: 'slate8',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ComboboxLib.Button
|
|
||||||
className={`w-full text-left border-solid border rounded-xl h-11 py-3 px-10 text-sm leading-5 text-slate11 outline-none ${
|
|
||||||
error ? 'border-red9' : 'border-slate7'
|
|
||||||
}`}
|
|
||||||
onBlur={onBlur}
|
|
||||||
>
|
|
||||||
{selectedValue && selectedValue.label
|
|
||||||
? selectedValue.label
|
|
||||||
: 'Search'}
|
|
||||||
</ComboboxLib.Button>
|
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-4">
|
|
||||||
<Icon name="chevron-down" css={{ fontSize: '$xs' }} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition duration-400 ease-out"
|
|
||||||
leave="transition ease-out duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
afterLeave={handleLeaveTransition}
|
|
||||||
>
|
|
||||||
<div className="absolute max-h-60 mt-2 w-full z-10 overflow-auto rounded-xl border-solid border-slate6 border bg-black pt-2 px-3 text-base focus:outline-none sm:text-sm">
|
|
||||||
<ComboboxInput onChange={handleInputChange} onBlur={onBlur} />
|
|
||||||
<Separator />
|
|
||||||
<ComboboxLib.Options className="mt-1 z-20">
|
|
||||||
{[...autocompleteItems, ...filteredItems].length === 0 ||
|
|
||||||
filteredItems === undefined ? (
|
|
||||||
<NoResults />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{autocompleteItems.length > 0 && <span>Create new</span>}
|
|
||||||
{autocompleteItems.map(
|
|
||||||
(autocompleteOption: ComboboxItem) => (
|
|
||||||
<ComboboxOption
|
|
||||||
key={autocompleteOption.value}
|
|
||||||
option={autocompleteOption}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
{autocompleteItems.length > 0 &&
|
|
||||||
filteredItems.length > 0 && (
|
|
||||||
<Separator css={{ mb: '$2' }} />
|
|
||||||
)}
|
|
||||||
{filteredItems.map((option: ComboboxItem) => (
|
|
||||||
<ComboboxOption key={option.value} option={option} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ComboboxLib.Options>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</ComboboxLib>
|
|
||||||
|
{filteredItems.map((item) => (
|
||||||
|
<CS.Option key={JSON.stringify(item)} value={item}>
|
||||||
|
{optionRenderer(item, selected === item)}
|
||||||
|
{selected === item && <CS.RightPositionedIcon name="check" />}
|
||||||
|
</CS.Option>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!loading && filteredItems.length === 0 && EmptyRender}
|
||||||
|
|
||||||
|
{loading && LoadingRender}
|
||||||
|
</CS.Options>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Combobox.displayName = 'Combobox';
|
const Field = <T,>({
|
||||||
|
children,
|
||||||
|
disableChevron,
|
||||||
|
...props
|
||||||
|
}: Combobox.FieldProps<T>): JSX.Element => {
|
||||||
|
const {
|
||||||
|
selected: [selected],
|
||||||
|
} = useContext() as Combobox.Context<T>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CS.Field {...props}>
|
||||||
|
{children(selected)}
|
||||||
|
{!disableChevron && <CS.RightPositionedIcon name="chevron-down" />}
|
||||||
|
</CS.Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Combobox = <T,>({
|
||||||
|
children,
|
||||||
|
selected,
|
||||||
|
isLoading: loading = false,
|
||||||
|
items,
|
||||||
|
queryKey,
|
||||||
|
...props
|
||||||
|
}: Combobox.RootProps<T>): JSX.Element => {
|
||||||
|
const [value, setValue] = selected;
|
||||||
|
const query = useState('');
|
||||||
|
|
||||||
|
const queryFilter = useCallback(
|
||||||
|
(query: string, item: T): boolean => {
|
||||||
|
if (typeof queryKey === 'undefined')
|
||||||
|
return `${item}`.includes(query.toLowerCase());
|
||||||
|
|
||||||
|
const keys = Array.isArray(queryKey) ? queryKey : [queryKey];
|
||||||
|
|
||||||
|
const searchString = keys
|
||||||
|
.reduce((acc, key) => {
|
||||||
|
const value = item[key];
|
||||||
|
return `${acc} ${value}`;
|
||||||
|
}, '')
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
return searchString.includes(query.toLowerCase());
|
||||||
|
},
|
||||||
|
[queryKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
const TypedProvider = Provider as React.Provider<Combobox.Context<T>>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HeadlessCombobox
|
||||||
|
as={CS.Wrapper}
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<TypedProvider
|
||||||
|
value={{
|
||||||
|
selected,
|
||||||
|
query,
|
||||||
|
loading,
|
||||||
|
open,
|
||||||
|
items,
|
||||||
|
queryFilter,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children({
|
||||||
|
Options: Options<T>,
|
||||||
|
Input: Input<T>,
|
||||||
|
Field: Field<T>,
|
||||||
|
Message: CS.Message,
|
||||||
|
})}
|
||||||
|
</TypedProvider>
|
||||||
|
)}
|
||||||
|
</HeadlessCombobox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export namespace Combobox {
|
||||||
|
export type Context<T> = {
|
||||||
|
items: T[];
|
||||||
|
selected: [T | undefined, (newState: T | undefined) => void];
|
||||||
|
query: ReactState<string>;
|
||||||
|
loading: boolean;
|
||||||
|
open: boolean;
|
||||||
|
queryFilter: (query: string, item: T) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OptionsProps<T> = Omit<
|
||||||
|
React.ComponentPropsWithRef<typeof CS.Options>,
|
||||||
|
'children'
|
||||||
|
> & {
|
||||||
|
disableSearch?: boolean;
|
||||||
|
children:
|
||||||
|
| ((item: T, selected: boolean) => React.ReactNode)
|
||||||
|
| [
|
||||||
|
(item: T, selected: boolean) => React.ReactNode,
|
||||||
|
React.ReactNode?,
|
||||||
|
React.ReactNode?
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InputProps<T> = ComboboxInputProps<'input', T | undefined>;
|
||||||
|
|
||||||
|
export type FieldProps<T> = Omit<
|
||||||
|
React.ComponentPropsWithRef<typeof CS.Field>,
|
||||||
|
'children'
|
||||||
|
> & {
|
||||||
|
children: (item: T | undefined) => React.ReactElement | React.ReactNode;
|
||||||
|
disableChevron?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Elements<T> = {
|
||||||
|
Options: React.FC<OptionsProps<T>>;
|
||||||
|
Input: React.FC<InputProps<T>>;
|
||||||
|
Field: React.FC<FieldProps<T>>;
|
||||||
|
Message: typeof CS.Message;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RootProps<T> = Omit<
|
||||||
|
React.ComponentPropsWithRef<typeof CS.Wrapper>,
|
||||||
|
'defaultValue' | 'onChange' | 'children'
|
||||||
|
> &
|
||||||
|
Pick<Context<T>, 'selected' | 'items'> & {
|
||||||
|
isLoading?: boolean;
|
||||||
|
children: (elements: Elements<T>) => React.ReactNode;
|
||||||
|
} & (T extends object
|
||||||
|
? { queryKey: keyof T | (keyof T)[] }
|
||||||
|
: { queryKey?: undefined });
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export const cleanString = (str: string): string =>
|
|
||||||
str.toLowerCase().replace(/\s+/g, '');
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
export * from './combobox';
|
|
||||||
export * from './dropdown';
|
export * from './dropdown';
|
||||||
|
export * from './combobox';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { forwardRef } from 'react';
|
import { forwardStyledRef } from '@/theme';
|
||||||
|
|
||||||
import { IconStyles } from './icon.styles';
|
import { IconStyles } from './icon.styles';
|
||||||
import { IconLibrary, IconName, IconType } from './icon-library';
|
import { IconLibrary, IconName, IconType } from './icon-library';
|
||||||
|
|
@ -8,19 +8,18 @@ export type IconProps = {
|
||||||
iconElementCss?: React.CSSProperties;
|
iconElementCss?: React.CSSProperties;
|
||||||
} & React.ComponentProps<typeof IconStyles.Container>;
|
} & React.ComponentProps<typeof IconStyles.Container>;
|
||||||
|
|
||||||
export const Icon: React.FC<IconProps> = forwardRef<HTMLSpanElement, IconProps>(
|
export const Icon: React.FC<IconProps> = forwardStyledRef<
|
||||||
(props, ref) => {
|
HTMLSpanElement,
|
||||||
const { name, iconElementCss, ...rest } = props;
|
IconProps
|
||||||
const IconElement: IconType<typeof name> = IconLibrary[name];
|
>((props, ref) => {
|
||||||
|
const { name, iconElementCss, ...rest } = props;
|
||||||
|
const IconElement: IconType<typeof name> = IconLibrary[name];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconStyles.Container {...rest} ref={ref}>
|
<IconStyles.Container {...rest} ref={ref}>
|
||||||
<IconElement
|
<IconElement style={{ width: '1em', height: '1em', ...iconElementCss }} />
|
||||||
style={{ width: '1em', height: '1em', ...iconElementCss }}
|
</IconStyles.Container>
|
||||||
/>
|
);
|
||||||
</IconStyles.Container>
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Icon.displayName = 'Icon';
|
Icon.displayName = 'Icon';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import React, { forwardRef } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { forwardStyledRef } from '@/theme';
|
||||||
|
|
||||||
import { IconName } from '../icon';
|
import { IconName } from '../icon';
|
||||||
import { InputIconStyled, InputStyled, TextareaStyled } from './input.styles';
|
import { InputIconStyled, InputStyled, TextareaStyled } from './input.styles';
|
||||||
|
|
@ -10,24 +12,26 @@ export const LogoFileInput = StyledInputFile;
|
||||||
|
|
||||||
type InputProps = {
|
type InputProps = {
|
||||||
leftIcon?: IconName;
|
leftIcon?: IconName;
|
||||||
css?: string; //tailwind css
|
wrapperClassName?: string; //tailwind css
|
||||||
} & React.ComponentPropsWithRef<typeof InputStyled>;
|
} & React.ComponentPropsWithRef<typeof InputStyled>;
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
export const Input = forwardStyledRef<HTMLInputElement, InputProps>(
|
||||||
const { leftIcon, css, ...ownProps } = props;
|
(props, ref) => {
|
||||||
|
const { leftIcon, wrapperClassName: css = '', ...ownProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative ${css ? css : ''}`}>
|
<div className={`relative ${css}`}>
|
||||||
{leftIcon && (
|
{leftIcon && (
|
||||||
<InputIconStyled name={leftIcon} css={{ fontSize: '$lg' }} />
|
<InputIconStyled name={leftIcon} css={{ fontSize: '$lg' }} />
|
||||||
)}
|
)}
|
||||||
<InputStyled
|
<InputStyled
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
css={{ ...(leftIcon && { pl: '$10' }), ...(ownProps.css || {}) }}
|
css={{ ...(leftIcon && { pl: '$10' }), ...(ownProps.css || {}) }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
Input.displayName = 'Input';
|
Input.displayName = 'Input';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import React, { forwardRef, useMemo, useState } from 'react';
|
||||||
import { hasValidator } from '@/utils';
|
import { hasValidator } from '@/utils';
|
||||||
import { fileToBase64 } from '@/views/mint/nfa-step/form-step/form.utils';
|
import { fileToBase64 } from '@/views/mint/nfa-step/form-step/form.utils';
|
||||||
|
|
||||||
import { ColorPicker, Combobox, ComboboxItem } from '../core';
|
import { ColorPicker, Combobox } from '../core';
|
||||||
import { Input, LogoFileInput, Textarea } from '../core/input';
|
import { Input, LogoFileInput, Textarea } from '../core/input';
|
||||||
import {
|
import {
|
||||||
FormProvider,
|
FormProvider,
|
||||||
|
|
@ -134,7 +134,10 @@ export abstract class Form {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
static readonly Combobox: React.FC<Form.ComboboxProps> = (props) => {
|
static readonly Combobox = <T,>({
|
||||||
|
handleValue,
|
||||||
|
...props
|
||||||
|
}: Form.ComboboxProps<T>): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
validators,
|
validators,
|
||||||
|
|
@ -142,21 +145,16 @@ export abstract class Form {
|
||||||
validationEnabled: [validationEnabled, setValidationEnabled],
|
validationEnabled: [validationEnabled, setValidationEnabled],
|
||||||
} = useFormFieldContext();
|
} = useFormFieldContext();
|
||||||
|
|
||||||
const comboboxValue = useMemo(() => {
|
const selected = useMemo(() => {
|
||||||
// if it's with autocomplete maybe won't be on the items list
|
const item = props.items.find((item) => handleValue(item) === value);
|
||||||
const item = props.items.find((item) => item.label === value);
|
|
||||||
if (props.withAutocomplete && !item && value !== '') {
|
|
||||||
//return the selected value if the item doesn't exist
|
|
||||||
return { label: value, value: value };
|
|
||||||
}
|
|
||||||
return item;
|
return item;
|
||||||
}, [props.items, props.withAutocomplete, value]);
|
}, [props.items, value, handleValue]);
|
||||||
|
|
||||||
const isValid = useFormFieldValidatorValue(id, validators, value);
|
const isValid = useFormFieldValidatorValue(id, validators, value);
|
||||||
|
|
||||||
const handleComboboxChange = (option: ComboboxItem): void => {
|
const setSelected = (option: T): void => {
|
||||||
if (props.onChange) props.onChange(option);
|
if (props.onChange) props.onChange(option);
|
||||||
setValue(option.label);
|
setValue(handleValue(option));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleComboboxBlur = (): void => {
|
const handleComboboxBlur = (): void => {
|
||||||
|
|
@ -166,8 +164,7 @@ export abstract class Form {
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
{...props}
|
{...props}
|
||||||
onChange={handleComboboxChange}
|
selected={[selected, setSelected]}
|
||||||
selectedValue={comboboxValue || ({} as ComboboxItem)}
|
|
||||||
onBlur={handleComboboxBlur}
|
onBlur={handleComboboxBlur}
|
||||||
error={validationEnabled && !isValid}
|
error={validationEnabled && !isValid}
|
||||||
/>
|
/>
|
||||||
|
|
@ -255,7 +252,7 @@ export abstract class Form {
|
||||||
|
|
||||||
const handleFileInputChange = async (
|
const handleFileInputChange = async (
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
e: React.ChangeEvent<HTMLInputElement>
|
||||||
): void => {
|
): Promise<void> => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
//Convert to string base64 to send to contract
|
//Convert to string base64 to send to contract
|
||||||
|
|
@ -294,12 +291,10 @@ export namespace Form {
|
||||||
'value' | 'error'
|
'value' | 'error'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type ComboboxProps = {
|
export type ComboboxProps<T> = Omit<Combobox.RootProps<T>, 'selected'> & {
|
||||||
onChange?: (option: ComboboxItem) => void;
|
handleValue: (item: T) => string;
|
||||||
} & Omit<
|
onChange?: (item: T) => void;
|
||||||
React.ComponentProps<typeof Combobox>,
|
};
|
||||||
'error' | 'selectedValue' | 'onChange'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type LogoFileInputProps = Omit<
|
export type LogoFileInputProps = Omit<
|
||||||
React.ComponentProps<typeof LogoFileInput>,
|
React.ComponentProps<typeof LogoFileInput>,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { ComboboxItem } from '@/components';
|
|
||||||
|
|
||||||
const listSites = [
|
const listSites = [
|
||||||
{
|
{
|
||||||
|
|
@ -29,13 +28,13 @@ const listSites = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const listBranches: ComboboxItem[] = [
|
const listBranches = [
|
||||||
{ value: '4573495837458934', label: 'main' },
|
{ value: '4573495837458934', label: 'main' },
|
||||||
{ value: '293857439857348', label: 'develop' },
|
{ value: '293857439857348', label: 'develop' },
|
||||||
{ value: '12344', label: 'feat/nabvar' },
|
{ value: '12344', label: 'feat/nabvar' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const fetchMintedSites = async (): Promise<ComboboxItem[]> => {
|
export const fetchMintedSites = async (): Promise<typeof listBranches> => {
|
||||||
return new Promise((resolved) => {
|
return new Promise((resolved) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolved(listBranches);
|
resolved(listBranches);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { ComboboxItem } from '@/components';
|
|
||||||
import { githubActions, RootState } from '@/store';
|
import { githubActions, RootState } from '@/store';
|
||||||
import { AppLog } from '@/utils';
|
import { AppLog } from '@/utils';
|
||||||
|
|
||||||
|
|
@ -25,7 +24,7 @@ export const fetchBranchesThunk = createAsyncThunk<void, FetchBranches>(
|
||||||
|
|
||||||
const branches = await githubClient.fetchBranches(owner, repository);
|
const branches = await githubClient.fetchBranches(owner, repository);
|
||||||
|
|
||||||
dispatch(githubActions.setBranches(branches as ComboboxItem[]));
|
dispatch(githubActions.setBranches(branches));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
AppLog.errorToast(
|
AppLog.errorToast(
|
||||||
'We have a problem trying to get your branches. Please try again later.'
|
'We have a problem trying to get your branches. Please try again later.'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import { AppLog } from '@/utils';
|
import { AppLog } from '@/utils';
|
||||||
|
|
||||||
import { GithubClient, UserData } from '../github-client';
|
import { GithubClient } from '../github-client';
|
||||||
import { githubActions } from '../github-slice';
|
import { githubActions } from '../github-slice';
|
||||||
|
|
||||||
export const fetchUserAndOrgsThunk = createAsyncThunk(
|
export const fetchUserAndOrgsThunk = createAsyncThunk(
|
||||||
|
|
@ -19,24 +19,22 @@ export const fetchUserAndOrgsThunk = createAsyncThunk(
|
||||||
|
|
||||||
const githubClient = new GithubClient(token);
|
const githubClient = new GithubClient(token);
|
||||||
|
|
||||||
const response = await Promise.all([
|
const [userResponse, orgsResponse] = await Promise.all([
|
||||||
githubClient.fetchUser(),
|
githubClient.fetchUser(),
|
||||||
githubClient.fetchOrgs(),
|
githubClient.fetchOrgs(),
|
||||||
]);
|
]);
|
||||||
const userResponse = response[0];
|
|
||||||
const orgsResponse = response[1];
|
|
||||||
|
|
||||||
let comboboxItems: UserData[] = [];
|
const items: GithubClient.UserData[] = [];
|
||||||
|
|
||||||
if (userResponse) {
|
if (userResponse) {
|
||||||
comboboxItems.push(userResponse);
|
items.push(userResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orgsResponse) {
|
if (orgsResponse) {
|
||||||
comboboxItems = [...comboboxItems, ...orgsResponse];
|
items.push(...orgsResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(githubActions.setUserAndOrgs(comboboxItems));
|
dispatch(githubActions.setUserAndOrgs(items));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
AppLog.errorToast('We have a problem. Please try again later.');
|
AppLog.errorToast('We have a problem. Please try again later.');
|
||||||
dispatch(githubActions.setQueryState('failed'));
|
dispatch(githubActions.setQueryState('failed'));
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,7 @@
|
||||||
import { Octokit } from 'octokit';
|
import { Octokit } from 'octokit';
|
||||||
|
|
||||||
import { DropdownItem } from '@/components';
|
|
||||||
|
|
||||||
import { GithubState } from './github-slice';
|
import { GithubState } from './github-slice';
|
||||||
|
|
||||||
export type UserData = {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
avatar: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class GithubClient {
|
export class GithubClient {
|
||||||
octokit: Octokit;
|
octokit: Octokit;
|
||||||
token: string;
|
token: string;
|
||||||
|
|
@ -21,7 +13,7 @@ export class GithubClient {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchUser(): Promise<UserData> {
|
async fetchUser(): Promise<GithubClient.UserData> {
|
||||||
const { data: userData } = await this.octokit.request('GET /user');
|
const { data: userData } = await this.octokit.request('GET /user');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -31,7 +23,7 @@ export class GithubClient {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchOrgs(): Promise<UserData[]> {
|
async fetchOrgs(): Promise<GithubClient.UserData[]> {
|
||||||
const { data: organizationsData } = await this.octokit.request(
|
const { data: organizationsData } = await this.octokit.request(
|
||||||
'GET /user/orgs'
|
'GET /user/orgs'
|
||||||
);
|
);
|
||||||
|
|
@ -63,7 +55,10 @@ export class GithubClient {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchBranches(owner: string, repo: string): Promise<DropdownItem[]> {
|
async fetchBranches(
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
): Promise<GithubClient.Branch[]> {
|
||||||
const branches = await this.octokit
|
const branches = await this.octokit
|
||||||
.request('GET /repos/{owner}/{repo}/branches', {
|
.request('GET /repos/{owner}/{repo}/branches', {
|
||||||
owner,
|
owner,
|
||||||
|
|
@ -72,8 +67,8 @@ export class GithubClient {
|
||||||
.then((res) =>
|
.then((res) =>
|
||||||
res.data.map((branch) => {
|
res.data.map((branch) => {
|
||||||
return {
|
return {
|
||||||
label: branch.name,
|
name: branch.name,
|
||||||
value: branch.commit.sha,
|
commit: branch.commit.sha,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -81,3 +76,16 @@ export class GithubClient {
|
||||||
return branches;
|
return branches;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace GithubClient {
|
||||||
|
export type UserData = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
avatar: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Branch = {
|
||||||
|
name: string;
|
||||||
|
commit: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { ComboboxItem } from '@/components';
|
|
||||||
import { RootState } from '@/store';
|
import { RootState } from '@/store';
|
||||||
import { useAppSelector } from '@/store/hooks';
|
import { useAppSelector } from '@/store/hooks';
|
||||||
|
|
||||||
import * as asyncThunk from './async-thunk';
|
import * as asyncThunk from './async-thunk';
|
||||||
import { UserData } from './github-client';
|
import { GithubClient } from './github-client';
|
||||||
|
|
||||||
export namespace GithubState {
|
export namespace GithubState {
|
||||||
export type Token = string;
|
export type Token = string;
|
||||||
|
|
@ -20,7 +19,7 @@ export namespace GithubState {
|
||||||
|
|
||||||
export type QueryLoading = 'idle' | 'loading' | 'failed' | 'success';
|
export type QueryLoading = 'idle' | 'loading' | 'failed' | 'success';
|
||||||
|
|
||||||
export type UserAndOrganizations = Array<UserData>;
|
export type UserAndOrganizations = Array<GithubClient.UserData>;
|
||||||
|
|
||||||
export type Repository = {
|
export type Repository = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -30,7 +29,7 @@ export namespace GithubState {
|
||||||
|
|
||||||
export type Repositories = Array<Repository>;
|
export type Repositories = Array<Repository>;
|
||||||
|
|
||||||
export type Branches = Array<ComboboxItem>;
|
export type Branches = Array<GithubClient.Branch>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GithubState {
|
export interface GithubState {
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from './github-slice';
|
export * from './github-slice';
|
||||||
|
export * from './github-client';
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
import { Card, Grid, Icon, IconButton, Stepper } from '@/components';
|
import { Card, Grid, Icon, IconButton, Stepper } from '@/components';
|
||||||
|
|
||||||
import { CreateAccessPoint } from './create-ap.context';
|
|
||||||
import { CreateAccessPointFormBody } from './create-ap.form-body';
|
import { CreateAccessPointFormBody } from './create-ap.form-body';
|
||||||
|
|
||||||
export const CreateAccessPointForm: React.FC = () => {
|
export const CreateAccessPointForm: React.FC = () => {
|
||||||
const { prevStep } = Stepper.useContext();
|
const { prevStep } = Stepper.useContext();
|
||||||
|
|
||||||
const { nfa } = CreateAccessPoint.useContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card.Container css={{ width: '$107h' }}>
|
<Card.Container css={{ width: '$107h' }}>
|
||||||
<Card.Heading
|
<Card.Heading
|
||||||
title={`Create Access Point - ${nfa.label || ''}`}
|
title="Create Access Point"
|
||||||
leftIcon={
|
leftIcon={
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Add"
|
aria-label="Add"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { ComboboxItem } from '@/components';
|
import { Token } from '@/graphclient';
|
||||||
import { EthereumHooks } from '@/integrations';
|
import { EthereumHooks } from '@/integrations';
|
||||||
import { useFleekERC721Billing } from '@/store';
|
import { useFleekERC721Billing } from '@/store';
|
||||||
import { AppLog, createContext, pushToast } from '@/utils';
|
import { AppLog, createContext, pushToast } from '@/utils';
|
||||||
|
|
||||||
|
type NFA = Pick<Token, 'id' | 'name'>;
|
||||||
|
|
||||||
export type AccessPointContext = {
|
export type AccessPointContext = {
|
||||||
billing: string | undefined;
|
billing: string | undefined;
|
||||||
nfa: ComboboxItem;
|
nfa: NFA | undefined;
|
||||||
setNfa: (nfa: ComboboxItem) => void;
|
setNfa: ReactState<NFA | undefined>[1];
|
||||||
};
|
};
|
||||||
|
|
||||||
const [CreateAPProvider, useContext] = createContext<AccessPointContext>({
|
const [CreateAPProvider, useContext] = createContext<AccessPointContext>({
|
||||||
|
|
@ -29,7 +31,7 @@ export abstract class CreateAccessPoint {
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [billing] = useFleekERC721Billing('AddAccessPoint');
|
const [billing] = useFleekERC721Billing('AddAccessPoint');
|
||||||
const [nfa, setNfa] = useState<ComboboxItem>({} as ComboboxItem);
|
const [nfa, setNfa] = useState<NFA>();
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
billing,
|
billing,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { Combobox, ComboboxItem } from '@/components';
|
import { Combobox } from '@/components';
|
||||||
import { getLatestNFAsDocument } from '@/graphclient';
|
import { getLatestNFAsDocument } from '@/graphclient';
|
||||||
import { AppLog } from '@/utils';
|
import { AppLog } from '@/utils';
|
||||||
|
|
||||||
|
|
@ -11,25 +11,26 @@ export const NfaPicker: React.FC = () => {
|
||||||
const { nfa, setNfa } = CreateAccessPoint.useContext();
|
const { nfa, setNfa } = CreateAccessPoint.useContext();
|
||||||
const { data, loading, error } = useQuery(getLatestNFAsDocument);
|
const { data, loading, error } = useQuery(getLatestNFAsDocument);
|
||||||
|
|
||||||
const handleNfaChange = (item: ComboboxItem): void => {
|
const items = useMemo(() => data?.tokens || [], [data]);
|
||||||
setNfa(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const items = useMemo(() => {
|
|
||||||
return data
|
|
||||||
? data.tokens.map(
|
|
||||||
(nfa) => ({ value: nfa.id, label: nfa.name } as ComboboxItem)
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (loading) return <div>Loading...</div>;
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
AppLog.errorToast('Error loading NFA list');
|
AppLog.errorToast('Error loading NFA list', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox items={items} selectedValue={nfa} onChange={handleNfaChange} />
|
<Combobox
|
||||||
|
isLoading={loading}
|
||||||
|
items={items}
|
||||||
|
selected={[nfa, setNfa]}
|
||||||
|
queryKey={['name', 'id']}
|
||||||
|
>
|
||||||
|
{({ Field, Options }) => (
|
||||||
|
<>
|
||||||
|
<Field>{(selected) => selected?.name || 'Select NFA'}</Field>
|
||||||
|
|
||||||
|
<Options>{(item) => item.name}</Options>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,74 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Combobox, ComboboxItem, Flex } from '@/components';
|
import { Combobox, Flex, Icon, IconName } from '@/components';
|
||||||
|
|
||||||
const itemsCombobox = [
|
type Item = { id: number; label: string; icon: IconName };
|
||||||
{ label: 'Item 1', value: 'item-1' },
|
|
||||||
{ label: 'Item 2', value: 'item-2' },
|
const Items: Item[] = [
|
||||||
{ label: 'Item 3', value: 'item-3' },
|
{ id: 1, label: 'Option 1', icon: 'branch' },
|
||||||
|
{ id: 2, label: 'Option 2', icon: 'ethereum' },
|
||||||
|
{ id: 3, label: 'Option 3', icon: 'metamask' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ComboboxTest: React.FC = () => {
|
export const ComboboxTest: React.FC = () => {
|
||||||
const [selectedValue, setSelectedValue] = useState({} as ComboboxItem);
|
const selected = useState<Item>();
|
||||||
const [selectedValueAutocomplete, setSelectedValueAutocomplete] = useState(
|
|
||||||
{} as ComboboxItem
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleComboboxChange = (value: ComboboxItem): void => {
|
|
||||||
setSelectedValue(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleComboboxChangeAutocomplete = (value: ComboboxItem): void => {
|
|
||||||
setSelectedValueAutocomplete(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
css={{
|
css={{
|
||||||
|
position: 'relative',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
margin: '100px',
|
|
||||||
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '10px',
|
gap: '10px',
|
||||||
|
width: '600px',
|
||||||
|
alignSelf: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h1>Components Test</h1>
|
<Combobox unattached items={Items} selected={selected} queryKey="label">
|
||||||
<Flex css={{ width: '600px', gap: '$2' }}>
|
{({ Field, Options }) => (
|
||||||
<Combobox
|
<>
|
||||||
items={itemsCombobox}
|
<Field>
|
||||||
selectedValue={selectedValue}
|
{(selected) => (
|
||||||
onChange={handleComboboxChange}
|
<>
|
||||||
leftIcon="github"
|
<Icon name={selected?.icon || 'search'} />
|
||||||
/>
|
{selected?.label || 'Select an option'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Combobox
|
<Options>
|
||||||
items={itemsCombobox}
|
{(item) => (
|
||||||
selectedValue={selectedValueAutocomplete}
|
<>
|
||||||
onChange={handleComboboxChangeAutocomplete}
|
<Icon name={item.icon} /> {item.label}
|
||||||
withAutocomplete
|
</>
|
||||||
/>
|
)}
|
||||||
</Flex>
|
</Options>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
|
||||||
|
<Combobox unattached items={Items} selected={selected} queryKey="label">
|
||||||
|
{({ Field, Options }) => (
|
||||||
|
<>
|
||||||
|
<Field>
|
||||||
|
{(selected) => (
|
||||||
|
<>
|
||||||
|
<Icon name={selected?.icon || 'search'} />
|
||||||
|
{selected?.label || 'Select an option'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Options>
|
||||||
|
{(item) => (
|
||||||
|
<>
|
||||||
|
<Icon name={item.icon} /> {item.label}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Options>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { Flex, NFACard, NFACardSkeleton, NoResults } from '@/components';
|
import { Flex, NFACard, NFACardSkeleton } from '@/components';
|
||||||
import { lastNFAsPaginatedDocument } from '@/graphclient';
|
import { lastNFAsPaginatedDocument } from '@/graphclient';
|
||||||
import { useWindowScrollEnd } from '@/hooks';
|
import { useWindowScrollEnd } from '@/hooks';
|
||||||
|
|
||||||
|
|
@ -75,7 +75,14 @@ export const NFAListFragment: React.FC = () => {
|
||||||
<NFACard data={token} key={token.id} />
|
<NFACard data={token} key={token.id} />
|
||||||
))}
|
))}
|
||||||
{isLoading && <LoadingSkeletons />}
|
{isLoading && <LoadingSkeletons />}
|
||||||
{!isLoading && tokens.length === 0 && <NoResults />}
|
{!isLoading && tokens.length === 0 && (
|
||||||
|
// TODO: update this after designs are done
|
||||||
|
<div
|
||||||
|
className={`relative cursor-default select-none pt-2 px-3.5 pb-4 text-slate11 text-center`}
|
||||||
|
>
|
||||||
|
Nothing found.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { ComboboxItem, Flex, Form, Spinner } from '@/components';
|
import { Flex, Form, Icon, Spinner } from '@/components';
|
||||||
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
|
import {
|
||||||
|
githubActions,
|
||||||
|
GithubClient,
|
||||||
|
useAppDispatch,
|
||||||
|
useGithubStore,
|
||||||
|
} from '@/store';
|
||||||
import { AppLog } from '@/utils';
|
import { AppLog } from '@/utils';
|
||||||
import { Mint } from '@/views/mint/mint.context';
|
import { Mint } from '@/views/mint/mint.context';
|
||||||
import { useMintFormContext } from '@/views/mint/nfa-step/form-step';
|
import { useMintFormContext } from '@/views/mint/nfa-step/form-step';
|
||||||
|
|
@ -24,33 +29,34 @@ export const RepoBranchCommitFields: React.FC = () => {
|
||||||
const { repositoryName, selectedUserOrg } = Mint.useContext();
|
const { repositoryName, selectedUserOrg } = Mint.useContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryLoading === 'idle') {
|
if (!(queryLoading === 'idle' && selectedUserOrg && repositoryName)) return;
|
||||||
dispatch(
|
dispatch(
|
||||||
githubActions.fetchBranchesThunk({
|
githubActions.fetchBranchesThunk({
|
||||||
owner: selectedUserOrg.label,
|
owner: selectedUserOrg?.label,
|
||||||
repository: repositoryName.name,
|
repository: repositoryName.name,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}, [queryLoading, dispatch, selectedUserOrg, repositoryName]);
|
||||||
}, [queryLoading, dispatch, selectedUserOrg.label, repositoryName.name]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
queryLoading === 'success' &&
|
queryLoading !== 'success' ||
|
||||||
branches.length > 0 &&
|
branches.length === 0 ||
|
||||||
repositoryName.defaultBranch !== undefined &&
|
!repositoryName ||
|
||||||
gitBranch === '' //we only set the default branch the first time
|
gitBranch !== ''
|
||||||
) {
|
)
|
||||||
const defaultBranch = branches.find(
|
return;
|
||||||
(branch) =>
|
|
||||||
branch.label.toLowerCase() ===
|
const defaultBranch = branches.find(
|
||||||
repositoryName.defaultBranch.toLowerCase()
|
(branch) =>
|
||||||
);
|
branch.name.toLowerCase() ===
|
||||||
if (defaultBranch) {
|
repositoryName.defaultBranch.toLowerCase()
|
||||||
setGitBranch(defaultBranch.label);
|
);
|
||||||
setGitCommit(defaultBranch.value);
|
|
||||||
}
|
if (defaultBranch) {
|
||||||
|
setGitBranch(defaultBranch.name);
|
||||||
|
setGitCommit(defaultBranch.commit);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
AppLog.errorToast('We had a problem. Try again');
|
AppLog.errorToast('We had a problem. Try again');
|
||||||
|
|
@ -58,7 +64,7 @@ export const RepoBranchCommitFields: React.FC = () => {
|
||||||
}, [
|
}, [
|
||||||
queryLoading,
|
queryLoading,
|
||||||
branches,
|
branches,
|
||||||
repositoryName.defaultBranch,
|
repositoryName,
|
||||||
gitBranch,
|
gitBranch,
|
||||||
setGitBranch,
|
setGitBranch,
|
||||||
setGitCommit,
|
setGitCommit,
|
||||||
|
|
@ -78,9 +84,9 @@ export const RepoBranchCommitFields: React.FC = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBranchChange = (branch: ComboboxItem): void => {
|
const handleBranchChange = (branch: GithubClient.Branch): void => {
|
||||||
setGitBranch(branch.label);
|
setGitBranch(branch.name);
|
||||||
setGitCommit(branch.value);
|
setGitCommit(branch.commit);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -88,10 +94,26 @@ export const RepoBranchCommitFields: React.FC = () => {
|
||||||
<Form.Field context={gitBranchContext}>
|
<Form.Field context={gitBranchContext}>
|
||||||
<Form.Label>Git Branch</Form.Label>
|
<Form.Label>Git Branch</Form.Label>
|
||||||
<Form.Combobox
|
<Form.Combobox
|
||||||
leftIcon="branch"
|
|
||||||
items={branches}
|
items={branches}
|
||||||
onChange={handleBranchChange}
|
onChange={handleBranchChange}
|
||||||
/>
|
queryKey="name"
|
||||||
|
handleValue={(item) => item.name}
|
||||||
|
>
|
||||||
|
{({ Field, Options }) => (
|
||||||
|
<>
|
||||||
|
<Field>
|
||||||
|
{(selected) => (
|
||||||
|
<>
|
||||||
|
<Icon name="branch" />
|
||||||
|
{selected?.name || 'Select a branch'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Options>{(item) => item.name}</Options>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.Combobox>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<Form.Field context={gitCommitContext}>
|
<Form.Field context={gitCommitContext}>
|
||||||
<Form.Label>Git Commit</Form.Label>
|
<Form.Label>Git Commit</Form.Label>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,6 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import { Card, Flex, Grid, Icon, IconButton, Spinner } from '@/components';
|
||||||
Card,
|
|
||||||
ComboboxItem,
|
|
||||||
Flex,
|
|
||||||
Grid,
|
|
||||||
Icon,
|
|
||||||
IconButton,
|
|
||||||
Spinner,
|
|
||||||
} from '@/components';
|
|
||||||
import { Input } from '@/components/core/input';
|
import { Input } from '@/components/core/input';
|
||||||
import { useDebounce } from '@/hooks/use-debounce';
|
import { useDebounce } from '@/hooks/use-debounce';
|
||||||
import { useGithubStore } from '@/store';
|
import { useGithubStore } from '@/store';
|
||||||
|
|
@ -50,7 +42,7 @@ export const GithubRepositoryConnection: React.FC = () => {
|
||||||
|
|
||||||
const handlePrevStepClick = (): void => {
|
const handlePrevStepClick = (): void => {
|
||||||
setGithubStep(1);
|
setGithubStep(1);
|
||||||
setSelectedUserOrg({} as ComboboxItem);
|
setSelectedUserOrg(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -79,13 +71,13 @@ export const GithubRepositoryConnection: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
<Card.Body css={{ pt: '$4' }}>
|
<Card.Body css={{ pt: '$4' }}>
|
||||||
<Grid css={{ rowGap: '$2' }}>
|
<Grid css={{ rowGap: '$2' }}>
|
||||||
<Flex css={{ gap: '$4', pr: '$3h' }}>
|
<Flex css={{ gap: '$4', pr: '$3h', position: 'relative' }}>
|
||||||
<UserOrgsCombobox />
|
<UserOrgsCombobox />
|
||||||
<Input
|
<Input
|
||||||
leftIcon="search"
|
leftIcon="search"
|
||||||
placeholder="Search repo"
|
placeholder="Search repo"
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
css="flex-1"
|
wrapperClassName="flex-1"
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
{queryLoading === 'loading' ||
|
{queryLoading === 'loading' ||
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { Flex, NoResults } from '@/components';
|
import { Flex } from '@/components';
|
||||||
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
|
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
|
||||||
import { Mint } from '@/views/mint/mint.context';
|
import { Mint } from '@/views/mint/mint.context';
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const RepositoriesList: React.FC<RepositoriesListProps> = ({
|
||||||
}, [searchValue, repositories]);
|
}, [searchValue, repositories]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryLoading === 'idle' && selectedUserOrg.value) {
|
if (queryLoading === 'idle' && selectedUserOrg?.value) {
|
||||||
dispatch(githubActions.fetchRepositoriesThunk(selectedUserOrg.value));
|
dispatch(githubActions.fetchRepositoriesThunk(selectedUserOrg.value));
|
||||||
}
|
}
|
||||||
}, [queryLoading, dispatch, selectedUserOrg]);
|
}, [queryLoading, dispatch, selectedUserOrg]);
|
||||||
|
|
@ -56,7 +56,12 @@ export const RepositoriesList: React.FC<RepositoriesListProps> = ({
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<NoResults css="text-center" />
|
// TODO: update this after designs are done
|
||||||
|
<div
|
||||||
|
className={`relative cursor-default select-none pt-2 px-3.5 pb-4 text-slate11 text-center`}
|
||||||
|
>
|
||||||
|
Nothing found.
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,37 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { Avatar, Combobox, ComboboxItem } from '@/components';
|
import { Avatar, Combobox, Icon } from '@/components';
|
||||||
import { githubActions, useAppDispatch, useGithubStore } from '@/store';
|
import {
|
||||||
|
githubActions,
|
||||||
|
GithubClient,
|
||||||
|
useAppDispatch,
|
||||||
|
useGithubStore,
|
||||||
|
} from '@/store';
|
||||||
import { AppLog } from '@/utils';
|
import { AppLog } from '@/utils';
|
||||||
import { Mint } from '@/views/mint/mint.context';
|
import { Mint } from '@/views/mint/mint.context';
|
||||||
|
|
||||||
|
const renderSelected = (selected?: GithubClient.UserData): JSX.Element => (
|
||||||
|
<>
|
||||||
|
{selected ? (
|
||||||
|
<Avatar
|
||||||
|
src={selected.avatar}
|
||||||
|
alt={selected.label}
|
||||||
|
css={{ fontSize: '$2xl' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon name="github" css={{ fontSize: '$2xl' }} />
|
||||||
|
)}
|
||||||
|
{selected?.label || 'Select'}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderItem = (item: GithubClient.UserData): JSX.Element => (
|
||||||
|
<>
|
||||||
|
<Avatar src={item.avatar} alt={item.label} />
|
||||||
|
{item.label}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
export const UserOrgsCombobox: React.FC = () => {
|
export const UserOrgsCombobox: React.FC = () => {
|
||||||
const { queryUserAndOrganizations, userAndOrganizations } = useGithubStore();
|
const { queryUserAndOrganizations, userAndOrganizations } = useGithubStore();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
@ -17,7 +44,9 @@ export const UserOrgsCombobox: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [dispatch, queryUserAndOrganizations]);
|
}, [dispatch, queryUserAndOrganizations]);
|
||||||
|
|
||||||
const handleUserOrgChange = (item: ComboboxItem): void => {
|
const handleUserOrgChange = (
|
||||||
|
item: GithubClient.UserData | undefined
|
||||||
|
): void => {
|
||||||
if (item) {
|
if (item) {
|
||||||
dispatch(githubActions.fetchRepositoriesThunk(item.value));
|
dispatch(githubActions.fetchRepositoriesThunk(item.value));
|
||||||
setSelectedUserOrg(item);
|
setSelectedUserOrg(item);
|
||||||
|
|
@ -29,10 +58,10 @@ export const UserOrgsCombobox: React.FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
queryUserAndOrganizations === 'success' &&
|
queryUserAndOrganizations === 'success' &&
|
||||||
selectedUserOrg.value === undefined &&
|
selectedUserOrg?.value === undefined &&
|
||||||
userAndOrganizations.length > 0
|
userAndOrganizations.length > 0
|
||||||
) {
|
) {
|
||||||
//SET first user
|
// sets the first user
|
||||||
setSelectedUserOrg(userAndOrganizations[0]);
|
setSelectedUserOrg(userAndOrganizations[0]);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|
@ -44,18 +73,18 @@ export const UserOrgsCombobox: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
items={userAndOrganizations.map(
|
items={userAndOrganizations}
|
||||||
(item) =>
|
unattached
|
||||||
({
|
css={{ flex: 1 }}
|
||||||
label: item.label,
|
selected={[selectedUserOrg, handleUserOrgChange]}
|
||||||
value: item.value,
|
queryKey="label"
|
||||||
icon: <Avatar src={item.avatar} />,
|
>
|
||||||
} as ComboboxItem)
|
{({ Field, Options }) => (
|
||||||
|
<>
|
||||||
|
<Field>{renderSelected}</Field>
|
||||||
|
<Options>{renderItem}</Options>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
selectedValue={selectedUserOrg}
|
</Combobox>
|
||||||
onChange={handleUserOrgChange}
|
|
||||||
leftIcon="github"
|
|
||||||
css="flex-1"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { ComboboxItem } from '@/components';
|
|
||||||
import { EthereumHooks } from '@/integrations';
|
import { EthereumHooks } from '@/integrations';
|
||||||
import { GithubState, useFleekERC721Billing } from '@/store';
|
import { GithubClient, GithubState, useFleekERC721Billing } from '@/store';
|
||||||
import { AppLog, createContext } from '@/utils';
|
import { AppLog, createContext } from '@/utils';
|
||||||
|
|
||||||
export type MintContext = {
|
export type MintContext = {
|
||||||
billing: string | undefined;
|
billing: string | undefined;
|
||||||
selectedUserOrg: ComboboxItem;
|
selectedUserOrg: GithubClient.UserData | undefined;
|
||||||
repositoryName: GithubState.Repository;
|
repositoryName: GithubState.Repository | undefined;
|
||||||
githubStep: number;
|
githubStep: number;
|
||||||
nfaStep: number;
|
nfaStep: number;
|
||||||
verifyNFA: boolean;
|
verifyNFA: boolean;
|
||||||
setGithubStep: (step: number) => void;
|
setGithubStep: (step: number) => void;
|
||||||
setNfaStep: (step: number) => void;
|
setNfaStep: (step: number) => void;
|
||||||
setSelectedUserOrg: (userOrgValue: ComboboxItem) => void;
|
setSelectedUserOrg: (userOrgValue: GithubClient.UserData | undefined) => void;
|
||||||
setRepositoryName: (repo: GithubState.Repository) => void;
|
setRepositoryName: (repo: GithubState.Repository | undefined) => void;
|
||||||
setVerifyNFA: (verify: boolean) => void;
|
setVerifyNFA: (verify: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -35,9 +34,10 @@ export abstract class Mint {
|
||||||
|
|
||||||
static readonly Provider: React.FC<Mint.ProviderProps> = ({ children }) => {
|
static readonly Provider: React.FC<Mint.ProviderProps> = ({ children }) => {
|
||||||
//Github Connection
|
//Github Connection
|
||||||
const [selectedUserOrg, setSelectedUserOrg] = useState({} as ComboboxItem);
|
const [selectedUserOrg, setSelectedUserOrg] =
|
||||||
|
useState<GithubClient.UserData>();
|
||||||
const [repositoryName, setRepositoryName] =
|
const [repositoryName, setRepositoryName] =
|
||||||
useState<GithubState.Repository>({} as GithubState.Repository);
|
useState<GithubState.Repository>();
|
||||||
const [githubStep, setGithubStepContext] = useState(1);
|
const [githubStep, setGithubStepContext] = useState(1);
|
||||||
|
|
||||||
//NFA Details
|
//NFA Details
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { DomainField } from './domain-field';
|
||||||
import { EnsField } from './ens-field';
|
import { EnsField } from './ens-field';
|
||||||
|
|
||||||
export const EnsDomainField: React.FC = () => (
|
export const EnsDomainField: React.FC = () => (
|
||||||
<Flex css={{ columnGap: '$4' }}>
|
<Flex css={{ columnGap: '$4', position: 'relative' }}>
|
||||||
<EnsField />
|
<EnsField />
|
||||||
<DomainField />
|
<DomainField />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useAccount } from 'wagmi';
|
import { useAccount } from 'wagmi';
|
||||||
|
|
||||||
import { getENSNamesDocument } from '@/../.graphclient';
|
import { getENSNamesDocument } from '@/../.graphclient';
|
||||||
import { ComboboxItem, Form } from '@/components';
|
import { Form } from '@/components';
|
||||||
import { AppLog } from '@/utils';
|
import { AppLog } from '@/utils';
|
||||||
|
|
||||||
import { useMintFormContext } from '../../mint-form.context';
|
import { useMintFormContext } from '../../mint-form.context';
|
||||||
|
|
||||||
export const EnsField: React.FC = () => {
|
export const EnsField: React.FC = () => {
|
||||||
const { address } = useAccount();
|
const { address } = useAccount();
|
||||||
const { data, error } = useQuery(getENSNamesDocument, {
|
const { data, error, loading } = useQuery(getENSNamesDocument, {
|
||||||
variables: {
|
variables: {
|
||||||
address: address?.toLowerCase() || '', //should skip if undefined
|
address: address?.toLowerCase() || '', //should skip if undefined
|
||||||
},
|
},
|
||||||
|
|
@ -34,25 +34,30 @@ export const EnsField: React.FC = () => {
|
||||||
}, [error, showError]);
|
}, [error, showError]);
|
||||||
|
|
||||||
const ensNames = useMemo(() => {
|
const ensNames = useMemo(() => {
|
||||||
const ensList: ComboboxItem[] = [];
|
if (!(data && data.account && data.account.domains)) return [];
|
||||||
if (data && data.account && data.account.domains) {
|
return data.account.domains.map((ens) => ens.name as string);
|
||||||
data.account.domains.forEach((ens) => {
|
|
||||||
const { name } = ens;
|
|
||||||
if (name) {
|
|
||||||
ensList.push({
|
|
||||||
label: name,
|
|
||||||
value: name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return ensList;
|
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Field context={ens} css={{ flex: 1 }}>
|
<Form.Field context={ens} css={{ flex: 1 }}>
|
||||||
<Form.Label>ENS</Form.Label>
|
<Form.Label>ENS</Form.Label>
|
||||||
<Form.Combobox items={ensNames} />
|
<Form.Combobox
|
||||||
|
unattached
|
||||||
|
isLoading={loading}
|
||||||
|
items={ensNames}
|
||||||
|
handleValue={(item) => item}
|
||||||
|
>
|
||||||
|
{({ Field, Options, Message }) => (
|
||||||
|
<>
|
||||||
|
<Field>{(selected) => selected || 'Select an ENS'}</Field>
|
||||||
|
|
||||||
|
<Options>
|
||||||
|
{(item) => item}
|
||||||
|
<Message>No owned ENS names found</Message>
|
||||||
|
</Options>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.Combobox>
|
||||||
<Form.Overline />
|
<Form.Overline />
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue