Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/attach-all-variables-feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tokens-studio/figma-plugin": patch
---

Add "Attach all variables" button to Manage themes dialog that attaches local variables to all themes at once, respecting collection and mode names. Also add loading prop to "Attach local variables" and "Attach local styles" buttons to show progress feedback.
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import debounce from 'lodash.debounce';
import { Button, EmptyState } from '@tokens-studio/ui';
import { styled } from '@stitches/react';
import { useTranslation } from 'react-i18next';
import { activeThemeSelector, themesListSelector } from '@/selectors';
import {
activeThemeSelector, themesListSelector, tokensSelector, isWaitingForBackgroundJobSelector,
} from '@/selectors';
import Modal from '../Modal';
import { Dispatch } from '@/app/store';
import { Dispatch, RootState } from '@/app/store';
import Stack from '../Stack';
import IconPlus from '@/icons/plus.svg';
import { CreateOrEditThemeForm, FormValues } from './CreateOrEditThemeForm';
Expand All @@ -26,6 +28,10 @@ import { TreeItem, themeListToTree } from '@/utils/themeListToTree';
import { ItemData } from '@/context';
import { checkReorder } from '@/utils/motion';
import { ensureFolderIsTogether, findOrderableTargetIndexesInThemeList } from '@/utils/dragDropOrder';
import { AsyncMessageChannel } from '@/AsyncMessageChannel';
import { AsyncMessageTypes } from '@/types/AsyncMessages';
import { BackgroundJobs } from '@/constants/BackgroundJobs';
import { wrapTransaction } from '@/profiling/transaction';

type Props = unknown;

Expand All @@ -44,6 +50,10 @@ export const ManageThemesModal: React.FC<React.PropsWithChildren<React.PropsWith
const dispatch = useDispatch<Dispatch>();
const themes = useSelector(themesListSelector);
const activeTheme = useSelector(activeThemeSelector);
const tokens = useSelector(tokensSelector);
const isAttachingLocalVariables = useSelector(useCallback((state: RootState) => (
isWaitingForBackgroundJobSelector(state, BackgroundJobs.UI_ATTACHING_LOCAL_VARIABLES)
), []));
const { confirm } = useConfirm();
const [themeEditorOpen, setThemeEditorOpen] = useState<boolean | string>(false);
const [themeListScrollPosition, setThemeListScrollPosition] = useState<number>(0);
Expand Down Expand Up @@ -186,6 +196,47 @@ export const ManageThemesModal: React.FC<React.PropsWithChildren<React.PropsWith

const debouncedHandleThemeListScroll = useMemo(() => debounce(handleThemeListScroll, 200), [handleThemeListScroll]);

const handleAttachAllVariables = useCallback(async () => {
if (themes.length === 0) return;

dispatch.uiState.startJob({
name: BackgroundJobs.UI_ATTACHING_LOCAL_VARIABLES,
isInfinite: true,
});

const themeVariableResults: Record<string, any> = {};

for (const theme of themes) {
try {
const result = await wrapTransaction({ name: 'attachVariables' }, async () => await AsyncMessageChannel.ReactInstance.message({
type: AsyncMessageTypes.ATTACH_LOCAL_VARIABLES_TO_THEME,
tokens,
theme,
}));

if (result.variableInfo) {
themeVariableResults[theme.id] = result.variableInfo;
}
} catch (error) {
console.error(`Error attaching variables to theme ${theme.name}:`, error);
}
}

const totalAttached = Object.values(themeVariableResults).reduce((sum, info) => (
sum + Object.values(info.variableIds || {}).length
), 0);

if (totalAttached > 0) {
track('Attach variables to all themes', {
count: totalAttached,
themesProcessed: Object.keys(themeVariableResults).length,
});
dispatch.tokenState.assignVariableIdsToTheme(themeVariableResults);
}

dispatch.uiState.completeJob(BackgroundJobs.UI_ATTACHING_LOCAL_VARIABLES);
}, [themes, tokens, dispatch]);

return (
<Modal
id="manage-themes-modal"
Expand All @@ -197,14 +248,25 @@ export const ManageThemesModal: React.FC<React.PropsWithChildren<React.PropsWith
footer={(
<Stack gap={2} direction="row" justify="end">
{!themeEditorOpen && (
<Button
data-testid="button-manage-themes-modal-new-theme"
variant="secondary"
icon={<IconPlus />}
onClick={handleToggleOpenThemeEditor}
>
{t('newTheme')}
</Button>
<>
<Button
data-testid="button-manage-themes-modal-attach-all-variables"
variant="secondary"
disabled={isAttachingLocalVariables || themes.length === 0}
loading={isAttachingLocalVariables}
onClick={handleAttachAllVariables}
>
{t('attachAllVariables')}
</Button>
<Button
data-testid="button-manage-themes-modal-new-theme"
variant="secondary"
icon={<IconPlus />}
onClick={handleToggleOpenThemeEditor}
>
{t('newTheme')}
</Button>
</>
)}
{themeEditorOpen && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const ThemeStyleManagementCategory: React.FC<React.PropsWithChildren<Reac
<Button
size="small"
disabled={isAttachingLocalStyles}
loading={isAttachingLocalStyles}
onClick={onAttachLocalStyles}
>
{t('attachLocalStyles')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export const ThemeVariableManagement: React.FC<React.PropsWithChildren<React.Pro
<Button
size="small"
disabled={isAttachingLocalVariables}
loading={isAttachingLocalVariables}
onClick={handleAttachLocalVariables}
>
Attach local variables
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ jest.mock('../../../hooks/useConfirm', () => ({
}),
}));

jest.mock('@/AsyncMessageChannel', () => ({
AsyncMessageChannel: {
ReactInstance: {
message: jest.fn(),
},
},
}));

describe('ManageThemesModal', () => {
it('should render', () => {
const mockStore = createMockStore({
Expand Down Expand Up @@ -89,4 +97,46 @@ describe('ManageThemesModal', () => {
expect(result.getByText('delete')).toBeInTheDocument();
});
});

it('should render attach all variables button', () => {
const mockStore = createMockStore({
tokenState: {
themes: [
{
id: 'light',
name: 'Light',
selectedTokenSets: {},
$figmaStyleReferences: {},
},
{
id: 'dark',
name: 'Dark',
selectedTokenSets: {},
$figmaStyleReferences: {},
},
],
},
});

const result = render(
<Provider store={mockStore}>
<ManageThemesModal />
</Provider>,
);

expect(result.getByTestId('button-manage-themes-modal-attach-all-variables')).toBeInTheDocument();
});

it('should disable attach all variables button when no themes', () => {
const mockStore = createMockStore({});

const result = render(
<Provider store={mockStore}>
<ManageThemesModal />
</Provider>,
);

const button = result.getByTestId('button-manage-themes-modal-attach-all-variables');
expect(button).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"returnToOverview": "Return to overview",
"stylesVarMultiDimensionalThemesWarning": "Note: When using multi-dimensional themes where values depend on tokens of another theme, connecting styles might not work as expected.",
"attachLocalStyles": "Attach local styles",
"attachAllVariables": "Attach all variables",
"selectAll": "Select all",
"detachSelected": "Detach selected",
"saveTheme": "Save theme",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
"returnToOverview": "Volver a la descripción general",
"stylesVarMultiDimensionalThemesWarning": "Nota: Cuando se utilizan temas multidimensionales donde los valores dependen de tokens de otro tema, es posible que la conexión de estilos no funcione como se esperaba.",
"attachLocalStyles": "Adjuntar estilos locales",
"attachAllVariables": "Adjuntar todas las variables",
"selectAll": "Seleccionar todo",
"detachSelected": "Separar seleccionado",
"saveTheme": "Guardar tema",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
"returnToOverview": "Retour à l'aperçu",
"stylesVarMultiDimensionalThemesWarning": "Remarque : lors de l'utilisation de thèmes multidimensionnels dont les valeurs dépendent des jetons d'un autre thème, les styles de connexion peuvent ne pas fonctionner comme prévu.",
"attachLocalStyles": "Attacher des styles locaux",
"attachAllVariables": "Attacher toutes les variables",
"selectAll": "Tout sélectionner",
"detachSelected": "Détacher la sélection",
"saveTheme": "Enregistrer le thème",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
"returnToOverview": "सिंहावलोकन पर लौटें",
"stylesVarMultiDimensionalThemesWarning": "नोट: बहु-आयामी थीम का उपयोग करते समय जहां मान किसी अन्य थीम के टोकन पर निर्भर करते हैं, कनेक्टिंग शैलियाँ अपेक्षा के अनुरूप काम नहीं कर सकती हैं।",
"attachLocalStyles": "स्थानीय शैलियाँ संलग्न करें",
"attachAllVariables": "सभी चर संलग्न करें",
"selectAll": "सबका चयन करें",
"detachSelected": "अलग करें चयनित",
"saveTheme": "थीम सहेजें",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
"returnToOverview": "Terug naar overzicht",
"stylesVarMultiDimensionalThemesWarning": "Opmerking: bij het gebruik van multidimensionale thema's waarbij waarden afhankelijk zijn van tokens van een ander thema, werken verbindingsstijlen mogelijk niet zoals verwacht.",
"attachLocalStyles": "Voeg lokale stijlen toe",
"attachAllVariables": "Voeg alle variabelen toe",
"selectAll": "Selecteer alles",
"detachSelected": "Geselecteerd loskoppelen",
"saveTheme": "Thema opslaan",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@
"returnToOverview": "返回概览",
"stylesVarMultiDimensionalThemesWarning": "注意:当使用多维主题时,其中值取决于另一个主题的标记,连接样式可能无法按预期工作。",
"attachLocalStyles": "附加本地样式",
"attachAllVariables": "附加所有变量",
"selectAll": "全选",
"detachSelected": "分离选定的",
"saveTheme": "保存主题",
Expand Down