Skip to content

Commit

Permalink
feat: Tags input for entering scopes (NangoHQ#556)
Browse files Browse the repository at this point in the history
* feat: tags input

* chore: remove comment lines

* fix: change tooltip text

* fix: lint and type-errors
  • Loading branch information
Chakravarthy7102 authored Apr 20, 2023
1 parent c02b0fe commit 7340924
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 16 deletions.
76 changes: 76 additions & 0 deletions packages/webapp/src/components/ui/TagsInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { forwardRef, type KeyboardEvent, useState } from 'react';
import { X } from '@geist-ui/icons';

import useSet from '../../hooks/useSet';

type TagsInputProps = Omit<JSX.IntrinsicElements['input'], 'defaultValue'> & { defaultValue: string };

const TagsInput = forwardRef<HTMLInputElement, TagsInputProps>(function TagsInput({ className, defaultValue, ...props }, ref) {
const defaultScopes = !!defaultValue ? defaultValue.split(',') : [];
const [enteredValue, setEnteredValue] = useState('');
const [selectedScopes, addToScopesSet, removeFromSelectedSet] = useSet<string>(defaultScopes);

function handleEnter(e: KeyboardEvent<HTMLInputElement>) {
//quick check for empty inputs
if (e.key === 'Enter') {
e.preventDefault();
if (!!e.currentTarget.value) {
e.currentTarget.value = '';
handleAdd(e.currentTarget.value);
}
}
}

function handleAdd(value?: string) {
if (value) {
addToScopesSet(value.trim());
} else {
addToScopesSet(enteredValue);
setEnteredValue('');
}
}

function removeScope(scopeToBeRemoved: string) {
removeFromSelectedSet(scopeToBeRemoved);
}

return (
<>
<div className="flex gap-3">
<input required value={Array.from(selectedScopes.values()).join(',')} {...props} hidden />
<input
ref={ref}
value={enteredValue}
onChange={(e) => setEnteredValue(e.currentTarget.value)}
onKeyDown={handleEnter}
minLength={1}
className="border-border-gray bg-bg-black text-text-light-gray focus:border-white focus:ring-white block h-11 w-full appearance-none rounded-md border px-3 py-2 text-base placeholder-gray-400 shadow-sm focus:outline-none"
/>
<button
onClick={() => handleAdd()}
type="button"
className="text-center px-8 text-sm font-medium bg-white text-black rounded-lg cursor-pointer"
>
Add
</button>
</div>
{!!Array.from(selectedScopes.values()).length && (
<div className="px-2 pt-2 mt-3 pb-11 mb-3 flex flex-wrap rounded-lg border border-border-gray">
{Array.from(selectedScopes.values()).map((selectedScope, i) => {
return (
<span
key={selectedScope + i}
className="flex flex-wrap gap-2 pl-4 pr-2 py-2 m-1 justify-between items-center text-sm font-medium rounded-lg cursor-pointer bg-gray-100 text-black"
>
{selectedScope}
<X onClick={() => removeScope(selectedScope)} className="h-5 w-5" />
</span>
);
})}
</div>
)}
</>
);
});

export default TagsInput;
29 changes: 29 additions & 0 deletions packages/webapp/src/hooks/useSet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useState, useRef, useCallback } from 'react';

export default function useSet<T>(initialValue?: T[], limit?: number) {
const [, setInc] = useState(false);

const set = useRef(new Set<T>(initialValue));

const add = useCallback(
(item: T) => {
if (set.current.has(item) || (limit && Array.from(set.current.values()).length >= limit)) return;
setInc((prev) => !prev);
set.current.add(item);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[setInc]
);

const remove = useCallback(
(item: T) => {
if (!set.current.has(item)) return;
setInc((prev) => !prev);
set.current.delete(item);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[setInc]
);

return [set.current, add, remove] as const;
}
24 changes: 8 additions & 16 deletions packages/webapp/src/pages/IntegrationCreate.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { useNavigate, useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
import { useState, useEffect } from 'react';
import AlertOverLay from '../components/AlertOverLay';
import { HelpCircle } from '@geist-ui/icons';
import { Tooltip } from '@geist-ui/core';
import { Prism } from '@mantine/prism';

import { defaultCallback } from '../utils/utils';
import {
useGetIntegrationDetailsAPI,
useGetProvidersAPI,
Expand All @@ -15,6 +10,12 @@ import {
useCreateIntegrationAPI,
useDeleteIntegrationAPI
} from '../utils/api';
import AlertOverLay from '../components/AlertOverLay';
import { HelpCircle } from '@geist-ui/icons';
import { Tooltip } from '@geist-ui/core';
import { defaultCallback } from '../utils/utils';
import { Prism } from '@mantine/prism';
import TagsInput from '../components/ui/TagsInput';
import { LeftNavBarItems } from '../components/LeftNavBar';
import DashboardLayout from '../layout/DashboardLayout';

Expand Down Expand Up @@ -218,17 +219,8 @@ export default function IntegrationCreate() {
<HelpCircle color="gray" className="h-5 ml-1"></HelpCircle>
</Tooltip>
</div>

<div className="mt-1" key={selectedProvider}>
<input
id="unique_key"
name="unique_key"
type="text"
required
defaultValue={selectedProvider}
minLength={1}
className="border-border-gray bg-bg-black text-text-light-gray focus:border-white focus:ring-white block h-11 w-full appearance-none rounded-md border px-3 py-2 text-base placeholder-gray-400 shadow-sm focus:outline-none"
/>
<div className="mt-1">
<TagsInput id="scopes" name="scopes" type="text" required defaultValue={selectedProvider} minLength={1} />
</div>
</div>
</div>
Expand Down

0 comments on commit 7340924

Please sign in to comment.