diff --git a/packages/icon-export-ui/src/app/api/githubFilesFetcher.ts b/packages/icon-export-ui/src/app/api/githubFilesFetcher.ts index 600b4a1..e50454b 100644 --- a/packages/icon-export-ui/src/app/api/githubFilesFetcher.ts +++ b/packages/icon-export-ui/src/app/api/githubFilesFetcher.ts @@ -87,7 +87,7 @@ export const updateCommit = async (octokit: Octokit, owner: string, repo: string }); export const createBranch = async (octokit: Octokit, owner: string, repo: string, branchName: string) => { - const { commitSha } = await getCurrentSha(octokit, owner, repo, 'master'); + const { commitSha } = await getCurrentSha(octokit, owner, repo, 'dev'); await octokit.rest.git.createRef({ owner, @@ -107,7 +107,7 @@ export const createPullRequest = async ( octokit.rest.pulls.create({ owner, repo, - base: 'refs/heads/master', + base: 'refs/heads/dev', head: `refs/heads/${branchName}`, title, }); @@ -131,6 +131,7 @@ export const getFilesSource = async ( owner, repo, path, + ref: 'dev', }); return result.data; diff --git a/packages/icon-export-ui/src/app/components/form/Form.tsx b/packages/icon-export-ui/src/app/components/form/Form.tsx index 46815c9..7ea6ff4 100644 --- a/packages/icon-export-ui/src/app/components/form/Form.tsx +++ b/packages/icon-export-ui/src/app/components/form/Form.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, FC, FormEvent, useCallback, useMemo, useState } from 'react'; +import React, { ChangeEvent, FC, FormEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { TextField, Select } from '@salutejs/plasma-web'; import type { FormPayload, IconPayload, SelectItem } from '../../../types'; @@ -29,6 +29,17 @@ interface FormProps { export const Form: FC = ({ onSubmit = () => {}, iconsMetaData }) => { const [state, setState] = useState({ ...defaultState, iconsMetaData }); + useEffect(() => { + if (!iconsMetaData.length) { + return; + } + + setState((prevState) => ({ + ...prevState, + iconsMetaData, + })); + }, [iconsMetaData]); + const onSubmitForm = useCallback( async (event: FormEvent) => { event.preventDefault(); diff --git a/packages/icon-export-ui/src/app/components/iconItem/IconItem.tsx b/packages/icon-export-ui/src/app/components/iconItem/IconItem.tsx index 81c5855..5a24a25 100644 --- a/packages/icon-export-ui/src/app/components/iconItem/IconItem.tsx +++ b/packages/icon-export-ui/src/app/components/iconItem/IconItem.tsx @@ -1,8 +1,8 @@ import React, { ChangeEvent, FC } from 'react'; -import { Select, TextField } from '@salutejs/plasma-web'; +import { TextField } from '@salutejs/plasma-web'; import { Input } from '../input/Input'; -import type { IconPayload, SelectItem } from '../../../types'; +import type { IconPayload } from '../../../types'; import { IconPreview } from '../iconPreview/IconPreview'; import { StyledFirstBlock, StyledIconItem, StyledSecondBlock } from './IconItem.style'; @@ -10,20 +10,20 @@ import { StyledFirstBlock, StyledIconItem, StyledSecondBlock } from './IconItem. interface IconItemProps { name: string; item: IconPayload; - category: string; - categories: SelectItem[]; onChangeInput: (event: ChangeEvent) => void; } -export const IconItem: FC = ({ name, item, category, categories, onChangeInput }) => ( - - - } /> - } /> - - - } /> - } /> - - -); +export const IconItem: FC = ({ name, item, onChangeInput }) => { + return ( + + + } /> + } /> + + + } /> + } /> + + + ); +}; diff --git a/packages/icon-export-ui/src/app/components/iconList/IconList.tsx b/packages/icon-export-ui/src/app/components/iconList/IconList.tsx index d087435..6dba2ba 100644 --- a/packages/icon-export-ui/src/app/components/iconList/IconList.tsx +++ b/packages/icon-export-ui/src/app/components/iconList/IconList.tsx @@ -1,27 +1,11 @@ -import React, { ChangeEvent, FC, useCallback, useState } from 'react'; +import React, { ChangeEvent, FC, useCallback, useEffect, useState } from 'react'; import { ParagraphText1 } from '@salutejs/plasma-web'; -import type { SelectItem, IconPayload } from '../../../types'; +import type { IconPayload } from '../../../types'; import { IconItem } from '../iconItem/IconItem'; import { StyledIconList, StyledIconListContainer } from './IconList.style'; -const categories: SelectItem[] = [ - { value: '--no-category--', label: '--no-category--' }, - { value: 'navigation', label: 'Navigation' }, - { value: 'universal', label: 'Universal' }, - { value: 'action', label: 'Action' }, - { value: 'alert', label: 'Alert' }, - { value: 'av', label: 'Av' }, - { value: 'connection', label: 'Connection' }, - { value: 'hardware', label: 'Hardware' }, - { value: 'communication', label: 'Communication' }, - { value: 'files', label: 'Files' }, - { value: 'map', label: 'Map' }, - { value: 'other', label: 'Other' }, - { value: 'logo', label: 'Logo' }, -]; - interface IconListProps { iconsMetaData: IconPayload[]; onChangeIconsName: (data: IconPayload[]) => void; @@ -31,12 +15,7 @@ interface IconListProps { * Список выбранных иконок. */ export const IconList: FC = ({ onChangeIconsName, iconsMetaData }) => { - const [state, setState] = useState( - iconsMetaData.reduce((acc: Record, item, i) => { - acc[`${item.name}${i}`] = item; - return acc; - }, {}), - ); + const [state, setState] = useState>(); const onChangeInput = useCallback( (event: ChangeEvent) => { @@ -44,12 +23,17 @@ export const IconList: FC = ({ onChangeIconsName, iconsMetaData } const iconKey = event.target.name; + if (!state) { + return; + } + const newState = { ...state, [iconKey]: { size: state[iconKey].size, svg: state[iconKey].svg, name: event.target.value, + category: state[iconKey].category, }, }; @@ -60,20 +44,31 @@ export const IconList: FC = ({ onChangeIconsName, iconsMetaData } [state, onChangeIconsName], ); + useEffect(() => { + if (!iconsMetaData.length) { + return; + } + + const fromData = iconsMetaData.reduce((acc: Record, item, i) => { + acc[`${item.name}${i}`] = item; + return acc; + }, {}); + setState(fromData); + }, [iconsMetaData]); + return ( Icon list: {iconsMetaData.length} - {iconsMetaData.map(({ name }, i) => ( - - ))} + {state && + iconsMetaData.map(({ name }, i) => ( + + ))} ); diff --git a/packages/icon-export-ui/src/app/components/utils.ts b/packages/icon-export-ui/src/app/components/utils.ts index 674b735..c380cbd 100644 --- a/packages/icon-export-ui/src/app/components/utils.ts +++ b/packages/icon-export-ui/src/app/components/utils.ts @@ -3,7 +3,7 @@ import prettier from 'prettier/standalone'; import type { Options } from 'prettier'; import { FilesPayloadResponse, IconComponents, IconPayload } from '../../types'; -import { getIconAsset, getIconComponent, getIconSource, getIndexSource } from '../../source'; +import { getIconAsset, getIconComponent, getIconSource, getIndexSource, getIconCategories } from '../../source'; import { getFilesSource } from '../api/githubFilesFetcher'; const prettierSetting: Options = { @@ -24,6 +24,7 @@ export const prettify = (source: string) => prettier.format(source, prettierSett export const getFilesPath = (iconName?: string, iconSize?: number) => ({ iconSourceExport: 'packages/plasma-icons/src/scalable/index.ts', + iconSourceComponent: 'packages/plasma-icons/src/scalable/Icon.tsx', iconSourceImport16: 'packages/plasma-icons/src/scalable/Icon.assets.16/index.ts', iconSourceImport24: 'packages/plasma-icons/src/scalable/Icon.assets.24/index.ts', iconSourceImport36: 'packages/plasma-icons/src/scalable/Icon.assets.36/index.ts', @@ -32,9 +33,9 @@ export const getFilesPath = (iconName?: string, iconSize?: number) => ({ }); export const getFilesPayload = (iconsMetaData: IconPayload[], ...args: string[]): FilesPayloadResponse => { - let [iconSourceExport, iconSourceImport16, iconSourceImport24, iconSourceImport36] = args; + let [iconSourceExport, iconSourceComponent, iconSourceImport16, iconSourceImport24, iconSourceImport36] = args; - const iconsComponents = iconsMetaData.map(({ size, name = 'IconName', svg }) => { + const iconsComponents = iconsMetaData.map(({ size, name = 'IconName', category, svg }) => { iconSourceExport = getIndexSource(iconSourceExport, name); if (size === 16) { @@ -47,6 +48,8 @@ export const getFilesPayload = (iconsMetaData: IconPayload[], ...args: string[]) iconSourceImport36 = getIconSource(iconSourceImport36, name); } + iconSourceComponent = getIconCategories(iconSourceComponent, name, size, category); + return { iconSize: size, iconName: name, @@ -58,6 +61,7 @@ export const getFilesPayload = (iconsMetaData: IconPayload[], ...args: string[]) return { iconsComponents, iconSourceExport, + iconSourceComponent, iconSourceImport16, iconSourceImport24, iconSourceImport36, @@ -70,6 +74,7 @@ export const getGitHubData = async (token?: string, owner = 'salute-developers', repo, [ getFilesPath().iconSourceExport, + getFilesPath().iconSourceComponent, getFilesPath().iconSourceImport16, getFilesPath().iconSourceImport24, getFilesPath().iconSourceImport36, @@ -87,6 +92,7 @@ export const getFlatIconFiles = (iconsComponents: IconComponents[]) => export const getFilesTree = ({ iconSourceExport, + iconSourceComponent, iconSourceImport16, iconSourceImport24, iconSourceImport36, @@ -96,6 +102,7 @@ export const getFilesTree = ({ return { [getFilesPath().iconSourceExport]: iconSourceExport, + [getFilesPath().iconSourceComponent]: iconSourceComponent, [getFilesPath().iconSourceImport16]: iconSourceImport16, [getFilesPath().iconSourceImport24]: iconSourceImport24, [getFilesPath().iconSourceImport36]: iconSourceImport36, diff --git a/packages/icon-export-ui/src/app/hooks/useRunGithubPRProcess.ts b/packages/icon-export-ui/src/app/hooks/useRunGithubPRProcess.ts index bb85b60..8f30d48 100644 --- a/packages/icon-export-ui/src/app/hooks/useRunGithubPRProcess.ts +++ b/packages/icon-export-ui/src/app/hooks/useRunGithubPRProcess.ts @@ -24,9 +24,11 @@ interface CreatPR { token?: string; } -const saveMetaData = (octokit: Octokit, owner: string, repo: string) => (fn: (...args: any[]) => Promise) => ( - ...args: any[] -) => fn(octokit, owner, repo, ...args); +const saveMetaData = + (octokit: Octokit, owner: string, repo: string) => + (fn: (...args: any[]) => Promise) => + (...args: any[]) => + fn(octokit, owner, repo, ...args); /** * Хук для запуска процесса создания пул реквеста в GitHub. Возвращает: @@ -44,7 +46,7 @@ export const useRunGithubPRProcess = ({ owner, repo, branchName }: RunProcessGit const withMetaData = saveMetaData(octokit, owner, repo); - if (branchName !== 'master') { + if (branchName !== 'master' && branchName !== 'dev') { setStep(0); await withMetaData(createBranch)(branchName); } @@ -66,7 +68,7 @@ export const useRunGithubPRProcess = ({ owner, repo, branchName }: RunProcessGit await withMetaData(updateCommit)(branchName, newCommitSha); let pullRequest; - if (branchName !== 'master') { + if (branchName !== 'master' && branchName !== 'dev') { setStep(6); pullRequest = await withMetaData(createPullRequest)(branchName, prTitle); } diff --git a/packages/icon-export-ui/src/plugin/main.ts b/packages/icon-export-ui/src/plugin/main.ts index a137384..8e2d259 100644 --- a/packages/icon-export-ui/src/plugin/main.ts +++ b/packages/icon-export-ui/src/plugin/main.ts @@ -10,34 +10,69 @@ const defaultSetting = { width: 700, }; +/** + * Получить имя, размер и категорию из полного имени + * Example: + * 24 / Operation / 24_ShareScreenOutline - новый формат + * Player / ic_36_pause_outline - старый формат + */ const getNormalizedName = (name: string) => { - const secondPart = camelize(name).split('/')[1] || camelize(name); - const withoutPrefix = secondPart - .trim() - .replace(/^(\s*)[a-zA-Z_]+(\d\d)/g, '') - .replace(/\s/g, ''); - return upperFirstLetter(withoutPrefix); + const trimmedName = name.replace(/\s/g, ''); + // в новом формате + const [size, category, nameWithSize] = trimmedName.split('/'); + + if (!size || !category || !nameWithSize) { + const last = camelize(trimmedName).split('/').slice(-1)[0]; + const withoutPrefix = last + .trim() + .replace(/^(\s*)[a-zA-Z_]+(\d\d)/g, '') // убирает все символы и размер перед названием: ic36pauseOutline -> pauseOutline + .replace(/\s/g, ''); + return upperFirstLetter(withoutPrefix); + } + + return { + size, + category, + name: upperFirstLetter(nameWithSize.split('_').slice(-1)[0]), // убирает размер перед названием + }; +}; + +const getNames = async (node: ComponentNode | InstanceNode) => { + const { width, name: nodeName } = node; + + const normalizedName = getNormalizedName(nodeName); + const svg = await getExportSvg(node); + + const isString = typeof normalizedName === 'string'; + + return { + size: isString ? Math.round(width) : Number(normalizedName.size), + category: isString ? 'Other' : normalizedName.category, + name: isString ? normalizedName : normalizedName.name, + svg, + }; }; const sendMetaDataInfo = async (selections: readonly SceneNode[]) => { const iconsMetaData = await Promise.all( selections.map(async (selection) => { - const { width, name } = selection; + const { type } = selection; + + if (type !== 'FRAME') { + return []; + } - const normalizedName = getNormalizedName(name); - const svg = await getExportSvg(selection); + const nodes = (selection as FrameNode).findAllWithCriteria({ + types: ['COMPONENT', 'INSTANCE'], + }); - return { - size: Math.round(width), - name: normalizedName, - svg, - }; + return await Promise.all(nodes.map(getNames)); }), ); const payload: UIMessage = { type: 'update-icon-data', - payload: iconsMetaData, + payload: iconsMetaData.flat(), }; figma.ui.postMessage(payload); }; diff --git a/packages/icon-export-ui/src/source/iconCategories.ts b/packages/icon-export-ui/src/source/iconCategories.ts new file mode 100644 index 0000000..93491b6 --- /dev/null +++ b/packages/icon-export-ui/src/source/iconCategories.ts @@ -0,0 +1,60 @@ +import { insertString, lowerFirstLetter } from '../utils'; + +const EXPORT_ICON_SET_OBJECT_LINE = 'interface Props'; +const CATEGORIES_OBJECT_LINE = 'export const iconSectionsSet = {'; + +const getStartIndex = (source: string, text: string) => source.search(text); + +const getEndIndex = (source: string, index: number) => source.substring(index).search('};'); + +const getCategoryEndIndex = (source: string, index: number) => source.substring(index).search(' },'); + +const addToIconCategory = (source: string, index: number, iconName: string) => + insertString(source, index, ` ${lowerFirstLetter(iconName)}: ${iconName},\n`); + +const addToCategories = (source: string, start: number, iconName: string, category: string) => { + let newSource = source; + let end = start + getEndIndex(newSource, start); + let categories = source.substring(start, end); + let categoryStart = getStartIndex(categories, ` ${category}: {`); + + if (categoryStart === -1) { + newSource = createIconCategory(source, end, category); + end = start + getEndIndex(newSource, start); + categories = newSource.substring(start, end); + categoryStart = getStartIndex(categories, ` ${category}: {`); + } + const categoryEnd = getCategoryEndIndex(categories, categoryStart); + + return addToIconCategory(newSource, start + categoryStart + categoryEnd, iconName); +}; + +const createIconCategories = (source: string, index: number) => + insertString(source, index, `\n\n${CATEGORIES_OBJECT_LINE}\n};\n`); + +const createIconCategory = (source: string, index: number, category: string) => + insertString(source, index, ` ${category}: {\n },\n`); + +const getIconImport = (iconName: string, size: number) => + `import { ${iconName} } from './Icon.assets.${size}/${iconName}';\n`; + +/** + * Функция модификации файла `/Icon.tsx`. Здесь вставляется сгенерированный импорт иконки, + * и добавляется её компонент в список иконок по категориям + */ +export default (source: string, iconName: string, size: number, category: string) => { + if (source.includes(`{ ${iconName} }`)) { + return source; + } + + const index = source.search(EXPORT_ICON_SET_OBJECT_LINE) - 1; + let newSource = insertString(source, index, getIconImport(iconName, size)); + + let startIndexCategories = getStartIndex(newSource, CATEGORIES_OBJECT_LINE); + if (startIndexCategories === -1) { + newSource = createIconCategories(newSource, newSource.lastIndexOf('\n')); + startIndexCategories = getStartIndex(newSource, CATEGORIES_OBJECT_LINE); + } + + return addToCategories(newSource, startIndexCategories, iconName, category); +}; diff --git a/packages/icon-export-ui/src/source/iconComponent.ts b/packages/icon-export-ui/src/source/iconComponent.ts index 08c9d68..f611d3f 100644 --- a/packages/icon-export-ui/src/source/iconComponent.ts +++ b/packages/icon-export-ui/src/source/iconComponent.ts @@ -9,6 +9,6 @@ export default (name: string) => import { IconRoot, IconProps } from '../IconRoot'; export const Icon${name}: React.FC = ({ size = 's', color, className }) => { - return ; + return ; }; `; diff --git a/packages/icon-export-ui/src/source/iconSource.ts b/packages/icon-export-ui/src/source/iconSource.ts index b1129c3..3da29fe 100644 --- a/packages/icon-export-ui/src/source/iconSource.ts +++ b/packages/icon-export-ui/src/source/iconSource.ts @@ -12,7 +12,7 @@ const addToIconSet = (source: string, index: number, iconName: string) => const getIconImport = (iconName: string) => `import { ${iconName} } from './${iconName}';\n`; /** - * Функция модификации файла `/icon.tsx`. Здесь вставляется сгенерированный импорт иконки, + * Функция модификации файла `/icon.ts`. Здесь вставляется сгенерированный импорт иконки, * и добавляется её компонент в общий список иконок */ export default (source: string, iconName: string) => { diff --git a/packages/icon-export-ui/src/source/index.ts b/packages/icon-export-ui/src/source/index.ts index f09f0bf..c273343 100644 --- a/packages/icon-export-ui/src/source/index.ts +++ b/packages/icon-export-ui/src/source/index.ts @@ -2,3 +2,4 @@ export { default as getIconSource } from './iconSource'; export { default as getIndexSource } from './indexSource'; export { default as getIconAsset } from './iconAsset'; export { default as getIconComponent } from './iconComponent'; +export { default as getIconCategories } from './iconCategories'; diff --git a/packages/icon-export-ui/src/types.ts b/packages/icon-export-ui/src/types.ts index 7d66ea1..9d62364 100644 --- a/packages/icon-export-ui/src/types.ts +++ b/packages/icon-export-ui/src/types.ts @@ -14,6 +14,7 @@ export interface IconPayload { size: number; svg: string; name: string; + category: string; } export interface TokenPayloadRequest { @@ -31,6 +32,7 @@ export interface IconComponents { export interface FilesPayloadResponse { iconSourceExport: string; + iconSourceComponent: string; iconSourceImport16: string; iconSourceImport24: string; iconSourceImport36: string;