Skip to content

Commit

Permalink
feat: ignore first part of token name for variables (#1992)
Browse files Browse the repository at this point in the history
Applies the same behavior to variables as could be previously done to
styles.

Let me know if this should be done differently in some way (e.g. a
separate settings value?).
  • Loading branch information
mihkeleidast authored Nov 27, 2023
1 parent caebd05 commit 28cd5d7
Show file tree
Hide file tree
Showing 22 changed files with 174 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-numbers-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tokens-studio/figma-plugin': minor
---

Allow skipping first part of token name when creating variables, similarly to how it has worked for styles
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const mockSettings: SavedSettings = {
width: 800,
height: 500,
ignoreFirstPartForStyles: false,
ignoreFirstPartForVariables: false,
inspectDeep: false,
prefixStylesWithThemeName: false,
showEmptyGroups: true,
Expand Down
11 changes: 11 additions & 0 deletions src/app/components/Settings/Settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ describe('Settings Component', () => {
expect(store.getState().settings.ignoreFirstPartForStyles).toBe(true);
});

it('can toggle ignoreFirstPartForVariables', () => {
render(<Settings />);

const checkbox = screen.getByTestId('ignoreFirstPartForVariables');
fireEvent.click(checkbox, {
target: checkbox,
});

expect(store.getState().settings.ignoreFirstPartForVariables).toBe(true);
});

it('can toggle prefixStylesWithThemeName', () => {
render(<Settings />);

Expand Down
38 changes: 35 additions & 3 deletions src/app/components/Settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Heading from '../Heading';
import { Dispatch } from '../../store';
import Label from '../Label';
import {
ignoreFirstPartForStylesSelector, storeTokenIdInJsonEditorSelector, prefixStylesWithThemeNameSelector, uiStateSelector,
ignoreFirstPartForStylesSelector, ignoreFirstPartForVariablesSelector, storeTokenIdInJsonEditorSelector, prefixStylesWithThemeNameSelector, uiStateSelector,
} from '@/selectors';
import Stack from '../Stack';
import Box from '../Box';
Expand All @@ -38,6 +38,7 @@ function Settings() {

const { removeRelaunchData } = useDebug();
const ignoreFirstPartForStyles = useSelector(ignoreFirstPartForStylesSelector);
const ignoreFirstPartForVariables = useSelector(ignoreFirstPartForVariablesSelector);
const prefixStylesWithThemeName = useSelector(prefixStylesWithThemeNameSelector);
const storeTokenIdInJsonEditor = useSelector(storeTokenIdInJsonEditorSelector);
const uiState = useSelector(uiStateSelector);
Expand Down Expand Up @@ -82,13 +83,20 @@ function Settings() {
}
getSessionId();
});
const handleIgnoreChange = React.useCallback(
const handleIgnoreFirstPartForStylesChange = React.useCallback(
(state: CheckedState) => {
track('setIgnoreFirstPartForStyles', { value: state });
dispatch.settings.setIgnoreFirstPartForStyles(!!state);
},
[dispatch.settings],
);
const handleIgnoreFirstPartForVariablesChange = React.useCallback(
(state: CheckedState) => {
track('setIgnoreFirstPartForVariables', { value: state });
dispatch.settings.setIgnoreFirstPartForVariables(!!state);
},
[dispatch.settings],
);

const handlePrefixWithThemeNameChange = React.useCallback(
(state: CheckedState) => {
Expand Down Expand Up @@ -142,7 +150,7 @@ function Settings() {
id="ignoreFirstPartForStyles"
checked={!!ignoreFirstPartForStyles}
defaultChecked={ignoreFirstPartForStyles}
onCheckedChange={handleIgnoreChange}
onCheckedChange={handleIgnoreFirstPartForStylesChange}
/>
<Label htmlFor="ignoreFirstPartForStyles">
<Stack direction="column" gap={2}>
Expand Down Expand Up @@ -176,6 +184,30 @@ function Settings() {
</Stack>
</Label>
</Stack>
<Stack direction="row" gap={3} align="start">
<Checkbox
id="ignoreFirstPartForVariables"
checked={!!ignoreFirstPartForVariables}
defaultChecked={ignoreFirstPartForVariables}
onCheckedChange={handleIgnoreFirstPartForVariablesChange}
/>
<Label htmlFor="ignoreFirstPartForVariables">
<Stack direction="column" gap={2}>
<Box css={{ fontWeight: '$bold' }}>{t('ignoreVariablesPrefix')}</Box>
<Box css={{ color: '$fgMuted', fontSize: '$xsmall', lineHeight: 1.5 }}>
{t('usefulIgnore')}
{' '}
<code>colors</code>
{' '}
{t('inAToken')}
{' '}
<code>colors.blue.500</code>
{' '}
{t('forYourVariables')}
</Box>
</Stack>
</Label>
</Stack>
{idStorage
&& (
<Stack direction="row" gap={3} align="start">
Expand Down
5 changes: 5 additions & 0 deletions src/app/store/models/settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ describe('settings', () => {
expect(store.getState().settings.ignoreFirstPartForStyles).toEqual(true);
});

it('should be able to set ignoreFirstPartForVariables', () => {
store.dispatch.settings.setIgnoreFirstPartForVariables(true);
expect(store.getState().settings.ignoreFirstPartForVariables).toEqual(true);
});

it('should be able to set baseFontSize', () => {
store.dispatch.settings.setBaseFontSize('24px');
expect(store.getState().settings.baseFontSize).toEqual('24px');
Expand Down
18 changes: 12 additions & 6 deletions src/app/store/models/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ type WindowSettingsType = {
type TokenModeType = 'object' | 'array';

export interface SettingsState {
language: string,
language: string;
uiWindow?: WindowSettingsType;
updateMode: UpdateMode;
updateRemote: boolean;
updateOnChange?: boolean;
updateStyles?: boolean;
tokenType?: TokenModeType;
ignoreFirstPartForStyles?: boolean;
ignoreFirstPartForVariables?: boolean;
prefixStylesWithThemeName?: boolean;
inspectDeep: boolean;
shouldSwapStyles: boolean;
Expand Down Expand Up @@ -187,6 +188,12 @@ export const settings = createModel<RootModel>()({
ignoreFirstPartForStyles: payload,
};
},
setIgnoreFirstPartForVariables(state, payload: boolean) {
return {
...state,
ignoreFirstPartForVariables: payload,
};
},
setStoreTokenIdInJsonEditorSelector(state, payload: boolean) {
return {
...state,
Expand Down Expand Up @@ -231,6 +238,9 @@ export const settings = createModel<RootModel>()({
setIgnoreFirstPartForStyles: (payload, rootState) => {
setUI(rootState.settings);
},
setIgnoreFirstPartForVariables: (payload, rootState) => {
setUI(rootState.settings);
},
setInspectDeep: (payload, rootState) => {
setUI(rootState.settings);
},
Expand All @@ -250,10 +260,6 @@ export const settings = createModel<RootModel>()({
setStoreTokenIdInJsonEditorSelector: (payload, rootState) => {
setUI(rootState.settings);
},
...Object.fromEntries(
(Object.entries(settingsStateEffects).map(([key, factory]) => (
[key, factory()]
))),
),
...Object.fromEntries(Object.entries(settingsStateEffects).map(([key, factory]) => [key, factory()])),
}),
});
2 changes: 2 additions & 0 deletions src/i18n/lang/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"whereTokensStoredOnboarding": "Connect your tokens to an external source of truth that you can push and pull to. This allows you to use tokens across files.",
"baseFontExplanation": "Lets you configure the value 1rem represents. You can also set this to a token, to have it change between sets.",
"ignorePrefix": "Ignore first part of token name for styles",
"ignoreVariablesPrefix": "Ignore first part of token name for variables",
"prefixStyles": "Prefix styles with active theme name",
"prefixStylesExplanation": "Adds the active theme name to any styles created. Note: Using this with multi-dimensional themes will lead to unexpected results.",
"storeTokenId": "Store IDs on storage",
Expand All @@ -12,6 +13,7 @@
"usefulIgnore": "Useful if you want to ignore",
"inAToken": "in a token called",
"forYourStyles": "for your styles",
"forYourVariables": "for your variables",
"resetOnboarding": "Reset onboarding",
"removeRelaunchData": {
"button": "Remove deprecated data",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/lang/es/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
"secondScreenExplainer": "Potencie su flujo de trabajo de diseño, permitiéndole administrar sus tokens de diseño en una segunda pantalla.",
"language": "Idioma",
"languageExplainer": "Tokens Studio ha sido traducido automáticamente, por lo tanto, las traducciones pueden parecer inexactas.\n ¿No ves tu idioma? Enviar una solicitud de función",
"liveSyncActive": "Sincronización en vivo activa"
"liveSyncActive": "Sincronización en vivo activa",
"ignoreVariablesPrefix": "Ignorar la primera parte del nombre del token para las variables",
"forYourVariables": "para tus variables"
}
4 changes: 3 additions & 1 deletion src/i18n/lang/fr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
"secondScreenExplainer": "Renforcez votre flux de travail de conception en vous permettant de gérer vos jetons de conception sur un deuxième écran.",
"language": "Langue",
"languageExplainer": "Tokens Studio a été traduit automatiquement, donc les traductions peuvent sembler inexactes.\n Vous ne voyez pas votre langue ? Soumettre une demande de fonctionnalité",
"liveSyncActive": "Synchronisation en direct active"
"liveSyncActive": "Synchronisation en direct active",
"ignoreVariablesPrefix": "Ignorer la première partie du nom du jeton pour les variables",
"forYourVariables": "pour vos variables"
}
4 changes: 3 additions & 1 deletion src/i18n/lang/hi/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
"secondScreenExplainer": "अपने डिज़ाइन वर्कफ़्लो को सशक्त बनाएं, जिससे आप दूसरी स्क्रीन में अपने डिज़ाइन टोकन प्रबंधित कर सकें।",
"language": "भाषा",
"languageExplainer": "टोकन स्टूडियो का मशीनी अनुवाद किया गया है, इसलिए अनुवाद गलत लग सकते हैं।\n अपनी भाषा नहीं देखते? एक सुविधा अनुरोध सबमिट करें",
"liveSyncActive": "लाइव सिंक सक्रिय"
"liveSyncActive": "लाइव सिंक सक्रिय",
"ignoreVariablesPrefix": "वेरिएबल के लिए टोकन नाम के पहले भाग को अनदेखा करें",
"forYourVariables": "आपके चरों के लिए"
}
4 changes: 3 additions & 1 deletion src/i18n/lang/nl/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
"secondScreenExplainer": "Versterk uw ontwerpworkflow, zodat u uw ontwerptokens in een tweede scherm kunt beheren.",
"language": "Taal",
"languageExplainer": "Tokens Studio is automatisch vertaald, daarom kunnen vertalingen onnauwkeurig lijken.\n Zie je je taal niet? Dien een functieverzoek in",
"liveSyncActive": "Live synchronisatie actief"
"liveSyncActive": "Live synchronisatie actief",
"ignoreVariablesPrefix": "Negeer het eerste deel van de tokennaam voor variabelen",
"forYourVariables": "voor uw variabelen"
}
4 changes: 3 additions & 1 deletion src/i18n/lang/zh/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
"secondScreenExplainer": "增强您的设计工作流程,让您可以在第二个屏幕中管理您的 Token。",
"language": "语言",
"languageExplainer": "Tokens Studio 已经过 Mr. Biscuit 手动翻译,因此翻译较为准确。\n 没有看到您的语言?提交功能请求",
"liveSyncActive": "实时同步激活"
"liveSyncActive": "实时同步激活",
"ignoreVariablesPrefix": "忽略变量的令牌名称的第一部分",
"forYourVariables": "为你的变量"
}
1 change: 1 addition & 0 deletions src/plugin/asyncMessageHandlers/setUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const setUi: AsyncMessageChannelHandlers[AsyncMessageTypes.SET_UI] = asyn
updateOnChange: msg.updateOnChange,
updateStyles: msg.updateStyles,
ignoreFirstPartForStyles: msg.ignoreFirstPartForStyles,
ignoreFirstPartForVariables: msg.ignoreFirstPartForVariables,
prefixStylesWithThemeName: msg.prefixStylesWithThemeName,
inspectDeep: msg.inspectDeep,
baseFontSize: msg.baseFontSize,
Expand Down
1 change: 1 addition & 0 deletions src/plugin/notifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type SavedSettings = {
updateOnChange: boolean;
updateStyles: boolean;
ignoreFirstPartForStyles: boolean;
ignoreFirstPartForVariables: boolean;
prefixStylesWithThemeName: boolean;
inspectDeep: boolean;
shouldSwapStyles: boolean;
Expand Down
55 changes: 51 additions & 4 deletions src/plugin/setValuesOnVariable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { mockCreateVariable } from '../../tests/__mocks__/figmaMock';
import { SingleToken } from '@/types/tokens';
import setValuesOnVariable from './setValuesOnVariable';
import { TokenTypes } from '@/constants/TokenTypes';
import { SettingsState } from '@/app/store/models/settings';

describe('SetValuesOnVariable', () => {
const mockSetValueForMode = jest.fn();
Expand Down Expand Up @@ -33,8 +34,8 @@ describe('SetValuesOnVariable', () => {
type: TokenTypes.BORDER_RADIUS,
variableId: '123',
},
] as SingleToken<true, { path: string, variableId: string }>[];
setValuesOnVariable(variablesInFigma, tokens, collection, mode);
] as SingleToken<true, { path: string; variableId: string }>[];
setValuesOnVariable(variablesInFigma, tokens, collection, mode, {} as SettingsState);
expect(mockSetValueForMode).toBeCalledWith(mode, 8);
});

Expand All @@ -47,8 +48,54 @@ describe('SetValuesOnVariable', () => {
value: '16',
type: TokenTypes.SIZING,
},
] as SingleToken<true, { path: string, variableId: string }>[];
setValuesOnVariable(variablesInFigma, tokens, collection, mode);
] as SingleToken<true, { path: string; variableId: string }>[];
setValuesOnVariable(variablesInFigma, tokens, collection, mode, {} as SettingsState);
expect(mockCreateVariable).toBeCalledWith('button/primary/width', 'VariableCollectionId:309:16430', 'FLOAT');
});

describe('ignoreFirstPartForVariables=true', () => {
const variablesInFigma2 = [
{
id: 'VariableID:309:16431',
key: '123',
name: 'primary/borderRadius',
setValueForMode: mockSetValueForMode,
} as unknown as Variable,
{
id: 'VariableID:309:16432',
key: '124',
name: 'primary/height',
setValueForMode: mockSetValueForMode,
} as unknown as Variable,
] as Variable[];
const settings = { ignoreFirstPartForVariables: true } as SettingsState;
it('when there is a variable which is connected to the token, we just update the value', () => {
const tokens = [
{
name: 'button.primary.borderRadius',
path: 'button/primary/borderRadius',
rawValue: '{accent.default}',
value: '8',
type: TokenTypes.BORDER_RADIUS,
variableId: '123',
},
] as SingleToken<true, { path: string; variableId: string }>[];
setValuesOnVariable(variablesInFigma2, tokens, collection, mode, settings);
expect(mockSetValueForMode).toBeCalledWith(mode, 8);
});

it('when there is no variable which is connected to the token, we create new variable', () => {
const tokens = [
{
name: 'button.primary.width',
path: 'button/primary/width',
rawValue: '{accent.onAccent}',
value: '16',
type: TokenTypes.SIZING,
},
] as SingleToken<true, { path: string; variableId: string }>[];
setValuesOnVariable(variablesInFigma2, tokens, collection, mode, settings);
expect(mockCreateVariable).toBeCalledWith('primary/width', 'VariableCollectionId:309:16430', 'FLOAT');
});
});
});
16 changes: 12 additions & 4 deletions src/plugin/setValuesOnVariable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import setNumberValuesOnVariable from './setNumberValuesOnVariable';
import setStringValuesOnVariable from './setStringValuesOnVariable';
import { convertTokenTypeToVariableType } from '@/utils/convertTokenTypeToVariableType';
import { checkCanReferenceVariable } from '@/utils/alias/checkCanReferenceVariable';
import { convertTokenNameToPath } from '@/utils/convertTokenNameToPath';
import { SettingsState } from '@/app/store/models/settings';

export type ReferenceVariableType = {
variable: Variable;
Expand All @@ -14,19 +16,22 @@ export type ReferenceVariableType = {

export default function setValuesOnVariable(
variablesInFigma: Variable[],
tokens: SingleToken<true, { path: string, variableId: string }>[],
tokens: SingleToken<true, { path: string; variableId: string }>[],
collection: VariableCollection,
mode: string,
settings: SettingsState,
) {
const variableKeyMap: Record<string, string> = {};
const referenceVariableCandidates: ReferenceVariableType[] = [];
try {
tokens.forEach((t) => {
const variableType = convertTokenTypeToVariableType(t.type);
// Find the connected variable
let variable = variablesInFigma.find((v) => (v.key === t.variableId && !v.remote) || v.name === t.path);
const slice = settings?.ignoreFirstPartForVariables ? 1 : 0;
const tokenPath = convertTokenNameToPath(t.name, null, slice);
let variable = variablesInFigma.find((v) => (v.key === t.variableId && !v.remote) || v.name === tokenPath);
if (!variable) {
variable = figma.variables.createVariable(t.path, collection.id, variableType);
variable = figma.variables.createVariable(tokenPath, collection.id, variableType);
}
if (variable) {
variable.description = t.description ?? '';
Expand Down Expand Up @@ -59,11 +64,14 @@ export default function setValuesOnVariable(
referenceTokenName = t.rawValue!.toString().substring(1);
}
variableKeyMap[t.name] = variable.key;

const referenceVariable = convertTokenNameToPath(referenceTokenName, null, slice);

if (checkCanReferenceVariable(t)) {
referenceVariableCandidates.push({
variable,
modeId: mode,
referenceVariable: referenceTokenName.split('.').join('/'),
referenceVariable,
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/plugin/updateVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function updateVariables({
variablesToCreate.push(mapTokensToVariableInfo(token, theme, settings));
}
});
const variableObj = setValuesOnVariable(figma.variables.getLocalVariables().filter((v) => v.variableCollectionId === collection.id), variablesToCreate, collection, mode);
const variableObj = setValuesOnVariable(figma.variables.getLocalVariables().filter((v) => v.variableCollectionId === collection.id), variablesToCreate, collection, mode, settings);
return {
variableIds: variableObj.variableKeyMap,
referenceVariableCandidate: variableObj.referenceVariableCandidates,
Expand Down
7 changes: 7 additions & 0 deletions src/selectors/ignoreFirstPartForVariablesSelector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createSelector } from 'reselect';
import { settingsStateSelector } from './settingsStateSelector';

export const ignoreFirstPartForVariablesSelector = createSelector(
settingsStateSelector,
(state) => state.ignoreFirstPartForVariables,
);
Loading

0 comments on commit 28cd5d7

Please sign in to comment.