Commit 704a87ac authored by leadream's avatar leadream

my first plugin is born

parents
node_modules
{
"git.ignoreLimitWarning": true
}
\ No newline at end of file
# Figma Icon Automation Plugin
Figma Icon Automation is a plugin that helps you synchronize your icons' SVG code to a Github Repository, Then the Github Action will convert them to React code and update to NPM automatically.
\ No newline at end of file
export const base = 'https://api.github.com'
export const getContent = (filePath, githubData) => {
return fetch(`${base}/repos/${githubData.owner}/${githubData.name}/contents/${filePath}`, {
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
.then(res => (
res.sha ?
{sha: res.sha, contents: JSON.parse(window.atob(res.content))} :
{}
))
}
export const getCommit = (githubData) => {
return fetch(`${base}/repos/${githubData.owner}/${githubData.name}/commits/refs/heads/master`, {
headers: {
'content-type': 'application/json'
}
})
.then(response => response.json())
}
export const createBranch = (sha, githubData) => {
const branchName = `figma-update-${(new Date()).getTime()}`
const body = { ref: `refs/heads/${branchName}`, sha }
return fetch(`${base}/repos/${githubData.owner}/${githubData.name}/git/refs`, {
headers: {
'content-type': 'application/json',
'Authorization': `token ${githubData.githubToken}`
},
body: JSON.stringify(body),
method: 'POST'
})
.then(response => response.json())
}
export const updatePackage = (message, sha, contents, branch, githubData) => {
const content = window.btoa(JSON.stringify(contents, null, 2))
const body = JSON.stringify({ branch, sha, content, message })
return fetch(`${base}/repos/${githubData.owner}/${githubData.name}/contents/package.json`, {
headers: {
'content-type': 'application/json',
'Authorization': `token ${githubData.githubToken}`
},
body,
method: 'PUT'
})
.then(response => response.json())
}
export const createPullRequest = (title, content, branchName, githubData) => {
const body = {
title,
body: content,
head: `${githubData.owner}:${branchName}`,
base: "master"
}
return fetch(`${base}/repos/${githubData.owner}/${githubData.name}/pulls`, {
headers: {
'content-type': 'application/json',
'Authorization': `token ${githubData.githubToken}`
},
body: JSON.stringify(body),
method: 'POST'
})
.then(response => response.json())
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{
"name": "icon-automation",
"id": "739395588962138807",
"api": "1.0.0",
"main": "dist/code.js",
"ui": "dist/ui.html"
}
\ No newline at end of file
{
"name": "icon-automation",
"version": "1.0.0",
"description": "A figma plugin to push icon svg code to Github automatically.",
"main": "index.js",
"author": "Jun",
"license": "MIT",
"scripts": {
"dev": "npx webpack --mode=development --watch",
"build": "npx webpack --mode=production"
},
"dependencies": {
"css-loader": "^3.2.0",
"html-webpack-inline-source-plugin": "^0.0.10",
"html-webpack-plugin": "^3.2.0",
"parse-github-url": "^1.0.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"style-loader": "^1.0.0",
"ts-loader": "^6.0.4",
"typescript": "^3.5.3",
"url-loader": "^2.1.0",
"webpack": "^4.39.1",
"webpack-cli": "^3.3.6"
},
"devDependencies": {
"@types/react": "^16.9.0",
"@types/react-dom": "^16.8.5"
}
}
figma.showUI(__html__, { width: 320, height: 320 })
// get github settings
function getGithubSettings () {
return figma.clientStorage.getAsync('githubData')
}
// set github settings
function setGithubSettings (data) {
figma.clientStorage.setAsync('githubData', data)
}
// send github data to UI
function init () {
getGithubSettings()
.then(githubData => {
figma.ui.postMessage({ type: 'githubDataGot', githubData })
})
}
figma.ui.onmessage = msg => {
switch (msg.type) {
case 'setGithubData':
setGithubSettings(msg.githubData)
break
case 'cancel':
figma.closePlugin()
break
}
}
init()
import * as React from 'react'
import { validateGithubURL } from '../../utils/helper'
declare function require(path: string): any
export interface Props {
onGithubSet?: (data) => void;
githubData: {owner?: string, name?: string, githubToken?: string};
visible: boolean;
settingSwitch: boolean;
}
export default class Settings extends React.Component<Props> {
state = {
githubRepo: '',
githubToken: '',
warning: ''
}
handleChange = e => {
this.setState({[e.target.name]: e.target.value})
}
handleSubmit = e => {
const { onGithubSet } = this.props
const { githubRepo, githubToken } = this.state
const repo = validateGithubURL(githubRepo)
if (!repo) {
this.setState({warning: 'Github Repository is required.'})
} else if (!repo.owner || !repo.name) {
this.setState({warning: 'Github Repository URL is invalid.'})
} else if (!githubToken) {
this.setState({warning: 'Github Token is required.'})
} else {
const githubData = {
owner:repo.owner,
name: repo.name,
githubToken: `${githubToken}`
}
parent.postMessage({ pluginMessage: { type: 'setGithubData', githubData } }, '*')
onGithubSet(githubData)
}
}
componentDidUpdate (prevPorps) {
const { githubData, settingSwitch } = this.props
if ((!prevPorps.githubData && githubData) || (prevPorps.settingSwitch !== settingSwitch)) {
this.setState({
githubRepo: `https://github.com/${githubData.owner}/${githubData.name}`,
githubToken: githubData.githubToken,
})
}
}
render () {
const { visible } = this.props
const { githubRepo, githubToken, warning } = this.state
return (
<div className={!visible ? 'hide' : ''}>
<div className="onboarding-tip">
<div className="onboarding-tip__icon">
<div className="icon icon--smiley"></div>
</div>
<div className="onboarding-tip__msg">Hi, Welcome here. This plugin helps you convert icons to react component and publish to NPM. It should be used with Github and NPM. Please read the docs before using.<br/><br/><a href="https://github.com/leadream/figma-icon-automation" target="_blank">Docs here</a></div>
</div>
{
warning &&
<div className="form-item">
<div className="type type--pos-medium-normal alert alert-warning">{ warning }</div>
</div>
}
<div className="form-item">
<input
name="githubRepo"
className="input"
placeholder="Github Repository URL"
onChange={this.handleChange}
value={githubRepo}
/>
</div>
<div className="form-item">
<input
name="githubToken"
className="input"
placeholder="Github Token"
onChange={this.handleChange}
value={githubToken}
/>
</div>
<div className="form-item">
<button className='button button--primary button-block' onClick={this.handleSubmit}>Go</button>
</div>
<div className="setting-footer form-item type type--pos-medium-normal">
developed by <a href="https://github.com/leadream" target="_blank">Jun</a>
</div>
</div>
)
}
}
import * as React from 'react'
import { getContent, getCommit, updatePackage, createPullRequest, createBranch } from '../../api/github'
import { versionValue } from '../../utils/helper'
declare function require(path: string): any
export interface Props {
onSucceed: () => void;
githubData: {owner?: string, name?: string, githubToken?: string};
visible: boolean;
}
export default class Settings extends React.Component<Props> {
state = {
isPushing: false,
version: '',
message: '',
versionTip: '',
messageTip: '',
sha: '',
contents: { version: '0.0.0' },
currentVersion: '',
currentVersionTip: '',
resultTip: '',
prUrl: ''
}
getVersion = async (githubData) => {
const { contents, sha } = await getContent('package.json', githubData)
const currentVersion = contents.version
this.setState({
sha,
contents,
currentVersion,
currentVersionTip: `The current version is ${currentVersion}`
})
}
createBranch = async () => {
const { githubData } = this.props
const { sha } = await getCommit(githubData)
const { ref } = await createBranch(sha, githubData)
return { branchName: ref.replace('refs/heads/', '') }
}
changeVersion = async (branch) => {
const { githubData } = this.props
const { version, message, contents, sha } = this.state
contents.version = version
await updatePackage(message, sha, contents, branch, githubData)
}
createCommitAndPR = async (branchName) => {
const { githubData } = this.props
const { version, message } = this.state
return await createPullRequest(
`[figma]:update to ${version}`,
message,
branchName,
githubData
)
}
handleChange = e => {
const { name, value } = e.target
this.setState({[name]: value})
}
validate = (callback) => {
const { version, message, currentVersion } = this.state
// TODO: should validate async
// this.getVersion(this.props.githubData)
// .then(() => {
// const { currentVersion } = this.state
// currentVersion
// })
if (!version) {
this.setState({versionTip: 'Version is required.'})
return
} else if (!/^[0-9]\d?(\.(0|[1-9]\d?)){2}$/.test(version)) {
this.setState({versionTip: 'Version should be like 1.17.2.'})
return
} else if (versionValue(version) - versionValue(currentVersion) <= 0) {
this.setState({versionTip: 'Should be bigger than current version.'})
return
}
this.setState({
versionTip: ''
})
if (!message) {
this.setState({messageTip: 'Commit message is required.'})
return
}
this.setState({
messageTip: ''
})
callback && callback()
}
handleSubmit = () => {
this.validate(() => {
this.setState({isPushing: true})
this.createBranch()
.then(({branchName}) => {
this.changeVersion(branchName)
.then(() => {
this.createCommitAndPR(branchName)
.then(({html_url}) => {
this.props.onSucceed()
this.setState({
version: '',
message: '',
isPushing: false,
resultTip: 'Pushing successfully! You can now go to Github and merge this PR. Then your icons will be published to NPM automatically.',
prUrl: html_url
})
})
})
})
})
}
onCancel = () => {
parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*')
}
componentDidUpdate (prevProps) {
if (!prevProps.githubData && this.props.githubData) {
this.getVersion(this.props.githubData)
}
}
render () {
const { visible } = this.props
const { isPushing, version, message, versionTip, messageTip, currentVersionTip, resultTip, prUrl } = this.state
return (
<div className={'updator ' + (!visible ? 'hide' : '')}>
<div className="form-item">
{
!resultTip &&
<p className="type type--pos-medium-normal">Please fill the version and commit message below.</p>
}
{
(currentVersionTip && !resultTip) &&
<div className="type type--pos-medium-bold">{currentVersionTip}</div>
}
{
resultTip &&
<div className="type type--pos-medium-bold alert alert-success">
<h3>Congratulations!</h3>
{resultTip}
Click <a href={prUrl} target="_blank">here</a> to open the PR link.
</div>
}
</div>
<div className={'form-item '+(resultTip ? 'hide' : '')}>
<input
name="version"
className="input"
placeholder="The new version, such as 1.17.2"
onChange={this.handleChange}
value={version}
/>
{
versionTip &&
<div className="type type--pos-medium-normal help-tip">{versionTip}</div>
}
</div>
<div className={'form-item '+(resultTip ? 'hide' : '')}>
<textarea
rows={2}
name="message"
className="textarea"
placeholder="what has been changed?"
onChange={this.handleChange}
value={message}
/>
{
messageTip &&
<div className="type type--pos-medium-normal help-tip">{messageTip}</div>
}
</div>
<div className={'form-item '+(resultTip ? 'hide' : '')}>
<button
onClick={this.handleSubmit}
className='button button--primary'
style={{marginRight: '8px'}}
disabled={isPushing}
>{isPushing ? 'pushing…' : 'push to Github'}</button>
{
!isPushing &&
<button onClick={this.onCancel} className='button button--secondary'>close</button>
}
</div>
{
resultTip &&
<button onClick={this.onCancel} className='button button--secondary'>close</button>
}
</div>
)
}
}
\ No newline at end of file
/* these styles are just used for presentation of components */
body {
margin: 20px;
font-size: 20px;
font-family: Arial,x-locale-body,sans-serif;
}
h1, h2, h3 {
box-sizing: border-box;
margin-top: 12px;
margin-bottom: 12px;
font-family: sans-serif;
}
h1 {
font-size: 24px;
}
h2 {
font-size: 16px;
font-weight: 500;
}
h3 {
font-size: 14px;
font-weight: normal;
}
hr {
margin: 48px 0 48px 0;
border-top: 1px solid #cccccc;
border-bottom: none;
outline: none;
}
ul{
margin: 0;
padding-left: 16px;
}
textarea{
width: 100%!important;
margin: 0!important;
}
a{
color: #18a0fb
}
.container{
padding-top: 20px;
}
.bar-adjust{
display: flex;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 32px;
border-bottom: 1px solid #F3F3F3;
background: #FFF;
}
.adjust-item{
padding: 2px 10px;
line-height: 30px;
cursor: pointer;
color: #999;
border-bottom: 1px solid transparent;
}
.adjust-item.active{
color: #333;
border-color: #333;
}
.setting-footer{
padding: 10px 0;
text-align: center;
color: #999;
}
.form-item{
margin-bottom: 8px;
}
.form-item textarea{
margin-bottom: 8px!important;
}
.form-item .help-tip{
padding: 5px;
color: #e85007;
}
.form-item button:not(.button-block){
margin-right: 8px!important;
}
.button-block {
width: 100%;
}
.alert{
padding: 5px 10px;
border-radius: 2px;
color: #666;
background-color: #F8F8F8;
border: 1px solid #CCC;
}
.alert-warning {
color: #F90;
background-color: rgba(255, 153, 0, 0.05);
border: 1px solid rgba(255, 153, 0, 0.24);
}
.alert-success {
color: #09cf83;
background-color: #d8f9ec;
border: 1px solid rgb(197, 245, 226);
}
.hide {
display: none;
}
<div id="react-page"></div>
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import Settings from './components/Settings'
import Updator from './components/Updator'
import '../assets/ds.css'
import './style.css'
declare function require(path: string): any
class App extends React.Component {
state = {
updatorVisible: false,
githubData: null,
settingSwitch: false,
isDone: false
}
onSucceed = () => {
this.setState({ isDone: true })
}
toggleView = (githubData?) => {
const { updatorVisible } = this.state
this.setState({updatorVisible: !updatorVisible})
if (githubData===true) {
const { settingSwitch } = this.state
this.setState({settingSwitch: !settingSwitch})
} else if (githubData) {
this.setState({
githubData: githubData
})
}
}
componentDidMount () {
// 所有的消息接收集中在这里
window.onmessage = async (event) => {
const msg = event.data.pluginMessage
switch (msg.type) {
case 'githubDataGot':
if (msg.githubData) {
this.setState({
updatorVisible: true,
githubData: msg.githubData
})
}
break
}
}
}
render() {
const { updatorVisible, githubData, settingSwitch, isDone } = this.state
return (
<div className="container">
<div className={'bar-adjust '+ ((githubData&&!isDone) ? '' : 'hide')}>
<div
className={'adjust-item type type--pos-medium-bold '+(updatorVisible ? '' : 'active')}
onClick={e => this.toggleView()}
>
Setting
</div>
<div
className={'adjust-item type type--pos-medium-bold '+(updatorVisible ? 'active' : '')}
onClick={e => this.toggleView(true)}
>
Publish
</div>
</div>
<Settings
visible={!updatorVisible}
githubData={githubData}
onGithubSet={this.toggleView}
settingSwitch={settingSwitch}
/>
<Updator
onSucceed={this.onSucceed}
visible={updatorVisible}
githubData={githubData}/>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('react-page'))
{
"compilerOptions": {
"target": "es6",
"jsx": "react"
}
}
import gh from 'parse-github-url'
export const Uint8ArrayToString = fileData => {
var dataString = "";
for (var i = 0; i < fileData.length; i++) {
dataString += String.fromCharCode(fileData[i]);
}
return dataString
}
export const formattedSelections = selections => {
const iconsPromise = selections
.map(async c => {
let svgCode = await c.exportAsync({format: 'SVG'})
svgCode = Uint8ArrayToString(svgCode)
return {id: c.id, name: c.name, code: svgCode}
})
return Promise.all(iconsPromise)
}
export const validateGithubURL = url => {
return gh(url)
}
export const versionValue = (versions) => {
return versions
.split('.')
.map(n => n - 0)
.reduce((accumulator, currentValue, index) => {
return accumulator + currentValue*Math.pow(100, 2 - index)
}, 0)
}
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = (env, argv) => ({
mode: argv.mode === 'production' ? 'production' : 'development',
// This is necessary because Figma's 'eval' works differently than normal eval
devtool: argv.mode === 'production' ? false : 'inline-source-map',
entry: {
ui: './src/ui.tsx', // The entry point for your UI code
code: './src/code.ts', // The entry point for your plugin code
},
module: {
rules: [
// Converts TypeScript code to JavaScript
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
// Enables including CSS by doing "import './file.css'" in your TypeScript code
{ test: /\.css$/, loader: [{ loader: 'style-loader' }, { loader: 'css-loader' }] },
// Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI
{ test: /\.(png|jpg|gif|webp|svg)$/, loader: [{ loader: 'url-loader' }] },
],
},
// Webpack tries these extensions for you if you omit the extension like "import './file'"
resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js'] },
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'), // Compile into a folder called "dist"
},
// Tells Webpack to generate "ui.html" and to inline "ui.ts" into it
plugins: [
new HtmlWebpackPlugin({
template: './src/ui.html',
filename: 'ui.html',
inlineSource: '.(js)$',
chunks: ['ui'],
}),
new HtmlWebpackInlineSourcePlugin(),
],
})
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment