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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"license": "MIT",
"dependencies": {
"@stackone/expressions": "^0.16.0",
"@stackone/malachite": "^0.3.2",
"@stackone/malachite": "^0.4.0",
"@tanstack/react-query": "^5.77.2"
},
"peerDependencies": {
Expand Down
13 changes: 10 additions & 3 deletions src/modules/integration-picker/IntegrationPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const IntegrationPicker: React.FC<IntegrationPickerProps> = ({
setSelectedIntegration,
setFormData,
handleConnect,
resetConnectionState,
} = useIntegrationPicker({
token,
baseUrl,
Expand All @@ -56,26 +57,32 @@ export const IntegrationPicker: React.FC<IntegrationPickerProps> = ({
dashboardUrl,
});

const onBack = () => {
setSelectedIntegration(null);
resetConnectionState();
};

return (
<Card
footer={
<CardFooter
selectedIntegration={selectedIntegration}
isLoading={connectionState.loading}
onBack={accountData ? undefined : () => setSelectedIntegration(null)}
showActions={!connectionState.loading && !connectionState.success}
onBack={accountData ? undefined : onBack}
onNext={handleConnect}
/>
}
title={
selectedIntegration && (
<CardTitle
selectedIntegration={selectedIntegration}
onBack={accountData ? undefined : () => setSelectedIntegration(null)}
onBack={accountData ? undefined : onBack}
guide={guide}
/>
)
}
height={height}
padding="0"
>
{isHubLinkAccountReleaseEnabled && (
<IntegrationPickerContent
Expand Down
137 changes: 75 additions & 62 deletions src/modules/integration-picker/components/IntegrationFields.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Alert, Dropdown, Form, Input, Spacer, TextArea, Typography } from '@stackone/malachite';
import {
Alert,
Dropdown,
Form,
Input,
Padded,
Spacer,
TextArea,
Typography,
} from '@stackone/malachite';
import { useEffect, useState } from 'react';
import { ConnectorConfigField } from '../types';

Expand Down Expand Up @@ -62,68 +71,72 @@ export const IntegrationForm: React.FC<IntegrationFieldsProps> = ({
};

return (
<Spacer direction="vertical" size={8} fullWidth>
{guide && <Alert type="info" message={guide?.description} hasMargin={false} />}
{error && <Alert type="error" message={error.message} hasMargin={false} />}
{error && <Typography.CodeText>{error.provider_response}</Typography.CodeText>}
<Spacer direction="vertical" size={20} fullWidth>
{fields.map((field) => {
const key =
typeof field.key === 'object'
? JSON.stringify(field.key)
: String(field.key);
return (
<div key={key} style={{ width: '100%' }}>
{(field.type === 'text' ||
field.type === 'number' ||
field.type === 'password') && (
<Input
name={key}
required={field.required}
placeholder={field.placeholder}
defaultValue={field.value?.toString()}
onChange={(value: string) => handleFieldChange(key, value)}
disabled={field.readOnly}
label={field.label}
tooltip={field.guide?.tooltip}
description={field.guide?.description}
type={field.type}
/>
)}
<Padded vertical="large" horizontal="medium">
<Spacer direction="vertical" size={8} fullWidth>
{guide && <Alert type="info" message={guide?.description} hasMargin={false} />}
{error && <Alert type="error" message={error.message} hasMargin={false} />}
{error && <Typography.CodeText>{error.provider_response}</Typography.CodeText>}
<Spacer direction="vertical" size={20} fullWidth>
{fields.map((field) => {
const key =
typeof field.key === 'object'
? JSON.stringify(field.key)
: String(field.key);
return (
<div key={key} style={{ width: '100%' }}>
{(field.type === 'text' ||
field.type === 'number' ||
field.type === 'password') && (
<Input
name={key}
required={field.required}
placeholder={field.placeholder}
defaultValue={field.value?.toString()}
onChange={(value: string) => handleFieldChange(key, value)}
disabled={field.readOnly}
label={field.label}
tooltip={field.guide?.tooltip}
description={field.guide?.description}
type={field.type}
/>
)}

{field.type === 'text_area' && (
<TextArea
name={key}
required={field.required}
defaultValue={formData[key] || ''}
placeholder={field.placeholder}
onChange={(value: string) => handleFieldChange(key, value)}
disabled={field.readOnly}
label={field.label}
tooltip={field.guide?.tooltip}
/>
)}
{field.type === 'select' && (
<Dropdown
defaultValue={formData[key] || ''}
disabled={field.readOnly}
items={
field.options?.map((option) => ({
id: option.value,
label: option.label,
})) ?? []
}
onItemSelected={(value) => handleFieldChange(key, value ?? '')}
name={key}
label={field.label}
tooltip={field.guide?.tooltip}
description={field.guide?.description}
/>
)}
</div>
);
})}
{field.type === 'text_area' && (
<TextArea
name={key}
required={field.required}
defaultValue={formData[key] || ''}
placeholder={field.placeholder}
onChange={(value: string) => handleFieldChange(key, value)}
disabled={field.readOnly}
label={field.label}
tooltip={field.guide?.tooltip}
/>
)}
{field.type === 'select' && (
<Dropdown
defaultValue={formData[key] || ''}
disabled={field.readOnly}
items={
field.options?.map((option) => ({
id: option.value,
label: option.label,
})) ?? []
}
onItemSelected={(value) =>
handleFieldChange(key, value ?? '')
}
Comment thread
adefreitas marked this conversation as resolved.
name={key}
label={field.label}
tooltip={field.guide?.tooltip}
description={field.guide?.description}
/>
)}
</div>
);
})}
</Spacer>
</Spacer>
</Spacer>
</Padded>
);
};
91 changes: 80 additions & 11 deletions src/modules/integration-picker/components/IntegrationList.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import {
ButtonList,
Divider,
Flex,
FlexAlign,
FlexDirection,
FlexGapSize,
FlexJustify,
Input,
Padded,
PillButton,
Spacer,
Typography,
} from '@stackone/malachite';
import { useCallback, useMemo, useState } from 'react';
import { CATEGORIES_WITH_LABELS } from '../../../shared/categories';
import { Integration } from '../types';

Expand Down Expand Up @@ -52,20 +57,84 @@ export const IntegrationList: React.FC<{
integrations: Integration[];
onSelect: (integration: Integration) => void;
}> = ({ integrations, onSelect }) => {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);

const [search, setSearch] = useState<string>('');

const handleCategoryClick = useCallback(
(category: string) => {
if (selectedCategory === category) {
setSelectedCategory(null);
} else {
setSelectedCategory(category);
}
},
[selectedCategory],
);

const availableIntegrations = useMemo(() => {
return integrations.filter(
(integration) =>
integration.active &&
integration.name &&
(selectedCategory ? integration.type === selectedCategory : true) &&
(search ? integration.name.toLowerCase().includes(search.toLowerCase()) : true),
);
}, [integrations, selectedCategory, search]);

const availableCategories = useMemo(() => {
return Array.from(new Set(integrations.map((integration) => integration.type)));
}, [integrations]);

return (
<>
<Padded vertical="medium" horizontal="small" fullHeight={false}>
<Typography.SecondaryText>Select integration</Typography.SecondaryText>
</Padded>
<ButtonList
buttons={integrations
?.filter((integration) => integration.active && integration.name)
.map((integration) => ({
key: integration.provider,
children: <IntegrationRow integration={integration} />,
onClick: () => onSelect(integration),
}))}
<Input
name="search"
placeholder="Search Integrations"
variant="ghost"
size="large"
onChange={setSearch}
/>
<Divider />
<Padded vertical="small" horizontal="medium" fullHeight={false}>
<Spacer direction="horizontal" size={4} align="start">
{availableCategories.length > 1 &&
availableCategories.map((category) => (
<PillButton
key={category}
label={
CATEGORIES_WITH_LABELS.find((c) => c.value === category)
?.label || category
}
selected={selectedCategory === category}
onClick={() => handleCategoryClick(category)}
/>
))}
</Spacer>
</Padded>
<Divider />
{availableIntegrations.length > 0 ? (
<Padded vertical="small" horizontal="small" fullHeight={true}>
<Spacer direction="vertical" size={10} align="start">
<Padded vertical="none" horizontal="small">
<Typography.SecondaryText className="text-left">
Add integration
</Typography.SecondaryText>
</Padded>
<ButtonList
buttons={availableIntegrations.map((integration) => ({
key: integration.provider,
children: <IntegrationRow integration={integration} />,
onClick: () => onSelect(integration),
}))}
/>
</Spacer>
</Padded>
) : (
<Flex justify={FlexJustify.Center} align={FlexAlign.Center} fullHeight={true}>
<Typography.SecondaryText>No integrations found</Typography.SecondaryText>
</Flex>
)}
</>
);
};
10 changes: 5 additions & 5 deletions src/modules/integration-picker/components/cardFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ import { Integration } from '../types';
interface CardFooterProps {
selectedIntegration: Integration | null;
fullWidth?: boolean;
isLoading?: boolean;
showActions?: boolean;
onBack?: () => void;
onNext: () => void;
}

const CardFooter: React.FC<CardFooterProps> = ({
fullWidth = true,
selectedIntegration,
isLoading,
showActions,
onBack,
onNext,
}) => {
const buttons = useMemo(() => {
if (!selectedIntegration || isLoading) {
if (!selectedIntegration || !showActions) {
return [];
}

Expand Down Expand Up @@ -57,7 +57,7 @@ const CardFooter: React.FC<CardFooterProps> = ({
});

return buttons;
}, [selectedIntegration, onBack, onNext, isLoading]);
}, [selectedIntegration, onBack, onNext, showActions]);

if (buttons.length === 0) {
return <FooterLinks fullWidth={fullWidth} />;
Expand All @@ -66,7 +66,7 @@ const CardFooter: React.FC<CardFooterProps> = ({
return (
<Spacer direction="horizontal" size={0} justifyContent="space-between">
<FooterLinks fullWidth={fullWidth} />
<Padded vertical="none" horizontal="medium" fullHeight={false}>
<Padded vertical="none" horizontal="small" fullHeight={false}>
<Flex direction={FlexDirection.Horizontal} justify={FlexJustify.Right}>
<Spacer direction="horizontal" size={10}>
{buttons.map((button) => (
Expand Down
5 changes: 5 additions & 0 deletions src/modules/integration-picker/hooks/useIntegrationPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ export const useIntegrationPicker = ({
const isLoading = isLoadingHubData || isLoadingConnectorData || isLoadingAccountData;
const hasError = !!(errorHubData || errorConnectorData || errorAccountData);

const resetConnectionState = useCallback(() => {
setConnectionState({ loading: false, success: false });
}, []);

return {
// Data
hubData,
Expand All @@ -390,5 +394,6 @@ export const useIntegrationPicker = ({
setSelectedIntegration,
setFormData,
handleConnect,
resetConnectionState,
};
};