Skip to content

Implement selective import of Variable Collections and Modes #3380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React, { useState, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Checkbox, Label, Stack, Heading } from '@tokens-studio/ui';
import Modal from './Modal';
import Box from './Box';
import { VariableCollectionInfo, SelectedCollections } from '@/types/VariableCollectionSelection';

type Props = {
isOpen: boolean;
onClose: () => void;
onConfirm: (selectedCollections: SelectedCollections, options: { useDimensions: boolean; useRem: boolean }) => void;
collections: VariableCollectionInfo[];
};

export default function ImportVariablesDialog({ isOpen, onClose, onConfirm, collections }: Props) {
const { t } = useTranslation();
const [selectedCollections, setSelectedCollections] = useState<SelectedCollections>({});
const [useDimensions, setUseDimensions] = useState(false);
const [useRem, setUseRem] = useState(false);

Check failure on line 20 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

't' is assigned a value but never used. Allowed unused vars must match /^_/u

Check failure on line 20 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / copilot

't' is assigned a value but never used. Allowed unused vars must match /^_/u
// Log collections data for debugging
console.log('Collections passed to dialog:', collections);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove all debugging logs that you added

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed all debugging console.log statements from ImportVariablesDialog and useImportVariables in commit 50a2e4b.

// Initialize all collections as selected with all modes selected by default
useEffect(() => {
if (collections.length > 0) {
const initialSelection: SelectedCollections = {};
collections.forEach((collection) => {
initialSelection[collection.id] = {
name: collection.name,
selectedModes: collection.modes.map((mode) => mode.modeId),
};
});
setSelectedCollections(initialSelection);
console.log('Initial selection set:', initialSelection);
}
}, [collections]);

const handleCollectionToggle = useCallback((collectionId: string, collectionName: string, modes: { modeId: string; name: string }[]) => {
setSelectedCollections((prev) => {
const isCurrentlySelected = prev[collectionId];
if (isCurrentlySelected) {
// Remove collection
const newSelection = { ...prev };
delete newSelection[collectionId];
return newSelection;
} else {
// Add collection with all modes selected
return {
...prev,
[collectionId]: {
name: collectionName,
selectedModes: modes.map((mode) => mode.modeId),
},
};
}
});
}, []);

const handleModeToggle = useCallback((collectionId: string, modeId: string) => {
setSelectedCollections((prev) => {
const collection = prev[collectionId];
if (!collection) return prev;

const isCurrentlySelected = collection.selectedModes.includes(modeId);
const newSelectedModes = isCurrentlySelected
? collection.selectedModes.filter((id) => id !== modeId)
: [...collection.selectedModes, modeId];

// If no modes are selected, remove the collection entirely
if (newSelectedModes.length === 0) {
const newSelection = { ...prev };
delete newSelection[collectionId];
return newSelection;
}

return {
...prev,
[collectionId]: {
...collection,
selectedModes: newSelectedModes,
},
};
});
}, []);

const handleConfirm = useCallback(() => {
onConfirm(selectedCollections, { useDimensions, useRem });
}, [selectedCollections, useDimensions, useRem, onConfirm]);

const hasSelections = Object.keys(selectedCollections).length > 0;

return (
<Modal
title="Import variables"
showClose
isOpen={isOpen}
close={onClose}
footer={(
<Stack direction="row" gap={4} justify="between">
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
<Button variant="primary" disabled={!hasSelections} onClick={handleConfirm}>
Import
</Button>
</Stack>
)}
>
<Stack direction="column" gap={4} css={{ padding: '$4' }}>
<Box css={{ fontSize: '$small', color: '$fgMuted' }}>
Select which variable collections and modes to import. Sets will be created for each selected mode.
</Box>

{/* Options */}
<Stack direction="column" gap={2}>
<Heading size="small">Options</Heading>
<Stack direction="row" gap={2} align="center">

Check failure on line 118 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / copilot

JSX props should not use arrow functions
<Checkbox
checked={useDimensions}

Check failure on line 120 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / copilot

JSX props should not use arrow functions
onCheckedChange={(checked) => setUseDimensions(checked === true)}
id="useDimensions"
/>
<Label htmlFor="useDimensions">

Check failure on line 124 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

JSX props should not use arrow functions
Convert numbers to dimensions
</Label>
</Stack>
<Stack direction="row" gap={2} align="center">
<Checkbox
checked={useRem}

Check failure on line 130 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / copilot

JSX props should not use arrow functions
onCheckedChange={(checked) => setUseRem(checked === true)}
id="useRem"
/>
<Label htmlFor="useRem">

Check failure on line 134 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

JSX props should not use arrow functions
Use rem for dimension values
</Label>
</Stack>
</Stack>

{/* Collections */}
<Stack direction="column" gap={3}>
<Heading size="small">Variable Collections</Heading>
{collections.length === 0 ? (
<Box css={{ padding: '$3', backgroundColor: '$bgMuted', borderRadius: '$small', textAlign: 'center' }}>
There are no collections present in this file
</Box>
) : (
collections.map((collection) => {
const isCollectionSelected = !!selectedCollections[collection.id];
const selectedModes = selectedCollections[collection.id]?.selectedModes || [];
const allModesSelected = isCollectionSelected && selectedModes.length === collection.modes.length;

return (

Check failure on line 153 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / copilot

'allModesSelected' is assigned a value but never used. Allowed unused vars must match /^_/u
<Box key={collection.id} css={{ borderLeft: '2px solid $borderMuted', paddingLeft: '$3' }}>
<Stack direction="column" gap={2}>
<Stack direction="row" gap={2} align="center">
<Checkbox

Check failure on line 157 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'allModesSelected' is assigned a value but never used. Allowed unused vars must match /^_/u
checked={isCollectionSelected}
onCheckedChange={() => handleCollectionToggle(collection.id, collection.name, collection.modes)}
id={`collection-${collection.id}`}
/>

Check failure on line 161 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / copilot

JSX props should not use arrow functions
<Label htmlFor={`collection-${collection.id}`} css={{ color: '$fgDefault', fontWeight: 'bold', userSelect: 'none' }}>
{collection.name || `Collection ${collection.id.slice(0, 8)}`}
</Label>
</Stack>

Check failure on line 165 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

JSX props should not use arrow functions

{isCollectionSelected && (
<Stack direction="column" gap={1} css={{ marginLeft: '$4' }}>
{collection.modes.map((mode) => (
<Stack key={mode.modeId} direction="row" gap={2} align="center">
<Checkbox
checked={selectedModes.includes(mode.modeId)}
onCheckedChange={() => handleModeToggle(collection.id, mode.modeId)}
id={`mode-${collection.id}-${mode.modeId}`}
/>

Check failure on line 175 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / copilot

JSX props should not use arrow functions
<Label htmlFor={`mode-${collection.id}-${mode.modeId}`} css={{ color: '$fgDefault', userSelect: 'none' }}>
{mode.name || `Mode ${mode.modeId.slice(0, 8)}`}
</Label>
</Stack>

Check failure on line 179 in packages/tokens-studio-for-figma/src/app/components/ImportVariablesDialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

JSX props should not use arrow functions
))}
</Stack>
)}
</Stack>
</Box>
);
})
)}
</Stack>

{!hasSelections && (
<Box css={{ padding: '$3', backgroundColor: '$dangerBg', color: '$dangerFg', borderRadius: '$small' }}>
Please select at least one collection and mode to import.
</Box>
)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a special treatment: If there are no variable collections at all - render something that says "There are no collections present in this file"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added empty state handling with the message "There are no collections present in this file" when no variable collections are available in commit 618ada6.

</Stack>
</Modal>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,44 @@ import { useTranslation } from 'react-i18next';
import { DropdownMenu, Button } from '@tokens-studio/ui';
import useTokens from '../store/useTokens';

import { activeTokenSetReadOnlySelector, editProhibitedSelector } from '@/selectors';
import { activeTokenSetReadOnlySelector, editProhibitedSelector, themesListSelector } from '@/selectors';
import ManageStylesAndVariables from './ManageStylesAndVariables/ManageStylesAndVariables';
import ImportVariablesDialog from './ImportVariablesDialog';
import { useImportVariables } from '../hooks/useImportVariables';
import { useIsProUser } from '../hooks/useIsProUser';

export default function StylesDropdown() {
const editProhibited = useSelector(editProhibitedSelector);
const activeTokenSetReadOnly = useSelector(activeTokenSetReadOnlySelector);
const themes = useSelector(themesListSelector);
const proUser = useIsProUser();
const importDisabled = editProhibited || activeTokenSetReadOnly;

const { pullStyles, pullVariables } = useTokens();
const { pullStyles } = useTokens();
const { t } = useTranslation(['tokens']);

const [showModal, setShowModal] = React.useState(false);
const {
isDialogOpen,
collections,
isLoading,
openDialog,
closeDialog,
importVariables,
} = useImportVariables();

const handleOpenModal = useCallback(() => {
setShowModal(true);
}, [setShowModal]);

const handleImportVariables = useCallback(() => {
openDialog();
}, [openDialog]);

const handleConfirmImport = useCallback((selectedCollections, options) => {
importVariables(selectedCollections, options, themes, proUser);
}, [importVariables, themes, proUser]);

return (
<>
<DropdownMenu>
Expand All @@ -33,12 +54,18 @@ export default function StylesDropdown() {
<DropdownMenu.Portal>
<DropdownMenu.Content side="top">
<DropdownMenu.Item textValue="Export styles & variables" onSelect={handleOpenModal}>{t('exportStylesAndVariables')}</DropdownMenu.Item>
<DropdownMenu.Item textValue="Import variables" disabled={importDisabled} onSelect={pullVariables}>{t('importVariables')}</DropdownMenu.Item>
<DropdownMenu.Item textValue="Import variables" disabled={importDisabled || isLoading} onSelect={handleImportVariables}>{t('importVariables')}</DropdownMenu.Item>
<DropdownMenu.Item textValue="Import styles" disabled={importDisabled} onSelect={pullStyles}>{t('importStyles')}</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
{showModal && <ManageStylesAndVariables showModal={showModal} setShowModal={setShowModal} />}
<ImportVariablesDialog
isOpen={isDialogOpen}
onClose={closeDialog}
onConfirm={handleConfirmImport}
collections={collections}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useState, useCallback } from 'react';
import { AsyncMessageChannel } from '@/AsyncMessageChannel';
import { AsyncMessageTypes } from '@/types/AsyncMessages';
import { VariableCollectionInfo, SelectedCollections } from '@/types/VariableCollectionSelection';
import { ThemeObjectsList } from '@/types/ThemeObjectsList';

export function useImportVariables() {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [collections, setCollections] = useState<VariableCollectionInfo[]>([]);
const [isLoading, setIsLoading] = useState(false);

const openDialog = useCallback(async () => {
setIsLoading(true);
try {
// Fetch available collections first
const response = await AsyncMessageChannel.ReactInstance.message({
type: AsyncMessageTypes.GET_AVAILABLE_VARIABLE_COLLECTIONS,
});

console.log('Received collections in hook:', response.collections);
setCollections(response.collections);
setIsDialogOpen(true);
} catch (error) {
console.error('Error fetching variable collections:', error);
setCollections([]);
} finally {
setIsLoading(false);
}
}, []);

const closeDialog = useCallback(() => {
setIsDialogOpen(false);
setCollections([]);
}, []);

const importVariables = useCallback(async (
selectedCollections: SelectedCollections,
options: { useDimensions: boolean; useRem: boolean },
themes: ThemeObjectsList,
proUser: boolean
) => {
try {
await AsyncMessageChannel.ReactInstance.message({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add a track() analytics call here letting us know what options were chosen, and the amount of themes that people select here (number) as well as the total number of themes available

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added analytics tracking for variable imports with the following metrics: useDimensions, useRem options, number of selected collections vs total collections, and number of selected modes vs total available modes in commit 50a2e4b.

type: AsyncMessageTypes.PULL_VARIABLES,
options: {
useDimensions: options.useDimensions,
useRem: options.useRem,
selectedCollections,
},
themes,
proUser,
});
closeDialog();
} catch (error) {
console.error('Error importing variables:', error);
}
}, [closeDialog]);

return {
isDialogOpen,
collections,
isLoading,
openDialog,
closeDialog,
importVariables,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,6 @@ describe('useToken test', () => {
await expect(result.current.pullStyles()).resolves.not.toThrow();
});

it('pullVariables test', async () => {
mockConfirm.mockImplementation(() => Promise.resolve());
await act(async () => {
await result.current.pullVariables();
});
await expect(result.current.pullVariables()).resolves.not.toThrow();
});

it('should send message to pull styles from figma', async () => {
const messageSpy = jest.spyOn(AsyncMessageChannel.ReactInstance, 'message');
mockConfirm.mockImplementation(() => Promise.resolve({
Expand Down
24 changes: 0 additions & 24 deletions packages/tokens-studio-for-figma/src/app/store/useTokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
const store = useStore<RootState>();
const tokensContext = useContext(TokensContext);
const shouldConfirm = useMemo(() => updateMode === UpdateMode.DOCUMENT, [updateMode]);
const proUser = useIsProUser();

Check failure on line 72 in packages/tokens-studio-for-figma/src/app/store/useTokens.tsx

View workflow job for this annotation

GitHub Actions / ESLint

'proUser' is assigned a value but never used. Allowed unused vars must match /^_/u

Check failure on line 72 in packages/tokens-studio-for-figma/src/app/store/useTokens.tsx

View workflow job for this annotation

GitHub Actions / copilot

'proUser' is assigned a value but never used. Allowed unused vars must match /^_/u
const VALID_TOKEN_TYPES = [
TokenTypes.DIMENSION,
TokenTypes.BORDER_RADIUS,
Expand Down Expand Up @@ -189,29 +189,7 @@
}
}, [confirm]);

const pullVariables = useCallback(async () => {
const userDecision = await confirm({
text: 'Import variables',
description: 'Sets will be created for each variable mode.',
choices: [
{ key: 'useDimensions', label: 'Convert numbers to dimensions', enabled: false },
{ key: 'useRem', label: 'Use rem for dimension values', enabled: false },
],
confirmAction: 'Import',
});

if (userDecision) {
AsyncMessageChannel.ReactInstance.message({
type: AsyncMessageTypes.PULL_VARIABLES,
options: {
useDimensions: userDecision.data.includes('useDimensions'),
useRem: userDecision.data.includes('useRem'),
},
themes,
proUser,
});
}
}, [confirm, themes, proUser]);

const removeTokensByValue = useCallback((data: RemoveTokensByValueData) => {
track('removeTokensByValue', { count: data.length });
Expand Down Expand Up @@ -779,7 +757,6 @@
createStylesFromSelectedTokenSets,
createStylesFromSelectedThemes,
pullStyles,
pullVariables,
remapToken,
remapTokensInGroup,
removeTokensByValue,
Expand All @@ -805,7 +782,6 @@
createStylesFromSelectedTokenSets,
createStylesFromSelectedThemes,
pullStyles,
pullVariables,
remapToken,
remapTokensInGroup,
removeTokensByValue,
Expand Down
Loading
Loading